解析进程:从原理到实践
本文介绍了进程的概念、本质及管理方法。进程是程序的执行实例,每个进程拥有独立的内存和资源。操作系统通过进程控制块(PCB/task_struct)管理进程。文章详细讲解了进程创建(fork)、状态(R/S/D/T/Z)、优先级(PRI/NI)以及进程切换等内容。特别分析了僵尸进程和孤儿进程的产生与解决方案。最后阐述了进程存在的意义,包括并发执行、资源隔离、权限控制等。通过系统调用和内核机制,进程实
目录

进程的本质
进程是程序的执行实例,是资源分配的基本单位。这么说太官方了,实际上,跑起来的程序就叫做进程。每个进程都有自己的内存和资源,互不打扰,各行其是。比如在电脑上同时打开浏览器和VSCode,它们就是两个不同的两个进程。

更具体的说明就是,通过命令ps aux查到的全是进程。

进程在内核中的描述——task_struct
通过以上对进程的简单理解,不难发现计算机中的进程是很多的。这里提出两个问题,第一个问题:那么多的进程,操作系统要不要将它们管理起来?第二个问题:怎么管理?
不管是从操作系统的安全性还是系统性能上来讲,那么多的进程肯定是要被管理起来的。管理的方法论基本上是固定的——先描述,再组织。也就是说,先把每一个进程的相关信息保存到一个结构体中,然后再用链表把这些结构体组织起来,这样,操作系统就可以对系统中的所有进程进行管理了。保存进程相关信息的结构体叫做进程控制块PCB(process control block),在Linux中,这个进程控制块叫做task_struct。
//task_struct部分源码
pid_t pid; // 进程的唯一标识符
pid_t tgid; // 线程组 ID
volatile long state; // 任务状态
int exit_state; // 退出状态
int exit_code; // 退出代码
int exit_signal; // 退出信号
int prio; // 动态优先级
int static_prio; // 静态优先级
struct mm_struct *mm; // 内存管理结构
struct vm_area_struct *mmap; // 虚拟内存区域链表
struct files_struct *files; // 文件结构体
struct fdtable *fdt; // 文件描述符表
//...
创建进程
系统提供了创建进程的接口fork()。
pid_t fork(void);
fork函数值得一讲的就是它的返回值。如果子进程创建成功,父进程接收到的返回值是子进程的pid,子进程接收到的返回值是0.如果子进程创建失败,那么父进程接收到的返回值是-1。下面演示一下如何创建进程。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == -1) {
// fork failed
perror("fork");
}else if (pid == 0) {
// child process
printf("Child process:%d\n", getpid());
}else{
// parent process
printf("Parent process:%d\n", getpid());
}
return 0;
}
//执行结果:
Parent process:7483
Child process:7484
在创建完子进程后,父进程和子进程会从fork()的下一行开始执行,所以一般用 if 来进行分流执行。
进程的状态
在Linux内核中,进程的状态分为以下几种。
static const char *const task_state_array[] = {
"R (running)", /*0 */
"S (sleeping)", /*1 */
"D (disk sleep)", /*2 */
"T (stopped)", /*4 */
"t (tracing stop)", /*8 */
"X (dead)", /*16 */
"Z (zombie)", /*32 */
};
下面介绍常见的进程状态。
R运行状态(running): 并不意味着进程⼀定在运行中,它表明进程要么是在运行中,要么在运行队列里。
• S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
• D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
• T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。其他进程可以通过发送 SIGCONT 信号让这个被暂停的进程继续运行。
• Z状态(zombie):父进程没有读取子进程的退出信息就会出现僵尸进程。
下面,介绍一个查看进程状态的命令:ps aux / ps ajx
a:表示显示所有进程。
u:以用户友好的格式显示进程信息。通常包括进程的用户、PID、CPU 和内存使用率等信息。
x:显示没有控制终端的进程。

下面介绍两个特殊进程:僵尸进程和孤儿进程。
先来说说僵尸进程,它的成因是子进程退出后,父进程没有读取子进程的退出信息,导致子进程成为僵尸进程。下面写一段代码演示僵尸进程的产生。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
}else if (pid == 0){
printf("Child process[pid:%d]\n", getpid());
sleep(3);
exit(EXIT_SUCCESS);
}else {
printf("Parent process[pid:%d]\n", getpid());
sleep(30);
}
return 0;
}
//运行结果:
Parent process[pid:9072]
Child process[pid:9073]

为什么我们在写程序的时候要避免僵尸进程?
因为僵尸进程会导致系统内核资源泄露,比如进程的pid,pid的数量是有限的,用一个就少一个,僵尸进程占着pid,但却什么都不做,这就是一种浪费。

如何避免产生僵尸进程?
父进程调用wait() / waitpid()函数来读取子进程的退出信息即可。
pid_t wait(int *_Nullable wstatus);
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
wait() 函数使调用它的父进程暂停执行,直到它的一个子进程结束(即调用 exit() 或 _exit() ),并返回该子进程的进程 ID(PID)。如果调用成功,还可以通过指针参数 wstatus 获取子进程的退出状态。成功时,返回终止的子进程的PID,失败时返回-1。
waitpid() 函数提供了更多的控制,它允许父进程等待特定的子进程结束。与 wait() 不同, waitpid() 允许指定要等待的子进程 PID,以及一些选项来控制等待的行为。
> pid:为正,表示要等待的子进程的pid,为-1,表示等待任意子进程,为0,表示等待同进程组的所有子进程。
> wstatus:状态信息指针,可以获取子进程的退出状态。
> options:指定等待的行为选项,比如 WNOHANG :如果没有任何已结束的子进程可等待, waitpid() 不会挂起调用进程的执行,而是立即返回。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
}else if (pid == 0){
printf("Child process[pid:%d]\n", getpid());
exit(EXIT_SUCCESS);
}else {
printf("Parent process[pid:%d]\n", getpid());
wait(NULL); // 等待子进程结束
}
return 0;
}
下面介绍孤儿进程。
至于孤儿进程,顾名思义,没有父进程的子进程就是孤儿进程。也就是说,父进程比子进程先退出了,此时的子进程就变成了孤儿进程。孤儿进程会由1号进程领养,在它退出时读取它的退出信息,避免成为僵尸进程。下面通过一段代码演示孤儿进程被1号进程领养:
while : ; do ps axj | grep test | grep -v grep;sleep 1; echo "#################"; done
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
}else if (pid == 0){
printf("Child process[pid:%d]\n", getpid());
sleep(15);
}else {
printf("Parent process[pid:%d]\n", getpid());
sleep(3);//父进程比子进程先退出
exit(1);
}
return 0;
}

可以看到,子进程的父进程由pid为13185的进程转变为1号进程。
这里做一个小总结:僵尸进程是子进程先退出,父进程未读取它的退出信息而导致该子进程变为僵尸进程。孤儿进程是父进程先退出,子进程还在执行,还在执行的子进程就成为了孤儿进程。这是僵尸进程和孤儿进程的一个区别,但是,它们之间也可能存在着联系,比如一个孤儿进程退出时,1号进程未能及时回收,那么这个孤儿进程就转变为了僵尸进程。
在了解了孤儿进程会被1号进程领养后,解决僵尸进程的方式就不止主动回收子进程一种了,还可以利用双fork()技术,脱离父子关系。请看代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void do_work() {
printf("Hello World!\n");
}
int main(int argc, char *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
}else if (pid == 0){
printf("Child process[pid:%d]\n", getpid());
if (fork() == 0) {
if (fork() == 0) {
do_work();
exit(0);
}
exit(0);
}
wait(NULL);
}else {
printf("Parent process[pid:%d]\n", getpid());
}
return 0;
}
如果对上面代码有疑惑,可以画图帮助理解!
进程优先级
进程优先级(Process Priority) 是操作系统调度器用于决定CPU资源分配顺序的关键数值指标。它定义了进程获取CPU时间片的相对权重,直接影响进程的响应速度和执行频率。

通过ps -l命令可以看到:

PRI:表示进程的优先级。
● 普通进程的PRI范围是100-139。
● 实时进程的PRI范围是0-99,不受nice值影响。
NI:进程的nice值,影响进程的优先级。
● nice值的范围在-20-19之间。
在Linux系统中,PRI(实际调度优先级)和nice值之间存在这样的数学关系:
PRI = 基础值 + NI,这个基础值可能因系统版本不同而不同,可能是120,也可能是80。最后需要明确的一点是:PRI的值越小,优先级越高;反之,优先级就越低。
查看进程优先级和nice值的命令:
第一个:ps -eo pid,pri,ni
● e:显示所有进程(every process)。
● o:自定义输出格式。

第二个:top -p pid(指定监视的进程pid)

进程切换
进程切换也叫做上下文切换,是指操作系统在CPU上停止执行当前进程转而执行另一个进程的过程。导致进程切换的原因有多种,最典型的就是当前进程的时间片耗尽,操作系统把它从CPU上剥离下来,还需要明确一点,上下文切换是有开销的。
| 指标 | 典型值 | 影响 |
|---|---|---|
| 切换开销 | 1-100微秒 | CPU时间损耗 |
| 切换频率 | 100-1000次/秒 | 系统负载敏感 |
| 切换原因占比 | ||
| 时间片耗尽 | 60-70% | 公平调度 |
| I/O阻塞 | 20-30% | 资源等待 |
| 高优先级抢占 | 5-15% | 实时性需求 |

进程存在的意义
进程存在的意义这个问题,它很基础,但说实话我感觉并不好回答。

看,没骗你们吧~但是,话又说回来,既然学了进程,如果不了解它存在的意义的话,是不是有点不可理喻啊?言归正传:
首先第一点——并发执行,允许单核处理器同时处理多个程序。当然,这里的同时实际上并不是真正意义上的同时,但由于计算机太快,给我们的感觉就是同时执行。
第二点——资源隔离,每个进程都有自己对应的上下文数据,可以确保执行状态独立。
第三点——程序抽象,前面说到跑起来的程序就是进程,仅仅一个“跑”字,程序和进程之间就存在着本质的区别,程序它就是存储在硬盘上的一些指令和数据,它是静态的,而进程不一样,它是动态的。那么,居然说进程是程序的抽象,抽象什么了?就是那一堆指令。用户不需要关心那一条条的指令是如何执行的,进程为程序的执行提供了一个统一的接口,无论是简单的脚本程序还是复杂的多线程应用程序,操作系统都通过进程这个抽象来管理它们。
第四点——状态管理,进程的状态管理是操作系统对进程调度和控制的核心机制,比如当一个进程处于阻塞状态时,操作系统就把它从CPU上切换下来,转而执行就绪的进程,可以提高系统的效率。
第五点——权限控制,比如有些进程只能在用户空间执行,而不能执行需要内核权限的操作,这样做有利于系统的安全。还有用户组这些概念,都是权限控制的意义所在。
第六点——可调度实体,进程是资源分配的基本单位,有明确的生命周期,标准化的接口和独立的运行环境,有助于实现并发执行。
第七点——错误隔离,进程之间通过地址隔离、资源隔离和执行环境隔离,确保一个进程的错误不会影响其他进程或者整个操作系统,这种隔离机制不仅保护了系统的稳定性,还保护了用户数据的安全性和完整性。
结语
在读完本篇博客后,希望提供的内容能对您有所帮助。若仍有疑问或需要进一步探讨,欢迎随时交流。感谢您的关注与支持,期待未来能有更多机会共同学习和成长。
祝一切顺利!
完~
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)