Linux学习:进程的控制

前面我们已经学习了进程的相关概念。本篇我们就来学习Linux系统中进程的控制

相关代码已经上传至作者的个人gitee:楼田莉子/Linux学习 - Gitee.com喜欢请点个赞谢谢

目录

进程的创建

fork函数的介绍

fork函数返回值

写时拷贝

fork常规用法

fork调用失败的原因

进程的终止

进程退出

退出码

[Linux 进程退出码表格](#Linux 进程退出码表格)

信号相关退出码 (128 + 信号编号)

特殊用途退出码

_exit函数

exit函数

[_exit函数 vs exit函数 的主要区别](#_exit函数 vs exit函数 的主要区别)

return函数

进程等待

进程等待的必要性

进程等待的方法

wait方法

waitpid方法

获取子进程status

阻塞与非阻塞等待

进程的阻塞等待

进程的非等待阻塞

进程程序替换

替换原理

替换函数

命名理解

函数解释

[1. execl - 列表形式 + 完整路径](#1. execl - 列表形式 + 完整路径)

[2. execlp - 列表形式 + PATH搜索](#2. execlp - 列表形式 + PATH搜索)

[3. execle - 列表形式 + 环境变量](#3. execle - 列表形式 + 环境变量)

[4. execv - 数组形式 + 完整路径](#4. execv - 数组形式 + 完整路径)

[5. execvp - 数组形式 + PATH搜索](#5. execvp - 数组形式 + PATH搜索)

[6. execve - 数组形式 + 环境变量](#6. execve - 数组形式 + 环境变量)

替换函数可以实现执行其他语言的程序

替换函数与环境变量

Linux信号含义表

实时信号说明

关键特性总结

补充说明


进程的创建

fork函数的介绍

在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程

cpp 复制代码
#include <unistd.h>
pid_t fork(void);
//返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

• 分配新的内存块和内核数据结构给子进程

• 将父进程部分数据结构内容拷贝至子进程

• 添加子进程到系统进程列表当中

• fork返回,开始调度器调度

就像下图:

当⼀个进程调⽤fork之后,就有两个⼆进制代码相同的进程。⽽且它们都运⾏到相同的地⽅。但每个进程都将可以开始它们⾃⼰的旅程,看如下程序。

cpp 复制代码
#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++代码中通常是这样的

cpp 复制代码
int main()
{
    //code
    return 0;
}

其中return 0代表终止。这就有以下几种情况

  1. 代码跑完,结果正确

  2. 代码跑完,结果不正确

  3. 代码都没跑完,进程异常了!

进程退出

之前有的代码我们可以通过打印来获取结果,但是很多时候代码的结果是不那么容易显示出来的(比如网络发送等),那么我们是如何确认代码正确与否呢?所以main函数才会返回0,代表着进程的退出。0表示成功,非0表示错误。而main函数返回的0会交给父进程(这里就是bash)。通过以下命令来打印上一个子进程的退出码数字

bash 复制代码
echo $?

正常终止

  1. 从main返回

  2. 调用exit

  3. _exit

异常退出

• ctrl + c,信号终止

退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。

代码 1 或 0 以外的任何代码都被视为不成功。

退出码的捕获可以用strerror函数来获取,我们可以写一段程序来检验

cpp 复制代码
#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;
}

结果为:(括号内容为翻译)

bash 复制代码
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)

接下来我们举例验证:

cpp 复制代码
#include <stdio.h>

// 这段代码既可以在C语言环境下编译也可以在C++环境下编译
#ifdef __cplusplus
    #include <cerrno>
    #include <cstring>
    using std::strerror;
#else
    #include <errno.h>
    #include <string.h>
#endif

int main() 
{
    FILE *file = fopen("nonexistent.txt", "r");
    if (file == NULL) 
    {
        // 函数失败后检查errno
        printf("错误信息为%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宏定义来自定义错误码,案例如下

cpp 复制代码
#define FAILED 1

代码的正常结束有以下几类:

  1. 代码跑完(代码运行期间,没有收到信号) 0 && return 0 -> signumber :0 && 退出码: 0

  2. signumber :0 && 退出码:0

  3. 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函数

cpp 复制代码
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

_exit 是一个系统调用,用于立即终止当前进程,不执行任何清理操作。它直接返回到操作系统内核。

status:进程的退出状态,通常 0 表示成功,非零表示错误

说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现返回值是255。

exit函数

cpp 复制代码
#include <stdlib.h>
void exit(int status);

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。

  2. 关闭所有打开的流,所有的缓存数据均被写入

  3. 调用_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方法

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
    成功返回被等待进程pid,失败返回-1。
参数:
    输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

如果父进程wait子进程,但是子进程不退出,那么父进程就会阻塞到wait中

waitpid方法

cpp 复制代码
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比特位):

我们以这段代码验证

cpp 复制代码
#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秒后输出

阻塞与非阻塞等待

进程的阻塞等待

我们写一段代码来验证:

cpp 复制代码
#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;
}

​

​

​

结果为:

我们的代码中是有野指针的,不过仍然正常退出。我们想排查错误可以通过

bash 复制代码
kill -l

来查找推出信号。

这些信号的内容和相关含义可以去文章末尾去查找,这里不再赘述。

cpp 复制代码
#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, WEXITSTATUS

int main()
{
    pid_t pid;
    pid = fork();
    
    if(pid < 0){
        printf("%s fork error\n",__FUNCTION__);
        return 1;
    } else if( pid == 0 ){ //child
        printf("child is run, pid is : %d\n",getpid());
        sleep(5);
        exit(257);
    } else{ //parent
        int status = 0;
        pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
        printf("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;
}

进程的非等待阻塞

cpp 复制代码
#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函数

cpp 复制代码
#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 - 列表形式 + 完整路径
cpp 复制代码
int execl(const char *path, const char *arg, ...);
  • 参数列表形式传递参数

  • 需要提供完整路径

  • 参数以NULL结束

cpp 复制代码
execl("/bin/ls", "ls", "-l", "-a", NULL);
2. execlp - 列表形式 + PATH搜索
cpp 复制代码
int execlp(const char *file, const char *arg, ...);
  • 参数列表形式传递参数

  • PATH环境变量中搜索可执行文件

  • 参数以NULL结束

cpp 复制代码
execlp("ls", "ls", "-l", "-a", NULL);  // 不需要完整路径
3. execle - 列表形式 + 环境变量
cpp 复制代码
int execle(const char *path, const char *arg, ..., char *const envp[]);
  • 参数列表形式传递参数

  • 需要提供完整路径

  • 可以指定自定义环境变量数组

cpp 复制代码
char *env[] = {"HOME=/home/user", "PATH=/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, env);
4. execv - 数组形式 + 完整路径
cpp 复制代码
int execv(const char *path, char *const argv[]);
  • 参数数组形式传递参数

  • 需要提供完整路径

cpp 复制代码
char *args[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", args);
5. execvp - 数组形式 + PATH搜索
cpp 复制代码
int execvp(const char *file, char *const argv[]);
  • 参数数组形式传递参数

  • PATH环境变量中搜索可执行文件

cpp 复制代码
char *args[] = {"ls", "-l", "-a", NULL};
execvp("ls", args);  // 最常用的版本
6. execve - 数组形式 + 环境变量
cpp 复制代码
int execve(const char *path, char *const argv[], char *const envp[]);
复制代码
  • 参数数组形式传递参数

  • 需要提供完整路径

  • 可以指定自定义环境变量数组

  • 这是唯一的系统调用,其他都是库函数

cpp 复制代码
char *args[] = {"ls", "-l", NULL};
char *env[] = {"HOME=/home/user", NULL};
execve("/bin/ls", args, env);

事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。

下面我们以以下代码来做案例:

cpp 复制代码
#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++测试代码

cpp 复制代码
#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语言代码

cpp 复制代码
//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。其语法如下

以以下代码为例子

cpp 复制代码
#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号信号为实时信号,具有排队功能,不会像标准信号那样丢失

  • 默认行为

    • 终止:进程立即结束

    • 终止并核心转储:进程结束并生成核心转储文件用于调试

    • 停止:进程暂停执行

    • 继续:让停止的进程恢复执行

    • 忽略:信号被忽略,不做任何处理

本期的内容就到这里啦,喜欢的话请点个赞谢谢

封面图自取:

相关推荐
JiMoKuangXiangQu2 小时前
Linux:文件 mmap 读写流程简析
linux·内存管理·file mmap
大菠萝学姐2 小时前
基于springboot的旅游攻略网站设计与实现
前端·javascript·vue.js·spring boot·后端·spring·旅游
云动雨颤2 小时前
服务器有哪些功能?网站托管/CDN加速/云计算部署必知方案
服务器·云计算·cdn
捷智算云服务2 小时前
H100服务器维修“病历卡”:五大常见故障现象与根源分析
运维·服务器
wzlsunice882 小时前
用vir-manager创建kvm虚拟机(创建网桥和配置网络等)
运维·网络
回家路上绕了弯2 小时前
服务器大量请求超时?从网络到代码的全链路排查指南
分布式·后端
SimonKing2 小时前
SpringBoot邮件发送怎么玩?比官方自带的Mail更好用的三方工具
java·后端·程序员
武子康2 小时前
大数据-150 Apache Druid 单机部署实战:架构速览、启动清单与故障速修
大数据·后端·apache
IT_陈寒2 小时前
Redis 高并发实战:我从 5000QPS 优化到 5W+ 的7个核心策略
前端·人工智能·后端