轻松Linux-5.进程控制
不知不觉也是到了进程控制这一篇了,进程这个东西在Linux中也是相当重要,不过bro也是会奋力写清这里的每一个点,咱们肘走
1. 进程创建
1.1 fork()函数
我们在上一章知道fork()函数是用于创建子进程的,它在Linux中也是一个是十分重要的函数,fork()之后会有两个进程,分别是子进程和父进程
#include<unistd.h>
pid_t fork(void);
fork有两个返回值,父进程拿到的返回值是子进程的PID,子进程拿到的则是0,创建失败的返回值为-1
进程调用fork()之后,内核就会执行以下操作:
1:分配新的内存块和内核数据结构给子进程(粗略的理解为复制了一份父进程的task_struct)
2:将父进程中的部分数据结构内容拷贝给子进程(↑也就是)
3:添加子进程到系统的进程调度队列中
4:fork()返回,(调度器)开始调度进程
...
实验demo在上一章已经有了,大火可以去...看看
在fork()之后,父子两个进程会分别执行,并且执行顺序由进程调度器决定
1.2 写时拷贝
在我们fork()之后,父子进程的代码块是共享的(可以先理解为父子进程task_struct中指向代码块的指针指向同一代码块,即子进程拷贝父进程的task_struct,子进程和父进程几乎是一样的),父进程和子进程对这些代码是只读状态,但是代码本身还是父进程的,一旦子进程修改其中的某个变量,如果没有写时拷贝就有可能影响到父进程代码的执行,所以子进程在修改某个变量时,OS会会重新给子进程开辟内存空间,来存储新的变量
有了写时拷贝的存在,父进程和子进程才彻底的分为两个独立的进程
写时拷贝是一种延时申请技术,可以大大提高整机内存使用率
1.3
2. 进程终止
进程终止,就是进程退出,本质是释放系统资源,释放的是在系统中申请的数据结构以及其指向的数据块
进程常见的退出场景:
1:代码执行完毕,正常退出。
2:代码执行出现异常,终止
常见的退出方法:
正常退出(可以通过echo $?来查看退出码):
1:main函数返回
2:调用exit函数(C库函数,最终调用_exit函数)
3:调用_exit函数(系统调用)
异常退出:
信号退出(例如Ctrl + C,它会对进程发送2号信号来杀死进程)
2.1 退出码
退出码,可以告诉我们进程最后一次执行命令的状态。在命令结束之后,其返回的退出码可以告诉我们命令是正常执行结束还是错误异常退出的。一般程序返回退出码0时代表执行成功,程序返回1或者0以外的其它退出码,则表示异常退出。
退出码 | 意义 |
0 | 命令执行成功 |
1 | 通用错误代码 |
2 | 命令(或参数)使用不当 |
126 | 权限被拒绝或无法执行 |
127 | 未找到命令,或PATH错误 |
128+n | 命令被信号从外部终止,或遇到致命错误 |
130 | 通过Ctrl+C 或SIGINT终止(终止代码为2或键盘终端) |
143 | 通过SIGTERM终止(默认终止) |
255/* | 退出码超过了0~255的范围,会进行重新运算 |
比较理想的情况是退出码为0,命令执行无误
退出码1,可以理解为“不被允许的操作”,例如除0操作(i/=0)
退出码130(SIGINT或Ctrl + C)和 143(SIGTERM)等是典型的终止信号,他们属于128+n信号,其中n对应终止码
我们可以使用strerror函数来获取退出码对应的描述
2.1.1 _exit函数和exit函数
_exit()函数:
#include<unistd.h>
void _exit(int status);
参数status表示进程的终止状态,父进程可以通过wait来获得它
注status虽然为int类型,但当传参为-1时,使用echo $?时,它显示的是255(转变为unsigned)
exit()函数:
#include<unistd.h>
void exit(int status);
我们知道它会调_exit()函数,但在这之前exit还做了其他事情:
1. 执行用户atexit和no_exit定义的清理函数
2. 关闭所有执行流,所有缓存数据被写入(清理缓存数据)
3. 最后调用_exit()
...
...
2.1.2 return返回
return返回是我们最常见的返回方式,在main函数中return n就等同于exit(n),因为调用main函数的程序会将main函数中return的值,当做参数传给exit()函数
3. 进程等待
如果退出的进程得不到等待,那么这个就会变为僵尸进程,连kill -9也无法杀死这个进程,因为系统无法杀死一个已经死了的进程,进而造成内存泄露
并且父进程需要知道子进程的执行情况,那么父进程就需要知道子进程的退出码,来确认子进程的任务是正常退出,还是异常退出
父进程好比是古代的皇帝,皇帝下达的政策、命令需要知道执行的情况,以此来判断自己的政策是否达到预期的效果
所以进程等待9分有10分必要
3.1 进程等待的方法
3.1.1 wait()函数和waitpid()函数
wait()函数:
返回值:成功返回被等待进程的pid,失败返回-1
参数:输出型参数(穿函数的地址,可在函数内通过指针直接修改变量的值),获取子进程的退出状态,不关心其退出状态可以穿NULL
#include<sys/types.h>
#include<sys/wait.h>pid_t wait(int *status);
waitpid()函数:
返回值:成功返回被等待进程的pid,失败返回-1
参数:
1. 第一个参数是子进程pid,填pid就是指定等待子进程,填-1就是等待所有子进程
2. 第二个参数是子进程的退出状态(输出型参数跟exit() 一样),返回的值需要用宏来解析:
WIFEXITED(
status)
:子进程正常退出(如调用 exit()
)
WEXITSTATUS(
status)
:获取子进程的退出码(需先通过 WIFEXITED判断)
WIFSIGNALED(status):子进程因信号终止
WTERMSIG(status):获取终止子进程的信号的编号
WIFSTOPPED(status):子进程因信号暂停(如SIGSTOP)
WSTOPSIG(status):获取暂停子进程的信号的编号
3. 第三个参数是等待的模式:
WNOHANG:非阻塞模式,没有子进程退出的时候就立即返回0
WUNTRACED:如果子进程被暂停了就立即返回,对子进程的退出状态不关心,WIFSTOPPED (status)宏确定返回值是否对应同一个暂停子进程(我来解释一下:WUNTRACED选项与 WIFSTOPPED宏的组合用于监控子进程的暂停状态,例如:
waitpid(pid, &status, WUNTRACED);
if (WIFSTOPPED(status)) {printf("子进程因信号暂停,信号编号:%d\n", WSTOPSIG(status));
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);} else if (pid == 0) { // 子进程printf("子进程运行中,PID: %d\n", getpid());sleep(10); // 模拟长时间运行exit(0);} else { // 父进程int status;while (1) {pid_t result = waitpid(pid, &status, WUNTRACED | WNOHANG);if (result == -1) {perror("waitpid");exit(1);} else if (result == 0) {printf("子进程未暂停或终止,继续轮询...\n");sleep(1);} else {if (WIFSTOPPED(status)) {printf("子进程 %d 被信号 %d 暂停\n", pid, WSTOPSIG(status));// 可在此发送 SIGCONT 恢复子进程kill(pid, SIGCONT);} else if (WIFEXITED(status)) {printf("子进程 %d 正常退出,退出码:%d\n", pid, WEXITSTATUS(status));break;}}}}return 0;
}
可以检测子进程的状态,代码看看就ok,不懂的后面都会讲 )
pid_t waitpid(pid_t pid, int *status, int options);
1.如果子进程已经退出,再去调用wait以及waitpid,函数会立即返回,并释放资源,获得子进程的退出信息
2.若子进程没有退出,父进程无论在什么时候调wait以及waitpid函数,都可能会阻塞
3.若没有子进程,则出错返回
3.1.2阻塞式等待以及非阻塞式等待
这里说的阻塞等待以及非阻塞等待,都是通过修改waitpid的第三个参数来实现的,如果waitpid的第三个参数是0,则为阻塞式等待,如果要非阻塞等待可以设置为WNOHANG
来看小段代码
阻塞式等待:(wait函数默认是阻塞式等待)
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <errno.h>int main()
{pid_t pid = fork();if (pid == -1)perror("fork"), exit(1);if (pid == 0){// 子进程sleep(6);exit(0);}else{// 父进程int status = 0;pid_t ret = waitpid(-1, &status, 0); // 阻塞式等待,等待5Sprintf("this is test for wait\n");if (WIFEXITED(status) && ret == pid){printf("等子进程6s,子进程退出码:%d.\n",WEXITSTATUS(status));}else{printf("等子进程失败, return.\n");return 1;}}return 0;
}
非阻塞式等待:
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <errno.h>int main()
{pid_t pid = fork();if (pid == -1)perror("fork"), exit(1);if (pid == 0){// 子进程printf("我是子进程,我的pid: %d\n", getpid());sleep(6);exit(0);}else{// 父进程int status = 0;pid_t ret = 0;do{sleep(2);ret = waitpid(-1, &status, WNOHANG); // 非阻塞式等待,等待6Sif (ret == 0){printf("子进程正在运行\n");continue;}} while (ret == 0);if (WIFEXITED(status) && ret == pid){printf("等子进程6s,子进程退出码:%d.\n", WEXITSTATUS(status));}else{printf("等子进程失败, return.\n");return 1;}}return 0;
}
3.1.3子进程返回status
相信大家在上面的代码都看到了,wait和waitpid函数都有一个输出型参数---status,它记录了子进程的退出状态,在子进程退出时,系统会填上这个参数,如果传的时NULL,系统会根据相应的参数,将子进程的退出信息反馈给父进程
status是通过bit位来存子进程的相关信息的,我们来看它的低16位,status的低8位记录的时进程的退出码如图
。。。
在进程正常退出时,低8位全为0,它的高8位显示的是通过调用exit()和_exit()函数退出的退出码,如调用exit(8)函数,那么高八位表示的数就是8
在异常退出的情况下,第八位是core dump标志,低7位记录的信号编号,当生成了核心转储文件是第8位就会被置1
4. 进程替换
原理:在我们通过fork()创建子进程时,子进程会开辟自己的进程地址空间,子进程默认是执行父进程的代码和数据,如果要让子进程运行别的程序,我们可以让子进程调用exec系列的函数,该函数(不会创建新进程,调用前后子进程的pid是不变的)通过将磁盘中要运行的程序的代码和数据,加载到子进程的进程地址空间中,替换掉子进程原来的代码和数据,这样子进程就可以执行新的程序了(exec系列函数没有返回值,只要有就是替换失败了)
exec系列函数
头文件
#include <unistd.h>
exec系列函数
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 execve(const char *path, char *const argv[], char *const envp[]);
exec系列函数命名也是有迹可循的(字母均为小写),规律如下:
l : 表示参数采用列表
p:表示会自动搜寻环境变量PATH
v:表示参数采用数组
e:表示需要我们自己维护环境变量
在这些函数中只有execve()这一个系统调用,其他的函数都是直接或间接的去调用execve()这个系统调用,他们的关系大致是这样的: