1. 进程创建
在Linux系统中fork()函数是非常重要的函数,它用来在一个已经存在的进程中创建一个新的进程。新进程成为子进程,原进程称为父进程。
//
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以执行它们各自己的代码。即fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork() 调用后,fork() 给父进程的返回值是子进程的 PID(>0),继续执行 after 之后的代码。fork()给子进程的返回值是 0,也从同一个 after 处开始执行。虽然执行路径相同,但各自拥有独立的用户空间(各自的堆栈、数据段等)。
为了提高效率,父子进程在 fork() 后并不立即复制所有物理页,而是共享这些页,只在进程尝试写入时才真正分配新页并复制内容。因此,fork() 本质上是浅拷贝,直到写时才深拷贝。
总之,fork() 就是内核为子进程拷贝父进程的执行上下文和大部分资源,然后父子进程各自独立地从 fork 调用点(after)继续执行。
fork常规用法:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数来完成进程替换。
2. 进程终止
进程退出的场景:
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止。
进程常见退出方法:
- main() 中 return表示进程终止(函数return表示函数结束)。
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
- 在代码任意位置调用exit()函数表示进程终止。
- 使用系统调用 _exit终止进程。
exit和_exit的区别:
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:执行用户通过 atexit或on_exit定义的清理函数。关闭所有打开的流,所有的缓存数据均被写入。调用_exit。
通过下面代码也可以看出:
3. 进程等待
什么是进程等待?
进程等待(process waiting)通常指父进程为了同步子进程的结束并回收其资源,主动挂起自己,直到子进程状态发生变化(最常见的是结束)。父进程调用 wait() 或waitpid() 这样的系统调用,自己进入阻塞(waiting)状态,直到某个子进程退出或收到信号而改变状态。期间,父进程不会继续执行,直到子进程终止或满足指定条件。
为什么要等待?
子进程一旦退出,会先变成僵尸进程(zombie),它的 PCB 和退出状态还挂在系统里,直到父进程调用 wait*() 才释放这些资源。父进程通过 wait,解决子进程的僵尸问题,回收系统资源,避免造成内存泄漏。其次,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
进程等待的方法
- wait方法
#include <sys/types.h>
#include <sys/wait.h>
int *status 输出参数,子进程的“状态”存放位置;如果只关心进程结束,不想获取状态,可传 NULL。
返回值
成功:返回结束(或被信号终止)的子进程的 PID;
失败:返回 −1,并设置 errno。
pid_t wait(int *status);
- waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。
使用wait等待子进程,并通过位操作解析status,获取子进程的退出信息:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (0 == id)
{
printf("I am a child process! pid: %d\n", getpid());
sleep(10);
_exit(123);
}
else if (id > 0)
{
int status = 0;
pid_t rid = wait(&status);
if (rid > 0 && (status & 0x7F) == 0)
{
printf("exit_code:%d\n", ((status >> 8) & 0xFF));
}
else if (rid > 0)
{
printf("exit_single:%d\n", ((status) & 0x7F));
}
else
{
perror("waitpid");
}
}
else
{
perror("fork");
}
return 0;
}
子进程睡眠10秒后正常结束,退出码和代码中一致是123:
在另一个进程kill掉子进程:
上面通过位操作解析 status 的值,也可以通过如下宏来解析status 的值:
1. WIFEXITED(status)
作用:判断进程是否正常退出。
原理:检查 status 的低 8 位是否非零,且未被信号终止。
实现:
#define WIFEXITED(status) (((status) & 0x7F) == 0)
2. WEXITSTATUS(status)
作用:提取进程的退出码。
原理:取 status 的 8-15 位(即右移 8 位后取低 8 位)。
实现:
#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)
使用waitpid等待子进程,并通过宏解析status,获取子进程的退出信息:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (0 == id)
{
printf("I am a child process! pid: %d\n", getpid());
sleep(10);
_exit(123);
}
else if (id > 0)
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0 && WIFEXITED(status))
{
printf("exit_code:%d\n", WEXITSTATUS(status));
}
else
{
printf("wait child process failed, return\n");
}
}
else
{
perror("fork");
}
return 0;
}
进程的阻塞等待和非阻塞等待
1.阻塞等待:
在 Linux 系统编程中,如果需要让 waitpid 阻塞等待子进程终止(即父进程暂停执行,直到目标子进程退出或被信号终止),应将 options 参数设置为 0(即不启用任何特殊选项)。此时 waitpid 的行为与 wait 类似。
调用 waitpid 后,父进程暂停执行,直到子进程状态变化(如终止、被信号杀死、暂停等)。父进程必须等待子进程完成后才能继续。
阻塞等待流程:
父进程代码
│
▼
调用 wait() 或 waitpid(..., 0)
│
▼
父进程暂停执行,进入阻塞状态(等待子进程终止)
│ ↙ 子进程运行中...
▼ ↙
子进程终止,内核通知父进程
│
▼
父进程恢复执行,处理子进程状态
│
▼
继续后续代码
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// 子进程执行任务
sleep(2);
exit(42); // 退出码 42
}
else
{
// 父进程阻塞等待指定子进程
int status;
pid_t rid = waitpid(id, &status, 0); // options=0,阻塞等待
if (rid == id && WIFEXITED(status))
{
printf("child %d 退出,状态码: %d\n", id, WEXITSTATUS(status)); // 输出 42
}
else
{
perror("waitpid 失败");
}
}
return 0;
}
2.非阻塞等待
如果需要让 waitpid 非阻塞等待子进程终止,应将 options 参数设置为WNOHANG。
调用 waitpid 后,父进程立即返回,可继续执行其他任务。需循环调用 waitpid 检查子进程状态(轮询机制)。
非阻塞等待流程:
父进程代码
│
▼
调用 waitpid(..., WNOHANG)
│
▼
内核检查子进程状态:
├─ 子进程已终止 → 立即返回 PID,父进程处理状态
└─ 子进程未终止 → 返回 0,父进程继续执行其他任务
│
▼
父进程循环调用 waitpid 轮询子进程状态
│
▼
直到子进程终止,父进程处理状态
│
▼
继续后续代码
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(2); // 子进程运行 2 秒
exit(42);
}
else
{
int status;
while (1)
{
// 非阻塞等待子进程
pid_t rid = waitpid(id, &status, WNOHANG);
if (rid == id && WIFEXITED(status))
{
printf("子进程退出,状态码: %d\n", WEXITSTATUS(status));
break;
}
else if (rid == 0)
{
printf("子进程未退出,父进程继续工作...\n");
sleep(1); // 模拟父进程执行其他任务
}
else
{
perror("waitpid 错误");
break;
}
}
}
return 0;
}
3.形象比喻
阻塞等待: 你在一家咖啡店排队,必须等到咖啡做好后才能离开(父进程挂起,直到子进程完成)。
非阻塞等待:你点单后拿到一个取餐号,可以继续逛街,每隔一段时间回来问咖啡是否做好(父进程轮询检查子进程状态)。
4. 进程替换
4.1 exec 系列函数
在 Linux 系统编程中,进程替换通过 exec 系列函数实现,它的核心作用是将当前进程的代码段、数据段等替换为新的程序,使当前进程转而执行另一个程序。
以下是 exec 系列函数的详细解析:
exec 系列函数的的核心作用
用新程序的代码、数据、堆栈等替换当前进程的原有内容。PID 保持不变(不创建新进程),原进程的打开文件描述符、信号处理等属性默认保留。exec 成功后,原进程的代码不再执行(被新程序完全替代)。
exec 系列函数的的命名规则和原型
所有 exec 函数均定义在 <unistd.h> 中,命名遵循以下规则:
后缀 l(list):参数以 可变参数列表 形式传递(NULL 结尾)。
后缀 v(vector):参数以 字符串数组 形式传递。
后缀 e(environment):允许自定义环境变量(需额外传递 envp 数组)。
后缀 p(PATH):自动在 PATH 环境变量指定的目录中搜索可执行文件。
函数原型 | 特点 |
---|---|
int execl(const char *path, const char *arg0, …, NULL) | 参数列表形式,需指定完整路径。 |
int execlp(const char *file, const char *arg0, …, NULL) | 参数列表形式,自动搜索 PATH。 |
int execle(const char *path, const char *arg0, …, NULL, char *const envp[]) | 参数列表形式,可自定义环境变量。 |
int execv(const char *path, char *const argv[]) | 参数数组形式,需指定完整路径。 |
int execvp(const char *file, char *const argv[]) | 参数数组形式,自动搜索 PATH。 |
int execvpe(const char *file, char *const argv[], char *const envp[]) | 参数数组形式,自动搜索 PATH 并自定义环境变量。 |
exec 系列函数使用示例
- execl 使用示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进程启动, 即将被替换为ls命令!!!\n");
//path:可执行文件的 完整路径(如 /usr/bin/ls)。
//arg0, ..., NULL:参数列表,以 NULL 结尾。
//第一个参数 arg0 通常是程序名称(即 argv[0])。
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
perror("execl 失败");
return 0;
}
- execlp 使用示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进程启动, 即将被替换为ls命令!!!\n");
//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索
//arg0, ..., NULL:参数列表,以 NULL 结尾。
//第一个参数 arg0 通常是程序名称(即 argv[0])。
execlp("ls", "ls", "-a", "-l", NULL);
perror("execp 失败");
return 0;
}
- execle 原使用示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进程启动, 即将被替换为ls命令!!!\n");
//path:可执行文件的 完整路径(如 /usr/bin/ls)。
//arg0, ..., NULL:参数列表,以 NULL 结尾。
//第一个参数 arg0 通常是程序名称(即 argv[0])。
//env:自定义环境变量数组,以 NULL 结尾。
char* env[] = {"USER=ZhuZebo", NULL};
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, env);
perror("execle 失败");
return 0;
}
- execv 原使用示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进程启动, 即将被替换为ls命令!!!\n");
//path:可执行文件的 完整路径(如 /usr/bin/ls)。
//argv:参数数组,以 NULL 结尾。
//argv[0] 通常是程序名称。
char* argv[] = {"ls", "-a", "-l", NULL};
execv("/usr/bin/ls", argv);
perror("execv 失败\n");
return 0;
}
- execvp 原使用示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进程启动, 即将被替换为ls命令!!!\n");
//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索
//argv:参数数组,以 NULL 结尾。
//argv[0] 通常是程序名称。
char* argv[] = {"ls", "-a", "-l", NULL};
execvp("ls", argv);
perror("execvp 失败\n");
return 0;
}
- execvpe 原使用示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("进程启动, 即将被替换为ls命令!!!\n");
//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索
//argv:参数数组,以 NULL 结尾。
//argv[0] 通常是程序名称。
//env:自定义环境变量数组,以 NULL 结尾。
char* argv[] = {"ls", "-a", "-l", NULL};
char* env[] = { "USER=ZhuZebo", NULL};
execvpe("ls", argv, env);
perror("execvpe 失败\n");
return 0;
}
4.2 替换原理
程序替换通过 exec 系列函数实现,其本质是 将当前进程的代码和数据替换为新程序的代码和数据,但保持以下属性不变:
进程标识符(PID):进程的 ID 不变。
内核态信息:文件描述符表、进程优先级等。
替换步骤:
1.内核验证与权限检查:检查文件路径、权限、格式。
2.解析可执行文件(ELF):读取代码段、数据段、入口地址。
3 释放原进程的内存资源:释放原进程内存,加载新程序到虚拟地址空间。
4. 继承与重置属性:保留文件描述符,重置堆栈,恢复默认信号处理。
5.跳转到新程序入口:CPU 从新入口地址开始执行。
程序替换通过替换进程的代码和数据段实现进程重生。
至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !!!