Linux 进程控制(进程创建,进程等待)

目录

进程创建

fork函数初识

fork函数返回值

写时拷贝

fork常规用法

fork调用失败的原因

进程终止

进程退出场景

进程退出码

进程常见退出方法

exit函数

_exit函数

return退出

return、exit和_exit之间的区别与联系

进程异常退出

进程等待

进程等待的必要性

获取子进程status

进程等待的方法

wait方法

waitpid方法

多进程创建以及等待的代码模型

非阻塞轮询


进程创建

fork函数初识

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

返回值:

子进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做: 分配新的内存块和内核数据结构给子进程 将父进程部分数据结构内容拷贝至子进程 添加子进程到系统进程列表当中 fork返回,开始调度器调度

fork之后,父子进程代码共享

运行之后:

我们可以观察到fork之前的代码执行了一次,而fork之后的代码执行了两次,其中Before是由父进程打印的,而调用fork函数之后打印的两个After,则分别由父进程和子进程两个进程执行。也就是说,fork之前父进程独立执行,而fork之后父子进程两个执行流分别执行

**注意:**fork之后,父进程和子进程谁先执行完全是由调度器决定

fork函数返回值

fork函数为什么要给子进程返回0,给父进程返回子进程的pid?

一个父进程可以创建很多个子进程,而一个子进程只能有一个父进程。因此对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的pid才能很好的对子进程指派任务

为什么fork有两个返回值?

父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块(PCB - task_struct),创建子进程的进程地址空间,创建子进程对应的页表等等。子进程创建完毕之后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了

也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也同样需要执行,这就是fork有两个返回值的原因

写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

1.为什么数据要进行写时拷贝?

进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程

2.为什么不创建子进程的时候就进行数据的拷贝?

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延迟分配),这样可以高效的使用内存空间

3.代码会不会进行写时拷贝?

90%的情况下不会的,但着并不代表代码不能进行写时拷贝,例如再进行进程替换的时候,则需要进行代码的写时拷贝

fork常规用法

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。

2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

fork函数创建子进程也可能会失败,有以下两种情况:

1.系统中有太多的进程

2.实际用户的进程数超过了限制

进程终止

进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

进程退出码

进程退出码

我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2022当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。

既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。

当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。

例如,对于下面这个简单的代码:

该进程结束之后我们可以查看该进程的退出码:

使用echo $?指令

这时,我们就可以确定以上代码顺序执行

为什么以0表示代码执行成功,以非0表示代码执行错误?

因为成功只有一种情况,成功了就算成功了,而失败有很多种情况,例如:野指针问题,除0错误,栈溢出,越界访问,内存空间不足等原因

c语言中的strerror函数可以通过错误码来获取该错误码对应的错误信息:

运行之后,我们就可以得到错误码所对应的错误信息

实际上再Linux中的 pwd ,ls指令都是可执行程序,在其执行完毕之后也会有退出码

顺序运行之后退出码为0

但是,如果我们使用的是错误的指令,它会返回非0的错误码

进程常见退出方法

exit函数

  1. 执行用户通过 atexit或on_exit定义的清理函数。

  2. 关闭所有打开的流,所有的缓存数据均被写入(刷新缓冲区)

  3. 调用**_exit**终止进程

exit在退出进程时,会先将缓冲区的数据输出,在终止进程

运行之后,我们可以看到,缓冲区的数据输入到了缓冲区上

_exit函数

尽量不要去使用这个接口,它可以在程序的任何地方使用,使用时会直接终止掉程序,并不会再终止程序前做任何收尾工作

以下代码在使用_exit终止程序,缓冲区的数据不会被刷新出来

代码运行之后:

通过观察可以发现缓冲区的数据并没有刷新出来

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。

return、exit和_exit之间的区别与联系

return、exit和_exit之间的区别

只有在main函数中return 才能起到退出进程的作用,在其它子函数中只会退出该函数并不会退出进程,exit和_exit可以在代码中任何地方使用,都有退出进程的作用

使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

return、exit和_exit之间的联系

执行return n等同于执行exit(n),因为在main函数运行结束之后,return的返回值会当作exit函数的参数,来调用exit函数

使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。

进程异常退出

情况一:向进程发出信号导致进程异常退出

例如:对一个进程使用 kill -9 pid 或者 使用ctrl+C使进程异常退出

情况二:代码错误导致进程运行时异常退出

例如:代码执行时遇到野指针或者遇到除0错误时,进程异常退出

进程等待

进程等待的必要性

1.子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。

2.进程一旦变成僵尸状态,连kill -9 也无法杀掉该进程,因为谁也没有办法杀死一个已经死去的进程。

3.父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。

4.父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

获取子进程status

下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充

如果对status这个参数传入NULL,则表示不关心子进程的退出状态信息,否则操作系统会通过该参数将子进程的退出信息反馈给父进程

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

在status低十六位中,高八位表示进程的退出状态(退出码),

进程若是被信号所杀,则低7位表示终止信号,而第八位比特位是core dump 标记

我们可以通过一系列位操作,就可以根据status得到进程的退出码和退出信息

cpp 复制代码
exitCode = (status >> 8) & 0xff; //退出码
exitSignal = status & 0x7f; //退出信号

对于次系统还提供了两个宏来获取退出码和退出信号

WIFEXITED(status):用于查看进程是否正常退出,本质是检查是否收到信号

WEXITSTATUS(status):用于获取进程的退出码

cpp 复制代码
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //获取退出码

注意:当一个进程非正常退出时,说明该进程是被信号所杀死,那么该进程的退出码也就没意义了

进程等待的方法

wait方法

函数原型: pid_t wait(int* status);

作用:等待任意子进程

返回值:等待成功返回被等待进程的pid,等待失败返回-1

参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL

例如:创建子进程之后,父进程可使用wait函数一直等待子进程,直到子进程退出后读取子进程的退出信息

cpp 复制代码
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <sys/types.h>
  5 #include <sys/wait.h>
  6 
  7 int main()
  8 {
  9         pid_t id = fork();
 10         if(id == 0) //child
 11         {
 12                 int cnt = 10;
 13                 while(cnt)
 14                 {
 15                         printf("I am child!. pid: %d, ppid: %d\n", getpid(), getppid())    ;
 16                         sleep(1);
 17                         cnt--;
 18                 }
 19                 exit(0);
 20         }
 21 
 22         //parent
 23         int status = 0;
 24         pid_t ret = wait(&status);
 25 
 26         if(ret > 0)
 27         {
 28                 printf("wait child success...\n");
 29                 if(WIFEXITED(status)) //exit normal
 30                 {       
 31                         printf("exit code: %d", WEXITSTATUS(status));
 32                 }
 33         }
 34         
 35         sleep(3);
 36         return 0;
 37 }

我们可以使用监控脚本来对本进程进行实时监控:

cpp 复制代码
while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; echo "###############"; sleep 1; done
 

这时我们可以看到,当子进程退出之后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了

waitpid方法

函数原型: 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.option:当设置WNOHANG|时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待,若正常结束,则返回该子进程的pid

例如:创建子进程之后,父进程可以使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息

cpp 复制代码
1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <sys/types.h>
  5 #include <sys/wait.h>
  6 
  7 int main()
  8 {
  9         pid_t id = fork();
 10         if(id == 0) //child
 11         {
 12                 int count = 10;
 13                 while(count)
 14                 {
 15                         printf("I am child..., pid: %d, ppid: %d\n", getpid(), getppid());
 16                         sleep(1);
 17                         count--;
 18                 }
 19                 exit(0);
 20         }
 21 
 22         //parent
 23         int status = 0;
 24         pid_t ret = waitpid(id, &status, 0);
 25         if(ret > 0)
 26         {
 27                 printf("Wait success....\n");
 28                 //wait success
 29                 if(WIFEXITED(status))
 30                 {
 31                        //exit normal
 32                         printf("exit code: %d\n", WEXITSTATUS(status));
 33                 }
 34                 else
 35                 {
 36                         //signal kill
 37                         printf("signal kill: %d\n", status&0x7F);
 38                 }
 39         }
 40 
 41         sleep(3);
 42 
 43         return 0;
 44 }

在父进程运行的过程中,我们可以尝试使用kill -9命令将子进程杀死,这时父进程也能等待子进程成功

**注意:**被信号杀死而退出的进程,其退出码将没有意义

多进程创建以及等待的代码模型

实际上我们还可以同时创建多个子进程,然后让父进程依次等待子进程退出,这叫做多进程创建以及等待的代码模型

例如以下代码中同时创建了10个子进程,同时将子进程这些子进程的pid放入到数组中

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>  
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{ 

	int ids[10] = {0};
	for(int i = 0; i < 10; ++i)
	{
		pid_t id  = fork();
		if(id == 0) //child
		{
			printf("child create success...:%d pid: %d, ppid: %d\n", i, getpid(), getppid());
			//sleep(1);
			exit(i);
		}
		//parent
		sleep(1);
		ids[i] = id;
	}

	int status = 0;
	for(int i = 0; i < 10; ++i)
	{
		pid_t ret = waitpid(ids[i], &status, 0);
		if(ret >= 0)
		{
			printf("wait success...\n");
			if(WIFEXITED(status))
			{
				//exit normal
				printf("exit code: %d\n", WEXITSTATUS(status));
				sleep(1);
			}
			else
			{
				//signal kill
				printf("exit signal: %d\n", status&0x7F);
				sleep(1);
			}
		}
	}

	return 0;
}

运行如下:

非阻塞轮询

在上述的例子中,当子进程未退出时,父进程都在一直等待子进程的退出,在等待期间,在等待期间父进程没有做任何事情,这种父进程处于阻塞状态下的等待叫做阻塞等待

实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未推出时父进程苦于做一些自己的事情,当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即:非阻塞等待

向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回子进程的pid

例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以去做一些其他事,过一段时间在调用waitpid函数读取子进程的退出信息

cpp 复制代码
int main()
{
	pid_t id = fork();
	//child
	if(id == 0)
	{
		int cnt = 3;
		while(cnt--)
		{
			printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(3);
		}
		exit(0);
	}
	
	//parent
	while(1)
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, WNOHANG);
		if(ret > 0)
		{
			printf("wait child success...\n");
			printf("exit code:%d\n", WEXITSTATUS(status));
			break;
		}
		else if(ret == 0)
		{
			printf("parent do other things...\n");
			sleep(1);
		}
		else
		{
			printf("waitpid error...\n");
			break;
		}
	}
	return 0;
}

运行结果:父进程每隔一段时间进去查看子进程是否退出,若为退出则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出之后读取子进程的退出信息

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式