ChCore Lab1 Report
SJTU ChCore Lab1 思考题个人解答
[1. 控制单一核完成初始化]
通过 CPU 的逻辑 ID,控制 CPU 0 进入初始化流程,而其他 CPU 进入忙等直到 BSS 段清空完成
[4. 为什么进入 C 函数前要设置启动栈]
C 函数需要使用栈空间保存函数状态、局部变量等信息
[5. 为什么要清空 BSS 段]
为了压缩 ELF 文件大小,ELF 文件中只有文件头中存有 BSS 段的长度信息,但不包含段信息. 内核默认这些变量初始为 0. 若不清空 BSS 段,未初始化的全局变量/静态局部变量是未知的,导致内核初始状态不可控,可能引发不可预测的行为
bss 段: Blocked Started by Symbol, 用来存放程序中未初始化/初始化为 0 的全局变量/静态局部变量的内存区域,属于静态内存分配
[UART 交互逻辑]
参考课程文档 rpi-os
Raspberry Pi 3 预留 0x3F000000 往上的物理地址作为外设空间. 内核通过读写外设的寄存器来实现对外设的控制.
在裸机环境中,UART 设备的寄存器地址通常是硬编码的, 在硬件设计时会在总线上分配不重叠的物理地址范围给不同外设
启动阶段 MMU 未开启,内核直接使用物理地址访问外设寄存器(片上总线,与内存无关)
uart_init
整体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void early_uart_init(void)
{
unsigned int ra;
ra = early_get32(GPFSEL1);
/* Set GPIO14 as function 0. */
ra &= ~(7 << 12);
ra |= 4 << 12;
/* Set GPIO15 as function 0. */
ra &= ~(7 << 15);
ra |= 4 << 15;
early_put32(GPFSEL1, ra);
early_put32(GPPUD, 0);
delay(150);
early_put32(GPPUDCLK0, (1 << 14) | (1 << 15));
delay(150);
early_put32(GPPUDCLK0, 0);
/* Close serial briefly. */
early_put32(RASPI3_PL011_CR, 0);
/* Set baud rate as 115200. */
early_put32(RASPI3_PL011_IBRD, 26);
early_put32(RASPI3_PL011_FBRD, 3);
/* Enable FIFO. */
early_put32(RASPI3_PL011_LCRH, (1 << 4) | (3 << 5));
/* Inhibit interrupt. */
early_put32(RASPI3_PL011_IMSC, 0);
/* Enable serial to send or receive data. */
early_put32(RASPI3_PL011_CR, 1 | (1 << 8) | (1 << 9));
}
我们可以分成几个部分拆解:
印脚配置
1
2
3
4
5
6
7
8
9
10
ra = early_get32(GPFSEL1);
/* Set GPIO14 as function 0. */
ra &= ~(7 << 12);
ra |= 4 << 12;
/* Set GPIO15 as function 0. */
ra &= ~(7 << 15);
ra |= 4 << 15;
early_put32(GPFSEL1, ra);
这一段代码实际上是配置 GPIO 的 14, 15 印脚为 UART 功能:
GDFSEL1
寄存器控制 GPIO 10-19 的功能选择,每个 GPIO 占用 3 位GPIO14
对应寄存器的第 12-14 位,GPIO15
对应寄存器的第 15-17 位- GPIO14 为
100
(ALT0) 表示将设置为TXD0
功能,表示 UART 传输印脚 - GPIO15 为
100
(ALT0) 表示将设置为RXD0
功能,表示 UART 接受印脚
上下拉电阻配置
1
2
3
4
5
early_put32(GPPUD, 0);
delay(150);
early_put32(GPPUDCLK0, (1 << 14) | (1 << 15));
delay(150);
early_put32(GPPUDCLK0, 0);
根据 BCM2837 ARM Peripherals
文档,GPPUD
寄存器用于控制 GPIO 的上拉/下拉电阻:
1
2
3
4
5
6
7
8
9
10
11
12
13
The GPIO Pull-up/down Clock Registers control the actuation of internal pull-downs on
the respective GPIO pins. These registers must be used in conjunction with the GPPUD
register to effect GPIO Pull-up/down changes. The following sequence of events is
required:
1. Write to GPPUD to set the required control signal (i.e. Pull-up or Pull-Down or neither
to remove the current Pull-up/down)
2. Wait 150 cycles – this provides the required set-up time for the control signal
3. Write to GPPUDCLK0/1 to clock the control signal into the GPIO pads you wish to
modify – NOTE only the pads which receive a clock will be modified, all others will
retain their previous state.
4. Wait 150 cycles – this provides the required hold time for the control signal
5. Write to GPPUD to remove the control signal
6. Write to GPPUDCLK0/1 to remove the clock
初始化 UART
1
2
3
4
5
6
7
8
9
10
11
/* Close serial briefly. */
early_put32(RASPI3_PL011_CR, 0);
/* Set baud rate as 115200. */
early_put32(RASPI3_PL011_IBRD, 26);
early_put32(RASPI3_PL011_FBRD, 3);
/* Enable FIFO. */
early_put32(RASPI3_PL011_LCRH, (1 << 4) | (3 << 5));
/* Inhibit interrupt. */
early_put32(RASPI3_PL011_IMSC, 0);
/* Enable serial to send or receive data. */
early_put32(RASPI3_PL011_CR, 1 | (1 << 8) | (1 << 9));
uart_send
1
2
3
4
5
6
static void early_uart_send(unsigned int c)
{
/* Check if the send fifo is full. */
while (early_uart_fr() & (1 << 5));
early_put32(RASPI3_PL011_DR, c);
}
[8. 多级页表优劣分析]
- 优势:
- 节省内存:只为实际使用的内存区域分配页表
- 灵活性高:支持大块连续内存映射和小块不连续内存映射
- 扩展性好:适应不同大小的虚拟地址空间
- 劣势:
- 查找开销大:多级页表需要多次内存访问,增加了地址转换的时间开销
- 实现复杂:多级页表的管理和维护比单级页表复杂
- 内存碎片:多级页表可能导致内存碎片化,影响内存利用率
[11. init_kernel_pt 配置低地址页表的原因]
在正式启动内核之前,CPU 处于物理地址模式,需要通过低地址页表映射来访问内核代码和数据,否则无法正确执行内核代码
1
2
3
4
5
BEGIN_FUNC(el1_mmu_activate)
... # enable MMU
ldp x29, x30, [sp], #16
ret
END_FUNC(el1_mmu_activate)
实际实验中,若不配置低地址页表,内核会在启动页表后返回时需要从栈中弹出返回地址和帧指针,由于此时内核运行在低地址 ($sp=0x89fe0
) 且低地址页表未配置,会立即发生地址翻译错误,进而尝试跳转到异常处理函数。后续流程与未配置启动页表相同,内核会陷入 0x200
处非法指令的无限循环。
[12. 其他核心什么时候恢复运行]
在 CPU 0 完成计时器,调度器,中断向量表,PMU 以及锁的初始化后,为所有核心创造 IDLE 线程后,修改 secondary_boot_flag
变量,唤醒其他核心,其他核心在 secondary_init_c
后进入 IDLE 线程正式运行
可以发现,初始化流程中,调度器、中断向量表、PMU 都是共享资源,必须由单一核心完成初始化,防止多个核心同时操作导致数据竞争,因此只能由 CPU 0 完成初始化