Linux程序设计—多进程编程

文章目录

  • 1、进程
    • 1.1、创建进程
      • 1.1.1、fork()
      • 1.1.2、vfork()
    • 1.2、执行进程——exec函数族
    • 1.3、进程退出
      • 1.3.1、exit()和_exit()
    • 1.4、进程回收
      • 1.4.1、僵尸进程
      • 1.4.2、wait()
      • 1.4.3、waitpid()
  • 2、写在最后

1、进程

进程的定义:
进程是程序处于一个执行环境中在一个数据集上的一次运行过程,它是系统进行资源分配和调度的一个独立单位。每个进程都有自己独立的系统资源,一个进程中可以有多个线程,系统是系统资源分配的基本单位。

1.1、创建进程

整个Linux操作系统都是由父子进程结构组成,每个进程都有创建者,也就是父进程,但是有一个进程例外,也就是init进程,其为系统启动初始化后执行的第一个进程。

1.1.1、fork()

函数原型:

#include 
pid_t fork(void); //pid_t等价于有符号整型

主要作用: 创建一个子进程,这也就代表着,父进程可通过调用该函数创建一个子进程,父子进程各自独立,拥有自己的PCB,内存用户区,临时资源等,各自独立参与CPU调度
返回值:pid_t类型的变量,一共有两个返回值(父进程返回一个,子进程返回一个)

细节探究:
fork函数执行的流程:(1)调用_CREATE函数,也就是进程创建部分,子进程进行虚拟地址申请,在子进程的内核空间进行不完全拷贝(2)调用_CLONE函数,向父进程拷贝必要资源,子进程的用户空间进行完全拷贝,子进程继承所有父进程资源,如临时堆栈拷贝,代码完全拷贝(3)子进程执行fork函数剩余部分,执行最后这个语句,fork函数就会有二次返回,如果成功返回0,不成功返回1。不成功的主要原因有:
a.系统内存不够 b.进程表满(容量一般为200~400)c.用户的子进程太多(一般不超过25个)

所以fork函数的返回值情况如下:
父进程调用fork(),返回子进程pid(>0)
子进程调用fork(),子进程返回0,调用失败的话就返回-1
这也就说明了fork函数的返回值是2个

fork的应用场景:一个父进程希望复制自己,使父进程和子进程执行不同的代码段。在网络编程中常用到fork函数,例如:父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。示例程序如下所示(仅展示关键代码):

void test_fork()
{
	pid_t pid;
	int data;

	while(1){
   		printf("please input command number:");
		scanf("%d", &data);

		if(1 == data){
			pid = fork();
			if(pid > 0){

			}else if(0 == pid){
				while(1){
					printf("do net require, pid = %d\n", getpid()); //模拟子进程处理请求
					sleep(3);
				}
			}else{
				perror("create process failed\n"); //创建进程失败
				exit(-1);
			}
		}
		else{
			printf("isn't an excepted number\n");
		}
	}
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第1张图片
父进程执行时一直在等待用户输入数据,当用户输入1时,创建子进程(pid为1785),并建立网络链接,父进程则继续等待用户输入下一个数据,并根据输入的具体值完成指定的操作。

总结:父子进程都执行fork函数,但执行不同的代码段,获取不同的返回值。创建出来的子进程在继承了所有的父进程资源后会从代码的fork()处继续向下执行,该特性可被如下的程序段所验证:

#include 
#include 
#include 

int main()
{
	pid_t pid;
	printf("aaaaaa process pid = %d\n", getpid());
	pid = fork();
	printf("bbbbbb process pid = %d\n", getpid());
	while(1)
	{
		sleep(1);
	}
	return 0;
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第2张图片

不难看出,父进程执行的代码段打印了"aaaaaa"和"bbbbbb"这两串字符串,而子进程仅打印了"bbbbbb"这一串字符串,即可验证上文所述。

1.1.2、vfork()

函数原型:

#include 
pid_t vfork(void); //pid_t等价于有符号整型

主要作用: 创建一个子进程,作用与fork函数一致

细节探究:
vfork函数的调用序列和返回值与fork相同,但二者的语义不同
(1)vfork直接使用父进程存储空间,不拷贝(可将存储空间理解为联合体,每个成员均为vfork函数创建的子进程,所有子进程共用同一段内存)
(2)vfork保证子进程先运行,在子进程调用exitexec函数族之后父进程才会被调度执行,如果在子进程退出之前子进程依赖父进程的进一步动作,则会导致死锁。下文程序段可验证vfork函数的这一特性(仅展示关键代码):

void test_vfork()
{
	pid_t pid;
	int cnt = 0;
    
	pid = vfork();
	if(pid > 0){
		while(1){
			printf("i am a parent process\n");
			sleep(1);
		}
	}else if(0 == pid){
		while(1){
			printf("i am a child process\n");
			cnt++;
			sleep(1); //在子进程占用一段运行时间后,主动结束子进程
			if(3 == cnt){
				cnt = 0;
				exit(0);
			}
		}
	}else{
		perror("create process failed\n");
		exit(-1);
	}
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第3张图片

父进程调用vfork函数创建子进程,先运行子进程,在合适的条件下子进程退出后父进程得以执行。

1.2、执行进程——exec函数族

函数原型: Linux下的exec函数族是6个以exec开头的函数,具体如下:

#include 

int execl(const char *path, const char *arg, ...); //arg...传递给执行的程序的参数列表
int execv(const char *path, char *const argv[]); //arg...封装成指针数组的形式传递
int execle(const char *path, const char *arg, ..., char *const envp[]); //使用默认的环境变量(environment),在envp[]中指定当前进程所使用的环境变量
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);

主要作用: 通过fork()或者vfork()函数创建子进程后,子进程几乎复制了父进程的全部内容,如果我们想要父子进程执行的内容不同,可以通过exec函数族实现。exec函数族提供了一个在进程中执行另一个程序的方法,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代当前进程的数据段,代码段和堆栈段。在执行完后,当前进程除了进程号外,其它内容都被替换。

细节探究:
日常开发中,exec函数族最常被用到的函数是:

int execl(const char *path, const char *arg, ...); 

execl的示例程序如下所示(仅展示关键代码):

func.c

void test_exec()
{
	pid_t pid;
    pid = fork();

	if(pid > 0){
		printf("i am a parent process\n");
	}else if(0 == pid){
		printf("i am a child process\n");
		if(execl("/home/pi/project/process_1/subprocedure", "subprocedure", NULL) < 0){
			perror("execl failed\n");
			exit(1); //exec调用失败,主动结束子进程
		}
	}else{
		perror("create process failed\n");
		exit(-1);
	}
	printf("end mark point\n");
}

subproc.c

#include 
#include 
#include 

int main()
{
	int cnt = 0;
	while(cnt < 3){
		printf("child process execute subprocedure\n");
		cnt++;
		sleep(3);
	}
	exit(0);
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第4张图片

可以发现,父进程剩下的部分,子进程并没有执行。子进程在调用exec函数族后,就只执行subproc.c编译出来的可执行文件subprocedure。通过exec函数族实现了父子进程执行不同的内容。

特别注意:
(1)确保形参格式正确:如果exec函数族的第一个形参为path,则必须以“/”开头,否则将视其为文件名;path需要为完整的文件目录路径,即被执行的文件也需要被包含在path中。如果第一个形参为file则会自动在path中搜索
(2)要判断exec是否执行成功,如果执行失败应结束该子进程。一种执行失败的情况如下所示:
Linux程序设计—多进程编程_第5张图片

这种情况是path路径的末尾未包含可执行文件所导致,exec执行失败,结束子进程。

1.3、进程退出

进程常见的退出方法主要有以下三种:
(1)main函数中调用return实现进程退出,main函数对应的进程的状态信息将会自动被系统中的特定进程所回收
(2)ctrl+c中断正在运行的进程(信号机制)
(3)任何进程调用exit()_exit()实现进程退出

1.3.1、exit()和_exit()

函数原型:

#include 
void exit(int status);

#include 
void _exit(int status);

主要作用: 两个函数功能和用法是一样的,都能结束调用此函数的进程
参数:status为进程退出时的一个状态信息,ANSIC标准要求使用值0或宏EXIT_SUCCESS指示程序正常终止,使用值1或宏EXIT_FAILURE指示程序异常终止

细节探究:
exit()_exit()最主要的区别有以下几点:
(1)exit()属于标准库函数(标准c库中的函数),_exit()属于系统调用函数(Linux系统中的函数)。使用时,两个函数所包含的头文件不一样
(2)exit()在执行时,系统会检测进程打开文件情况,并将处于文件缓冲区的内容写入到文件当中再退出。而_exit()则直接退出,不会将缓冲区的内容写入文件
Linux程序设计—多进程编程_第6张图片
下文给出的程序段可以验证exit()_exit()之间的区别:
测试函数test()

#include 
#include 
#include 

int main()
{
	printf("test begin\n");
	printf("output string buffer");
	exit(0); //在main函数中等价于return 0
	printf("test end\n");
	//return 0;
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第7张图片

测试函数_test()

#include 
#include 
#include 

int main()
{
	printf("test begin\n");
	printf("output string buffer");
	_exit(0); //在main函数中等价于return 0
	printf("test end\n");
	//return 0;
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第8张图片
printf()使用缓存I/O的方式,该函数在遇到'\n时自动从缓冲区中将记录读出。而exit()也能将文件缓冲区的内容写入到文件中再退出。所以不难看出两个运行结果的差异:执行exit()代码段的第二行信息能被打印,而执行_exit()代码段的第二行信息不能被打印。如果第一行输出也没有格式化控制符\n,则_exit()也不会将其打印出来。

1.4、进程回收

1.4.1、僵尸进程

僵尸进程简介: 在Linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程。即父进程永远也无法预测子进程到底什么时候结束。当一个子进程完成它的工作退出之后,它的父进程需要采用合适的手段对子进程实现资源回收,否则就会产生僵尸(Zombie)进程。

僵尸进程危害:
(1)僵尸进程会造成一定的资源浪费,占用不必要的资源。任何一个子进程(init除外)在退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然会保留一些信息(包括进程号,退出状态,运行时间等),称为僵尸进程的数据结构。
(2)当进程id达到了最大值的时候,因为有僵尸进程占用了部分进程id,使得无法再打开新的进程。

僵尸进程产生示例:

void test_zombie()
{
	pid_t pid;
	pid = fork();

	if(pid > 0){
		sleep(1); //延时片刻,保证子进程先运行
		printf("i am a parent process\n");
		while(1); //父进程保持不退出
	}else if(0 == pid){
		printf("i am a child process\n"); //子进程直接退出
	}else{
		perror("create process failed\n");
		exit(-1);
	}
	printf("end mark point\n");
}

执行该段代码编译生成的可执行文件,并另一个终端使用指令ps -ajx查看,可得到如下结果:
在这里插入图片描述
进程状态为ZZ+的进程即为僵尸进程

僵尸进程避免:
实际开发中有多种手段避免僵尸进程的出现,如子进程退出时向父进程发生SIGCHILD信号,多次fork(),将子进程变成孤儿进程等。下文着重讲解的,也是最常用的一种方法,是调用wait()/waitpid()函数,使子进程退出时被父进程回收。

1.4.2、wait()

函数原型:

#include 
#include 

pid_t wait(int *status);

主要作用: 父进程调用wait()阻塞等待子进程退出,并回收子进程的状态信息
参数: *status指向的整型对象来保存子进程结束时的状态,即exit()的形参值。此外,子进程的结束状态也可以有一些特定的宏来测定
返回值: 若成功回收子进程则返回子进程的进程号,失败则返回-1

细节探究:
使用wait()回收进程时应该注意以下几点:
(1)如果子进程没有结束,则父进程会阻塞等待,无法向前推进,直到子进程结束。如果父进程占用某个临界资源,则容易出现死锁现象
(2)wait()一次只能回收一个子进程,如果创建了多个子进程,则哪个子进程先结束就先被回收
(3)形参*status可以为NULL,表示直接释放子进程PCB,不接收返回值

wait()的示例程序如下所示(仅展示关键代码):

void test_wait()
{
	pid_t pid;
	pid = fork();

	if(pid > 0){
		pid_t retval;
		int status;
		printf("parent process is waiting...\n");
		retval = wait(&status); //阻塞等待子进程退出
		printf("parent process wait done, retval = %d status = %d\n", retval, status); //打印已回收子进程的进程号和结束状态
	}else if(0 == pid){
		int cnt = 0;
		while(cnt < 3){
			cnt++;
			printf("child process is running\n");
			sleep(3);
		}
		exit(0); //子进程退出
	}else{
		perror("create process failed\n");
		exit(-1);
	}
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第9张图片
父进程创建子进程之后阻塞等待子进程的退出,当子进程退出后父进程将回收子进程的状态信息。

1.4.3、waitpid()

函数原型:

#include 
#include 

pid_t waitpid(pid_t pid, int *status, int option);

主要作用:wait一致,等待子进程退出并回收子进程的状态信息
参数:
pid:主要有两种情况,pid = -1表示回收任何一个子进程,pid > 0表示回收对应pid的任一子进程
status:与wait()中的参数status作用一致
option:指定回收方式,常见的为0(阻塞等待子进程结束)或WNOHANG(非阻塞等待)
返回值: 若成功回收子进程则返回子进程的进程号,失败则返回-1,返回0表示optionWNOHANG且没有子进程退出

细节探究:
waitpid()的作用和wait()一样,但它并不一定等待第一个结束的子进程。waitpid()提供了若干选项,可以实现非阻塞的进程回收功能。事实上在Linux内部实现wait()时直接调用的就是waitpid(),可以将wait()理解成waitpid()的一个特例。下文代码展示了两个函数之间的等效关系:

//retval = wait(&status);
retval = waitpid(-1, &status, 0); //阻塞等待子进程退出

waitpid()去跑上文wait()示例程序可得到一样的结果,如下图所示:
Linux程序设计—多进程编程_第10张图片
下文给出的程序段是验证waitpid()的非阻塞回收功能(仅展示关键代码):

void test_waitpid()
{
	pid_t pid;
	pid = fork();

	if(pid > 0){
		sleep(1); //睡眠一段时间,确保子进程先运行
		pid_t retval;
		int status;

		printf("parent process is waiting...\n");
		if((retval = waitpid(-1, &status, WNOHANG)) == 0){
			printf("parent process didn't wait child process exit\n"); //非阻塞等待子进程退出,回收成功
		}else{
		    printf("parent process wait done, retval = %d status = %d\n", retval, status); //回收失败
		}
		while(1); //父进程不能退出,否则子进程会成为孤儿进程,由init进程完成状态收集工作
	}else if(0 == pid){
		int cnt = 0;
		while(cnt < 4){
			cnt++;
			printf("child process is running\n");
			sleep(2);
		}
		exit(0);
	}else{
		perror("create process failed\n");
		exit(-1);
	}
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第11张图片
可以发现,父进程未能成功回收子进程,子进程在退出后成为僵尸进程,在另一个Linux终端输入命令ps -ajx可观察到这一僵尸进程。
在这里插入图片描述
若想以非阻塞方式成功回收进程,可以定期执行waitpid(),直到成功回收子进程的状态信息。示例程序如下所示(仅展示关键代码):

void test_waitpid()
{
	pid_t pid;
	pid = fork();

	if(pid > 0){
		pid_t retval;
		int status;

		printf("parent process is waiting...\n");
		while((retval = waitpid(-1, &status, WNOHANG)) == 0){
			sleep(2); //非阻塞等待子进程退出,如果未退出则睡眠一段时间
		}
		printf("parent process wait done, retval = %d status = %d\n", retval, status);
	}else if(0 == pid){
		int cnt = 0;
		while(cnt < 4){
			cnt++;
			printf("child process is running\n");
			sleep(2);
		}
		exit(0);
	}else{
		perror("create process failed\n");
		exit(-1);
	}
}

执行该段代码编译生成的可执行文件可得到如下结果:
Linux程序设计—多进程编程_第12张图片
父进程以一定的时间间隔非阻塞等待子进程退出,当子进程退出后成功回收其状态信息。

2、写在最后

Linux程序设计是一门很深的学问,由于时间关系,还有很多我想补充的内容都未能放在这上面。即将开启一段新的旅程了,日后有时间还会不断地去完善,祝好运!

你可能感兴趣的:(linux,嵌入式,vim,c语言)