👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】
一,进程创建
在Linux中我们通过fork来创建一个新的进程。新创建的进程叫做子进程,原来的进程叫做父进程。
fork()给父进程返回子进程的id,给子进程返回0 ,出错时返回-1 。
为什么会有两个返回值的问题我们前面的文章已经提到过了。
如下:
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret=fork();
if(ret<0)
{
perror("错误");
}
else if(ret>0)
{
printf("父进程:%d\n",getpid());
sleep(2);
}
else if(ret==0)
{
printf("父进程:%d,子进程:%d\n",getppid(),getpid());
}
return 0;
}
为什么在父进程那里要休眠两秒呢?
是因为若不休眠,则我们的父进程就可能先执行完退出了,那么子进程就成了孤儿进程,它的父进程就成了bash。
有了上述对fork的简单认识,我们接下来回答一个问题:fork创建子进程,操作系统都做了什么?
fork创建子进程后系统中就多了一个进程。进程具体是什么?----->内核结构+进程代码和数据。代码和数据一般是从磁盘中来的,内核结构就是我们一直说的所谓的pcb,为了描述和控制进程模块,系统为每一个进程都定义了一个数据结构—也就是我们的task_struct。它是进程实体的一部分,也是进程存在的唯一标志。
Linux下用于创建进程的API有三个:fork ,vfork,clone。这三个函数分别是通过系统调用sys_fork,sys_vfork以及sys_clone实现的。但是其最终都会且只会调用do_fork。
创建进程,当然要先创建它自己的进程控制模块,因为它是进程存在的唯一标志!!!
所以下面我们讲讲子进程创建进程控制模块的过程。
先是为该进程创建一个内核栈然后通过拷贝父进程的task_struct,为子进程创建一个task_struct。此时的父进程和子进程是没有区别的,接着,会根据自己的情况修改task_struct中的一些参数。这也是进程独立性的一方面体现。
讲完了内核结构,我们再将进程的数据与代码。 进程的数据与代码采用读时共享,写时拷贝的设计方法。
我们在创建内核结构后,进程也就有了自己的地址空间。但此时父子映射的物理内存还都是同一块,为了提高内存的使用效率,当我们只读某些数据时,便不需要进行拷贝,父子共享就可以;只有当遇到要写的数据时,我们再将这个数据进行拷贝,使得在物理内存中,其相互不干扰,这是进程独立性的另一种体现。
如下图:
再来回答一个问题:fork之后,父子代码共享,是所有代码共享还是fork函数之后的代码分享!!!
那么子进程是怎么知道它的代码起始位置在哪里呢?
我们的计算机中会有记录当前执行代码的下一行代码的地址的寄存器—PC也叫程序计数器。因此在子进程创建之时就把它的首地址给它了。所以它虽然能够看到父进程的代码但从fork之后才开始执行。
fork调用失败的原因:系统进程过多;用户的进程数超过限制
二,进程等待
首先我们来回答问什么要有进程等待???
要回答这个问题我们先来了解僵尸进程。僵尸进程就是当一个子进程在退出时,父进程没有收到其状态信息便形成了僵尸进程。
僵尸进程有什么危害呢?
- unix提供了一种机制来使得父进程能够得知子进程结束时的状态信息。这种机制是当每个进程在退出时,内核会释放其所有资源包括,打开的文件,占用的内存等,但是仍会保留一部分信息(退出信息)。知道父进程用wait或waitpid来获取时才会释放。
因此当一个程序的僵尸进程过多时,便会造成资源的浪费。 - 僵尸进程已经相当于一个死进程,我们无法杀死一个已经死掉的进程。
- 获取不到子进程退出时的状态信息,我们就无法得知父进程派给其的任务完成的怎么样了。
因此进程等待就很有必要了!!!
那么该如何等待呢?
其实在回答上一个问题时我们就已经给出了答案-----wait或waitpid。
我们来看下面的代码:
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret=fork();
if(ret<0)
{
perror("错误");
}
else if(ret==0)
{
sleep(2);
printf("I am child:%d\n",getpid());
}
else{
int id= wait(NULL);
if(id>0)
{
printf("回收成功\n");
}
else if(id==-1)
{
printf("失败\n");
}
else{
}
}
return 0;
}
若没有回收操作:
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
int ret=fork();
if(ret<0) { perror("错误"); }
else if(ret==0)
{
printf("I am child:%d\n",getpid());
}
else{
sleep(5);
}
return 0;
}
我们发现其子进程处于僵尸状态。
除了上述这种方法,还有一种进程等待的方法:waitpid函数。
这里就不再进行演示。
接下来我们详细说说这两个函数的形参及其返回值!!!
返回值:
- wait:成功返回回收进程的pid,失败返回-1.
- waitpid:正常返回收集到的进程的pid。失败返回-1 。若是设置了WNOHANG,若发现没有已退出的子进程可回收,返回0 。
参数:
- pid :pid=-1,回收任何进程,与wait一样;pid>0回收与pid进程号一样的子进程。
- status :status是一个输出型参数,为int指针,在这里我们只研究其低十六位:次低八位表示退出状态;最低七位表示退出信号,中间那一位叫做core-dump我们现在只需知道它是用于调试的。
- option:若为0,则表示在等待期间父进程会挂起,阻塞式等待,若为WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID 。
下面我们进行详细的验证:
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
int ret=fork();
if(ret>0)
{
int status;
int t;
do
{
t= waitpid(-1,&status,WNOHANG);
if(t>0)
{
printf("正常退出,退出码:%d,退出信号为:%d\n",status>>8&0xff,status&0x7f);
}
}
while(!t);
}
else{
sleep(2);
printf("我是子进程:%d\n",getpid());
exit(1);
}
return 0;
}
用kill命令将子进程杀死
我们发现退出信号变为了9!!!
因此程序异常退出也不光光是代码的问题,也可能是外部原因
我们再看认识两个宏:WIFEXITED:查看进程是否正常退出,非0表示正常退出;0表示不正常退出。WEXITSTATUS:若进程正常退出,则提取进程的退出码。
既然进程具有独立性,进程退出码,退出信号是子进程的数据,父进程凭什么能拿到呢?
首先我们要知道,拿到子进程退出状态的信息,实际是去读子进程的task_struct结构体。并且,wait/waitpid是系统调用,它们有权利去访问内核空间!!!
接下来我们进行详细的演示进程等待的两种方式!!!
阻塞式等待:
void Hang()
{
int ret=fork();
if(ret==0)
{
printf("I am child:%d\n",getpid());
sleep(5);
exit(1);//退出码为1
}
else if(ret>0)
{
printf("I am father:%d\n",getpid());
int status=0;
waitpid(ret,&status,0);//挂起
if(WIFEXITED(status))
{
printf("退出成功--退出码:%d\n",WEXITSTATUS(status));
}
else{
printf("退出失败---退出信号:%d\n",status&0x7f);
}
}
else{
perror("错误\n");
}
}
int main()
{
Hang();
return 0;
}
正常退出:
杀掉进程:
非阻塞式等待:
void NOHang()
{
int pid=fork();
if(pid==0)
{
printf("I am child:%d\n",getpid());
sleep(5);
}
else if(pid>0)
{
int ret=0;
int status=0;
do{
ret=waitpid(pid,&status,WNOHANG);
if(ret==0)
{
printf("child is running\n");
}
sleep(1);
}
while(!ret);
if(WIFEXITED(status))
{
printf("正常退出--退出码:%d\n",WEXITSTATUS(status));
}
else{
printf("不正常退出---退出码:%d,退出信号:%d\n",WEXITSTATUS(status),status&0x7f);
}
}
else{
perror("错误\n");
}
}
int main()
{
// Hang();
NOHang();
return 0;
}
正常退出:
杀掉进程:
三,进程终止
进程终止的常见方式:
- 代码运行完毕结果正确
- 代码运行完毕结果不正确
- 代码异常退出
这里我们再来回答一个问题:我们经常写的main函数的返回值的意义是什么?
我们在上一小节中讲了进程等待。并题到进程退出时会将退出状态的信息给父进程读。那么父进程也是另一个进程的子进程,那么其应该将退出状态的信息,其父进程也因该能读到。所以我们经常在main函数写的return
0就是返回给上一进程的退出码。所以它可以是任何值,只是为了方便定位错误信息,会对其返回码做出规定。
常见的进程退出方法:
正常退出
- main返回
- 调用exit
- 调用_exit
异常退出 - ctrl C
- 信号终止
我们接下来看exit与_exit的区别!!!
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("hhhhhh");
//exit(1);
_exit(1);
return 0;
}
由于我们的printf没有带换行,所以没办法自己冲刷缓冲,通过结果我们看到,当用exit时,其能够输出,用_exit时,没有输出,则证明,exit具有冲刷缓冲的作用而_exit没有。
其时exit最终调用的也是系统函数_exit。只不过在调用之前还做了一些事情:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit。