我们创建一个进程的时候是使用系统调用接口:
- #include
- pid_t fork(void);
- // 返回值,子进程中返回0,父进程返回子进程的id,出错返回-1
当进程调用fork()之后,当控制转移到内核中的fork代码后,内核做:
我们在之前就已经说过了,一开始父进程和子进程指向的其实都是同一份空间。
此时,我们把页表项100这个部分改成非只读,这个时候我们就会发生缺页中断:
缺页中断:进程往一个不可写的内存写入了,操作系统就会把进程中断,例如上面的图片,我本来是只读的,我把它改成可读可写了,这个时候映射关系就会出现问题了,操作系统这个时候检测到了之后就会把进程停止,发生缺页中断,然后修改页表和对应关系,这个时候我的子进程被中断了,不知道发生了什么,这是分离现象。修改完之后写时拷贝,物理内存分开。
因此,进程创建从时间和空间上的成本都非常高。
进程退出的时候会发生三种情况:
我们知道当程序结束的时候,我们可以使用return,但是有的同学可能知道,exit(EXIT_SUCCESS)也可以完成进程退出操作,那么这两者有什么不同呢?
exit终止进程,强制终止进程,不需要进行进程的后序收尾工作,例如刷新缓冲区,而return可以进行后序的收尾工作。
进程退出的时候,系统层面少了一个进程,free PCB,free mm_struct, free页表和各种映射关系,代码和数据申请的空间也要全部去掉。
函数原型:
pid_t wait(int* status);
作用:等待任意子进程。
返回值:等待成功返回被等待进程的pid,等待失败返回-1。
参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
函数原型:pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子进程或任意子进程。
返回值:
1、等待成功返回被等待进程的pid。
2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
参数:
1、pid:待等待子进程的pid,若设置为-1,则等待任意子进程。
2、status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
3、options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。
我们的父进程fork之后可以得到子进程,子进程存在的目的是帮助父进程完成某种任务,于是我们的父进程在程序的结尾需要wait子进程,以保证子进程一定在父进程之后退出,为什么要这么做呢?
理由其实很简单:
这个core dump一般是0
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
- exitCode = (status >> 8) & 0xFF; //退出码(1111 1111)
- exitSignal = status & 0x7F; //退出信号(0111 1111)
对于此,系统当中提供了两个宏来获取退出码和退出信号。
- exitNormal = WIFEXITED(status); //是否正常退出
- exitCode = WEXITSTATUS(status); //获取退出码
需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。
举一个现实生活中的例子。假如说我要找张三借复习资料,但是张三说,我现在在看书,等我30分钟。那么我这个时候给张三打了一个电话,并且没有挂断电话,等张三下楼之前我都不会挂断电话,也就是说我电话功能全部被封死了,一直在通话中。这个叫阻塞等待
但是假如我每过两分钟给张三打一个电话的话,每次打电话都确认他是否可以下来了,那么这个时候我的电话就有被闲置的时间让我完成其他的任务,这个叫基于非阻塞等待的轮询方案。
不管是阻塞还是非阻塞,都是等待的一种方式。谁等??等谁??等什么??
等子进程。子进程退出是一个条件or事件。
阻塞了是不是意味了父进程不被执行了或者不被调度执行了呢??
父进程如果在阻塞状态等子进程的话,那么它就是在纯等,什么都不会干。父进程本来是R(运行状态的)就会被成为S(等待状态),被加入到等待队列里面去。等子进程搞完之后发现父进程在等待,那么就再把等待队列里面的父进程放过来变成R。
阻塞的本质就是把进程的PCB被放入了等待队列,并将进程的状态改为S状态。返回的本质,进程的PCB从等待队列拿到R队列,从而被CPU调度。
非阻塞等待的代码:
- #include
- #include
- #include
- #include
- int main()
- {
- pid_t pid;
-
- pid = fork();
- if(pid < 0){
- printf("%s fork error\n",__FUNCTION__);
- return 1;
- } else if( pid == 0 ){ //child
- printf("child is run, pid is : %d\n",getpid());
- sleep(5);
- exit(1);
- } else{
- int status = 0;
- pid_t ret = 0;
- do
- {
- ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
- if( ret == 0 ){
- printf("child is running\n");
- }
- sleep(1);
- }while(ret == 0);
-
- if( WIFEXITED(status) && ret == pid ){
- printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
- }else{
- printf("wait child failed, return.\n");
- return 1;
- }
- }
- return 0;
- }
我们可以看到有一个WNHOANG,加上这个,我们就可以实现非阻塞的等待了。
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
当进行进程程序替换时,有没有创建新的进程?
进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。
子进程进行进程程序替换后,会影响父进程的代码和数据吗?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
也就是说进程替换就是用一个老的进程的壳子去执行一个新的代码,没用创建新的进程就执行了代码。
它的本质是:把指定的进程代码 + 数据,加载进特定进程的上下文中去。
exec函数实际上起到了加载器的作用,通过程序替换的方式来把磁盘上的东西加载到内存。