fork、vfork、wait、waitpid

fork函数:
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

fork调用的一个奇妙之处就是它仅仅被调用一次
却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回03)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功
则出现两个进程,一个是子进程,一个是父进程
在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID
我们可以通过fork返回的值来判断当前进程是子进程还是父进程。


fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

子父进程执行过程:

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        pid_t pid;
        pid_t fpid;
        pid=getpid();
        printf("before fork:pid=%d\n",getpid());
        fpid=fork();
        printf("after fork:pid=%d\n",getpid());
        if(pid==getpid()){
                printf("This is father printf,pid=%d\n",pid);
        }
        else{
                printf("This son printf,pid:%d\n",getpid());
        }
        return 0;
}

运行结果:
before fork:pid=14396
after fork:pid1=4396
This is father printf,pid=14396
after fork:pid14397
This son printf,pid:14397

由结果可知在程序中父进程会把符合条件的整个代码
都执行一遍,然后子进程开始执行fork()函数之后的
代码,子进程执行过程中不会执行fork()函数之前的
代码,但是可以访问fork()函数之前父进程中的变量。
在语句fpid=fork()之前,只有一个进程在执行这段代
码,但在这条语句之后,就变成两个进程在执行了

fork函数创建的新进程的存储空间是如何分配的?
每一个进程都有自己的存储空间,创建的新的进程也不例外,在早期linux系统中会把父进程中的命令行参数、堆、栈、未初始化数据、初始化数据和正文全部拷贝一份到自己开辟的内存空间,后来随着linux内核技术的更新,并不是把所有的东西全部拷贝到自己的内存,而是写时拷贝,什么时候会写时才拷贝?很显然,当然是在共享同一块内存的类发生内容改变时,才会发生。

写时拷贝技术:
学习过fork我们都知道是父进程创建出一个子进程,子进程作为父进程的副本, 是父进程的拷贝。
可是每次fork出的子进程难道还要把父进程的各种数据拷贝一份?有人会说不是父子进程不共享各种数据段吗?如全局变量区 ,栈区 , 堆区 。如果不拷贝那不就成共享的吗?其实有关子进程拷贝父进程的数据是这样的。如果子进程只是对父进程的数据进行读取操作,那么子进程用的就是父进程的数据。如果子进程需要对某数据进行修改,那么在修改前,子进程才会拷贝出需要修改的这份数据,对这份备份进行修改。这就满足了父子进程的数据相互独立,互不影响的要求。这么做的初衷也是为了节省内存。

举个栗子如果一份代码中,定义了10个数据。父进程执行的部分对这10个数据全部进行修改,而子进程执行的部分只修改了一个数据,子进程明明用不到其他9个数据,那还何必让子进程拷贝全部数据,多占用9个永远使用不到的数据内存?
因此创建子进程只是将原父进程的pcb拷贝了一份。父子进程的pcb全部指向的是父进程原本就有的数据,如果子进程里对数据进行了修改,那么子进程的pcb里指向 被修改的数据的指针会指向一个自己新开辟的内存,新开辟的内存里将父进程的数据拷贝过来,然后再进行修改。这就是写时拷贝技术,顾名思义,只在写的时候才拷贝的技术。

关于参数的修改问题:

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
        pid_t pid;
        pid_t fpid;
        pid=getpid();
        int data=10;
        printf("before fork:pid=%d\n",getpid());
        fpid=fork();
        printf("after fork:pid%d\n",getpid());
        if(pid==getpid()){
                printf("This is father printf,pid=%d\n",pid);
        }
        else{
                printf("This son printf,pid:%d\n",getpid());
                data=data+10;
        }
        printf("data=%d\n",data);
        return 0;
}
运行结果:
before fork:pid=14462
after fork:pid14462
This is father printf,pid=14462
data=10
after fork:pid14463
This son printf,pid:14463
data=20//当数据发生改变时才会从父进程中将要改变的值拷贝一份到子进程自己开辟的内存中去。
	  //不影响父进程中的值

fork创建一个子进程的一般目的:

  • .一个父进程希望复制自己,使父子进程同时执行不同的代码段,在这个网络服务进程中是常见的。父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
  • 一个进程要执行一个不同的程序,这对shell常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

简单使用fork(有bug后续完善):

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <unistd.h>
int main()
{
        pid_t pid;
        pid_t fpid1,fpid2;
        pid=getpid();
        int data=10;
        while(1){
                printf("请输入数字:\n");
                scanf("%d",&data);
                if(data==1){
                        fpid1=fork();
                        if(fpid1==0){
                                printf("这是创建的第一个子进程\n");
                                while(1){
                                        printf("-----------,pid=%d\n",getpid());
                                        sleep(3);
                                }
                        }
                }
                else if(data==2){
                        fpid2=fork();
                        if(fpid2==0){
                                printf("这是创建的第二个子进程\n");
                                while(1){
                                        printf("-----------,pid=%d\n",getpid());
                                       sleep(3);
                                }
                        }
                }
        }
        return 0;
}    

vfork函数:

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:
	vfork() 函数和 fork() 函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。

返回值:
    成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为无符号整型。
    失败:返回 -1

fork() 与 vfock() 都是创建一个进程,那它们有什么区别呢?

  • fork(): 父子进程的执行次序不确定。
    vfork():保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。
  • fork(): 子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。
    vfork():子进程共享父进程的地址空间(准确来说,在调用 exec(进程替换) 或 exit(退出进程) 之前与父进程数据是共享的)

示例演示:

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include<stdlib.h>
int main()
{
        pid_t pid;
        pid_t fpid;
        pid=getpid();
        int count=0;
        fpid=vfork();
        if(fpid>0){
                while(1){
                        printf("这是父进程,PID=%d,count=%d\n",pid,count);
                        sleep(1);
                }
        }
        else if(fpid==0){
                while(1){
                        printf("这是子进程,PID=%d\n",getpid());
                        sleep(1);
                        count++;
                        if(count==3){
                                exit(0);
                        }
                }
        }
        return 0;
}
以下是程序运行的结果:
这是子进程,PID=17935
这是子进程,PID=17935
这是子进程,PID=17935
这是父进程,PID=17934,count=3
这是父进程,PID=17934,count=3
这是父进程,PID=17934,count=3
由此可看出由vfork创建的子进程在退出前共享父进程地址空间
因为在子进程退出时父进程没有收集子进程的状态,所以子进程变为僵尸进程。z+表示僵尸进程,s+表示正在运行。
fhn       17999  0.0  0.0      0     0 pts/2    Z+   21:03   0:00 [vfork] <defunct>

进程的退出方式

(1)正常退出

  • 在main函数中执行return
  • 调用exit()函数
  • 调用_exit()或者_Exit()函数
  • 进程最后一个线程返回
  • 最后一个线程调用pthread_exit

(2)异常退出

  • 调用about函数
  • 进程受到某个信号(如ctrl+c),而该信号使程序终止

总结:不管是那种退出方式,最终都会执行内核中的同一段代码。这段代码用来关闭进程中所有打开的文件描述符,释放它所占用的内存和其他资源。

退出方式比较:

  • exit和return的区别:exit是一个函数,有参数;而return是函数执行完后的返回。exit把控制权交给系统,而return将控制权交给调用函数。
  • exit和abort的区别:exit是正常终止进程,而about是异常终止。
  • exit(int exit_cod):exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生,比如溢出,除数为0。
  • exit()和_exit()的区别:exit头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中。两个函数均能正常终止进程,但是_exit()会执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。

父子进程终止的先后顺序不同会产生不同的结果。在子进程退出前父进程退出,则系统会让init进程接管子进程。当子进程先于父进程终止,而父进程又没有调用wait函数等待子进程结束,子进程进入僵死状态,并且会一直保持下去除非系统重启。子进程处于僵死状态是,内核只保存该进程的一些必要信息以备父进程所需。此时子进程始终占用着资源,同时也减少了系统可以创建的最大进程数。如果子进程先于父进程终止,且父进程调用了wait或waitpid函数,则父进程会等待子进程结束。

等待子进程退出:

为什么要等待子进程退出?因为创建子进程的目的就是为了执行别的代码,然而子进程代码的执行情况我门不了解,也不知道子进程是不是正常退出,所以我们要等待子进程的退出收集子进程退出时返回的状态(正常退出时:根据退出码查看退出是代码的执行情况,异常退出时:查看异常退出的原因)。如果父进程在子进程退出时没有收集子进程的退出状态,则子进程就会变为僵尸进程(创建子进程后,子进程退出状态不被收集,变成僵尸进程。爹不要它了除非爹死后变孤儿init进程养父接收。如果父进程是死循环,那么该僵尸进程就变成游魂野鬼消耗空间。)。

wait函数:

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
参数status用来保存被收集进程退出时的一些状态
它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意
只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL。

可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1.  WIFEXITED(status) 为非0 → 进程正常结束

	WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)

 2. WIFSIGNALED(status) 为非0 → 进程异常终止

	WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。

3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态

	WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。

	WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
	
//下面是使用方法:注意&status是指针
wpid = wait(&status)
if(WIFEXITED(status)){	//正常退出
			printf("I'm parent, The child process "
					"%d exit normally\n", wpid);
			printf("return value:%d\n", WEXITSTATUS(status));
 
		} 

返回值:
如果成功,wait会返回被收集的子进程的进程ID
如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

waitpid函数:

pid_t waitpid(pid_t pid, int *wstatus, int options);

从本质上讲,系统调用waitpid和wait的作用是完全相同的但waitpid多出了两个可由用户控制的参数pid和options。

  • pid:从参数的名字pid和类型pid_t中就可以看出这里需要的是一个进程ID,但当pid取不同的值时,在这里有不同的意义。
  • pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  • pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
  • pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
  • pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

options:options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

如果我们不想使用它们,也可以把options设为0,如:ret=waitpid(-1,NULL,0);

如果使用了WNOHANG(不挂起)参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去,就像当于在父进程执行的闲暇时间检查有没有退出的进程。虽然使用了这个收集到子进程退出的信息,但是子进程还会变为僵尸进程。
而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。

waitpid的返回值比wait稍微复杂一些,一共有3种情况:

  • 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
  • 当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值