linux入门---fork函数和进程退出

fork函数的三个问题

通过之前的学习大家应该会使用fork函数来实现一些功能,比如说通过fork函数创建父子进程,然后让父子进程执行相同的代码,或者使用if else语句来执行不同的代码,那这里我们再来聊聊fork函数的几个小问题。

问题一

为什么在父进程中fork函数会返回子进程的id,而在子进程中fork函数的返回值却是0?原因很简单因为一个父进程可以通过fork函数创建多个子进程,但是每个子进程都只有一个父进程,就好比一个家庭有4个儿子每个儿子都有着不同的名字比如说张三,李四,王五,狗蛋,但是不同名字的儿子都只有一个名叫李华的父亲,子进程之所以返回0是因为在子进程中可以通过getpid函数获取父进程的id所以fork函数可以直接返回0,但是父进程中却没有函数能够获取子进程的id,所以就只能通过fork函数的返回值来记录子进程的id以便后面的使用.

问题二

为什么调用一个fork函数却能够有两个返回值?在平时写函数的时候会在函数体里面实现一些具体的功能或者让函数帮我们解决一些问题,在函数的最后通过return将执行的结果告诉使用着比如下面的add_function函数:

int add_function(int a ,int b)
{
	int c=a+b;//具体功能实现
	return c;//将实现的结果告诉使用者
}

在将变量c作为结果返回之前,该函数已经完成了两个整型变量的相加也就是说已经实现了add_function函数的具体功能,那类比到fork函数呢?fork函数的功能是创建一个子进程,在创建子进程的过程中首先得创建子进程的PCB并将父进程的PCB赋值给子进程,然后创建子进程的地址空间并赋值,然后创建子进程的页表并把子进程的PCB放入进程的list当中等等等,完成一些列内容之后再将子进程的pid作为返回值进行返回:

pid_t fork()
{
	1.创建子进程的PCB
	2.赋值
	3.创建子进程的地址空间
	4.赋值
	5.创建子进程的页表
	6.将子进程放入进程的list
	......
	return pid;
}

在return执行之前该函数已经实现了上述的种种功能这些功能累加起来就是fork函数的功能,fork函数是用来创建子进程的也就是说在return执行之前子进程已经创建好了,当子进程创建好之后就会有两个执行流来执行下面的代码,所以return指令会被执行两次所以一个fork函数会有两个不同的返回值,父进程返回子进程的id,子进程返回0。

问题三

首先来看看下面的代码:

#include<stdio.h>
#include<unistd.h>
int main()
{
	pid_t id =fork()if(id==0)
	{
		//执行子进程的内容
	}
	else
	{
		//执行父进程的内容
	}
	return 0;
}

这段代码大家应该不陌生,通过fork函数的不同返回值和if else语句可以让父子进程执行后序代码的不同部分,通过上面的讲解大家应该能够理解为什么一个fork函数会有两个返回值,那这里还有个问题我们说fork函数之后父子进程会执行后面的代码,那这里执行的代码就一定包括:pid_t id =fork(),fork函数有两个返回值那为什么一个变量却能够记录两个不同的返回值呢?原因也很简单因为写时拷贝,创建子进程之后内存中就多了一个子进程来执行后面的代码,但是此时的父子进程使用的代码和数据是同一份的,比如说下面的图片在没有创建子进程的时候是这样的:
在这里插入图片描述
创建子进程之后就成了这样:
在这里插入图片描述
此时父子进程共用内存上面的代码区和数据区,当子进程想对内存上的公共数据进行修改时操作系统都会再开辟一个空间把原来的数据区的内容复制到新空间里面并修改子进程的页表使其指向新空间,所以这时子进程对数据进行修改就不会影响到父进程,那么我们把这样的拷贝称为写时拷贝:
在这里插入图片描述
调用fork函数创建子进程之后是子进程先执行后面的代码,还是父进程先执行后面的代码我们是不知道的,这和调度器有关,但是我们知道的一点就是不管谁先执行后序的代码,肯定有一方会对公共数据(变量id)进行修改,一旦发生修改就会进行写时拷贝,所以此时的内存上会存在两个变量来记录fork函数的返回值,而不是一个变量能够记录两个值,希望大家能够理解。

fork函数的常规用法

fork函数一般有两个用法一个是让父子进程共同执行后序的内容,比如说下面代码:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5     fork();
  6     printf("调用一个printf函数但是打印了两句话\n");
  7     return 0;                                                                                                                       
  8 }

这里只调用一次printf函数,但是却打印了两个相同的内容就是因为fork函数创建了子进程,父子进程都执行了printf就会出现这样的结果
在这里插入图片描述
当然也可以对上述的代码进行修改让父子进程执行后序代码的不同部分

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5     int id = fork();
  6     if(id==0)
  7     {
  8         printf("我是一个子进程\n");
  9     }
 10     else 
 11     {
 12         printf("我是一个父进程\n");
 13     }
 14     return 0;
 15 }  

这段代码的运行结果如下:
在这里插入图片描述
我们把上述fork函数的用法称为子成父业,父子进程同时执行后序代码就好比生活中有些人是某个行业的佼佼者然后他们的孩子也跟着他一起在这个行业上深耕发展,但是生活还有这么一些人他们的父亲在某些行业上非常的优秀但是他们却不想和父亲干一样的工作,所以他们就会去其他的领域进行学习干其他方向的事情。在生活中有这样的孩子那么在程序中也会有这样的用法,我们可以通过fork函数创建一个子进程然后让子进程干其他的事情执行其他程序的代码,那么这就是fork函数的第二个用法,那具体怎么做以后再给大家进行讲解。

fork调用失败的原因

在生活中做一些事情往往都会有个度,比如说小酒调情,过度饮酒有害身体健康;适度游戏益脑,成谜游戏伤身等等。在生活中是这样的,那么用fork函数创建子进程的时候也是同样的道理,创建子程序会占用机器的内存消耗cpu的资源,当我们不停的使用fork函数创建子进程的时候就会导致内存和cpu的资源被消耗完,所以这时再调用fork函数就会报错,比如说下面的代码:

#include<stdio.h>
#include<unistd.h>
int main()
{
	int cnt =1;
	while(1)
	{
		int ret=fork();
		if(ret<0)
		{
			printf("fork error,cnt:%d\n",cnt);
		}
		else if(ret==0)
		{
	    	while(1)
	    	{
	    		sleep(1);
	    	}
		}
	}	
	cnt++;
	return 0;
}

这段代码就是通过while循环不停的创建子进程,然后在子进程里面通过while循环和sleep函数阻止进程结束,所以 当这段代码运行起来后就会不断地创建无法结束的子进程,等服务器的资源被消耗殆尽了服务器就会崩溃,然后打印出来fork error并显示当前一共创建了多少个子进程。

main函数的返回值

我们之前写的程序在main函数里面总是以return 0结尾,我们把main函数里面的return 0称为退出码,退出码的作用就是标定进程的执行结果是否正确,main函数返回0表示当前进程的执行结果是正确的,如果为非0的话就表明当前程序执行的时候发生了错误,比如说写一个函数该函数的功能就是计算一个整数累加到另外一个数的和,那这里就可以根据main函数的返回值来判断该函数的实现是否正确,比如说下面的代码:

  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 int addtotarget(int from,int to)  
  4 {  `在这里插入代码片`
  5     int sum =0;  
  6     for(int i=from;i<to;++i)  
  7     {  
  8         sum+=i;  
  9     }  
 10     return sum;  
 11 }  
 12 int main()  
 13 {  
 14     int num =addtotarget(1,100);  
 15     if(num==5050)  
 16     {  
 17         return 0;
 18     }
 19     else 
 20     {
 21         return 1;                                                                                                                   
 22     }                                                                                                                    
 23     return 0;                                                                                                            
 24 }  

因为我们知道从1加到100的结果是5050,所以通过if语句可以判断这里函数的执行结果是否正确,如果是对的话main函数就返回0,如果不对的话main就返回1,在linux里面有一个变量名为$? 这个变量永远记录着最近一个进程在命令行中执行完毕时所对应的退出码(即main函数中的return xxx),所以当一个进程执行完毕想看其执行的结果时就可以使用echo $?来进行查看比如说下面的图片,我们上面的代码写的是错的,所以这个进程运行结束的退出码就是1
在这里插入图片描述
但是大家再执行一下echo $?时就可以看到此时的退出码变成了0
在这里插入图片描述
之所以出现这样的原因是因为第一次查看的是myproc程序的退出码,而echo自己本身也是一个程序所以第二次查看退出码时就查看的是第一次执行的echo指令的退出码,因为这个指令运行的结果是对的所以这个第二次以及第三次echo $?打印的结果都是0,希望大家能偶理解,将上面代码的i<to改成i<=to然后再查看程序退出码时就可以看到这里打印出来的结果为0:
在这里插入图片描述
大家在写程序的时候如果不关心进程的退出码直接return 0就行,如果未来写的程序要关心退出码的话就得在程序里面返回特定的数据表明特定的错误,返回0表示当前程序的运行结果正确,如果返回!0就表示当前程序运行的结果错误,!0具体是几就表示着不同的错误。数字对人不友好,但是数字对计算机却非常的友好,所以一般而言退出码都必须有对应的退出码的文字描述,这个描述可以自定义也可以直接使用系统的映射关系。

如何查看不同退出码所对应的内容

在c语言中有个名为strerror的函数,这个函数的作用就是将不同的退出码转换成不同的文字信息
在这里插入图片描述
我们来看看下面的代码:

  1 #include<stdio.h>
  2 #include<string.h>
  3 int main()
  4 {
  5     for(int i=0;i<=200;++i)
  6     {
  7         printf("%d:%s\n",i,strerror(i));                                                                                            
  8     }                                                                                                                 
  9     return 0;                                                                                                         
 10 }   

这代码的运行结果如下:
在这里插入图片描述
我们可以看到这里显示了各种退出码所对应的文字描述信息,并且往后翻大家可以看到这里有些退出码没有对应的错误信息:
在这里插入图片描述
也就是说退出码的范围就是从0开始到133结束,并且我们使用一些指令故意输入一些错误信息再查看退出码时就可以发现指令报错的信息和指令运行所对应的退出码是一样的,比如说下面的操作:
在这里插入图片描述

如何退出进程

进程有三个退出情况:
1.代码跑完了,结果正确这时退出码为0。
2.代码跑完了,结果不正确这时退出码为非0.
3.代码没跑完了程序异常了,这时程序的退出码就没有任何意义。

进程有三个退出的方式:
1.在main函数里面通过return来结束进程
2.在任意地方调用exit函数来结束进程。
3.使用系统调用接口_exit函数来结束进程。

其中exit函数就是对系统函数_exit进行的封装,在exit函数内部调用_exit函数来实现的进程退出,这两个函数有个区别exit函数在退出进程的时候会刷新缓冲区,但是_exit函数不会刷新缓冲区 ,比如说下面的代码:

  1 #include<stdio.h>  
  2 #include<stdlib.h>                                                                                                                  
  3 int main()                                          
  4 {                                      
  5     printf("hello world");                                                 
  6     exit(1);                                                        
  7 }                                                    

这段代码执行的结果如下:
在这里插入图片描述
我们再来看看这段代码

  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 int main()  
  4 {  
  5     printf("hello world");                                                                                                          
  6     _exit(1);                           
  7 }         

这段代码的运行结果如下:
在这里插入图片描述
我们可以看到使用_exit函数结束程序时不会刷新缓冲区,那么这就是两个函数的区别并且exit函数和_exit函数是有参数的,这个参数就代表着进程退出时的退出码,而不同的退出码就对应着不同的信息希望大家能够理解。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶超凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值