本章ChChor多核相关内容。
1 多核启动
所有的CPU核心在开机时被同时启动,在引导时则会被分为两种类型。
一个指定的CPI核心会引导整个操作系统和初始化自身,被称为主CPU。
其他CPU只初始化自身即可,被称为副CPU。
CPU核心仅在系统引导时有所区分,在其他阶段,每个CPU核心都是被相同对待的。
选定主CPU:
1 | mrs x8, mpidr_el1 |
mpidr_el1 寄存器低8位为0,则表示主CPU。
阻塞其他副CPU的执行:
1 | wait_for_bss_clear: |
- 等待主CPU清除
bss
段,循环检查clear_bss_flag
变量 - 转为
EL1
异常级别 - 准备栈指针,这里的
boot_cpu_stack
是每个cpu都有自己单独的栈 - 循环检查
secondary_boot_flag
变量 - 设置
cpu id
后跳转到secondary_init_c
执行,激活副cpu
这里引用boot一文中的流程图:
思考:这里所有的副核心是依次激活,而不是并行激活。如果并行激活,会有问题吗?
每个CPU核心不共享内核栈。
1
char kernel_stack[PLAT_CPU_NUM][KERNEL_STACK_SIZE];
各个副CPU仅共享
clear_bss_flag
,但仅读取其中的数据,不会导致数据竞争。
2 大内核锁
现在,内核代码已经可以运行在多核上了。为了确保代码不会由于并行执行而引起错误,需要先解决并发问题。这里使用的是大内核锁,即内核的全局共享锁。
在CPU核心以内核态访问任何数据之前,应该先获得大内核锁。在退出内核态之前释放大内核锁。以确保同时只有一个CPU核心执行内核代码、访问内核数据。
kernel/common/lock.c中提供了一个简单的排号锁,以充当大内核锁。
排号锁 (ticket lock)
排号锁是sync一节的内容,这里用到了,简单介绍写实现:
数据结构:
1 | struct lock { |
主要是一个owner和一个next成员,owner == next 的时候,才是给自己的锁,否则是别人的锁。这样实现了先来后到。
1 | struct lock big_kernel_lock; |
3 锁、放锁时机
- 在主CPU激活副CPU之前需要先获取大内核锁
- 副CPU初始化完成之后且副CPU返回用户态之前获取大内核锁
- exception_table.S中的el0_syscall:在跳转到syscall_table中相应的syscall条目之前获取大内核锁
- handle_entry_c:在该异常处理函数第一行获取大内核锁。但是内核态也可能发生异常,如果异常是在内核中捕获的,则不应该获取大内核锁
1 | void handle_entry_c(int type, u64 esr, u64 address) |
- handle_irq:在中断处理函数第一行获取大内核锁(同样如果是内核异常,则不获取)
1 | void handle_irq(int type) |
- exception_return:释放大内核锁。所有情况下,内核态返回用户态都使用exception_return,因此这是唯一需要调用
unlock_kernel()
的位置。