欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客
https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (bingbingsupercool) - Gitee.com
https://gitee.com/bingbingsurercool
系列文章推荐
目录
前言
前面的章节中我们介绍了进程的概念,进程的创建,以及地址空间等进程的组成的要素,今天我们要对进程有更深入的了解,理解一下怎么控制进程,如何回收僵尸进程!
1.创建进程
1.1再识fork函数
在前面的进程概念的时候我们知道可以使用fork函数来创建子进程来实现不同的作用。并且对fork函数的返回值做出了论述。
头文件:#include<unistd.h>
pid_t fork (void);
返回值:创建子进程失败返回-1,成功给父进程返回子进程pid,给子进程返回0
当我们调用fork函数后,如果创建进程成功,则代码将父子进程共享。那么fork创建子进程,操作系统都做了什么?
根据以前的知识我们可以理解,fork函数创建一个进程,意味着操作系统中多了一个进程,那么操作系统要给这个进程创建自己的task_struct以及mm_struct,页表等内核结构,然后加载自己的代码和数据。但是子进程在创建后并没有加载代码和数据,所以子进程要使用父进程的代码和数据!
虽然子进程是在fork之后才创建出来,但并不意味着fork之前的代码数据子进程看不到!只是没有执行。CPU中有对应的寄存器数据,用来记录当前进程的的执行位置,即便进程被切出去,下次回来时还是从记录的位置开始执行。
父进程和子进程都会独立执行fork的代码,当数据需要写入时将发生写时拷贝,子进程将会独立拷贝一份父进程的数据并对其进行写入,然后更新页表的映射关系,指向新的数据内存空间。
fork之后,父子进程究竟谁先执行完全由调度器决定!
创建出子进程的目的不是让子进程做与父进程相同的事情,一般使用fork创建子进程有两种用法:
(1)父进程复制自己,使父子进程执行同一代码的不同逻辑
(2)创建子进程执行不同的程序
fork函数也不是一直能够成功的创建子进程,当系统中有太多的进程,或者实际用户的进程超过了限制的时候,进程就创建失败了!
1.2写时拷贝
在前面我们或多或少的都提到过这种写时拷贝的技术,写时拷贝是操作系统中常用的一种技术,正式因为这种技术的存在,操作系统在内存使用率上可以达到接近100%,极大的提高了效率。
在进程中,代码都是不能写的,因此父子进程可以共享,但是数据可能被修改,必须分离!
其实在进程创建的时候我们有两种拷贝技术:一种是直接就将父进程的数据拷贝出来,然后申请内存存放,与父进程的数据形成两份独立的数据空间;一种是子进程虽然创建出来,但是不会立即给数据申请空间,而是和父进程共享,直到父进程或者子进程更改的时候,才会有内存开辟,这就是写时拷贝技术。
第一种方式更加直接,数据一开始就分离,后续的更改操作可以随意进行,两个进程互不干扰。但是这种方式可能会照成浪费。如果我们拷贝的数据并不会被用到,或者即便用到也是读取操作,不会对其进行写入,那么我们复制出来一份的数据就毫无意义,只会浪费空间!还有,就算我们会使用这份空间,但也不见得立即使用,这也会浪费空间。
操作系统极具注重效率,因此这种方式对操作系统来说不能采用,但是操作系统是无法预知哪些空间是写入的,因此操作系统直接采用写实拷贝技术,当你执行写入操作时我在拷贝数据到新空间!这样效率就提高了,内存也不会出现申请了不用的情况。
所以正式因为写实拷贝技术的使用,让进程之间得以彻底分离,既能完成进程独立性的技术保证,又能延迟申请内存,提高效率!
2.进程终止
进程被创建后总有终止的时候,那么进程是如何被终止的呢?又有哪些方式终止呢?
2.1进程终止的场景
首先我们要知道,当一个进程被终止,操作系统需要释放进程申请的相关内存数据和代码,本质上是释放系统资源。
进程终止的方式无外乎有三种:
(1)代码执行完毕,结果正确。
(2)代码执行完毕,结果不正确。
(3)代码没执行完毕,程序崩溃。
我们在书写程序时,自己创建的主程序main函数最后都会写一句return 0;这是为什么呢?
int main()
{
printf("hello world\n");
return 0;
}
其实我们所书写的return 0;是为了返回进程的退出码,每个进程在执行完毕后都存在进程的退出码,其中退出码0表示成功,及进程运行结束,结果正确。非0值则表示进程运行虽然成功,但是结果不对。 这样我们就可以通过进程的退出码来判断一个进程是否执行成功,结果是否正确。对于父子进程来说,父进程就可以使用这种方式来判断子进程是否完成任务。
非0数值有无数个,不同的退出码则表示不同的错误原因,我们可以根据退出码来判断程序的错误方式,从而来判断程序在哪里出错了。在linux系统中,使用echo $? 命令可以查看最近一个进程的退出码。
echo $? :查看最近一个进程的退出码
例如使用命令查看ls命令的退出码:
当使用cd命令进入到不存在的目录时,退出码将不再是0。
我们还发现,进程失败后不仅返回了进程退出码,还将错误信息打印出来,那么每个退出码究竟代表什么错误信息呢?
Linux系统中含有134个错误码,对应134条错误信息,我们可以使用strerror()函数查看,错误信息都存在该函数内部。
#include<string.h>
int main()
{
int i=0;
for(i=0; i<150;i++)
{
printf("%d : %s \n",i,strerror(i));
}
return 0;
}
部分错误原因如下所示:
这是操作系统提供的错误码对应的错误信息,我们当然也可以自己定义错误码和错误信息。
当程序崩溃退出时,退出码就失去了意义,一般而言,return语句没有被执行!
2.2 进程退出的方式
进程退出的方式有两种,正常退出和异常退出。
异常退出就是我们通常使用的快捷键,ctrl+c,实际上本质使用的是信号终止,操作系统通过发送信号的方式将进程杀掉,例如kill -9 +进程id杀掉进程,其中就是发送的9号信号。
对于正常终止进程,我们可以采用三种方式。
(1)return语句,main函数中return语句后面的是进程退出码。
(2)exit函数,C语言提供的库函数,在任何地方用都是直接终止进程。
(3)_exit 系统函数,与C语言中的exit函数功能差不多。
注意:
(1)return语句只有在main函数中才是进程终止,其他函数中并不是,而是函数结果返回。
(2)任何地方调用exit和_exit函数都会终止进程!
(3) exit函数是C语言库函数提供的,在调用终止进程时,相比于_exit函数会刷新缓冲区,然后再终止进程,实际上,exit函数在底层还是调用的_exit函数执行的终止操作。
(4)缓冲区一定不在系统内部,是C标准库维护的
(5)main函数中执行return n 等同于exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
3.进程等待
3.1进程等待的必要性
之前我们将进程状态的时候就提到过,如果子进程退出,父进程不管子进程,那么子进程就会变成僵尸进程,从而造成内存泄漏。僵尸状态的进程,kill -9 也无能为力,你无法杀死一个已经死亡的进程!
父进程创建子进程是为了让子进程去工作,那么父进程就需要获得子进程执行的结果,从而判断是否执行任务成功。
为了完成这些工作,父进程是通过进程等待的方式来回收僵尸进程或者获取进程退出的信息。
3.2进程等待的方法
进程等待通常使用wait和waitpid函数实现。
pid_t wait (int* status) ;
头文件:#include<sys/types.h>
#include<sys/wait.h>
返回值:成功则返回等待进程pid,失败则返回-1。
参数:输出型参数,获取子进程退出状态,不关心可以设置成为NULL
我们可以使用下列代码来测试僵尸进程的回收:
int main()
{
pid_t id = fork();
if(id<0)
{
printf("进程创建失败!\n");
return 1;
}
else if (id==0)
{
int i=5;
while(i>0)
{
printf("%d:i am child,pid:%d ppid%d\n",i,getpid(),getppid());
sleep(1);
i--;
}
printf("子进程退出\n");
exit(10);
}
else
{
int j=7;
while(j>0)
{
printf("i am father , pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
j--;
}
printf("父进程回收子进程\n");
pid_t ret = wait(NULL);
printf("i am father , pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
我们发现,僵尸进程被回收了!
pid_t waitpid(pid_t pid,int * stastus, int options);
头文件:与wait函数一致
返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返
回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:Pid=-1,等待任一个子进程。与wait 等效。Pid>0.等待其进程ID 与 pid 相等的子进程。status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)options:WNOHANG: 若pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。
waitpid函数的参数options一般情况下都默认为0,表示阻塞等待,status为输出型参数。当我们这样调用waitpid(-1,NULL,0)时,与wait(NULL)函数表示含义一致。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。 如果不存在该子进程,则立即出错返回。
3.3status参数的含义
status参数由操作系统填充,是一个输出型参数,如果传递NULL,表示不关心子进程的退出状态信息。否则操作系统会根据该参数将子进程的退出信息反馈给父进程。
status并不是按照整数的形式使用的,它是按照比特位进行填充的。
我们可以用如下代码验证:
int main()
{
pid_t id =fork();
if(id<0)
{
printf("进程创建失败!\n");
return 1;
}
else if (id==0)
{
printf("i am child process!\n");
int cnt=5;
while(cnt>0)
{
printf("child , %d,pid: %d,ppid: %d\n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
exit(105);
}
else{
int status=0;
printf("i am father process!,wait the child\n");
pid_t i= waitpid(id,&status,0);
printf("father,pid:%d,ppid:%d\n",getpid(),getppid());
printf("退出码:%d,退出信号:%d,coredump:%d\n",\
(status>>8)&0xFF,status&0x7F,status&0x80);
printf("等待结束!\n");
}
return 0;
}
退出码的确为子进程退出时的105!
此时我们发现,向上面这样操作确实可以得到进程退出码,但是未免有些麻烦,我还得使用位运算。其实并不用,status参数还提供了两个宏来方便我们使用。也就是前面提到的WIFEXITED和WEXITSTATUS这两个宏,我们可以使用WIFEXITED(status)来辨别进程是否正常退出,如果正常退出,那么将返回真,否则返回假。当进程正常退出后我们便可以使用WEXITSTATUS(status)命令来检查进程退出码。
if(WIFEXITED(status))
{
//子进程退出码
printf("退出码:%d\n",WEXITSTATUS(status));
}
else
{
//异常退出
printf("%d\n",WIFEXITED(status));//通常返回0
}
3.4options参数与非阻塞等待
通过上面的例子我们还发现一点,当子进程在运行时,父进程好像并没有做其他事情,而是一直停在waitpid函数调用那里,等着子进程结束,一旦子进程结束,父进程才会执行后面的代码。
其实这与我们传入的options有关,我们传入的options选项默认为0,0就代表系统将进行阻塞等待。当子进程还在运行没有退出,那么父进程将会挂起,不在继续执行,直到子进程运行结束,父进程被重新唤起,从挂起时后面的代码继续执行。waitpid目前可以控制进程退出的顺序。
当进程阻塞时,其实就是CPU并没有对其进行调度,要么在阻塞队列,要么是等待被调度。一般出现阻塞状态,进程都会被切换,不在执行。如果我们不想让父进程被挂起我们可以设置options的选项为WNOHANG,WNOHANG其实就是一个宏定义,底层就是被定义为1。
当options传入的参数为这个时,就意味着进程将执行非阻塞等待,如果父进程通过调用waitpid来等待进程,发现子进程没有退出,那么waitpid这个系统调用会立马返回,并不会将父进程挂起,父进程此时可以执行其他的任务,并且在这期间父进程将会再次检测子进程状态,直到子进程退出,父进程才会对其回收,并执行后续工作。这个过程叫做基于非阻塞调用的轮询检测方案。
小结:
现在有个问题: 父进程通过wait和waitpid函数来获取子进程的退出结果,为啥要用这么麻烦的方式,直接使用全局变量不行吗?
当然不行,因为每个进程都具备独立性,当数据发生更改时,会采取写时拷贝的形式将数据拷贝到其他内存中,此时全局变量在每个进程都有,子进程修改不会影响父进程!
那进程具备独立性,进程退出码也是子进程的数据呀,父进程又凭什么能拿到呢?
原因在于,wait/waipid函数其实就是系统调用,即便是僵尸进程,它的数据被释放了,但是进程的PCB结构不会释放,里面记录了任何进程的退出结果信息,系统可以通过PCB来获取信息!