Linux 进程控制完全指南:从创建到回收
在 Linux 系统中,进程是程序的一次执行过程,也是操作系统进行资源分配的基本单位。掌握进程控制,意味着你能够理解并驾驭程序的创建、执行、终止以及资源回收的整个生命周期。本文将深入浅出地介绍 Linux 下的进程控制核心概念和系统调用,并通过实例帮助你彻底掌握它们。
一、进程的创建 —— fork
创建一个新进程最常用的方法是调用 fork。这个系统调用的特点是:调用一次,返回两次。
c
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
return 1;
} else if (pid == 0) {
printf("我是子进程,PID = %d,父进程 PID = %d\n", getpid(), getppid());
} else {
printf("我是父进程,PID = %d,子进程 PID = %d\n", getpid(), pid);
}
return 0;
}
-
在父进程中,
fork返回新创建的子进程的 PID。 -
在子进程中,
fork返回 0。 -
如果出错,返回 -1。
fork 之后,子进程会获得父进程的数据空间、堆、栈的副本,父子进程并发执行,执行顺序由调度器决定。你可能会有疑问:那子进程不是有可能修改父进程的数据了吗?其实,操作系统在这里采用了写时拷贝的技术:当你修改某个数据时,操作系统将为你单独开一块空间来保存修改的结果。
二、进程的执行 —— exec 族函数
fork 创建的子进程通常用来执行另一个程序,这时就需要 exec 系列函数。它们会用新的程序替换当前进程的代码段、数据段、堆和栈,不会创建新进程,只是修改进程映像。
c
#include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]);
常用的是 execlp 和 execvp,它们会在 PATH 环境变量中搜索可执行文件。一个典型的 fork + exec 组合:
c
pid_t pid = fork();
if (pid == 0) {
// 子进程执行新程序
execlp("ls", "ls", "-l", NULL);
perror("execlp error"); // 只有 exec 失败才会执行这里
exit(1);
} else if (pid > 0) {
wait(NULL); // 等待子进程结束
}
注意:exec 成功时不返回,失败时返回 -1。
三、进程的终止 —— exit 和 _exit
进程终止有两种方式:
-
正常终止:从
main返回、调用exit、调用_exit。 -
异常终止:收到信号(如
SIGKILL)。
exit 是标准 C 库函数,它会执行清理操作(如刷新缓冲区、调用 atexit 注册的函数),最终调用内核的 _exit。而 _exit 是系统调用,直接进入内核终止进程。
c
#include <stdlib.h> // exit
#include <unistd.h> // _exit
void cleanup(void) {
printf("清理函数被调用\n");
}
int main() {
atexit(cleanup); // 注册终止处理函数
printf("程序开始\n");
exit(0); // 会调用 cleanup,并刷新缓冲区
// _exit(0); // 不会调用 cleanup,也不会刷新缓冲区
}
四、进程的等待 —— wait 和 waitpid
父进程通常需要等待子进程结束,以获取子进程的退出状态,并回收子进程占用的资源,防止子进程成为僵尸进程。
wait
c
#include <sys/wait.h> pid_t wait(int *status);
-
阻塞直到有一个子进程结束。
-
参数
status用于获取子进程的终止状态(可通过WIFEXITED、WEXITSTATUS等宏解析)。 -
返回结束的子进程 PID,出错返回 -1。
waitpid
c
pid_t waitpid(pid_t pid, int *status, int options);
-
pid:指定等待的子进程 PID(-1表示等待任意子进程,>0 表示特定子进程)。 -
options:常用WNOHANG表示非阻塞(如果没有子进程结束,立即返回 0)。 -
功能更强大,可以指定等待特定子进程或非阻塞等待。
示例:
c
int status;
pid_t ret = waitpid(pid, &status, 0); // 阻塞等待指定子进程
if (WIFEXITED(status)) {
printf("子进程正常退出,状态码 = %d\n", WEXITSTATUS(status));
}
五、进程的状态与僵尸进程
进程状态(用 ps 查看)
-
R (running):正在运行或可运行。
-
S (sleeping):可中断睡眠,等待某个事件。
-
D (uninterruptible sleep):不可中断睡眠,通常等待 I/O。
-
Z (zombie):僵尸状态,进程已终止但父进程尚未回收其资源。
-
T (stopped):停止,通常由信号控制。
僵尸进程
当子进程先于父进程退出,父进程没有调用 wait 时,子进程的进程描述符仍保留在内核中,成为僵尸进程。僵尸进程不占用内存和 CPU,但占用 PID 资源,大量僵尸会妨碍新进程创建。
避免僵尸的方法:
-
父进程调用
wait或waitpid。 -
采用信号处理,在
SIGCHLD信号处理函数中调用wait。 -
让父进程忽略
SIGCHLD信号(某些系统默认行为不同,需谨慎)。
孤儿进程
父进程先于子进程退出,子进程成为孤儿进程,被 init 进程(PID=1)收养,由 init 负责回收。
六、实战演练:一个简单的进程池框架
结合上述知识,我们可以实现一个简单的进程池框架,父进程创建多个子进程,并通过管道分配任务。这个框架可以用于理解多进程协作。
由于篇幅所限,这里仅给出核心思路:
-
父进程创建 N 个子进程。
-
每个子进程进入循环,等待父进程通过管道发送任务码。
-
父进程维护一个任务队列,并通过某种负载均衡(如轮询)选择子进程发送任务。
总结
Linux 进程控制是系统编程的基础,掌握 fork、exec、exit 和 wait 的配合使用,能够让我们灵活地创建和管理多进程应用。理解进程状态变迁以及僵尸进程的成因,可以帮助我们写出更健壮的程序。希望本文能帮助你构建起清晰的进程控制知识体系。
更多推荐



所有评论(0)