Linux 系统编程进程模块的学习总结
一、进程是什么
进程是一个程序执行的过程。它不只是磁盘上一段静态的代码,而是程序被加载到内存后,由 CPU 调度执行时的动态实体。一个进程要正常运行,必须获得必要的资源,包括:
- CPU 调用:进程被执行时需要使用 CPU 时间片。
- 内存资源:保存代码、数据、堆栈等运行时的内容。
- 文件系统:访问文件、目录等资源。
- 网络与输入输出设备:完成数据交互。
Linux 系统通过 PCB(Process Control Block,进程控制块)来管理进程。PCB 中保存了操作系统管理一个进程所需的全部信息,其中常见的重要成员包括:PID 进程标识符、当前工作路径、umask、进程打开的文件列表、信号相关设置、用户 ID 和组 ID、进程资源上限等。
二、并发、并行与进程状态
并发是指在单 CPU 场景下,同一时刻看似有多个任务同时进行,实际上 CPU 在多个任务之间快速切换调度。并行是指在多 CPU 场景下,多个任务在同一个时刻真正同时执行。
进程在运行过程中会在不同状态之间切换,例如就绪、运行、阻塞等状态。Linux 进程状态图中比较常见的状态标记包括 R(运行或可运行)、S(可中断睡眠)、D(不可中断睡眠)、Z(僵尸状态)等,这些状态可以通过 ps 命令的 stat 字段观察。
常用调度算法包括短任务优先、先来先服务、时间片轮询等。Linux 使用完全公平调度算法,并采用多级队列等机制来平衡不同类型任务的执行。
三、常用进程命令
ps aux:查看系统上所有进程的相关信息,包括 PID、STAT、COMMAND 等。top:动态刷新进程信息,类似 Windows 的任务管理器。kill、killall:给某个进程发送信号,大部分信号会用于结束进程。pstree:查看进程之间的父子关系。
四、进程相关核心函数
4.1 fork:创建子进程
fork() 用于从当前进程中克隆一个同名的新进程。克隆出来的进程称为子进程,原有进程称为父进程。子进程是父进程的完全拷贝,并且从 fork() 之后继续执行,与父进程具有相同的代码逻辑。
它的最大特点是一次调用,两次返回:
- 在父进程中,成功时返回子进程的 PID,大于 0;失败返回 -1。
- 在子进程中,成功时返回 0。
下面是一个简单的 fork 示例:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
printf("child process, pid = %d\n", getpid());
} else {
printf("parent process, child pid = %d\n", pid);
}
return 0;
}
需要重点理解的是:fork 之后父子进程谁先运行并不确定,父子进程之间的普通变量也不共享。
4.2 getpid 与 getppid
getpid() 用于获得调用该函数的进程的 PID。getppid() 用于获得当前进程的父进程 PID。它们常用于调试、日志记录或判断父子进程关系。
五、写时复制
在 fork 的过程中,操作系统会先复制父进程的 PCB 给子进程,但子进程 PCB 中的 PID 与父进程不同。fork 完成后,子进程的用户空间与父进程处于共享状态。只有当父进程或子进程中的某一方对数据进行修改时,子进程才会真正开辟对应的内存空间。这种机制被称为写时复制。
写时复制的优势在于:如果 fork 之后马上执行 exec 加载新程序,就可以减少不必要的内存复制,从而提升进程创建效率。这也解释了为什么 fork 能够快速创建进程,同时父子进程在写数据之后会各自拥有独立的内存空间。
六、进程退出的几种情况
进程正常退出主要有以下几种方式:
- main 函数中使用 return。
- 调用 C 库函数
exit(),它会执行 IO 库的清理工作,并执行 atexit 注册的清理函数。 - 调用
_exit()或_Exit(),它们会关闭所有已经打开的文件,但不执行清理函数。 - 主线程退出。
- 主线程调用
pthread_exit()。
进程异常终止的方式包括:调用 abort()、通过 kill 等发送信号结束进程,以及最后一个线程被 pthread_cancel 取消等。
七、孤儿进程与僵尸进程
7.1 孤儿进程
如果父进程先消亡,而子进程仍然存在,那么这个子进程被称为孤儿进程。孤儿进程会重新找到新的父进程,后续由系统继续管理和回收,因此通常不需要过多关注。
7.2 僵尸进程
如果子进程先消亡,它的用户空间会被释放,也不会再被调度,但是对应的 PCB 块没有被回收,仍然占用内核内存空间。这种进程被称为僵尸进程。僵尸进程需要由父进程进行回收,例如父进程调用 wait 或 waitpid 获取子进程退出状态,操作系统才能释放其 PCB。
如果父进程不回收僵尸进程,就会造成内核资源泄漏,这也是进程编程中需要特别注意的问题。
八、总结
从进程开始学习 Linux 系统编程,核心是理解“程序”与“进程”的区别,以及操作系统如何通过 PCB 管理进程。在此基础上,掌握 fork 创建子进程、写时复制机制、进程退出方式,以及孤儿进程和僵尸进程的产生与回收,就基本建立起进程编程的整体框架。
后续可以继续围绕进程间通信、信号处理、exec 函数族等内容深入学习。建议在学习每个函数时,不要死记函数原型,而是结合 man 手册和实际代码调试,理解参数、返回值和典型使用场景。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)