📝前言:
这篇文章我们来讲讲进程控制
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
一,进程创建
fork
:如何使用可以看:进程概念(二)- 创建子进程的意义:
- ⼀个⽗进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,⽣成子进程来处理请求。
- 通过子进程调用其他程序。
fork
失败的原因- 系统中有太多的进程
- 实际用户的进程数超过了限制
二,进程终止
(1)进程退出的场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
以main函数为例,说明上面三种情况:
- 运行完毕,结果正确:返回
0
- 运行完毕,结果不正确:返回
非零值
- 异常终止:此时返回值无意义,会
产生一个信号
(信号用来反映异常情况)
这个返回值也叫退出码。(向操作系统或调用者传达程序的执行状态)
(2)进程常见退出方法
正常终止(退出码有意义):
- 函数返回
return
- 调用
exit
_exit
异常终止(退出码无意义):
- 因为异常的退出,如
ctrl + c
退出码
- 意义:退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态
- 查看退出码:
echo $?
,?
会存储最近一个程序的退出码 - Linux Shell 中的主要退出码(可以通过
strerror
函数查看退出码对应的错误描述)
示例:
#include <stdio.h>
int main()
{
printf("hello world\n");
return 2;
}
在 Linux Shell 里,退出码能够反映命令执行后的状态。下面为你介绍一些常用的退出码及其含义:
退出码 | 含义 | 示例情况 |
---|---|---|
0 | 成功 | 命令正常执行完毕,未出现错误。例如,ls 命令成功列出目录内容。 |
1 | 一般错误 | 大多数命令在遇到非特定错误时返回此退出码。比如命令语法有误、缺少必要的参数等。 |
2 | 误用 Shell 命令 | 通常是因为使用了错误的 Shell 命令选项或者参数。例如,在使用 cd 命令时输入了不存在的目录。 |
126 | 命令不可执行 | 尝试执行一个没有可执行权限的文件。例如,直接运行一个没有添加执行权限的脚本文件。 |
127 | 命令未找到 | 输入的命令在系统的 PATH 环境变量指定的路径中不存在。比如输入了一个拼写错误的命令。 |
128 | 无效的退出参数 | 在使用 exit 命令时,传递了一个无效的退出码参数。例如,exit abc 是不合法的。 |
128 + N | 因信号 N 导致进程终止 | 当进程接收到某个信号而终止时,退出码为 128 加上信号编号。例如,接收到 SIGTERM (信号编号 15)时,退出码为 143(128 + 15)。 |
255 | 退出码超出范围 | 退出码的取值范围是 0 - 255,当使用 exit 命令指定的退出码超出这个范围时,实际的退出码会被截断为 255。例如,exit 300 实际退出码为 255。 |
exit和_exit
exit
和_exit
都是用来退出程序的,用法:
exit(退出码); // 头文件:<stdlib.h>
_exit(退出码); // 头文件:<unistd.h>
区别是:
exit
:会对C语言缓存区进行刷新,因为exit
是C语言的库函数,_exit
是系统调用。exit
对_exit
进行了封装。
注意如果输入 -1
:exit(1)
,最后echo $?
的输出结果会是255
,这是因为status
只有后八位会被父进程所用。(在后面的wait
可以再理解)
return
执行return n
等同于执行exit(n)
,因为调用main
的运行时函数会将·main·的返回值当做exit
的参数
三,进程等待(重点)
(1)进程等待的意义
- 子进程有僵尸状态,保存着PCB和退出信息(忘记的可以看:【Linux】进程状态)
- ⽗进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
- 僵尸进程会造成资源浪费,等待是解决僵尸进程的有效方法
(2)进程等待的方法
wait 和 waitpid
先讲各自特点:
wait
(阻塞等待):pid_t wait(int *wstatus);
- 头文件:
<sys/types.h>
和<sys/wait.h>
- 返回值:
- 等待成功:返回
被等待进程pid
- 等待失败:返回
-1
- 等待成功:返回
- 参数:输出型参数
wstatus
,获取子进程退出状态,不关心则可以设置成为NULL
waitpid
:pid_t waitpid(pid_t pid, int *wstatus, int options);
参数
options
:- 默认为
0
:选择阻塞等待 WNOHANG
:选择非阻塞等待(此时会影响返回值)
- 默认为
pid
:用于指定要等待的子进程,取值有以下几种情况:pid > 0
:等待进程 ID 等于pid
的子进程。pid == -1
:等待任意一个子进程,此时waitpid
与wait
函数作用类似。
wstatus
:输出型参数,一个整型指针,用来反映子进程退出状态(后面详细讲解)
返回值:
- 成功:子进程的进程 ID
- 失败:小于
0
WNOHANG
选项被设置,且没有子进程退出:返回0
。
再讲使用共性:
- 如果子进程已经退出,调用
wait/waitpid
时,wait/waitpid
会立即返回,并且释放资源,获得子进程退出信息。 - 如果在任意时刻调用
wait/waitpid
,子进程存在且正常运行,则进程可能阻塞。 - 如果不存在该子进程,则立即出错返回。
阻塞等待和非阻塞等待
- 阻塞等待:父进程一直等子进程,直到子进程退出,有点像
scanf
一直等输入 - 非阻塞等待:采用非阻塞轮询的方法(即:不断(循环)检查子进程是否退出了),但不立即进入阻塞状态,父进程可以继续执行其他任务
输出型参数wstatus
wstatus
用来反映子进程退出状态,是一个int
指针,但是不能当做整型看待,而是一个位图。(wstatus
是OS从子进程的PCB里面拿到数据写入到父进程的输出型参数wstatus
的)
wstatus
并不能直接表示进程退出信息- 32个
bit
位主要被划分成四个区域[31, 16]
:前16个高bit
位无用,后16个低bit
位才有用[15, 8]
:表示退出状态,即退出码(如果是异常终止、被信号所杀,则这部分无意义)[7]
:core dump
标志[6, 0]
:终止信号
下面显示正常终止和异常终止时,后16
位有效信息:
获得对应的信息有两种方法:
- 位运算操作:
- 比如,要获得退出状态:
(status >> 8) & 0xff
,获得信号编号:status & 0x7f
- 比如,要获得退出状态:
- 使用对应的宏:
WEXITSTATUS(status)
:获取正常退出时的退出码WTERMSIG(status)
:获取导致子进程终止的信号编号
示例代码
等待指定子进程终止并分别用位运算和宏操作获取退出码和信号编号
正常代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
sleep(2);
exit(42);
} else {
// 父进程
int status;
pid_t result = waitpid(pid, &status, 0);
if (result == -1) {
perror("waitpid");
return 1;
}
if(WIFEXITED(status)) // 正常退出
{
// 使用宏获取退出码
printf("子进程正常退出,退出状态码: %d\n", WEXITSTATUS(status));
// 使用位运算操作获得退出码
printf("子进程正常退出,退出状态码:%d\n", (status >> 8) & 0xff);
}
else if(WIFSIGNALED(status))
{
// 用信号杀掉进程,位操作获取异常退出的信号码
printf("子进程异常退出,退出信号编号:%d\n", status & 0x7f);
// 用宏获取异常退出的信号码
printf("子进程异常退出,退出信号编号:%d\n", WTERMSIG(status));
}
}
return 0;
}
运行结果:
子进程正常退出,退出状态码: 42
子进程正常退出,退出状态码:42
如果在子进程里面进行 / 0
操作,使它异常退出:
运行结果:
子进程异常退出,退出信号编号:8
子进程异常退出,退出信号编号:8
通过我们先让子进程结束,然后让父进程sleep
几秒再等待到,就可以看到子进程的状态的变化,从Z
到彻底结束
使用 WNOHANG
选项非阻塞等待
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
sleep(5);
exit(0);
} else {
// 父进程
int status;
pid_t result;
do {
result = waitpid(pid, &status, WNOHANG);
if (result == 0) {
printf("子进程还在运行,继续做其他事情...\n");
sleep(1);
}
} while (result == 0);
if (result == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("子进程正常退出,退出状态码: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
子进程还在运行,继续做其他事情...
子进程还在运行,继续做其他事情...
子进程还在运行,继续做其他事情...
子进程还在运行,继续做其他事情...
子进程还在运行,继续做其他事情...
子进程正常退出,退出状态码: 0
四,进程程序切换(重点)
进程程序切换是利用系统调用的exec*
接口来实现:在不创建新进程的情况下,让一个进程执行一个全新的程序。
(1)替换原理
以execl
为例:
- 假如我们现在
fork()
了一个子进程a
- 在子进程内调用
execl
进行程序切换 execl
并不创建新进程,而是把要执行的新程序的代码和数据,直接覆盖原来子进程上的代码和数据(可以理解为,在这里数据会写时拷贝,代码也会)- 当
execl
切换成功,则原来子进程后面的代码就不再执行了
execl
返回结果:
- 成功时不返回
- 失败时,返回:
-1
示例:
int main()
{
int ret = fork();
if(ret == 0)
{
// child
printf("这是子进程, PID是:%d\n",getpid());
printf(" 程序切换,调用ls\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("如果切换成功,这条语句不会被执行\n");
exit(42);
}
waitpid(ret, NULL, 0);
return 0;
}
执行结果:
下面具体讲解替换函数的使用。
(2)六个替换库函数
execl基本使用
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
path
:传入要调用的可执行程序的路径(绝对路径) + 程序名arg
:命令行调用程序(执行命令)的时候怎么写,这里就怎么写- 注意:
arg
最后要多加一个NULL
,即以NULL
结尾,表示参数完成传递。(因为这个arg
最后会被组织成一个“命令行参数表”) path
作用:我要执行谁?arg
:我要怎么执行
如:调用ls
:
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
其他程序切换函数
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 execve(const char *path, char *const argv[], char *const envp[]);
这些都大同小异,都属于exec*
带p的
先讲p
,如:execlp
有p
代表,路径默认有缺省,这个缺省值是PATH
。
如果我们要执行PATH
里面的路径里的程序,就可以只传程序名
如:
execlp("ls", "ls", "-a", "-l", NULL);
带v的
再讲v
,v
和l
相对:
l
:list
,代表像列表的方式传递可变参数v
:vector
,代表像数组一样传递可变参数v
:要求我们传入一个指针数组(也就是命令行参数表),指针数组里面存的就是命令行参数,要以NULL
结尾、
示例:
char * argv[] = {"ls", "-a", "-l", NULL};
execvp("ls", argv);
带e的
e
代表:envp
环境变量表,有e
则要求我们传入一个环境变量表(这也是个指针数组,注意要以:NULL
结尾)- 我们自行传入的环境变量表会自动覆盖原来从父进程继承的环境变量表
示例:
int main()
{
int ret = fork();
if(ret == 0)
{
// child
char * argv[] = {"printenv", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
printf("这是子进程, PID是:%d\n",getpid());
printf(" 程序切换,调用 printenv 查看环境变量\n");
execve("/usr/bin/printenv", argv, envp);
printf("如果切换成功,这条语句不会被执行\n");
exit(42);
}
waitpid(ret, NULL, 0);
return 0;
}
运行结果:
可以发现确实被覆盖了
putenv添加环境变量
如果我们先要在原来环境变量的基础上新增的话,可以使用:
putenv()
来新增:本质上改的是全局变量environ
指向的环境变量数组
- 成功返回
0
,不成功返回非0
示例:
int main()
{
int ret = fork();
if(ret == 0)
{
// child
char * argv[] = {"printenv", NULL};
if(putenv("NEWVALUE=123456") != 0)
return -1;
printf("这是子进程, PID是:%d\n",getpid());
printf(" 程序切换,调用 printenv 查看环境变量\n");
extern char** environ;
execve("/usr/bin/printenv", argv, environ);
printf("如果切换成功,这条语句不会被执行\n");
exit(42);
}
waitpid(ret, NULL, 0);
return 0;
}
运行结果:
那execv
这种,没有e
的难道就没有传入环境变量吗?
不是的,实际上exec*
这些函数都是库函数,底层封装了同一个系统调用execve
。
当我们不显式传入环境变量表的时候,会自动传(继承)父进程的环境变量表。
(3)切换系统调用
上面的 exec*
系列 都是对 execve
的封装
我们可以看到,这个系统调用,都是需要传入命令行参数表和环境变量表的。即:父进程环境变量会传给子进程(如果没有自行传递覆盖的话)。
- 当我们执行
ls
命令的时候,因为ls
也是用C语言实现的,也有main
- 而我们执行的命令是进程,其实是
bash
开的一个子进程 - 子进程会通过库函数(如果
execl
)调用ls
程序,而这个库函数又是对execve
的封装调用 - 如果调用的库函数是
execl
这种不要求传入e
的库函数。实际上,在库函数封装execve
的时候,会自动将父进程的环境变量传递给子进程(即会传execve
的envp
这个参数)
(4)总结
l(list)
: 参数采用列表v(vector)
: 参数用数组p(path)
: 自动搜索环境变量PATH
e(env)
: 表示自己维护环境变量
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!