在 Linux 操作系统的世界里,进程是程序运行的动态实体,它们如同一个个忙碌的工作者,承载着系统中各种任务的执行。无论是系统服务的稳定运行,还是用户程序的交互响应,都离不开进程的支持。深入理解进程的生命周期,包括创建、终止、等待以及程序替换等关键环节,对于掌握 Linux 系统编程和开发高性能应用程序至关重要。
在 Linux 系统中,fork 函数可从已有进程创建新进程。新进程是子进程,原进程为父进程。其函数声明为 pid_t fork(void),返回值规则为:子进程中返回 0,父进程中返回子进程的 pid,出错则返回 -1。
#include <unistd.h>
pid_t fork(void); 当进程调用 fork 函数,控制转移到内核代码后,内核会进行以下操作:
fork 返回并开始调度。实际上,完成前两步,子进程就已创建。
通过代码演示,能看到 fork 前父进程独自执行,fork 后父子进程分别执行,且谁先执行由调度器决定。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("befor pid:%d\n", getpid());
fork();
printf("after pid:%d\n", getpid());
return 0;
}
fork 返回后,调用 exec 函数。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define N 5
void func()
{
int cnt = 10;
while(cnt)
{
printf("I am chid, pid:%d, ppid:%d\n", getpid(), getppid());
cnt--;
sleep(1);
}
return;
}
int main()
{
int i = 0;
for(i = 0; i < N; i++)
{
pid_t id = fork();
if(id == 0)// 只有子进程会进去
{
func();
exit(0);// 子进程走到这里就退出了
}
}
sleep(1000);
return 0;
}
父进程执行的速度是很快的,由于父进程的 for 循环里没有 sleep 函数,所以五个子进程几乎是在同一时间被创建出来,创建出来的每一个子进程会去调用 func 函数,每一个子进程执行完 func 函数后会执行 exit 函数退出。父子进程谁先执行完全是由调度器来决定的。
进程终止意味着其生命周期结束,通常包括以下场景:
kill 命令)。main 函数里常写的 return 0 的作用,就是每个进程终止时都会返回的一个退出码(Exit Code),用于标识运行结果:
提示:父进程最关心子进程的运行情况,main 函数返回的退出码会被父进程(如 bash)获取并保留,可通过 echo $? 查看最近进程的退出码。
int main()
{
printf("模拟一段逻辑!\n");
return 0;
}
strerror 函数是一个标准 C 库函数,用于将错误码转换为可读的错误信息字符串。它非常适合程序员在调试和错误处理时,将数字形式的错误码(如 errno)转化为更直观的文本信息,方便理解和输出。
下面的代码展示了如何遍历并打印当前系统支持的所有错误码及其对应的描述信息:
#include <stdio.h>
#include <string.h>
int main() {
for (int i = 0; i < 200; i++) {
printf("%d: %s\n", i, strerror(i)); // 输出错误码及其描述
}
return 0;
}说明:
strerror 仅对系统支持的错误码返回有效的错误描述。"Unknown error"。
errno 全局变量errno 是 C 语言提供的全局变量,记录最近一次函数调用失败时的错误码。当调用 C 标准库函数失败,errno 会被赋值为特定数值,结合 strerror 函数可获取错误详细信息。例如 malloc 内存分配失败时,通过 errno 和 strerror 输出错误码及描述。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int ret = 0;
char* str = (char*)malloc(1000*1000*1000*4);
if(str == NULL)
{
printf("malloc error:%d, %s\n", errno, strerror(errno));
ret = errno;
}
else
{
printf("malloc success!\n");
}
return ret;
}
程序异常(如除零操作、空指针解引用)会导致进程未执行到 return 语句即终止,此时退出码无参考价值。程序异常本质是进程接收到操作系统发送的信号,如空指针解引用会触发段错误信号,导致进程异常结束。Linux 系统的所有信号如下图所示:

进程退出分为正常终止和异常终止两类:
main 函数 return 语句返回、调用库函数 exit、调用系统函数 _exit。Ctrl + C 中断,或因接收到特定信号导致进程强制结束。kill -9 也无能为力,因为谁也没有办法杀死一个死去的进程。进程等待就是在父进程的代码中,通过系统调用 wait/waitpid,来进行对子进程进行状态检测与回收的功能。

wait方法在 Linux 系统中,wait 方法是用于等待子进程结束的系统调用或库函数。其功能是让父进程挂起,直到一个子进程终止,并返回子进程的终止状态。
1. 功能描述
wait() 方法会暂停父进程的执行,直到任意一个子进程结束。wait() 会立即返回 -1,并设置错误信息为 ECHILD。2. 原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);3. 参数解释
WIFEXITED、WEXITSTATUS 等)解析该状态。4. 返回值
-1,并设置 errno。5. 父进程只等待一个进程(阻塞式等待)
以下是一个使用 wait() 的简单示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(0);
}
else
{
int cnt = 10;
// parent
while(cnt)
{
printf("I am parent, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
int ret = wait(NULL);
if(ret == id)
{
printf("wait success!\n, ret:%d\n", ret);
}
sleep(5);
}
return 0;
}注意:前五秒父子进程同时运行,紧接着子进程退出变成僵尸状态,五秒钟后父进程对子进程进行了等待,成功将子进程释放掉,最后再五秒钟后父进程也退出,整个程序执行结束。


6. 父进程等待多个子进程(阻塞式等待)
一个 wait 只能等待任意一个子进程,因此父进程如果要等待多个子进程可以通过循环来多次调用 wait 实现等待多个子进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 5
// 父进程等待多个子进程
void RunChild()
{
int cnt = 5;
while(cnt--)
{
printf("I am child, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
return;
}
int main()
{
for(int i = 0; i < N; i++)
{
pid_t id = fork();// 创建一批子进程
if(id == 0)
{
// 子进程
RunChild();
exit(0);
}
// 父进程
printf("Creat process sucess:%d\n", id);
}
sleep(10);
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL);
if(id > 0)
{
printf("Wait process:%d, success!\n", id);
}
}
sleep(5);
return 0;
}waitpid 方法waitpid 是 wait 的增强版本,它可以指定等待特定的子进程终止,同时支持非阻塞等待。相比 wait,waitpid 提供了更大的灵活性,适用于复杂的进程管理场景。
1. 函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);2. 参数说明
pid: pid > 0:等待进程 ID 为 pid 的子进程。pid == 0:等待与调用进程属于同一进程组的任何子进程。pid < -1:等待进程组 ID 为 |pid|(绝对值)的所有子进程。pid == -1:等待任意子进程(等效于 wait())。status: status 的值(见 状态解析宏 部分)。options: waitpid 的行为。WNOHANG:非阻塞等待。如果没有子进程终止,立即返回 0。WUNTRACED:如果子进程由于信号暂停(但未终止),则返回。WCONTINUED:如果子进程接收到 SIGCONT 信号恢复运行,则返回。3. 返回值
WNOHANG 且没有子进程终止,则返回 0。ECHILD:没有符合条件的子进程。EINTR:调用被信号中断。4. 状态解析宏
子进程退出状态存储在 status 参数中,可以通过以下宏解析:
WIFEXITED(status): WEXITSTATUS(status): WIFEXITED 为真时)。5. 获取子进程的退出信息(阻塞式等待)
进程有三种退出场景,父进程等待希望获得子进程退出的以下信息:子进程代码是否异常?没有异常,结果对嘛?不对是因为什么呢? 子进程这些所有的退出信息都被保存在 status 参数里面。
wait 和 waitpid 都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
NULL,表示不关心子进程的退出状态信息。
status 不能简单的当做整型来看待,例如 int 型总共占 32 位, 则具体细节需要关注位,如下图:

注意:操作系统没有0号信号,因此,如果低七位是0说明子进程没有收到任何信号。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
int cnt = 5, a = 10;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
a /= 0; // 故意制造一个异常
}
exit(11); // 将退出码故意设置成11
}
else
{
// parent
int cnt = 10;
while(cnt)
{
printf("I am parent, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
// 目前为止,进程等待是必须的!
//int ret = wait(NULL);
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
// 获取子进程退出状态信息的关键代码
// 0111 1111:0x7F,1111 1111 0000 0000:0xFF00
printf("wait success! exit signal:%d, exit code:%d!\n", status&0X7F, (status >> 8)&0XFF);
}
sleep(5);
}
return 0;
}
在代码运行结果中,我们可以看到以下几点:
a /= 0; 的除零操作,触发了 SIGFPE 信号(信号编号为 8),导致子进程异常终止。exit(11),因此我们设置的退出码 11 并没有生效。0,这是因为进程收到信号并被异常终止时,退出码是不可信的。6. 一般的进程等待代码
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
// 0111 1111:0x7F,1111 1111 0000 0000:0xFF00
//printf("wait success! exit signal:%d, exit code:%d!\n", status&0X7F, (status >> 8)&0XFF);
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码是:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程被异常终止!\n");
}
}wait 和 waitpid 的本质wait 和 waitpid 的核心工作是:
Z 状态)。status 获取子进程的退出状态信息。非阻塞轮询等待的核心思想是,父进程在等待子进程的同时,继续执行其他任务,而不是一直阻塞在等待操作上。通过 waitpid 的 WNOHANG 选项,可以实现非阻塞的轮询等待。
waitpid 的 WNOHANG选项:
0。
-1 并设置 errno。
sleep),以避免占用过多 CPU。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
// 父进程只等待一个子进程(非阻塞轮询等待)
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(11);
}
else
{
// parent
// 目前为止,进程等待是必须的!
while(1)
{
int status = 0;
int ret = waitpid(id, &status, WNOHANG);
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码是:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程被异常终止!\n");
}
break;
}
else if(ret == 0)
{
// 父进程的任务可以写在这里
printf("child process is running...\n");
}
else
{
printf("等待出错!\n");
}
sleep(1);
}
sleep(2);
}
return 0;
}注意:进程等待在一定程度上确保了父进程一定是最后一个退出的,这样可以避免子进程变为僵尸进程,进而导致内存泄露的问题。

#include <stdio.h>
#include <unistd.h>
int main()
{
printf("befor: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
// exec类函数的标准写法
// execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execl("/usr/bin/top", "top", NULL);
printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
return 0;
}输出示例:

函数原型:
int execl(const char *path, const char *arg0, ..., NULL);execl 是一个可变参数函数,用于执行程序替换操作。path)。argv),并以 NULL 结尾。各个参数意义:
/bin/ls:可执行程序的绝对路径,这是需要加载的新程序。"ls":新程序的第一个命令行参数(argv[0]),通常用作程序名(与 shell 中运行 ls 类似)。NULL:表示参数列表结束,这是可变参数函数的结束标志。程序替换行为:
ls)替换。/bin/ls。ls 是 Linux 中的一个命令行工具,用于列出目录中的文件和目录。注意点:
execl 成功执行,新程序的代码替换当前程序,后续代码(如 printf)不会执行。execl 执行失败(例如路径不存在或权限不足),execl 会返回 -1,并设置 errno。通常在失败时用 perror 或 strerror(errno) 打印错误原因。在 Linux 中,程序替换操作(Program Replacement)是通过 exec 系列接口 实现的。这些接口将当前进程的执行内容替换为新程序,并保留原有进程的 PID。以下是 exec 系列的七个接口及其用法。
(上文已经解释,这里不再过多赘述。)
int execlp(const char *file, const char *arg0, ..., NULL);描述:与 execl 类似,但会在环境变量 PATH 中搜索 file,而无需指定绝对路径。
参数:
file: 可执行程序的名称(会从 PATH 中查找)。execl 一致。示例:
execlp("ls", "ls", "-l", "-a", NULL);ls 程序,execlp 会自动在 /bin 等目录中查找 ls。int execle(const char *path, const char *arg0, ..., NULL, char *const envp[]);描述:与 execl 类似,但允许显式指定新的环境变量 envp。
参数:
path: 新程序的文件路径。arg0: 通常为程序名(argv[0])。..., NULL: 命令行参数。envp[]: 指定的环境变量数组(如 {"KEY=VALUE", NULL})。示例:
char *envp[] = {"MY_ENV=HelloWorld", NULL};
execle("/usr/bin/env", "env", NULL, envp);env 程序,并设置环境变量 MY_ENV=HelloWorld。int execv(const char *path, char *const argv[]);描述:与 execl 类似,但通过数组传递命令行参数。
参数:
path: 新程序的文件路径。argv[]: 参数数组,argv[0] 通常是程序名,最后一项必须为 NULL。示例:
char *argv[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", argv);/bin/ls 程序,使用参数数组。int execvp(const char *file, char *const argv[]);描述:与 execv 类似,但会在环境变量 PATH 中搜索程序。
参数:
file: 程序名(会从 PATH 环境变量中查找)。argv[]: 参数数组。示例:
char *argv[] = {"ls", "-l", "-a", NULL};
execvp("ls", argv);ls 程序,execvp 会从 PATH 中查找 ls。int execve(const char *path, char *const argv[], char *const envp[]);描述:是所有 exec 系列函数的底层系统调用,直接调用内核执行程序替换。
参数:
path: 新程序的文件路径。argv[]: 参数数组。envp[]: 环境变量数组。示例:
char *argv[] = {"ls", "-l", "-a", NULL};
char *envp[] = {"MY_ENV=HelloWorld", NULL};
execve("/bin/ls", argv, envp);/bin/ls 程序,并显式传递参数和环境变量。int execvpe(const char *file, char *const argv[], char *const envp[]);描述:扩展接口,结合了 execvp 和显式的环境变量指定功能。
参数:
file: 程序名(会从 PATH 环境变量中查找)。argv[]: 参数数组。envp[]: 环境变量数组。注意:此函数并非 POSIX 标准,某些系统可能不支持。
示例:
char *argv[] = {"ls", "-l", "-a", NULL};
char *envp[] = {"MY_ENV=HelloWorld", NULL};
execvpe("ls", argv, envp);接口 | 参数传递方式 | 是否使用 PATH | 是否支持显式 envp |
|---|---|---|---|
execl | 可变参数列表 | 否 | 否 |
execlp | 可变参数列表 | 是 | 否 |
execle | 可变参数列表 + envp | 否 | 是 |
execv | 参数数组 | 否 | 否 |
execvp | 参数数组 | 是 | 否 |
execve | 参数数组 + envp | 否 | 是 |
execvpe | 参数数组 + envp | 是 | 是 |
注意点:
envp 的时候,要注意此时新替换的进程将会覆盖原来父进程的环境变量。
exec* 的形式,后缀中 l(list) 表示列表,v(vector) 表示数组,p 表示是否使用环境变量 PATH,e 表示是否支持显式 envp。
1. mycommand.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char* argv[], char* env[])
{
pid_t id = fork();
if(id == 0)
{
// child
printf("before: I am process, pid: %d, ppid: %d\n", getpid(), getppid());
// 自定义命令行参数
char *const myargv[] = {"otherExe", "-a", "-b", "-c", NULL};
// 自定义环境变量
char *const myenv[] = {
"MY_YAL=123456",
"MY_NAME=lesson17",
NULL
};
execve("./otherExe", myargv, myenv);
printf("after: I am process, pid: %d, ppid: %d\n", getpid(), getppid());
exit(1);
}
// father
pid_t ret = waitpid(id, NULL, 0);
if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
return 0;
}2. otherExe.cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
int main(int argc, char* argv[], char* env[])
{
cout << "这是命令行参数" << endl;
for(int i = 0; argv[i]; i++)
{
cout << i << " : " << argv[i] << endl;
}
cout << "这是环境变量" << endl;
for(int j = 0; env[j]; j++)
{
cout << j << " : " << env[j] << endl;
}
return 0;
}3. makefile
.PHONY: all
all: otherExe mycommand
mycommand: mycommand.c
gcc -o $@ $^ -std=c99
otherExe: otherExe.cpp
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -f mycommand otherExe
不难看出,环境变量已经被覆盖了。
至此,我们完成了对 Linux 进程从创建到替换全流程的深入探讨。从fork函数的神奇复制,到进程终止时的各种场景与退出方式;从进程等待对资源回收和状态获取的重要性,到程序替换实现进程功能蜕变的原理与多样接口,每一个环节都展现了 Linux 进程管理的精妙之处。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!