【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 缺一不可。

相关推荐
王闯写bug25 分钟前
linux一次启动多个jar包
linux·jar
脑子慢且灵29 分钟前
计算机操作系统——存储器管理
linux·服务器·windows·操作系统·存储器·存储器多级结构
吃饭了呀呀呀39 分钟前
🐳 《Android》 安卓开发教程 - 三级地区联动
android·java·后端
小码过河.1 小时前
CentOS 安装 Docker
linux·docker·centos
HappyGame022 小时前
Linux命令-vim编辑
linux·vim
笑远2 小时前
Vim/Vi 常用命令速查手册
linux·编辑器·vim
撒旦骑路西法,大战吕布2 小时前
如果你在使用 Ubuntu/Debian:使用 apt 安装 OpenSSH
linux·ubuntu·debian
_祝你今天愉快2 小时前
深入剖析Java中ThreadLocal原理
android
_李筱夜2 小时前
ubuntu桌面版使用root账号进行登录
linux·ubuntu
张力尹2 小时前
谈谈 kotlin 和 java 中的锁!你是不是在协程中使用 synchronized?
android