前言
进程等待 + 进程程序替换,有了这些就可以自己去模拟一个shell了
一、进程等待
进程状态中提到,一个进程进入僵尸状态,父进程不回收资源,这个僵尸进程无法被杀死,需要父进程主动回收 ----进程等待。
是什么、为什么、怎么写
什么是进程等待?
父进程通过调用系统调用接口wait或者waitpid来对子进程进行状态检测和回收的功能!
为什么要有进程等待?
可以通过进程等待的定义来看,一方面进行状态检测、另一方面进行回收。
1.僵尸进程无法被杀死,需要通过进程等待来杀掉它,回收资源,解决内存泄露问题。
2.我们可以通过进程等待来获取子进程的退出情况,知道我布置给子进程的任务的完成情况。
怎么写进程等待的代码?

wait和waitpid中都带有int* status的参数,这是一个输出型参数,并且这个int类型是被当作几部分来使用的。
wait
先来看看wait的使用,wait等待任意一个子进程退出并且进行回收,返回值就如果大于0是回收的pid,出现错误-1被返回
cpp
int main()
{
pid_t id = fork();
if(id == 0)
{
exit(0);
}
else if(id > 0)
{
sleep(3);
}
pid_t wid = wait(nullptr);
if(wid == id)
{
printf("回收成功!\n");
}
sleep(2);
return 0;
}
我们先不关心status,让创建的子进程直接退出成为僵尸进程,父进程先等待三秒,可以查看僵尸,然后回收,此时只有父进程,2s后,父进程也结束。
监控脚本:while :; do ps ajx | head -1 && ps ajx | grep blog | grep -v vscode | grep -v grep; sleep 1;done

结果也如预期所示。
另外:如果子进程没有退出,那么父进程在wait的时候就不会返回,这被称之为阻塞状态。有没有非阻塞呢?当然有---waitpid中的options,后面再说
如果有多个子进程的情况,wait该回收谁呢??
wait会随机选取一个子进程回收,多个子进程回收多次即可。
status
上面说它是一个输出型参数,由OS来填充,填为nullptr表示不关心子进程的退出状态,否则会将这个值交给父进程,父进程应该关心子进程的什么退出状态呢??进程退出的三种情况,代码运行完毕,结果正确;代码运行完毕,结果不正确;代码异常终止。所以int被分成多部分中一定能看到这三种情况!
int类型4个字节32的比特位,只使用了低16位

如果代码异常终止,收到信号,不关心退出状态。
如果运行完毕,就看退出状态,表示结果是否正确。
拿到对应的信号和退出状态都是位运算的知识,拿到前七位,就是和前七位全1,其他全0取&即可,这个数是0x7F,拿到8-15位,把这个数右移8位然后 & (0xFF)即可。我们可以验证一下,比如手动调用函数使得进程收到信号,再比如不让他收到信号,让他打开一个不存在的文件,看看退出码和描述。
cpp
int main()
{
pid_t id = fork();
if(id == 0)
{
//kill(getpid(),9);
int fd = open("?.txt",O_RDONLY);
cout << errno << endl;
if(fd < 0)
{
exit(errno);
}
exit(0);
}
else if(id > 0)
{
sleep(3);
}
int st;
pid_t wid = wait(&st);
if(wid == id)
{
printf("回收成功!\n");
int sig = (st & (0x7F));
int exitcode = (st >> 8 & (0xFF));
if(sig == 0){
cout << "没收到信号!退出码是" << exitcode << ",描述:"<<
strerror(exitcode);
}
else {
cout << "代码异常终止!,信号是";
cout << sig << endl;
}
}
return 0;
}
使用kill(getpid(),9)时:

打开一个不存在的文件时:

当然这样获取信号和退出状态有点麻烦,系统提供了几个宏来进行更简单的获取:
WIFEXITED(status),返回值是bool类型,若为正常终止,为真,异常终止为假
WEXITSTATUS(status)若非0,则提取退出码。
WIFSIGNALED(status) 判断子进程是否被信号终止,返回非0表示是
WTERMSIG(status) 当WIFSIGNALED为真时,提取终止子进程的信号编号
就可以这么改了:
cpp
pid_t wid = wait(&st);
if(wid == id)
{
printf("回收成功!\n");
int sig = WTERMSIG(st);
int exitcode = WEXITSTATUS(st);
if(WIFEXITED(st)){
cout << "没收到信号!退出码是" << exitcode << ",描述:"<<
strerror(exitcode);
}
else {
cout << "代码异常终止!,信号是";
cout << sig << endl;
}
}
这里讲的status都对waitpid中的status适用
waitpid
wait是waitpid功能的一个子集。
pid_t waitpid(pid_t pid,int *status,int options)
pid : pid = -1表示等待任意一个子进程,pid > 0.等待指定子进程的pid
options :0表示阻塞等待方式,如果子进程没有退出就一直在等.
非阻塞轮询:WNOHANG这是一个宏,代表着不要阻塞!非阻塞 + 循环->非阻塞轮询 + 做自己的事情
非阻塞轮询情况下怎么判断条件就没就绪呢?根据返回值

翻译出来的意思就是:大于0表示回收子进程的pid,如果选项被设置成WHOHANG并且返回值等于0,代表条件没有就绪,小于0就是出错。
非阻塞写法:因为是非阻塞轮询,所以一定要打循环
cpp
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(3);
exit(0);
}
int st;
while (1)
{
pid_t wid = waitpid(0, &st, WNOHANG);
if (wid == id)
{
printf("回收成功!\n");
int sig = WTERMSIG(st);
int exitcode = WEXITSTATUS(st);
if (WIFEXITED(st))
{
cout << "没收到信号!退出码是" << exitcode << ",描述:" << strerror(exitcode);
}
else
{
cout << "代码异常终止!,信号是";
cout << sig << endl;
}
break;
}
else if(wid == 0)
{
sleep(1);
cout << "条件没有就绪!\n";
cout << "可以执行自己的任务了!\n";
//这里可以执行自己的任务了!
}
}
return 0;
}

原理
父进程调用wait时,内核会做两件事:
检查父进程的子进程中是否有僵尸进程;若有,将该子进程 PCB 中的退出信息(exit_code和exit_signal) 通过位运算拷贝到父进程的status参数中;将僵尸进程的状态从Z改成X。
如果没有,则阻塞等待。
这里不能用全局变量,因为进程具有独立性。
二、进程程序替换
什么是进程程序替换
fork之后,我们可以让子进程和父进程执行不同的分支,但是我如果想让子进程执行一份新的程序呢?---进程程序替换
进程程序替换是通过exec一系列接口,让一个正在运行的进程,完全替换自身的代码段、数据段、堆、栈等资源,转而执行另一个程序。
程序替换的时候也没有创建子进程,只是进行了进程的程序代码和数据的替换工作。
程序替换中环境变量信息不会被替换,创建子进程的时候环境变量就已经继承给子进程了,extern char** environ;所以,程序替换中环境变量不会被替换。
现象:如果成功进行程序替换,exec系列后续代码不会执行,只有失败了才会执行后续代码,所以exec系列函数只有失败返回值,失败返回-1,没有成功返回值。
exec系列

虽然有一系列函数,但是每个字符都是有意义的
l(list) : 表示参数采用链表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
就拿最基础的execl来说,要执行一个新程序,首先一定得找到这个程序,第一个参数就是什么路径下的什么程序!找到之后决定如何执行这个程序---带不带选项,带哪些选项。
具体怎么写先看写法。
cpp
execl("/usr/bin/ls","ls","-l","-a",nullptr);
不加p表示需要自己提供路径,我们提过,这些指令不需要路径也能运行的原因就是有环境变量中的PATH!如果函数有p,那我们就不需要提供路径了!
后面的写法和shell中的写法一样,只不过以nullptr结尾!
char* const argv[] 指的是指针本身不能修改,const在前面指的是指向的内容不能修改。
演示一下其他函数的写法:
cpp
execl("/usr/bin/ls", "ls", "-l", "-a", nullptr);
execlp("ls", "ls", "-l", "-a", nullptr);
execle("/usr/bin/ls", "ls", "-a", "-l", nullptr, environ);
char *const argv[] = {
const_cast<char *>("ls"),
const_cast<char *>("-a"),
const_cast<char *>("-l"),
NULL};
execv("/usr/bin/ls", argv);
execvp("ls", argv);
execvpe("ls", argv, environ);
argv里面不加const_cast不一定可以编译通过,ls是const char*类型,赋值给非const数组。
那我们的exec能不能执行自己的命令呢?当然可以,和上面的道理是一模一样的!比如执行一个hello world的可执行
cpp
execl("./other","other",nullptr);
我如果想给子进程传递环境变量,该怎么传递?
1.新增环境变量
putenv添加一个环境变量,添加在调用进程的环境变量中
如:putenv("VALUE=666");
execle是自己传,可以传默认的
2.彻底替换 ----这里是彻底替换不是追加
使用自己传的myenv
char* const myenv[] = {
"VALUE=666",
...
};
execle("./main","main","-a","-b","-c",nullptr,myenv);
最后我们发现这都是三号手册的,也合理,因为之前说过这些接口一定封装了系统调用接口----execve

你上面调用的所有函数都会转化为这个函数!