《深入理解linux内核》学习笔记
本文最后更新于:2026年7月13日 上午
学了就忘()
1.绪论
都是蜻蜓点水的知识没啥看头。。
2.内存寻址
2.1 内存地址
逻辑地址
线性地址(虚拟地址)
物理地址
MMU(分段单元)
2.2 分段
实模式下的分段:
目标地址=段寄存器*0x10+偏移量
在保护模式下,段寄存器不再存放地址,而是存放一个16位的段选择子,作为一个索引指向一个存在GDT(全局描述符表)或者LDT(局部描述符表)的一个表项(段描述符)里面。这个段描述符李保存了关于段的详细属性信息,包括但不限于段基址,长度等信息。
段选择子
大致长这样:
| 索引号 | TI | RPL |
|---|---|---|
| 15-3 | 2 | 1-0 |
RPL两位表示0-3,代表访问该段时使用的特权级,可能权限等于CPL(CPU的特权级),也有可能低于CPL。CPL的特权级存在CS段寄存器的selector(段选择符)中,也就是直接存在了段选择符的RPL里面(复用)。
段描述符
一个长8字节的结构,它描述了段的特征。一般放在GDT(全局描述符表)或者LDT(局部描述符表)中。

其中字段有如下含义
| 字段 | 含义 |
|---|---|
| BASE | 段描述符里面有多个BASE,它们组成了段的起始地址 |
| G | 这个标记位的开启代表段以页(4KB)为单位 |
| Limit | 记录了这个段的末尾元素偏移(相当于长度,取值范围和G挂钩) |
| DPL | 访问这个段需要的最低权限 |
| Type | 该段的类型 |
| S | 这个位表示该段是否为系统段,如果为0,则表明是一个系统段,存放如LDT和TSSD这种数据结构 |
| P | 这个位表示该段现在是否存在内存中,在Linux里这个位一定是1,因为linux从来不会主动把整个段换出主存 |
| B or D | 神奇标记位,在代码段和数据段意义不同 |
| RPL | 访问这个段需要的最低权限 |
| AVL | Linux没有使用这个位 |
两个典型的S为0的段描述符是LDTD和TSSD
快速访问段描述符
6个可编程的寄存器,可以暂存段描述符(在体系结构里应该相当于一种专用的高速缓存)
分段单元
硬件把逻辑地址转化为线性地址的过程:
- 根据段寄存器里的段选择符的TI,确定该段存在GDT还是LDT
- 从gdtr和ldtr里找到GDT或者LDT,然后根据索引取得段描述符
- 从段描述符获得段基址,与偏移地址相加
使用了段描述符相关的可编程寄存器的时候,前两部仅仅在段描述符信息发生改变的时候触发。
linux的分段
linux并未充分地使用分段机制。所有的段基址都是从0开始,长度为整个虚拟地址空间。这就导致逻辑地址的偏移部分其实就是线性地址。常见的Linux的四种段描述符如下:

CPL为3时,各种段寄存器中的段选择子必须指向用户段,同样的,CPL为0时,这些段寄存器中的段选择子必须指向内核段。
Linux GDT & LDT
GDT是对应到CPU的,在多处理器系统中,每个CPU都有一个GDT与之对应。(这是一个值得思考的问题,接下来学习GDT里的细节就可以理解为什么是这样了。)
下面是一个GDT的细节

出了常规的用户代码段和数据段之外,还需要关注:
TSS
这里有疑惑!需要学完进程管理再回来补
2.3 硬件分页
3.进程管理
3.1 进程&线程
什么是进程
进程的创建
线程,为什么线程不能纯用户态进行管理,轻量级线程
线程组
3.2 进程描述符
依旧是最最最最最经典的task_struct:

3.2.1 进程状态
可运行状态
可中断的等待状态
不可中断的等待状态
暂停状态
跟踪状态
僵死状态(EXIT_ZOMBIE)
僵死撤销状态(EXIT_DEAD)
3.2.2 标识进程
进程和进程描述符是一一对应关系,事实上,即便是轻量级进程,作为一个执行上下文也应该有task_struct与之对应。
作为操作系统的用户,更常用的一种标识出一个进程的方法是使用pid。
pid存在进程描述符里,pid取用时逐渐递增,有上限,循环取用。
pidmap_array
多线程应用,pid和tgid(反直觉的getpid和gettid)
进程描述符处理
内核为进程开辟了一个极其紧凑的8K的小空间(一般是连续的两个页面),用于存放内核栈和thread_info结构。其中,后者存在页面的起始位置(栈生长的方向)
空间小是因为内核函数不需要过大的堆栈空间
thread_info和task_struct可以互相指向(.ttask,.thread_info)、
标识当前进程
由于页面连续的特点,可以执行 and ecx,esp (其中ecx为0xffffe000)直接定位到内核数据段的thread_info,因此有专门的current_thread_info宏可以直接获取当前进程的thread_info,而基于此的current宏可以直接根据task指针找到task_struct。
多处理器优势(?)
初见·双向链表list_head
哦天哪是双向链表大人
1 | |
这个结构就是一个双向链表的表头。内核里面各种复杂的双向链表元素都是内含了一个这样的表头,配合各种宏进行操作。(不再需要为不同的结构体设计不同的原语操作了)

新链表是LIST_HEAD(list_name)宏创建的。可以看到表头(list_name)是一个占位的空元素。
一些常用的宏:

其中带有list_entry的会根据结构体名字和内部list_head的偏移,找到包含list_head的结构体的起始地址。而两个for循环宏则是用于遍历链表元素(当成for(xxx;xxx;xxx)用就行,后面加大括号)
另一种双向链表是散列表。
进程链表
task_struct中一个包含了list_head的tasks字段,用来串联起所有的进程描述符
init_task
for_each_process
TASK_RUNNING状态的进程链表
run_list字段,用来串联所有正在运行的进程描述符
进程有不同的优先级,因此要分到多个双向链表,表头由prio_array_t管理:

enqueue_task(p,array)宏,用于把某个进程描述符p插入prio_array_t的某个queue字段(array)里面。
3.2.3 进程间的关系
记录亲属关系的字段
| 字段 | 说明 |
|---|---|
| real_parent | 指向创建了P的进程描述符 |
| parent | ????? |
| children | 指向进程的一个子进程 |
| sibling | 指向兄弟进程的描述符,相当于父进程的children链表的next |
pidhash表及链表
一个哈希表,用来把pid转化到进程描述符
使用哈希表是因为并不是全部可用pid都被使用的,一一对应会浪费很多空间