linux内核体系结构004:从内核态到用户态的“灵魂穿越”:Linux 0.11 进程初始化的魔法舞台
引言:那第一次心跳之前的静默
想象一下这样一个场景:你是一个刚刚降生的婴儿,你的第一口气还没有吸入,你的心脏还没有开始跳动,但是——你的身体已经完整了。你的四肢、你的器官、你的骨骼,全部都已经到位。它们只是等待一个信号,让生命的第一个瞬间开始。
Linux 0.11 的内核启动过程,就是这样一个“等待心跳”的瞬间。当 BIOS 引导完成,当 boot 程序把内核代码从硬盘加载到内存,当内存管理、中断处理、设备驱动这些子系统各就各位——整个系统还没有真正“活”起来。
为什么?因为此时此刻,CPU 还运行在最高特权级(0级),而真正的用户进程——包括第一个 shell 进程——都应该运行在最低特权级(3级)。这就好比一个将军(特权级0)穿着军装、拿着指挥刀,但接下来他要变成一个普通的士兵(特权级3)去执行任务。
从 0 到 3 的跨越,不只是数字的变化,而是一个完整的“灵魂穿越”——从内核态的“神性”到用户态的“人性”。这中间的转换,隐藏着整个 Linux 内核最精妙的魔法:中断返回机制(iret)。
这,就是本章我们要讲的 进程初始化 故事的开端。
第一章:你在哪里,你要去哪里?—— 特权级的“三界六道”
在进入细节之前,我们需要明确一个根本性问题:为什么操作系统要区分特权级?
1.1 特权级:CPU 的“门禁系统”

在 80x86 架构中,CPU 设计了 4 个特权级(0、1、2、3)。Linux 只用了两个:
- 特权级 0(内核态):最高权限,可以访问所有硬件资源,执行所有指令。它就像是系统管理员,拥有“上帝视角”。
- 特权级 3(用户态):最低权限,只能访问受限的内存区域,不能直接操作硬件。它就像是普通用户,只能在划分好的“格子间”里活动。
这好比一个大型图书馆:
- 内核态是图书管理员,可以在所有书架间穿梭,甚至可以打开书库的后门。
- 用户态是普通读者,只能在阅览室看书,不能进入书库。
1.2 为什么必须从内核态切换到用户态?
我们仔细推敲一下你的三张图片中的一段话:
“此后程序把自己‘手工’移动到任务0(进程0)中运行,并使用 fork() 调用首次创建出进程1。”
这里的关键词是 “手工移动”。起初,内核启动后的第一个进程就是 init_task(任务0),它其实只是一个「空壳」——它没有自己的地址空间,它的代码段和数据段直接指向内核代码。
如果任务0一直运行在内核态,那么它就拥有了最高权限,但问题来了:
- 任务0的代码其实是从内核代码中“借用”的,它本质上是内核的一部分,而不是独立的用户程序。
- Linux 的哲学是:所有用户进程(包括第一个 shell)都应在用户态运行,这样即使某个进程崩溃了,也不会拖垮整个系统。
因此,必须在启动的早期,就把任务0“降级”到用户态。这个降级过程,就叫做 “从内核态转移到用户态”。
第二章:一个完美的伏笔 —— 任务0的“伪造”数据结构
2.1 一个“伪造”的身份证:TSS 和 LDT
在 sched_init() 中,系统预先设置了任务0的 TSS(任务状态段) 和 LDT(局部描述符表)。
TSS:这是 CPU 用来保存任务上下文的一块内存区域。当发生任务切换时,CPU 会把当前任务的寄存器(EAX, EBX, ECX…)保存在 TSS 里,并把新任务的 TSS 中的值加载到寄存器。
LDT:这是任务自己的一套“门牌号”系统,用来管理它自己私有的代码段、数据段、栈段。
在原书的描述中,任务0的初始数据结构被填写得“满得不能再满”:
tss.ss0:内核数据段选择符(KERNEL_DS)tss.esp0:指向任务0的栈顶tss.eax等寄存器:全部被初始化为 0
但是,最关键的一点是: 任务0的代码段和数据段的基地址还是在内核区域(0 到 16MB),而不是用户区域。也就是说,任务0虽然已经有了“壳”(TSS、LDT),但它还没有自己的“房子”。
2.2 一个“魔术”般的宏:move_to_user_mode
书中提到:
“把 main.c 程序执行流从内核态(特权级0)移动到了用户态(特权级3)的任务0中继续运行。在移动之前,系统在对调度程序的初始化过程(sched_init())中,首先对任务0的运行环境进行了设置。”
这个“移动”的实际操作,是在 main.c 里通过一个宏 move_to_user_mode() 完成的。它的原理非常精妙:
中断返回(iret) 本来应该用于从中断处理程序返回被中断的进程。但内核工程师们“脑洞大开”,利用 iret 的特性来主动降级。
为什么 iret 能做到这一点?
因为 iret 在执行时,会从栈中弹出 CS、EIP、EFLAGS,然后根据这些值恢复执行。如果栈中的 CS 段选择符的特权级是 3(用户态),而当前 CPU 的特权级是 0(内核态),CPU 就会检测到这个变化,并触发特权级转换。
所以,这个“魔术”的步骤是:
- 在内核栈中伪造一个“中断返回”时的栈结构:
- SS:用户数据段选择符(特权级3)
- ESP:用户栈指针
- EFLAGS:标志寄存器
- CS:用户代码段选择符(特权级3)
- EIP:任务0中 main.c 的入口地址
- 执行
iret指令。CPU 误以为它刚从一次中断中返回,于是从栈中弹出这些值,自动从特权级0切换到了特权级3。 - CPU 开始执行任务0的代码(在
main.c中,也就是继续初始化后面的部分),但此时已经是在用户态了。
这就是 “瞒天过海” 的绝妙操作。
第三章:图解特权级切换 —— 一张图看懂“灵魂穿越”

让我们把原书 图 2-7 仔细拆解一遍。我把这张图的数据和逻辑转换成了一个更直观的 Mermaid 流程图:
关键术语翻译:
- SS:Stack Segment,堆栈段。
- ESP:Extended Stack Pointer,堆栈指针。
- EFLAGS:CPU 的状态标志。
- CS:Code Segment,代码段。
- EIP:Extended Instruction Pointer,指令指针。
这个图展示了 Linux 0.11 中最关键的一步:从 0 到 3 的跨越。如果没有这一步,所有后来的用户进程都将无法运行,整个系统也就只能是一个“只能跑内核代码”的测试机。
第四章:生命繁衍 —— 任务0的“分身术” fork()
4.1 从“空壳”到“生命”:task 1 的诞生
当任务0成功“降级”到用户态后,它首先要做的一件事就是:繁衍。
原书在 2.4.4 节中写道:
“使用 fork() 系统调用,所有进程均通过复制进程0得到(进程0为根父进程)。在完成复制后,任务1将被创建。”
这个过程用一句大白话讲就是:任务0 “生” 了任务1。任务0是“母亲”,任务1是“孩子”。
4.2 fork() 的复制原理
Linux 0.11 的 fork() 系统调用是通过 clone() 来实现的。它本质上做的是 “复制一份”。
具体步骤(对照原书 2.4.4):
- 找空位:在任务数组
task[NR_TASKS]中找到一个空闲的位置。 - 复制数据结构:把父进程(任务0)的
task_struct完全复制一份给子进程(任务1)。 - 修改关键字段:
pid:新进程号。state:设为TASK_RUNNING(马上可以运行)。counter:初始化为 15 个时间片(150ms)。tss.eax:置为 0(这是子进程 fork() 的返回值,父进程返回的是子进程的 pid)。
- 设置 TSS 和 LDT:子进程的 TSS 指向新分配的内存区域,LDT 从父进程复制。
4.3 为什么是 0 ?
在 fork() 的返回值中,一个非常关键的地方是:
- 父进程:fork() 返回子进程的 PID。
- 子进程:fork() 返回 0。
为什么子进程要返回 0?因为内核在 copy_process() 中显式设置了 p->tss.eax = 0,而 eax 寄存器正是函数调用的返回值。
这样,在 fork() 之后的代码中,可以这样区分父子进程:
if (fork() == 0) {
// 我是子进程
} else {
// 我是父进程
}
这种设计简单而高效,是 fork() 的精髓。
第五章:竞争与公平 —— 进程调度算法

5.1 “公平”不是平均主义
原书 2.4.5 节详细描述了 Linux 0.11 的调度算法。它并不是简单的时间片轮转(Round-Robin),而是一种基于优先级和运行时间片的动态加权算法。
它的核心逻辑是:
- 扫描所有
TASK_RUNNING状态的进程。 - 找到
counter(剩余时间片)最大的那个进程。 - 让这个进程运行。
- 当所有进程的
counter都为 0 时,重新为所有进程(包括睡眠的进程)计算counter,计算公式:counter = (counter >> 1) + priority
这个公式的意思是:
- 原本
counter大的进程,在重新计算后,依然会得到更大的counter,但增长幅度递减。 - 原本
counter小的(甚至睡眠了很久的),会稍微得到一点点补偿。 - 整体而言,优先级高的进程会得到更多的 CPU 时间,但不是绝对的“优先级抢占”,而是一种动态的“加权分配”。
5.2 为什么要在用户态抢占?
原书中一句话非常关键:
“Linux 0.11 采用抢占式调度,但抢占仅发生在用户态,内核态不可抢占。”
为什么?
- 内核态抢占会引发并发问题:在内核态执行系统调用时,如果被抢占,可能导致数据结构损坏(如链表被破坏、内存泄露)。
- 内核态应该快速完成:大多数系统调用(如读写文件)都不长,没必要在中间切走。
- 简化设计:允许内核态抢占需要复杂的锁机制,而 Linux 0.11 是单核系统,没必要这么复杂。
因此,只有在用户态的进程才可能被时钟中断触发调度,而内核态的代码会一直执行到结束或自愿放弃 CPU(如 sleep_on)。
第六章:代码实战 —— 模拟一个“多任务时钟调度器”
为了让你真实体验这些过程,我写了一个 极简的、模拟 Linux 0.11 调度器 的程序。它只包含最核心的逻辑:
- 创建两个进程(模拟任务0和任务1)。
- 模拟 10ms 的时钟中断。
- 实现
schedule()调度算法。 - 演示从任务0切换到任务1的过程。
6.1 代码文件
sim_sched.c
/**
* @file sim_sched.c
* @brief 模拟 Linux 0.11 调度器的精简版。
*
* 本程序模拟了一个基于优先级和时间片的单核进程调度器。
* 包含以下功能:
* - 创建进程表
* - 模拟 10ms 时钟中断
* - 实现 schedule() 调度算法
* - 模拟 task 0 到 task 1 的切换
*
* 编译方法: gcc -o sim_sched sim_sched.c -Wall -g
* 运行方法: ./sim_sched
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* =========================================================================
* 数据结构定义
* ========================================================================= */
/**
* @brief 进程控制块 (PCB),模拟原书的 task_struct
*/
typedef struct task_struct {
int pid; /**< 进程 ID */
int state; /**< 进程状态: 0=运行, 1=等待, 2=停止, 3=僵尸 */
int priority; /**< 优先级 (对应原书的 priority) */
int counter; /**< 剩余时间片 (对应原书的 counter) */
char name[32]; /**< 进程名 */
unsigned long esp; /**< 堆栈指针 (模拟) */
} task_t;
/* 进程状态常量 */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 3
/* 全局变量 */
#define MAX_TASKS 10
task_t *task[MAX_TASKS]; /**< 任务数组 */
int nr_tasks = 0; /**< 当前任务数量 */
int current_task_idx = 0; /**< 当前运行任务的索引 */
unsigned long jiffies = 0; /**< 系统滴答数 */
/* =========================================================================
* 核心辅助函数
* ========================================================================= */
/**
* @brief 初始化任务数组,创建两个模拟进程
*/
void init_tasks(void) {
/* 创建任务0 (根进程) */
task[0] = malloc(sizeof(task_t));
task[0]->pid = 0;
task[0]->state = TASK_RUNNING;
task[0]->priority = 15; /* 初始优先级 */
task[0]->counter = 15; /* 初始时间片 */
strcpy(task[0]->name, "task0");
nr_tasks++;
/* 创建任务1 (第一个用户进程) */
task[1] = malloc(sizeof(task_t));
task[1]->pid = 1;
task[1]->state = TASK_RUNNING;
task[1]->priority = 15;
task[1]->counter = 15;
strcpy(task[1]->name, "task1");
nr_tasks++;
current_task_idx = 0; /* 初始运行 task0 */
printf("[系统] 初始化完成: task0 (pid=0), task1 (pid=1)\n");
}
/**
* @brief 查找下一个可运行进程 (模拟 schedule() 核心)
* @return 指向下一个 task_struct 的指针,若没有则返回 NULL
*/
task_t *next_task(void) {
int max_counter = -1;
int next_idx = -1;
/* 1. 查找 counter > 0 且 state == TASK_RUNNING 中 counter 最大的 */
for (int i = 0; i < nr_tasks; i++) {
if (task[i]->state == TASK_RUNNING && task[i]->counter > 0) {
if (task[i]->counter > max_counter) {
max_counter = task[i]->counter;
next_idx = i;
}
}
}
/* 2. 如果没找到,说明所有进程 counter 都已耗尽,重新计算 */
if (next_idx == -1) {
printf("[调度] 所有进程时间片耗尽,重新计算 counter\n");
for (int i = 0; i < nr_tasks; i++) {
if (task[i]->state == TASK_RUNNING) {
/* 重新计算: counter = counter/2 + priority */
task[i]->counter = (task[i]->counter >> 1) + task[i]->priority;
printf("[调度] PID %d 新 counter = %d\n", task[i]->pid, task[i]->counter);
}
}
/* 重新查找 */
return next_task();
}
return task[next_idx];
}
/**
* @brief 模拟上下文切换 (switch_to)
* @param next 下一个要运行的进程
*/
void switch_to(task_t *next) {
printf("\n>>> [内核] 上下文切换: [%s (pid=%d)] -> [%s (pid=%d)]\n",
task[current_task_idx]->name, task[current_task_idx]->pid,
next->name, next->pid);
/* 更新当前任务索引 */
for (int i = 0; i < nr_tasks; i++) {
if (task[i] == next) {
current_task_idx = i;
break;
}
}
}
/**
* @brief 调度程序 (schedule())
*/
void schedule(void) {
task_t *next = next_task();
if (next && next != task[current_task_idx]) {
switch_to(next);
}
}
/**
* @brief 模拟时钟中断处理 (do_timer)
*
* 这是系统的“心脏”,每 10ms 触发一次。
*/
void do_timer(int cpl) {
jiffies++;
task_t *current = task[current_task_idx];
if (current == NULL) return;
/* 只有在用户态 (cpl==0) 才递减时间片 */
if (cpl == 0 && current->state == TASK_RUNNING) {
current->counter--;
printf("[时钟中断] PID %d 剩余 counter = %d\n", current->pid, current->counter);
if (current->counter <= 0) {
printf("[时钟中断] PID %d 时间片耗尽\n", current->pid);
current->state = TASK_INTERRUPTIBLE; /* 模拟进程等待 */
schedule();
/* 调度回来后再恢复状态 */
task[current_task_idx]->state = TASK_RUNNING;
}
}
}
/* =========================================================================
* 模拟用户程序
* ========================================================================= */
/**
* @brief 模拟任务0的主循环
*/
void task0_loop(void) {
printf("[task0] 我在用户态运行... (pid=%d)\n", task[current_task_idx]->pid);
sleep(1);
}
/**
* @brief 模拟任务1的主循环
*/
void task1_loop(void) {
printf("[task1] 我也在用户态运行... (pid=%d)\n", task[current_task_idx]->pid);
sleep(1);
}
/* =========================================================================
* 主程序
* ========================================================================= */
/**
* @brief 主程序入口
*/
int main(void) {
int cycles = 20; /* 模拟 20 次时钟中断 */
printf("========== 模拟 Linux 0.11 调度器启动 ==========\n");
init_tasks();
printf("\n开始模拟... 每次循环=10ms\n");
for (int t = 0; t < cycles; t++) {
printf("\n----- 第 %d 次时钟中断 (jiffies=%lu) -----\n", t+1, jiffies);
/* 1. 触发时钟中断 (假设当前在用户态) */
do_timer(0); /* cpl=0 表示用户态 */
/* 2. 模拟当前进程执行用户态代码 */
if (task[current_task_idx]->pid == 0) {
task0_loop();
} else if (task[current_task_idx]->pid == 1) {
task1_loop();
}
/* 暂停一小会儿,模拟真实时间流逝 */
usleep(100000); /* 100ms */
}
printf("\n========== 模拟结束 ==========\n");
printf("累计 jiffies: %lu\n", jiffies);
/* 清理内存 */
for (int i = 0; i < nr_tasks; i++) {
free(task[i]);
}
return 0;
}
6.2 Makefile
Makefile
# 编译器
CC = gcc
# 编译选项: -Wall 显示所有警告, -g 包含调试信息, -O0 关闭优化以便调试
CFLAGS = -Wall -g -O0
# 目标文件
TARGET = sim_sched
# 默认目标
all: $(TARGET)
# 链接规则
$(TARGET): sim_sched.c
$(CC) $(CFLAGS) -o $(TARGET) sim_sched.c
# 清理规则
clean:
rm -f $(TARGET)
# 运行规则
run: $(TARGET)
./$(TARGET)
.PHONY: all clean run
6.3 操作说明
-
编译:
make clean && make -
运行:
./sim_sched -
解读输出:
- 你会看到
[系统] 初始化完成,创建了task0和task1。 - 每次
[时钟中断],PID 0 的counter会递减。 - 当
counter降到 0 时,会触发schedule(),切换到 PID 1。 - 你会看到类似
>>> [内核] 上下文切换: [task0 (pid=0)] -> [task1 (pid=1)]的输出。 - 所有进程的
counter全部耗尽后,会看到[调度] 所有进程时间片耗尽,重新计算 counter的提示,然后counter会按公式重新分配。
- 你会看到
-
调整参数:
- 你可以修改
task[i]->priority的值,观察优先级高的进程是否获得了更多的 CPU 时间。
- 你可以修改
终章:一个完整的故事 —— 从开机到多进程
现在,让我们把这些零散的知识点串联成一个完整的故事:
- 内核启动:BIOS 加载引导程序 → 引导程序加载内核 → 内核初始化内存、中断、设备。
- 任务0诞生:
sched_init()创建任务0,并伪造 TSS 和 LDT。 - 降级:使用
move_to_user_mode()和iret,把任务0从内核态(0级)“穿越”到用户态(3级)。 - 创建任务1:任务0调用
fork(),复制自身,创建任务1。任务1有了独立的 pid、栈、TSS 和 LDT。 - 调度:时钟中断(10ms)每次触发
do_timer(),递减当前进程的counter。当counter为 0 时,调用schedule()选择下一个进程,循环往复。 - 多任务:就这样,多个进程在不同时间片内交替运行,由于时间片很短(15 个 tick = 150ms),看起来像“同时”在运行。
这就是 Linux 0.11 进程管理的完整生命史。从一颗种子(内核),到一个胚胎(任务0),再到第一个细胞分裂(任务1),最终形成一个蓬勃发展的多任务系统。
希望你通过这次深度探索,不仅读懂了代码,更读懂了设计的智慧。下一次当你面对 Linux 内核时,你不再是“黑盒”的门外汉,而是 “深知其道” 的内行人。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)