进程创建
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
我们发现打印了俩个,原因就是因为fork()函数之后创建了一个子进程,子进程执行了一次,父进程执行了一次。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程,将父进程部分数据结构内容拷贝至子进程,添加子进程到系统进程列表当中fork返回,开始调度器调度。
pid_t fork(void):返回值:自进程中返回0,父进程返回子进程id,出错返回-1.
fork()函数为啥要有俩个返回值呢?
首先我们创建子进程是为了父和子执行不同的事情,因此为了执行不同的代码块,让fork()有了不同的返回值。
fork函数为什么要给子进程返回0,给父进程返回子进程的PID?
一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。
fork()函数是怎么做到俩个不同的返回值的呢?
在这里我们可以先了解一下写实拷贝的概念。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副
本。具体见下图
当我们的发生写实拷贝的时候,我们的虚拟内存是不变的,操作系统会对要进行数据修改的一方在操作系统重新开辟一个空间,通过页表的映射,找到该物理地址,因此发生写实拷贝的时候虚拟内存不变,而发生变化的是物理内存。
我们可以通过下面的代码来看看:
在上面的代码中,我们可以发现代码是具有独立性的,因此数据必须独立,不能因为子进程的改变而影响父进程。
fork常规用法
1一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
fork调用失败的原因
fork函数创建子进程也可能会失败,有以下两种情况:
系统中有太多的进程,内存空间不足,子进程创建失败。
实际用户的进程数超过了限制,子进程创建失败。
进程的终止
进程退出场景
进程退出只有三种情况:
代码运行完毕,结果正确。
代码运行完毕,结果不正确。
代码异常终止(进程崩溃)。
进程退出码
我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。
既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。
当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。
为什么以0表示代码执行成功,以非0表示代码执行错误?
因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。
C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:
我们知道我们的命令行也是进程,当他运行不成功的时候也会有退出码。
进程常见退出方法
return退出
在main函数中使用return退出进程是我们常用的方法。
退出码为0,表示该进程成功退出。
exit()函数
使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:执行用户通过atexit或on_exit定义的清理函数。
关闭所有打开的流,所有的缓存数据均被写入。
调用_exit函数终止进程。
下面的代码我没有用换行的符号,但是exit会将缓冲区的数据刷新。
_exit函数
例如,以下代码中使用_exit终止进程,则缓冲区当中的数据将不会被输出。
eturn、exit和_exit之间的区别与联系
只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。
使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。
return、exit和_exit之间的联系
return num等于exit(num),,因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。
使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。.
进程异常退出
情况一 向进程发生信号导致进程异常退出。
这个死循环程序,我们可以模拟实现一下进程这个信号异常退出的情况。
我使用了kill -19的信号暂停了该进程,
我使用了kill -9的信号杀死了该进程
情况二:代码错误导致进程运行时异常退出。
例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。
进程等待
什么是进程等待呢?
通过系统调用wait/waitpid来进行对子进程进行状态检测与回收的功能。
为什么要进程等待呢?
我们在之前的博客写道,子进程退出的时候,需要等待父进程来回收他,不然就会变成僵尸进程,一直存在,存在内存泄露的风险。
我们应该怎么办呢?
通过系统调用接口wait/waitpid来回收僵尸进程
wait/waitpid函数的介绍
通过等待来改变进程的状态。
完整的进程创建完之后应该等待然后销毁掉
等待成功的时候就会将该进程僵尸状态给回收掉,因此进程等待是必须的,wait是等待任意一个进程结束的。
多进程的创建与销毁
wait当任意一个进程退出的时候,wait回收子进程,如果任意一个子进程不退出呢?
如果子进程一直不退出,那么父进程就会一直在等待,父进程一直在wait的时候,调用这个系统调用,也就不返回,默认叫做阻塞状态。
因此我们可以查看一下子进程的退出结果,而退出结果一般就是顺利运行,要么就是运行失败,要么就是收到了信号影响。
我们可以使用waitpid()第二个参数来接收子进程的退出码以及他是是否是正常退出的。
可是我们接收到的子进程的退出码是2861是啥原因呢?
因为在32位的数字中,我们只考虑低16位的数字,其中次低7位的表示终止信号,次低八位表示退出码。2816是有11的二进制序列转化而来,我们在代码中改成这个样子就可以转化过来了。
实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。
方法很简单,向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。
例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。
代码示例: