我们平常查看进程,一般用 ps,或者看看 /proc/ 目录。

操作系统里同时跑着这么多进程,是靠什么把它们区分开、组织起来的?
一、PID:进程的身份标识
PID 是什么?
Process ID,每个进程运行的时候都有一个属于自己的编号,用来把不同的进程区分开。
所以 ps 这个命令,本质上就是去遍历内核里的那个进程链表,然后把拿到的信息格式化打印出来。
PID 放在哪里?
PID 是进程的一个属性,存放在 PCB(进程控制块)里面。
有个问题:学校的保安也在学校里面,那保安算不算学生呢?
------ 有点像这个意思:PID 虽然在 PCB 里,但它只是用来标识进程的一个字段,不能说它本身就是一个进程。
同一个进程再次启动,PID 可能会变
就像高考,不满意考上的院校,于是你二战。最后你又考上了同一所学校,但这次你的学号相对于之前可能就变了。
如何获取自己的 PID?
我们做的所有操作,最后都要变成一个进程来跑。
那如果我想在程序里拿到自己这个进程的 PID,该怎么做呢?
所有操作都得通过系统接口来完成,所以系统提供了一个获取 PID 的接口:getpid()。
二、PPID:父进程是谁
知道了自己的 PID,那它的"上级"是谁?
这就引出了 PPID------父进程的 PID。
每次登录云服务器,操作系统都会给我们新建一个 bash 进程作为当前会话的父进程,而原来的 bash 进程也不会消失,只是不再是你的父进程了。
我们所有的命令行操作,父进程都是 bash,即命令行解释器。
bash 只负责解释命令行,子进程运行出问题也不会影响 bash 进程------这就是通过创建子进程来实现的(后面会讲)。
三、常用命令
查看进程
Shell
ps ajx | head -1 # 看表头
ps ajx | grep xxx # 过滤想要的内容
杀死进程
Shell
kill -9 22146
四、进程的创建
我们上文一直提到进程,那进程是如何创建的?
当然也是通过调用操作系统提供的接口。
-
./xxx------ 命令行层面创建进程 -
fork()------ 代码层面创建进程
fork() 干了什么?
为父子进程分别创建 PCB,让子进程与父进程共享代码(代码是只读的,可以共享)。
创建子进程,本质上就是系统中多了一个进程。
为什么 fork 要有两个返回值?
是为了区分让不同的执行流执行不同的代码块。
一般而言,fork 之后的代码父子共享,但通过返回值可以分开执行不同逻辑。
为什么给子进程返回 0,给父进程返回子进程的 PID?
现实生活中,一个父亲能有多个子女,而这些子女只有一个父亲。
给父进程返回子进程 PID,是为了让父进程能区分和管理自己的多个子进程;给子进程返回 0,是因为子进程想要知道自己的父进程非常简单:getppid()即可。
一个函数是如何做到返回两次的?
关键在于:fork 在返回之前,进程创建的操作已经完成了。
这时候父子进程都已经存在,所以 fork 会在父子进程里各返回一次------父进程返回子进程的 PID,子进程返回 0。
从调用者的视角看,就像是 fork 一次调用,返回了两次。
一个变量怎么会有不同的内容?
任何平台,进程在运行时都具有独立性,互不影响。
因为数据可能被修改,所以为了确保独立性,父子进程不会共享"同一份"数据。
写时拷贝
为了节约系统资源,操作系统不会将父进程的数据完全拷贝给子进程,而是按需分配。
只有某一方尝试修改数据时,才会把对应的那一页内存拷贝一份,让父子各自拥有一份副本。
父子进程创建好后,谁先运行?
由调度器决定,没有具体的先后顺序。
但这对我们并没有什么影响,我们只关心最后进程能否正常运行。
五、进程状态
经典进程状态 :运行、阻塞、挂起
运行态与就绪态
-
运行态:进程正在 CPU 上真正执行。
-
就绪态:进程已经具备运行条件,在运行队列里排队,随时可以被调度上 CPU。
进程在被 CPU 执行前,都会先被链入一个"运行队列"(里面放的是就绪态进程),然后 CPU 从中一个个读取。
一个进程只要开始运行,是不是就要一直执行完毕才让出 CPU?
不是!
每个进程都有一个时间片 的概念。时间片用完后,内核会触发时钟中断,把 CPU 交给下一个进程。
这种分时扫描的执行方式,让我们感觉所有进程都在同时跑------这也是 RTOS 在单核 CPU 上跑出多任务(并发)的原理。
阻塞状态
学过单片机编程的都知道,单片机中的 delay 会导致等待。
但要注意区别:
-
裸机的
delay:CPU 在空转(忙等),仍然占用 CPU -
操作系统中的阻塞:进程主动让出 CPU,进入等待队列,CPU 去执行其他进程
操作系统管理硬件资源的方式,同样是"先描述,再组织"------这和我们单片机编程时配置 GPIO、UART、I2C、SPI 的思路是一致的。
被链入等待队列的进程,就处于阻塞状态。
挂起状态
当操作系统内部资源严重不足时,为了保证运行态进程正常运行,操作系统会将内存中处于阻塞态进程的代码和数据转移到磁盘中。
此时的进程就处于挂起状态。
绝大多数挂起都是阻塞挂起,运行挂起非常罕见。
六、Linux 中的具体进程状态
在初步了解了经典进程状态后,我们可以进一步看看,在 Linux 中进程具体有哪些状态。
一个进程的状态,取决于它的 PCB 被放在了哪个队列里,和进程的代码、数据所在位置关系不大。
就像你属不属于这个学校的学生,并不取决于你本人是否在学校里,而是你的个人档案是否在学校里。
Linux 中常见的进程状态:
R(运行状态,TASK_RUNNING)
并不意味着进程一定在运行中,它表明进程要么正在运行,要么在运行队列里排队(即就绪态)。
S(睡眠状态,TASK_INTERRUPTIBLE)
可中断睡眠,意味着进程在等待某个事件完成(如等待输入)。
例如执行 scanf 时,进程就处于 S 状态,等用户输入完成后被唤醒。
所以睡眠态常见于等待输入/输出外设。
D(磁盘休眠状态,TASK_UNINTERRUPTIBLE)
也叫不可中断睡眠。这种状态的进程通常在等待 I/O 结束(如向磁盘写入数据)。
为什么需要 D 状态?
当系统内存严重不足时,哪怕用上了 swap(内存交换)也撑不住了,操作系统就会开始杀进程,挑一个"看着不太忙"的干掉,腾出资源。
想象一下:A 进程正在向磁盘写入机密数据,此时系统资源紧张,操作系统如果"吧唧"一下把看似空闲的 A
进程杀了,磁盘数据写入到一半发现没人管了,就会出问题。
D 状态就是为了保护这类关键 I/O 操作不被中断------让进程在等磁盘 I/O 的时候,操作系统杀不动它。
所以,如果你能看到 D 状态的进程,说明你的操作系统可能已经快撑不住了。
T(停止状态,TASK_STOPPED)
可以通过发送 SIGSTOP 信号暂停进程,此时进程进入 T 状态;发送 SIGCONT 信号可让其继续运行。
停止态和睡眠态的区别在于:睡眠态一定是在等待某些资源(如输入),而停止态可能只是单纯被暂停,比如我们调试程序时,断点处就是停止态。
X(死亡状态,TASK_DEAD)
这个状态只是一个返回状态,你不会在任务列表里看到它。此时进程进入回收队列,释放占用的资源。
我们用
ps查看进程时,看到的大多是 S 状态(可中断睡眠),而不是 R 状态。因为绝大多数程序运行过程中,大部分时间都在"等待"------等时间片、等 I/O、等用户输入。
而CPU 执行速度极快,每个进程的时间片一眨眼就用完了。你敲
ps的那一刻,它大概率正在排队,而不是正好在 CPU 上跑。

七、僵尸进程
什么是僵尸进程?
当子进程退出,而父进程没有调用 wait() 或 waitpid() 读取子进程的退出状态时,子进程就会进入僵尸状态(Zombie) 。
僵尸进程会以终止状态保留在进程表中,等待父进程读取它的退出码。
为什么要有僵尸状态?
进程退出后,它的退出状态(成功还是失败,返回码是多少)必须被维持下去,因为它要告诉父进程:你交给我的任务,我办得怎么样了。
如果父进程一直不读取,子进程就一直处于 Z 状态,它的 PCB 也一直保留着。
僵尸进程的危害
一个父进程创建了很多子进程却不回收,这些子进程的 PCB 就会一直占用内存,造成内存泄漏 。
怎么避免?后面讲。
就像案发现场,警察不会直接处理后事,而是先保护现场、收集信息,然后通知家属。如果家属一直不来,案发现场就不得不一直保持原样。
八、孤儿进程
什么是孤儿进程?
如果父进程先退出,而子进程还在运行,那么子进程就成了"孤儿进程"。
孤儿进程会被 1 号进程(init 进程)收养,后续由 init 负责回收。
为什么要领养?
因为孤儿进程将来也会退出,必须有人负责回收它占用的资源。
领养后,子进程的父进程就变成了 1 号进程。
九、进程优先级
什么是优先级?
CPU 资源分配的先后顺序,就是指进程的优先权。
优先级高的进程有优先执行的权利。
优先级和权限的区别
-
优先级:决定使用资源的先后顺序
-
权限:决定你是否有权使用资源
为什么要引入优先级?
因为 CPU 资源是有限的,而进程是多个的,所以进程之间天然存在竞争性 。
操作系统必须保证大家良性竞争,所以需要确定优先级。
如果一个进程长时间得不到 CPU 资源,它的代码就无法推进------这就是进程饥饿问题。
很好理解:每天打饭都要排队,为什么学校不给我们每个人配个厨师?因为资源有限嘛。
如果学校不保证良性竞争,身体弱的学生就可能一直抢不到饭。
PRI 和 NI
-
PRI:进程的优先级,值越小,优先级越高,越早被执行。
-
NI(nice) :进程优先级的修正值。
最终优先级计算公式:
PRI(new) = PRI(old) + nicenice 的取值范围是 -20 到 19,共 40 个级别。
nice 为负值时,优先级变高;nice 为正值时,优先级变低。
需要强调:nice 值不是优先级,而是优先级的修正数据。
能随意修改优先级吗?
理论上可以(如裸机开发场景下),但操作系统不允许你随意调整,它只允许你在规定范围内调整(通过 nice 值)。
查看进程优先级的命令
-
ps -l可以查看 PRI 和 NI -
top命令可以动态查看并调整已存在进程的 nice 值(在 top 界面按r键,然后输入 PID 和新的 nice 值)
十、其它概念
-
竞争性:系统进程数目众多,而 CPU 资源有限,进程之间具有竞争属性。为了高效完成任务,合理竞争资源,便有了优先级。
-
独立性:多进程运行时,各自独享资源,互不干扰。
-
并行 :多个进程在多个 CPU 上同时运行。
-
并发:多个进程在一个 CPU 上通过进程切换的方式,在一段时间内都得以推进。
十一、bash 与子进程的关系
现在我们再回头看最开始提到的 bash。
我们所有的命令行操作,父进程都是 bash,那 bash 如何保证它创建的进程与自己互不影响?
------ 通过创建子进程来完成。如何创建?fork()。
这也是 bash 执行命令解释的基本方式:fork 出一个子进程,然后子进程去执行命令,父进程(bash)继续等待或做自己的事。