《Linux内核设计与实现》读书笔记

只重点关注了进程,内存,IO部分,日后应用开发可能会遇到的背景知识,没有深究内核数据结构,和它们的具体实现。

进程管理

进程相关的资源描述都保存在 task_struct 结构中, task_struct 的创建和销毁是有slab分配器管理的,同时利用 写时复制 技术,最大限度的减小进程创建的开销

Linux中线程实现为一种特殊的进程,通过和进程类似的创建方式进行创建(通过控制 clone 方法的参数)。
还有一些内核线程在内核空间执行内核制定的后台操作。如 flush

进程树

根进程是PID为1的进程,在系统启动时最先启动的进程。
task_struct 结构中保存了指向父进程的指针和指向子进程的指针链表,逻辑上构成了一个进程树

进程创建

进程创建分为两步:

  1. fork:从父进程拷贝数据
  2. exec:启动进程

进程销毁

进程调用 exit 之后,会释放大部分资源,并且进程状态设置为 EXIT_ZOMBIE,永远不会被CPU调度。但是PID和 task_struct 此时并没有被释放

只有当父进程调用 wait 之后进程的所有独占资源,包括PID和 task_struct 才会真正回收

  1. 孤儿进程:当父进程先于子进程结束,内核需要在父进程的兄弟进程中为那些子进程寻找养父,如果寻找不到,这些子进程就成了孤儿进程,被托管给init进程。
  2. 僵尸进程:当父进程结束时没有调用 wait 就先于子进程结束时,那些子进程就成了没有父进程的僵尸进程,他们占用的PID就永远不会被收回,那些资源又是有限的,这就会对系统资源造成浪费。

进程地址空间


虚拟地址空间由 mm_struct 结构描述;地址空间中的各个段,如代码段,数据段,共享内存段,栈等,由 vm_area_struct 称为VMA。

进程地址空间管理大部分都在与对VMA的操作

VMA的分配与销毁

  1. 创建地址空间:do_mmap()
  2. 销毁地址空间:do_mummap()

系统调用mmap和mummap是对他们的一层封装
do_mmap 的调用参数 file
如果传入参数fd,则表明是一个文件映射。如果传入null,则是匿名映射,扩展堆空间大概就是这么调用的。

系统调用

应用程序 -> API -> 系统调用 -> 内核程序

Unix的接口设计原则:提供机制,而不是策略。对于我们开发中的接口设计也有参考价值。尽量将接口设计成原子性的,基础的操作,强调上层对他们的组合调用。

工作模式

每个系统调用关联一个 系统调用号 ,保存在 sys_call_table 中。
通过软中断,产生异常陷入内核,对应的中断处理程序就是系统调用处理程序。
将中断号和调用参数保存在寄存器中,传递给系统调用。

中断

为了消除处理器和外围硬件设备的速度差异,而采取的一种通信方式
中断号 与特定中断设备相关联,不同的中断又有不同的中断处理程序来响应。
为了使得中断处理程序快速执行,并且完成大量的工作,内核将中断处理分为两部分:

  1. 上半部(top half):响应中断,复位硬件,做简单的操作
  2. 下半部(bottom half):处理复杂的任务

内存管理

Slab分配器


用于内核级的内存管理,主要用于大量的小内存对象的分配和释放。
大多数的内核级结构对象,都是从这里获得的,如上面提到的 mm_structVMA ,还有 bio 等等

虚拟文件系统

VFS提供了一层抽象,一组通用数据结构,一组通用接口,使得用户空间能通过统一的系统调用访问各种文件系统。

  • super:文件系统元数据,super_block。例如磁盘,将文件系统的超级对象存在特定扇区中,文件系统安装时,将这部分信息读取出来,在内存中填充成 super_block 结构
  • inode:文件元数据
  • dentry:目录项
  • file:文件

块设备

块(block),是VSF的最小寻址单位。一个块大小要求是扇区大小(512B)的整数倍,并且小于等于页面大小。通常一个块大小为512B,1KB,4KB,视体系结构而定。

关键数据结构

  1. 缓冲区头:一个块对应一个内存缓冲区,缓冲区头 buffer_head 就用来描述一个缓冲区。它表示的粒度很小
  2. bio:多个片段,每个片段是一小块连续的内存缓冲区。它表示的粒度更大。
    一次IO请求表示为 request,它可能由多个 bio 组成

IO调度

为了优化磁盘的寻址时间,IO调度程序会对到来的IO请求进行 合并排序
因为读请求多是同步的,而写请求可以是异步的,所以优化的时候,首先要保证读不超时。

  1. 老旧的linux电梯算法:简单合并与插入排序,可能会导致饥饿。
  2. deadline:划分读写队列,保证没有请求饥饿,但是频繁的读写响应会降低吞吐量
  3. Anticipatory:在deadline基础上为了提高吞吐量,每次读请求处理完毕之后,不立即返回,而是等待,如果有相邻扇区的IO请求到来,则可以减少寻道时间。
  4. CFQ:为每个进程划分请求队列,轮训调度,保证高负荷的情况下也有很好的表现
  5. Noop:只做合并不做排序,使用与随机读写设备。如SSD

页缓存和回写

页缓存是在磁盘之上的一层内存缓存,顾名思义是以页面为单位的。
读命中,直接从页缓存中取数据;未命中则去磁盘请求,然后更新缓存。

主要的写策略有3种

  1. 缓存穿透,不使用缓存,直接写磁盘
  2. 同步写,先写缓存,再写磁盘,整个过程分为2部分,保证数据的一致性
  3. 异步写,写完缓存就返回。由内核线程将缓存数据和磁盘同步,成为 写回

淘汰策略:
Linux使用修改过的LRU,称为 双链策略
Linux内核的文件Cache管理机制简介

flusher线程写回条件

  1. 内存不足,写回脏页释放内存。阈值通过 dirty_background_ratio 设置
  2. 脏页驻留时间超过阈值。内核线程会被周期性唤醒,写回过期的脏页
  3. 主动调用sync fsync刷盘。

可以通过/proc/sys/vm 设置写回参数

最后附上Linux IO栈: