☰
Linux 0.11 fork源码逐行拆解:进程复制与TSS切换的奥秘
2026/10/21 13:57:23 网站建设 项目流程

Linux 0.11 的 fork() 源码,放在今天看依然是一份很值得反复读的教材。它没有现代内核里那些复杂的调度域、cgroups、RCU 锁,整个进程创建路径短到让人感动,但该有的东西一样不少:int 0x80 中断入口、寄存器快照传递、任务结构体复制、页表复制、文件描述符表复制,最后再用一个巧妙的 TSS 构建让父子进程从同一位置“分道扬镳”。这篇文章不打算泛泛而谈,而是直接进入源码,一行一行拆给你看,适合已经学过操作系统概念、想真正打开内核看细节的人,也适合正在读内核源码但被各种宏和结构体绕晕的新手。

1. 整体设计思路:先想清楚 fork 到底要解决什么问题

1.1 fork 的核心任务拆解

fork() 要做的事情,用一句话概括就是:根据当前进程(父进程)完整复制出一个新进程(子进程),并且让这两个进程从 fork() 返回的位置继续各自执行,只是返回值不同。

这个概念听起来很简单,但落到 x86 内核实现上,就引出了一连串问题:

  • 新进程的 task_struct 放哪里?从哪里分配内存?
  • 子进程的代码段、数据段、堆栈怎么映射?是直接复制物理内存,还是先共享页表?
  • 文件的打开状态(fd)怎么继承?引用计数怎么处理?
  • 最关键的是:子进程第一次被调度执行时,CPU 怎么知道从哪条指令开始跑?怎么让子进程认为自己是从 fork() 返回的?
  • 最后,为什么父子进程返回的值不一样?

Linux 0.11 的设计思路非常朴素:把一个进程在 CPU 上的所有“现场”(寄存器、栈指针、代码指针、段寄存器)全部保存进任务结构体的 TSS 段中,然后让新进程复用这套现场,稍加修改,最后通过任务切换机制让子进程从修改过的现场恢复执行。整个 fork 过程本质上就是在做“现场的复制与篡改”。

1.2 一次 fork() 调用在内核中的完整路径

在进入源码之前,先在脑子里建立一条完整路径图,后面看代码就不会迷路:

用户态调用 fork() → libc 封装:调用 int 0x80,系统调用号放入 eax → CPU 自动压栈 ss/esp/eflags/cs/eip,切换到内核态 → system_call 汇编入口:继续压栈 ds/es/fs/edx/ecx/ebx → 通过 sys_call_table 跳转到 sys_fork() → sys_fork 调 find_empty_process() 分配进程槽位和 pid → sys_fork 把 gs/esi/edi/ebp/nr 压栈后调用 copy_process() → copy_process 分配新的 task_struct,复制父进程内容 → copy_mem() 复制 LDT 和页表 → 复制文件表、信号处理、tty 等外围资源 → 修改子进程 TSS 中的 eip/esp/eax 等字段 → 返回 last_pid,沿着中断返回路径释放所有压栈内容 → 回到用户态:父进程 eax = 子进程 pid,子进程 eax = 0

这条路径里,每一步都依赖前一步。后面的源码分析其实就是把这几个环节逐一点亮。

1.3 需要提前备好的几个基础概念

读这段源码前有三个概念最好先弄明白,否则很容易卡壳。

第一,GDT 和 LDT 的关系。Linux 0.11 是段页式内存管理,每个进程在 GDT 里有两个表项指向自己的 LDT,而 LDT 里又定义了代码段(索引1)和数据段(索引2)。这就是为什么后面 copy_mem 要同时处理 LDT 和页表,段基址变了,页表才能跟着重新定位。

第二,TSS(任务状态段)。x86 CPU 天生支持硬件任务切换,每个进程都有一个 TSS,里面保存了全部的寄存器现场。Linux 0.11 干脆把进程的 CPU 上下文就放在 task_struct 内部的 tss 字段里。tr 寄存器指向当前任务的 TSS,每次切换任务时就恢复新 TSS 里的寄存器。

第三,内核对用户内存的映射方式。0.11 中每个进程的逻辑地址空间大小是 64MB,物理内存则通过页表映射。代码段和数据段的基址不是从 0 开始,而是按进程槽位错开:第 n 个进程的段基址是 n × 64MB。这个设计让页表复制变得非常规整。

2. 走进入口:从 libc、中断到 sys_fork 的寄存器搬运

2.1 系统调用号与 int 0x80 中断入口

用户态调用 fork() 时,实际控制权会交给 libc 的封装函数。它的逻辑可以简化为:

#define __NR_fork 2 int fork(void) { long ret; __asm__ volatile( "int $0x80\n" : "=a" (ret) : "0" (__NR_fork) : "memory" ); return ret; }

关键动作就两个:往 eax 里放系统调用号 2,然后执行 int 0x80。从这之后,CPU 开始主动干活:它会把用户态的 ss、esp、eflags、cs、eip 依次压入内核栈,然后根据 IDT 表中 0x80 对应的门描述符跳转到内核的 system_call 入口。

为什么要用中断而不是直接 call 函数?因为用户态没有权限访问内核代码段,必须通过中断门完成特权级切换,从特权级 3 跳到特权级 0。CPU 压栈的那五个值,就是这次切换的“入场券”,它们让内核知道用户程序执行到哪了、栈在哪、标志位是什么。

2.2 system_call 与 sys_fork 的寄存器快照

进入 system_call 入口之后,内核自己又压了一批寄存器。下面这段是 0.11 的核心汇编,去掉了信号处理等次要逻辑,保留主干:

_system_call: cmpl $nr_system_calls-1, %eax # 系统调用号越界检查 ja bad_sys_call push %ds push %es push %fs pushl %edx pushl %ecx pushl %ebx call sys_call_table(,%eax,4) # 跳转到对应系统调用处理函数

这里压栈的顺序是有讲究的。ds/es/fs 是段寄存器,edx/ecx/ebx 是通用寄存器,加上前面 CPU 压入的 cs/eip/eflags/esp/ss,等到 sys_fork 真正执行时,内核栈上几乎保存了用户态的全部执行现场。这就是后面 copy_process 能拿到“寄存器快照”的原因。

sys_fork 本身也用汇编写了一段,它首先调用 find_empty_process 找一个空闲的进程槽位:

_sys_fork: call _find_empty_process testl %eax, %eax js 1f # 返回负数说明分配失败,直接返回 push %gs pushl %esi pushl %edi pushl %ebp pushl %eax # 进程槽位作为 copy_process 第一个参数 call _copy_process addl $20, %esp # 清理 5 个参数 1: ret

这里可以注意到,sys_fork 压栈的是 gs、esi、edi、ebp,而不是 ebx、ecx、edx。原因是 ebx/ecx/edx 早在 system_call 入口就已经压过了,此时栈里的顺序恰好能组成 C 函数 copy_process 需要的参数列表。通过这种压栈顺序的拼接,内核把“CPU 现场”完整传给了 C 函数,整个过程没有任何额外拷贝。

提示:这里有个容易混淆的地方。copy_process 的参数虽然名字叫 ebx、ecx、edx,但它们不是指这些寄存器本身传值,而是指从栈上取出的、中断发生时这些寄存器的快照值。理解这一点后,再看后面的 tss.eip = eip 就不会犯迷糊了。

2.3 last_pid 与 find_empty_process:进程号怎么发出去

进程号的分配在 0.11 里不是原子操作,也没有位图或 idr 机制,就是一个全局变量 last_pid 加一次全表扫描。代码思路很直接:

int find_empty_process(void) { int i; repeat: if ((++last_pid) & 0xffff0000) last_pid = 1; for (i = 0; i < NR_TASKS; i++) if (task[i] && task[i]->pid == last_pid) goto repeat; for (i = 0; i < NR_TASKS; i++) if (!task[i]) return i; return -EAGAIN; }

NR_TASKS 在 0.11 中是 64,也就是说系统最多同时存在 64 个进程。last_pid 递增到 0xffff0000 时会被拉回 1 重新开始,这是为了避免 pid 过大影响后面的段基址计算——毕竟进程的虚拟段基址是 pid × 64MB,如果 pid 太大,地址空间直接就越界了。

find_empty_process 返回的是 task[] 数组的索引,这个索引就是新进程的 slot 号,后面 copy_mem 计算新进程段基址时也会用到它。这里还能看到:PID 的回收和槽位的复用,在 0.11 里是完全分离的两件事。某个进程退出后,它的 task[i] 会被置空,但 last_pid 不会回退,所以新进程拿到的 pid 一定比之前大,除非发生 32 位回绕。

3. 核心函数 copy_process 逐段拆解

3.1 第一步:分配 task_struct,并复制父进程的“灵魂”

copy_process 是 fork 的真正心脏。它的前半段做的事情可以概括为:从内核内存中拿一页物理内存(4KB)作为新进程的 task_struct,然后把当前进程的内容整体复制过去,再清掉那些子进程不应该继承的字段。

int copy_process(int nr, long ebx, long ecx, long edx, long orig_eax, long fs, long es, long ds, long cs) { struct task_struct *p; int i; struct file *f; p = (struct task_struct *) get_free_page(); if (!p) return -EAGAIN; // 内核内存不足,返回“资源暂时不可用” *p = *current; // 整个 task_struct 结构体直接赋值

这段代码最震撼的就是*p = *current。它把当前进程的 task_struct 按字节全部复制过去,包括信号处理表、tty、umask、文件描述符表、LDT 和 TSS。也就是说,子进程一出生就拥有和父进程完全一样的“配置”。但有些字段必须马上改,否则子进程就是个不折不扣的克隆人:

p->pid = nr; p->father = current->pid; p->counter = current->counter; p->priority = current->priority; p->signal = 0; // 子进程不能继承未决信号 p->alarm = 0; p->utime = p->stime = p->cutime = p->cstime = 0; // 时间统计清零 p->start_time = jiffies; // 记录创建时刻

这里有个很微妙的点:p->counter 复制了父进程剩余的时间片。这会导致子进程一出生就能接着用父进程没用完的时间片,而不是重新分一个满额时间片。这个设计在现代内核里已经变了,子进程通常继承的是优先级而不是剩余时间片,但在 0.11 这种极简内核里,直接复制是最自然的做法。

3.2 第二步:copy_mem,把父进程的地图变成子进程的

task_struct 复制完之后,新进程在逻辑上已经有了同样的 LDT,但它的段基址还是父进程的,两者映射到同一段物理内存,这可不行。copy_mem 干的事情就是:给子进程分配新的段基址,并复制页表。

int copy_mem(int nr, struct task_struct *p) { unsigned long old_data_base, new_data_base, data_limit; old_data_base = get_base(current->ldt[2]); // 父进程数据段基址 data_limit = get_limit(current->ldt[2]); // 数据段限长 new_data_base = nr * 0x4000000; // 子进程基址 = 槽位 × 64MB p->ldt[1] = set_base(current->ldt[1], new_data_base); // 重设代码段基址 p->ldt[2] = set_base(current->ldt[2], new_data_base); // 重设数据段基址 if (copy_page_tables(old_data_base, new_data_base, data_limit)) return -1; return 0; }

为什么是 64MB?因为 0.11 默认每个进程最多能使用 64MB 地址空间,而页目录有 1024 项,每一项对应 4MB。64MB 刚好是 16 个页目录项。用 pid 乘 64MB 作为 LDT 基址,让不同进程的逻辑地址在“段基址”这一层就彻底错开了,互不重叠。

copy_page_tables 是真正干体力活的地方。它的作用是把父进程的页表内容复制到子进程的新页表中:

int copy_page_tables(unsigned long from, unsigned long to, long size) { unsigned long *from_page_table, *to_page_table; unsigned long this_page; int i; from &= 0xfffff000; to &= 0xfffff000; size &= 0xfff000; for ( ; size > 0; size -= 0x400000) { // 每次处理 4MB from_page_table = (unsigned long *)((from & 0xffc00000) + 0xffc00000 + ((from >> 10) & 0xffc)); to_page_table = (unsigned long *) get_free_page(); // 新页表占一页 if (!to_page_table) return -1; for (i = 0; i < 1024; i++, from_page_table++, to_page_table++) { this_page = *from_page_table; if (!this_page) continue; if (this_page & 0x4) { // 用户页:父子设置成只读共享 this_page &= ~0x2; *from_page_table = this_page; *to_page_table = this_page; } else { *to_page_table = this_page; } } } return 0; }

这里的核心思路在 if (this_page & 0x4) 分支里:对于用户进程的页,它不是把物理内存真的复制一份,而是把父进程的页表项复制到子进程的页表,同时把页表项的写权限位清掉。也就是说,父子进程此刻共享同一批物理页,但这些页对谁都是只读的。

写保护位清掉之后,一旦父子进程中有谁要往共享页里写数据,就会触发缺页异常。但 0.11 的缺页处理里并没有实现写时复制(COW),所以这个只读位实际上是一种“临时保护”。真正落地的 COW 是在后续版本才完善的。理解这一点很重要:0.11 的 fork 属于“共享页表 + 写保护预埋”,它已经有了 COW 的雏形,但还没有 COW 的完整闭环。

注意:copy_page_tables 在复制页表项时,对内核页(非用户页)是直接复制页表项,不改写权限。因为内核空间是所有进程共享的,内核页本来就不需要按进程隔离。

3.3 第三步:外围资源复制与引用计数

内存搞定了,接下来是文件、当前目录、根目录这些资源。这部分代码不长,但每一行都隐含一个设计决策:

for (i = 0; i < NR_OPEN; i++) if ((f = p->filp[i])) f->f_count++; // 每复制一个文件描述符,引用计数 +1 if (current->pwd) current->pwd->i_count++; // 当前目录的 inode 引用计数 +1 if (current->root) current->root->i_count++; // 根目录引用计数 +1 if (current->executable) current->executable->i_count++; // 可执行文件引用计数 +1

为什么要复制文件表?因为子进程继承父进程已经打开的文件描述符,这是 POSIX 规定的行为。但继承不应该是简单地赋值,因为父进程和子进程是各自独立的 task_struct,它们指向同一个 file 结构。如果有另一个进程 close 了这个文件,系统要在最后一个引用者释放时才真正关闭文件。这就是 f_count 引用计数的意义。

实战心得:看这里的代码时,重点不是记住每一条赋值语句,而是理解“引用计数”这个概念贯穿了整个 Linux 文件系统的生命周期。后面你在分析 close、exit、dup 时都会反复遇到它。

3.4 第四步:精心“篡改” TSS,让子进程醒来时不迷路

这一步是整个 fork 源码最精彩的部分。前面复制的 TSS 还是父进程的现场,但如果子进程第一次被调度时直接恢复这套现场,那它就会像一个“复活的父进程”,连 eax 里放的系统调用号都一样,根本不知道自己是子进程。内核必须做几处关键的修改:

p->tss.esp0 = PAGE_SIZE + (long) p; // 子进程内核栈 p->tss.ss0 = 0x10; // 内核数据段选择子 p->tss.eip = eip; // 返回地址:int 0x80 的下一条指令 p->tss.esp = esp; // 用户栈指针:与父进程一致 p->tss.eax = 0; // 返回值:子进程拿到 0 p->tss.ecx = ecx; p->tss.edx = edx; p->tss.ebx = ebx; p->tss.esi = esi; p->tss.edi = edi; p->tss.ebp = ebp; p->tss.cs = cs & 0xffff; p->tss.ss = ss & 0xffff; p->tss.ds = ds & 0xffff; p->tss.es = es & 0xffff; p->tss.fs = fs & 0xffff; p->tss.gs = gs & 0xffff; p->tss.ldt = _LDT(nr); // 切换 LDT 到子进程自己的表

这里每个赋值都有它的意义:

  • eip:子进程恢复时从这个地址开始取指令。因为 fork 是通过 int 0x80 进入内核的,执行完中断返回时 CPU 会恢复用户态的 eip,也就是 int 0x80 的下一条指令。所以子进程“醒来”后,不会重新进入内核,而是直接接着父进程的用户代码继续执行。
  • esp:子进程和父进程的用户栈完全一样。这很自然,子进程从同一位置返回,栈当然也在同一位置。注意这里千万不能给子进程分配一个新的用户栈,否则它一返回就栈崩了。
  • eax = 0:这是“fork 返回两次”的秘密武器。子进程恢复现场后,eax 被强制写成了 0。于是用户态的 libc 封装函数读到 eax,就知道自己是子进程。而父进程走的是正常的中断返回路径,最终会通过另一个 pushl 指令,把 copy_process 的返回值(last_pid)放回 eax。
  • cs/ds/es/fs/gs/ss:这些段寄存器也要一并恢复,否则子进程没法访问自己的代码段和数据段。尤其注意 ldt 选择子被指向了新的 LDT,这样 CPU 做段地址翻译时就会找到子进程自己的段描述符。

除此之外,0.11 还会设置一些协处理器相关的字段:p->tss.i387 = 0;,这是为了清空浮点寄存器上下文。因为浮点寄存器的状态是懒加载的,父进程可能没被用到,直接复制反而可能出错。

3.5 返回收尾:一条路径,两种结局

copy_process 的最后几行:

p->state = TASK_RUNNING; // 把子进程设为可运行状态 return last_pid; // 返回值给父进程

然后在 system_call 的返回路径里,内核从栈上恢复所有寄存器和段选择子,再执行 iret 返回用户态。父进程此时拿到的是 eax = last_pid(子进程的进程号),子进程稍后被调度时,从自己的 TSS 恢复现场,拿到的是 eax = 0。

这就是 fork 的“双返回值”魔法:

  • 父进程的返回值来自“copy_process 的 C 函数返回值”,经过中断返回路径放进了 eax。
  • 子进程的返回值来自“TSS 中被预先写好的 eax = 0”,经过任务切换恢复进 eax。

两者走的是完全不同的硬件路径。

4. 实操:让 Linux 0.11 在调试器里跑起来

4.1 环境搭建与编译要点

光看代码太抽象,我强烈建议你把 0.11 内核跑起来,在 copy_process 入口下个断点,亲眼看看 TSS 的字段变化。

准备工作的核心是一个 Linux 0.11 源码包和一个模拟器。0.11 原本是在 80386 时代的真实硬件上跑的,今天我们用模拟器来复现会省掉很多兼容性问题。源码准备好后,直接 make 编译。0.11 的 Makefile 比较老,如果用的是新系统编译,可能会遇到缺少 as86/ld86 这类老工具链的问题,通常需要先安装对应的 16 位汇编工具。这是第一个常见的卡点,建议一开始就确认好。

编译通过后,把生成的 Image 放到模拟器的工作目录,配置一个小型 root 文件系统。比较省事的方案是用现成的 0.11 实验镜像,里面通常已经预置了编译好的 Image、root 文件系统和启动配置。

4.2 关键断点与观察清单

调试时建议在以下几个位置下断点:

  • _system_call:观察系统调用进入时,内核栈上被压入了什么。这里特别适合确认硬件压栈的 ss/esp/eflags/cs/eip 和内核手动压栈的 ds/es/fs/edx/ecx/ebx 是否如预期排列。
  • _find_empty_process:观察 last_pid 的变化,以及返回的槽位号。
  • copy_process入口:这是最值得花时间的地方。进入后立即打印 p、p->pid、p->tss.eip、p->tss.esp、p->tss.eax 的值。
  • iret之后:分别在父进程和子进程的视角打印 fork 的返回值,感受“双返回值”的实感。

如果调试器支持查看物理内存,可以在 copy_process 执行前后对比父进程 LDT 和子进程 LDT 的段基址变化。你会看到父进程的段基址可能是 0x00000000,而子进程的段基址变成了 nr × 0x4000000。这个差异正是 copy_mem 和 copy_page_tables 的成果。

提示:0.11 的 task_struct 里 TSS 字段很长,调试器里一屏显示不完。建议先看 eip、esp、eax、cs 这四项,理解“现场恢复”后,再看 LDT 字段(ldt[1] 和 ldt[2])的 base 地址,理解“段映射”。

4.3 手把手验证 fork 返回两次的现象

写一个最简单的 C 程序,只做 fork(),然后在父子两边分别打印返回值。编译后放进 0.11 的镜像里运行,输出可能会让你当场愣住:

int main() { int pid = fork(); if (pid == 0) { printf("child: pid=%d, fork_result=%d\n", getpid(), pid); } else { printf("parent: pid=%d, child_pid=%d\n", getpid(), pid); } return 0; }

留意输出顺序。在 0.11 环境下,父进程通常会先运行,因为 copy_process 把子进程设置为 TASK_RUNNING 后并不立即执行任务切换,而是继续走完中断返回路径。只有当下一次时钟中断到来、schedule() 被调用时,子进程才可能被选中运行。所以大概率你会先看到 parent 的输出,再看到 child 的输出。但这也不是绝对的,如果时钟中断恰好在你 fork 之后马上发生,子进程也可能先跑。这就是为什么 fork 之后父子进程的执行顺序从来都不应该被假设。

如果你在调试器里同时跟踪两个进程,会发现:

  • 父进程的 EIP 和子进程的 EIP 指向同一条指令(int 0x80 的下一条)。
  • 父进程的 EAX 是子进程 PID,子进程的 EAX 是 0。
  • 两者的 ESP 相同,CS/DS/SS 的段基址不同。

看到这四组数据的那一刻,fork 的整个机制才算真的“通”了。

5. 常见问题与排查技巧实录

5.1 高频疑问速查表

问题解释关键点
fork 为什么有两个返回值父进程从中断返回路径拿 copy_process 的返回值;子进程被调度时从 TSS 恢复 eax=0两条完全不同的硬件路径
子进程从哪里开始执行从 int 0x80 的下一条指令,即用户态调用 fork 的地方TSS 里的 EIP 是中断前被硬件压栈的 EIP
父子进程谁先执行不确定0.11 中父进程通常先返回,但时钟中断可能改变调度顺序
为什么子进程不需要重新走一遍系统调用因为子进程不是“再次调用 fork”,而是直接恢复现场任务切换 vs 函数调用
fork 时 Linux 0.11 有没有实现 COW没有完整实现,只有共享页表和写保护位0.12 才完善了缺页写保护处理
子进程的内核栈放在哪task_struct 所在物理页的下方,esp0 = PAGE_SIZE + (long)p每个任务一页物理内存,内核栈从页顶向下

5.2 新手最容易踩的 3 个坑

第一个坑是搞混“父进程的返回”和“子进程的返回”。有人以为子进程在 copy_process 里 return 出去就回到用户态了,其实不是。copy_process 的 return 是把值交给父进程的汇编调用方;子进程根本不会执行这个 return,它是在未来某个时刻,由调度器切换 TSS 后从用户态“凭空出现”的。

第二个坑是混淆逻辑地址、线性地址和物理地址。0.11 fork 相关的代码里,copy_mem 操作的是段基址,copy_page_tables 操作的是页表项里的物理页帧号。如果你把这几层混在一起,很容易觉得代码里的地址计算毫无规律。正确思路是:先通过 LDT 拿到逻辑地址,再用 GDT 和页表翻译成物理地址。

第三个坑是忽略 EAX 的妙用。很多人在看 copy_process 时,注意力全在 eip 和 esp 上,把 eax = 0 当成无关紧要的一行。实际上,这行代码决定了用户态 fork() 的返回值分支,是整个“返回两次”机制的最后一块拼图。少了它,子进程会把自己当成父进程,认错 pid,直接逻辑爆炸。

6. 从 0.11 到现代内核:fork 的后续演进

6.1 0.11 与后续版本的差异对比

0.11 的 fork 确实是 Linux 内核史上最经典的教学版本,但如果你带着它去对比现代内核,会发现差异很大。

页表复制策略上,0.11 是在 fork 的时候就把父进程的页表复制一份给子进程,然后清写保护位。这与现代内核的 COW 有本质差别:COW 的关键在于 fork 时只复制页表项、不复制物理页,等发生写缺页异常时再复制物理页。0.11 缺少的正是“写缺页时复制物理页”这一步,所以它只能算 COW 的地基。

任务切换上,0.11 依赖硬件 TSS 切换,用一条长跳转指令触发 CPU 自动加载新任务的全部寄存器。现代 x86-64 内核不再使用硬件任务切换,而是用软件完成寄存器保存和恢复,原因很现实:硬件切换要保存一大堆冗余的段寄存器、调试寄存器,成本高,而且在 64 位模式下很多机制已经不再适用。

进程创建接口上,fork 也演变成了更精细的 clone(),支持按需共享内存、文件表、信号处理器等资源。你可以把 clone 理解成一个带参数表的 fork:flags 决定哪些资源共享,哪些资源复制。

6.2 用 0.11 的源码思维看现代内核

我在实际阅读现代内核源码时,最深刻的体会是:虽然代码行数膨胀了几十倍,宏和抽象层多到让人头疼,但核心的问题域和上面的 fork 一模一样——从哪里拿内存、如何复制状态、如何构造返回现场、如何管理引用计数。

现代 Linux 的 copy_process 依然是那几步:复制 task_struct、初始化新进程的调度字段、复制或共享文件表、复制或共享内存描述符。0.11 中“通过寄存器快照构造 TSS”的思想,在 64 位内核里变成了“通过 pt_regs 结构体保存寄存器现场”。只要把 0.11 这条主线吃透,再去看任何版本的内核代码,都不会觉得完全陌生。

最后分享一个小技巧:我在分析 fork 类代码时,一定会画一张“状态继承清单”,把父子进程哪些共享、哪些复制、哪些重新初始化列得清清楚楚。0.11 的清单是:用户内存页共享但暂时只读、文件表和 inode 引用计数增加、信号清零、时间片直接继承、内核栈独立。带着这张清单去看现代内核的 clone flags,你会发现一切都只是这张清单的扩展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询