《Linux 内核设计的艺术》学习笔记
2022/11/16:感觉这种大砖头书一刷不太够,经常看不懂,回头再二刷……
第一章:从开机到执行 main.c
-
开机上电硬件启动 BIOS(此时为 16 位实模式
-
BIOS 执行:
-
加载中断向量表和中断服务程序
-
调中断 int 0x19 加载第一扇区 bootsect.s(引导程序)
-
加载四个扇区和扇区内容到内存
-
-
bootsect.s 执行:
-
调中断 int 0x13 加载 setup.s 程序到内存
-
调中断 int 0x13 加载 system.s 程序到内存
-
-
setup.s 执行:
-
读机器系统数据
-
关中断,将 system 移动到内存起始(覆盖 BIOS 中断 )
-
设置中断描述符表和全局描述符表,将专用寄存器(IDTR, GDTR)指向表
-
打开 A20 实现 32 位寻址
-
重编程两块 8259 芯片将 CPU 工作模式设为保护模式
-
-
head.s 执行:
- 将寄存器转换为保护模式,设置页表,跳入 main.c 程序
第二章:从 main.c 到怠速
-
main.c 执行:
-
设置进程管理数据结构,如进程槽 task[],进程的 task_struct 等
-
复制根设备号、硬盘参数表
-
根据物理内存大小规划使用
-
设置外设虚拟盘
-
初始化内存管理结构
-
挂接异常处理中断
-
准备使进程 0 能在 32 位保护模式下工作
-
初始化块设备请求项管理 request[]
-
挂接串行口、键盘、显示器中断
-
设置开机启动时间
-
-
创建进程 0
-
进程 0 执行:
-
挂接进程 0 的数据结构和全局描述符表
-
设置时钟中断
-
挂接 system_call 和中断描述符表
-
从内核态转换到用户态,调用 fork() 函数创建进程 1
-
在进程槽 task[] 为进程 1 申请空间
-
复制进程信息、管理结构 task_struct 到进程 1
-
设置进程 1 的线性地址空间、物理页面
-
调用系统中断 pause() 切换到进程 1
-
-
进程 1 执行:
-
设置硬盘管理相关数据结构
-
用虚拟盘代替软盘作为根设备
-
用虚拟盘中的数据加载根文件系统
-
从虚拟盘读取根文件系统的超级块,加载到超级块管理结构
-
虚拟盘中读 i 节点,加载到 i 节点管理结构
-
-
进程 1 执行:
-
挂接进程 1 task_struct 的 filp[0] 和文件管理结构 file_table[0]
-
解析
/dev/tty
文件路径 -
找到 dev 目录的 tty0,载入 i 节点表中
-
复制文件句柄建立关系,设定标准终端输出设备为 tty0
-
进程 1 调用 fork() 函数创建进程 2
-
发生时钟中断,中断返回,切换到进程 2
-
-
进程 2 执行:
-
为 shell 程序的执行做配置、环境准备(检测 shell 程序所在文件、调整进程 2 管理信息、地址值,调整 shell 程序第一条指令)
-
执行 shell 第一条指令,引发缺页中断
-
将 shell 程序载入新页
-
-
shell 进程执行:
-
创建新进程 update
-
执行
/etc/rc
文件 -
调用 exit() 退出,切换去执行 update 进程
update 进程的主要任务是定期同步缓冲区和外设,因为 Linux 默认写操作是先写缓冲区再定期写回外设
-
-
重建 shell 进程
-
两次创建的区别:第一次 file[0] 挂载普通文件
/etc/rc
,第二次挂载 tty0 -
自此操作系统支持用户和计算机交互
-
-
第三章:安装文件系统
-
用户在 shell 输入
mount /dev/hd1 /mnt
开始安装文件系统 -
系统调用 sys_mount() (代码见
/fs/super.c
)执行:-
获取硬盘设备号、获取虚拟盘挂接点
-
获取设备文件超级块,载入到超级块管理结构 super_block[8]
-
挂接硬盘设备文件超级块和 mnt 目录文件指定的 i 节点
-
第四章:文件操作
-
文件系统的数据结构:
-
事先把硬盘存储空间人为分成 1KB 的块
-
用 i 节点数据结构管理属于同一个文件的所有块,i 节点和文件 1:1 映射
-
用 i 节点位图管理 i 节点的使用情况
-
用逻辑块位图管理硬盘块使用情况
-
用超级块管理上述 3 种数据结构
-
-
使用方式:
-
找硬盘空闲块:超级块
->
逻辑块位图->
数据块 -
找文件数据块:超级块
->
i 节点位图->
i 节点表中的 i 节点->
数据块
-
第五章:用户进程和内存管理
用户进程的运行
-
创建:
-
调用 fork() 函数,产生 int 0x80 软中断,执行 sys_fork 系统调用
-
再用 find_empty_process() 函数申请进程号和进程槽空位
-
copy_process() 函数复制原进程信息(管理结构、文件结构)并调整当前进程的管理结构
-
copy_mem() 函数复制原进程页表
-
copy_process() 函数关联新进程和全局描述符表 GDT
-
最后 copy_process() 函数将新进程设为就绪态
-
-
加载:
-
调用 execve() 函数进行准备,包括 i 节点读取、参数、环境变量、栈指针,对进程管理结构进行调整,设置 EIP 使进程真正开始执行
-
根据代码需要,在后续执行、加载程序时产生缺页中断,申请新页面并加载要读取的代码
-
-
退出:
-
调用 exit() 函数(映射到系统调用 sys_exit() 函数)
-
进程自身释放进程代码和数据占用的物理内存,解除进程和可执行文件间的关系
-
释放进程管理结构 task_struct 占用的物理内存,退出进程槽
-
多个用户进程在内存中如何同时工作?
-
任何时间都只有一个进程执行,一个进程执行完会调用 shedule() 函数进行切换
-
Linux 页写保护实现:父子进程需要写同一个共享页面时,系统会在主内存申请空闲页面做备份供父进程和子进程分别使用(un_wp_page() 函数)
-
用户进程和内核的隔离:内核中限制了用户进程的线性地址空间只能在 64MB 到 4GB 的范围,不能进入物理内存 0~16MB 的区域。
第六章:多个进程同时操作文件
-
n 个进程操作同一个文件:
-
进程打开文件时调用 open() 函数
- open() 函数映射到 sys_open() 函数,该函数会在 file_table 申请空闲表项目,让进程管理结构和文件 i 节点挂接
-
进程读取文件时调用 read() 函数
-
read() 函数映射到 sys_read() 函数,最后调 bread() 读一个数据块进缓冲区
-
读进缓冲区时调用 wait_on_buffer() 函数挂起这个进程,如果缓冲块已经加锁则进入 sleep_on() 函数,让该进程进入不可中断等待态
调度策略可参考 进程管理:一文读懂Linux内核中的任务间调度策略 ,代码实现看 schedule() 函数和
/kernel/shed.c
的 sleep_on() 函数
-
-
-
进程能直接将数据写入缓冲块的充要条件:缓冲块空闲且没有未写回硬盘的脏数据
- 如果有空闲但未写回的数据,就会先写回
第七章:管道、信号机制
管道
-
代码可见
/fs/pipe.c
-
允许两个进程同时操作一个内存页面,一个写入一个读出
-
本质是一块被操作系统按文件管理的内存页面
信号
-
类似于局部的中断,在进程执行时,一旦其他进程接受到信号,就先暂停去执行这个进程的信号处理
- 发送信号的两种方式:
-
调用特定库函数 signal()
-
用户产生键盘中断
原理都是设置 task_struct 中的成员 signal,signal 按位存储每个进程接收到的信号
-
- 操作系统检测信号的两种方式:
-
在系统调用返回之前检测
-
在时钟中断产生后检测
-
- 发送信号的两种方式:
第八章:操作系统的设计指导思想
主从机制:操作系统和应用程序/操作系统内核和用户进程之间的关系,需要严格约束特权级和边界,进程就是接受操作系统组织、管理、协调的程序