目录
1.进程创建
1.1初识fork()函数
在Linux系统中,fork函数是一个关键的系统调用,它通过复制当前进程来创建新的子进程。调用fork后,系统会生成两个独立运行的进程:原始进程被称为父进程,新建的进程则称为子进程。
子进程返回0,父进程返回子进程ID,出错时返回-1。
当进程调用fork函数时,控制权会转移到内核中的fork执行流程。内核会执行以下操作:
- 为子进程分配新的内存空间和内核数据结构
- 将父进程的部分数据结构内容复制到子进程
- 将新创建的子进程添加到系统进程列表中
- 完成fork调用后返回,由调度器开始进行进程调度
当⼀个进程调⽤fork之后,就有两个⼆进制代码相同的进程。⽽且它们都运⾏到相同的地⽅。但每个进 程都将可以开始它们⾃⼰的旅程,看如下程序。
看到上图 进程27174打印了before,而进程27175没有打印before,这又是为什么呢?那就一起来看看下图所示吧!!
在fork操作之前,父进程独立运行;fork之后,父进程和子进程将各自执行不同的代码路径。需要注意的是,fork之后两个进程的执行顺序完全由系统调度器决定。
1.2 写时拷贝
父子代码通常共享同一数据,在双方都不写入时数据保持共享。当任意一方尝试写入时,系统会通过写时复制机制创建各自的独立副本。详见下图:
那么问题来了为什么要进行写时拷贝呢?
性能优化
- 避免不必要的拷贝操作:在只读场景下,多个进程/线程可以共享同一份数据,无需立即创建副本
- 延迟拷贝时机:只有当真正需要修改数据时才进行拷贝,减少即时开销
- 典型应用:Linux的fork()系统调用创建子进程时,父子进程最初共享物理内存页
资源节约
- 减少内存占用:共享的只读数据不需要多份拷贝
- 降低CPU消耗:避免不必要的拷贝操作节省CPU周期
- 示例场景:虚拟内存管理系统、数据库的快照隔离机制
注意:
得益于写时拷贝技术的实现,父子进程得以完全隔离,从而确保了进程运行的独立性。写时拷贝作为一种延迟资源分配机制,有效提升了系统内存的整体利用率。
1.3 fork常规用法
• 父进程可通过复制自身来创建子进程,实现父子并行执行不同代码段。例如,父进程持续监听客户端请求,而子进程负责处理具体请求。
• 进程可通过调用exec函数来运行新程序。例如,子进程在fork返回后调用exec替换当前程序。
注意:
fork可能会调用失败,那为什么会失败呢?
• 系统进程数量过多
• 实际用户进程数超出限制
2.进程终止
前提:
进程终止的本质在于释放系统资源,具体包括释放进程申请的内核数据结构及其对应的代码与数据。
2.1进程退出场景
• 代码执行完成,结果正确
• 代码执行完成,结果不正确
• 代码异常终止
2.2进程常见退出方法
正常终止(可通过以下方式实现):
- 从
main
函数返回 - 调用
exit
函数 - 调用
_exit
函数
异常终止:
- 通过信号终止(如
Ctrl+C
) - 使用
echo $?
可查看进程退出码
2.2.1 退出码
退出码(或退出状态)能反映上一条命令的执行结果。命令完成后,我们可以通过它判断命令是成功执行还是出现错误。通常,程序返回0表示成功运行,而返回1或其他非零值则表明执行失败。
Linux Shell 中的常见退出状态码:
- 0:成功执行
- 1:一般性错误
- 2:命令用法错误
- 126:命令不可执行
- 127:命令未找到
- 128:无效退出参数
- 130:通过 Ctrl+C 终止
- 137:进程被强制终止 (kill -9)
- 255:退出状态超出范围
这些状态码是 Shell 脚本和程序执行后返回的数值,用于判断执行结果。
2.3_exit函数
1 2 3
#include <unistd.h>
void _exit(int status);
参数说明:
status - 指定进程的终止状态值,父进程可通过wait()系统调用获取该状态值
• 说明:即使status是int类型,但只有低8位会被父进程使用。因此当调用_exit(-1)时,在终端执行$?会显示返回值为255。
2.4 exit函数
函数exit
最终会调用_exit
,但在调用前会执行以下操作:
- 调用用户通过
atexit
或on_exit
注册的清理函数 - 关闭所有已打开的流,确保缓冲数据全部写入
- 最后调用
_exit
完成终止
看下图,可以更好的理解:
2.5 return退出
return
是一种更常见的进程退出方式。执行return n
等同于执行exit(n)
,因为调用main
函数的运行时会将返回值作为exit
的参数传递。
3.进程等待
3.1进程等待必要性
• 若子进程退出后父进程未及时处理,可能导致"僵尸进程"问题,进而引发内存泄漏
• 一旦进程进入僵尸状态,便无法被常规手段终止——即便是强制终止命令kill -9也无济于事,因为无法杀死一个已死亡的进程
• 此外,父进程需要了解子进程的任务执行情况:是否正常完成、结果是否正确、退出状态是否正常
• 通过进程等待机制,父进程可以回收子进程资源并获取其退出信息
3.2 进程等待的方法
3.2.1wait方法
3.2.2 waitpid
基本概念
waitpid 是 Unix/Linux 系统中的一个系统调用函数,用于父进程等待子进程的状态变化。它比 wait 方法提供了更精细的控制,允许父进程等待特定的子进程,并且可以控制等待的方式。
函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数详解
pid 参数:
pid > 0
:等待进程ID等于pid的子进程pid = -1
:等待任意子进程,与wait等效pid = 0
:等待与调用进程同进程组的任何子进程pid < -1
:等待进程组ID等于pid绝对值的任何子进程
status 参数: 这是一个指向整数的指针,用于存储子进程的退出状态。可以使用以下宏来检查状态:
WIFEXITED(status)
:判断子进程是否正常退出WEXITSTATUS(status)
:获取子进程的退出码WIFSIGNALED(status)
:判断子进程是否被信号终止WTERMSIG(status)
:获取导致子进程终止的信号编号WIFSTOPPED(status)
:判断子进程是否被停止WSTOPSIG(status)
:获取导致子进程停止的信号编号
options 参数:
WNOHANG
:如果没有子进程退出,立即返回,不阻塞WUNTRACED
:如果子进程被停止,也返回其状态WCONTINUED
(Linux特有):如果停止的子进程被继续,也返回其状态
返回值
成功时返回状态已改变的子进程的PID
如果指定了WNOHANG且没有子进程退出,返回0
出错时返回-1,并设置errno
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) { // 子进程
printf("Child process (PID: %d) running\n", getpid());
sleep(3);
printf("Child process exiting\n");
exit(42); // 子进程退出码为42
} else if (pid > 0) { // 父进程
int status;
printf("Parent process (PID: %d) waiting for child\n", getpid());
pid_t child_pid = waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child %d exited with status %d\n",
child_pid, WEXITSTATUS(status));
}
} else {
perror("fork failed");
exit(1);
}
return 0;
}
• 调用wait/waitpid时,若子进程已退出,函数会立即返回并释放资源,同时获取子进程的退出状态信息
• 当子进程仍在正常运行的情况下调用wait/waitpid,可能导致进程阻塞
• 若指定的子进程不存在,则函数会立即返回错误
3.2.3获取子进程status
wait
和waitpid
函数都有一个status
参数,该参数属于输出型参数,由操作系统负责填充。- 若传入
NULL
,表示父进程不关心子进程的退出状态信息。 - 若传入有效指针,操作系统会通过该参数将子进程的退出状态信息返回给父进程。
- 不能将
status
简单地视为整型变量,而应将其看作位图结构(具体分析时只需关注低 16 位),详情如下图所示:
4.进程程序替换
4.1 替换原理
使用fork创建子进程后,子进程会执行与父进程相同的程序(但可能运行不同的代码分支)。通常,子进程需要调用exec系列函数来执行另一个程序。当进程调用exec函数时,其用户空间的代码和数据将被新程序完全替换,并从新程序的入口点开始执行。
需要注意的是,exec调用不会创建新进程,因此在调用前后该进程的ID保持不变。
4.2 替换函数
有六种以exec开头的函数,统称为exec系列函数。
• 若函数调用成功,将加载新程序并从启动代码开始执行,不再返回原调用处。
• 若调用出错,则返回错误代码-1。
• 因此,exec系列函数仅会在出错时返回-1,成功执行时无返回值。
这些函数原型看似容易混淆,但掌握规律后就能轻松记忆:
l
(list):表示参数采用列表形式v
(vector):参数使用数组形式p
(path):自动搜索环境变量PATHe
(env):表示自行维护环境变量
#include <unistd.h>
#include <stdlib.h>
int main() {
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
// execl: 需指定完整路径,参数列表逐个传入
execl("/bin/ps", "ps", "-ef", NULL);
// execlp: 使用PATH环境变量查找程序,无需完整路径
execlp("ps", "ps", "-ef", NULL);
// execle: 需指定完整路径,并自定义环境变量
execle("/bin/ps", "ps", "-ef", NULL, envp);
// execv: 需指定完整路径,参数通过数组传递
execv("/bin/ps", argv);
// execvp: 使用PATH环境变量查找程序,参数通过数组传递
execvp("ps", argv);
// execve: 需指定完整路径,参数通过数组传递,并自定义环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
以下是一个完整的 exec 函数簇示例: