✨前言✨
📘 博客主页:to Keep博客主页
🙆欢迎关注,👍点赞,📝留言评论
⏳首发时间:2024年10月11日
📨 博主码云地址:渣渣C
📕参考书籍:C语言程序与设计 和 数据结构(C语言版)
📢编程练习:牛客网+力扣网
进程控制
1 进程的创建
~~~~ 在Linux的进程学习中,我们就已经介绍了,利用fork函数进行子进程的创建!在子进程的创建的过程中,子进程会创建自己的PCB(里面部分数据内容会拷贝父进程的内容)以及拷贝父进程的程序地址空间,页表!如果发生了父进程(子进程)发生了写时拷贝,那么父进程(子进程)就会复制父进程的数据到新的一块物理内存空间地址上,然后进行更改数据,最后更新页表的内容!在这里还是需要特别注意的是:
1️⃣fork函数返回给父进程是子进程的pid
2️⃣返回给子进程是0
3️⃣fork函数是可能出现创建进程失败的情况的,如果当前系统的进程过多或者是用户的进程数超过了限制!
2 进程的终止
~~~~
在聊进程终止之前,我们先来了解一下在我们写main函数的时候,通常是以return 0结束的,这表示程序运行到这里就结束了!从进程的角度来看,0表示的就是该进程的退出码
!我们可以通过退出码就可以知道进程的执行情况,也就有助于我们发现程序中可能存在的bug,我们可以通过如下命令来查看一个进程的退出码!
echo $?(查看最近一次进程的退出码)
如下图所示,我写了一个除0错误的代码!然后运行起来,利用上述命令查看进程的退出码!
我们还可以通过C语言中的strerror库函数,来将我们程序执行结果转化为字符描述!代码如下所示:
#include <stdio.h>
#include <string.h>
int main()
{
for(int i=0;i<255;i++)
{
printf("[%d]:%s\n",i,strerror(i));
}
return 0;
}
运行结果如下所示:
我们可以发现,strerror一共打印了134种类型的错误!但是我们上面查询退出码是136呀,这是怎么一回事呢?其实,我们利用strerror查询到的是进程从启动到退出之后返回给我们的一个退出码(就相当于我们程序从头到尾执行完毕之后,返回给我们的一个结果,只不过这个结果使用数字表示,然后利用strerror转化为字符串来表述)!而上述我写的一个除0错误是我们进程启动起来,操作系统会认为这是一个异常,进程就会收到了操作系统发出的退出信号,从而打断了程序的执行,让进程退出,从而返回异常信号的退出码!简单总结一下就是说我们的进程执行情况由进程的退出码以及异常信号的退出码来决定的
,为了表示两者的区别,我们将前者称为退出码,后者就称为信号码!
既然我们知道了退出码是表示进程的执行情况的(其实类似与我们也可以从函数的返回值知道函数的执行情况),我们可以自定义实现一个类似于strerror的函数!代码如下所示:
#include <stdio.h>
#include <string.h>
enum err{
success=0,
malloc_err,
other_err
};
const char* errDesc(int code)
{
switch(code){
case success:
return "sccessful complete";
case malloc_err:
return "malloc error";
case other_err:
return "other_err";
default:
return "unknow error";
}
}
int main()
{
int code = malloc_err;
printf("%s\n",errDesc(code));
return 0;
}
原理就是获取到我们的进程的退出码,然后将退出码转化为字符描述!了解完进程终止之后的退出码!我们在回归到进程终止这个话题上,对于进程终止简单来说就是我们通常使用exit函数来终止进程!而exit带的参数就是退出码!而我们的return等同于exit函数的执行,因为main函数执行完毕,就会将参数传递给exit函数!exit函数底层原理就是调用了我们的系统接口函数_exit函数!两者之间的区别就是exit会刷新我们的缓冲区,而_exit是不会的!这也可以间接的说明我们的缓冲区并不是在操作系统中,例子如下所示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
printf("hello cpp");
exit(1);
}
如果我们将exit前面加上下划线变成_exit运行程序,那么缓冲区是刷新不出来的,hello cpp是打印不到显示器上的!
3 进程的等待
在进程的状态中我们了解到,在进程中有一种状态叫做僵尸状态,利用我们的kill 9命令是杀不死的!产生僵尸状态的原因就是我们的父进程没有拿到子进程的退出信息,而导致子进程PCB结构一直存在内存当中,从而会引发内存泄露的问题!进程等待就是为了解决这一问题,让父进程可以拿到子进程的退出信息!父进程也能通过进程等待的方式,回收子进程资源,获取子进程退出信息,知道我们子进程的运行情况如何!我们一般用以下两个系统调用函数来进行等待!
3.1 wait函数
如下图所示:
1️⃣返回值是一个pid_t类型,也就是一个整数,如果等待成功就会返回子进程的pid,等待失败返回-1
2️⃣参数是一个指向整数的指针,可以从中获取到子进程的退出状态,如不关注子进程的终止情况,可以直接设置为NULL
另外wait方法默认就是阻塞等待!使用演示代码如下所示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id==0)
{
printf("i am child process\n");
exit(0);
}
printf("I am father process\n");
pid_t id1=wait(NULL);
if(id1>0)
{
printf("wait successful\n");
}
}
在这里还需要特别说明一点的是,fork创建子进程之后,父子进程谁先被调度,这是由调度器自己决定的!但是有一点必须要保证,父进程必须要最后退出,防止出现僵尸进程!wait函数我们简单了解一下,下面我们重点来谈论一下waitpid函数!
3.2 waitpid函数
如下图所示:
1️⃣返回值:和wait一样,等待成功返回子进程的pid,失败返回-1,如果设置了选项参数为WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
2️⃣pid=-1表示等待任意一个子进程(等同于wait的效果),如果pid>0就是等待与pid相等的子进程
3️⃣输出型参数,就是表示子进程的退出状态的
4️⃣选项参数,选项为0,则表示阻塞等待,如果选项为WNOHANG表示非阻塞等待!也就是说若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
在这里特别解释一下什么是阻塞等待,阻塞等待就是父进程必须一直等,期间不可以做其他的事情!但是非阻塞等待就不一样了,父进程就可以处理其他事情,我们一般利用非阻塞等待返回值为0进行循环检测,要是为0,父进程就去做其他的事情,要是不为0,说明等待到了我们的子进程,就可以退出循环了!如下所示:
#include <stdio.h>
int main()
{
pid_t id = fork();
if(id==0)
{
//子进程
sleep(10);
exit(0);
}
int status = 0;
pid_t rid = waitpid(id,&status,WNOHANG);
while(1)
{
if(rid==0)
{
//父进程做其他事情
}else{
break;
}
}
return 0;
}
3.3 输出型参数
看完以上wait与waitpid函数可能你还是不明白什么是输出型参数!下面我们来解释一下,在进程终止中我们就说过,判断进程的执行情况,是由程序执行退出码和异常信号退出码这两个数字来判断的!那么我们就将这两个数字保存在一个整数中!怎么做的呢,其实很简单,我们用二进制来表示这两个数字!一个整形是有32个字节,前面高的16字节我们不存放数据,后面16个字节存放数据情况如下图所示:
那么我们如何用代码的方式来获取呢?我们就需要进行位操作了!
获取异常信号的退出码:status&0x7F
获取退出码:(status>>8)&0xFF
那么在实际的应用当中,我们是否会采取位运算来查看呢?其实是利用以下两个C库函数来进行判断提取退出码的
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 提取子进程退出码。(查看进程的退出码)
4 进程的替换
我们在创建子进程的时候,如果我们想让子进程要去执行其他程序的代码,我们就需要将子进程的代码与数据进行替换,然后重新加载到内存中,页表以及进程地址空间也会根据需要做出相应的变化!在C库函数中,提供了以下几个方法来进行程序的替换!
我们就介绍前面三个函数的用法,其他的用法也是类似的!这六个程序替换失败返回值是-1,成功就没有返回值的!
4.1 execl函数
1️⃣path参数,路径+要执行的文件(绝对路径与相对路径都可以)。例如我们的ls命令就是在/usr/bin/ls这个路径下!我们可以使用which命令查看
2️⃣arg参数就是保存我们命令的字符串的指针,必须以NULL结尾
3️⃣…参数表示是一个可变参数列表,表示arg参数可以有多个,是可变的
下面我们就用shell内置的命令ls(本质也是一个程序)来进行程序替换,代码如下所示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id==0)
{
printf("I am child process\n");
int c = execl("/usr/bin/ls","ls","-al",NULL);
printf("%d\n",c);
printf("this is testing\n");
}
printf("I am Waiting\n");
return 0;
}
执行结果如下所示:
我们可以发现,子进程替换程序之后,后面的代码就不会在执行了!这是因为替换程序成功之后,会将当前进程的所有代码和数据进行替换,包括已经执行的和没执行的!
4.2 execvp函数
在这个函数中,第一个参数是可以直接给出可执行程序的名称(当然也可以给出程序的相对路径与绝对路径),它会默认去从环境变量PATH位置下去找看有没有这个可执行程序,第二个参数就不像我们上面execl函数一样,这里不在是以可变参数的形式给出了,而是以一个数组的形式给了出来!但是在数组最后一个位置也必须设置为NULL!使用方法如下所示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
char* const argv[]={"ls","-al",NULL};
pid_t id = fork();
if(id==0)
{
printf("I am child process\n");
execv("ls",argv);
printf("this is testing\n");
}
printf("I am Waiting\n");
return 0;
}
4.3 execle函数
经过上述两个函数的认识,我们这里就主要介绍以下最后一个参数,最后一个参数就是我们的环境变量参数!有什么作用呢?作用如下所示:
1️⃣继承父进程的环境变量(传递一个environ指针),因为我们有一个全局变量environ指针,维护了一张环境变量表!我们在进程地址空间也提及过,父进程的环境变量也会复制一份给我们的子进程的!也就是说我们如果没有这第三个参数,子进程也拿得到父进程的环境变量!
2️⃣可以利用这个参数,给子进程传递全新的环境变量
当然在环境变量中,我们还学习到了,利用putenv就可以给某个进程单独的增加或者修改某个环境变量!!!这里具体的代码示例也和前面的是相似的,这里就不写了!我们最后来论证一个,程序替换之后为什么没有创建一个新的进程!代码如下所示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id==0)
{
printf("I am child process\n");
int cnt = 5;
while(cnt){
sleep(2);
printf("change before pid:%d\n",getpid());
cnt--;
}
execl("/usr/bin/top","top","-b",NULL);
printf("this is testing\n");
}
printf("I am Waiting\n");
return 0;
}
我们将程序运行起来之后,在查询进程情况,如下所示,我们发现pid相同的进程,执行的程序不一样了!
本小节我们主要学习了进程的终止,进程的等待,进程的替换三个重要内容!