linux-0.11初始化程序笔记
启动代码完成了 linux 系统的启动,并获取了大量的硬件信息,供操作系统完成内核初始化的工作。
在 init/ 目录下有一个 main.c 文件,完成了内核初始化的所有工作。这个文件做的工作如下图所示:
一、计算内存边界值
ROOT_DEV = ORIG_ROOT_DEV; //根文件系统所在的设备号
drive_info = DRIVE_INFO; //硬盘参数表基址,存放着之前 setup.s 获取的硬盘参数信息
//计算内存边界
memory_end = (1<<20) + (EXT_MEM_K<<10); //EXT_MEM_K 为 1M 以后的扩展内存大小,同样是通过 setup.s 获得的
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
上述代码计算出了三个内存边界值,分别是 main_memory_start、memory_end和buffer_memory_end
又因为 main_memory_start 等于 buffer_memory_end ,所以其实只计算了两个值,依靠这三个边界值,划分出了两个区域,分别是缓冲区和主内存:
如果定义了内存虚拟盘,还需要初始化虚拟盘。
二、初始化
1.主内存初始化
void main(void) {
...
mem_init(main_memory_start, memory_end); //输入参数,之前计算出的主内存起始地址和结束地址
...
}
#define LOW_MEM 0x100000 //内存低端 1M,最低的 1M 主要给内核用。
#define PAGING_MEMORY (15*1024*1024) //于是分页内存还剩 15M
#define PAGING_PAGES (PAGING_MEMORY>>12) //4 个字节为一页
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12) //将指定的内存地址映射为页号
#define USED 100 //页面被占用的标志
static long HIGH_MEMORY = 0;
static unsigned char mem_map[PAGING_PAGES] = { 0, }; //默认所有内存页面都没有被占用
// start_mem = 2 * 1024 * 1024
// end_mem = 8 * 1024 * 1024
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem; //之前计算出的主内存末尾地址
for (i=0 ; i<PAGING_PAGES ; i++) //先将所有内存页面置为占用状态
mem_map[i] = USED;
i = MAP_NR(start_mem); //计算主内存起始页面号
end_mem -= start_mem; //末尾地址和起始地址的差值
end_mem >>= 12; //主内存页面个数
while (end_mem-->0) //将主内存的所有页面置空
mem_map[i++]=0;
}
但是需要置空的页面个数是置占用的个数的 15 倍,如果先把所有内存页面置空,再令缓冲区部分为占用状态,程序效率会更高。
2.中断初始化
void trap_init(void) {
int i;
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
set_trap_gate(39,¶llel_interrupt);
}
这段初始化用到了两个函数,分别是:set_trap_gate、set_system_gate,而这两个函数都来自于 _set_gate 函数:
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
其中 n 为中断号,addr 为中断程序偏移地址。
_set_gate 的实现如下:
这段代码用 gcc 内联汇编实现。
gate_addr 为描述符地址,通过中断号和 idt 数组选择。
type 为描述符类型阈值
dpl 描述符特权层值
addr 为偏移地址
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ //%0,dpl 和 type 通过某种方式组合成类型标志字
"o" (*((char *) (gate_addr))), \ //%1,描述符地址低 4 个字节
"o" (*(4+(char *) (gate_addr))), \ //%2,描述符地址高 4 个字节
"d" ((char *) (addr)), //%3,偏移地址
"a" (0x00080000)) //%4,a 代表 eax 寄存器(高字中含有段选择符)
关于 idt 表相关寄存器还没有进一步认识,无法进一步分析,总之这段代码将中断号与中断处理程序关联起来,当中断产生时,CPU 根据中断号选择对应的中断处理程序区执行。
3.块设备初始化
读硬盘需要有块设备驱动程序,块设备的初始化只需要把 request 结构体中的 dev 成员赋为 -1,把 next 成员赋为 NULL 即可:
void blk_dev_init(void) {
int i;
for (i=0; i<32; i++) {
request[i].dev = -1;
request[i].next = NULL;
}
}
关于 request 结构体的介绍如下:
struct request {
int dev; /* -1 if no request */
int cmd; /* READ or WRITE */
int errors; //操作时产生的错误次数
unsigned long sector; //起始扇区(1 块 = 2 扇区)
unsigned long nr_sectors; //读/写扇区数
char * buffer; //数据缓冲区
struct task_struct * waiting; //表示发起请求的进程
struct buffer_head * bh; //缓冲区头指针
struct request * next; //下一请求项
};
4.控制台初始化
由于字符设备初始化在这一版代码中没有实现,所以直接跳到控制台初始化。这个初始化完成后,就可以在屏幕上显示东西了!
void tty_init(void)
{
rs_init(); //初始化串行中断和串行接口 1 和 2
con_init(); //初始化控制台终端
}
跳过串行接口的初始化,据说现在不怎么用了,如果最新一期代码还有的话,在阅读最新代码的时候再深究。
首先是一段很长的 if-else 语句,通过宏 ORIG_VIDEO_MODE 来判断显示模式,在启动代码中,将显示模式存入地址为 0x90006 的内存区。
选择完显示模式后,接下来就要将字符显示在屏幕上。内存中有一部分区域是和显存映射的,也就是说,将数据写在下图的内存区域中,相当于将数据写入了显存中,而写入显存的数据是可以直接显示在屏幕上的。
初始化代码第一部分是获取显示模式的相关信息,是从内存中读取的:
第二部分是初始化一些显存相关的参数,分别是 video_mem_start (显存起始地址)、video_port_reg(显示索引寄存器端口)、video_port_val(数据寄存器端口)、video_type (显示类型)、video_mem_end(显存末端地址)。
第三部分是初始化一些滚屏的变量:
第四部分定位光标并开启键盘中断。
5.时间初始化
代码如下所示,需要关注的只有两个函数,分别是 CMOS_READ 和 BCD_TO_BIN:
CMOS_READ 的实现为将 0x80 | addr 的值写入到 0x70 端口,再从 0x71 端口读数据,这个版本的汇编都有些古老,就不细看了:
BCD_TO_BIN 就是将获取到的时间数据从 BCD 转成二进制。
kernel_mktime 函数通过读取到的时间,返回当前时间减去 1970 年的秒数,但是它没有对年份做判断,万一读取的数据出现问题小于 70 ,year 会变成负数:
6.进程调度初始化
sched_init 函数负责初始化进程调度需要用到的数据结构,首先初始化 TSS 和 LDT,这两个结构体位于 GDT 中。其中,TSS 叫做任务状态段,用于保存和恢复进程上下文的,也就是保存各个寄存器的信息,这样在进程切换的时候,才能做到保存和恢复上下文,继续执行;LDT 叫做局部描述符表,内核代码用 GDT 中的代码和数据;进程代码用 LDT 中的代码和数据:
接下来是一个循环,这个循环做了两件事,一个是为 task_struct 结构体数组赋初值,这个数组里每一个结构体代表每一个进程的信息;把 GDT 剩下的位置填充 0 ,也就是把所有的 TSS 和 LDT 填充 0 ,以后每创建一个进程,就会为一组 TSS 和 LDT 赋值。一开始初始化的 TSS 和 LDT 对应进程 0,也就是正在执行的代码:
接下来为 tr 和 ldt 寄存器赋初值,这两个寄存器分别指向 TSS 和 LDT 在内存中的位置:
最后,设置了两个重要中断,分别是时钟中断和系统调用中断:
7.缓冲区初始化
buffer_init 函数负责初始化缓冲区,传入参数 buffer_memory_end 是之前计算内存边界值时计算出的。这个函数创建了一个结构体指针和一个空指针,并初始化结构体指针指向的结构体。循环体所做的事情就是从缓冲区起始地址开始初始化每一块缓冲区(1024字节),直到初始化完所有缓冲区。buffer_end 是之前计算出的缓冲区末尾地址,start_buffer 是缓冲区的起始地址,是由链接器计算出的内核代码的末尾地址,用变量 end 存储。初始化就是为 buffer_head 结构体赋 0 值,为 b_prev_free 和 b_next_free 前后空闲指针赋上正确的地址。
h 是一个双向链表,通过以下三行代码,创建了一个双向环状链表,free_list 指向 h 的第一个结点:
至此,缓存头的结构已经初始化完成,但如何知道缓冲区中已经有了要读取的块设备中的数据呢,虽然可以遍历双向链表,但是效率太低,所以选择用一个哈希表的结构方便快速查找,于是创建一个数组 hash_table 并初始化:
哈希表加双向链表可以实现 LRU 算法,在缓冲区的管理中起到了重要作用。
8.硬盘初始化
最后两个初始化分别是硬盘初始化和软盘初始化,但是软盘初始化几乎被淘汰了,所以只看硬盘初始化即可。blk_dev 是操作系统用来管理硬盘的数据结构体。