1. 程序 vs 进程:打破“运行起来的程序”的笼统认知

在很多教材里,我们经常看到这样一句话:“进程就是运行起来的程序,或者内存中的程序”。这句话不能说错,但过于笼统,不够深入本质。

我们先来厘清一个概念:什么是程序?
我们在写完 C/C++ 代码(如 code.c)并编译后,会生成一个二进制可执行文件(如 Windows 下的 code.exe 或 Linux 下的可执行程序),而我们平时在 Linux 命令行敲的各种指令,本质上也是磁盘特定路径下的可执行程序。
只要它还静静地躺在磁盘上,它就只是一个普通文件(本质是一堆 0 和 1 组成的代码和数据),此时它绝对不是进程。

那么,到底什么才叫做进程呢?

我们先思考几个连环问题:

  1. 你的操作系统内,能同时运行很多的程序吗?
    当然可以!我们在电脑上可以一边听歌、一边挂着微信、一边用浏览器查资料。
  2. 每一个程序要运行,都必须先做什么?
    根据冯·诺依曼体系结构,程序要运行,必须先从磁盘加载到内存中。因此,内存里一定会同时存在大量加载进来的程序。
  3. 内存中同时存在这么多运行中的程序,操作系统要不要把它们管理起来?怎么管理?
    当然要管理!而且在内存中,在还没有启动我们自己的进程之前,第一款加载进内存的软件就是操作系统本身。操作系统管理任何软硬件资源的终极法则只有六个字——“先描述,再组织”!

这就像你未来毕业去找工作:

  • HR 面试官在筛选候选人时,真的是把成百上千个求职者大活人直接拉到办公室里排成一排去管理吗?并不是!
  • 第一步,每个求职者都要先写一份面试简历——这本质上就是在 “描述自己”(记录你的姓名、专业、技能、联系方式等所有属性)。
  • 第二步,HR 把所有人的简历收上来,摞在一起排好队——这本质上就是在**“组织起来”**!

同理,当程序被加载到内存后,真正让操作系统把进程组织起来的,并不是把一堆死板的可执行程序代码直接串起来,而是操作系统在内核中先用 C 语言定义了一个结构体(包含进程的所有属性)去描述这个进程,然后再用链表等数据结构把这些结构体组织起来!


2. 进程 = 内核数据结构对象(PCB) + 自己程序的代码和数据

结合上面的底层图解,我们来完整还原一个程序变成进程的全过程:

  1. 什么不是进程?
    磁盘上的 code.c(源码文件)和编译出来的 code.exe(二进制可执行文件),本质都只是存放在磁盘上的 0/1 代码和数据文件,它们还不是进程。
  2. 仅仅把程序加载到内存,就是进程了吗?
    程序由操作系统通过 input 拷贝放入内存,此时内存中虽然有了该程序的代码和数据,但如果只有代码和数据,它依然还不算一个完整的进程!
  3. 真正的进程是什么?
    因为每个程序的代码和数据大小不一、杂乱无章,我们在管理进程的时候,不可能直接去把各个程序的代码和数据穿成一串管理起来。
    所以,每当一个程序被加载到内存时,操作系统都会在内核中为它创建一个对应的结构体对象(里面包含了该进程的所有属性信息,以及指向内存中代码和数据的指针)。
    将“内核数据结构对象”和“加载到内存中的代码和数据”加起来,才真正叫做进程!

核心公式:进程 = 内核数据结构对象(PCB) + 自己程序的代码和数据

有了这个描述进程的结构体对象后,操作系统就可以用指针把所有进程的结构体对象连接起来,最后在操作系统层面上得到一张链表。
从此以后,操作系统对进程的管理(新建进程、销毁进程、查找进程、调度进程),就彻底转化为了对这张进程链表的增、删、查、改!

什么是 PCB 与 task_struct?

  • PCB(Process Control Block,进程控制块):这是所有操作系统学科教材里,对“描述进程的结构体”的统称(就像电影里的“媒婆”这一职业统称,不管是 Windows 还是 Linux 都有 PCB)。
  • struct task_struct:这是在具体的 Linux 操作系统内核中,用来实现 PCB 的那个 C 语言结构体的真实名字(就像媒婆里的具体人物“王婆”)。

在 Linux 下,进程要被调度、被切换、被维护,本质上全都是操作系统拿着 struct task_struct(就像面试官拿着你的简历)去进行调度管理。我们在命令行输入 ./cmd、在 Windows 下双击图标、在手机上点开一个 App,本质全都是在系统内启动一个进程(创建 task_struct + 加载代码和数据)!


3. task_struct:Linux 内核中的 PCB 属性分类

既然 struct task_struct 包含了进程的所有属性(直接或间接都能通过这个结构体找到),那么它里面到底装了些什么呢?我们可以将其核心内容分为以下几大类:

  • 标示符(PID):描述本进程的唯一标示符,用来区别系统中的其他进程(本质就是一个整数)。
  • 状态:包括任务状态(如运行、休眠、暂停等)、退出代码、退出信号等。
  • 优先级:相对于其他进程的优先级(在 Linux 中,优先级数值越小,代表优先级越高,越早被 CPU 执行)。
  • 程序计数器(PC / EIP):记录程序中即将被执行的下一条指令的地址。
  • 内存指针:包括指向程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据:进程执行时,处理器的寄存器中的数据(极其重要,下文重点剖析)。
  • I/O 状态信息:包括显式的 I/O 请求、分配给进程的 I/O 设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器运行时间总和、使用的时钟数总和、时间限制、记账号等。这是操作系统评判进程、实现公平调度的重要依据(比如当多个进程优先级一致时,操作系统会优先挑选累计占用 CPU 时间更少的进程投入运行)。
  • 其他信息:如父子进程关系、兄弟进程关系、连接所有任务的双链表节点字段等。

4. 函数返回值是怎么传递的?CPU 寄存器、EIP 与时间片轮转初探

4.1 函数返回值是怎么传递的?

我们来看右侧这段再熟悉不过的 C 语言代码:

int add(int x, int y)
{
    int z = x + y;
    return z;
}

void main()
{
    int result = add(x, y);
    printf("result: %d\n", result);
}

在学习 C 语言时我们知道,add 函数内部的变量 z 是一个局部变量(临时变量),只在 add 函数内部有效,函数一旦结束栈帧销毁,z 就没了。那么,z 是如何跨越函数边界,把自己的值成功返回并赋给 main 函数里的 result 的呢?

这就需要结合 CPU 内部的寄存器来理解:

  1. 首先,当 main 函数调用 add 函数时,会先把实参 x、y 的值以及相加指令加载到 CPU 的寄存器和运算单元中。
  2. add 函数内部的加法计算是在 CPU 上进行的,算出来的结果除了放到内存的临时变量 z 中,在执行 return z; 时,CPU 会把返回值暂存到 CPU 内部的通用寄存器(如 eax 寄存器)中。
  3. 当 add 函数返回到 main 函数执行 int result = add(x, y); 时,CPU 直接把刚刚保存在寄存器里的那个结果值,拷贝写入到内存中 result 变量所在的空间里!

同时,CPU 内部还有一个极其关键的寄存器叫做 程序计数器(PC,在 x86 架构下叫 EIP)。
代码并不是一口气瞬间执行完的,而是一行指令一行指令往下跑的。CPU 怎么知道下一行该执行哪条指令?全靠 PC / EIP 寄存器!它里面时刻存放着当前正在执行指令的下一条指令的地址。


4.2 时间片轮转与上下文切换:从“当兵保留学籍”看懂现场保护与恢复

现在思考一个关键问题:一个进程拿到 CPU 之后,会一直霸占着 CPU 把自己的代码全部执行完,才放弃 CPU 吗?

答案是:绝对不会!
试想一下,如果一个进程写了一个 while(1) 死循环,一旦它拿到 CPU 就不撒手,那单核 CPU 的电脑岂不是瞬间卡死,连鼠标都动不了了?
为了防止这种情况,现代操作系统全部采用基于时间片的轮转调度机制(Round-Robin):

  • 操作系统会给每一个进程分配一小段固定的 CPU 使用时间,叫做时间片(其实就是一个计数器,比如每个进程分配 10 毫秒)。
  • 假设系统里有 10 个进程,每个进程每次最多只在 CPU 上跑 10 毫秒。一旦 10 毫秒时间片耗尽,哪怕你的代码才跑了一半,操作系统也会强制把你从 CPU 上剥离下来,放到后面重新排队,并挑选下一个进程放到 CPU 上运行。
  • 10 个进程全部轮转一遍只需要 100 毫秒(0.1 秒),1 秒钟内每个进程都能被推进 10 次,所以在人类感官上,所有程序就像是在同时流畅运行一样。

为什么要保护上下文与恢复上下文?

既然进程的时间片一到就会被中途从 CPU 上切走,那就会面临一个严重的问题:

  • 比如你正在下载一个 1GB 的电影,刚下载了 500MB 时间片到了被切走,等下次轮到你上 CPU 时,难道要从 0MB 重新开始下载吗?当然不能,必须接着上次的 500MB 继续往下下!
  • 再打个生动的比方:一个正在读大二的大学生被选中去当兵(进程被切走)。他能直接背上书包就走人吗?不能!如果直接走了,学校看他两年没来上课,直接按旷课开除了,等他退伍回来还得重新高考读大一。
    正确的做法是:走之前先找辅导员办理休学,保留学籍(把大一、大二的各科成绩、学籍状态全部打包存进档案袋);等两年后退伍回来,再去学校办理复学,恢复学籍,然后直接接着大三继续往下读!

在操作系统中,这个“学籍档案信息”就叫做进程的硬件上下文数据(Context)!

  • 什么是上下文数据?
    就是进程在 CPU 上运行期间,CPU 内部各个寄存器(包括算了一半的中间变量寄存器、函数返回值寄存器、栈指针寄存器,以及记录跑到了哪一行代码的 EIP 程序计数器等)里存放的临时数据。
  • 保护上下文(保留学籍):当进程时间片耗尽被切走时,操作系统不能直接丢弃这些寄存器里的临时数据,必须先把当前 CPU 所有寄存器里的数据,全部拷贝保存到该进程自己的 PCB(task_struct)结构体里带走。
  • 恢复上下文(恢复学籍):等下一次该进程再次被调度上 CPU 时,操作系统先把之前保存在该进程 task_struct 里的上下文数据,原封不动地重新写回到 CPU 的各个寄存器中(尤其是恢复 EIP 寄存器),这样程序就能无缝接着上次被打断的地方继续往后运行!

极易混淆的核心考点:寄存器硬件 vs 寄存器里的上下文数据

这里有一个初学者最容易犯迷糊的关键点,一定要把**“寄存器硬件本身”和“寄存器里面存放的数据”**彻底区分开:

  • CPU 内部的寄存器硬件,物理上只有一套(一份)! 所有进程在运行时都要共享使用这套寄存器硬件。
  • 但是寄存器里面的数据(即上下文),可以有多份,分别对应不同的进程,属于各个进程私有!

这就好比 C 语言中的变量赋值 int a = 10;:

  • 变量 a 的内存空间(左值,对应 CPU 寄存器硬件)只有一份,大家都可以往里面写数据;
  • 但空间里面存放的数值(右值,对应 进程的上下文数据)在不同时间段可以反复修改为 1、2、3 等不同的值。
  • 上下文指的是寄存器当中的数据(右值),而不是寄存器硬件这个存储空间(左值)!

5. 直击 Linux 2.6 与 Linux 0.11 内核源码:亲眼看看 task_struct 与 tss_struct

为了让大家对 PCB 和上下文保存建立最直观的认识,我们直接翻开 Linux 内核源码来看一看。

5.1 Linux 2.6 内核中的 struct task_struct

在 Linux 2.6.18 内核源码中检索,可以找到进程控制块 struct task_struct 的定义。这个结构体极其庞大,光一个结构体定义就有几百行代码:

struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped (进程状态) */
    struct thread_info *thread_info;
    atomic_t usage;
    unsigned long flags;    /* per process flags, defined below */
    unsigned long ptrace;
    int lock_depth;         /* BKL lock depth */
    // ...
    int load_weight;        /* for niceness load balancing purposes */
    int prio, static_prio, normal_prio; /* 优先级相关 */
    struct list_head run_list;          /* 把所有进程任务连接起来的双链表节点 */
    struct prio_array *array;
    // ...
    pid_t pid;              /* 进程标识符,本质就是一个整数 */
    pid_t tgid;
    struct task_struct *real_parent; /* real parent process (when being debugged) */
    struct task_struct *parent;      /* parent process (父进程指针) */
    // ...
};

可以看到,前面我们讲的进程状态(state)、优先级(prio)、连接各个进程的双链表(run_list)、进程标识符(pid)、父子关系指针(parent)等,全都在这个结构体里一一体现!

5.2 上下文数据到底存在 task_struct 的哪里?(Linux 0.11 源码揭秘)

既然进程被切换时要把 CPU 寄存器的硬件上下文保存到 PCB 里,那么在内核源码的 task_struct 中,上下文到底保存在哪个字段里呢?

在早期的操作系统(如经典精简的 Linux 0.11 内核,全部代码加起来仅一万多行)中,就像五六十年代没有电脑教务系统时,辅导员把学生的全部档案装进档案袋交给学生自己背着走一样,老内核直接在 sched.h 的 struct task_struct 内部,结构体套结构体地内嵌了一个叫做 struct tss_struct tss(Task State Segment,任务状态段)的成员!

点进 Linux 0.11 的 struct tss_struct 定义,你会发现里面整整齐齐全是 CPU 寄存器的名字:

  • 包含了 eip(程序计数器)、eflags(标志寄存器)、eax、ecx、edx、ebx、esp、ebp、esi、edi 等通用寄存器,以及 es、cs、ss、ds、fs、gs 等段寄存器(全是 32 位字段,整个结构体在内存中占据约 104 字节的空间)。
  • 因为整个 task_struct 变量都位于内存的进程控制块里,所以当进程切换时,内核就是直接把 CPU 各个寄存器里的值,拷贝写到该进程 task_struct 内部所对应的 tss 结构体字段里!

补充说明:随着后来 CPU 技术和操作系统技术的发展,CPU 越来越复杂、寄存器越来越多,在新版内核(如 Linux 2.6)中,为了给 task_struct 减负,就把这个专门存寄存器上下文的结构体(如 thread_struct / thread_info)从 task_struct 本体中单独拎了出去,改由 task_struct 内部的一个指针去指向它(相当于学校有了电子档案室,学生手里只拿一个档案编号指针,通过指针依然能从 task_struct 找到上下文保存位置)。无论是老式做法还是新式做法,**“通过当前进程的 task_struct 保存和恢复 CPU 硬件上下文”**的核心逻辑完全一致!


6. 常见认知误区精准纠偏:四个关于“进程、PCB 与寄存器”的灵魂拷问

很多同学在学完这一节后,自己复述进程切换过程时很容易说出下面这段话:

❌ 错误复述:“一个进程运行是在 CPU 里面的寄存器里面的,当这个程序的时间片到了停止工作,会将上下文信息保存到 PCB 也就是 task_struct 里面,这个 PCB 会保存在内存的寄存器里面,当需要恢复的时候再将内存中的 PCB 恢复到 CPU 上面。”

这段话看似方向大体正确,但里面藏着几个非常致命的概念混淆!我们逐句来精准纠偏,帮你建立最严密的心智模型:

纠偏 1:“一个进程运行是在 CPU 里面的寄存器里面的”

  • 问题所在:进程本身(包含代码、数据、PCB)体积很大,全都是存放在内存里的,CPU 的寄存器一共才几十个字节,怎么可能装得下整个进程?
  • 准确表述:进程的硬件上下文(当前正在执行的指令指针 PC/EIP、栈指针、通用寄存器值等)在运行时位于 CPU 寄存器中,而不是“进程运行在寄存器里”。

纠偏 2:“时间片到了,停止工作,会将上下文信息保存到 PCB 也就是 task_struct 里面”

  • 评判:完全正确! 老内核(如 Linux 0.11)直接保存在 task_struct 内嵌的 tss 结构体里,新内核通过 task_struct 的指针保存在关联结构体里,本质都是通过 PCB 保存上下文。

纠偏 3:“这个 PCB 会保存在内存的寄存器里面”

  • 问题所在:内存里根本没有“寄存器”这种东西! 寄存器是 CPU 内部特有的高速硬件单元。
  • 准确表述:PCB(整个 task_struct 结构体变量)本身就位于内核内存中,因此保存进去的上下文数据(寄存器值)自然也就存放在内存中的 PCB 里。应该说“PCB 保存在内存中”。

纠偏 4:“当需要恢复的时候,再将内存中的 PCB 恢复到 CPU 上面”

  • 问题所在:恢复时绝对不是把“整个 PCB”搬到 CPU 上!因为 PCB 里面除了上下文数据,还有进程状态、PID、优先级、链表指针等几百行管理信息,这些东西根本不需要放到 CPU 寄存器里。
  • 准确表述:恢复时,内核从内存中的 PCB(或其指向的 TSS)里取出之前保存的寄存器值,写回 CPU 寄存器,进程即可从上次中断的断点处继续执行。

7. 【本篇小结】深入理解进程生命周期基石的三大核心心智模型

最后,我们用最精炼的三条核心结论,为本篇内容画上圆满的句号:

  1. 进程的存在模型:
    进程 = 内核数据结构对象(struct task_struct) + 自己程序的代码和数据。操作系统对进程的一切管理,本质上都是对内存中 task_struct 双链表的增删查改。
  2. 空间与内容的分离模型:
    CPU 内部的寄存器硬件(左值/空间)全系统只有一套,所有进程共享;但寄存器里面的上下文数据(右值/内容)可以有多份,归各个进程各自私有。
  3. 进程切换的标准流转表述:
    进程运行时,其硬件上下文(PC/EIP、栈指针、通用寄存器、标志寄存器等)位于 CPU 寄存器中。时间片到期发生进程切换时,内核把当前进程的 CPU 寄存器值保存到该进程的 PCB(task_struct) 中,而 PCB 本身位于内核内存里(切换时:CPU 寄存器 -> 内存中的 PCB)。下次恢复该进程时,内核从 PCB 中取出保存的寄存器值写回 CPU 寄存器(恢复时:内存中的 PCB -> CPU 寄存器),进程便从上次断开的地方继续向后执行。
Logo

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

更多推荐