接上一篇,这次我们从sbi_ipi_init
接着看
该函数为实现核间软中断做初始化。核间软中断的硬件支持来自clint
设备,把cilnt
设备的相应比特位置1,即可触发对应hart的软中断。但是触发软中断的hart后续应该干嘛?这就需要额外的辅助信息了。
opensbi的处理方法大致如下:有一个全局的ipi_event队列,队列中每个元素指向一个sbi_ipi_event_ops
,这个结构体指示了发送ipi的额外动作(update
, sync
成员),以及收到ipi后如何处理(process
成员)。v0.8版的opensbi共有三个预定义的sbi_ipi_event_ops
:
ipi_smode_ops
:用于实现sbi_send_ipi
这个sbi调用ipi_halt_ops
:用于实现sbi_shutdown
这个sbi调用tlb_ops
:用于实现remote fence
系列拓展sbi_ipi_event_create
负责在全局的ipi_event队列中找到一个空指针,修改使其指向需要注册的sbi_ipi_event_ops
,返回该指针在队列中的位置。在sbi_init
中,冷启动的cpu为每个hart分配了一个ipi_data
结构体,在发送软中断前设置ipi_data
相应bit位为1,通过置1的位置在全局的ipi_event队列中找到相应的sbi_ipi_event_ops
,调用其process
函数来处理该软中断(具体的逻辑可以从看代码sbi_ipi_send
,sbi_ipi_process
函数)。
/** IPI event operations or callbacks */
struct sbi_ipi_event_ops {
/** Name of the IPI event operations */
char name[32];
/**
* Update callback to save/enqueue data for remote HART
* Note: This is an optional callback and it is called just before
* triggering IPI to remote HART.
*/
int (* update)(struct sbi_scratch *scratch,
struct sbi_scratch *remote_scratch,
u32 remote_hartid, void *data);
/**
* Sync callback to wait for remote HART
* Note: This is an optional callback and it is called just after
* triggering IPI to remote HART.
*/
void (* sync)(struct sbi_scratch *scratch);
/**
* Process callback to handle IPI event
* Note: This is a mandatory callback and it is called on the
* remote HART after IPI is triggered.
*/
void (* process)(struct sbi_scratch *scratch);
};
static const struct sbi_ipi_event_ops *ipi_ops_array[SBI_IPI_EVENT_MAX];
// 全局的ipi_event队列
struct sbi_ipi_data {
unsigned long ipi_type;
};
int sbi_ipi_init(struct sbi_scratch *scratch, bool cold_boot)
{
// 略去部分无关紧要的代码
int ret;
struct sbi_ipi_data *ipi_data;
if (cold_boot) {
ipi_data_off = sbi_scratch_alloc_offset(sizeof(*ipi_data),
"IPI_DATA");
ret = sbi_ipi_event_create(&ipi_smode_ops);
ipi_smode_event = ret;
ret = sbi_ipi_event_create(&ipi_halt_ops);
ipi_halt_event = ret;
}
ipi_data = sbi_scratch_offset_ptr(scratch, ipi_data_off);
ipi_data->ipi_type = 0x00;
/* Platform init */
ret = sbi_platform_ipi_init(sbi_platform_ptr(scratch), cold_boot);
/* Enable software interrupts */
csr_set(CSR_MIE, MIP_MSIP);
// 因为clint的提供的核间中断是M态的软中断,所以需要开启MIP_MSIP位
return 0;
}
sbi_platform_ipi_init
函数则与之前是一样的逻辑,最终会选择到fdt_ipi_client
这个driver。因为clint
设备实际提供了软中断和时钟中断,这里只初始化了软中断部分相关的数据结构。关键的操作只有一步,就是记录clint
设备的物理地址,这样就知道该把哪一位置1来触发核间中断。
该初始化函数也算是核间中断实现的一部分,主要负责前面谈到的remote fence
。
int sbi_tlb_init(struct sbi_scratch *scratch, bool cold_boot)
{
// 略去部分无关紧要的代码
if (cold_boot) {
tlb_sync_off = sbi_scratch_alloc_offset(sizeof(*tlb_sync),
"IPI_TLB_SYNC");
tlb_fifo_off = sbi_scratch_alloc_offset(sizeof(*tlb_q),
"IPI_TLB_FIFO");
tlb_fifo_mem_off = sbi_scratch_alloc_offset(
SBI_TLB_FIFO_NUM_ENTRIES * SBI_TLB_INFO_SIZE,
"IPI_TLB_FIFO_MEM");
ret = sbi_ipi_event_create(&tlb_ops);
// 这里注册了之前谈到的tlb_ops
tlb_event = ret;
tlb_range_flush_limit = sbi_platform_tlbr_flush_limit(plat);
} else {
if (!tlb_sync_off ||
!tlb_fifo_off ||
!tlb_fifo_mem_off)
return SBI_ENOMEM;
if (SBI_IPI_EVENT_MAX <= tlb_event)
return SBI_ENOSPC;
}
tlb_sync = sbi_scratch_offset_ptr(scratch, tlb_sync_off);
tlb_q = sbi_scratch_offset_ptr(scratch, tlb_fifo_off);
tlb_mem = sbi_scratch_offset_ptr(scratch, tlb_fifo_mem_off);
*tlb_sync = 0;
sbi_fifo_init(tlb_q, tlb_mem,
SBI_TLB_FIFO_NUM_ENTRIES, SBI_TLB_INFO_SIZE);
return 0;
}
执行冷启动的cpu为每个hart分配了三块变量区域tlb_sync
, tlb_fifo
, tlb_fifo_mem
,这些变量都是为实现remote fence
准备的。
struct sbi_fifo { // 对应变量 tlb_fifo
void *queue; // 指向 tlb_fifo_mem
spinlock_t qlock;
u16 entry_size;
u16 num_entries;
u16 avail;
u16 tail;
};
// 一个环形buffer,每个buffer元素是一个sbi_tlb_info,
// sbi_tlb_info记录了remote fence的类型
struct sbi_tlb_info {
unsigned long start;
unsigned long size;
unsigned long asid;
unsigned long vmid;
unsigned long type;
struct sbi_hartmask smask; // 等待sync的hart的掩码
};
opensbi对remote fence
的sbi调用的实现都是同步的,意味着调用返回后所有被请求的hart的同步都已被完成(我个人感觉这个东西也没办法做成异步?)
基本的流程如下:
remote fence
调用,陷入opensbi的sbi_ipi_send_many
函数,sbi_ipi_send_many
依次调用sbi_ipi_send
,每次传入需要同步的hartid
sbi_ipi_send
函数根据注册的ipi_event
拿到tlb_ops
,然后调用其update
函数, 再发送核间软中断,最后回调tlb_ops
的sync
函数(这与上一节描述的机制一致)。tlb_ops
的update
就是向相应的hart的fifo buffer
塞入一个sbi_tlb_info
,指示具体的同步方式(虽然opensbi实际上会进行一些优化,在v0.8这个版本中,在塞入sbi_tlb_info
前,会遍历该buffer
查看是否一些sbi_tlb_info
可以进行合并)tlb_ops
的sync
函数负责等待同步完成:在需要同步的hart被触发软中断后,检查自己的ipi_data
,据此从全局的ipi_event
队列中拿到tlb_ops
,调用它的process
回调函数,process
函数则负责根据队列中的sbi_tlb_info
进行同步操作,同步完成后,查看smask
成员,将smask
中比特位为1对应的hart的tlb_sync
变量置1,告知这些hart自己已经完成了同步(smask对应位为1表示相应的hart在tlb_ops
的sync
中等待该hart同步完成的消息)。tlb_sync
变量被置1后,等待的hart从sync
函数返回,针对一个hart的remote fence
结束,当sbi_ipi_send_many
对所有需要同步的hart调用的sbi_ipi_send
返回后(这里是串行的,一个sbi_ipi_send
返回后再调用下一个),整个remote fence
结束。int sbi_timer_init(struct sbi_scratch *scratch, bool cold_boot)
{
// 略去部分无关紧要的代码
u64 *time_delta;
if (cold_boot) {
time_delta_off = sbi_scratch_alloc_offset(sizeof(*time_delta),
"TIME_DELTA");
}
time_delta = sbi_scratch_offset_ptr(scratch, time_delta_off);
*time_delta = 0;
ret = sbi_platform_timer_init(plat, cold_boot);
return 0;
}
sbi_platform_timer_init
和之前的sbi_platform_ipi_init
函数如出一辙,负责初始化clint
设备的时钟中断部分。
在clint_warm_timer_init
函数的最后,设置了mtimecmp
为-1,此时禁用了时钟中断,当S态软件调用sbi_set_timer
时,时钟中断开启。
int clint_warm_timer_init(void){
// ....
clint->time_wr(-1ULL,
&clint->time_cmp[target_hart - clint->first_hartid]);
}
struct sbi_ecall_extension {
struct sbi_dlist head;
unsigned long extid_start; // 这俩变量表示该sbi extension对应的
unsigned long extid_end; // extension ID范围
int (* probe)(unsigned long extid, unsigned long *out_val);
int (* handle)(unsigned long extid, unsigned long funcid,
unsigned long *args, unsigned long *out_val,
struct sbi_trap_info *out_trap);
};
int sbi_ecall_init(void)
{
int ret;
/* The order of below registrations is performance optimized */
ret = sbi_ecall_register_extension(&ecall_time);
ret = sbi_ecall_register_extension(&ecall_rfence);
ret = sbi_ecall_register_extension(&ecall_ipi);
ret = sbi_ecall_register_extension(&ecall_base);
ret = sbi_ecall_register_extension(&ecall_hsm);
ret = sbi_ecall_register_extension(&ecall_legacy);
ret = sbi_ecall_register_extension(&ecall_vendor);
return 0;
}
sbi_ecall_init
负责组织一个双链表,链表中每个元素都是一个sbi_ecall_extension
结构体,对应一个riscv sbi extension
。比如前面提到的remote fence
调用,对应的就是这里的ecall_rfence
。在S态软件调用ecall
陷入opensbi时,opensbi就会根据a7寄存器保存的extension id
,在这个双链表中查找,来确定S态软件希望调用哪个extension。
该函数负责做一些最后的首尾工作(主要是对设备树的内容进行一些调整),在generic
平台下,会调用generic_final_init
函数。
static int generic_final_init(bool cold_boot)
{
void *fdt;
int rc;
if (generic_plat && generic_plat->final_init) {
rc = generic_plat->final_init(cold_boot, generic_plat_match);
if (rc)
return rc;
}
if (!cold_boot)
return 0;
fdt = sbi_scratch_thishart_arg1_ptr();
fdt_cpu_fixup(fdt);
fdt_fixups(fdt);
if (generic_plat && generic_plat->fdt_fixup) {
rc = generic_plat->fdt_fixup(fdt, generic_plat_match);
if (rc)
return rc;
}
return 0;
}
可以看到,如果之前没有被fw_platform_lookup_special
截获,该函数就只会调用fdt_cpu_fixup
和fdt_fixups
这两个函数,而对于暖启动的hart该函数则相当于空操作。
fdt_cpu_fixup
就是之前谈到的把opensbi无法启动的cpu(可能的情况有hartid
大于128,该cpu在设备树中没有mmu-type
这个property等等)在设备树中的状态修改为disable
。
而在fdt_fixups
中又调用了fdt_plic_fixup
和fdt_reserved_memory_fixup
这两个函数。
void fdt_fixups(void *fdt)
{
fdt_plic_fixup(fdt, "riscv,plic0");
fdt_reserved_memory_fixup(fdt);
}
前者负责把plic在设备中的interrupt-extended
这个property中对应M态的外部中断的interrupt specifier
修改为-1,在plic device tree binding中,-1表示禁用该context
(因为S态软件没办法使用M态的中断)。而后者负责在设备树中增加一个/reserved-memory
子节点,reserved-memory binding指示了一些物理内存区域需要OS特殊对待。在generic
平台下,该函数在/reserved-memory
下新增了一个子节点,指示OS不要使用opensbi所占用的这段物理内存。
最后吐槽一下opensbi是如何修改设备树的。考虑到设备树在内存中是展平的,其实要修改是很麻烦的。opensbi采取了非常简单粗暴的做法。其中一个重要的辅助函数是fdt_open_into
和fdt_splice_
。
fdt_open_into
函数负责拓展strings block
后面的free space
的大小(参考dtb memory structure
),实际干的事情就是修改fdt_header
中的totalsize
。而fdt_splice_
函数负责平移设备树的内容。比如希望给某个node
增加一个property
,就把这个node后面的设备树内容向后平移相应的字节数,然后把property
插入到多出的空隙中。每次对设备树的修改就需要把设备树的内容整体进行平移,这样做的效率是相当低的。
最后剩下的代码如下,wake_coldboot_harts
负责把coldboot_done
置1,这样暖启动的cpu从wait_for_coldboot
函数中退出,陷入到sbi_hsm_hart_wait
的循环等待中
static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid)
{
// ....
wake_coldboot_harts(scratch, hartid);
init_count = sbi_scratch_offset_ptr(scratch, init_count_offset);
(*init_count)++;
sbi_hsm_prepare_next_jump(scratch, hartid);
sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
scratch->next_mode, FALSE);
}
最后,冷启动的cpu在sbi_hart_switch_mode
中调用mret
,控制权传递给下一级bootloader。a0
寄存器存放冷启动cpu的hartid
,a1
存放设备树的地址。
值得注意的是,在冷启动的cpu把控制权传递给bootloader时,此时暖启动的cpu仍处于sbi_hsm_hart_wait
的循环等待中。依赖于bootloader或者OS在合适的时候调用sbi_hart_start
来启动这些cpu。
整个firmware的工作可以总结为初始化各类M态的寄存器和自己相关的数据结构,为后面服务S态软件的各种请求做好准备。
可以发现,目前riscv体系结构还是有很多问题的。从opensbi的实现中可以看出至少两点:
mip
寄存器把时钟中断转发给S态。随后S态的软件又需要通过sbi_set_timer
调用再次陷入opensbi,设置下次时钟中断的时间。不过前不久提出了riscv sstc extension
,希望能够增加stimecmp
寄存器来减少时钟中断的开销,貌似目前这个还没有加入到spec中。remote fence
机制:这个实现的开销实在太大了,对于像JVM这样的需要在运行时修改指令的程序,remote fence
又是必需的,目前还不清楚是否有更好的解决方案。