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;
}

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

相关推荐
skywalk816317 分钟前
体验智谱清言的AutoGLM进行自动化的操作(Chrome插件)
运维·人工智能·自动化·glm·autoglm
一杯敬朝阳 一杯敬月光1 小时前
WIN11 企业版 部署Dify+Docker
运维·docker·容器
犽戾武1 小时前
浅谈多个虚拟机(WSL和VMWare)的网络配置
linux·网络
故事与他6451 小时前
TBKDVR硬盘录像机device.rsp命令执行漏洞
服务器·网络·数据库·安全·网络安全·apache
最后一个bug1 小时前
教你快速理解linux中的NUMA节点探测是干什么用的?
linux·c语言·开发语言·arm开发·嵌入式硬件
awei09161 小时前
Linux系统安装RabbitMQ
linux·运维·rabbitmq·ruby
linux kernel2 小时前
第八部分:进程创建退出等待和替换
linux·运维·服务器
awei09162 小时前
Jenkins服务器报磁盘空间不足的问题解决方案
linux·运维·jenkins
dessler2 小时前
Kubernetes(k8s)-日志(logs)和exec内部逻辑
linux·运维·kubernetes
山山而川粤2 小时前
SSM考研信息查询系统
java·大数据·运维·服务器·开发语言·数据库·考研