字数 6619,阅读大约需 34 分钟
第五章:Linux 0.12内核体系结构概述
5.1 概述
在深入探索Linux 0.12内核的每一个细节之前,我们需要先站在整体架构的角度了解Linux 0.12内核的全貌。从整体架构的角度来看系统布局、各个子模块以及它们之间的关系,然后再深入具体实现的时候,你就能更好地理解每个细节存在的意义。
Linux 0.12内核就是一座精密的"数字工厂"。它大约包含11000行C代码和汇编代码,看似不多,却已经具备了现代操作系统的完整骨架。本章将为你展示这个内核的整体架构,各个子系统如何组织,它们之间如何协作,以及内核在内存中的布局。掌握了这个整体视角,后续深入学习各个模块时就能够理解它们在整个系统中的位置和作用。
5.2 Linux 0.12的特点与定位
Linux 0.12诞生于1992年1月,是Linux 0.1x系列的最后一个版本,也是通向1.0正式版的重要里程碑。相比最初的Linux 0.01版本(只有约10000行代码,功能非常简单),0.12版本增加了许多重要特性,特别是虚拟内存的交换机制(swap)和更完善的文件系统支持。
与现代Linux内核(超过2500万行代码)相比,Linux 0.12显得非常精简。这种精简带来了重要的学习优势:代码规模适中,一个人可以在几个月内通读全部代码;架构清晰,没有被层层抽象和优化所掩盖;功能完整,已经可以运行真正的应用程序。
Linux 0.12运行在Intel 80386处理器的保护模式下,支持虚拟内存、多任务、进程间通信、文件系统、设备驱动等核心操作系统功能。它采用宏内核(Monolithic Kernel)架构,所有核心功能都运行在内核空间,通过系统调用为用户程序提供服务。这种设计虽然不如微内核那样模块化,但性能优异,也是现代Linux内核继续采用的架构。
5.3 内核整体架构
Linux 0.12内核可以划分为几个主要的子系统,每个子系统负责特定的功能,它们相互协作,共同构成完整的操作系统。
Linux 0.12内核整体架构图从这个架构图可以看出,Linux 0.12内核采用了分层设计。最底层是硬件抽象层,处理中断、异常和时钟等硬件事件。中间层是各个核心子系统——进程管理、内存管理、文件系统和设备驱动,它们实现操作系统的核心功能。最上层是系统调用接口,为用户空间程序提供服务入口。这种分层设计使得内核结构清晰,易于理解和维护。
5.4 源码目录结构
Linux 0.12的源码组织反映了其模块化的设计思想。每个目录对应一个主要的功能模块,目录间的依赖关系清晰明了。就像一个图书馆的分类系统,不同类别的书籍放在不同的书架上,方便查找和管理。
下表详细列出了Linux 0.12源码的目录结构及各目录的功能:
| | |
|---|
| bootsect.S, setup.S, head.s | 引导加载程序,负责系统启动,从实模式切换到保护模式 |
| | |
| sched.c, fork.c, exit.c, signal.c, sys.c, traps.c | |
| hd.c, floppy.c, ll_rw_blk.c, ramdisk.c | |
| tty_io.c, console.c, keyboard.S, serial.c | |
| math_emulate.c, add.c, mul.c, div.c等 | |
| | |
| buffer.c, inode.c, super.c, namei.c, open.c, read_write.c等 | |
| string.c, malloc.c, errno.c等 | |
| | |
| sched.h, fs.h, mm.h, kernel.h等 | |
| io.h, system.h, segment.h, memory.h | |
这种目录结构体现了模块化设计的原则。顶层目录按功能分类(引导、内核、内存、文件系统等),每个目录内部又进一步细分。例如,kernel目录下又分出blk_drv、chr_drv、math三个子目录,分别处理块设备、字符设备和数学运算。这种组织方式使得开发者可以快速定位相关代码,也便于后续的扩展和维护。
5.5 内核模块功能详解
5.5.1 进程管理子系统
进程管理是操作系统的核心功能之一,它如同一个高效的工厂车间调度系统,负责管理多个"工人"(进程)在有限的"工位"(CPU)上轮流工作。Linux 0.12的进程管理子系统主要包含以下功能模块:
进程控制块(task_struct) 是进程的"档案袋",存储了进程的所有重要信息。这个数据结构定义在include/linux/sched.h中,包含进程状态、寄存器值、内存映射、打开的文件、信号处理等信息。每个进程都有一个唯一的task_struct,内核通过这个结构来跟踪和管理进程。
进程调度器(scheduler) 决定下一个运行的进程。Linux 0.12采用简单的时间片轮转算法,每个进程获得一定的时间片(counter值),时钟中断每次触发时减少当前进程的时间片。当时间片用完或进程主动让出CPU时,调度器从所有可运行进程中选择counter值最大的进程运行。这种算法虽然简单,但公平且易于实现,适合Linux 0.12这样的教学系统。
进程创建(fork)和退出(exit) 机制实现了进程的生命周期管理。fork系统调用通过复制父进程创建子进程,采用写时复制(Copy-On-Write)技术延迟内存复制,提高了效率。exit系统调用负责进程的清理工作,包括关闭文件、释放内存、向父进程发送信号等。
信号机制 提供了进程间异步通信的手段。信号就像进程间传递的"纸条",可以通知进程发生了某个事件(如Ctrl+C、段错误、定时器到期等)。进程可以注册信号处理函数,也可以选择忽略某些信号(除了SIGKILL和SIGSTOP)。
5.5.2 内存管理子系统
内存管理子系统如同一个智能的仓库管理系统,负责分配和回收内存资源,确保每个进程都有足够的空间,同时防止进程之间相互干扰。Linux 0.12的内存管理分为物理内存管理和虚拟内存管理两个层次。
物理内存管理使用简单的位图(mem_map)来跟踪物理页的使用情况。每个物理页(4KB)对应位图中的一位,0表示空闲,1表示已使用。get_free_page函数在位图中搜索空闲页并分配,free_page函数释放页面。这种设计简单高效,适合管理有限的物理内存。
虚拟内存管理通过分页机制为每个进程提供独立的4GB虚拟地址空间。页目录和页表构成两级地址转换结构,将虚拟地址映射到物理地址。当访问不在内存中的页面时,触发页错误(Page Fault)异常,操作系统可以从磁盘加载页面(按需分页)或换入已换出的页面。
Linux 0.12特有的交换机制(swap)实现了虚拟内存的核心功能。当物理内存不足时,内核可以将不常用的页面换出到磁盘上的交换分区,释放物理内存供其他进程使用。需要时再从磁盘换入。这个机制就像仓库的"租借服务"——不常用的货物存到外部仓库,需要时再取回,从而使有限的仓库空间可以存放更多货物。
5.5.3 文件系统子系统
文件系统是用户存储和访问数据的主要接口,它将磁盘上的原始数据块组织成用户友好的文件和目录树结构。这就像一个图书馆的管理系统,将大量书籍按照分类、编号组织起来,方便读者查找和借阅。
Linux 0.12采用Minix文件系统,这是一个简单但功能完整的文件系统设计。超级块(super block)存储文件系统的全局信息,如块大小、inode数量、空闲块位图等,就像图书馆的总目录。inode(索引节点)存储文件的元数据,包括文件大小、权限、时间戳和数据块指针,就像每本文的索引卡片。目录是特殊的文件,其内容是目录项列表,每个目录项包含文件名和对应的inode号。
缓冲区缓存(buffer cache)是文件系统性能优化的关键。它在内存中缓存最近访问的磁盘块,避免频繁的磁盘I/O操作。当读取文件时,先检查缓冲区缓存,如果命中则直接返回,否则从磁盘读取并加入缓存。写入时也先写到缓冲区,标记为脏(dirty),后续由内核异步写回磁盘。这种机制就像图书馆的阅览室——常用的书籍放在阅览室,读者可以快速取阅,不常用的书籍存在书库,需要时再取。
虚拟文件系统(VFS)层提供了统一的文件操作接口。虽然Linux 0.12只支持Minix文件系统,但VFS层的设计为将来支持多种文件系统奠定了基础。open、read、write、close等系统调用通过VFS层转换为具体文件系统的操作,应用程序不需要关心底层文件系统的实现细节。
5.5.4 设备驱动子系统
设备驱动是内核与硬件设备之间的桥梁,它将复杂多样的硬件抽象为统一的接口,使得上层代码可以用标准方式访问各种设备。这就像电器的标准插座——不管是什么电器,只要符合插座规格,都可以接入电源使用。
Linux 0.12将设备分为三类:字符设备、块设备和网络设备。字符设备以字节流方式访问,如终端、键盘、串口。块设备以固定大小的块(通常是512字节或1024字节)访问,如硬盘、软盘。网络设备在Linux 0.12中尚未完全实现。
字符设备驱动包括TTY(终端)驱动、控制台驱动、键盘驱动和串口驱动。TTY驱动提供了行缓冲、回显、信号生成等终端功能,是用户与系统交互的主要接口。控制台驱动管理文本模式的显示输出,处理光标移动、颜色设置、滚动等操作。键盘驱动响应键盘中断,将扫描码转换为ASCII字符,支持组合键和功能键。
块设备驱动包括硬盘驱动、软盘驱动和RAM盘驱动。硬盘驱动控制IDE硬盘,处理读写请求,实现数据传输。软盘驱动较为复杂,需要处理软驱电机控制、磁道寻址、DMA传输等细节。RAM盘驱动在内存中模拟磁盘,提供高速的块设备接口,常用于存放临时文件或作为根文件系统。
块设备驱动采用请求队列机制优化I/O性能。多个读写请求不是立即执行,而是放入请求队列,内核可以对请求进行排序(电梯调度算法),减少磁盘寻道时间,提高整体吞吐量。这就像电梯的运行策略——不是响应每个按钮就立即改变方向,而是沿着一个方向依次停靠各楼层,提高了运输效率。
设备驱动结构图5.6 内存布局
理解内核在内存中的布局对于理解系统的运行机制至关重要。Linux 0.12的内存布局是精心设计的,不同功能的代码和数据被放置在不同的内存区域,既保证了系统的安全性,又优化了访问效率。
5.6.1 物理内存布局
在80386保护模式下,Linux 0.12可以访问最多4GB的物理内存。但在实际中,1992年的典型PC机只有4MB或8MB内存。Linux 0.12的物理内存布局针对这种环境进行了优化。
低端1MB内存区域有特殊用途。
- • 0x00000-0x003FF(1KB)是BIOS中断向量表,用于实模式中断处理。
- • 0x00400-0x004FF(256字节)是BIOS数据区。
- • 0x00500-0x07BFF(约30KB)在引导阶段用于存放setup程序获取的硬件信息。
- • 0x07C00-0x07DFF(512字节)是引导扇区加载位置,BIOS从磁盘读取第一个扇区到这里并执行。
- • 0x08000-0x0FFFF(32KB)用于存放setup程序代码。
- • 0x10000-0x8FFFF(512KB)用于存放system模块(内核)的临时位置,后续会被移动到0x00000开始的位置。
- • 0x90000-0x9FFFF(64KB)是显存映射区域(VGA显示缓冲)。
- • 0xA0000-0xFFFFF(384KB)是BIOS ROM和扩展卡ROM区域。
1MB以上的内存区域用于内核和用户程序。0x100000(1MB)开始是内核代码和数据的最终位置。内核大约占用几百KB空间。内核之后的内存用于存放进程的页表、内核数据结构、缓冲区缓存等。剩余的高端内存分配给用户进程使用。
低端1M内存区域下表总结了关键的物理内存区域:
5.6.2 虚拟内存布局
在启用分段和分页机制后,Linux 0.12采用分段+分页的两级内存管理模型。首先,通过分段机制将4GB线性地址空间划分为64个64MB的槽位,每个进程独占一个槽位。然后通过分页机制将线性地址映射到物理内存。
内核空间占据线性地址空间的前16MB(0x00000000-0x00FFFFFF),采用恒等映射方式直接对应物理内存的前16MB。这意味着线性地址与物理地址相等(如线性地址0x00100000对应物理地址0x00100000)。这种设计简化了内核访问自己的代码和数据,也便于进程通过系统调用进入内核。
每个进程的用户空间是其在4GB线性地址空间中所占的64MB槽位。假设进程的槽位号为n,则其线性地址范围为 n×64MB 到 (n+1)×64MB - 1,虚拟地址(段内偏移)范围为 0 到 64MB-1。不同进程的虚拟地址虽然都是从0开始,但通过分段单元加上不同的段基址后,会映射到不同的线性地址区域,再通过各自独立的页表映射到不同的物理内存,从而实现进程隔离。用户空间通常包括代码段(text)、数据段(data)、BSS段、堆(heap)和栈(stack)。
代码段存放程序的可执行指令,通常是只读的。数据段存放已初始化的全局变量和静态变量。BSS段存放未初始化的全局变量和静态变量,这个名字来源于早期汇编语言的"Block Started by Symbol"。堆向高地址增长,用于动态内存分配(malloc)。栈向低地址增长,用于函数调用和局部变量。堆和栈相向增长,中间的空闲区域提供了扩展空间。
5.7 系统调用接口
系统调用是用户空间程序访问内核服务的唯一正当途径,它就像银行的业务窗口——客户(用户程序)不能直接进入金库(内核)操作,必须通过柜台(系统调用)提出请求,由银行职员(内核)代为执行。这种设计保证了系统的安全性和稳定性。
Linux 0.12通过int 0x80中断实现系统调用。用户程序将系统调用号放入EAX寄存器,参数放入EBX、ECX、EDX等寄存器,然后执行int 0x80指令。CPU从用户态(特权级3)切换到内核态(特权级0),跳转到系统调用处理程序。内核根据系统调用号查找系统调用表,调用相应的内核函数。函数返回后,结果放入EAX寄存器,CPU通过iret指令返回用户态,用户程序继续执行。
Linux 0.12定义了约70个系统调用,涵盖进程管理、文件操作、内存管理、设备控制等各个方面。下表列出了一些重要的系统调用及其功能:
| | | |
|---|
| | | |
| | | |
| | | int fd, char *buf, int count |
| | | int fd, char *buf, int count |
| | | char *filename, int flags, int mode |
| | | |
| | | char *filename, char **argv, char **envp |
| | | |
| | | int fd, off_t offset, int whence |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
系统调用的实现涉及特权级切换、参数传递、返回值处理等复杂机制。让我们通过一个简单的例子来理解系统调用的完整流程:
// 用户程序调用write系统调用int fd = 1; // 标准输出char *buf = "Hello, World!\n";int count = 14;int ret = write(fd, buf, count);// write函数实际上是一个封装宏,展开后:int ret;__asm__ volatile ( "int $0x80" : "=a" (ret) : "0" (4), // 系统调用号4(__NR_write) "b" (fd), // 第1个参数 "c" (buf), // 第2个参数 "d" (count) // 第3个参数);
这段代码执行int 0x80中断,CPU切换到内核态,跳转到系统调用处理程序system_call。内核从EAX读取系统调用号4,在系统调用表中查找对应的sys_write函数。sys_write函数从EBX、ECX、EDX读取参数,执行实际的写操作。操作完成后,将结果写入EAX,通过iret返回用户态。用户程序从EAX读取返回值。
这个过程虽然看起来复杂,但大部分由硬件和内核自动完成,用户程序只需要调用封装好的库函数。这种封装就像使用ATM机取款——用户只需按几个按钮,背后的复杂过程(验证身份、检查余额、扣款、出钞)由银行系统自动处理。
5.8 内核初始化流程
理解内核的初始化流程,就像理解一座工厂的开工流程——哪些设备先启动,哪些后启动,各个系统如何协调配合,最终达到正常运转状态。Linux 0.12的初始化过程分为引导阶段和内核初始化阶段两个大的步骤。
引导阶段由BIOS、bootsect、setup和head程序完成。BIOS加电自检后,从启动设备(软盘或硬盘)读取第一个扇区(bootsect)到内存0x7C00并执行。bootsect将自己移动到0x90000,然后从磁盘读取setup程序到0x90200,再读取system模块(内核)到0x10000。setup程序获取硬件信息(内存大小、显示模式、硬盘参数等),关闭中断,将system模块移动到0x00000,切换到保护模式,跳转到head程序。head程序设置GDT、IDT,建立分页机制,最后跳转到main函数。
内核初始化阶段由main函数统一协调。main函数位于init/main.c,它是内核真正的"主程序"。main函数的执行流程体现了系统初始化的逻辑顺序:首先初始化内存管理(mem_init),建立物理内存位图;然后初始化异常处理(trap_init),设置异常处理程序;接着初始化块设备(blk_dev_init)和字符设备(chr_dev_init),准备I/O子系统;初始化TTY驱动(tty_init)、时钟中断(time_init)、调度器(sched_init);最后创建第一个用户进程(init进程)并切换到用户模式。
init进程是所有用户进程的祖先,它的PID为1。init进程首先挂载根文件系统,然后打开控制台设备,最后fork出shell进程供用户交互。至此,系统完成初始化,进入正常运行状态。
5.9 本章总结
本章从整体视角俯瞰了Linux 0.12内核的架构。我们了解到Linux 0.12虽然代码量不大,但已经具备了现代操作系统的完整骨架,包括进程管理、内存管理、文件系统、设备驱动等核心子系统。
内核采用模块化设计,源码目录结构清晰,每个目录对应一个主要功能模块。进程管理子系统负责进程的创建、调度和销毁,实现多任务并发。内存管理子系统通过物理内存管理和虚拟内存管理两个层次,为进程提供独立的地址空间,支持按需分页和页面交换。文件系统子系统采用Minix文件系统,通过VFS层提供统一的文件操作接口,缓冲区缓存优化了磁盘I/O性能。设备驱动子系统将各种硬件抽象为统一的接口,包括字符设备和块设备两大类。
内存布局体现了系统的设计思想。物理内存低端1MB区域有特殊用途,高端内存用于内核和用户进程。虚拟内存空间划分为内核空间和用户空间,内核空间在所有进程间共享,用户空间是进程私有的,实现了进程隔离。
系统调用是用户程序访问内核服务的接口,通过int 0x80中断实现特权级切换。内核提供了约70个系统调用,涵盖系统的各个方面。系统调用机制保证了系统的安全性——用户程序不能直接访问内核,必须通过受控的接口。
内核初始化流程展示了系统如何从关机状态启动到正常运行。引导阶段完成硬件初始化和模式切换,内核初始化阶段按照依赖关系依次初始化各个子系统,最后创建init进程并切换到用户模式,系统进入就绪状态。
掌握了这个整体架构,我们就拥有了一张内核的"地图"。在后续章节中深入学习各个子系统时,你可以随时参考这张地图,了解当前模块在整个系统中的位置和作用。这种整体视角将帮助你更好地理解细节,也能让你看到设计者的巧思——各个模块如何协同工作,共同构建出一个完整的操作系统。
本章思考
- 1. Linux 0.12采用宏内核架构,所有核心功能都在内核空间运行。微内核架构则将大部分服务移到用户空间,只保留最小的内核。这两种架构各有什么优劣?在什么场景下微内核更有优势?
- 2. 系统调用通过int 0x80中断实现,涉及用户态和内核态的切换,这个过程有一定的开销。为什么不让用户程序直接调用内核函数,而要通过中断这种间接方式?这种设计体现了什么样的安全原则?
- 3. Linux 0.12的虚拟内存布局中,内核空间在所有进程间共享,这简化了内核访问自己的代码和数据。但这也意味着内核空间对所有进程可见(虽然普通进程无法访问)。现代操作系统如何进一步加强内核空间的隔离和保护?
参考资料
- • Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3 (System Programming Guide)
- • 第1章:System Architecture Overview
- • 1.2节:Overview of the System-Level Architecture
- • 1.3节:Memory Organization
- •
include/linux/sched.h:进程管理相关定义 - •
include/linux/fs.h:文件系统相关定义 - •
include/linux/mm.h:内存管理相关定义
- • oldlinux.org:Linux 0.01-0.12历史资料
- • https://www.kernel.org/:Linux内核官方网站
"不识庐山真面目,只缘身在此山中。"——苏轼《题西林壁》