【Linux】进程创建、进程终止、进程等待

Linux

1.进程创建

1.fork 函数

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

c 复制代码
#include <unistd.h>
pid_t fork(void);
//创建子进程成功:子进程中返回0,父进程返回子进程id。创建子进程失败:返回-1

进程调用 fork 函数,当控制转移到内核中的 fork 代码后,内核做:

  1. 为代码和数据分配新的内存块、分配内核数据结构 task_struct 给子进程。
  2. 将父进程部分数据结构内容拷贝给子进程(task_struct、虚拟地址空间、页表),task_struct 中个别的属性进行修改(pid、ppid)
  3. 添加子进程到系统进程列表当中。
  4. fork返回,开始调度器调度。

fork 函数用法:

  1. 父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成的子进程来处理请求。
  2. 一个进程要执行一个不同的程序。例如子进程从 fork 返回后,调用exec函数。

fork 函数调用失败的原因:

  1. 系统中有太多的进程,内存空间不足。
  2. 实际用户的进程数超过了限制。

2.写时拷贝

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

  1. 页表存在读写权限,进程的代码的虚拟地址经过页表映射,权限是只读的,代码不能修改。
  2. 如果父进程没有创建子进程时:数据段是拥有读写权限的。父进程一但创建了子进程时:操作系统将两进程的数据段的权限修改为只读,当其中一个进程进行修改数据时,操作系统查页表发现物理内存数据是只读的,"出错",满足拥有子进程,修改数据,触发 "写时拷贝"。

3.为什么要有写时拷贝?

  1. 因为有写时拷贝技术的存在,所以父子进程得以彻底分离,完成了进程独立性的技术保证。
  2. 写时拷贝,是一种延时申请技术(要修改数据时:再开辟空间,拷贝数据),减少创建进程时间,减少内存浪费。

2.进程终止

进程终止:释放系统资源,就是释放进程申请的相关内核数据结构和对应的代码和数据。

但是存在僵尸进程,子进程退出后,父进程退出前,子进程释放代码和数据,但是 task_struct 需要存放子进程的退出信息,不能释放,当父进程获取子进程的退出信息后,子进程释放 task_struct。

1.进程退出场景

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止。

注意:父进程创建子进程,子进程退出时,父进程要知道子进程执行的结果(正常/异常退出)

2.退出码

退出码(退出状态):可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。代码 非0 代码都被视为不成功。

Linux Shell 中的主要退出码:

3.进程常见退出方法

1.main函数return

  1. main 函数的返回值,通常表明你的程序的执行情况:代码运行完毕,结果正确,返回0;代码运行完毕,结果不正确,返回非0(不同的值,表明不同的出错原因)
  2. main 函数的返回值是进程退出时的 "退出码",main 函数退出时,该进程变成僵尸进程,退出码需要写到僵尸进程的 task_struct 中,父进程需要从僵尸进程的 task_struct 中获取退出码。

如何查看退出码?echo $?:打印最近一个程序(进程)的退出码。

获取退出码的具体含义:char* strerror(int errnum),头文件string.h

根据具体情况,返回真正的错误码:errno,头文件 errno.h



  1. 程序运行完毕后,结果是否正确,由该进程的退出码决定。0代表正确,非0代码错误。
  2. 一但一个进程出现了异常,退出码就无意义了,出现异常的进程之所以结束,是因为进程收到了信号!

2.exit库函数

c 复制代码
#include <stdlib.h>
void exit(int status); //status:退出状态,就是退出码
  1. 任何地方调用exit,表示进程结束,将函数中的退出码返回给父进程。
  2. main函数结束,表示进程结束。
  3. 其它函数结束,只表示该函数调用完成,返回。

3._exit系统调用

c 复制代码
#include <unistd.h>
void _exit(int status); //status:退出状态,就是退出码


二者的区别:

  1. 进程 exit 退出的时候,会进行缓冲区的刷新。
  2. 进程 _exit 退出的时候,不会进行缓冲区的刷新。
  3. exit 是 C 提供的库函数,_exit 是系统调用,由于操作系统是进程的管理者,能杀到进程的只有操作系统,_exit 能杀到进程,但是 exit 能杀掉进程的原因是内部调用了系统调用 _exit,只不过调用 _exit 之进行了 fflush 刷新缓冲区。
  4. 缓冲区是库级别(C语言提供)的缓冲区,而不是操作系统内部的缓冲区。

3.进程等待

1.概念

进程等待:父进程等待子进程退出,对子进程进行资源回收和获取子进程退出信息的过程。

2.必要性

  1. 当子进程退出,父进程如果不管不顾,就可能造成 "僵⼫进程" 的问题,进而造成内存泄漏。
  2. 进程一旦变成僵尸状态,即便是 "kill -9 进程id" 也无能为力,因为没有办法杀死⼀个已经死去的进程。
  3. 父进程派给子进程的任务完成的如何,父进程需要知道子进程的退出信息(例如:进程运行完成,结果对还是不对,或者是否正常退出)

结论:父进程通过进程等待的方式,回收子进程资源(必须回收 task_struct,防止内存泄漏),获取子进程退出信息(可选的)

以下的代码:子进程先退出,父进程没有获取子进程的退出信息,子进程处于僵尸状态,直到父进程退出,操作系统出手,对子进程进行资源回收。

c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程                                                
		int cnt = 3;
		while (cnt)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(1);
			cnt--;
		}
		exit(0); //子进程退出
	}
	
	//父进程
	sleep(5);

	return 0;
}
bash 复制代码
#循环查看进程proc
while : ; do ps axj | head -1 && ps axj | grep proc; sleep 1; done

3.方法

1.wait

c 复制代码
#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);
  1. 返回值:成功时:返回子进程的 pid。失败时:返回 -1
  2. 参数:输出型参数,通过 status 获取子进程的退出码(注意:status 不是退出码),不关心退出码时可以置为NULL
  3. 作用:等待任意一个子进程退出。子进程未退出时:父进程阻塞在 wait 调用处(类似scanf)。子进程退出时:父进程回收子进程的资源,获取子进程的退出码,wait 调用结束。
c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程                                                
		int cnt = 3;
		while (cnt)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(1);
			cnt--;
		}
		exit(0); //子进程退出
	}
	
	//父进程
	pid_t rid = wait(NULL); //父进程一直等待子进程退出,当子进程退出时,父进程开始执行后面的代码
	if(rid > 0)
	{
		printf("wait success! rid:%d\n", rid);
	}
	sleep(5);

	return 0;
}

2.waitpid

c 复制代码
#include<sys/types.h>
#include<sys/wait.h>

pid_ t waitpid(pid_t pid, int* status, int options);
  1. pid 参数:pid = -1 时:等待任意一个进程,与wait等效。pid > 0 时:等待其进程 id 与 pid 相等的子进程。
  2. 返回值:成功时:返回子进程的 pid。失败时:返回 -1,表示调用进程没有子进程。
  3. 作用:等待任意/指定子进程退出。子进程未退出时:父进程阻塞在 wait 调用处(类似scanf)。子进程退出时:父进程回收子进程的资源,获取子进程的退出码,wait 调用结束。
c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程                                                
		int cnt = 3;
		while (cnt)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(1);
			cnt--;
		}
		exit(0); //子进程退出
	}
	
	//父进程
	//pid_t rid = waitpid(-1, NULL, 0); 与wait效果相同
	pid_t rid = waitpid(id, NULL, 0); //等待指定pid的进程
	if(rid > 0)
	{
		printf("wait success! rid:%d\n", rid);
	}
	sleep(5);

	return 0;
}

等待失败的案例:

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

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程                                                
		int cnt = 3;
		while (cnt)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(1);
			cnt--;
		}
		exit(0); //子进程退出
	}
	
	//父进程
	pid_t rid = waitpid(id + 1, NULL, 0); //该子进程不存在,返回-1
	if(rid > 0)
	{
		printf("wait success! rid:%d\n", rid);
	}
	else
	{
		printf("wait fail! error:%s\n", strerror(errno));
	}
	sleep(5);

	return 0;
}

父进程获取子进程的退出信息(退出码)的案例:

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

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程                                                
		int cnt = 3;
		while (cnt)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(1);
			cnt--;
		}
		exit(1); //子进程退出:返回退出码1
	}
	
	//父进程
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if(rid > 0)
	{
		printf("wait success! rid:%d, status:%d\n", rid, status); 
	}
	else
	{
		printf("wait fail! error:%s\n", strerror(errno));
	}
	sleep(5);

	return 0;
}

3.参数status

  1. wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  2. 如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  3. status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低16 比特位)
  1. 第 8 ~ 15 比特位表示退出状态(退出码),第 7 个比特位是core dump标志位(不考虑),第 0 ~ 6 比特位表示进程退出时的退出信号。
  2. 进程正常终止时:退出码有效,退出信号默认为0000000。
  3. 进程异常终止时:退出码无意义,退出信号有效。
  4. 获取退出码:(status >> 8) & 0xFF、获取退出信号:status & 0x7F
  5. 若定义一个全局变量 exit_code 获取子进程的退出码,可行吗?答案是:不行。因为父子进程时独立的,父进程无法获取子进程的退出码,因为子进程修改 exit_code,发生 "写时拷贝"。
  6. 子进程的退出信息存在哪里?答案是:子进程退出时,释放代码和数据,但是要保留 task_struct 来存储退出信息(exit_code、exit_signal),父进程通过系统调用(wait、waitpid)+ 定义输出型参数 status,来获取子进程的退出信息保存到 status 中。getpid 和 getppid 也类似。

较为麻烦,系统提供了宏来获取,WIFEXITED(status):若子进程正常终止返回真,异常终止返回假,WEXITSTATUS(status):若WIFEXITED非零,获取子进程退出码。但未提供获取退出信号的宏。

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

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程                                                
		int cnt = 3;
		while (cnt)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(1);
			cnt--;
		}
		exit(1); //子进程退出:返回退出码1
	}
	
	//父进程
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if(rid > 0)
	{
		if(WIFEXITED(status))
			printf("wait success! rid:%d, exit_code:%d, exit_signal:%d\n", rid, WEXITSTATUS(status), status & 0x7f);
		else
			printf("子进程退出异常"); 
	}
	else
	{
		printf("wait fail! error:%s\n", strerror(errno));
	}
	sleep(5);

	return 0;
}

4.参数option

  1. 当 option 为0时:父进程一直等待子进程退出,父进程阻塞在 wait/waitpid 处,表示阻塞调用(例如scanf)
  2. 当 option 为 WNOHANG 时:若 pid 指定的子进程没有结束,则 waitpid() 函数返回0,不予以等待。若正常结束,则返回该子进程的 id。WNOHANG表示不要夯住,表示非阻塞调用(Non Block)
  3. 非阻塞轮询:以循环的方式,不断进行非阻塞调用,就是非阻塞轮询。

5.非阻塞轮询

以下是非阻塞轮询的代码:

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

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程
		while (1)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(3);
		}
		exit(10);
	}

	while (1)
	{
		int status = 0;
		pid_t rid = waitpid(id, &status, WNOHANG);
		if (rid > 0) 
		{
			printf("wait success! rid:%d, exit_code:%d, exit_signal:%d\n", rid, WEXITSTATUS(status), status & 0x7f);
			break;
		}
		else if (rid == 0)
		{
			printf("本轮非阻塞调用结束,子进程没有退出\n");
			sleep(1);
		}
		else
		{
			printf("等待失败\n");
			break;
		}
	}

	return 0;
}

在等待子进程退出时,父进程可以通过非阻塞轮询做自己的事情,效率比阻塞调用高一些,代码如下:

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

//函数指针类型
typedef void (*func_t)();

#define NUM 5
func_t handlers[NUM];

//如下是任务
void DownLoad()
{
	printf("我是一个下载的任务...\n");
}
void Flush()
{
	printf("我是一个刷新的任务...\n");
}
void Log()
{
	printf("我是一个记录日志的任务...\n");
}

//注册
void registerHandler(func_t h[], func_t f)
{
	int i = 0;
	for (; i < NUM; i++)
	{
		if (h[i] == NULL) break;
	}
	if (i == NUM) return;
	h[i] = f;
	h[i + 1] = NULL;
}

int main()
{
	//注册任务
	registerHandler(handlers, DownLoad);
	registerHandler(handlers, Flush);
	registerHandler(handlers, Log);

	pid_t id = fork();
	if (id == 0)
	{
		//子进程
		while (1)
		{
			printf("我是子进程。pid:%d, ppid:%d\n", getpid(), getppid());
			sleep(3);
		}
		exit(10);
	}

	while (1)
	{
		int status = 0;
		pid_t rid = waitpid(id, &status, WNOHANG);
		if (rid > 0) 
		{
			printf("wait success! rid:%d, exit_code:%d, exit_signal:%d\n", rid, WEXITSTATUS(status), status & 0x7f);
			break;
		}
		else if (rid == 0)
		{
			//边等待子进程退出,父进程做自己的事情:采用函数指针进行回调
			int i = 0;
			for (; handlers[i]; i++)
			{
				handlers[i]();
			}

			printf("本轮非阻塞调用结束,子进程没有退出\n");
			sleep(1);
		}
		else
		{
			printf("等待失败\n");
			break;
		}
	}

	return 0;
}

多进程代码:fork,exit、wait 缺一不可。

相关推荐
云手机管家20 分钟前
CDN加速对云手机延迟的影响
运维·服务器·网络·容器·智能手机·矩阵·自动化
云手机管家22 分钟前
账号风控突破:云手机设备指纹篡改检测与反制技术解析
android·运维·网络协议·网络安全·智能手机·矩阵·自动化
丢掉幻想准备斗争30 分钟前
Linux-进程概念(一)
linux
孤的心了不冷31 分钟前
【Docker】CentOS 8.2 安装Docker教程
linux·运维·docker·容器·eureka·centos
千里马-horse36 分钟前
Detected for tasks ‘compileDebugJavaWithJavac‘ (17) and ‘kspDebugKotlin‘ (21).
android·jdk·kspdebugkotlin
淡水猫.2 小时前
hbit资产收集工具Docker(笔记版)
运维·docker·容器
柯南二号2 小时前
【Android】Android 实现一个依赖注入的注解
android
.生产的驴3 小时前
Vue3 加快页面加载速度 使用CDN外部库的加载 提升页面打开速度 服务器分发
运维·服务器·前端·vue.js·分布式·前端框架·vue
程序员JerrySUN3 小时前
Linux 内核核心知识热点题分析:10 个连环打通的难点
linux·运维·服务器
R_.L4 小时前
Linux : 线程【同步与互斥】
linux