跟着大佬的文章读一下 Linux 0.11 的源码
github.com/sunym1993/flash-linux0.11-talk
一个操作系统的主函数
main
函数在 init/main.c
这里用了 https://github.com/karottc/linux-0.11
中注释过的代码
// 内核初始化主程序。初始化结束后将以任务0(idle任务即空闲任务)的身份运行。
void main(void) {
/* This really IS void, no error here. */
/* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
// 下面这段代码用于保存:
// 根设备号 ->ROOT_DEV;高速缓存末端地址->buffer_memory_end;
// 机器内存数->memory_end;主内存开始地址->main_memory_start;
// 其中ROOT_DEV已在前面包含进的fs.h文件中声明为extern int
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO; // 复制0x90080处的硬盘参数
memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb + 扩展内存(k)*1024 byte
memory_end &= 0xfffff000; // 忽略不到4kb(1页)的内存数
if (memory_end > 16*1024*1024) // 内存超过16Mb,则按16Mb计
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) // 如果内存>12Mb,则设置缓冲区末端=4Mb
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024) // 否则若内存>6Mb,则设置缓冲区末端=2Mb
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024; // 否则设置缓冲区末端=1Mb
main_memory_start = buffer_memory_end;
// 如果在Makefile文件中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘。此时主内存将减少。
// 以下是内核进行所有方面的初始化工作
mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c
trap_init(); // 陷阱门(硬件中断向量)初始化,kernel/traps.c
blk_dev_init(); // 块设备初始化,kernel/blk_drv/ll_rw_blk.c
chr_dev_init(); // 字符设备初始化, kernel/chr_drv/tty_io.c
tty_init(); // tty初始化, kernel/chr_drv/tty_io.c
time_init(); // 设置开机启动时间 startup_time
sched_init(); // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c)
buffer_init(buffer_memory_end); // 缓冲管理初始化,建内存链表等。(fs/buffer.c)
hd_init(); // 硬盘初始化,kernel/blk_drv/hd.c
floppy_init(); // 软驱初始化,kernel/blk_drv/floppy.c,后面直接忽略了
// 完成初始化工作,切换到用户态模式
sti(); // 所有初始化工作都做完了,开启中断
// 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。
move_to_user_mode(); // 移到用户模式下执行
if (!fork()) { /* we count on this going ok */
init(); // 在新建的子进程(任务1)中执行。
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
// pause系统调用会把任务0转换成可中断等待状态,再执行调度函数。但是调度函数只要发现系统中
// 没有其他任务可以运行是就会切换到任务0,而不依赖于任务0的状态。
for(;;) pause();
}
整个 main
函数可以分为四个部分,第一个部分是内存的一些计算和参数设置,第二个部分是内核所有方面的初始化,第三个部分负责切换到用户态,最后则是将这个程序作为 idle
程序,陷入死循环
变量计算(规划内存)
第一部分的代码
ROOT_DEV = ORIG_ROOT_DEV; // ORIG_ROOT_DEV = *(0x901FC)
drive_info = DRIVE_INFO; // DRIVE_INFO = *(0x90080) 复制0x90080处的硬盘参数
memory_end = (1<<20) + (EXT_MEM_K<<10); // EXT_MEM_K = *(0x90002) 内存大小=1Mb + 扩展内存(k)*1024 byte
memory_end &= 0xfffff000; // 忽略不到 4kb(1 page) 的内存数
if (memory_end > 16*1024*1024) // 内存超过 16Mb,则按 16Mb 计
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) // 如果内存 >12Mb,则设置缓冲区末端为 4Mb
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024) // 否则若内存 >6Mb,则设置缓冲区末端为 2Mb
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024; // 否则设置缓冲区末端为 1Mb
main_memory_start = buffer_memory_end;
这里的代码很乱,主要是计算了几个值:ROOT_DEV
, drive_info
, memory_end
, buffer_memory_end
, main_memory_start
把直接赋值的去掉就只剩下 memory_end
和 buffer_memory_end
了
首先算的是 memory_end
,EXT_MEM_K
是从内存 0x90002
处取值,根据之前对 boot
部分的分析,可以知道这里存的是 扩展内存数(Kb)
,所以这里得到的就是 内存+1Mb
的大小(单位为字节)
在初始计算完成后,做了一个 & 0xfffff000
,因为分页机制需要对其到 4Kb
,所以需要忽略多余的大小
随后用一个 if
限制了内存的大小为 16Mb
,这是因为之前做分页初始化的时候,只初始化了 1 个页表目录,4 个页表,所以能映射的地址最大值只有 16Mb
接下来是根据内存的大小设置 buffer_memory_end
,这个变量用于划分缓冲区和主内存,根据内存大小动态设置缓冲区大小,最小为 1Mb
关于这几个变量的意义,大佬的图画的很好
+------------------+ <-- 0x00000000
| System |
+------------------+
| Buffer Memory |
+------------------+ <-+ buffer_memory_end
| | | main_memory_start
| Main Memory |
| |
+------------------+ <-- memory_end
| {Unused Memory} |
+------------------+
后面内存的初始化需要根据这几个内存进行
// ...
mem_init(main_memory_start, memory_end);
// ...
buffer_init(buffer_memory_end);
// ...
所有东西都需要初始化
主内存初始化 (mem_init)
mem_init
在 mm/memory.c
中
// 物理内存管理初始化
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem; // 设置内存最高端
for (i = 0; i < PAGING_PAGES; i++) // (15 * 1024 * 1024) >> 12
mem_map[i] = USED; // USED = 100
i = MAP_NR(start_mem); // MAP_NR(addr) (((addr)-LOW_MEM)>>12)
// 主内存区其实位置处页面号
end_mem -= start_mem;
end_mem >>= 12; // 主内存区中的总页面数
while (end_mem-- > 0)
mem_map[i++] = 0; // 主内存区页面对应字节值清零
}
之前的注释太多了,不如自己看,其实整个函数都是在对页表进行初始化
主内存初始化,传入的参数为 main_memory_start
, memory_end
HIGH_MEMORY = end_mem;
HIGH_MEMORY
是一个静态的全局变量,这里将其初始化为 memory_end
,也就是内存的最大值
PAGING_MEMORY
设置为 15Mb
(为所有可能作为主内存的内存都需要留一个位置,1Mb~16Mb
),按照 1 页大小为 4Kb,可以计算得到总页数的理论最大可能,然后按照这个大小设置一个内存空间 mem_map[PAGING_PAGES]
,用于记录对应的页是否被占用
先用一个 for
循环对该数组的每一项都标记为 USED=100
,然后
首先把主内存开头的第一个页对应的编号计算一下,使用宏定义 MAP_NR
,将 start_mem
换算为页的编号(这个宏定义还是比较好理解的,就不说了)
随后用 (end_mem - start_mem) >> 12
得到主内存占的页数
最后一个 while 循环,将主内存中用到的页标记为未占用
+----------------+
| |
+----------------+ 1Mb <- mem_map[0]
| 4K |
+----------------+ <----- mem_map[1]
| 4K |
+----------------+ <----- mem_map[2]
| .... |
+----------------+ <----- mem_map[PAGING_PAGE - 1]
| 4K |
+----------------+ 16Mb
对应关系如上图所示,在主内存的页标记为未使用,不在主内存的页标记为已使用,这样就可以进行后续的分页管理了
比如 get_free_page
就是取首个空闲页面,并标记为已使用
中断初始化 (trap_init)
之前初始化了中断,但只有默认中断,这里就逐渐往里加操作系统中断了
初始化函数在 kernel/traps.c
// 异常(陷阱)中断程序初始化子程序。设置他们的中断调用门(中断向量)。
// set_trap_gate()与set_system_gate()都使用了中断描述符表IDT中的陷阱门(Trap Gate),
// 他们之间的主要区别在于前者设置的特权级为0,后者是3.因此断点陷阱中断int3、溢出中断
// overflow和边界出错中断bounds可以由任何程序产生。
// 这两个函数均是嵌入式汇编宏程序(include/asm/system.h中)
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);
// 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
// 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21); // 允许8259A主芯片的IRQ2中断请求。
outb(inb_p(0xA1)&0xdf,0xA1); // 允许8259A从芯片的IRQ3中断请求。
set_trap_gate(39,¶llel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
}
主要利用了 set_trap_gate
和 set_system_gate
两个函数来设置中断,很明显的规律,第一个参数是中断号,第二个参数是中断处理函数的地址
中断处理函数主要在 asm.s
, system_call.s
中定义,暂时先不管,主要看一下两个 set
函数的定义
在 include/asm/system.h
#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)
都是调用了 _set_gate
#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 i: 立即数
"o" (*((char *) (gate_addr))), \ // %1 o: 内存单元
"o" (*(4+(char *) (gate_addr))), \ // %2
"d" ((char *) (addr)), \ // dx = addr
"a" (0x00080000) // ax = 0x80000
)
用内联汇编实现的 _set_gate
大概猜测了一下
_EAX = 0x80000;
_EDX = addr;
_EAX = _EDX;
_EDX = (short) (0x8000 + (dpl<<13) + (type << 8));
*((char*)(gate_addr)) = _EAX;
*((char*)(gate_addr)+4) = _EDX;
不太懂为什么一开始初始化了
eax
,可能是为了在调用这段汇编前,编译器会自动把eax
的值保存下来?
其实就是对 gate_addr
,按照提供的 dpl
, type
已经中断处理函数的地址 addr
进行赋值
中断的初始化其实就是这些,但这还没有结束,有几条中断后续会再进一步添加,比如 keybord_interrupt
就会在 tty_init
时再加上
块设备请求项初始化 (blk_dev_init)
要将硬盘中的数据读取到内存,需要使用块设备驱动程序
初始化函数在 kernel/blk_drv/ll_rw_blk.c
struct request request[NR_REQUEST];
void blk_dev_init(void) {
int i;
for (i = 0; i < NR_REQUEST; i++) {
request[i].dev = -1;
request[i].next = NULL;
}
}
整个初始化的流程只有一个 for
循环,将 request
结构体的 dev
设置为 -1
,next
设置为空
结构体的定义在 kernel/blk_drv/blk.h
文件
#define NR_BLK_DEV 7
#define NR_REQUEST 64
/**
* Ok, this is an expanded form so that we can use the same
* request for apging requests when that is implemented.
* In paging, 'bh' is NULL, and 'waiting' is used to wait for
* read/write completion.
*/
struct request {
int dev; // 设备号,-1 for no request
int cmd; // READ or WRITE
int errors; // 错误次数
unsigned long sector; // 起始扇区
unsigned long nr_sectors; // 扇区数量
char * buffer; // 数据缓冲区,硬盘中的数据读取到内存的位置
struct task_struct * waiting; // 发起请求的进程
struct buffer_head * bh; // 缓冲区头指针
struct request * next; // 下一个请求项
}
所以,一个 request
结构体完整描述了一次操作,而初始化做的事情就是在为结构体赋初值
控制台初始化 (tty_init)
这个函数执行后,就能够将键盘的输入输出到显示器上了
函数在 kernel/chr_drv/tty_io.c
void tty_init(void) {
rs_init();
con_init();
}
整个初始化流程又能够分成两个部分
第一个函数在 kernel/chr_drv/serial.h
void rs_init(void) {
set_intr_gate(0x24, rs1_interrupt);
set_intr_gate(0x23, rs2_interrupt);
init(tty_table[1].read_q.data);
init(tty_table[2].read_q.data);
outb(inb_p(0x21)&0xE7, 0x21);
}
set_intr_gate
是设置中断程序,这里设置的主要是串口中断,由于已经很少用到,此处可以忽略
第二个函数在 kernel/chr_drv/console.c
,这里就是初始化控制台了
void con_init(void) {
register unsigned char a;
char *display_desc = "????";
char *display_ptr;
video_num_columns = ORIG_VIDEO_COLS; // 显示器显示字符列数
video_size_row = video_num_columns * 2; // 每行需要使用的字节数(一个字符需要两个字节)
video_page = (unsigned char)ORIG_VIDEO_PAGE; // 显示器显示字符行数
cideo_erase_char = 0x0720; // 擦除字符(0x20显示字符,0x07是属性)
// 判断是单色还是彩色显示器,如果原始显示模式为7,说明是单色
if (ORIG_VIDEO_MODE == 7) { /* IS this a monochrome display? */
video_mem_start = 0xb0000; // 设置单显映像内存起始地址
video_port_reg = 0x3b4; // 设置单显索引寄存器端口
video_port_val = 0x3b5; // 设置单显数据寄存器端口
// 判断显示类型是 EGA 还是 MDA
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {
video_type = VIDEO_TYPE_EGAM; // 设置显示类型
video_mem_end = 0xb8000; // 设置显示内存末端地址
display_desc = "EGAm"; // 设置显示描述字符串
} else {
video_type = VIDEO_TYPE_MDA;
video_mem_end = 0xb2000;
display_desc = "*MDA";
}
} else { /* IF not, it is color. */
video_mem_start = 0xb8000;
video_port_reg = 0x3d4;
video_port_val = 0x3d5;
// 判断显示类型是 EGA 还是 CGA
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {
video_type = VIDEO_TYPE_EGAC;
video_mem_end = 0xbc000;
display_desc = "EGAc";
} else {
video_type = VIDEO_TYPE_CGA;
video_mem_end = 0xba000;
display_desc = "*CGA";
}
}
一开始的 if else
是根据此前从 BIOS 中断中读取到的数据,将显示器分为单色 or 彩色,EGA or MDA/CGA 四种情况,主要部分是设置了内存,结果如下:
+-------------+ <- 0xA0000
| EGA color |
| |
+-------------+ <- 0xB0000
| Mono text |
+-------------+ <- 0xB8000
| video buf |
+-------------+ <- 0xC0000
第二部分的代码是将类型字符串输出到屏幕右上角
/* Let the user known what kind of display driver we are using */
// 在屏幕右上角输出 显示描述字符串(display_desc)
// 首先将显示指针 display_ptr 指到屏幕第一行右端差 4 字符处(一个字符对应两个字节)
display_ptr = ((char *) video_mem_start) + video_size_row - 8;
//循环复制,每次循环后字符串指针+1,屏幕指针+2(空开属性字符的位置)
while (*display_desc) {
*display_ptr++ = *display_desc++;
display_ptr++;
}
根据这一部分代码,就可以得知屏幕输出字符的逻辑
简单的说,想在显示器上输出一个字符,只需要修改 video buf
部分的内存即可,例如
mov [0xB8000], 'a'
mov [0xB8002], 'b'
mov [0xB8004], 'c'
这句话的效果就是在显示器第一行第一个字符处输出字符 abc
,需要隔着放就是因为显示一个字符需要两个字节,第一个为显示的字符,第二个为属性
最后一部分
// 初始化用于滚屏的变量(主要用于 EGA/VGA)
origin = video_mem_start; // 滚屏起始显示内存地址
scr_end = video_mem_start + video_num_lines * video_size_row; // 滚屏结束内存地址
top = 0; // 最顶行号
bottom = video_num_lines; // 最底行号
gotoxy(ORIG_X, ORIG_Y); // 初始化光标位置
set_trap_gate(0x21, &keyboard_interrupt); // 设置键盘中断处理函数
outb_p((unsigned char)(inb_p(0x21) & 0xfd), 0x21); // 取消 8259A 中对键盘中断的屏蔽,允许 IRQ1
a = inb_p(0x61); // 延迟读取键盘端口 0x61(8255A 端口 PB)
outb_p((unsigned char)(a | 0x80), 0x61); // 设置禁止键盘工作(位 7 设置为 1)
outb(a, 0x61); // 再允许键盘工作,用以复位键盘操作
}
先设置了滚屏用的一些变量,包括顶部行号和末尾行号等内容
随后将光标定位到此前保存的光标位置(内存 0x90000
处)
最后设置了键盘的中断处理函数,重启键盘工作
在完成这个初始化后,屏幕上就能显示输入的内容了
简单看一下键盘中断的处理:
_keyboard_interrupt:
...
call _do_tty_interrupt
...
void do_tty_interrupt(int tty) {
copy_to_cooked(tty_table+tty);
}
void copy_to_cooked(struct tty_struct * tty) {
...
tty->write(tty);
...
}
void con_write(strct tty_struct * tty) {
_asm{
mov al, c;
mov ah, attr;
mov ebx, pos;
mov [ebx], ax;
}
pos += 2;
x++;
}
根据调用连,发现最后在屏幕上输出是通过 con_write
函数实现的,这段汇编其实就是在 pos
地址存入字符 c
,随后 pos+=2
和 x++
,用于调整光标
时间初始化 (time_init)
时间初始化函数位于 init/main.c
static void time_init(void) {
struct tm time;
do {
time.tm_sec = CMOS_READ(0);
time.tm_min = CMOS_READ(2);
time.tm_hour = CMOS_READ(4);
time.tm_mday = CMOS_READ(7);
time.tm_mon = CMOS_READ(8);
time.tm_year = CMOS_READ(9);
} while (time.tm_sec != CMOS_READ(0));
BCD_TO_BIN(time.tm_sec);
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
time.tm_mon--;
startup_time = kernel_mktime(&time);
}
这段代码主要使用了两个宏定义
#define CMOS_READ(addr) ({ \
outb_p(0x80 | addr, 0x70); \
inb_p(0x71); \
})
CPU 通过端口与外设交换数据,向端口写入数据表示要进行的操作,随后从另一个端口读取外设返回的数据,这样操作系统就不需要考虑外设的具体实现
原文是以键盘为例
端口 | 读 | 写 |
---|---|---|
0x1F0 | 数据寄存器 | 数据寄存器 |
0x1F1 | 错误寄存器 | 特征寄存器 |
0x1F2 | 扇区计数寄存器 | 扇区计数寄存器 |
0x1F3 | 扇区号寄存器或 LBA 块地址 0~7 | 扇区号或 LBA 地址 0~7 |
0x1F4 | 磁道数低 8 位或 LBA 块地址 8~15 | 磁道数低 8 位或 LBA 块地址 8~15 |
0x1F5 | 磁道数高 8 位或 LBA 块地址 16~23 | 磁道数高 8 位或 LBA 块地址 16~23 |
0x1F6 | 驱动器/磁头或 LBA 块地址 24~27 | 驱动器/磁头或 LBA 块地址 24~27 |
0x1F7 | 命令寄存器或状态寄存器 | 命令寄存器 |
此处与 CMOS 进行交互,在 CMOS 中 0x70
是写端口号,0x80 | addr
是要读取的 CMOS 内存地址,而 0x71
是读端口号
这里再放一个 CMOS 的部分地址信息表
地址偏移 | 存储内容 |
---|---|
0x00 | 当前秒值(实时钟) |
0x01 | 报警秒值 |
0x02 | 当前分钟值(实时钟) |
0x03 | 报警分钟值 |
0x04 | 当前小时值(实时钟) |
0x05 | 报警小时值 |
0x06 | 一周中的当前天(实时钟) |
0x07 | 一月中的当前日期(实时钟) |
0x08 | 当前月份(实时钟) |
0x09 | 当前年份(实时钟) |
0x0A | RTC 状态寄存器A |
CMOS 是主板上可读写的 RAM 芯片
所以初始化过程中,第一部分的代码就是根据 CMOS 的端口,获取当前的时间信息
随后的 BCD_TO_BIN
是将 CMOS 使用的 BCD 码转换为数字
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
BCD 码:用 4 位二进制数表示 1 位十进制数中 0~9 这 10 个数码,例如
0=0b0000
,1=0b0001
,10=0b00010000
这一部分就很好理解了
随后 tm_mon--
是因为此处结构体的设计是以 0-11
的形式来存储月份,而获取到的结果为 1-12
最后是执行了 kernel_mktime(&time)
函数,位于 kernel/mktime.c
,这个函数用于计算从 1970 年 1 月 1 日至今的秒数
#define MINUTE 60
#define HOUR (60 * MINUTE)
#define DAY (24 * HOUR)
#define YEAR (365 * DAY)
static int month[12] = {
0,
DAY * (31),
DAY * (31 + 29),
DAY * (31 + 29 + 31),
DAY * (31 + 29 + 31 + 30),
DAY * (31 + 29 + 31 + 30 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30),
};
long kernel_mktime(struct tm * tm) {
long res;
int year;
year = tm->tm_year - 70; // 从 70 年至今经过的年数
res = YEAR * year + DAY * ((year + 1) / 4); // 年数 * 365 + 闰年的数量 = 这些年经过的秒数
res += month[tm->tm_mon]; // 本年到本月经过的秒数(先按闰年计算,随后判断是否需要减少)
if (tm->tm_mon > 1 && ((year + 2) % 4)) // 非闰年且大于 2 月的话需要减少 1 天
res -= DAY;
res += DAY * (tm->tm_mday - 1); // 照常加上就行本月、当天、上一小时、上一分钟经过的秒数即可
res += HOUR * tm->tm_hour;
res += MINUTE * tm->tm_min;
res += tm->tm_sec;
return res;
}
这个闰年的计算略有些抽象,没有考虑 2000
年的问题
进程调度初始化 (sched_init)
位于 kernel/sched.c
void sched_init(void) {
int i;
struct desc_struct *p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt + FIRST_TSS_ENTRY, &(init_task.task.tss));
set_ldt_desc(gdt + FIRST_LDT_ENTRY, &(init_task.task.ldt));
p = gdt + 2 + FIRST_TSS_ENTRY;
for (i = 1; i < NR_TASKS; i++) {
task[i] = NULL;
p->a = p->b = 0;
p++;
p->a = p->b = 0;
p++;
}
__asm__("pushfl; andl $0xffffbfff, (%esp); popfl"); // 复位 NT 标志
ltr(0); // 将任务 0 的 TSS 加载到任务寄存器 tr
lldt(0); // 将局部描述符加载到局部描述符寄存器
// 初始化 8253 定时器
outb_p(0x36, 0x43); // binary, mode 3, LSB/MSB, ch 0
outb_p(LATCH & 0xff, 0x40); // LSB, 定时值低字节
outb(LATCH >> 8, 0x40); // MSB,定时值高字节
set_intr_gate(0x20, &timer_interrupt); // 设置时钟中断处理程序
outb(inb_p(0x21) & ~0x01, 0x21); // 修改中断控制器屏蔽码,允许时钟中断
set_system_gate(0x80, &system_call); // 设置系统调用中断
}
代码首先初始化了 TSS 和 LDT,此前 GDT 全局描述符表共初始化了 256 个项,这里在此前的末尾追加了 TSS 和 LDT,后续的 252 个项都是为 TSS 和 LDT 留的
TSS(任务状态段):用于保存和恢复进程的上下文
LDT(局部描述符表):与 GDT 对应,用于为用户态进程提供数据段和代码段
每个用户态进程都会分配一对 TSS 和 LDT,LDT 中记录了进程的代码段和数据段,用于计算得到线性地址,线性地址再通过页表计算得到物理地址
在完成这两个段的初始化后,执行了一个循环
在此循环中,首先为 struct task_struct *task[NR_TASKS]
这个指针数组全部赋初值为 NULL
,这个结构体代表每一个进程的信息,包括了进程当前的状态、优先值等等内容
随后为 gdt 中剩余部分赋值为 0,后续需要时再使用
现在就建立好第一个 TSS 和 LDT,是因为当前代码未来会化身为进程 0 (空闲进程)的代码,所以在初始化的时候,需要提前将进程的信息写好
随后的 ltr
和 lldt
设置了 tr
和 ldtr
两个寄存器,这两个寄存器分别指向 TSS
和 LDT
,作用上与 gdtr
和 idtr
对应
最后一段设置是和计时器的初始化,硬件真的是一如既往的抽象,通过端口与计时器交互,在执行完最后这段代码后,计时器会以固定频率向 CPU 发送中断信号(0x20)
开启计时器后,计时器发送的中断设置了一个中断程序 timer_interrupt
,最后这是了系统调用中断(0x80),也就是 orw
里日常用的 int 0x80
缓冲区初始化 (buffer_init)
位于 fs/buffer.c
struct buffer_head * start_buffer = (struct buffer_head *)(&end);
void buffer_init(long buffer_end) { // buffer_init(buffer_memory_end)
void * b;
if (buffer_end == 1 << 20)
b = (void *)(640 * 1024);
else
b = (void *)buffer_end;
struct buffer_head * h = start_buffer;
while ((b = (char*)b - BLOCK_SIZE) >= ((void *)(h + 1))) {
h->b_dev = 0;
h->b_dirt = 0;
h->b_count = 0;
h->b_lock = 0;
h->b_uptodate = 0;
h->b_wait = NULL;
h->b_next = NULL;
h->b_prev = NULL;
h->b_data = (char *)b;
h->b_prev_free = h - 1;
h->b_next_free = h + 1;
h++;
NR_BUFFERS++;
if (b == (void *)0x100000)
b = (void *)0xA0000;
}
h--;
free_list = start_buffer;
free_list->b_prev_free = h;
h->b_next_free = free_list;
int i;
for (i = 0; i < NR_HASH; i++)
hash_table[i] = NULL;
}
文章里讲的第一行代码其实是定义在全局变量的,end
是一个外部变量,是由链接程序 ld
生成的位于程序末端的变量,而缓冲区变量的开始位置就被定义在了这个地址,也就是内核程序的末尾
整个初始化函数可以大致分成三段
第一段仍然是一个参数的定义,设置了 buffer
的内存末尾,如果缓冲区末尾在 1Mb 处,此时 640KB-1MB 的内存被显示内存和 BIOS 占用了,需要手动将末尾地址修改为 640KB,所以是对缓冲区末尾的设置(原文中假设了总内存为 8MB,所以这里的 b
就是 buffer_end
)
接下来的 while
循环是对结构体的设置,先看一下整个循环的结构
struct buffer_head * h = start_buffer;
while ((b = (char*)b - BLOCK_SIZE) >= ((void *)(h + 1))) { // BLOCK_SIZE = 1024
h++;
NR_BUFFERS++; // counter
if (b == (void *)0x100000)
b = (void *)0xA0000;
}
这里的循环每次 b -= 1024
和 h++
,画个图吧
low
+---------------+ <- 0
| Kernel |
+---------------+ <- h = start_buffer
| | |
| | v
| Buffer Memory |
| | ^
| | |
+---------------+ <- b = buffer_memory_end
high
所以这里其实是将 Buffer 拆分成了两种空间,第一种用 h
指针遍历,第二种用 b
指针遍历,直到两种结构体出现重合为止
此外,当 b
遍历到 1Mb 时,会直接跳到 640Kb,原因上面解释过了,需要将显示内存和 BIOS 占用的空间空出来
中间部分代码其实就比较好理解了
h->b_dev = 0; // 使用该缓冲区的设备号
h->b_dirt = 0; // 脏版本,缓冲区修改标识
h->b_count = 0; // 该缓冲区的引用计数
h->b_lock = 0; // 缓冲区锁定标识
h->b_uptodate = 0; // 缓冲区更新标识(数据有效标识)
h->b_wait = NULL; // 指向等待该缓冲区解锁的进程
h->b_next = NULL; // 指向具有相同 hash 值的下一个缓冲头
h->b_prev = NULL; // 指向具有相同 hash 值的上一个缓冲头
h->b_data = (char *)b;
h->b_prev_free = h - 1;
h->b_next_free = h + 1;
对所有 h
结构体进行了设置,h->b_data = (char*)b
这里就解释了 b
是什么,简单的说,h
是 buffer_head
的结构体,代表了缓冲头,而 b
是缓冲块,每个大小为 1024 Byte,所以每一项 h
都指向了一个对应的 b
此外一个比较重要的就是 b_prev_free
和 b_next_free
,这两个结构体将所有的空闲块串成了一个双向链表,其余部分暂时不是很重要,直接把注释抄过来了
在循环结束后还有几行代码
h--;
free_list = start_buffer;
free_list->b_prev_free = h;
h->b_next_free = free_list;
这就比较好理解了,就是把双向链表的头和尾连上,并且用 free_list
指向链表的开头
在 while
循环结束后还有一个 for
循环
for (int i = 0; i < NR_HASH; i++) // NR_HASH = 307
hash_table[i] = NULL;
这里有一个 307 项的 hash_table
,每一项都被赋值为 NULL
在读取块设备(硬盘)中的数据时,需要先将数据读到缓冲区中,如果缓冲区中已经存有数据,将不再从块设备读取,而是直接从缓冲区取数据,为了加快查找的速度,这里用到了 hash_table
这个 hashmap
结构
在需要读取某个块设备的数据时,通过 (dev ^ block) % 307
来找到 hash_table
里的索引下标,找到后就加到 hash_table[index]
这个链表中,h->b_next
和 h->b_prev
就构成了 hash_table
中双向链表
所以这里涉及到的整个结构就是哈希表+双向链表,后续可以在这个基础上实现 LRU
算法
硬盘初始化 hd_init
位于 kernel/blk_drv/hd.c
void hd_init(void) {
blk_dev[MAJOR_NR].request_fn = do_hd_request; // MAJOR_NR = 3, 硬盘主设备号
set_intr_gate(0x2E, &hd_interrupt);
outb_p(inb_p(0x21) & 0xfb, 0x21); // 复位接联的主 8259A int2 的屏蔽位,允许从片发出中断请求
outb(inb_p(0xA1) & 0xbf, 0xA1); // 复位硬盘的中断请求屏蔽位(在从片上),允许硬盘控制器发送中断请求信号
}
第一行是把 blk_dev
数组中硬盘主设备位置的块设备管理结构 blk_dev_struct
的 request_fn
赋值为 do_hd_request
这里 blk_dev
用于管理所有的块设备,每一个索引代表了一个块设备,包括 mem
, fd
, hd
, ttyx
, tty
, lp
,每个块设备执行的读写请求都由单独的函数实现,都由 request_fn
指向函数,所以上层可以屏蔽底层的差异(相当于多态,父类引用 request_fn
指向子类 do_hd_request
)
第二行是熟悉的设置中断,将中断号 0x2E
的处理函数设置为 hd_interrupt
最后两行也是经典的 IO 端口,效果是允许硬盘控制器发送中断请求信号
小结
这些 init
函数很多都比较好理解,比如对硬件设备的初始化大体的套路:
- 向某些 IO 端口读写一些数据,表示关闭或开启
- 将该硬件的中断处理程序加入到中断向量表中
- 初始化一些数据结构进行管理
读这一部分代码的主要目标还是从中可以看到整个内核中核心的一些数据结构,这在系统设计上是至关重要的,比如之前在写 malloc lab 的时候,当提供了需要用到的数据结构(和核心函数),剩下的一些函数其实都是水到渠成了
这一部分讲的主要是操作系统的初始化,main
函数的最后一个部分将会单独列出一个部分来讲