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;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/272222.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java解析xml

<?xml version"1.0" encoding"UTF-8"?> <mimetype><default><mime-type>text/html</mime-type></default><mime-mapping><extension>zip</extension><mime-type>application/zip</mime-…

Linux常见英文报错中文翻译(菜鸟必知)

Linux常见英文报错中文翻译(菜鸟必知)1.command not found 命令没有找到2.No such file or directory 没有这个文件或目录3.Permission denied 权限不足4.No space left on device 磁盘没有剩余空间5.File exists 文件已经存在6.Is a directory 这是1个目录7.Not a directory 不…

阿里开源分布式事务seata带你入门

介绍 Seata 是阿里巴巴开源的分布式事务中间件&#xff0c;一种分布式事务解决方案&#xff0c;具有高性能和易于使用的微服务架构。 1:对业务无侵入&#xff1a;即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入 2:高性能&#xff1a;减少分布式事务解决方案…

exec族函数、system函数、popen函数、PATH

exec族函数函数的作用&#xff1a; 我们用fork函数创建新进程后&#xff0c;经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时&#xff0c;该进程被完全替换为新程序&#xff08;在exec都后面的代码不会被得到执行&#xff09;。因为调用exec函数并不创建…

jquery.validate.unobtrusive的使用

应用 一、引入 <script src"Scripts/jquery-1.7.1.min.js"></script> <script src"Scripts/jquery.validate.js"></script> <script src"Scripts/jquery.validate.unobtrusive.js"></script> 二、1&#xf…

Linux操作系统六大优点

❤️作者主页&#xff1a;IT技术分享社区 ❤️作者简介&#xff1a;大家好,我是IT技术分享社区的博主&#xff0c;从事C#、Java开发九年&#xff0c;对数据库、C#、Java、前端、运维、电脑技巧等经验丰富。 ❤️个人荣誉&#xff1a; 数据库领域优质创作者&#x1f3c6;&#x…

[webview] 放大缩小的问题

http://www.cocoachina.com/bbs/read.php?tid33249转载于:https://www.cnblogs.com/zxykit/p/5274831.html

进程间通信IPC(一)(半双工管道和消息队列)

引言&#xff1a; 之前学习的进程之间交换信息的方法只能由fork或exec传送打开文件&#xff0c;或者文件系统。但是这种通讯方式有局限性&#xff0c;接下来将说明进程之间相互通信的其他技术——IPC(InterProcessCommunication)&#xff0c;过去UNIX系统IPC是各种进程间通信方…

Seata相关概念

相关概念 XID&#xff1a;一个全局事务的唯一标识 Transaction Coordinator (TC)&#xff1a; 事务协调器&#xff0c;维护全局事务的运行状态&#xff0c;负责协调并驱动全局事务的提交或回滚&#xff08;可以理解为事务的协调者&#xff09;。 Transaction Manager (TM)&…

VSS配置

http://www.cnblogs.com/FreeDong/articles/2193151.html转载于:https://www.cnblogs.com/lacey/p/5275514.html

Program terminated with signal SIGSEGV, Segmentation fault.

问题&#xff1a; 在程序运行时出现了&#xff1a;Segmentation fault (core dumped)&#xff0c;分段故障(堆芯转储)也就是段错误。 我首先在一篇博客上面找到如何使用gdb调试&#xff1a; 1、首先使用ulimit -a查看你的core file size是否为0.如果不是的话就请跳到步骤3&am…

NFS简介

NFS 是Network File System的缩写&#xff0c;即网络文件系统。一种使用于分散式文件系统的协定&#xff0c;由Sun公司开发&#xff0c;于1984年向外公布。功能是通过网络让不同的机器、不同的操作系统能够彼此分享个别的数据&#xff0c;让应用程序在客户端通过网络访问位于服…

Php Laravel框架 多表关系处理 之 Eloquent一对多关系处理

Php Laravel框架 多表关系处理 之 Eloquent一对多关系处理 本博文主要介绍 Laravel 框架中 Eloquent 对一对多关系的处理以及在 Laravel Administrator(后台扩展包)中的应用。 您的数据库可能是彼此相关的。比方&#xff0c;一篇博客文章可能有很多评论&#xff0c;或者一个订…

进程间通信IPC(二)(共享内存、信号、信号量)

共享内存&#xff1a; 共享内存就是允许两个或多个进程共享一定的存储区。就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候&#xff0c;其它进程都会察觉到这个更改。因为数据不需要在客户机和服务器端之间复制&am…

分布式理论CAP定理

CAP原则又称CAP定理&#xff0c;指的是在一个分布式系统中&#xff0c; Consistency&#xff08;一致性&#xff09;、 Availability&#xff08;可用性&#xff09;、Partition tolerance&#xff08;分区容错性&#xff09;&#xff0c;三者不可兼得。 998年&#xff0c;加州…

xmlWriter 以UTF-8格式写xml问题

dom4j中的XMLWriter提供以下几种构造方法&#xff1a; XMLWriter() XMLWriter(OutputFormat format) XMLWriter(OutputStream out) XMLWriter(OutputStream out, OutputFormat format) XMLWriter(Writer writer) XMLWriter(Writer writer, OutputFormat format) 最简单常…

linux线程(互斥锁、条件)

线程概念&#xff1a; 典型的UNIX/Linux进程可以看成只有一个控制线程&#xff1a;一个进程在同一时刻只做一件事情。有了多个控制线程后&#xff0c;在程序设计时可以把进程设计成在同一时刻做不止一件事&#xff0c;每个线程各自处理独立的任务。 进程是程序执行时的一个实例…