《深入理解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(局部描述符表)中。

alt text

其中字段有如下含义

字段 含义
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个可编程的寄存器,可以暂存段描述符(在体系结构里应该相当于一种专用的高速缓存)

分段单元

硬件把逻辑地址转化为线性地址的过程:

  1. 根据段寄存器里的段选择符的TI,确定该段存在GDT还是LDT
  2. 从gdtr和ldtr里找到GDT或者LDT,然后根据索引取得段描述符
  3. 从段描述符获得段基址,与偏移地址相加

使用了段描述符相关的可编程寄存器的时候,前两部仅仅在段描述符信息发生改变的时候触发。

linux的分段

linux并未充分地使用分段机制。所有的段基址都是从0开始,长度为整个虚拟地址空间。这就导致逻辑地址的偏移部分其实就是线性地址。常见的Linux的四种段描述符如下:

alt text

CPL为3时,各种段寄存器中的段选择子必须指向用户段,同样的,CPL为0时,这些段寄存器中的段选择子必须指向内核段。

Linux GDT & LDT

GDT是对应到CPU的,在多处理器系统中,每个CPU都有一个GDT与之对应。(这是一个值得思考的问题,接下来学习GDT里的细节就可以理解为什么是这样了。)

下面是一个GDT的细节

alt text

出了常规的用户代码段和数据段之外,还需要关注:

TSS

这里有疑惑!需要学完进程管理再回来补

2.3 硬件分页

3.进程管理

3.1 进程&线程

什么是进程

进程的创建

线程,为什么线程不能纯用户态进行管理,轻量级线程

线程组

3.2 进程描述符

依旧是最最最最最经典的task_struct:

alt text

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
2
3
struct list_head {
struct list_head *next, *prev;
};

这个结构就是一个双向链表的表头。内核里面各种复杂的双向链表元素都是内含了一个这样的表头,配合各种宏进行操作。(不再需要为不同的结构体设计不同的原语操作了)

alt text

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

一些常用的宏:

alt text

其中带有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管理:

alt text

enqueue_task(p,array)宏,用于把某个进程描述符p插入prio_array_t的某个queue字段(array)里面。

3.2.3 进程间的关系

记录亲属关系的字段

字段 说明
real_parent 指向创建了P的进程描述符
parent ?????
children 指向进程的一个子进程
sibling 指向兄弟进程的描述符,相当于父进程的children链表的next

pidhash表及链表

一个哈希表,用来把pid转化到进程描述符

使用哈希表是因为并不是全部可用pid都被使用的,一一对应会浪费很多空间

3.2.4 如何组织进程

3.3 进程切换

3.4 创建进程

3.5 撤销进程


《深入理解linux内核》学习笔记
http://example.com/2026/07/13/Blog/kernel/深入理解linux内核/
作者
Jmp.Cliff
发布于
2026年7月13日
许可协议