进程创建
fork函数
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示:
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
当父进程创建子进程之后,子进程进行写入,会发生写时拷贝?重新申请空间,进行拷贝,修改页表,这部分工作都是由操作系统来做的。那么我想请问一下,你说发生写时拷贝就发生写时拷贝啊?你当前可是在发生写入啊,那么操作系统是怎么把握这个时机来完成写时拷贝的这个工作呢?
比如说你在家的时候,你说你饿了,你爸还没下班,你就准备动筷子吃饭了,你妈这个时候刚好叫住你说:"等一下,我先给你把打包一份你在吃",打包之后就说:"好了,现在你可以吃了。"就好比这样,但是呢,操作系统到底如何能做到什么时机应该完成写时拷贝呢?
其实,父进程创建子进程的时候首先把自己的读写权限改成只读,然后再创建子进程。这个工作我们作为用户是不知道的,那么用户就可能对某一批数据进行写入!而写入的时候由于权限是只读的,那么在我们的页表转换会因为权限问题而出错,一旦出错了,操作系统就可以介入了。出错的原因可能是真的出错了,也有可能不是出错,而是触发我们进行重新申请内存拷贝内容的策略机制。(写时拷贝)
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
下面我们通过写一份多进程的代码:
运行结果:
进程终止
我们平常写的C语言代码都有一个main函数,而这个main函数有一个返回值,我们通常返回的是0,那么我们main函数也是函数,所以它也会被调用,那么main函数是被谁调用呢?这个0又是给谁进行返回呢?
那我们下面来谈一谈main函数的 返回值。
其实我们的main函数一般是被一个__StartCRT的函数给调用了,既然被调用了那么这个main函数返回值应该就是交给调用他的函数,其实调用他的函数也要将返回值继续向上进行传递。为什么呢?
下面我们先把main函数退出码设为10.
我们运行起来
当前代码什么工作都没做,但是这个可执行程序一运行起来变成一个进程,变成进程之后他的父进程就是bash,而这个进程的main函数的返回值最终会交给父进程。
下面我们通过该指令获取一些父进程收到的退出结果。
我们就得到了一个退出结果是10,这个退出结果就是刚刚main函数的返回结果。
这是我们发现的一个现象。
其实在我们的生活中平常做的事情的情况来分,你做任何一件事情情况无外乎就这三种:
- 事情做完了,做的做的结果非常好。
- 事情做完了,做的结果很不好。
- 事情没做完,中间出问题出岔子。
比如说我们平常考试,第一,你把试考完了,但是考出来的结果不太好,第二种,你把试考完了,考出来的结果非常好,第三种,你考试没考完,中间出岔子了,比如说作弊被抓了,没考完。
同样在我们的程序当中也是从这个三种情况来看的:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
举个简单例子,我们在跑一个冒泡排序的代码的时候,要么就是代码跑完了,最后排出来的结果是正确的,要么呢就是代码跑完了跑出来的结果不正确,还有一种呢就是跑到一般代码出异常了。
我们上面所谈的main函数的返回值目前只考虑前两种情况,也就是把代码异常终止的情况排除在外。我们代码运行完毕结果不正确还是结果正确,我们得知道这个结果。那么这里说的我们指的是谁呢?如果我们写了一个单进程的代码,比如说链表逆置的算法,或者一个什么算法最终的结果是需要我们自己去看的,如果我们在多进程环境中,我们创建子进程的最终目的是什么?最终目的是帮我办事。子进程把事情办得怎么样呢?这里的我们一般都是指的父进程。而我们的子进程把事情办得怎么样了是通过main函数的返回值来进行识别的,比如说返回值是0那就说明办成功了,而非0代表出错了,但是出错的原因有很多啊,具体是由于什么原因出错的,非0的数字有很多,0只有一个,这个时候就是通过不同的数字来代表不同的出错的原因的。但是呢一般用这些纯数字来表示出错原因对于人来讲不便于人阅读,所以就有了将exit code-> exit code string! 也就是将对应的退出码转换成人便于阅读的字符串,其实c语言内置的一个函数接口:
其实我们一开始学习的时候也不知道怎么来描述的,所以我们通过一段代码来进行测试:
运行结果:
上述的错误码就是我们可能出现的错误信息与错误码之间的对应关系。
我们发现到了134之后就是都不认识了。
根据不同的结果现象,返回不同的退出码,什么现象?
C语言有一个全局变量叫errno是C语言的错误码。我们来看一下errno的说明:
退出码vs错误码
错误码通常是用来衡量一个库函数或者是一个系统调用一个函数的调用情况
退出码通常是一个进程退出的时候,他的退出结果。
虽然这两个是不同的概念,但是他们都有共同的表征,那就是当失败的时候,用来衡量函数,进程出错时的详细出错原因。
下面我们编写如下代码将错误信息打印出来:
运行结果:
下面呢我们再从代码异常终止的情况来了解异常问题:
下面我们用除0错误和野指针问题来进行说明:
运行结果:
我们可以看到出现了除0错误。
下面再用一个野指针问题
运行结果:
野指针问题,在Linux中叫做段错误。
如果我们的程序异常了,本质就是我们的进程异常了,进程出异常了,我们的操作系统为了不让该进程影响到别人通常会将这个异常的进程给杀掉,那么怎么杀掉呢?通过信号的方式:
我们刚刚的除0异常其实就是上图中的8号信号,而我们的段错误是上图中的11号信号。
上述我们说到的进程异常终止其实是因为进程收到了操作系统发送的信号导致的异常终止的,那么怎么证明呢?
下面我们通过运行这段代码
运行起来之后为了验证操作系统是通过发送信号让进程异常终止的同时报错异常终止的原因,那么我们是不是也可以通过发送信号让一个正常运行的进程报出相对应的错误呢?
我们再另一个窗口上发送了一个8号信号发现确实报错浮点数除零错误,发送了一个11号信号发现确实报错段错误。
所以我们就得出了一个结论:进程出异常,本质是进程收到了相对应的信号,进程终止了。
所以一个进程是否异常我们只要看有没有收到信号即可,而判断一个进程运行是否正确只需要通过进程的退出码来判断。所以父进程本质只需要关注这两个数字来判断子进程完成的任务如何。
进程常见退出方法
1.从main函数返回
首先我们可以看一下这一段代码:
我们运行一下:
发现最近一个进程的退出码就是main函数中return的值,所以这是一种进程退出的方法。
2.调用exit
我们先看看exit的手册:
exit()是c提供的一个接口,这个exit 中有一个参数,所以我们可以很容易的想到这个参数就是我们进程的退出码。
此时为了验证该函数是用来进行进程退出的,我们可以直接用以下代码进行测试:
运行结果:
我们发现进程的退出码是12,所以exit中的参数是进程的退出码,等价于main函数中的return.
下面我们把代码改成如下情况:
然后再运行之后:
第一个现象:fun函数被调用了,后续代码没有跑了。
第二个现象:当前退出码变成了21了,而不是12了。
所以我们得出了结论:任意地点调用exit表示进程退出,不进行后续执行。
为了进一步验证该结论我们可以再尝试一次代码的验证:
然后运行:
我们发现这个程序啥都没干就退出了,然后进程退出码变成了31.
所以后续我们想让一个进程直接退出的时候我们就可以直接调用exit();
3._exit
_exit的手册:
下面我们把我们上述写到的代码中的exit 换成_exit
进行运行:
发现跟exit的结果是一样的,其实在这里与exit的效果一样。
下面我们来讨论这两者有什么区别呢?
首先我们先对exit来进行操作:
然后运行:
这条打印的消息会先刷出来然后再暂停3s然后退出。
如果把\n去掉,也就是不让他刷新缓存区:
代码变成这样然后运行:
刚开始是这样
然后:
这样是在进程退出的时候是会自动刷新缓存区的。
如果我们换成_exit的话:
运行是这样:
这是通过行刷新来刷新缓冲区的,所以没有问题,但是如果我们把\n去掉之后运行:
运行之后:
发现打印消息直接不见了。
两者区别:
1.exit是库函数,_exit是系统调用。
2.exit终止进程的时候,会自动刷新缓冲区。_exit终止进程的时候不会自动刷新缓冲区。
我们目前知道的缓冲区,绝对不在操作系统内部。
不然的话双方都应该刷新。
我们把关于缓冲区这个问题留到后面。