Linux 0.11 源码解读:从 5 个关键函数看进程创建与切换的实现

Linux 0.11 源码解读:从 5 个关键函数看进程创建与切换的实现

Linux 0.11 源码解读:从 5 个关键函数看进程创建与切换的实现

在操作系统的核心机制中,进程管理无疑是最为精妙的设计之一。当我们翻开 Linux 0.11 的源码,这个发布于1991年的经典版本,就像打开了一本关于计算机科学的活教材。今天,我们将深入五个关键函数——fork()copy_processscheduleswitch_tosys_pause,通过代码级的分析,揭示进程创建与切换的实现奥秘。

1. fork():用户视角的进程创建入口

fork()是 Unix/Linux 系统中最著名的系统调用之一,它创造了"分身术"般的编程体验。在用户空间,我们看到的只是一个简单的函数调用:

pid_t pid = fork(); if (pid == 0) { // 子进程代码 } else { // 父进程代码 }

但在内核层面,这个简单的调用背后隐藏着复杂的机制。Linux 0.11 中,fork()的实现始于sys_fork函数(在kernel/system_call.s中):

_sys_fork: call _find_empty_process testl %eax,%eax js 1f push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call _copy_process addl $20,%esp 1: ret

这段汇编代码揭示了fork()的核心流程:

  1. 调用find_empty_process寻找空闲的进程槽
  2. 检查返回值(进程号)是否有效
  3. 保存关键寄存器状态
  4. 调用copy_process完成实际的进程复制
  5. 清理栈空间并返回

提示:Linux 0.11 中进程控制块(PCB)使用固定大小的数组(NR_TASKS,默认为64),而非现代Linux中的动态分配方式。

2. copy_process:进程复制的核心实现

copy_process函数(位于kernel/fork.c)是进程创建的核心,它完成了以下关键操作:

int copy_process(int nr, long ebp, long edi, long esi, long gs, long none, long ebx, long ecx, long edx, long fs, long es, long ds, long eip, long cs, long eflags, long esp, long ss) { struct task_struct *p; // 分配任务结构体内存 p = (struct task_struct *) get_free_page(); // 复制父进程任务结构 *p = *current; // 设置子进程特定属性 p->state = TASK_UNINTERRUPTIBLE; p->pid = last_pid; p->counter = p->priority; // 复制页表 copy_mem(nr, p); // 设置内核栈 set_tss_desc(nr, &(p->tss)); set_ldt_desc(nr, &(p->ldt)); // 修改子进程返回值为0(区分父子进程) p->tss.eax = 0; // 设置就绪状态 p->state = TASK_RUNNING; return last_pid; }

这个函数中几个关键点值得注意:

  • 内存分配:使用get_free_page获取一页内存(4KB)作为子进程的 task_struct
  • 结构复制:直接通过*p = *current复制父进程的整个任务结构
  • 返回值处理:通过修改子进程的tss.eax实现父子进程返回不同值
  • 内存管理copy_mem负责复制页表,实现写时复制(COW)机制

注意:Linux 0.11 还没有实现现代Linux中的写时复制优化,这里的页表复制是立即完成的。

3. schedule:进程调度的决策中心

进程切换的核心是调度器,Linux 0.11 的schedule函数(位于kernel/sched.c)实现了一个简单但高效的时间片轮转调度算法:

void schedule(void) { int i, next, c; struct task_struct ** p; // 遍历所有任务,寻找就绪任务中counter最大的 while (1) { c = -1; next = 0; for (p = &LAST_TASK; p > &FIRST_TASK; --p) { if (!*p || (*p)->state != TASK_RUNNING) continue; if ((*p)->counter > c) { c = (*p)->counter; next = i; } } // 找到可运行任务则跳出循环 if (c) break; // 所有任务时间片用完,重新分配时间片 for (p = &LAST_TASK; p > &FIRST_TASK; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } // 切换到选中的任务 switch_to(next); }

调度算法的核心逻辑可以总结为:

  1. 遍历任务数组,寻找counter值最大的就绪任务
  2. 如果所有就绪任务的counter都为0,则重新计算时间片:
    • 新时间片 = 原时间片/2 + priority
  3. 调用switch_to执行任务切换

这种设计实现了:

  • 优先级调度:高优先级任务获得更多CPU时间
  • 时间片轮转:保证公平性,防止任务饥饿
  • 动态调整:长时间运行的任务优先级会逐渐降低

4. switch_to:上下文切换的底层实现

实际的进程上下文切换发生在switch_to宏中(定义于include/linux/sched.h):

#define switch_to(n) {\ struct {long a,b;} __tmp; \ __asm__("cmpl %%ecx,_current\n\t" \ "je 1f\n\t" \ "xchgl %%ecx,_current\n\t" \ "movw %%dx,%1\n\t" \ "ljmp %0\n\t" \ "cmpl %%ecx,_last_task_used_math\n\t" \ "jne 1f\n\t" \ "clts\n" \ "1:" \ ::"m" (*&__tmp.a),"m" (*&__tmp.b), \ "d" (_TSS(n)),"c" ((long) task[n])); \ }

这段内联汇编完成了以下关键操作:

  1. 检查目标进程是否是当前进程(如果是则无需切换)
  2. 更新current指针指向新进程
  3. 使用ljmp指令跳转到新进程的TSS描述符
  4. 处理浮点运算状态(如果上一个使用协处理器的是目标进程)

上下文切换的核心是CPU状态的保存与恢复,这包括:

  • 通用寄存器:通过任务状态段(TSS)自动保存/恢复
  • 段寄存器:包括CS、DS等
  • 程序计数器:EIP
  • 栈指针:ESP

提示:现代Linux已不再使用硬件任务切换(TSS),而是采用软件上下文切换以提升性能。

5. sys_pause:主动让出CPU的系统调用

sys_pause是进程主动让出CPU的系统调用实现:

int sys_pause(void) { current->state = TASK_INTERRUPTIBLE; schedule(); return 0; }

这个简单函数展示了Linux调度的一个重要特性——协作式调度。进程可以通过将自己状态设置为非运行状态(这里是可中断等待)并调用调度器来主动放弃CPU。

这种机制被用于:

  • 等待信号(signal)
  • 实现sleep类函数
  • 等待资源可用

进程状态转换的全景图

结合这五个关键函数,我们可以描绘出Linux 0.11中进程状态转换的全景:

新建 (fork) → 就绪 (TASK_RUNNING) ↑ ↓ | 被调度选中 (schedule) | ↓ | 运行中 (switch_to) | ↓ | 主动放弃CPU (sys_pause) | ↓ └─── 等待 (TASK_INTERRUPTIBLE/UNINTERRUPTIBLE)

这个简单的状态机模型支撑起了整个Linux的进程管理机制。与现代Linux相比,0.11版本缺少:

  • 实时进程支持
  • 多核调度
  • CFS完全公平调度器
  • 更丰富的进程状态(如停止状态)

但正是这种简洁性,使得Linux 0.11成为学习操作系统原理的绝佳教材。通过这五个函数的分析,我们不仅理解了进程创建与切换的机制,也看到了操作系统设计中的一些基本思想:

  1. 资源复用:通过fork实现进程复制
  2. 时间片轮转:公平分配CPU时间
  3. 优先级调度:重要任务获得更多资源
  4. 状态隔离:通过上下文切换实现进程保护

在实际项目开发中,理解这些底层机制有助于我们:

  • 更合理地设计多进程/多线程应用
  • 优化进程创建开销(如使用进程池)
  • 理解各种调度策略的影响
  • 调试复杂的并发问题