linux内核初探

内核版本2.6,整理于《linux内核设计与实现》

进程管理

内核把进程的列表存放在叫做任务队列的双向循环链表中,链表中的每一项类型都是task_struct的进程描述符的结构。
linux内核对进程和线程并不特别区别,线程只不过是一种特殊的进程。
fork()系统调用从内核返回两次,一次回到父进程,一次回到新产生的子进程。

进程状态有五种:

  • TASK_RUNNING(运行): 进程是可执行的,它正在执行或在运行队列中等待执行。
  • TASK_INTERRUPTIBLE(可中断): 进程正在睡眠(阻塞),可接收信号。
  • TASK_UNINTERRUPTIBLE(不可中断): 不响应信号。
  • __TASK_TRACED: 被其他进程跟踪。
  • __TASK_STOPPED: 停止执行。通常发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候,此外提示期间接收任何信号也会进入这种状态。

linux的fork使用写时拷贝(Copy-on-Write)。
创建线程和普通进程类似,只不过在调用clone()的时候需要传入一些参数标志来指明共享资源(内存,文件系统,打开的文件)。
进程的终结都是通过exit()系统调用的,进程终结的清理工作和进程描述符的删除被分开执行,这样做是为了让系统在子进程终结后仍能获得它的信息。

进程调度

linux用的调度算法称为完全公平调度算法,简称CFS。
linux采用两种优先级划分。第一种用nice值,范围从-20到+19,nice值越大优先级越低。第二种是实时优先级,实时进程才有,实时优先级高于nice优先级,其值是可配置的,默认从0到99,值越大优先级越高。
CFS并不划分时间片,而是划分处理器的使用比。CFS的做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程。nice值在CFS中被作为进程获得的处理器运行比的权重。任何进程获得的处理器时间是由它自己和其他所有可运行进程nice值相对差值决定的,而不是绝对值。CFS中每个进程获得的时间片底线(最小粒度)是1ms。
CFS使用红黑树来组织可运行进程队列,红黑树是一个自平衡二叉搜索树。
用户抢占可能发生在从系统调用或者中断处理程序返回用户空间时。

内核抢占发生在:

  • 中断处理程序正在执行,且返回内核空间之前。
  • 内核代码再一次具有可抢占性的时候(即安全的情况下,或者说没有锁的情况)。
  • 内核任务显式调用schedule()。
  • 内核任务阻塞(这样也会调用schedule())。

实时调度策略:SCHED_FIFO和SCHED_RR。普通、非实时调度策略:SCHED_NORMAL。
SCHED_FIFO只能被比自己优先级高的进程抢占,否则会一直执行,而SCHED_RR则是加了时间片的SCHED_FIFO,时间片耗完就不能执行了。

系统调用

应用程序(用户空间)->C库(用户空间)->系统调用(内核空间)->内核执行(内核空间)
系统调用是通过软中断来通知内核的,即通过引发一个异常来促使系统切换到内核态去执行异常处理程序,此时的异常处理程序实际上就是系统调用处理程序。
系统调用linux中常称作syscall,每个系统调用都有对应的系统调用号。从用户空间调用系统调用除了库的方式也可以直接调用此系统调用宏的形式。
好处:

  • 创建容易使用方便。
  • linux系统调用性能很高。

问题:

  • 需要系统调用号,由官方分配。
  • 系统调用加入稳定内核后就固化了,不允许改动。
  • 需要将系统调用注册到每个需要支持的结构体系中去。
  • 脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。

替代方法:

  • 应用程序层面实现替代。

内核数据结构

linux最重要的四种数据结构:链表、队列、映射、红黑树

中断与中断处理

异常是同步中断,通常情况下说的中断是异步中断。
中断使得硬件得以发出通知给处理器,不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志。
内核对中断的响应程序叫做中断处理程序。
中断处理分为上半部和下半部,中断处理程序是上半部,接收到中断它会立即执行,能够被允许稍后完成的工作推迟到下半部。上半部通常满足以下情况:1、对时间非常敏感;2、和硬件相关;3、不能被其他中断打断。
下半部实现方式有:软中断、tasklet、任务队列。
软中断用的比较少,tasklet是通过软中断实现的。软中断是在编译期间静态分配的,而tasklet可以动态注册或注销。
软中断最多可能有32个。
如果下半部的任务需要睡眠就可以选择任务队列,不需要睡眠就可以选择软中断或tasklet,追求高性能就选择软中断。因为工作队列把任务交由一个内核线程去执行,这样就会进入到进程上下文中执行,所以工作队列允许重新调度甚至睡眠。软中断和tasklet是在中断上下文中执行的,不能睡眠和重新调度。

内核同步

同步方法:

  • 原子操作
  • 自旋锁:得不到锁的线程会轮询等待。
  • 信号量(Semaphore):信号量可以被多次获得,得不到锁的线程会进入等待队列睡眠。
  • 互斥体(Mutex):并不单指某一种锁,是指所有可以睡眠的强制互斥锁,比如计数是1的信号量。
  • 完成变量:类似于go中的channel。
  • BLK大内核锁:全局自旋锁。
  • 顺序锁:主要依靠一个序列计数器。
  • 禁止抢占
  • 顺序和屏障

内存管理

(看不懂,以后补~~)

虚拟文件系统

虚拟文件系统向上提供统一的接口,向下可以衔接各种文件系统,是文件系统的一层抽象。

虚拟文件系统有四个主要对象(目录是特殊的文件):

  • 超级块对象: 用于存储特定文件系统的信息。
  • 索引节点对象: 包含内核操作文件或目录时需要的全部信息(控制信息、元数据)。
  • 目录项对象: 用于目录相关操作。
  • 文件对象: 文件本身,普通意义上的文件。

块I/O层

扇区是最小物理可寻址单元,一般是2的整数倍,最常见的是512字节。块是最小逻辑可寻址单元,一定是扇区的倍数,但不会超过一页内存页的大小。
内核中块I/O操作的基本容器由bio结构体表示,该结构体代表了正在现场(活动中)以片断链表形式组织的块I/O操作,一个片断是一小块连续的内存缓冲区。

块I/O调度策略:

  • 最终期限I/O调度:给每个I/O请求都设定了一个超时时间,防止饥饿现象。
  • 预测I/O调度:在读请求提交之后会有意等待片刻(默认6ms),以便处理相邻磁盘位置的其他请求。
  • 完全公平排队I/O调度(默认调度策略):把来自不同进程的I/O请求放入不同的队列,然后轮转调度队列,处理队列中的请求(默认处理数目为4)。
  • 空操作的I/O调度:除了合并相邻的I/O请求就没有任何其他操作了。

进程地址空间

进程地址空间有进程可寻址的虚拟内存组成。每个进程(内核线程除外,内核线程没有用户上下文)都拥有自己的进程地址空间,内核使用内存描述符(mm_struct)来表示,给结构包含了和内存地址空间相关的全部信息。
linux中的内存区域通常是指虚拟内存区域(VMA),虚拟内存到物理内存的映射都是通过查询页表来完成,linux中使用三级页表完成地址转换。

页高速缓存和页回写

页高速缓存主要用来减少对磁盘的I/O操作。

写缓存三种策略:

  • 第一种:不缓存,直接写到磁盘。
  • 第二种:写透缓存,同时更新缓存和磁盘。
  • 第三种:回写,写操作直接写入缓存,不立即直接更新磁盘,由专门的回写进程周期将缓存中的数据写入磁盘。触发回写的三种情况:空闲内存低于特定阈值、脏页驻留时间超过特定的阈值、用户进程调用sync和fsync系统调用。

缓存回收策略:最近最少使用(LRU)、双链策略。
内核中管理页高速缓存的结构题是address_space,内核判断页是否存在页高速缓存中的方式是查询address_space中基树。

设备

在linux和所有unix系统中,设备分为以下三种类型:

  • 块设备: 可寻址,以块为单位,支持重定位操作,即对数据的随机访问。
  • 字符设备: 不可寻址,仅提供数据的流式访问,即一个个字符或字节。
  • 网络设备: 提供对网络的访问,通过一个物理适配器和特定协议进行。网络设备打破了unix中“一切皆文件”的设计原则,它不是通过设备节点来访问的,而是通过套接字API这样的特殊接口来访问。
Written on February 14, 2018