《Linux-0.12 源码篇》- 02 内核初始化
字数 13042,阅读大约需 66 分钟
第二章:内核初始化
在完成了引导程序的使命之后,Linux 0.12内核的控制权交接到了head.s设置好的32位保护模式环境。此时,一个完整的操作系统就像一辆静静停在起跑线上的赛车,已经点火启动,但各个系统部件还需要被唤醒、初始化,才能正常运转。这个从静止到运转的过程,就是内核初始化。
init/main.c文件中的main()函数是内核初始化的总指挥,它按照精心设计的顺序依次启动内存管理、中断处理、块设备驱动、字符设备驱动等子系统,最终创建出第一个用户进程init。整个初始化过程体现了Linus在设计操作系统时的工程智慧:既要保证各个模块按正确的依赖顺序初始化,又要尽可能简洁高效。
2.1 main()函数的整体流程
Linux 0.12的内核初始化入口是main()函数,这个函数在head.s执行完成后被调用。从某种角度看,main()函数扮演着”系统总工程师”的角色:它要确保内存分配器就位,中断机制启用,设备驱动准备完毕,并最终创建出真正的用户态进程,让系统从单线程的初始化流程转变为多任务的操作系统。
main()函数本身运行在特权级0(即内核态),但它有一个独特之处:为了避免在后续的fork()调用中发生栈混乱,main()在完成各项初始化任务后会通过move_to_user_mode()宏切换到特权级3的用户态,变身为进程0。之后再通过fork()创建进程1(init进程),这样就绕过了内核态fork时写时复制机制缺失带来的问题。
2.1.1 main()函数源码分析
让我们详细查看init/main.c中main()函数的完整实现:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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 = ORIG_ROOT_DEV; drive_info = DRIVE_INFO; memory_end = (1<<20) + (EXT_MEM_K<<10); 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;#ifdef RAMDISK main_memory_start += rd_init(main_memory_start, RAMDISK*1024);#endif mem_init(main_memory_start,memory_end); trap_init(); blk_dev_init(); chr_dev_init(); tty_init(); time_init(); sched_init(); buffer_init(buffer_memory_end); hd_init(); floppy_init(); sti(); move_to_user_mode(); if (!fork()) { /* we count on this going ok */ init(); }/* * 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. */ for(;;) pause();}
这段代码虽然不长,但每一行都至关重要。让我们逐步分析:
2.1.2 系统参数读取
1 2 ROOT_DEV = ORIG_ROOT_DEV;drive_info = DRIVE_INFO;
这两行代码从setup.s保存在0x90000开始的内存区域读取系统参数。ORIG_ROOT_DEV是根文件系统设备号,DRIVE_INFO是硬盘参数信息。这些宏定义在include/linux/config.h中:
1 2 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)#define DRIVE_INFO (*(struct drive_info *)0x90080)
2.1.3 内存布局计算
接下来是关键的内存布局计算:
1 2 3 4 memory_end = (1<<20) + (EXT_MEM_K<<10);memory_end &= 0xfffff000;if (memory_end > 16*1024*1024) memory_end = 16*1024*1024;
这段代码计算系统的物理内存总量。(1<<20)是1MB(第一个MB由内核占用),EXT_MEM_K是setup.s读取的扩展内存大小(单位KB),<<10将KB转换为字节。&= 0xfffff000将内存大小按页面对齐(4KB边界)。最后限制最大内存为16MB,因为Linux 0.12只支持16MB物理内存。
缓冲区内存的分配策略根据物理内存大小而定:
-
• 内存>12MB:分配4MB给buffer cache -
• 内存6-12MB:分配2MB给buffer cache -
• 内存<6MB:分配1MB给buffer cache
1 2 3 4 5 6 7 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;
主内存区从buffer_memory_end开始,用于进程页面分配。这种设计将低地址分配给buffer cache,高地址分配给用户进程,便于管理。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 物理地址 (16MB 系统)═══════════════════════════════════════════════════════════════════════════════0x000000 ┤ │ ┌─────────────────────────────────────────┐ │ │ │ │ │ BIOS 中断向量表 (1KB) │ │ │ 0x00000 - 0x003FF │ ├──┼─────────────────────────────────────────┤ │ │ BIOS 数据区 (256B) │ │ │ 0x00400 - 0x004FF │ ├──┼─────────────────────────────────────────┤ │ │ 空闲 / 临时数据 │ │ │ 0x00500 - 0x07BFF │ ├──┼─────────────────────────────────────────┤ │ │ bootsect (512B) 已移动至此 │ │ │ 0x07C00 - 0x07DFF → 实际已移到0x90000 │ ├──┼─────────────────────────────────────────┤ │ │ │ │ │ **缓冲区 (Buffer)** │ │ │ 4MB 区域 │ │ │ (0x000000 - 0x3FFFFF) │ │ │ │ │ │ ┌───────────────────────────────────┐ │ │ │ │ 0x90000: bootsect (参数区) │ │ │ │ │ 0x90200: setup 模块 │ │ │ │ │ 0x10000: system 模块 (内核) │ │ │ │ └───────────────────────────────────┘ │ │ │ │0x400000 ┤ └─────────────────────────────────────────┘ │ │ ═══════════ buffer_memory_end ═══════════ │ │ ┌─────────────────────────────────────────┐ │ │ │ │ │ **主内存 (Main Memory)** │ │ │ 12MB 区域 │ │ │ (0x400000 - 0xFFFFFF) │ │ │ │ │ │ 用于: │ │ │ - 内核数据段 │ │ │ - 内核堆栈 │ │ │ - 动态分配的内存 │ │ │ - 用户进程(当有进程管理时) │ │ │ │ │ │ │0xFFFFFF ┤ └─────────────────────────────────────────┘0x1000000 ┤ │memory_end═══════════════════════════════════════════════════════════════════════════════总内存 16MB 系统:████████████████████████████████████████████████████████████████████████<──── 缓冲区 4MB ────><─────────────────── 主内存 12MB ──────────────────>
2.1.4 初始化函数调用序列
整个main()函数的初始化流程可以概括为以下步骤:首先读取setup.s留在0x90000处的系统参数信息(比如扩展内存大小、根设备号等),然后依次调用mem_init()初始化内存管理、trap_init()设置中断描述符表、blk_dev_init()初始化块设备请求队列、chr_dev_init()初始化字符设备、tty_init()初始化终端设备、time_init()读取CMOS时钟设置系统时间、sched_init()初始化进程调度、buffer_init()分配缓冲区、hd_init()和floppy_init()初始化硬盘和软盘驱动。在这些准备工作完成后,通过sti()开启中断,再用move_to_user_mode()切换到用户态,最终fork()出init进程并进入idle循环。
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 main()│├── mem_init(main_memory_start, memory_end) // 主内存区初始化 (mm/memory.c)│├── trap_init() // 陷阱门(硬件中断向量)初始化 (kernel/traps.c)│├── blk_dev_init() // 块设备初始化 (blk_drv/ll_rw_blk.c)│├── chr_dev_init() // 字符设备初始化 (chr_drv/tty_io.c)│├── tty_init() // tty初始化 (chr_drv/tty_io.c)│├── time_init() // 设置开机启动时间│├── sched_init() // 调度程序初始化 (kernel/sched.c)│├── buffer_init(buffer_memory_end) // 缓冲管理初始化 (fs/buffer.c)│├── hd_init() // 硬盘初始化 (blk_drv/hd.c)│├── floppy_init() // 软驱初始化 (blk_drv/floppy.c)│├── sti() // 开启中断│├── Log(LOG_INFO_TYPE, "<<<<< Linux0.12 Kernel Init Finished... >>>>>") // 打印完成信息│├── move_to_user_mode() // 移到用户模式下执行 (include/asm/system.h)│└── if (!fork_for_process0()) { // 创建子进程 │ └── init() // 在子进程(任务1/init进程)中执行 }
2.2 内存管理初始化
内存管理初始化由mem_init()函数完成,这个函数定义在mm/memory.c中。它的任务是建立物理内存页面的管理数据结构mem_map数组,该数组中的每个元素对应一个4KB物理页面,元素值记录该页面被引用的次数(0表示空闲,大于0表示被占用)。
mem_init()被调用时,会传入两个参数:start_mem和end_mem,分别表示主内存区的起始地址和结束地址。在Linux 0.12中,内核代码、数据以及一些缓冲区占据了低地址部分,主内存区从内核结束位置开始直到物理内存末尾。
2.2.1 mem_init()源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define LOW_MEM 0x100000#define PAGING_MEMORY (15*1024*1024)#define PAGING_PAGES (PAGING_MEMORY>>12)#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)#define USED 100static unsigned char mem_map [ PAGING_PAGES ] = {0,};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;}
这段代码的逻辑非常清晰:
-
1. 初始化为USED:首先将mem_map数组的所有元素设置为USED(100),表示所有页面都被占用。这是一种安全的默认策略,防止误分配已经被内核或buffer cache使用的页面。 -
2. 计算起始页面: MAP_NR(start_mem)将物理地址转换为页面索引。公式为((addr)-LOW_MEM)>>12,即从地址1MB处开始计算,除以4096(4KB)得到页面号。 -
3. 计算可用页面数: end_mem -= start_mem计算主内存区大小,然后>>12转换为页面数。 -
4. 标记空闲页面:循环将主内存区对应的mem_map元素清零,表示这些页面可用。

2.2.2 内存分配实例
以一个具体的例子来说明,假设系统有16MB物理内存,buffer cache分配了4MB,那么:
-
• 物理内存总量: 16MB (0x000000 – 0xFFFFFF) -
• 内核区域: 0x000000 – 0x0FFFFF (1MB) -
• Buffer Cache: 0x100000 – 0x3FFFFF (1MB-4MB) -
• 主内存区: 0x400000 – 0xFFFFFF (4MB-16MB, 12MB可用)
mem_init()调用时:
-
• start_mem = 0x400000(4MB) -
• end_mem = 0x1000000(16MB) -
• 页面范围:1024-4095 (3072个页面,12MB)
执行后:
-
• mem_map[0..1023] = USED(内核和buffer cache区域) -
• mem_map[1024..4095] = 0(可分配的主内存区)
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.2.3 页面分配和释放函数
内存管理还包括页面分配和释放的基础函数:
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 /* * Get physical address of first (actually last :-) free page, and mark it * used. If no free pages left, return 0. */unsigned long get_free_page(void){ register unsigned long __res asm("ax"); __asm__("std ; repne ; scasb\n\t" "jne 1f\n\t" "movb $1,1(%%edi)\n\t" "sall $12,%%ecx\n\t" "addl %2,%%ecx\n\t" "movl %%ecx,%%edx\n\t" "movl $1024,%%ecx\n\t" "leal 4092(%%edx),%%edi\n\t" "rep ; stosl\n\t" "movl %%edx,%%eax\n" "1:" :"=a" (__res) :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES), "D" (mem_map+PAGING_PAGES-1) :"di","cx","dx"); return __res;}
这是一段经典的内嵌汇编代码,它使用x86的字符串指令高效地搜索空闲页面:
-
1. std: 设置方向标志,使scasb向低地址搜索 -
2. repne scasb: 重复比较al(0)与mem_map数组,直到找到0或搜索完 -
3. 标记为已用: 将找到的页面标记为1 -
4. 计算物理地址: (page_nr << 12) + LOW_MEM -
5. 清零页面: 使用stosl指令将整个4KB页面清零
释放页面的函数更加简单:
1 2 3 4 5 6 7 8 9 10 11 void free_page(unsigned long addr){ if (addr < LOW_MEM) return; if (addr >= HIGH_MEMORY) panic("trying to free nonexistent page"); addr -= LOW_MEM; addr >>= 12; if (mem_map[addr]--) return; mem_map[addr]=0; panic("trying to free free page");}
释放页面时,先检查地址合法性,然后将对应的mem_map计数减1。如果计数降到0且继续释放,说明出现了错误(释放已经空闲的页面),触发panic。
以一个具体的例子来说明,假设系统有16MB物理内存,内核占用了前4MB,那么start_mem就是4MB处,end_mem是16MB处。mem_init()会将mem_map[0]到mem_map[1023]全部设为USED(表示内核占用的前4MB的1024个页面),然后将mem_map[1024]到mem_map[4095]清零(表示后12MB的3072个页面可用)。
2.3 中断与陷阱初始化
中断和陷阱(trap)是操作系统与外部世界以及异常情况交互的重要机制。trap_init()函数负责设置中断描述符表(IDT)中的陷阱门和系统调用门,这个函数定义在kernel/traps.c中。
在x86保护模式下,CPU通过IDT来处理中断和异常。IDT中每个表项是一个门描述符,可以是中断门、陷阱门或任务门。trap_init()使用set_trap_gate()和set_system_gate()宏将各种异常处理函数注册到IDT中。
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 trap_init()│├── [CPU异常 - 陷阱门 DPL=0]│ ├── INT 0 : divide_error (除零错误)│ ├── INT 1 : debug (调试陷阱)│ ├── INT 2 : nmi (非屏蔽中断)│ ├── INT 6 : invalid_op (无效操作码)│ ├── INT 7 : device_not_available (设备不可用)│ ├── INT 8 : double_fault (双重故障)│ ├── INT 10 : invalid_TSS (无效TSS)│ ├── INT 11 : segment_not_present (段不存在)│ ├── INT 12 : stack_segment (栈段错误)│ ├── INT 13 : general_protection (通用保护异常)│ ├── INT 14 : page_fault (页错误)│ ├── INT 15 : reserved (保留)│ ├── INT 16 : coprocessor_error (协处理器错误)│ └── INT 17 : alignment_check (对齐检查)│├── [CPU异常 - 系统门 DPL=3]│ ├── INT 3 : int3 (断点中断)│ ├── INT 4 : overflow (溢出中断)│ └── INT 5 : bounds (边界检查)│├── [硬件中断 - 陷阱门 DPL=0]│ ├── INT 39 : parallel_interrupt (并行口中断)│ └── INT 45 : irq13 (协处理器中断)│└── [保留中断向量] └── INT 18-47 : reserved (循环设置,含INT15、INT18-38、40-44、46-47)
2.3.1 trap_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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 void trap_init(void){ int i; // 设置除法错误异常处理函数 (INT 0) // 当执行除法指令时发生除零或溢出会触发 set_trap_gate(0,÷_error); // 设置调试异常处理函数 (INT 1) // 用于单步调试和断点 set_trap_gate(1,&debug); // 设置非屏蔽中断处理函数 (INT 2) // 通常用于硬件故障(如内存错误) set_trap_gate(2,&nmi); // 设置断点异常处理函数 (INT 3) // int3指令触发,用于调试器设置断点 set_system_gate(3,&int3); /* int3-5 can be called from all */ // 设置溢出异常处理函数 (INT 4) // into指令在OF标志置位时触发 set_system_gate(4,&overflow); // 设置边界检查异常处理函数 (INT 5) // bound指令检查数组边界时触发 set_system_gate(5,&bounds); // 设置无效操作码异常处理函数 (INT 6) // 执行无效或未定义指令时触发 set_trap_gate(6,&invalid_op); // 设置设备不可用异常处理函数 (INT 7) // 尝试使用协处理器但EM标志置位时触发 set_trap_gate(7,&device_not_available); // 设置双重故障异常处理函数 (INT 8) // 处理异常时又发生异常触发 set_trap_gate(8,&double_fault); // 设置协处理器段超越异常 (INT 9) // 386时代的遗留异常,现已废弃 set_trap_gate(9,&coprocessor_segment_overrun); // 设置无效TSS异常处理函数 (INT 10) // 任务切换时TSS无效触发 set_trap_gate(10,&invalid_TSS); // 设置段不存在异常处理函数 (INT 11) // 访问不存在的段触发 set_trap_gate(11,&segment_not_present); // 设置栈段错误异常处理函数 (INT 12) // 栈操作超出栈段限制触发 set_trap_gate(12,&stack_segment); // 设置一般保护错误处理函数 (INT 13) // 各种保护违规触发,最常见的保护模式异常 set_trap_gate(13,&general_protection); // 设置页面错误异常处理函数 (INT 14) // 访问不存在的页面或违反页面保护触发 set_trap_gate(14,&page_fault); // INT 15 保留,未使用 set_trap_gate(15,&reserved); // 设置协处理器错误处理函数 (INT 16) // 387协处理器检测到错误时触发 set_trap_gate(16,&coprocessor_error); // 对齐检查异常(#AC)的处理函数(INT 17) // 启用对齐检查时,检测到未对齐的内存操作数 set_trap_gate(17, &alignment_check); // 将INT 18-47保留,暂时指向reserved处理函数 for (i=18;i<48;i++) set_trap_gate(i,&reserved); // 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求.设置并行口中断描述符. set_trap_gate(45, &irq13); // 允许8259A主芯片的IRQ2中断请求(连接从芯片) outb_p(inb_p(0x21)&0xfb, 0x21); // 允许8259A从芯片的IRQ13中断请求(协处理器中断) outb(inb_p(0xA1)&0xdf, 0xA1); // 设置并行口1的中断0x27陷阱门描述符 set_trap_gate(39, ¶llel_interrupt);}
2.3.2 门描述符设置宏
set_trap_gate()和set_system_gate()宏定义在include/asm/system.h中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 设置陷阱门,DPL=0,只能由内核调用#define set_trap_gate(n,addr) \ _set_gate(&idt[n],15,0,addr)// 设置系统门,DPL=3,可由用户态调用#define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr)// 实际设置门描述符的宏// gate_addr: IDT表项地址// type: 门类型(15=陷阱门,14=中断门)// dpl: 特权级(0=内核,3=用户)// addr: 处理函数地址#define _set_gate(gate_addr,type,dpl,addr) \__asm__ ("movw %%dx,%%ax\n\t" \ // 将处理函数地址低16位放入ax "movw %0,%%dx\n\t" \ // 将段选择符放入dx "movl %%eax,%1\n\t" \ // 将低32位(偏移低16位+选择符)存入门描述符 "movl %%edx,%2" \ // 将高32位(偏移高16位+属性)存入门描述符 : \ // 无输出 : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ // 属性字:P=1,DPL,type "o" (*((char *) (gate_addr))), \ // 门描述符低4字节 "o" (*(4+(char *) (gate_addr))), \ // 门描述符高4字节 "d" ((char *) (addr)),"a" (0x00080000)) // 处理函数地址和代码段选择符0x0008
IDT表结构和门描述符格式,如下图所示,DPL 表示描述符特权级。

2.3.3 异常处理函数实现
大部分异常处理函数都定义在kernel/asm.s中,它们的实现模式类似:
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 33 34 # 除法错误异常处理_divide_error: pushl $_do_divide_error # 将C函数地址压栈no_error_code: # 无错误码的异常统一入口 xchgl %eax,(%esp) # 交换eax和栈顶(C函数地址) pushl %ebx # 保存寄存器 pushl %ecx pushl %edx pushl %edi pushl %esi pushl %ebp push %ds # 保存段寄存器 push %es push %fs pushl $0 # 压入0作为错误码占位符 lea 44(%esp),%edx # 计算原始esp位置 pushl %edx # 将esp压栈,作为第3个参数 movl $0x10,%edx # 设置内核数据段 mov %dx,%ds mov %dx,%es mov %dx,%fs call *%eax # 调用C处理函数(如do_divide_error) addl $8,%esp # 清理参数 pop %fs # 恢复寄存器 pop %es pop %ds popl %ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax iret # 中断返回
这个通用框架保存了所有寄存器,然后调用对应的C函数处理异常。C函数可以访问被中断进程的所有状态。
2.3.4 系统调用门的特殊性
INT 0x80是系统调用的入口,它的DPL设置为3,这是唯一允许用户态程序直接调用的中断。系统调用的处理函数system_call定义在kernel/system_call.s中,它负责:
-
1. 保存用户态寄存器 -
2. 检查系统调用号合法性 -
3. 通过sys_call_table查找并调用对应的内核函数 -
4. 检查信号和进程调度 -
5. 恢复用户态寄存器并返回
2.4 块设备与字符设备初始化
Linux 0.12将设备分为块设备和字符设备两大类。块设备以固定大小的数据块(通常1KB)为单位进行读写,典型的块设备有硬盘、软盘和虚拟磁盘。字符设备则以字符流方式进行输入输出,比如终端、串口等。
blk_dev_init()函数初始化块设备的请求队列。块设备的访问是异步的,当内核需要读写块设备时,会将请求放入设备的请求队列,然后由设备驱动按队列顺序处理。blk_dev_init()将所有块设备的请求队列头指针初始化为NULL,这样各个块设备驱动在后续初始化时就可以构建自己的请求队列。
chr_dev_init()函数初始化字符设备,在Linux 0.12中这个函数实际上是一个空函数,因为字符设备的初始化工作被分散到各个具体驱动中。例如,终端设备的初始化由tty_init()完成,串口设备的初始化在rs_init()中进行。
块设备和字符设备的核心区别在于数据组织方式和缓存策略。块设备通过buffer cache实现数据缓存,可以进行随机访问,适合文件系统;字符设备则像水管流水,数据顺序流过,不支持随机访问。这就好比图书馆(块设备)和广播电台(字符设备)的区别:在图书馆你可以随意翻到某一页,而广播节目只能按时间顺序收听,错过就没了。

2.5 设备驱动初始化
在main()函数的初始化流程中,设备驱动的初始化是非常重要的一步。这些初始化函数设置了硬件设备的中断处理程序,并启用相关的中断。
2.7.1 硬盘驱动初始化
硬盘驱动的初始化由hd_init()函数完成,它设置硬盘中断处理程序并允许硬盘中断。Linux 0.12使用中断驱动的方式访问硬盘:当内核向硬盘控制器发出读写命令后,硬盘控制器会在完成操作后发出IRQ 14中断,CPU响应中断并调用hd_interrupt()处理函数读取数据或处理错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // kernel/blk_drv/hd.cvoid hd_init(void){ // 设置硬盘中断处理函数 // IRQ 14对应中断号0x2E (0x20 + 14) set_intr_gate(0x2E,&hd_interrupt); // 取消8259A从片的中断屏蔽寄存器(IMR) // 0xA1是8259A从片的IMR端口 outb_p(inb_p(0xA1)&0xbf,0xA1); // 清零bit 6,允许IRQ 14 // 取消8259A主片的中断屏蔽寄存器 // 0x21是8259A主片的IMR端口 outb(inb_p(0x21)&0xfb,0x21); // 清零bit 2,允许IRQ 2(从片级联)}
IRQ 14中断映射:
-
• IRQ 14在8259A从片上 -
• 8259A从片通过IRQ 2级联到主片 -
• 中断向量 = 0x20(主片基址) + 14 = 0x2E
2.7.2 软盘驱动初始化
软盘驱动的初始化由floppy_init()完成,它与hd_init()类似,设置IRQ 6的中断处理函数floppy_interrupt,并取消对该中断的屏蔽。
1 2 3 4 5 6 7 8 9 10 // kernel/blk_drv/floppy.cvoid floppy_init(void){ // 设置软盘中断处理函数 // IRQ 6对应中断号0x26 (0x20 + 6) set_intr_gate(0x26,&floppy_interrupt); // 取消8259A主片的中断屏蔽寄存器 outb(inb_p(0x21)&~0x40,0x21); // 清零bit 6,允许IRQ 6}
软盘的访问机制比硬盘更复杂,因为:
-
1. 马达控制:软盘驱动器的马达需要手动开启和关闭 -
2. 超时处理:马达开启后一段时间未使用需自动关闭 -
3. 错误重试:软盘介质容易出错,需要更多的错误处理和重试逻辑
2.7.3 终端设备初始化
tty_init()函数负责初始化终端设备:
1 2 3 4 5 6 7 8 9 // kernel/chr_drv/tty_io.cvoid tty_init(void){ // 初始化串口设备(COM1和COM2) rs_init(); // 初始化控制台设备 con_init();}
串口初始化 (rs_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 33 34 35 36 // kernel/chr_drv/serial.cvoid rs_init(void){ // 设置串口1(COM1, IRQ 4)的中断处理函数 set_intr_gate(0x24,&rs1_interrupt); // 0x20 + 4 = 0x24 // 设置串口2(COM2, IRQ 3)的中断处理函数 set_intr_gate(0x23,&rs2_interrupt); // 0x20 + 3 = 0x23 // 初始化串口硬件 init(tty_table[1].read_q.data); // COM1 init(tty_table[2].read_q.data); // COM2 // 允许IRQ 3和4的中断 outb(inb_p(0x21)&0xE7,0x21); // 清零bit 3和4}static void init(int port){ // 设置波特率为2400 bps outb_p(0x80,port+3); // 设置DLAB=1(访问波特率寄存器) outb_p(0x30,port); // 波特率低字节 outb_p(0x00,port+1); // 波特率高字节 // 设置数据格式: 8数据位,1停止位,无校验 outb_p(0x03,port+3); // 8N1 // 启用DTR, RTS和中断 outb_p(0x0b,port+4); // DTR=1, RTS=1, OUT2=1 // 启用接收中断 outb(0x01,port+1); // 允许接收数据中断 // 读取并清除任何未处理的数据 (void)inb(port);}
控制台初始化 (con_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 33 34 35 // kernel/chr_drv/console.c void con_init(void){ register unsigned char a; char *display_desc = "EGAcolor"; // 显示器描述 // 读取setup.s保存的显示器信息 video_num_columns = ORIG_VIDEO_COLS; // 列数(80) video_size_row = video_num_columns * 2; // 每行字节数 video_num_lines = ORIG_VIDEO_LINES; // 行数(25) video_page = ORIG_VIDEO_PAGE; // 当前显示页 video_erase_char = 0x0720; // 空格字符(白色) // 根据显示模式设置显存基址 if (ORIG_VIDEO_MODE == 7) { // MDA/Hercules video_mem_start = 0xb0000; video_port_reg = 0x3b4; video_port_val = 0x3b5; display_desc = "*MDA"; } else { // CGA/EGA/VGA video_mem_start = 0xb8000; video_port_reg = 0x3d4; video_port_val = 0x3d5; display_desc = "*CGA/EGA"; } // 设置键盘中断处理函数(IRQ 1) set_trap_gate(0x21,&keyboard_interrupt); // 允许键盘中断 outb_p(inb_p(0x21)&0xfd,0x21); // 清零bit 1 // 清屏 csi_J(2);}
2.7.4 8259A中断控制器配置
所有设备中断都通过8259A中断控制器管理。Linux 0.12使用两个8259A(主片和从片)级联,支持15个IRQ:

中断屏蔽寄存器(IMR)操作:
-
• outb(inb_p(0x21)&~0x01,0x21)– 允许IRQ 0(清零bit 0) -
• outb(inb_p(0x21)&~0x02,0x21)– 允许IRQ 1(清零bit 1) -
• outb(inb_p(0x21)&~0x04,0x21)– 允许IRQ 2(清零bit 2) -
• …
这种位操作方式允许精确控制哪些IRQ被启用,避免不必要的中断。

2.6 进程调度器初始化
进程调度是操作系统的核心功能之一。sched_init()函数负责初始化进程调度系统,它的工作包括:清空task数组、设置进程0的TSS和LDT、设置定时器中断以及加载TR和LDTR寄存器。
task数组是Linux 0.12的进程表,每个元素是一个指向task_struct结构的指针,task_struct描述一个进程的所有信息。
2.4.1 sched_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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 void sched_init(void){ int i;struct desc_struct * p; // 描述符表指针 // 判断系统是否支持64个任务 // 因为每个任务在GDT中需要2个描述符(TSS和LDT) if (sizeof(struct desc_struct) != 8) panic("Bad desc_struct size"); // 设置初始任务(进程0)的TSS和LDT在GDT中的位置 // GDT[4] = 进程0的TSS描述符 // GDT[5] = 进程0的LDT描述符 set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); // 清空GDT中其他任务的TSS和LDT描述符 // 从进程1开始到进程NR_TASKS-1 p = gdt+2+FIRST_TSS_ENTRY; // 跳过进程0的TSS和LDT for(i=1;i<NR_TASKS;i++) { task[i] = NULL; // 进程表指针置空 p->a=p->b=0; // 清空TSS描述符 p++; // 移动到LDT描述符 p->a=p->b=0; // 清空LDT描述符 p++; // 移动到下一个任务的TSS } // 清除NT标志位(Nested Task) // NT=1表示当前任务是嵌套任务,这里要清零 __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 加载任务寄存器TR,指向进程0的TSS // FIRST_TSS_ENTRY << 3 将描述符索引转换为选择符 ltr(FIRST_TSS_ENTRY<<3); // 加载LDT寄存器,指向进程0的LDT lldt(FIRST_LDT_ENTRY<<3); // 初始化8253定时器芯片,使其产生时钟中断 // 定时器通道0,工作模式3(方波发生器) outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */ // 设置定时器计数值 = LATCH (11930) // 8253的输入频率为1.193180 MHz // 计数值11930对应时间 = 11930 / 1193180 ≈ 0.01秒 = 10ms outb_p(LATCH & 0xff , 0x40); /* LSB */ outb(LATCH >> 8 , 0x40); /* MSB */ // 设置时钟中断处理函数 (IRQ 0) // 0x20是8259A中断控制器的IRQ 0对应的中断号 set_intr_gate(0x20,&timer_interrupt); // 取消8259A主片的中断屏蔽寄存器(IMR) outb(inb_p(0x21)&~0x01,0x21); // 清零bit 0,允许IRQ 0(时钟中断) // 设置系统调用门 (INT 0x80) // DPL=3允许用户态程序调用,这是用户程序进入内核的唯一合法途径 set_system_gate(0x80,&system_call);}
2.4.2 TSS和LDT描述符设置
TSS(Task State Segment)是任务状态段,保存了进程的所有寄存器状态。LDT(Local Descriptor Table)是局部描述符表,包含进程的代码段和数据段描述符。
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 33 34 35 36 37 38 39 40 // TSS结构体定义 (include/linux/sched.h)struct tss_struct { long back_link; // 前一个任务的TSS选择符 long esp0; // 内核态栈指针(ring 0) long ss0; // 内核态栈段(ring 0) long esp1; // ring 1栈指针(未使用) long ss1; // ring 1栈段(未使用) long esp2; // ring 2栈指针(未使用) long ss2; // ring 2栈段(未使用) long cr3; // 页目录基地址寄存器 long eip; // 指令指针 long eflags; // 标志寄存器 long eax,ecx,edx,ebx; // 通用寄存器 long esp; // 栈指针 long ebp; // 基址指针 long esi,edi; // 索引寄存器 long es,cs,ss,ds,fs,gs; // 段寄存器 long ldt; // LDT选择符 long trace_bitmap; // 调试位图和I/O位图偏移};// 设置TSS描述符的宏#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")// 设置LDT描述符的宏 #define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")// 实际设置描述符的宏#define _set_tssldt_desc(n,addr,type) \__asm__ ("movw $104,%1\n\t" \ // TSS限长104字节 "movw %%ax,%2\n\t" \ // 基址低16位 "rorl $16,%%eax\n\t" \ // 交换高低16位 "movb %%al,%3\n\t" \ // 基址中8位 "movb $" type ",%4\n\t" \ // 类型字节 "movb $0x00,%5\n\t" \ // 粒度为字节 "movb %%ah,%6\n\t" \ // 基址高8位 "rorl $16,%%eax" \ // 恢复原值 : \ // 无输出 :"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \ "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \ )
TSS与所有其他段一样,由段描述符定义。下图显示了TSS描述符的格式。TSS描述符只能放置在GDT中;他们不可能放置在LDT或IDT中。

2.4.3 进程0的初始化
进程0的task_struct是静态定义的,位于include/linux/sched.h:
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 // 进程0的全局变量定义union task_union {struct task_struct task; // 任务描述符 char stack[PAGE_SIZE]; // 4KB的内核栈};// 进程0的初始化宏定义#define INIT_TASK \/* state etc */ { 0,15,15, \/* signals */ 0,{{},},0, \/* ec,brk... */ 0,0,0,0,0,0, \/* pid etc.. */ 0,-1,0,0,0, \/* uid etc */ 0,0,0,0,0,0, \/* alarm */ 0,0,0,0,0,0, \/* math */ 0, \/* fs info */ -1,0022,NULL,NULL,NULL,0, \/* filp */ {NULL,}, \/* ldt */ {LDT_ENTRY(0x0f,0x00000000,0xfff,0xfa), \ LDT_ENTRY(0x0f,0x00000000,0xfff,0xf2)}, \/* tss */ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\ 0,0,0,0,0,0,0,0, \ 0,0,0x17,0x17,0x17,0x17,0x17,0x17, \ _LDT(0),0x80000000, \ {} \ } \}// 定义进程0struct task_struct * task[NR_TASKS] = {&(init_task.task), };union task_union init_task = {INIT_TASK,};
注意进程0的TSS中:
-
• esp0 = PAGE_SIZE+(long)&init_task: 内核态栈指向init_task的顶部 -
• ss0 = 0x10: 内核数据段选择符 -
• cr3 = (long)&pg_dir: 页目录地址 -
• ldt = _LDT(0): LDT选择符,指向GDT[5]
2.4.4 8253定时器初始化
8253是可编程定时器/计数器芯片,有三个独立的计数器通道。Linux使用通道0产生周期性的时钟中断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 定时器相关常量定义#define LATCH (1193180/HZ) // HZ=100, LATCH=11931.8 ≈ 11930// 8253工作模式:// 0x36 = 00110110b// 00 - 选择通道 0// 11 - 读写方式: 先低字节后高字节// 011 - 工作模式 3: 方波发生器// 0 - 二进制计数(BCD=0)// 时钟中断频率计算:// 输入频率: 1.193180 MHz// 计数值: 11930// 中断频率: 1193180 / 11930 ≈ 100 Hz// 中断周期: 1 / 100 = 10 ms = 1个时间片(jiffy)
8253定时器内部结构图如下所示,它由数据总线缓冲器、读/写控制逻辑、控制字寄存器以及3 个计数器(计数器0、计数器1、计数器2)等组成。

时钟中断的处理函数timer_interrupt定义在kernel/system_call.s中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 时钟中断处理程序_timer_interrupt: push %ds # 保存段寄存器 push %es push %fs pushl %edx # 保存通用寄存器 pushl %ecx pushl %ebx pushl %eax movl $0x10,%eax # 设置内核数据段 mov %ax,%ds mov %ax,%es movl $0x17,%eax # 设置用户数据段(用于fs) mov %ax,%fs incl _jiffies # 系统滴答计数器加1 movb $0x20,%al # 发送EOI(中断结束)到8259A outb %al,$0x20 movl CS(%esp),%eax # 取出被中断程序的CS andl $3,%eax # 检查CPL(当前DPL) pushl %eax # 压栈作为do_timer的参数 call _do_timer # 调用C函数do_timer addl $4,%esp # 清理参数 jmp ret_from_sys_call # 跳转到系统调用返回
do_timer()函数在kernel/sched.c中定义,它会:
-
1. 更新当前进程的时间片计数 -
2. 如果时间片用尽,调用schedule()进行进程切换 -
3. 更新系统时间 -
4. 处理定时器和告警
2.7 缓冲区管理初始化
buffer_init()函数负责初始化buffer cache,即文件系统的缓冲区高速缓存。在Linux 0.12中,所有对块设备的读写都要经过buffer cache,这样可以大大提高磁盘访问效率。
buffer cache由多个buffer_head结构组成,每个buffer_head管理一个1KB的数据缓冲区。buffer_init()根据系统内存大小决定分配多少内存给buffer cache,然后从end_mem向下逐个构建buffer_head,并将它们链接成空闲链表free_list。
2.5.1 buffer_head结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 缓冲区头结构体 (include/linux/fs.h)struct buffer_head { char * b_data; // 指向数据块(1024字节) unsigned long b_blocknr; // 块号 unsigned short b_dev; // 设备号 unsigned char b_uptodate; // 更新标志(数据是否有效) unsigned char b_dirt; // 修改标志(0=干净,1=脏,需要回写) unsigned char b_count; // 使用者计数 unsigned char b_lock; // 锁定标志(0=未锁定,1=已锁定)struct task_struct * b_wait; // 等待该缓冲区解锁的任务struct buffer_head * b_prev; // hash队列中前一块struct buffer_head * b_next; // hash队列中后一块struct buffer_head * b_prev_free;// 空闲表中前一块struct buffer_head * b_next_free;// 空闲表中后一块};
2.5.2 buffer_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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 void buffer_init(long buffer_end){struct buffer_head * h = start_buffer; // 第一个buffer_head位置 void * b; // buffer数据区指针 int i; // 如果buffer_end = 1MB,则b指向640KB处 // 这是因为384KB(0xA0000-0xFFFFF)被显存和ROM BIOS占用 if (buffer_end == 1<<20) b = (void *) (640*1024); // 640KB边界 else b = (void *) buffer_end; // 使用传入的结束地址 // 从高地址向低地址分配buffer // 每个buffer大小为1024字节,需要留出空间给buffer_head结构体 while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) { // 初始化buffer_head h->b_dev = 0; // 设备号=0(未使用) h->b_dirt = 0; // 不脏 h->b_count = 0; // 引用计数=0 h->b_lock = 0; // 未锁定 h->b_uptodate = 0; // 数据无效 h->b_wait = NULL; // 无等待者 h->b_next = NULL; // hash链表为空 h->b_prev = NULL; h->b_data = (char *) b; // 指向数据区 // 将当前buffer_head插入空闲链表的头部 h->b_prev_free = h-1; // 指向前一个buffer_head h->b_next_free = h+1; // 指向后一个buffer_head h++; // 移动到下一个buffer_head // 如果buffer数据区超过640KB且buffer_end=1MB // 则跳过384KB的显存/ROM区域 if (b == (void *) 0x100000) b = (void *) 0xA0000; // 从640KB继续 } // 设置空闲链表的头结点 h--; // 回到最后一个buffer_head free_list = start_buffer; // free_list指向第一个 free_list->b_prev_free = h; // 形成循环双向链表 h->b_next_free = free_list; // 初始化hash表,所有条目置为NULL for (i=0;i<NR_HASH;i++) hash_table[i]=NULL;}
2.5.3 缓冲区分配计算实例
假设buffer_end = 4MB,即分配了4MB给buffer cache:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // buffer_head结构大小sizeof(struct buffer_head) = 32字节// 可用空间 = 4MB - 1MB(内核) = 3MB = 3072KB// 每个buffer占用 = 1024字节(data) + 32字节(header) = 1056字节// buffer数量 = 3072KB / 1.03125KB ≈ 2979个// 内存布局:// 0x100000 (1MB) - 0x3FFFFF (4MB)// 低地址: buffer_head结构数组// 高地址: 1KB数据块,从高向低分配0x100000 [ buffer_head[0..N-1] ] [ 对齐填充 ] [ block[0..N-1] (1KB each) ] 0x3FFFFF └───── 描述符区 ────┘ └─┘ └────────── 数据区 ──────────┘
2.5.4 缓冲区查找和分配
当内核需要读取磁盘某个块时,首先调用getblk()函数查找buffer cache:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 // 获取指定设备和块号的缓冲区struct buffer_head * getblk(int dev, int block){struct buffer_head * tmp, * bh; repeat: // 1. 先在hash表中查找 if (bh = get_hash_table(dev,block)) return bh; // 找到则直接返回 // 2. 未找到,从空闲链表分配 tmp = free_list; do { // 跳过已锁定或正在使用的缓冲区 if (tmp->b_count) continue; // 找到一个可用缓冲区 if (!bh || BADNESS(tmp)<BADNESS(bh)) { bh = tmp; // 选择“最坏”的缓冲区 if (!BADNESS(tmp)) // 如果找到完全干净的,直接退出 break; } } while ((tmp = tmp->b_next_free) != free_list); // 3. 如果没有可用缓冲区,等待 if (!bh) { sleep_on(&buffer_wait); goto repeat; } // 4. 等待缓冲区解锁 wait_on_buffer(bh); // 5. 如果缓冲区已被使用(可能被其他进程分配) if (bh->b_count) goto repeat; // 6. 如果缓冲区是脏的,写回磁盘 while (bh->b_dirt) { sync_dev(bh->b_dev); wait_on_buffer(bh); if (bh->b_count) // 再次检查 goto repeat; } // 7. 从原hash表中移除,初始化 remove_from_queues(bh); bh->b_dev = dev; bh->b_blocknr = block; bh->b_uptodate = 0; bh->b_dirt = 0; bh->b_count = 1; // 设置引用计数 // 8. 插入到新的hash表位置 insert_into_queues(bh); return bh;}
下面以(设备0, 块5)为例,展示缓冲区查找流程。
1 2 3 请求(设备0,块5) ──► 查找缓存 ──┬── 命中 ──► 返回 bh1 │ └── 未命中 ──► 取空闲bh ──► 读磁盘 ──► 填充 ──► 返回
BADNESS宏用于评估缓冲区的“坏”程度:
1 2 3 4 5 6 // 评估缓冲区优先级,值越小越适合释放#define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)// b_dirt=1, b_lock=0: BADNESS=2 (脏的但未锁定)// b_dirt=0, b_lock=1: BADNESS=1 (干净但锁定)// b_dirt=0, b_lock=0: BADNESS=0 (干净且未锁定,最佳)// b_dirt=1, b_lock=1: BADNESS=3 (脏且锁定,最坏)
这种缓存策略称为LRU(Least Recently Used)的变体,优先释放干净的缓冲区,避免磁盘写回开销。
2.8 创建进程1与进入idle循环
在完成所有初始化工作并开启中断后,main()函数通过move_to_user_mode()宏切换到用户态。这个宏通过伪造一个中断返回栈帧,然后执行iret指令,使CPU从特权级0切换到特权级3。此时main()函数继续运行,但已经身处用户态,成为进程0。
紧接着,进程0调用fork()创建子进程,这就是进程1(init进程)。由于此时已经在用户态,fork()会正常使用写时复制机制。进程1执行init()函数,该函数会打开终端设备/dev/tty0作为标准输入输出,然后执行shell程序/bin/sh,从而进入用户交互界面。
2.6.1 move_to_user_mode()宏实现
move_to_user_mode()是一个巧妙的汇编宏,定义在include/asm/system.h中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 移动到用户模式宏// 通过伪造中断返回现场,利用iret指令实现特权级切换#define move_to_user_mode() \__asm__ ("movl %%esp,%%eax\n\t" \ // 保存当前esp到eax "pushl $0x17\n\t" \ // 压入用户数据段选择符ss (0x17=00010111b) // RPL=3(用户态), TI=0(GDT), index=2(数据段) "pushl %%eax\n\t" \ // 压入用户态栈指针esp "pushfl\n\t" \ // 压入标志寄存器eflags "pushl $0x0f\n\t" \ // 压入用户代码段选择符cs (0x0f=00001111b) // RPL=3(用户态), TI=0(GDT), index=1(代码段) "pushl $1f\n\t" \ // 压入返回地址(标号1的位置) "iret\n" \ // 中断返回,从栈中弹出上述5个值 // CPU自动切换到用户态(CPL=3) "1:\tmovl $0x17,%%eax\n\t" \ // 标号1: 已经在用户态 "movw %%ax,%%ds\n\t" \ // 设置用户数据段 "movw %%ax,%%es\n\t" \ // 设置附加段 "movw %%ax,%%fs\n\t" \ // 设置fs段 "movw %%ax,%%gs" // 设置gs段 ::) // 无输入输出操作数
这个宏的关键在于构造了一个完整的中断返回栈帧:
1 2 3 4 5 6 7 8 9 10 11 12 栈帧结构(从高地址到低地址):+-------------------+| ss (0x17) | +16 用户栈段选择符+-------------------+| esp (当前值) | +12 用户栈指针+-------------------+| eflags | +8 标志寄存器+-------------------+| cs (0x0f) | +4 用户代码段选择符+-------------------+| eip (标号1) | +0 返回地址+-------------------+ <--- esp
iret指令执行时会:
-
1. 从栈中弹出eip, cs, eflags -
2. 检查cs的RPL字段,发现是3(用户态) -
3. 继续从栈中弹出esp, ss -
4. 切换到新的特权级,更新CPL=3 -
5. 跳转到cs:eip继续执行
2.6.2 fork()创建进程1
进程0在切换到用户态后立即调用fork():
1 2 3 4 if (!fork()) { // fork()返回0表示子进程 init(); // 子进程(进程1)执行init()}// 父进程(进程0)继续执行
fork()系统调用的实现分为几个层次:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 // 1. 用户态调用 (lib/fork.c)int fork(void){ long __res; // 通过int 0x80触发系统调用 // eax=2表示__NR_fork系统调用号 __asm__ volatile ("int $0x80" : "=a" (__res) // 输出:eax中的返回值 : "0" (__NR_fork)); // 输入:eax=__NR_fork(2) if (__res >= 0) return (int) __res; errno = -__res; return -1;}// 2. 系统调用入口 (kernel/system_call.s)_system_call: cmpl $nr_system_calls-1,%eax // 检查调用号是否合法 ja bad_sys_call push %ds // 保存寄存器 push %es push %fs pushl %edx // 保存参数寄存器 pushl %ecx pushl %ebx movl $0x10,%edx // 设置内核数据段 mov %dx,%ds mov %dx,%es movl $0x17,%edx // 设置用户数据段(fs) mov %dx,%fs call _sys_call_table(,%eax,4) // 调用sys_fork() pushl %eax // 保存返回值 // ... 检查信号和调度 ...// 3. sys_fork实现 (kernel/system_call.s)_sys_fork: call _find_empty_process // 查找空闲进程槽 testl %eax,%eax // 检查返回值 js 1f // 负数表示错误 push %gs // 保存gs pushl %esi // 保存esi pushl %edi // 保存edi pushl %ebp // 保存ebp pushl %eax // 保存find_empty_process返回的pid call _copy_process // 调用C函数copy_process addl $20,%esp // 清理栈1: ret// 4. copy_process实现 (kernel/fork.c)int copy_process(int nr, long ebp, long edi, long esi, long gs, long none, long ebx, long ecx, long edx, long fs, long es, long ds, long eip, long cs, long eflags, long esp, long ss){struct task_struct *p; int i;struct file *f; // 分配一个页面用于task_struct和内核栈 p = (struct task_struct *) get_free_page(); if (!p) return -EAGAIN; task[nr] = p; // 加入进程表 *p = *current; // 复制父进程的task_struct // 修改子进程的特有属性 p->state = TASK_RUNNING; // 设置为运行态 p->pid = last_pid; // 设置新的pid p->father = current->pid; // 设置父进程pid p->counter = p->priority; // 重置时间片 p->signal = 0; // 清除信号 p->alarm = 0; // 清除告警 p->leader = 0; // 不是会话首领 p->utime = p->stime = 0; // 清除时间统计 p->cutime = p->cstime = 0; // 清除子进程时间 p->start_time = jiffies; // 设置启动时间 // 设置TSS(任务状态段) p->tss.back_link = 0; p->tss.esp0 = PAGE_SIZE + (long) p; // 内核栈指针 p->tss.ss0 = 0x10; // 内核栈段 p->tss.eip = eip; // 指令指针 p->tss.eflags = eflags; // 标志寄存器 p->tss.eax = 0; // fork()在子进程中返回0 p->tss.ecx = ecx; // 复制其他寄存器 p->tss.edx = edx; p->tss.ebx = ebx; p->tss.esp = esp; p->tss.ebp = ebp; p->tss.esi = esi; p->tss.edi = edi; p->tss.es = es & 0xffff; p->tss.cs = cs & 0xffff; p->tss.ss = ss & 0xffff; p->tss.ds = ds & 0xffff; p->tss.fs = fs & 0xffff; p->tss.gs = gs & 0xffff; p->tss.ldt = _LDT(nr); // LDT选择符 p->tss.trace_bitmap = 0x80000000; // 调试位图 // 设置子进程的LDT(局部描述符表) set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); // 复制内存页面(写时复制) copy_mem(nr,p); // 复制打开的文件 for (i=0; i<NR_OPEN;i++) if (f=p->filp[i]) f->f_count++; // 返回子进程pid(在父进程中) return last_pid;}
关键点:
-
1. 父进程返回子进程PID:copy_process返回last_pid -
2. 子进程返回0:通过设置 p->tss.eax = 0实现 -
3. 写时复制:copy_mem只复制页表,不复制实际数据
2.6.3 进程1的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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 void init(void){ int pid,i; // 设置终端设备为根文件系统 setup((void *) &drive_info); // 打开/dev/tty0作为标准输入(fd=0) (void) open("/dev/tty0",O_RDWR,0); // 复制文件描述符,作为标准输出(fd=1)和标准错误(fd=2) (void) dup(0); (void) dup(0); // 打印系统信息 printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS, NR_BUFFERS*BLOCK_SIZE); printf("Free mem: %d bytes\n\r",memory_end-main_memory_start); // 创建shell进程 if (!(pid=fork())) { close(0); // 关闭stdin if (open("/etc/rc",O_RDONLY,0)) // 尝试打开启动脚本 _exit(1); // 失败则退出 execve("/bin/sh",argv_rc,envp_rc); // 执行shell _exit(2); // 不应该到达这里 } // 父进程等待子进程 if (pid>0) while (pid != wait(&i)) /* nothing */; // 进入死循环,不断创建shell while (1) { if ((pid=fork())<0) { printf("Fork failed in init\r\n"); continue; } if (!pid) { // 子进程 close(0);close(1);close(2); setsid(); // 创建新会话 (void) open("/dev/tty0",O_RDWR,0); (void) dup(0); (void) dup(0); _exit(execve("/bin/sh",argv,envp)); // 执行shell } while (1) if (pid == wait(&i)) break; printf("\n\rchild %d died with code %04x\n\r",pid,i); sync(); // 同步文件系统 } _exit(0); // 不应该到达这里}
2.6.4 进程0的idle循环
而进程0自己则进入一个for(;;)死循环,不停调用pause()系统调用。pause()会让进程休眠,直到收到信号。实际上进程0的作用是作为idle进程:当系统中所有其他进程都处于等待状态时,调度器会调度进程0运行,让CPU不至于完全空转。
1 2 // main()函数的最后部分for(;;) pause();
pause()系统调用的实现:
1 2 3 4 5 6 7 8 9 10 11 12 // kernel/sys.cint sys_pause(void){ // 设置当前进程为可中断等待状态 current->state = TASK_INTERRUPTIBLE; // 调用调度函数,让出CPU schedule(); // 当进程被唤醒后返回 return 0;}
进程0在调度器中的特殊处理(kernel/sched.c):
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void schedule(void){ int i,next,c;struct task_struct ** p; // 检查所有任务的alarm和信号 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) { // 处理alarm定时器 if ((*p)->alarm && (*p)->alarm < jiffies) { (*p)->signal |= (1<<(SIGALRM-1)); (*p)->alarm = 0; } // 处理阻塞信号 if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && (*p)->state==TASK_INTERRUPTIBLE) (*p)->state=TASK_RUNNING; // 唤醒进程 } // 查找counter值最大的就绪进程 while (1) { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; // 从后向前遍历所有进程 while (--i) { if (!*--p) continue; // 找到counter最大且状态为RUNNING的进程 if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i; } // 如果找到可运行进程(c>0)或者只有进程0(c==0) if (c) break; // 如果所有进程的counter都是0,重新计算所有进程的counter // counter = counter/2 + priority // 这样休眠的进程会累积时间片 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } // 切换到选中的进程 switch_to(next); // 如果所有进程都不可运行,next=0即进程0}
进程0的特殊性:
-
• 进程0的counter总是0(因为它总是在pause()中) -
• 当所有其他进程都不可运行时,schedule()会选择进程0 -
• 进程0不需要时间片,它只是占位符 -
• 这就是为什么注释说「task0 gets activated at every idle moment」
main函数完成初始化后,切换到用户态,成为进程0,进而创建进程1的完整流程如下图所示:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 main() 完成初始化 │ ▼move_to_user_mode() ──► 切换到用户态,成为进程0 │ ▼fork() ──► 创建进程1 │ ├────────────────────────────────────┐ │ │ ▼ ▼┌─────────────────┐ ┌─────────────────┐│ 进程0 │ │ 进程1 │├─────────────────┤ ├─────────────────┤│ 进入 idle 循环 │ │ 执行 init() ││ pause() │ │ │└────────┬────────┘ └────────┬────────┘ │ │ ▼ ▼┌─────────────────┐ ┌─────────────────┐│ pause 设置 │ │ 打开 /dev/tty0 ││ INTERRUPTIBLE │ └────────┬────────┘└────────┬────────┘ │ │ ▼ ▼ ┌─────────────────┐┌─────────────────┐ │ 执行 /bin/sh ││ 调用 schedule() │ └────────┬────────┘└────────┬────────┘ │ │ ▼ ▼ ┌─────────────────┐┌─────────────────┐ │ 用户交互界面 ││ 有其他进程就绪? │ │ (shell 运行) │└────────┬────────┘ └─────────────────┘ │ ┌────┴────┐ │ │ ▼ 是 ▼ 否┌───────┐ ┌─────────────────┐│切换到 │ │ 继续运行进程0 ││其他进程│ └────────┬────────┘└───┬───┘ │ │ │ ▼ │┌─────────────┐ ││进程0等待调度 │ ││(被动等待) │ │└─────────────┘ │ │ │ └──────────┴──► 返回 pause 循环 │ ▼ (重复整个过程)
2.9 实际示例:系统启动的完整流程
为了更直观地理解内核初始化的全过程,我们可以通过一个实际的时间线来追踪系统启动:
-
• 从BIOS加载bootsect到内存0x7C00并执行开始,bootsect将自己移动到0x90000,然后加载setup到0x90200,再加载内核到0x10000。接着bootsect跳转到setup执行,setup读取BIOS提供的硬件参数保存到0x90000开始的区域,然后切换到保护模式并跳转到0x0处(内核被移动到这里)。head.s设置好分页机制和内核栈后跳转到main()。 -
• main()首先从0x90000读取setup保存的参数,比如内存大小8MB存储在EXT_MEM_K中。然后调用mem_init(1MB, 8MB)初始化内存管理,此时mem_map数组被建立,可分配的7MB物理页面被标记为空闲。接着trap_init()设置IDT,blk_dev_init()初始化块设备请求队列,tty_init()初始化终端。 -
• 在sched_init()执行时,进程0的TSS和LDT被加载到GDT的第4和第5项,TR寄存器被设为指向TSS,LDTR指向LDT。定时器被设置为每10ms产生一次IRQ 0中断。buffer_init()根据8MB内存大小分配约2MB空间给buffer cache,建立了大约2000个buffer_head。hd_init()设置硬盘中断处理程序并取消IRQ 14屏蔽。 -
• 随后sti()开启中断,从这一刻起时钟中断开始触发,但由于只有进程0存在且在运行,调度器不会切换进程。move_to_user_mode()执行后,进程0进入特权级3。fork()被调用,进程1诞生,它的task_struct被分配,PID为1,父进程是进程0。进程1执行init()函数打开/dev/tty0并执行/bin/sh,屏幕上出现shell提示符。此时进程0进入pause()循环,当用户在shell中输入命令时,进程1被唤醒执行命令,执行完毕后如果没有其他进程就绪,调度器重新选择进程0运行。
2.10 初始化顺序的依赖关系
内核初始化各个步骤的顺序不是随意的,而是严格遵循依赖关系。mem_init()必须最早执行,因为后续几乎所有初始化都需要分配内存。trap_init()需要在开启中断之前完成,否则一旦有异常发生,CPU找不到处理函数会导致系统崩溃。sched_init()必须在fork()之前执行,因为fork()需要使用进程管理的数据结构。buffer_init()要在文件系统操作之前完成,因为文件系统依赖buffer cache。设备驱动的初始化需要在使用这些设备之前完成,比如init()打开/dev/tty0之前,tty_init()必须已经执行。
这些依赖关系形成了一个有向无环图(DAG),main()函数按照拓扑排序的顺序依次调用各初始化函数。任何一个环节出错,后续的初始化都无法正常进行,系统会在启动阶段就崩溃。这就像盖房子,必须先打地基,再建框架,然后装修,顺序颛倒或跳过某个步骤都会导致整栋楼出问题。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.11 本章小结
通过本章的深入学习,我们完整地剖析了Linux 0.12内核从head.s交接控制权到创建第一个用户进程的整个初始化流程。这个过程展示了操作系统设计中模块化和依赖管理的精妙之处:每个子系统各司其职,按照严格的顺序初始化,最终组合成一个功能完整的多任务操作系统。
核心知识点回顾
1. 内存管理的建立
-
• mem_init()建立mem_map数组管理物理页面 -
• 采用引用计数机制跟踪页面使用 -
• get_free_page()使用高效的汇编指令搜索空闲页面 -
• 内存布局:内核 → buffer cache → 主内存区
2. 中断与异常机制
-
• trap_init()设置IDT中的陷阱门和系统调用门 -
• 区分DPL=0的陷阱门和DPL=3的系统门 -
• INT 0x80作为用户态进入内核的唯一合法途径 -
• 异常处理统一框架:保存现场 → 调用C函数 → 恢复现场
3. 进程调度系统
-
• sched_init()初始化进程0的TSS和LDT -
• 8253定时器产生10ms周期的时钟中断 -
• schedule()基于counter值选择下一个运行进程 -
• 进程0作为idle进程的特殊作用
4. 缓冲区高速缓存
-
• buffer_init()构建buffer_head链表 -
• 双链表结构:hash表+空闲链表 -
• BADNESS宏优先释放干净的缓冲区 -
• LRU变体缓存策略提高磁盘访问效率
5. 设备驱动框架
-
• 块设备和字符设备的区分 -
• 中断驱动的I/O模型 -
• 8259A级联管理15个IRQ -
• 设备初始化模式:注册中断处理函数 → 取消屏蔽
6. 用户态切换技巧
-
• move_to_user_mode()伪造中断返回栈帧 -
• iret指令自动完成特权级切换 -
• fork()在用户态正常使用写时复制 -
• init进程建立用户交互界面
设计哲学
Linux 0.12的初始化过程体现了Linus Torvalds的几个重要设计思想:
-
1. 简洁性:每个初始化函数职责单一,代码简洁明了 -
2. 正交性:各子系统相对独立,接口清晰 -
3. 效率优先:大量使用内联汇编优化关键路径 -
4. 安全第一:严格的特权级检查和错误处理
与现代Linux的对比
虽然Linux 0.12已经非常古老,但其初始化流程的基本思想在现代Linux内核中依然可见:
-
• 设备树(Device Tree):现代替代BIOS参数读取 -
• initramfs:现代的早期文件系统 -
• systemd/init:更复杂的初始化系统 -
• 动态设备管理:udev替代静态设备文件 -
• 多核支持:SMP初始化启动其他CPU
但核心原理未变:先建立内存管理,再设置中断,然后初始化设备,最后创建用户进程。
在后续章节中,我们将深入各个子系统的实现细节,进一步揭示Linux 0.12的设计奥秘。
2.12 参考资料
本章内容基于以下资料编写:
Intel官方文档
-
• Intel 80386 Programmer’s Reference Manual (1986) -
• Chapter 9: Exceptions and Interrupts – 详细描述了IDT和中断处理机制 -
• Chapter 10: Initialization – 介绍了保护模式下的系统初始化过程 -
• 在线阅读:https://css.csail.mit.edu/6.858/2014/readings/i386.pdf
Linux 0.12源代码文件
-
• init/main.c – 内核初始化主函数 -
• mm/memory.c – mem_init()内存管理初始化 -
• kernel/traps.c – trap_init()中断初始化 -
• kernel/sched.c – sched_init()进程调度初始化 -
• fs/buffer.c – buffer_init()缓冲区初始化 -
• kernel/blk_drv/hd.c – hd_init()硬盘驱动初始化 -
• kernel/blk_drv/floppy.c – floppy_init()软盘驱动初始化
“千里之行,始于足下。” —— 老子
夜雨聆风