QEMU-RISCV平台opensbi代码分析(2)

调用sbi_init(struct sbi_scratch *scratch)函数,进入sbi初始化。其中sbi_init的初始化流程如下图所示:

从上图大概可知opensbi的hart分为两种启动方式,第一种为冷启动,第二种为热启动。

在smp多核架构下,如果设备树没有指定冷启动的hart编号,则采用乐透方式来指定哪个hart进行冷启动(即通过原子操作,读取设置coldboot_lottery值;读取值为0时,表明是第一个操作此原子变量的hart,则其进入coldboot模式)。

一、hart冷启动流程分析

进入init_coldboot(struct sbi_scratch *scratch, u32 hartid)函数,进行冷启动流程。其主要实现了全套初始化:scratch/堆/域/HSM/平台/控制台/PMU/IRQ/IPI/TLB/定时器/ECALL 等,最后配置 PMP 并唤醒其它 warmboot HART,完成 HSM 的启动终态。

1.1、sbi_scratch_init

sbi_scratch_init主要功能代码如下所示:

cpp 复制代码
sbi_for_each_hartindex(i) {
    h = (plat->hart_index2id) ? plat->hart_index2id[i] : i;
    hartindex_to_hartid_table[i] = h;
    hartindex_to_scratch_table[i] =
        ((hartid2scratch)scratch->hartid_to_scratch)(h, i);
}

主要目的是初始化**struct sbi_scratch *hartindex_to_scratch_table[SBI_HARTMASK_MAX_BITS]**全局指针数组,此指针数组的每一个成员都指向了每个hart对应的sbi_scratch临时存储区结构体基地址,使用hart编号来进行数组索引。

后续一些scratch内存分配操作函数(如sbi_scratch_alloc_offset等) 都是基于hartindex_to_scratch_table数组来实现的,其实现原理下面将通过代码与空间分布图说明。

sbi_scratch_alloc_offset函数主要功能实现部分代码:

cpp 复制代码
// ret为scratch临时存储区基地址上的偏移量(在sbi_scratch结构体之上)
if (ret) {
    sbi_for_each_hartindex(i) {
        rscratch = sbi_hartindex_to_scratch(i);
        if (!rscratch)
            continue;
        // 循环查找每一个hart,并返回scratch临时存储区上偏移后的绝对地址
        ptr = sbi_scratch_offset_ptr(rscratch, ret);
        // 对每一个hart分配的内存空间进行清零初始化
        sbi_memset(ptr, 0, size);
    }
}
// 最后此函数返回的是ret,即基于scratch临时存储区基地址上的偏移量,而不是偏移后的绝对地址

下图为sbi_scratch_alloc_offset函数分配时使用的内存空间位置,分配出来的内存位于"可扩展分配空间"处:

1.2、sbi_heap_init

此函数主要调用sbi_heap_init_new 函数来进行堆内存的初始化操作,进行了struct sbi_heap_control global_hpctrl结构体全局变量的初始化。它为堆内存分配接口提供了分配基础,将堆内存加入了opensbi的堆内存分配器中。

堆内存分配器分配的内存地址处于上图中的"HEAP"区域,同时提供了sbi_malloc、sbi_free等接口用于堆内存的分配与释放。

cpp 复制代码
struct heap_node {
	struct sbi_dlist head;
	unsigned long addr;
	unsigned long size;
};

struct sbi_heap_control {
	spinlock_t lock;
	unsigned long base;
	unsigned long size;
	unsigned long resv;
	struct sbi_dlist free_node_list;
	struct sbi_dlist free_space_list;
	struct sbi_dlist used_space_list;
	struct heap_node init_free_space_node;
};

struct sbi_heap_control global_hpctrl;

opensbi的堆内存分配器实现原理大致为:

  1. 初始化全局global_hpctrl 结构体中的3个双向链表头,free_node_listfree_space_listused_space_list。同时将初始的"HEAP"区域抽象为一个heap_node节点加入到free_space_list链表中;
  2. sbi_malloc接口,其原理为首先在堆区顶部 分配heap_node节点空间(heap_node节点一旦被分配就不会被释放),然后将heap_node节点加入到free_node_list链表中。接着在free_space_list链表中的内存节点找到适合的空间后(堆区底部寻找),再在free_node_list链表中取一个heap_node节点将其调整为分配完内存后的空闲内存节点,再将其从free_node_list链表中删除,并插入到free_space_list链表中。最后再将分配好的内存节点插入到used_space_list链表中;
  3. sbi_free接口,其原理为查询释放的地址是否为used_space_list链表中内存节点内的有效地址,如果有效则将此节点从used_space_list链表中删除。然后再判断此内存是否与free_space_list链表中内存节点相邻,如果相邻,则将此节点中的内存加入到相邻的free内存节点,并接着释放此节点到free_node_list链表中;如果不相邻,则将此内存节点加入到free_space_list链表中。

1.3、sbi_domain_init

主要进行了root domain的初始化与注册,主要代码与分析如下:

cpp 复制代码
struct sbi_domain root = {
	.name = "root",
	.possible_harts = NULL,
	.regions = NULL,
	.system_reset_allowed = true,
	.system_suspend_allowed = true,
	.fw_region_inited = false,
};

int sbi_domain_init(struct sbi_scratch *scratch, u32 cold_hartid)
{
	int rc;
	struct sbi_hartmask *root_hmask;
	struct sbi_domain_memregion *root_memregs;
	int root_memregs_count = 0;

    // 初始化domain_list链表头
	SBI_INIT_LIST_HEAD(&domain_list);

    // 分配scratch临时存储区的内存,用于指向root domain
	domain_hart_ptr_offset = sbi_scratch_alloc_type_offset(void *);
	if (!domain_hart_ptr_offset)
		return SBI_ENOMEM;

    // 为root domain分配root mem内存信息结构体
	root_memregs = sbi_calloc(sizeof(*root_memregs), ROOT_REGION_MAX + 1);
	if (!root_memregs) {
		sbi_printf("%s: no memory for root regions\n", __func__);
		rc = SBI_ENOMEM;
		goto fail_deinit_context;
	}
	root.regions = root_memregs;

	root_hmask = sbi_zalloc(sizeof(*root_hmask));
	if (!root_hmask) {
		sbi_printf("%s: no memory for root hartmask\n", __func__);
		rc = SBI_ENOMEM;
		goto fail_free_root_memregs;
	}
	root.possible_harts = root_hmask;

	/* Root domain firmware memory region */
    // 初始化opensbi的代码段等内存区域属性为可读可执行(此内存区域均为运行内存区域)
	sbi_domain_memregion_init(scratch->fw_start, scratch->fw_rw_offset,
				  (SBI_DOMAIN_MEMREGION_M_READABLE |
				   SBI_DOMAIN_MEMREGION_M_EXECUTABLE |
				   SBI_DOMAIN_MEMREGION_FW),
				  &root_memregs[root_memregs_count++]);

    // 初始化opensbi的数据段等内存区域属性为可读可写(此内存区域均为运行内存区域)
	sbi_domain_memregion_init((scratch->fw_start + scratch->fw_rw_offset),
				  (scratch->fw_size - scratch->fw_rw_offset),
				  (SBI_DOMAIN_MEMREGION_M_READABLE |
				   SBI_DOMAIN_MEMREGION_M_WRITABLE |
				   SBI_DOMAIN_MEMREGION_FW),
				  &root_memregs[root_memregs_count++]);

	root.fw_region_inited = true;

	/*
	 * Allow SU RWX on rest of the memory region. Since pmp entries
	 * have implicit priority on index, previous entries will
	 * deny access to SU on M-mode region. Also, M-mode will not
	 * have access to SU region while previous entries will allow
	 * access to M-mode regions.
	 */
	sbi_domain_memregion_init(0, ~0UL,
				  (SBI_DOMAIN_MEMREGION_SU_READABLE |
				   SBI_DOMAIN_MEMREGION_SU_WRITABLE |
				   SBI_DOMAIN_MEMREGION_SU_EXECUTABLE),
				  &root_memregs[root_memregs_count++]);

	/* Root domain memory region end */
	root_memregs[root_memregs_count].order = 0;

	/* Root domain boot HART id is same as coldboot HART id */
	root.boot_hartid = cold_hartid;

	/* Root domain next booting stage details */
    // 设置root domain中下一阶段的启动参数、地址与特权模式
	root.next_arg1 = scratch->next_arg1;
	root.next_addr = scratch->next_addr;
	root.next_mode = scratch->next_mode;

	/* Root domain possible and assigned HARTs */
	sbi_for_each_hartindex(i)
		sbi_hartmask_set_hartindex(i, root_hmask);

	/* Finally register the root domain */
    // 注册root domain
	rc = sbi_domain_register(&root, root_hmask);
	if (rc)
		goto fail_free_root_hmask;

	return 0;
}

在sbi_domain_register注册函数中,对struct sbi_domain root 结构体完成了注册。主要完成了判断domain中mem region区域的合法性、domain加入domain_list 链表、将domain_hart_ptr_offset空间指向domain、最后设置domain的sbi_domain_date。

最终root domain的地址区域空间分配与属性设置如下图所示。其中在S、U模式下将全域内存都设置为了可读、可写、可执行是为了保证在跳转到下一阶段执行时,拥有访问内存的权限:

1.4、sbi_hsm_init

cpp 复制代码
int sbi_hsm_init(struct sbi_scratch *scratch, bool cold_boot)
{
	struct sbi_scratch *rscratch;
	struct sbi_hsm_data *hdata;

	if (cold_boot) {
        // 分配scratch内存空间
		hart_data_offset = sbi_scratch_alloc_offset(sizeof(*hdata));
		if (!hart_data_offset)
			return SBI_ENOMEM;

		/* Initialize hart state data for every hart */
		sbi_for_each_hartindex(i) {
			rscratch = sbi_hartindex_to_scratch(i);
			if (!rscratch)
				continue;

			hdata = sbi_scratch_offset_ptr(rscratch,
						       hart_data_offset);
            // 为每一个hart设置state状态,若为cold boot的hart,则设置其为START_PENDING;
            // 若不是cold boot的hart,则设置其为STOPPED
			ATOMIC_INIT(&hdata->state,
				    (i == current_hartindex()) ?
				    SBI_HSM_STATE_START_PENDING :
				    SBI_HSM_STATE_STOPPED);
			ATOMIC_INIT(&hdata->start_ticket, 0);
		}
	} else {
        // warm boot的hart则需等待其state状态改变为START_PENDING,再继续启动
		sbi_hsm_hart_wait(scratch);
	}

	return 0;
}

1.5、wake_coldboot_harts

设置coldboot_done标记为1,唤醒其他warm boot的hart继续进行启动流程。

1.6、sbi_platform_early_init

调用sbi_platform_ops(plat)->early_init(cold_boot)平台早期初始化函数,此部分和平台强相关,移植opensbi时,可能需要自己实现此部分平台相关的代码。

cpp 复制代码
/**
 * Early initialization for current HART
 *
 * @param plat pointer to struct sbi_platform
 * @param cold_boot whether cold boot (true) or warm_boot (false)
 *
 * @return 0 on success and negative error code on failure
 */
static inline int sbi_platform_early_init(const struct sbi_platform *plat,
					  bool cold_boot)
{
	if (plat && sbi_platform_ops(plat)->early_init)
		return sbi_platform_ops(plat)->early_init(cold_boot);
	return 0;
}

1.7、sbi_hart_init

初始化hart,根据读取hart指令集的特点来进行初始化:

cpp 复制代码
int sbi_hart_init(struct sbi_scratch *scratch, bool cold_boot)
{
	int rc;

	/*
	 * Clear mip CSR before proceeding with init to avoid any spurious
	 * external interrupts in S-mode.
	 */
    // 清除M模式下的中断挂起寄存器
	csr_write(CSR_MIP, 0);

	if (cold_boot) {
        // 根据指令集特点,来设置trap入口
		if (misa_extension('H'))
			sbi_hart_expected_trap = &__sbi_expected_trap_hext;

		hart_features_offset = sbi_scratch_alloc_offset(
					sizeof(struct sbi_hart_features));
		if (!hart_features_offset)
			return SBI_ENOMEM;
	}

    // 收集hart指令集特性,初始化特性结构体
	rc = hart_detect_features(scratch);
	if (rc)
		return rc;
    // 决定哪些中断从 M-mode 委托(delegate)给 S-mode 处理
	rc = delegate_traps(scratch);
	if (rc)
		return rc;

    // 根据hart指令集特性初始化,关闭中断使能
	return sbi_hart_reinit(scratch);
}

主要功能可以概况为初始化hart、委托M模式下的中断到S模式。

1.8、sbi_pmu_init

PMU是指Performance Monitoring Unit(性能监控单元),它是一整套用于"性能计数与分析"的硬件机制,用于统计 CPU 在运行过程中发生了哪些事件。用硬件计数器统计指令、周期、缓存、分支等性能事件,用于性能分析、调优和调度。

这里不进行拓展分析。

1.9、sbi_irqchip_init

进行irqchip初始化,也就是对PLIC中断控制器进行初始化。

首先根据注册的平台来调用**sbi_platform_ops(plat)->irqchip_init()**中断控制器初始化接口。在qemu的riscv平台中,其实现的功能为在设备树中寻找rscv的PLIC控制器节点,如果找到后,这调用对应驱动进行PLIC控制器的初始化。也就是最终将调用到以下函数:

cpp 复制代码
static int irqchip_plic_cold_init(const void *fdt, int nodeoff,
				  const struct fdt_match *match)
{
	int rc;
	struct plic_data *pd;

	pd = sbi_zalloc(PLIC_DATA_SIZE(sbi_hart_count()));
	if (!pd)
		return SBI_ENOMEM;

    // 获取PLIC控制器的寄存器基地址以及中断源的数量
	rc = fdt_parse_plic_node(fdt, nodeoff, pd);
	if (rc)
		goto fail_free_data;

	pd->flags = (unsigned long)match->data;

	rc = irqchip_plic_update_context_map(fdt, nodeoff, pd);
	if (rc)
		goto fail_free_data;

    // 初始化PLIC中断控制器,如中断优先级设置等;同时将寄存器地址注册到root domain
	rc = plic_cold_irqchip_init(pd);
	if (rc)
		goto fail_free_data;

	return 0;

fail_free_data:
	sbi_free(pd);
	return rc;
}

1.10、sbi_ipi_init

cpp 复制代码
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));
		if (!ipi_data_off)
			return SBI_ENOMEM;
        // 注册核间中断(即软中断)触发的操作接口
		ret = sbi_ipi_event_create(&ipi_smode_ops);
		if (ret < 0)
			return ret;
		ipi_smode_event = ret;
		ret = sbi_ipi_event_create(&ipi_halt_ops);
		if (ret < 0)
			return ret;
		ipi_halt_event = ret;
	} else {
		if (!ipi_data_off)
			return SBI_ENOMEM;
		if (SBI_IPI_EVENT_MAX <= ipi_smode_event ||
		    SBI_IPI_EVENT_MAX <= ipi_halt_event)
			return SBI_ENOSPC;
	}

	ipi_data = sbi_scratch_offset_ptr(scratch, ipi_data_off);
	ipi_data->ipi_type = 0x00;

	/* Clear any pending IPIs for the current hart */
	sbi_ipi_raw_clear(true);

	/* Enable software interrupts */
    // 使能软中断
	csr_set(CSR_MIE, MIP_MSIP);

	return 0;
}

1.11、sbi_tlb_init

为tlb分配内存空间,后续应该是用于存放tlb相关信息的;同时还注册了tlb相关的IPI核间中断处理事件。主要代码如下所示:

cpp 复制代码
int sbi_tlb_init(struct sbi_scratch *scratch, bool cold_boot)
{
	int ret;
	void *tlb_mem;
	atomic_t *tlb_sync;
	struct sbi_fifo *tlb_q;
	const struct sbi_platform *plat = sbi_platform_ptr(scratch);

	if (cold_boot) {
		tlb_sync_off = sbi_scratch_alloc_offset(sizeof(*tlb_sync));
		if (!tlb_sync_off)
			return SBI_ENOMEM;
		tlb_fifo_off = sbi_scratch_alloc_offset(sizeof(*tlb_q));
		if (!tlb_fifo_off) {
			sbi_scratch_free_offset(tlb_sync_off);
			return SBI_ENOMEM;
		}
		tlb_fifo_mem_off = sbi_scratch_alloc_offset(sizeof(tlb_mem));
		if (!tlb_fifo_mem_off) {
			sbi_scratch_free_offset(tlb_fifo_off);
			sbi_scratch_free_offset(tlb_sync_off);
			return SBI_ENOMEM;
		}
        // 注册ipi核间中断的tlb处理事件
		ret = sbi_ipi_event_create(&tlb_ops);
		if (ret < 0) {
			sbi_scratch_free_offset(tlb_fifo_mem_off);
			sbi_scratch_free_offset(tlb_fifo_off);
			sbi_scratch_free_offset(tlb_sync_off);
			return ret;
		}
		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_read_type(scratch, void *, tlb_fifo_mem_off);
	if (!tlb_mem) {
        // 为tlb信息结构体分配内存空间,每个hart一个SBI_TLB_INFO_SIZE大小内存
		tlb_mem = sbi_malloc(
				sbi_platform_tlb_fifo_num_entries(plat) * SBI_TLB_INFO_SIZE);
		if (!tlb_mem)
			return SBI_ENOMEM;
		sbi_scratch_write_type(scratch, void *, tlb_fifo_mem_off, tlb_mem);
	}

	ATOMIC_INIT(tlb_sync, 0);

    // 初始化struct sbi_tlb_info结构体,将tlb_mem清零,为tlb建立做准备
	sbi_fifo_init(tlb_q, tlb_mem,
		      sbi_platform_tlb_fifo_num_entries(plat), SBI_TLB_INFO_SIZE);

	return 0;
}

// tlb信息结构体
struct sbi_tlb_info {
	unsigned long start;
	unsigned long size;
	uint16_t asid;
	uint16_t vmid;
	enum sbi_tlb_type type;
	struct sbi_hartmask smask;
};

1.12、sbi_timer_init

对clint中的定时器进行初始化,用于产生系统时基等。主要功能为获取定时器寄存器地址、注册定时器加入到全局链表中进行管理以及提供定时器读取和设置接口等。

1.13、sbi_ecall_init

注册/初始化 SBI 系统调用表(ECALL handler、feature/extension registration),如下代码即为sbi的系统调用表。

cpp 复制代码
struct sbi_ecall_extension *const sbi_ecall_exts[] = {
	&ecall_time,
	&ecall_rfence,
	&ecall_ipi,
	&ecall_base,
	&ecall_hsm,
	&ecall_srst,
	&ecall_susp,
	&ecall_pmu,
	&ecall_dbcn,
	&ecall_cppc,
	&ecall_fwft,
	&ecall_legacy,
	&ecall_vendor,
	&ecall_dbtr,
	&ecall_sse,
	&ecall_mpxy,
	NULL
};

此函数通过注册上述的SBI系统调用表来将所有的SBI ecall事件加入到ecall_exts_list链表中。

当发生CAUSE_SUPERVISOR_ECALLCAUSE_MACHINE_ECALL 异常时,会触发进入mtvec中的trap入口并进入sbi_ecall_handler 执行分支。同时通用寄存器a7传入了extension_id,a6传入了func_id,通过extension_id可以在ecall_exts_list链表中找到对应处理接口,通过func_id可以在处理接口中执行对应功能分支。(这些id应该是riscv架构下的同一标准,目前我还未进行查证)

因此这里非常重要,我估计之后的mtvec将不会被更新,专门用于提供S-mode下Linux的opensbi固件功能接口的访问。即openSBI 不是启动阶段的一段代码,而是存在于整个系统生命周期中,M-mode模式下的"操作系统内核"。(其实与飞腾的pbf固件有点类似,使用smc来调用pbf固件中的某些接口功能)

1.14、sbi_hart_pmp_configure

根据 domain 信息与 platform 的 PMP 驱动,配置各 HART 的 PMP 条目,限制 M-mode 对 S/U 区域的访问(如果存在 SMEPMP 功能,则可能撤销 M-mode 对某些 S/U 空间的访问)。

实现物理内存保护功能,同时也是使S-mode模式下的下一阶段启动与运行过程中,不对其进行破坏,使openSBI能安全的存在于整个系统的生命周期中。

1.15、sbi_hsm_hart_start_finish

cpp 复制代码
void __noreturn sbi_hsm_hart_start_finish(struct sbi_scratch *scratch,
					  u32 hartid)
{
	unsigned long next_arg1;
	unsigned long next_addr;
	unsigned long next_mode;
	struct sbi_hsm_data *hdata = sbi_scratch_offset_ptr(scratch,
							    hart_data_offset);

	if (!__sbi_hsm_hart_change_state(hdata, SBI_HSM_STATE_START_PENDING,
					 SBI_HSM_STATE_STARTED))
		sbi_hart_hang();

    // 获取下一阶段的传参、跳转地址与特权模式
	next_arg1 = scratch->next_arg1;
	next_addr = scratch->next_addr;
	next_mode = scratch->next_mode;
	hsm_start_ticket_release(hdata);

    // 切换特权模式,并完成跳转(采用mret命令实现)
	sbi_hart_switch_mode(hartid, next_arg1, next_addr, next_mode, false);
}

如果openSBI使用的是jump模式,则跳转的地址可能由FW_JUMP_ADDR宏来定义;如果openSBI使用的是payload模式,则playload可能已经被打包进了openSBI中,获取地址直接跳转即可。

二、hart热启动流程分析

热启动流程就不再继续进行分析了,只对其启动中比较重要的一点进行说明:

在热启动过程中,hart会进入一个循环,一直持续等待hsm状态变为SBI_HSM_STATE_START_PENDING(因为在cold hart启动时,将其余hart的hsm状态都设置为了SBI_HSM_STATE_STOPPED)。而在整个cold hart启动过程中,都没有再改变其余hart的hsm状态。

由此分析,多核启动时,通过cold hart在启动Linux系统过程中,使用ecall调用SBI系统调用表中的hsm功能来唤醒释放其他hart正常启动。

至此,整个openSBI框架以及启动原理基本进行了一个完整的分析,从汇编启动,再到注册SBI系统调用表,再到下一阶段启动。

相关推荐
狗哥哥2 小时前
企业级 HTTP 客户端架构演进与设计
前端·架构
OliverH-yishuihan2 小时前
在 Windows 上安装 Linux
linux·运维·windows
zclinux_2 小时前
【Linux】虚拟化的内存气泡
linux·运维·服务器
爱潜水的小L2 小时前
自学嵌入式day33,互斥和同步
linux
松涛和鸣3 小时前
DAY33 Linux Thread Synchronization and Mutual Exclusion
linux·运维·服务器·前端·数据结构·哈希算法
A_New_World3 小时前
Linux内核配置、编译、安装
linux
kangk123 小时前
linux常见指令与实例(生物信息方向)
linux
前端阿森纳3 小时前
公司是否因为AI正在从“以人为本”走向“以核心数据集为本”?
架构·aigc
小宝哥Code3 小时前
区块链(Blockchain)—— 概念、架构与应用
架构·区块链