这部分我们讲子进程和父进程的各种联系, 学习进程等待,学习进程替换,学习进程终止和?
环境变量
我们可以使用fork();函数进行创建我们的子进程。
还是重新简单介绍一下fork();
对父进程返回子进程pid
对子进程返回0
错误时返回-1
我们在进程地址空间(跑路人笔记)中知道了我们fork();创建进程的过程。
分配新的内存块和内核数据结构给子进程。
将父进程的数据结构拷贝给子进程
添加子进程到系统进程中
fork返回开始调度
我们在进程地址空间(跑路人笔记)中详细的解释了发生过程。
其实就是我们的父进程和子进程在数据和代码是共享的,当一个进程需要对一部分进行修改的时候我们就会发生写时拷贝,及将被修改的部分重新开辟一段空间将它的值进行保存由此来保证进程的独立性。
我用图片来解释一下
为什么我们的子进程是从父进程fork位置开始进行呢?
fork生成的子进程其实是共享所有的代码,但是子进程只能从fork位置的出发。
CPU里有程序计数器eip,他会保存正在实行指令的下一条指令。
eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处开始执行。
抱歉这个我不知道如何演示。大家看字面意思理解吧。
每个进程都是要终止的。
正常终止:
- 从main函数结束(return x;
- 调用exit(x)函数
- 调用_exit(不常见
从main函数中return 的值和exit(x)这里的值都被我们的操作系统接收(如果父进程是bash的话)
为什么我们都喜欢在main函数的代码最底的时候reurn 0;?
这只是一个默契 我们规定0为正确的代码运行结束而非0为错误的运行结果。
exit
和_exit
有什么区别呢?很简单_exit
不会刷新缓冲区。
我们来演示一下:
注:我们printf不使用\n是不会刷新缓冲区的.这些在缓冲区的数据先保存在我们的缓冲区当我们程序退出时才会被刷新出来.而_exit不会刷新缓冲区.看演示:
int main()
{
printf("12345");
exit(0);//代码唯一改变就是这里是_exit还是exit.
return 0;
}
我们先使用exit();
我们需要的12345被打印了出来
我们再使用一下_exit;
并没有被打印.
在Linux中我们有一个环境变量用于保存最近一次进程的返回值?
我们可以使用echo $? 来查看它的值(当然其他可以查看环境变量的方法都可以)
int main()
{
return 3;
}
异常退出
- ctrl+c,信号终止
首先我们要知道我们
进程 = 内核数据结构(task_struct+mm_struct……) + 进程代码+ 数据
我们创建进程的时候创建的内核数据结构和代码等数据我们在终止的时候会将他们释放掉。
但是我们Linux会在终止的时候进行优化可能将内核数据结构不删除不释放,而保存下来用于下一个进程创建.(及省去下一个进程创建内核数据结构的时间).
优化结构我们用一个链表来将我们废弃的内核数据结构保存下来.这些数据结构一般使用分派器(slab分派器)进行保存(现在不讲比较难)
我们的父进程是通过进程等待的方式来得到我们的子进程的退出信息的。
这个退出信息非常重要。我们可以从退出信息得到子进程是否是正常退出。以及退出码等。
而且
我们使用wait函数来解决子进程Z状态(僵尸状态)。让子进程进入X状态(死亡状态)-----这里的状态我们之前讲过
pid_t wait(int *status);
wait会让我们的父进程等待我们子进程完毕.
当然我们不显示调用wait父进程也会等待,我们调用了之后就可以控制进程等待的位置了.
如果我们的父进程wait我们的子进程,子进程在结束后就会进入僵尸状态(Z状态),当等待完成后,子进程就会结束死亡.
我们用下面代码进行测试
#include
#include
#include
#include
int main()
{
int id = fork();//创建子进程
if(id == 0)
{
while(1)
{
printf("我是子进程我正在运行 PID:%d\n",getpid());
sleep(1);
}
}
else if(id>0)
{
printf("我是父进程我将在40秒后结束子进程我的pid: %d\n",getpid());
sleep(40);//保证我们将子进程杀死后父进程还没有等待他.
int status;
int pid = wait(&status);//status值在后面waitpid部分讲.
sleep(20);//等待完成后继续睡眠20s方便观看
printf("等待完成子进程的pid为%d status值为%d\n",pid,status);
}
return 0;
}
第一阶段,未杀死子进程.
使用kill杀死子进程.
第二阶段:子进程因为没有被等待是Z状态.
第三阶段:子进程被等待成功,父进程睡眠之后死亡.
所以我们说wait是用来解决子进程的僵尸问题的也不为过.
首先说明啊:wait和waitpid都是系统调用接口.
头文件和wait相同但是,waitpid相比于wait更加自由.
下面我们来介绍一下他的每个接口.
- pid: >0是多少就等待那个子进程(一个父进程有多个子进程.)
- -1:等待任意进程
- status: 这个参数是一个输出行参数.通过调用函数从函数内部拿出特定的数据(之前做题经常有遇到,尤其是C语言的题目.)
- 从哪里拿出来呢? 答:从子进程的task_struct中拿出子进程的退出码.
- options: 0:表示阻塞等待.宏(WNOHANG)表述非阻塞等待------后面会讲👍
返回值:等待成功返回值就是子进程的pid, 等待失败返回-1;
传-1值表示waitpid等待任意一个子进程,至于这个任意是哪一个,我们后面讲.
我们来讲status.
这个参数既然是输出性参数,那他的数据是什么呢?
我们使用下面代码来向大家演示.
#include
#include
#include
#include
int main()
{
int id = fork();
if(id == 0)
{
printf("我是子进程我在5秒后结束我的pid:%d\n",getpid());
exit(33);
}
else if(id>0)
{
int status;
int pid = wait(&status);
printf("等待完成子进程的pid为%d status值为%d\n",pid,status);
}
return 0;
}
//输出结果为:等待完成子进程的pid为7946 status值为8448.
就很奇怪.我们的status值却是好像对我们无意义的值.
其实我们的status值是通过位图的方式给我们信息的.
这里的位图我们只需要关心低16比特位即可(int有32个比特位).
分为两种情况
_exit
终止.
(status>>8)&0xff
就可得到.我们在代码中将其打印出来.kill -9
来完成的终止信号就是9来试试其他的数字.kill -l
来查看每个数字的意思.当然我们直接使用位操作有些麻烦,我们C语言有维护的宏来帮助我们使用status
有选项让我们,分为阻塞等待和非阻塞等待.0
表示阻塞等待
阻塞等待很简单,在我们的子进程结束之前,也就是说在我们的父进程不成功等待到我们的子进程之前父进程是不会进行任务的.
证明如下:
可以看出直到我们kill了子进程我们的父进程的后续命令(printf)才继续进行.
//代码如下
#include
#include
#include
#include
int main()
{
int id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程我死循环执行,我的pid:%d\n",getpid());
sleep(1);
}
//exit(33);
}
else if(id>0)
{
//printf("我是父进程我将在40秒后结束子进程我的pid: %d\n",getpid());
//sleep(40);
int status;
int pid = waitpid(id,&status,0);
//sleep(20);
printf("等待完成子进程的pid为%d status值为%d 子进程退出码为:%d 子进程终止信号为:%d\n",pid,status,(status>>8)&0xff,status&0x7f);
}
return 0;
}
非阻塞等待,这种等待我们需要通过循环进行反复等待,就是父进程循环的干一些活,干完之后再循环回去看看是否可以直接等待完成(及子进程完毕),不可以就继续干他的活.可以就结束.我们来看看.
看看我们的代码
#include
#include
#include
#include
int main()
{
int id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程我死循环执行,我的pid:%d\n",getpid());
sleep(1);
}
//exit(33);
}
else if(id>0)
{
int status = 0;
int pid = 0;
while(!pid)//子进程还未死亡的时候waitpid返回值为0,成功了就是返回子进程pid
{
pid = waitpid(id,&status,WNOHANG);
printf("干一些活哈\n");
sleep(1);
}
printf("等待完成子进程的pid为%d status值为%d 子进程退出码为:%d 子进程终止信号为:%d\n",pid,status,(status>>8)&0xff,status&0x7f);
}
return 0;
}
至于让父进程干什么活就很自由了.
比如可以用数组里面装几个函数指针让后依次调用函数来让父进程资源也不闲着.这很资本家👍
如何理解我们的父进程阻塞.
类似于其他进程阻塞,我们父进程的进程阻塞等待其实就是将我们的父进程投入到我们的等待队列中,当我们的子进程完成了之后就是条件就绪就可以继续将我们的父进程放到运行队列运行了.