《从零入门Linux系统篇(十七):进程篇·一——Linux进程本质:从PCB到fork创建机制》
上篇我们初探了“进程”这片领域,了解了冯·诺依曼体系结构的硬件骨架,也理清了操作系统与软硬件之间的层级关系。整篇文章里,有一句话最重要:
“先描述,再组织。”
这六个字,是贯穿整个进程管理的核心思想。今天,我们从这句话出发,正式开始拆解“进程”这个概念本身,看看操作系统到底用什么方式描述一个进程,又是如何组织成千上万个进程的。坐稳,我们发车。
目录
1.2.1 PCB(task_struct)——进程存在的唯一标识
结论一:进程不仅是代码和数据,而是PCB + 内存中的代码和数据
3.5 3.5 fork之后为什么同一个变量会拥有不同的值?
一、认识进程——从程序到运行实体
1.1 教科书中的进程概念
- 翻开教科书,进程的定义通常是这样写的:程序的一个执行实例,或者说正在执行的程序。
- 站在内核的视角,它被描述为担当分配系统资源(CPU 时间、内存)的实体。
- 再精简一点,当下的主流认知是:进程 = 内核数据结构(task_struct)+ 自己的程序代码与数据。
这些概括当然精准。它们是由已经完全理解了进程的人,站在高处往回看时提炼出的结晶。但如果我就这么把这几句话甩出来,然后直接翻到下一节,未免太不负责任了。这种浓缩的定义像一块压缩饼干——营养或许够,但直接拿来给初学者吃,多半会噎着,尝不出它本来的味道。所以,在记住上面那几行字之前,我们得先直面那个最原初的问题:进程,说到底到底是什么?
1.2 Linux中的进程本质
1.2.1 PCB(task_struct)——进程存在的唯一标识

上一篇文章我们理清了一条主线:程序在没被运行的时候,只是老老实实躺在磁盘上的文件;一旦跑起来,它就必须被加载到内存里。那么问题来了,成百上千个程序同时挤在内存中,操作系统凭什么把它们管得井井有条?
答案就是我们反复强调的那六个字:先描述,再组织。
操作系统做的第一件事,不是急着让程序跑,而是先拿出一个本子,把这个程序的各种关键信息一笔笔记下来。这个“本子”,就是PCB。
PCB,全称Process Control Block,即进程控制块。你可以把它理解成操作系统给每个进程办的“户口本”。进程在系统里是什么身份、占了多少资源、当前跑到哪里了、接下来该谁上CPU,这些生死攸关的信息,全登记在这本户口本上。
在Linux操作系统中,用来充当这个“户口本”的结构体,就叫struct task_struct。PCB是抽象的设计概念,而task_struct是Linux 内核里把这个概念落地的具体实现。它里面塞了上百个字段,几乎涵盖了进程的一切属性。我们挑几个最核心的提前混个脸熟,后面的文章里,它们都会陆陆续续走到前台:

- 标识符:每个进程的唯一ID,就跟我们的身份证号一样,靠它来区别茫茫人海中的每一个进程。
- 状态:任务当前是跑着呢、歇着呢,还是僵死了?退出信号和退出码也记在这里。
- 优先级:相对于其他进程,谁更有资格先上CPU。
- 程序计数器:记录下一条即将被执行的指令的地址,告诉CPU:“待会儿从这儿开始接着往下跑。”
- 内存指针:指向这个进程的代码段、数据段,以及它跟别人共享的那块内存。
- 上下文数据:进程被切下CPU时,必须把当时寄存器里的值保存起来——就像学生休学时要把当前的学习进度、笔记和考场座位号全部打包封存一样。等将来复学(重新被调度上 CPU),才能原封不动地接着往下学。这些寄存器里的快照数据,就是上下文。
- I/O 状态信息:包括进程申请过的I/O请求、分配给它的设备和它正打开着的文件列表。
- 记账信息:进程总共消耗了多少CPU时间、用了多少个时钟周期、有没有被设过执行时限等等。这些账目,操作系统都一笔笔记着。
1.2.2 操作系统如何组织进程——进程链表
有了“描述”的户口本(task_struct),下一步就是“组织”。操作系统是怎么把成百上千张户口本管起来的?做法很朴素,也很经典,在每个task_struct 里面塞一个指针,然后把它们一个接一个串起来,形成一个链表。没错,操作系统管理进程的核心数据结构,就是一条双向链表。这意味着什么呢?对进程的所有管理操作,最终都落地成了对这条链表的增、删、查、改。
- 增加:新进程诞生时,创建一个新的PCB,把它挂进链表里,再让它指向新加载进来的代码与数据。一张新户口本上好了户口,进程就算正式在系统里注册了。
- 删除:进程跑完了,代码退出了,操作系统就把它对应的PCB从链表中摘下来,户口本注销,这个进程在系统里的一切痕迹随之烟消云散。
- 修改:想改动一个进程的状态、优先级,或者记录它刚消耗的CPU时间?操作系统不会直接去碰那个进程的代码和数据,而是顺着链表找到它的PCB,在户口本上改几笔——进程的下一次调度、下一次资源分配,就会按照更新后的信息来执行。

但光这么说,进程链表在脑子里还是飘着的,不够实在。我们打个比方。
想象你去一家大厂面试。长长的走廊里排满了人,一个一个等着见面试官。面试官就是CPU,你们这些应聘者就是一个个进程。但真实的排队场景,并不是你们本人一直站在那里挪着步子,而是你们的简历在排队。你本人(代码和数据)可能正坐在等候区的椅子上啃面包、翻笔记,而你的简历(PCB)被收上去,放在一摞简历里,排着队等面试官叫号。面试官只看简历了解你这个人,叫你的时候你才走到桌前坐下,开始展示你的能力。
把这个场景翻译回操作系统:我们人是代码和数据,简历就是对应我们人的PCB。CPU调度时,摸到的不是一个个活生生的程序本身,而是那条由PCB串起来的链表。它从链表中挑出一张简历(PCB),根据简历上记录的信息恢复那个程序的执行现场,然后把CPU交出去。程序跑完了时间片,CPU又把当前的执行进度更新回这张简历,把它塞回链表末尾,接着翻下一张简历。
于是,“先描述,再组织”这六个字,在进程管理这里就彻底落地了:
-
描述 = 为每个进程创建一张户口本(PCB/task_struct),把它的身份、状态、优先级、寄存器快照、内存位置等所有关键信息填进去。
-
组织 = 把这些户口本用链表串起来,形成一个操作系统随时可以遍历、查找、增删的队列。
-
管理 = 对这个链表执行增删查改,创建进程是往链表里插新节点,销毁进程是从链表里摘掉节点,调度进程是遍历链表挑下一个节点,修改进程属性是在对应节点里更新字段。

1.2.3 两个核心认知+重新理解进程
把前面的铺垫收拢起来,我们可以从中提炼出两个结论,它们很可能跟你最初对“进程”的想象不太一样,甚至有点颠覆。但正是这两个结论,构成了操作系统管理进程的全部基石。
结论一:进程不仅是代码和数据,而是PCB + 内存中的代码和数据
我们通常以为,程序跑起来就是进程。但准确地说,程序代码跑在内存里,只是进程的一半;另一半,是操作系统为它建立的PCB。没有PCB,代码和数据就是一堆没人管的孤魂野鬼;有了PCB,这堆代码和数据才在系统里有了户口、有了身份、有了被调度和管理的资格。所以,别再说“进程就是运行的程序”——进程,是“运行的程序加上描述它的户口本”。
结论二:操作系统管理进程,本质是管理PCB数据结构
操作系统从来不直接操作进程的代码和数据,它只认PCB。创建进程,是在链表里插入一个新节点;销毁进程,是从链表里摘掉一个节点;调度进程,是遍历链表选出下一个节点;修改进程属性,是在对应节点上更新几个字段。代码和数据该怎么跑就怎么跑,操作系统只通过PCB这个“代理”来间接管理一切,这种设计,正是操作系统经典的解耦思想。

二、初识系统调用——从观察进程开始理解Linux运行机制
2.1 系统调用的存储位置
理论铺垫了这么多,现在是时候第一次触碰实实在在的系统调用了。在动手之前,我们得先知道它们“住”在哪里。Linux提供了man手册,这是一套离线的文档系统,里面分门别类地记录了几乎所有的命令、系统调用和库函数。跟进程相关的接口,主要分布在两个章节里:
- man 2:系统调用。这是操作系统内核暴露给用户层的直接入口,比如fork、wait、exit,都在这一章。
- man 3:库函数。这是C标准库或第三方库为我们封装好的函数,它们底层可能会调用系统调用,但对开发者屏蔽了细节。比如printf、malloc、pthread_create,属于这一章。
区分这两者有一个很直观的方法:如果某个函数的文档页右上角写着“Linux Programmer's Manual” 并且章节号是2,它就是系统调用;如果章节号是3,它就是库函数。记住查文档的通用格式:
man 2 fork # 查 fork 这个系统调用的文档
man 3 printf # 查 printf 这个库函数的文档
数字2和3就是章节号。不写数字的话,man默认从第1章开始搜,有时会搜到同名命令的文档而不是函数的文档,比如man printf可能会先给你展示shell命令printf的用法,而不是C语言的printf函数。查函数的时候,最好养成带章节号的习惯。

2.2 第一个系统调用——获取进程信息
理论说再多,不如亲手写一行代码。下面这段程序,就是我们叩开系统调用大门的第一砖。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while (1)
{
sleep(1);
printf("我是一个进程!,我的 PID: %d\n", getpid());
}
}
这里出现了三个头文件和两个陌生的函数调用,逐个解释一下:
- <unistd.h>:这是访问POSIX系统调用的门户。像getpid、fork、sleep这些与操作系统内核直接打交道的函数,它们的原型大多定义在这个头文件里。没有它,编译器就不认识你写的getpid是什么。
- <sys/types.h>:提供了系统级数据类型定义。比如getpid的返回值类型pid_t,它本质上是一个整数,但用专门的类型名封装了一层,语义更清晰。
- sleep(1):让当前进程进入休眠状态,一秒钟。这里暂时不展开——休眠是进程状态的一种,后面讲到进程状态时会详细拆解它的底层机制。现在只需要知道它让程序每隔一秒跑一次,而不是疯狂刷屏。
- getpid():这是我们接触到的第一个真正的系统调用。它做的事情很简单:向内核发起请求,查询当前进程的唯一标识符PID,然后把结果返回给你。这个过程,就是一次完整的“用户态 → 内核态 → 用户态”调用穿越。PID是进程在系统里的身份证号,全局唯一,操作系统正是靠它来区分千千万万个正在运行的进程。
编译并运行这段代码,终端会每隔一秒打印一行当前进程的PID。按Ctrl + C即可终止程序。除了getpid,还有一个与其紧密相关的系统调用需要一并介绍getppid,用于获取父进程的PID。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while (1)
{
sleep(1);
printf("我是一个进程!,我的 PPID: %d\n", getppid());
}
}
关于父进程,只需要先记住两点:
- 每一个进程都有一个创建它的父进程。你在终端里运行./myproc,这个myproc的父进程就是你正在使用的那个Shell(比如bash)。
- getppid返回的就是这个父进程的PID。后面讲fork的时候,父子进程的关系会成为核心主线,现在先混个脸熟就好。
2.3 查看进程状态与运行信息
进程跑起来了,如何亲眼看到它?答案是一个经典到不能再经典的命令:ps。加上一组选项,就能把系统里所有进程的详细信息列出来。
ps axj
这行命令的四个字母各有含义,拆开来看:
- a:显示所有用户拥有的进程,不限于当前终端。
- x:把那些没有绑定到任何终端的后台进程也显示出来。
- j:以“作业控制”格式输出,额外显示进程组ID、会话ID等字段,方便梳理父子关系。
- -:这个参数本身不带减号(即BSD风格),所以直接写成ps axj,不需要加-。
结合我们前面写的程序,编译运行后,打开另一个终端窗口,输入上述命令,就能看到它的PID、PPID(父进程ID)、状态等关键字段,这就是我们之前写在task_struct里那些信息的快照。

为了更高效地观察进程,我们通常会把ps和管道工具组合起来,再加上一行简单的脚本,搭出一套实时监控的小工具。
- 基础过滤:用grep '文件名'或grep '关键词'从进程列表中筛选出目标进程。
- 去除干扰:每次执行grep,它自己也会作为一个进程出现在ps的输出里,干扰结果。加上grep -v grep,就是把名字里带"grep"的那行多余输出过滤掉。
- 保留标题栏:ps输出的第一行是字段名(如 PID、PPID、STAT 等),直接grep会把它也筛掉。用head -1先把这一行单独抓出来显示,下面再跟过滤后的结果,看起来又清晰又完整。
- 实时监控脚本:把上面这些组合进一个while循环,配合sleep,就能在终端里持续刷新某个进程的状态:
while :; do ps axj | head -1 && ps axj | grep 'myprocess' | grep -v grep; sleep 1; done

PID不是一成不变的。当你Ctrl + C终止进程再重新运行时,它会拿到一个全新的PID,每一次启动都是一次新的加载、一张新的PCB,内核自然会分配一个新的身份编号。这里顺便补充一个终止进程的硬手段:除了Ctrl + C,还可以通过信号直接结束进程
kill -9 进程PID
-9对应的是SIGKILL信号,进程无法忽略、也无法捕获,内核直接终止它。这是最后通牒,当普通kill或Ctrl + C都不管用时,kill -9就是最终手段。不过,在满屏变化的PID中,细心的你肯定察觉到了,有一个PID始终不变。查一下它的名字,你会发现它就是bash。

也就是说,那个整天趴在终端里,不断打印 [abc@…]$这样的提示符,然后阻塞着等你输入命令的东西,它本身也是一个进程。操作系统为每一个登录用户都创建了一个bash进程,它就是你跟系统对话的传声筒。(bash前面如果带一条横杠,写成-bash,那是登录shell的标志,表示远程登录时自动启动。)

除了ps,Linux还提供了另一种更“Linux 风格”的进程查看方式:直接去/proc目录下看。系统把每一个运行中的进程都转化成了/proc下的一个同名文件夹,文件夹名就是进程的PID。进程启动,文件夹自动出现;进程终止,文件夹立刻消失。你甚至可以直接进入某个进程的文件夹:

cd /proc/21381

里面全是该进程的运行时信息,状态、内存映射、打开的文件描述符、命令行参数等等,都是纯文本,可以直接cat出来。这正是Linux “一切皆文件” 哲学的生动体现。
好,关于这里,我还有两个小知识点要补充。
2.4 进程与工作目录的关系
在/proc/进程PID/目录下,有两个特殊的链接文件值得专门拿出来讲讲:

- exe:指向当前进程可执行文件的完整路径,比如 /home/abc/code/merge_class/lesson/myprocess。它告诉进程“自己是从哪里来的”,它的代码和数据在磁盘上到底存在哪个位置。这个链接在进程运行时一般用不到,但当你怀疑某个进程“来路不明”时,顺着exe 一路查过去就能找到它的二进制真身。如果把这个可执行文件本身删了,进程照样跑,但exe链接会变成红色警告状态,因为它指向的磁盘文件已经没了。
- cwd:指向进程的当前工作目录,比如/home/abc/code/merge_class/lesson。这玩意在代码里用相对路径读写文件时极其关键,当你写fopen("hello.txt", "w"),系统会自动把cwd的路径和这个文件名拼接起来,最终确定文件在磁盘上的落点。这也解释了为什么你运行一个可执行程序后,新建的文件默认都出现在它的同级目录下:因为进程启动时的cwd,是从父进程(通常是Shell)继承过来的,而Shell的cwd就是你当时./myprocess时所在的目录。
更妙的是,进程可以在运行期间通过chdir系统调用动态切换自己的工作目录:

#include <unistd.h>
chdir("/home/abc"); // 把工作目录切到 /home/abc
fopen("hello.txt", "a"); // 此时文件会创建在 /home/abc 下
有没有这么想过?Shell本身也是一个进程。你在Shell里敲cd命令能切换目录,它底层封装的,正是这个 chdir 系统调用。事实确实如此,Shell调用chdir修改了自己的cwd,之后你用ls、touch 这些命令时,它们都继承Shell的cwd,自然就操作在了新目录下。一层层传下去,环环相扣。
三、进程之间的关系——父子进程模型
3.1 进程树——Linux中的层级关系
Linux下的进程,遵循的是一套严格的“单亲繁殖”规则——每个进程只能由一个父进程创建,但一个父进程可以生出一堆子进程。这套繁殖体系层层展开,最终形成了一棵从根部向下延伸的多叉树,我们称之为进程树。
在这棵树的顶端,蹲着系统启动后的第一个进程(通常是init或systemd),它是所有进程的最终祖先。顺着树干往下,分叉出各种系统服务、守护进程,再往下,就是一个个用户登录后启动的Shell。而我们敲下的每一条命令ls、cat、gcc、./myprocess它们全都有一个共同的父进程:bash。换句话说,bash就是这棵树上你所有命令的那根共同的“母枝”。你每敲一次命令,bash就在自己的枝条上发一个新芽(创建一个子进程),命令跑完,芽谢了(进程终止),bash继续等着发下一颗芽。
打开终端,敲一下ps axj | grep bash,你会看到bash安安稳稳地挂在那里,PID基本不变。而它下面不断冒出来又消失的那些进程,就是这棵树上生生不息、更迭不止的叶子。这套进程树的层级结构,是理解后面fork创建子进程、父进程等待子进程回收等一切进程间关系的地基。

3.2 子进程创建机制——fork系统调用
进程该怎么创建?这就不得不搬出Linux下最核心也最“反直觉”的一个系统调用fork()。
头文件:#include <unistd.h>
fork()就是进程界的“分身术”。调用一次,返回两次,原地复制出一个几乎一模一样的子进程,让程序真正做到“分头行动”。
具体来说,fork干了三件事:
- 创建分身:产生一个子进程,这个子进程完整继承了父进程的代码段和数据段,调用fork的那一瞬间,父子进程的代码和数据几乎一模一样。但它们的PID不同,PCB独立,从此各走各的路。
- 并发处理:父子进程同时运行,互不干扰。父进程可以继续处理主逻辑,子进程去执行另一条任务线,一个程序,两条执行流,并行推进。
- 环境隔离:子进程运行在独立的地址空间中。它如果崩溃了,不会拖累父进程;父进程如果退出了,子进程也可以被init进程接管,不至于变成孤儿。

我们来写一段代码,然后慢慢拆开看。
#include <iostream>
#include <unistd.h>
#include <string>
#include <sys/types.h>
using namespace std;
void Func_Test_Process()
{
pid_t _id = fork();
if (_id < 0)
{
string strerror("fork fail");
throw strerror;
}
else if (_id == 0)
{
while (1)
{
printf("这是一个子进程,子进程的 PID: %d,子进程的 PPID: %d\n",
getpid(), getppid());
sleep(1);
}
}
else if (_id > 0)
{
while (1)
{
printf("这是一个父进程,父进程的 PID: %d,父进程的 PPID: %d\n",
getpid(), getppid());
sleep(1);
}
}
}
int main()
{
try
{
Func_Test_Process();
}
catch (string& str)
{
cout << str << endl;
}
}
运行起来你会发现:父子俩各跑各的循环,一个打印“子进程”,一个打印“父进程”,互不干扰。这背后到底发生了什么?
fork()调用的那一刻,操作系统干了件很“偷懒”又很巧妙的事,它没有从头重新构造一个完整的子进程,而是直接把父进程的PCB抄了一份,然后改了改其中几项关键信息:PID换成新的,PPID指向父进程,记账信息和某些状态字段归零。至于进程的代码和数据?父子俩暂时指向同一块物理内存。这样一来,子进程“天生”就站在了fork()调用之后的下一行代码上,跟父进程完全相同的执行位置。接下来的代码,父子俩各走各的路:_id等于0的那条路通向子进程的死循环,_id大于0的那条路通向父进程的死循环。两个进程,同一份代码,分道扬镳,并行推进。fork()之前,系统里只有一条执行流在跑;fork()之后,突然就多出了一条一模一样的执行流,它们从同一行代码出发,各自奔向不同的分支。这就是进程分身的全部秘密。


为什么父进程拿到的是子进程 PID,而子进程拿到的是 0?
这背后是一套非常务实的逻辑。父进程作为管理者,必须知道每个子进程的身份证号,不然将来怎么回收资源、统一调度?而子进程根本不需要让fork()再返回父进程的PID,想找父亲,随时调一下 getppid()就行,没必要多此一举。
读到这里,你心里大概正在翻涌着一堆问号。没关系,fork()就是这么一个东西,它会把你从C到C++到数据结构一路攒下来的代码直觉,当场撞得七零八落。
摆在我们面前的有三个问题,每一个看起来都像在公然挑衅编程语言的基本规则:
- if和else if,凭什么能同时执行?
- 一个局部变量,凭什么能同时等于0,又大于0?
- 一个函数,凭什么能返回两个不同的值?
3.3 一个函数为什么可以产生两个返回值?
我们先挑第三个问题来动手,一个函数,凭什么能返回两个不同的值?

先问一个更基本的问题:一个函数执行到return的时候,它的核心任务做完了没有?答案是做完了。fork也不例外。也就是说,在fork内部那条return语句真正执行之前,所有该干的活儿早就干完了
- 申请新的PCB
- 把父进程的PCB拷贝一份给子进程
- 把子进程的PCB挂进进程链表
- 甚至已经把它塞进了调度队列里!
精确到语句层面,子进程被创建出来的时间点,一定在return之前。所以到了return这一步,系统里已经不是只有原来的那个进程了,两个进程同时站在return面前,各自拿走属于自己的返回值。父进程拿子进程的PID,子进程拿0。
所以结论很直白:不是“一个函数返回了两个值”,而是“一个调用,两次返回,各自只看到一个”。 从单个进程的角度看,fork()只返回了一次,跟普通函数没有任何区别。fork的“魔法”,说到底,是操作系统在return之前悄悄多造了一个进程出来。
3.4 为什么if和else if可以同时执行?
在之前的认知里,if和else if水火不容,一个条件成立,另一个必然被跳过。但到了Linux系统编程这里,这条铁律被“进程”这个新维度硬生生撕开了一道口子。先厘清一个核心概念:代码是共享的,但执行流是独立的。
正如前面所说,当fork的核心逻辑执行完毕时,系统里已经不再是只有一条执行流了,父进程和子进程同时存在,各自持有独立的PCB,各自拥有一条独立的执行路径。它们面前摆着同一份代码,但手里攥着不同的_id值:
- 父进程手里是_id > 0(那是子进程的PID),于是if (_id == 0) 对它不成立,else if (_id > 0) 命中,一脚踏进自己的while循环。
- 子进程手里是_id == 0,于是if (_id == 0) 直接命中,它走进了另一个while循环。
所以,不是“一个进程同时执行了两个分支”,而是两个进程,各自挑了属于自己的那条路走。只不过它们几乎同时在屏幕上刷出打印信息,才让你产生了一种“代码逻辑被违背”的错觉。
我只能先讲到这里。这个问题还有更深的一层,为什么父子进程能持有同一份代码却不互相踩踏?这牵扯到进程地址空间的隔离机制,后面讲到进程地址空间的时候,我们会把这块拼图彻底补上。
3.5 3.5 fork之后为什么同一个变量会拥有不同的值?
这大概是最让人崩溃的地方。同一个变量_id,凭什么在父进程眼里是一个大于0的整数,在子进程眼里却是0?这违反了我们学过的所有编程常识。答案藏在一块Linux内存管理的核心基石里,写时拷贝。
- 刚fork完那一刻:子进程的页表指向的物理内存,跟父进程是同一块。也就是说,父子俩最初确实共享着同一个_id的内存空间,物理上只存了一份数据。
- 写入发生时:当fork准备把返回值写进_id的时候,操作系统发现有两个进程想动这块内存。共享读没问题,但一旦有人要写,就必须拆伙,不然一个改了值,另一个会莫名其妙跟着变。
- 触发拷贝:操作系统当场为子进程另外开辟一块物理空间,把父进程当前的内容原样拷过去,然后修改子进程的页表映射关系,让它指向这块新空间。从此,父子俩的_id虽然名字一样,指向的却是两块完全不同的物理内存。
于是,你在父进程里看_id,它映射的那块内存里躺着一个大于0的PID。你在子进程里看_id,它映射的另一块内存里躺着一个干干净净的0。名字相同,地址独立,值当然可以不同。从这个机制里,我们还能拽出一条更底层的结论:进程具有独立性。
两个进程的PCB从一开始就是独立的两张户口本,代码段只读共享,谁也不会改谁。数据段呢?写时拷贝这道墙一立起来,你改你的、我改我的,互不干扰。后面讲到进程程序替换时会对“代码不可修改”这一点做修正,现在不妨先把它当作一个阶段性的结论记住。
3.6 特殊进程:守护进程(了解)
守护进程和精灵进程,指的是同一种东西。中文叫“守护进程”,英文叫Daemon,原意是“精灵”或“守护神”,所以有些翻译也称之为“精灵进程”。两个名字,一个意思。
什么是守护进程?它是一类生存期极长的进程。系统启动的时候它们被悄悄唤醒,系统关闭的时候它们才退出舞台。你在桌面上的任何操作,几乎都感觉不到它们的存在,但整台机器的正常运转,处处离不开它们。守护进程有三个核心特征,记住就行:
- 后台运行:它没有控制终端(TTY)。不像我们前面写的那些程序,霸着终端窗口,printf 全往屏幕上刷,守护进程不干这种事,它安安静静退到后台,你根本看不见它的输入输出。
- 独立于终端:即使你关掉了当前登录的Shell窗口,它照样在跑。用户的登录、注销,对它毫无影响。它不属于任何一个具体的终端会话,它属于整个系统。
- 周期性运行或长期等待:有的守护进程在等一个事件,比如Web服务器静静守在80端口上,等浏览器来敲门。有的守护进程则周期性地醒来干活,比如定时清理磁盘、备份日志,干完继续睡。
看完这三个特征你会发现,守护进程和我们前面写的那些“前台进程”,几乎是两种完全不同的生物。一种是终端里的短跑选手,一种是系统的长跑耐力选手,Linux的进程世界里,二者缺一不可。
感谢阅读,我们下篇见。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)