引言:那第一次心跳之前的静默

想象一下这样一个场景:你是一个刚刚降生的婴儿,你的第一口气还没有吸入,你的心脏还没有开始跳动,但是——你的身体已经完整了。你的四肢、你的器官、你的骨骼,全部都已经到位。它们只是等待一个信号,让生命的第一个瞬间开始。

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一直运行在内核态,那么它就拥有了最高权限,但问题来了:

  1. 任务0的代码其实是从内核代码中“借用”的,它本质上是内核的一部分,而不是独立的用户程序。
  2. 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 就会检测到这个变化,并触发特权级转换。

所以,这个“魔术”的步骤是:

  1. 在内核栈中伪造一个“中断返回”时的栈结构:
    • SS:用户数据段选择符(特权级3)
    • ESP:用户栈指针
    • EFLAGS:标志寄存器
    • CS:用户代码段选择符(特权级3)
    • EIP:任务0中 main.c 的入口地址
  2. 执行 iret 指令。CPU 误以为它刚从一次中断中返回,于是从栈中弹出这些值,自动从特权级0切换到了特权级3。
  3. CPU 开始执行任务0的代码(在 main.c 中,也就是继续初始化后面的部分),但此时已经是在用户态了。

这就是 “瞒天过海” 的绝妙操作。


第三章:图解特权级切换 —— 一张图看懂“灵魂穿越”

在这里插入图片描述

让我们把原书 图 2-7 仔细拆解一遍。我把这张图的数据和逻辑转换成了一个更直观的 Mermaid 流程图:

CPU 执行 iret 指令

执行 iret 之后 -- 用户态

CPU 检测到 CS 特权级=3 vs 当前=0

自动触发特权级转换

从栈中弹出 EIP, CS, EFLAGS

恢复执行

此时 CPU 在用户态执行任务0的代码

执行 iret 之前 -- 内核态

内核栈栈顶

SS: 用户数据段选择符 (特权级3)

ESP: 用户栈指针

EFLAGS: 标志寄存器

CS: 用户代码段选择符 (特权级3)

EIP: 用户入口地址

栈底

核心点:iret 不仅是中断返回指令,
也是特权级切换的开关

关键术语翻译:

  • 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):

  1. 找空位:在任务数组 task[NR_TASKS] 中找到一个空闲的位置。
  2. 复制数据结构:把父进程(任务0)的 task_struct 完全复制一份给子进程(任务1)。
  3. 修改关键字段
    • pid:新进程号。
    • state:设为 TASK_RUNNING(马上可以运行)。
    • counter:初始化为 15 个时间片(150ms)。
    • tss.eax:置为 0(这是子进程 fork() 的返回值,父进程返回的是子进程的 pid)。
  4. 设置 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),而是一种基于优先级和运行时间片的动态加权算法。

它的核心逻辑是:

  1. 扫描所有 TASK_RUNNING 状态的进程。
  2. 找到 counter(剩余时间片)最大的那个进程。
  3. 让这个进程运行。
  4. 当所有进程的 counter 都为 0 时,重新为所有进程(包括睡眠的进程)计算 counter,计算公式:
    counter = (counter >> 1) + priority

这个公式的意思是:

  • 原本 counter 大的进程,在重新计算后,依然会得到更大的 counter,但增长幅度递减。
  • 原本 counter 小的(甚至睡眠了很久的),会稍微得到一点点补偿。
  • 整体而言,优先级高的进程会得到更多的 CPU 时间,但不是绝对的“优先级抢占”,而是一种动态的“加权分配”。

5.2 为什么要在用户态抢占?

原书中一句话非常关键:

“Linux 0.11 采用抢占式调度,但抢占仅发生在用户态,内核态不可抢占。”

为什么?

  1. 内核态抢占会引发并发问题:在内核态执行系统调用时,如果被抢占,可能导致数据结构损坏(如链表被破坏、内存泄露)。
  2. 内核态应该快速完成:大多数系统调用(如读写文件)都不长,没必要在中间切走。
  3. 简化设计:允许内核态抢占需要复杂的锁机制,而 Linux 0.11 是单核系统,没必要这么复杂。

因此,只有在用户态的进程才可能被时钟中断触发调度,而内核态的代码会一直执行到结束或自愿放弃 CPU(如 sleep_on)。


第六章:代码实战 —— 模拟一个“多任务时钟调度器”

为了让你真实体验这些过程,我写了一个 极简的、模拟 Linux 0.11 调度器 的程序。它只包含最核心的逻辑:

  1. 创建两个进程(模拟任务0和任务1)。
  2. 模拟 10ms 的时钟中断。
  3. 实现 schedule() 调度算法。
  4. 演示从任务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 操作说明

  1. 编译

    make clean && make
    
  2. 运行

    ./sim_sched
    
  3. 解读输出

    • 你会看到 [系统] 初始化完成,创建了 task0task1
    • 每次 [时钟中断],PID 0 的 counter 会递减。
    • counter 降到 0 时,会触发 schedule(),切换到 PID 1。
    • 你会看到类似 >>> [内核] 上下文切换: [task0 (pid=0)] -> [task1 (pid=1)] 的输出。
    • 所有进程的 counter 全部耗尽后,会看到 [调度] 所有进程时间片耗尽,重新计算 counter 的提示,然后 counter 会按公式重新分配。
  4. 调整参数

    • 你可以修改 task[i]->priority 的值,观察优先级高的进程是否获得了更多的 CPU 时间。

终章:一个完整的故事 —— 从开机到多进程

现在,让我们把这些零散的知识点串联成一个完整的故事

  1. 内核启动:BIOS 加载引导程序 → 引导程序加载内核 → 内核初始化内存、中断、设备。
  2. 任务0诞生sched_init() 创建任务0,并伪造 TSS 和 LDT。
  3. 降级:使用 move_to_user_mode()iret,把任务0从内核态(0级)“穿越”到用户态(3级)。
  4. 创建任务1:任务0调用 fork(),复制自身,创建任务1。任务1有了独立的 pid、栈、TSS 和 LDT。
  5. 调度:时钟中断(10ms)每次触发 do_timer(),递减当前进程的 counter。当 counter 为 0 时,调用 schedule() 选择下一个进程,循环往复。
  6. 多任务:就这样,多个进程在不同时间片内交替运行,由于时间片很短(15 个 tick = 150ms),看起来像“同时”在运行。

这就是 Linux 0.11 进程管理的完整生命史。从一颗种子(内核),到一个胚胎(任务0),再到第一个细胞分裂(任务1),最终形成一个蓬勃发展的多任务系统。

希望你通过这次深度探索,不仅读懂了代码,更读懂了设计的智慧。下一次当你面对 Linux 内核时,你不再是“黑盒”的门外汉,而是 “深知其道” 的内行人。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐