Linux学习:进程的控制
前面我们已经学习了进程的相关概念。本篇我们就来学习Linux系统中进程的控制
相关代码已经上传至作者的个人gitee:楼田莉子/Linux学习 - Gitee.com喜欢请点个赞谢谢
目录
进程的创建
fork函数的介绍
fork函数返回值
写时拷贝
fork常规用法
fork调用失败的原因
进程的终止
进程退出
退出码
Linux 进程退出码表格
信号相关退出码 (128 + 信号编号)
特殊用途退出码
_exit函数
exit函数
_exit函数 vs exit函数 的主要区别
return函数
进程等待
进程等待的必要性
进程等待的方法
wait方法
waitpid方法
获取子进程status
阻塞与非阻塞等待
进程的阻塞等待
进程的非等待阻塞
进程程序替换
替换原理
替换函数
命名理解
函数解释
1. execl - 列表形式 + 完整路径
2. execlp - 列表形式 + PATH搜索
3. execle - 列表形式 + 环境变量
4. execv - 数组形式 + 完整路径
5. execvp - 数组形式 + PATH搜索
6. execve - 数组形式 + 环境变量
替换函数可以实现执行其他语言的程序
替换函数与环境变量
Linux信号含义表
实时信号说明
关键特性总结
补充说明
进程的创建
fork函数的介绍
在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程
#include <unistd.h>
pid_t fork(void);
//返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
• 分配新的内存块和内核数据结构给子进程
• 将父进程部分数据结构内容拷贝至子进程
• 添加子进程到系统进程列表当中
• fork返回,开始调度器调度
就像下图:
当⼀个进程调⽤fork之后,就有两个⼆进制代码相同的进程。⽽且它们都运⾏到相同的地⽅。但每个进程都将可以开始它们⾃⼰的旅程,看如下程序。
#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{pid_t pid;printf("Before: pid is %d\n", getpid());if ( (pid=fork()) == -1 ) perror("fork()"),exit(1);printf("After:pid is %d, fork return %d\n", getpid(), pid);sleep(1);return 0;
}
打印结果为:
为什么进程21281打印两次之后的但是21282却只打印了一次呢?原因如下图所示
所以,fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完全由调度器决定
fork函数返回值
• ⼦进程返回0,
• ⽗进程返回的是⼦进程的pid。
写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证!
写时拷贝,是一种延时申请技术,可以提高整机内存的使用率
fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
1、系统中有太多的进程
2、实际用户的进程数超过了限制
进程的终止
当我们的进程终止的时候,内核在做什么呢?
如果我们的进程中止一般有几种情况?
我们写C/C++代码中通常是这样的
int main()
{//codereturn 0;
}
其中return 0代表终止。这就有以下几种情况
-
代码跑完,结果正确
-
代码跑完,结果不正确
-
代码都没跑完,进程异常了!
进程退出
之前有的代码我们可以通过打印来获取结果,但是很多时候代码的结果是不那么容易显示出来的(比如网络发送等),那么我们是如何确认代码正确与否呢?所以main函数才会返回0,代表着进程的退出。0表示成功,非0表示错误。而main函数返回的0会交给父进程(这里就是bash)。通过以下命令来打印上一个子进程的退出码数字
echo $?
正常终止:
-
从main返回
-
调用exit
-
_exit
异常退出:
• ctrl + c,信号终止
退出码
退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。
代码 1 或 0 以外的任何代码都被视为不成功。
退出码的捕获可以用strerror函数来获取,我们可以写一段程序来检验
#include<stdio.h>
#include<string.h>
int main()
{for(size_t i=0;i<150;++i){printf("%zu号错误码为:%s\n",i,strerror(i));}return 0;
}
结果为:(括号内容为翻译)
0号错误码为:Success(成功)1号错误码为:Operation not permitted(操作不允许)2号错误码为:No such file or directory(没有这样的文件或目录)3号错误码为:No such process(没有这样的进程)4号错误码为:Interrupted system call(被中断的系统调用)5号错误码为:Input/output error(输入/输出错误)6号错误码为:No such device or address(没有这样的设备或地址)7号错误码为:Argument list too long(参数列表过长)8号错误码为:Exec format error(执行格式错误)9号错误码为:Bad file descriptor(错误的文件描述符)10号错误码为:No child processes(没有子进程)11号错误码为:Resource temporarily unavailable(资源暂时不可用)12号错误码为:Cannot allocate memory(无法分配内存)13号错误码为:Permission denied(权限被拒绝)14号错误码为:Bad address(错误的地址)15号错误码为:Block device required(需要块设备)16号错误码为:Device or resource busy(设备或资源忙)17号错误码为:File exists(文件已存在)18号错误码为:Invalid cross-device link(无效的跨设备链接)19号错误码为:No such device(没有这样的设备)20号错误码为:Not a directory(不是一个目录)21号错误码为:Is a directory(是一个目录)22号错误码为:Invalid argument(无效参数)23号错误码为:Too many open files in system(系统中打开文件过多)24号错误码为:Too many open files(打开文件过多)25号错误码为:Inappropriate ioctl for device(对设备不适当的ioctl操作)26号错误码为:Text file busy(文本文件忙)27号错误码为:File too large(文件太大)28号错误码为:No space left on device(设备无剩余空间)29号错误码为:Illegal seek(非法寻址)30号错误码为:Read-only file system(只读文件系统)31号错误码为:Too many links(链接过多)32号错误码为:Broken pipe(管道破裂)33号错误码为:Numerical argument out of domain(数值参数超出定义域)34号错误码为:Numerical result out of range(数值结果超出范围)35号错误码为:Resource deadlock avoided(避免资源死锁)36号错误码为:File name too long(文件名过长)37号错误码为:No locks available(无可用锁)38号错误码为:Function not implemented(功能未实现)39号错误码为:Directory not empty(目录非空)40号错误码为:Too many levels of symbolic links(符号链接层数过多)41号错误码为:Unknown error 41(未知错误41)42号错误码为:No message of desired type(无所需类型的消息)43号错误码为:Identifier removed(标识符已移除)44号错误码为:Channel number out of range(通道号超出范围)45号错误码为:Level 2 not synchronized(第二层未同步)46号错误码为:Level 3 halted(第三层停止)47号错误码为:Level 3 reset(第三层重置)48号错误码为:Link number out of range(链接号超出范围)49号错误码为:Protocol driver not attached(协议驱动程序未附加)50号错误码为:No CSI structure available(无CSI结构可用)51号错误码为:Level 2 halted(第二层停止)52号错误码为:Invalid exchange(无效交换)53号错误码为:Invalid request descriptor(无效请求描述符)54号错误码为:Exchange full(交换区满)55号错误码为:No anode(无阳极)56号错误码为:Invalid request code(无效请求代码)57号错误码为:Invalid slot(无效槽)58号错误码为:Unknown error 58(未知错误58)59号错误码为:Bad font file format(错误的字体文件格式)60号错误码为:Device not a stream(设备不是流)61号错误码为:No data available(无可用数据)62号错误码为:Timer expired(计时器超时)63号错误码为:Out of streams resources(流资源耗尽)64号错误码为:Machine is not on the network(机器不在网络上)65号错误码为:Package not installed(软件包未安装)66号错误码为:Object is remote(对象是远程的)67号错误码为:Link has been severed(链接已切断)68号错误码为:Advertise error(广告错误)69号错误码为:Srmount error(Srmount错误)70号错误码为:Communication error on send(发送时通信错误)71号错误码为:Protocol error(协议错误)72号错误码为:Multihop attempted(尝试多跳)73号错误码为:RFS specific error(RFS特定错误)74号错误码为:Bad message(错误消息)75号错误码为:Value too large for defined data type(值对于定义的数据类型过大)76号错误码为:Name not unique on network(网络上的名称不唯一)77号错误码为:File descriptor in bad state(文件描述符处于错误状态)78号错误码为:Remote address changed(远程地址已更改)79号错误码为:Can not access a needed shared library(无法访问所需的共享库)80号错误码为:Accessing a corrupted shared library(访问已损坏的共享库)81号错误码为:.lib section in a.out corrupted(a.out中的.lib节损坏)82号错误码为:Attempting to link in too many shared libraries(尝试链接过多的共享库)83号错误码为:Cannot exec a shared library directly(不能直接执行共享库)84号错误码为:Invalid or incomplete multibyte or wide character(无效或不完整的多字节或宽字符)85号错误码为:Interrupted system call should be restarted(被中断的系统调用应重新启动)86号错误码为:Streams pipe error(流管道错误)87号错误码为:Too many users(用户过多)88号错误码为:Socket operation on non-socket(在非套接字上执行套接字操作)89号错误码为:Destination address required(需要目标地址)90号错误码为:Message too long(消息过长)91号错误码为:Protocol wrong type for socket(套接字协议类型错误)92号错误码为:Protocol not available(协议不可用)93号错误码为:Protocol not supported(协议不支持)94号错误码为:Socket type not supported(套接字类型不支持)95号错误码为:Operation not supported(操作不支持)96号错误码为:Protocol family not supported(协议族不支持)97号错误码为:Address family not supported by protocol(协议不支持地址族)98号错误码为:Address already in use(地址已在使用中)99号错误码为:Cannot assign requested address(无法分配请求的地址)100号错误码为:Network is down(网络关闭)101号错误码为:Network is unreachable(网络不可达)102号错误码为:Network dropped connection on reset(网络在重置时断开连接)103号错误码为:Software caused connection abort(软件导致连接中止)104号错误码为:Connection reset by peer(对端重置连接)105号错误码为:No buffer space available(无缓冲空间可用)106号错误码为:Transport endpoint is already connected(传输端点已连接)107号错误码为:Transport endpoint is not connected(传输端点未连接)108号错误码为:Cannot send after transport endpoint shutdown(传输端点关闭后无法发送)109号错误码为:Too many references: cannot splice(引用过多:无法拼接)110号错误码为:Connection timed out(连接超时)111号错误码为:Connection refused(连接被拒绝)112号错误码为:Host is down(主机关闭)113号错误码为:No route to host(无路由到主机)114号错误码为:Operation already in progress(操作已在进展中)115号错误码为:Operation now in progress(操作现在在进展中)116号错误码为:Stale file handle(过时的文件句柄)117号错误码为:Structure needs cleaning(结构需要清理)118号错误码为:Not a XENIX named type file(不是XENIX命名类型文件)119号错误码为:No XENIX semaphores available(无XENIX信号量可用)120号错误码为:Is a named type file(是命名类型文件)121号错误码为:Remote I/O error(远程I/O错误)122号错误码为:Disk quota exceeded(磁盘配额超出)123号错误码为:No medium found(未找到介质)124号错误码为:Wrong medium type(错误的介质类型)125号错误码为:Operation canceled(操作已取消)126号错误码为:Required key not available(所需密钥不可用)127号错误码为:Key has expired(密钥已过期)128号错误码为:Key has been revoked(密钥已被撤销)129号错误码为:Key was rejected by service(密钥被服务拒绝)130号错误码为:Owner died(所有者死亡)131号错误码为:State not recoverable(状态不可恢复)132号错误码为:Operation not possible due to RF-kill(由于RF-kill,操作不可能)133号错误码为:Memory page has hardware error(内存页有硬件错误)134号错误码为:Unknown error 134(未知错误134)135号错误码为:Unknown error 135(未知错误135)136号错误码为:Unknown error 136(未知错误136)137号错误码为:Unknown error 137(未知错误137)138号错误码为:Unknown error 138(未知错误138)139号错误码为:Unknown error 139(未知错误139)140号错误码为:Unknown error 140(未知错误140)141号错误码为:Unknown error 141(未知错误141)142号错误码为:Unknown error 142(未知错误142)143号错误码为:Unknown error 143(未知错误143)144号错误码为:Unknown error 144(未知错误144)145号错误码为:Unknown error 145(未知错误145)146号错误码为:Unknown error 146(未知错误146)147号错误码为:Unknown error 147(未知错误147)148号错误码为:Unknown error 148(未知错误148)149号错误码为:Unknown error 149(未知错误149)
接下来我们举例验证:
#include <stdio.h>// 这段代码既可以在C语言环境下编译也可以在C++环境下编译
#ifdef __cplusplus#include <cerrno>#include <cstring>using std::strerror;
#else#include <errno.h>#include <string.h>
#endifint main()
{FILE *file = fopen("nonexistent.txt", "r");if (file == NULL) {// 函数失败后检查errnoprintf("错误信息为%d号,其内容为: %s\n", errno,strerror(errno));//errno是一个全局整型变量,用于存储系统调用和库函数执行失败时的错误代码。当函数调用失败时,它会设置errno为一个特定的值来指示错误类型。//strerror() - 将错误码转换为可读字符串//perror() - 直接打印错误信息//errno是线程安全的perror("fopen failed"); // 另一种输出错误信息的方式}//errno的使用场景//系统调用失败时 - open, read, write, fork等//标准库函数失败时 - fopen, malloc, strtol等//数学函数错误时 - 如除零错误、数值溢出等//网络编程错误 - socket, connect, bind等return 0;
}
结果为:
同时也可以用define宏定义来自定义错误码,案例如下
#define FAILED 1
代码的正常结束有以下几类:
-
代码跑完(代码运行期间,没有收到信号) 0 && return 0 -> signumber :0 && 退出码: 0
-
signumber :0 && 退出码:0
-
signumber :10 && 退出码无意义
进程执行的结果状态,可以用两个数字表示: sig(信号), exit_code(退出码);退出码
用户是不需要维护的。
当一个进程退出的时候,OS会把进程退出的详细信息写入到进程的task_struct 结构体中!所以,进程退出,需要僵尸维持自己的退出状态!
Linux Shell 中的主要退出码如下:
Linux 进程退出码表格
| 退出码 | 含义 | 说明 | 典型场景 |
|---|---|---|---|
| 0 | 成功 (Success) | 程序正常执行完成,没有错误 | 命令成功执行 |
| 1 | 一般错误 (Catchall for general errors) | 未明确分类的错误 | 权限不足、参数错误等 |
| 2 | Shell 内置命令误用 (Misuse of shell builtins) | Shell 内置命令使用不当 | 如 bash 中错误使用内置命令 |
| 126 | 命令不可执行 (Command cannot execute) | 命令文件存在但无法执行 | 权限问题、非可执行文件 |
| 127 | 命令未找到 (Command not found) | 在 PATH 中找不到命令 | 命令拼写错误、程序未安装 |
| 128 | 无效退出参数 (Invalid exit argument) | exit 参数无效 | exit 使用了无效参数 |
信号相关退出码 (128 + 信号编号)
| 退出码 | 对应信号 | 信号含义 | 触发场景 |
|---|---|---|---|
| 129 | SIGHUP (1) | 挂起 | 控制终端关闭 |
| 130 | SIGINT (2) | 键盘中断 | Ctrl+C 终止 |
| 131 | SIGQUIT (3) | 退出 | Ctrl+\ 终止 |
| 132 | SIGILL (4) | 非法指令 | 执行了非法CPU指令 |
| 133 | SIGTRAP (5) | 跟踪陷阱 | 调试器断点 |
| 134 | SIGABRT (6) | 异常中止 | abort() 函数调用 |
| 135 | SIGBUS (7) | 总线错误 | 内存访问错误 |
| 136 | SIGFPE (8) | 浮点异常 | 除零错误、算术溢出 |
| 137 | SIGKILL (9) | 强制杀死 | kill -9 无法捕获 |
| 138 | SIGUSR1 (10) | 用户定义信号1 | 用户自定义 |
| 139 | SIGSEGV (11) | 段错误 | 非法内存访问 |
| 140 | SIGUSR2 (12) | 用户定义信号2 | 用户自定义 |
| 141 | SIGPIPE (13) | 管道破裂 | 向已关闭管道写入 |
| 142 | SIGALRM (14) | 定时器信号 | 闹钟超时 |
| 143 | SIGTERM (15) | 终止信号 | kill 默认信号 |
| 255 | 超出范围 | 退出码超出 0-255 范围 | 程序返回了负数或大于255的值 |
特殊用途退出码
| 退出码 | 含义 | 使用场景 |
|---|---|---|
| 64 | 命令使用错误 | 按照 BSD sysexits.h 规范 |
| 65 | 数据格式错误 | 按照 BSD sysexits.h 规范 |
| 66 | 无法打开输入文件 | 按照 BSD sysexits.h 规范 |
| 67 | 地址未知 | 按照 BSD sysexits.h 规范 |
| 68 | 主机名未知 | 按照 BSD sysexits.h 规范 |
| 69 | 服务不可用 | 按照 BSD sysexits.h 规范 |
_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值
_exit 是一个系统调用,用于立即终止当前进程,不执行任何清理操作。它直接返回到操作系统内核。
status:进程的退出状态,通常 0 表示成功,非零表示错误
说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现返回值是255。
exit函数
#include <stdlib.h>
void exit(int status);
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
-
执行用户通过 atexit或on_exit定义的清理函数。
-
关闭所有打开的流,所有的缓存数据均被写入
-
调用_exit "
_exit函数 vs exit函数 的主要区别
| 对比方面 | _exit 函数 | exit 函数 |
|---|---|---|
| 头文件 | <unistd.h> | <stdlib.h> |
| 函数原型 | void _exit(int status); | void exit(int status); |
| 标准规范 | POSIX 标准 | C/C++ 标准 |
| 清理机制 | 立即终止,不执行任何清理 | 执行完整的清理流程 |
| I/O缓冲区 | ❌ 不刷新任何缓冲区 | ✅ 刷新所有标准I/O缓冲区 |
| atexit函数 | ❌ 不调用注册的退出函数 | ✅ 调用所有注册的退出函数 |
| 文件描述符 | ❌ 不自动关闭打开的文件 | ✅ 自动关闭所有打开的文件 |
| 临时文件 | ❌ 不删除临时文件 | ✅ 删除由tmpfile()创建的临时文件 |
| 退出状态 | status & 0377 | status & 0377 |
| 多线程安全 | 直接终止所有线程 | 会执行线程局部存储析构 |
| 使用场景 | 子进程退出、严重错误恢复 | 正常程序退出、资源清理 |
| 性能影响 | 快速,无额外开销 | 较慢,需要执行清理操作 |
| 资源泄漏风险 | 较高(文件、内存等可能泄漏) | 较低(自动释放资源) |
| 数据完整性 | 可能丢失缓冲区的数据 | 保证数据写入持久存储 |
| 调用链 | 内核 → 进程终止 | 用户清理 → 内核 → 进程终止 |
| 示例代码 | _exit(1); | exit(0); |
| 替代函数 | _Exit()(C99) | 无直接替代 |
可以参考这个图
终止进程最好使用exit函数
return函数
return是⼀种更常⻅的退出进程⽅法。执⾏return n等同于执⾏exit(n),因为调⽤main的运⾏时函数会将main的返回值当做 exit的参数
进程等待
进程等待的必要性
• 之前讲过,子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
• 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
• 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
• 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
如果父进程wait子进程,但是子进程不退出,那么父进程就会阻塞到wait中
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:默认为0,表⽰阻塞等待WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
• 之前讲过,子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
• 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
• 最后,父进程派给子进程的任务完成的如何,我们需要知道。如果子进程运行完成,结果对还是不对,或者是否正常退出。
• 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
参数status是来获取子进程退出的信息的,本质上是为了得到子进程推出的两个数字。
status有32位,但是我们只使用其低16位。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
我们以这段代码验证
#include <sys/wait.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h> // 添加这行int main()
{pid_t pid;if ((pid = fork()) == -1)perror("fork"), exit(1);if (pid == 0){sleep(2);exit(10);}else{int st;int ret = wait(&st);if (ret > 0 && (st & 0X7F) == 0){ // 正常退出printf("child exit code:%d\n", (st >> 8) & 0XFF);}else if (ret > 0){ // 异常退出printf("sig code : %d\n", st & 0X7F);}}
}
结果在等待2秒后输出
阻塞与非阻塞等待
进程的阻塞等待
我们写一段代码来验证:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>int main()
{printf("我是一个父进程:pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id < 0){exit(1);}else if(id == 0){int cnt = 5;while(cnt--){printf("我是一个子进程:pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);int* ptr=NULL;*ptr=100;//NULL的0号地址没办法写入,会导致野指针问题}exit(0);}else{int status = 0;pid_t rid = waitpid(-1, &status, 0);if(rid > 0){printf("等待成功,推出的子进程是:%d,退出码为:%d,退出信号为:%d\n", rid, (status >> 8) & 0xFF, status & 0x7F); // 修正:0xFF 和 0x7F//(status>>8)&0xFF的作用:提取进程的正常退出码//原理:status >> 8:将状态值右移8位,把退出码移到低位,& 0xFF:用位与操作取低8位(一个字节),//当进程正常退出时(通过 exit()),退出码存储在高8位中//status&0x7F的作用:提取导致进程异常退出的信号编号//原理:0x7F 的二进制是 0111 1111 //& 0x7F:用位与操作取低7位//当进程因信号而异常终止时,信号编号存储在低7位中}}return 0;
}
结果为:

我们的代码中是有野指针的,不过仍然正常退出。我们想排查错误可以通过
kill -l
来查找推出信号。

这些信号的内容和相关含义可以去文章末尾去查找,这里不再赘述。
#include <stdio.h> // 用于 printf
#include <stdlib.h> // 用于 exit
#include <unistd.h> // 用于 fork, sleep, getpid
#include <sys/types.h> // 用于 pid_t
#include <sys/wait.h> // 用于 waitpid, WIFEXITED, WEXITSTATUSint main()
{pid_t pid;pid = fork();if(pid < 0){printf("%s fork error\n",__FUNCTION__);return 1;} else if( pid == 0 ){ //childprintf("child is run, pid is : %d\n",getpid());sleep(5);exit(257);} else{ //parentint status = 0;pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5Sprintf("this is test for wait\n");if( WIFEXITED(status) && ret == pid ){printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else{printf("wait child failed, return.\n");return 1;}}return 0;
}
进程的非等待阻塞
#include<iostream>
#include<cstdio>
#include<vector>
#include<sys/wait.h>
#include<sys/types.h>
#include<cstdlib>
#include<string>
#include<unistd.h>
using std::cout;
using std::endl;
using std::vector;typedef void (*callback_t)(); // 函数指针类型enum
{OK, // 正确运行USAGE_ERR // 错误码
};void Task()
{int cnt = 5;while(cnt--){printf("我是一个子进程1, pid:>%d, ppid:>%d, cnt=%d\n", getpid(), getppid(), cnt);sleep(1);}
}void Hello()
{int cnt = 5;while(cnt--){printf("我是一个子进程2, 在完成hello任务, pid:>%d, ppid:>%d, cnt=%d\n", getpid(), getppid(), cnt);sleep(1);}
}// 新增任务函数3
void Work()
{int cnt = 5;while(cnt--){printf("我是一个子进程3, 正在工作, pid:>%d, ppid:>%d, cnt=%d\n", getpid(), getppid(), cnt);sleep(1);}
}// 新增任务函数4
void Play()
{int cnt = 5;while(cnt--){printf("我是一个子进程4, 正在玩耍, pid:>%d, ppid:>%d, cnt=%d\n", getpid(), getppid(), cnt);sleep(1);}
}// 对于参数一般而言
// 对于输入性参数:const &
// 对于输出性参数:*
// 对于输入输出性参数:&
template<class T>
void CreateChildProcess(int num, vector<T>* v, const vector<callback_t>& cbs)
{for(int i = 0; i < num; ++i){pid_t id = fork();if(id == 0) // child{ // 根据下标i选择对应的任务函数// 使用取模运算确保不会越界int task_index = i % cbs.size();cbs[task_index](); // 执行对应的任务函数exit(0);}v->push_back(id); // 因为v是指针,所以用箭头printf("创建子进程 %d,将执行任务 %lu\n", id, i % cbs.size() + 1);}
}template<class T>
void WaitChild(const vector<T>& subs)
{for(auto& pdi : subs){int status = 0;pid_t rid = waitpid(pdi, &status, 0);if(rid > 0){printf("子进程为 %d, 退出码:%d\n", rid, WEXITSTATUS(status)); sleep(1);}}
}// 启动多进程方案
// 要这么用:./xxx 5 ??我们要创建5个子进程
int main(int argc, char* argv[])
{if(argc != 2){cout << "用法: " << argv[0] << " <进程数量>" << endl;exit(USAGE_ERR);}int num = atoi(argv[1]); // 使用atoi避免兼容性问题vector<pid_t> subs; // 进程PID容器vector<callback_t> cbs; // 任务函数容器// 初始化任务函数容器cbs.push_back(Task);cbs.push_back(Hello);cbs.push_back(Work);cbs.push_back(Play);cout << "可用任务函数数量: " << cbs.size() << endl;cout << "将创建 " << num << " 个子进程" << endl;// 创建多进程CreateChildProcess(num, &subs, cbs);// 等待多进程WaitChild(subs);cout << "所有子进程执行完毕" << endl;return 0;
}
运行结果为:

父进程等待时间取决于子进程推出时间
阻塞等待在等待期间,什么都没干!!
非阻塞等待,等待期间,可以干其他事!把等待时间利用了起来!
进程程序替换
一个进程fork出父子进程后,谁先运行呢?这个是不那么确定的。
不过对于一般的父子进程来说,谁先退出呢?子进程,然后由父进程回收子进程的资源。
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

程序替换本质是从磁盘中的代码和数据拷贝到内存中。程序运行前必须加载到内存。
替换函数
其实有六种以exec开头的函数,统称exec函数
#include <unistd.h>//库函数
int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);int execle(const char *path, const char *arg, ...,char *const envp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[],char *const envp[]);
//系统调用函数
int execve(const char *path, char *const argv[], char *const envp[]);
命名理解
-
exec:表示执行新程序
-
l:参数以列表(list)形式传递
-
v:参数以向量/数组(vector)形式传递
-
p:在PATH环境变量中搜索可执行文件
-
e:可以指定环境变量(environment)
| 函数名 | 参数格式 | 是否使用PATH搜索 | 是否使用当前环境变量 | 常用程度 | 特点说明 |
|---|---|---|---|---|---|
| execl | 列表 | 否 | 是 | ★★☆☆☆ | 需要完整路径,参数逐个列出 |
| execlp | 列表 | 是 | 是 | ★★★☆☆ | 自动PATH搜索,参数逐个列出 |
| execle | 列表 | 否 | 否,须自己组装环境变量 | ★★☆☆☆ | 需要完整路径,可自定义环境变量 |
| execv | 数组 | 否 | 是 | ★★★☆☆ | 需要完整路径,参数用数组传递 |
| execvp | 数组 | 是 | 是 | ★★★★★ | 最常用,自动PATH搜索,参数用数组传递 |
| execve | 数组 | 否 | 否,须自己组装环境变量 | ★★★★☆ | 系统调用,可自定义环境变量 |
函数解释
• 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
• 如果调用出错则返回-1
• 所以exec函数只有出错的返回值而没有成功的返回值。
1. execl - 列表形式 + 完整路径
int execl(const char *path, const char *arg, ...);
-
参数列表形式传递参数
-
需要提供完整路径
-
参数以NULL结束
execl("/bin/ls", "ls", "-l", "-a", NULL);
2. execlp - 列表形式 + PATH搜索
int execlp(const char *file, const char *arg, ...);
-
参数列表形式传递参数
-
在PATH环境变量中搜索可执行文件
-
参数以NULL结束
execlp("ls", "ls", "-l", "-a", NULL); // 不需要完整路径
3. execle - 列表形式 + 环境变量
int execle(const char *path, const char *arg, ..., char *const envp[]);
-
参数列表形式传递参数
-
需要提供完整路径
-
可以指定自定义环境变量数组
char *env[] = {"HOME=/home/user", "PATH=/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, env);
4. execv - 数组形式 + 完整路径
int execv(const char *path, char *const argv[]);
-
参数数组形式传递参数
-
需要提供完整路径
char *args[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", args);
5. execvp - 数组形式 + PATH搜索
int execvp(const char *file, char *const argv[]);
-
参数数组形式传递参数
-
在PATH环境变量中搜索可执行文件
char *args[] = {"ls", "-l", "-a", NULL};
execvp("ls", args); // 最常用的版本
6. execve - 数组形式 + 环境变量
int execve(const char *path, char *const argv[], char *const envp[]);
-
参数数组形式传递参数
-
需要提供完整路径
-
可以指定自定义环境变量数组
-
这是唯一的系统调用,其他都是库函数
char *args[] = {"ls", "-l", NULL};
char *env[] = {"HOME=/home/user", NULL};
execve("/bin/ls", args, env);
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。

下面我们以以下代码来做案例:
#include<stdio.h>
#include<sys/wait.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{printf("我变成了一个进程:%d\n", getpid());pid_t id = fork();if(id == 0){// 执行另一个程序的代码execl("/usr/bin/ls", "ls", "-a", "-l", NULL); //程序替换函数exit(0);}wait(NULL);printf("我的代码运行中......\n");printf("我的代码运行中......\n");printf("我的代码运行中......\n");printf("我的代码运行中......\n");printf("我的代码运行中......\n");printf("我的代码运行中......\n");return 0;
}
对于程序替换,部分参数可以省略。
替换函数成功时没有返回值,失败时返回-1。
也可以自己替换自己,不过会陷入死循环。(不推荐)
替换函数可以实现执行其他语言的程序
我们举例说明。
C++测试代码
#include<iostream>
using std::cout;
using std::endl;int main()
{cout<<"我是一个C++程序"<<endl; cout<<"我是一个C++程序"<<endl; cout<<"我是一个C++程序"<<endl; cout<<"我是一个C++程序"<<endl; cout<<"我是一个C++程序"<<endl; cout<<"我是一个C++程序"<<endl; return 0;
}
C语言代码
//C语言程序
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include <sys/wait.h>
//C语言下调用执行C++程序
int main()
{printf("我变成了一个进程:%d\n", getpid());pid_t id = fork();if(id == 0){sleep(2);printf("下面的代码都是子进程在执行:\n");execl("./test/Test","Test",NULL);return 0;}// 父进程代码可以放在这里wait(NULL); // 等待子进程结束printf("父进程结束\n");int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){printf("wait success, exit code: %d\n", WEXITSTATUS(status));} return 0;
}
结果为:

不仅是C++,Java,python甚至脚本语言都可以调用执行
替换函数与环境变量
主要函数为execvpe。其语法如下
以以下代码为例子
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>int main()
{printf("我变成了一个进程:%d\n", getpid());pid_t id = fork();if(id == 0){sleep(2);printf("下面的代码都是子进程在执行:\n");char *myargv[] = {(char*)"./test/Test",(char*)"-a",(char*)"-b",NULL};char *myenv[] = {(char*)"PATH=/home/loukou-ruizi/linux-learning/course_control/Exec/test/Test",NULL};extern char**environ;execvpe("./test/Test", myargv, environ); //覆盖式的使用全新的环境变量表 execvpe("./test/Test", myargv, myenv); //使用父进程的环境变量表}return 0;
}


命令行参数表和环境变量表是父进程通过exec*传递给你的
Linux信号含义表
| 信号编号 | 信号名称 | 含义说明 | 默认行为 |
|---|---|---|---|
| 1 | SIGHUP | 挂起信号,终端断开连接 | 终止进程 |
| 2 | SIGINT | 中断信号,Ctrl+C产生 | 终止进程 |
| 3 | SIGQUIT | 退出信号,Ctrl+\产生 | 终止并核心转储 |
| 4 | SIGILL | 非法CPU指令 | 终止并核心转储 |
| 5 | SIGTRAP | 跟踪陷阱,调试器使用 | 终止并核心转储 |
| 6 | SIGABRT | 异常终止,abort()产生 | 终止并核心转储 |
| 7 | SIGBUS | 总线错误,内存访问错误 | 终止并核心转储 |
| 8 | SIGFPE | 浮点异常,如除零错误 | 终止并核心转储 |
| 9 | SIGKILL | 强制终止,不可捕获/忽略 | 终止进程 |
| 10 | SIGUSR1 | 用户自定义信号1 | 终止进程 |
| 11 | SIGSEGV | 段错误,非法内存访问 | 终止并核心转储 |
| 12 | SIGUSR2 | 用户自定义信号2 | 终止进程 |
| 13 | SIGPIPE | 管道破裂,向无读端管道写数据 | 终止进程 |
| 14 | SIGALRM | 定时器信号,alarm()超时 | 终止进程 |
| 15 | SIGTERM | 终止信号,友好终止请求 | 终止进程 |
| 16 | SIGSTKFLT | 栈错误 | 终止进程 |
| 17 | SIGCHLD | 子进程状态改变 | 忽略信号 |
| 18 | SIGCONT | 继续执行,让停止进程继续 | 继续进程 |
| 19 | SIGSTOP | 强制暂停,不可捕获/忽略 | 停止进程 |
| 20 | SIGTSTP | 终端停止信号,Ctrl+Z产生 | 停止进程 |
| 21 | SIGTTIN | 后台进程尝试读控制终端 | 停止进程 |
| 22 | SIGTTOU | 后台进程尝试写控制终端 | 停止进程 |
| 23 | SIGURG | 紧急数据到达 | 忽略信号 |
| 24 | SIGXCPU | 超过CPU时间限制 | 终止并核心转储 |
| 25 | SIGXFSZ | 超过文件大小限制 | 终止并核心转储 |
| 26 | SIGVTALRM | 虚拟定时器超时 | 终止进程 |
| 27 | SIGPROF | 性能分析定时器超时 | 终止进程 |
| 28 | SIGWINCH | 窗口大小改变 | 忽略信号 |
| 29 | SIGIO | 异步I/O事件 | 终止进程 |
| 30 | SIGPWR | 电源故障 | 终止进程 |
| 31 | SIGSYS | 非法系统调用 | 终止并核心转储 |
| 32 | (未使用) | 保留信号,通常未使用 | - |
| 33 | (未使用) | 保留信号,通常未使用 | - |
实时信号说明
| 信号范围 | 类型 | 说明 |
|---|---|---|
| 34-64 | SIGRTMIN到SIGRTMAX | 实时信号,用于应用程序自定义用途,可以排队不会丢失 |
关键特性总结
| 类别 | 信号 | 特点 |
|---|---|---|
| 不可捕获 | SIGKILL(9), SIGSTOP(19) | 不能被进程捕获、忽略或阻塞 |
| 常见错误 | SIGSEGV(11), SIGFPE(8), SIGABRT(6) | 程序错误导致的信号 |
| 用户交互 | SIGINT(2), SIGTSTP(20), SIGQUIT(3) | 用户通过终端产生的信号 |
| 进程控制 | SIGTERM(15), SIGKILL(9), SIGCONT(18) | 用于进程管理和控制的信号 |
| 保留信号 | 32, 33 | 系统保留,通常不使用 |
补充说明
-
信号32和33:在大多数Linux系统中,这两个信号是保留的,不用于常规用途
-
实时信号:34-64号信号为实时信号,具有排队功能,不会像标准信号那样丢失
-
默认行为:
-
终止:进程立即结束
-
终止并核心转储:进程结束并生成核心转储文件用于调试
-
停止:进程暂停执行
-
继续:让停止的进程恢复执行
-
忽略:信号被忽略,不做任何处理
-
本期的内容就到这里啦,喜欢的话请点个赞谢谢
封面图自取:
