4 Linux进程间通信(一)

4 Linux进程间通信

4.1 初识进程

​ 在日常工作/学习中,读者可能会经常听到如下一些词:“作业”,“任务”,“开了几个线程”,“创建了几个进程”,“多线程”,“多进程”等等。如果系统学习过《操作系统》这门课程,相信大家对这些概念都十分了解。但对很多电子、电气工程专业(或是其他非计算机专业)的同学来说,由于这门课程不是必修课程,我们脑海中可能就不会有这些概念,听到这些概念的时候就会不知所云,不过没有关系,先让我们克服对这些概念的恐惧。比如小时候刚开始学习数学的时候,先从正整数/自然数开始学习,然后逐步接触到分数、小数、负数、有理数、无理数、实数,再到复数等等。这些操作系统中的概念也是这样,让我们从初级阶段开始学起,逐步攻克这些新概念背后的真正含义。

​ 本篇主要讨论linux进程间通信方式,这个主题拆分开始来看,分为三个部分:linux(操作系统)、进程、进程间通信。Linux操作系统本篇暂且不谈,我们主要来关注后两个部分:进程,以及进程间通信。在探讨进程间通信之前,让我们先关注一个知识点概念----进程。

4.1.1 进程的概念

4.1.1.1 程序

​ 在探讨进程之前,先思考一个问题:什么是程序?

​ 嵌入式软件工程师每天的工作/学习内容就是看C/C++源代码、分析C/C++源代码、编写C/C++源代码(有人会说,应该还有最重要的调试程序,我每天的工作日常是三分写程序,七分调试程序,调试程序去哪里了,大家别着急,这里先卖一个关子)。这些独立的源代码就是一个个程序。它们有一个共同特点,在我们阅读、分析、编写的过程中,此刻都是静态的,它们存储在我们的硬盘上、公司的服务器上。

​ 程序:存储在磁盘上的指令和数据的有序集合。如下就是一个程序,此刻它正安静地躺在硬盘上。

01 #include <stdio.h>
02
03 int main(int argc, char *argv[])
04{
05	printf("hello world!\n");
06	return 0;
07}

4.1.1.2 进程

​ 有了上面程序的概念,先直接给出进程的定义。

​ 进程:具有一定独立功能的程序在一个数据集合上的一次动态执行过程。它是动态的,包括创建、调度、执行和消亡(由操作系统完成的)。

​ 定义中的每个词分开来我们都能理解,但是组合到一起成为一个句子时,我们又不知道什么意思了。图灵奖得主Pascal之父尼古拉斯·沃斯,提出过一个著名的公式:程序=算法+数据结构。所谓算法就是解决一个问题的方法,程序就是使用算法对特定数据进行处理,这些数据是一个广义上的概念,不单单指像1,2,3,…等等这样的数据。因此用更直白的语言来说,程序开始运行,对数据进行分析处理的过程就是一个进程。

4.1.1.3 进程和程序的联系

  1. 程序是产生进程的基础。

  2. 程序的每次执行构成不同的进程。

  3. 进程是程序功能的体现(还记得之前提到的程序员日常工作中的一个重要事项----调试程序吗?调试的过程实际上就是程序的执行,就是本次程序功能的体现,因此这个时候它就是一个进程)。

  4. 通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包含多个程序。

4.1.1.4 进程和程序的区别

程序 进程
状态 静态的,是有序代码的集合 动态的,是程序功能的执行过程
生命期 永久的,长久保存在存储设备上 暂时的,一个程序执行结束,则它对应的进程结束

下图反应了从程序到进程的变化过程。


我们以一个生活中的例子来加深对进程和程序的理解:

1.有一位计算机科学家,他的女儿要过生日了,他准备给女儿做一个生日蛋糕,于是他去找了一本菜谱,跟着菜谱学习做蛋糕。

菜谱=程序  科学家=CPU  做蛋糕的原材料=数据  做蛋糕的过程=进程

2.科学家正在做蛋糕的时候,突然他的小儿子跑过来,说他的手被扎破了,于是科学家又去找了一本医疗手册,给小儿子处理伤口,处理完伤口之后,继续做生日蛋糕

医疗手册=新程序  给小儿子处理伤口=新进程

从做蛋糕切换到优先包扎伤口=进程切换  处理完伤口继续做生日蛋糕=进程恢复

​ 介绍到这里,希望读者对进程已经建立起一些基础概念了,有关进程的深入部分,我们在这里暂且先不介绍,比如进程的组成包括哪些(代码段,用户数据段,系统数据段)?进程的类型有哪些?进程的状态有哪些等等?这些深入内容,在我们掌握了进程的基础知识之后,读者有兴趣的话,可以查阅相关书籍资料。

4.1.2 进程的操作(创建、结束、回收)

4.1.2.1 创建进程

使用fork函数来创建一个进程
头文件: #include <unistd.h>
函数原型: pid_t fork(void);
返回值: 成功时,父进程返回子进程的进程号(>0的非零整数),子进程中返回0;通过fork函数的返回值区分父子进程。
父进程: 执行fork函数的进程。
子进程: 父进程调用fork函数之后,生成的新进程。

​ 请重点注意:这个函数的返回值和我们接触的绝大部分函数的返回值不一样。

​ 一般地,一个函数的返回值只有一个值,但是该函数的返回值却有两个。实际上关于这个函数的返回值究竟有几个,可以换一种方式来理解,因为这个函数执行之后,系统中会存在两个进程----父进程和子进程,在每个进程中都返回了一个值,所以给用户的感觉就是返回了两个值。

进程的特点:

  1. 在linux中,一个进程必须是另外一个进程的子进程,或者说一个进程必须有父进程,但是可以没有子进程。

  2. 子进程继承了父进程的内容,包括父进程的代码,变量,pcb,甚至包括当前PC值。在父进程中,PC值指向当前fork函数的下一条指令地址,因此子进程也是从fork函数的下一条指令开始执行。父子进程的执行顺序是不确定的,可能子进程先执行,也可能父进程先执行,取决于当前系统的调度。

  3. 父子进程有独立的地址空间、独立的代码空间,互不影响,就算父子进程有同名的全局变量,但是由于它们处在不同的地址空间,因此不能共享。

  4. 子进程结束之后,必须由它的父进程回收它的一切资源,否则就会成为僵尸进程。

  5. 如果父进程先结束,子进程会成为孤儿进程,它会被INIT进程收养,INIT进程是内核启动之后,首先被创建的进程。

Tips:

​ 在linux下,当我们不熟悉某个系统接口API函数时(比如不知道调用这个函数需要包含的头文件,不知道这个函数的每个参数的意义等等),我们可以在ubuntu下使用man命令来查看这个函数的说明。

示例程序(参考:jz2440\process\1th_create_process\create_process.c)

01 /**********************************************************************
02  * 功能描述: 创建一个子进程
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 
16 int main(int argc, char *argv[])
17 {
18     pid_t pid;
19 
20     pid = fork();  // 创建子进程
21 
22     if (pid == 0) {  // 子进程
23         int i = 0;
24         for (i = 0; i < 5; i++) {
25             usleep(100);
26             printf("this is child process i=%d\n", i);
27         }
28     }
29     
30     if (pid > 0) {  // 父进程
31         int i = 0;
32         for (i = 0; i < 5; i++) {
33             usleep(100);
34             printf("this is parent process i=%d\n", i);
35         }
36     }
37 
38     while(1);   //不让进程结束,以便我们查看进程的一些状态信息
39     return 0;
40 }

JZ2440实验

​ 在jz2440开发板上实验,读者首先需要创建好NFS文件系统,jz2440开发板从网络文件系统启动,以便运行在ubuntu上编译好的可执行文件,关于如何搭建NFS文件系统请参考视频教程《u-boot_内核_根文件系统(ARM裸机1期加强版与2期驱动大全间的衔接)》。读者也可以在ubuntu上执行,将编译器从“arm-linux-gcc”换成“gcc”即可。

  • 编译程序
arm-linux-gcc create_process.c -o create_process
  • 将可执行文件test拷贝到NFS文件系统对应的目录下
cp create_process /work/nfs_root/first_fs
  • 在jz2440开发板的串口下此时能看到该可执行文件

  • 执行可执行文件

    ​ ”&”表示在后台执行,这样我们可以继续在串口控制台下敲入命令,控制台能够接收到输入字符并作出响应;如果不加”&”,表示在前台执行,控制台不能对输入字符作出响应。

./create_process &

  • top命令查看进程状态
top

​ 发现此时确实存在两个进程create_process,其中一个进程PID是777(它的父进程PID是776),另外一个进程PID是776(它的父进程PID是770)。

4.1.2.2 结束进程

使用exit函数来结束一个进程
头文件: #include <stdlib.h>
函数原型: void exit (int status)
使用_exit函数来结束一个进程
头文件: #include <unistd.h>
函数原型: void _exit(int status);

​ 两个函数的区别是:exit结束进程时会刷新缓冲区,_exit不会;

​ 这两个退出函数和return函数又有什么区别呢?exit和 _ exit函数是返回给操作系统的,return函数是当前函数返回,返回到调用它的函数中,如果正好是在main函数中,return函数也返回给了操作系统,这个时候return和exit、_exit起到了类似的作用。

程序实验:验证exit和_exit的区别

示例1:使用exit退出(参考:jz2440\process\2th_exit_process\exit_process.c)

01 /**********************************************************************
02  * 功能描述: 使用exit退出当前进程
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 #include <stdio.h>
11 #include <stdlib.h>
12 
13 int main(int argc, char *argv[])
14 {
15     printf("hello world\n");
16     printf("will exit");
17     exit(0);    //使用exit退出
18 }

示例2:使用_exit退出(参考:jz2440\process\3th_exit_process\exit_process.c)

01 /**********************************************************************
02  * 功能描述: 使用_exit退出当前进程
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 #include <stdio.h>
11 #include <stdlib.h>
12 
13 int main(int argc, char *argv[])
14 {
15     printf("hello world\n");
16     printf("will exit");
17     _exit(0);    //使用_exit退出
18 }

​ 在两个示例程序中,第15行比第16行的打印语句多了一个“\n”,它会强制将待打印的字符刷新到缓冲区,为了对比exit和_exit的区别,在第16行中就没有加上“\n”,按照上面两个退出函数的区别,示例1应该会同时打印“hello world”和“will exit”,示例2程序只会打印“hello world”,不会打印“will exit”,那么到底是不是这样呢?我们在jz2440下验证一下。

JZ2440实验

示例1

  • 编译
arm-linux-gcc exit_process.c -o exit_process
  • 拷贝到NFS
cp exit_process /work/nfs_root/first_fs
  • 运行
./exit_process

运行结果,确实同时打印了“hello world”和“will exit”
image

4.1.2.3 回收进程

使用wait函数来回收一个进程
头文件: #include <sys/types.h>
	   #include <sys/wait.h>
函数原型: pid_t wait(int *status);
返回值:  成功返回子进程的进程号,失败返回-1
使用waitpid函数来回收一个进程
头文件: #include <sys/types.h>
	   #include <sys/wait.h>
函数原型: pid_t waitpid(pid_t pid, int *status, int options);
返回值:  成功返回子进程的进程号,失败返回-1

程序示例:子进程退出,父进程回收子进程(参考:jz2440\process\4th_exit_wait\exit_wait.c)

1 /**********************************************************************
02  * 功能描述: 使用exit退出子进程,父进程使用waitpid回收子进程的资源
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 #include <unistd.h>
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <sys/types.h>
14 #include <sys/wait.h>
15 
16 int main(int argc, char *argv[])
17 {
18 	int status = -1;
19 	pid_t pid;
20 
21 	pid = fork();
22 	if (pid == 0){ //子进程
23 		printf("fork\n");
24 		exit(1);
25 	} else if (pid > 0) { //父进程
26 		pid = waitpid(pid, &status, 0);
27 		printf("status=0x%x\n", status);
28 	} else {
29 		perror("fork\n");
30 	}
31 
32 	return 0;
33 }

JZ2440实验

  • 编译
arm-linux-gcc exit_wait.c -o exit_wait
  • 拷贝到NFS
cp exit_wait /work/nfs_root/first_fs
  • 运行
./exit_wait

运行结果
image

4.2 进程为什么需要通信

​ 先让我们看如下两个简单的程序,这两个程序中都有一个同名全局变量“global”,唯一的区别是这个全局变量的初始值不同。说明:以下两个示例程序是为了让我们理解进程的一个特点,因此实验环境是Ubuntu虚拟机

程序1:

01 #include <stdio.h>
02 int global = 1;
03
04 void delay(void)
05 {
06 	unsigned int a = 1000000;
07 	while(a--);
08 }
09
10 int main(int argc, char *argv[])
11 {
12 	while (1) {
13 		printf("global=%d\n", global);
14 		delay();
15 	}
16 	return 0;
17 }

程序2:

01 #include <stdio.h>
02 int global = 2;
03
04 void delay(void)
05 {
06 	unsigned int a = 1000000;
07 	while(a--);
08 }
09
10 int main(int argc, char *argv[])
11 {
12 	while (1) {
13 		printf("global=%d\n", global);
14 		delay();
15 	}
16 	return 0;
17 }

两个程序的唯一区别如下红框所示:

  • 编译程序
gcc test1.c -o test1
gcc test2.c -o test2
  • 运行程序
./test1
./test2

image

程序1运行结果

![image|545x155](upload://hGcwMchttGKQkXduRFe9xiPBkvI.png)

程序2运行结果

​ 我们发现,两个程序运行之后,当前进程中的全局变量global的值并不会改变,它不会被改变成另外一个进程中的值,由此引出的进程的一个特点:**进程资源的唯一性,不共享性,它不能访问别的进程中的数据(地址空间),也不能被别的进程访问本身的数据(地址空间)。**每个进程对其他进程而言,就是一个黑盒(后面读者学习到线程的时候,会发现在这个特性上,线程是有别于进程的)。

​ 那么为什么会这样呢?这是因为操作系统为了保证系统的安全(进程A奔溃不会影响进程B,进程B仍然会继续运行),它会为每个进程分配特定的地址空间,每个进程只能在这个特定的地址空间执行指令、访问数据,如下图所示。程序需要访问某个变量时,都是通过变量地址去访问该变量的,在不同的进程中,同名变量对应不同的地址(处在当前进程地址空间范围内),进程无法访问分配给它的地址范围之外的地址空间,自然就无法获得其他进程中的变量值。
image

​ 进程间为何需要通信呢?从上面的两个示例程序中,可以得知:不同进程之间无法互相访问对方的地址空间。但是在我们实际的项目开发中,为了实现各种各样的功能,不同进程之间一定需要数据交互,那么我们应该如何实现进程间数据交互呢?这就是进程间通信的目的:实现不同进程之间的数据交互

​ 在linux下,内存空间被划分为用户空间和内核空间,应用程序开发人员开发的应用程序都存在于用户空间,绝大部分进程都处在用户空间;驱动程序开发人员开发的驱动程序都存在于内核空间。

​ 在用户空间,不同进程不能互相访问对方的资源,因此,在用户空间是无法实现进程间通信的。为了实现进程间通信,必须在内核空间,由内核提供相应的接口来实现,linux系统提供了如下四种进程通信方式。

进程间通信方式 分类
管道通信 无名管道、有名管道
IPC通信 共享内存、消息队列、信号灯
信号通信 信号发送、接收、处理
socket通信 本地socket通信,远程socket通信

​ linux有一个最基本的思想----“一切皆文件”,内核中实现进程间通信也是基于文件读写思想。不同进程通过操作内核里的同一个内核对象来实现进程间通信,如下图所示,这个内核对象可以是管道、共享内存、消息队列、信号灯、信号,以及socket。

4.3 进程通信之管道通信

管道分为无名管道和有名管道,其特点如下

类型 特点
无名管道 在文件系统中没有文件节点,只能用于具有亲缘关系的进程间通信(比如父子进程)
有名管道 在文件系统中有文件节点,适用于在同一系统中的任意两个进程间通信

4.3.1 无名管道

4.3.1.1 特点

​ 无名管道实际上就是一个单向队列,在一端进行读操作,在另一端进行写操作,所以需要两个文件描述符,描述符fd[0]指向读端,fd[1]指向写端。它是一个特殊的文件,所以无法使用简单open函数创建,我们需要pipe函数来创建。它只能用于具有亲缘关系的两个进程间通信。

4.3.1.2 创建无名管道

1.头文件#include <unistd.h>

2.函数原型: int pipe(int fd[2])

3.参数: 管道文件描述符,有两个文件描述符,分别是fd[0]和fd[1],管道有一个读端fd[0]和一个写端fd[1]

4.返回值: 0表示成功;-1表示失败

4.3.1.3 读、写、关闭管道

1.读管道 read,读管道对应的文件描述符是fd[0]

2.写管道 write,写管道对应的文件描述符是fd[1]

3.关闭管道 close,因为创建管道时,会同时创建两个管道文件描述符,分别是读管道文件描述符fd[0]和写管道文件描述符fd[1],因此需要关闭两个文件描述符

4.3.1.4 无名管道实现进程间通信

程序示例1

(参考:jz2440\process_pipe\1th_write_pipe\my_pipe_write.c)

01 /**********************************************************************
02  * 功能描述: 创建一个管道,并向管道中写入字符串,然后从管道中读取,验证
03               能否读取之前写入的字符串
04  * 输入参数: 无
05  * 输出参数: 无
06  * 返 回 值: 无
07  * 修改日期        版本号     修改人          修改内容
08  * -----------------------------------------------
09  * 2020/05/16       V1.0      zh(ryan)        创建
10  ***********************************************************************/
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 
15 int main(int argc, char *argv[])
16 {
17     int fd[2];
18     int ret = 0;
19     char write_buf[] = "Hello linux";
20     char read_buf[128] = {0};
21     
22     ret = pipe(fd);
23     if (ret < 0) {
24         printf("create pipe fail\n");
25         return -1;
26     }
27     printf("create pipe sucess fd[0]=%d fd[1]=%d\n", fd[0], fd[1]);
28     
29     //向文件描述符fd[1]写管道
30     write(fd[1], write_buf, sizeof(write_buf));
31     
32     //从文件描述符fd[0]读管道
33     read(fd[0], read_buf, sizeof(read_buf));
34     printf("read_buf=%s\n", read_buf);
35     
36     close(fd[0]);
37     close(fd[1]);
38     return 0;
39 }

JZ2440实验

  • 编译
arm-linux-gcc my_pipe_write.c -o my_pipe_write

  • 拷贝到NFS文件系统
cp my_pipe_write /work/nfs_root/first_fs
  • 运行
./my_pipe_write

运行结果,发现能够正确读到管道中的字符串” Hello linux”。

程序示例2

​ 在利用无名管道实现进程间通信之前,先让我们看一下如下的程序:我们知道父子进程的执行顺序是不确定的,是受系统调度的。我们在父进程中创建一个子进程,我们想让父进程控制子进程的运行,父进程设置“process_inter=1”,当“process_inter=1”时,子进程才会执行打印操作,否则子进程不执行打印操作。我们看如下的程序能够实现我们的目的吗?

(参考:jz2440\process_pipe\2th_comm\test.c)

01 /**********************************************************************
02  * 功能描述: 1.在父进程中创建一个子进程,
03             2.父进程执行完后,将变量process_inter赋值为1;
04             3.子进程判断process_inter为1则执行后面的打印语句,否则不执行。
05  * 输入参数: 无
06  * 输出参数: 无
07  * 返 回 值: 无
08  * 修改日期        版本号     修改人          修改内容
09  * -----------------------------------------------
10  * 2020/05/16       V1.0      zh(ryan)        创建
11  ***********************************************************************/
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 
17 int main(int argc, char *argv[])
18 {
19     pid_t pid;
20     int process_inter = 0;
21 
22     pid = fork();  // 创建子进程
23 
24     if (pid == 0) {  // 子进程
25         int i = 0;
26         while (process_inter == 0); //
27         for (i = 0; i < 5; i++) {
28             usleep(100);
29             printf("this is child process i=%d\n", i);
30         }
31     }
32     
33     if (pid > 0) {  // 父进程
34         int i = 0;
35         for (i = 0; i < 5; i++) {
36             usleep(100);
37             printf("this is parent process i=%d\n", i);
38         }
39         process_inter == 1;
40     }
41 
42     while(1);
43     return 0;
44 }

JZ2440实验

  • 编译
arm-linux-gcc test.c -o test
  • 拷贝到NFS文件系统
cp test /work/nfs_root/first_fs
  • 运行
./test

image

运行结果,发现第29行打印语句一直没有,子进程中process_inter一直为0。

程序示例3

(参考:jz2440\process_pipe\3th_pipe_comm\comm_fork.c)

01 /**********************************************************************
02  * 功能描述: 1.使用无名管道实现父子进程通信
03             2.父进程向管道中写入一个值
04             3.子进程从管道中读取该值,如果非零,则执行后面的打印,否则不执行
05  * 输入参数: 无
06  * 输出参数: 无
07  * 返 回 值: 无
08  * 修改日期        版本号     修改人          修改内容
09  * -----------------------------------------------
10  * 2020/05/16       V1.0      zh(ryan)        创建
11  ***********************************************************************/
12 
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17 
18 int main(int argc, char *argv[])
19 {
20     pid_t pid;
21     char process_inter = 0;
22     int fd[2], ret = 0;
23 
24     ret = pipe(fd);   //创建一个无名管道,必须在创建子进程之前
25     if (ret < 0) {
26         printf("create pipe fail\n");
27         return -1;
28     }
29     printf("create pipe sucess\n");
30     
31     pid = fork();  //创建子进程
32 
33     if (pid == 0) {  // 子进程
34         int i = 0;
35         read(fd[0], &process_inter, sizeof(process_inter));  // 如果管道为空,则休眠等待
36         while (process_inter == 0);
37         for (i = 0; i < 5; i++) {
38             usleep(100);
39             printf("this is child process i=%d\n", i);
40         }
41     } else if (pid > 0) {  // 父进程
42         int i = 0;
43         for (i = 0; i < 5; i++) {
44             usleep(100);
45             printf("this is parent process i=%d\n", i);
46         }
47         process_inter = 1;
48         sleep(2);
49         write(fd[1], &process_inter, sizeof(process_inter));
50     }
51 
52     while(1);
53     return 0;
54 }

JZ2440实验

  • 编译
arm-linux-gcc comm_fork.c -o comm_fork
  • 拷贝到NFS文件系统
cp comm_fork /work/nfs_root/first_fs
  • 运行
./comm_fork

运行结果,因为第38行2s延时,父进程打印结束后大约2s左右的时间,子进程中的打印也正确输出了,如下所示。

4.3.2 有名管道

4.3.2.1 特点

所谓有名管道,顾名思义,就是在内核中存在一个文件名,表明这是一个管道文件。Linux中存在7种文件类型,分别如下。

文件类型 文件特点
普通文件 标识符 ‘-’ ,用open方式创建
目录文件 标识符 ‘d’ ,用mkdir方式创建
链接文件 标识符 ‘l’, la -s, 又可以分为软链接,硬链接
(有名)管道文件 标识 ‘p’, 用mkfifo创建
socket文件 标识符 ‘s’,用socket创建
字符设备文件 标识符 ‘c’
块设备文件 标识符 ‘b’

​ 有名管道既可以用于具有亲缘关系的进程间通信,又可以用于非亲缘关系的进程间通信,在我们的实际项目中,很多进程之间是不具有亲缘关系的,因此有名管道使用的情况会更普遍一些。

4.3.2.2 创建有名管道

函数原型 : int mkfifo(const char * filename, mode_t mode)

参数 :管道文件文件名,权限,创建的文件权限仍然和umask有关系

返回值 : 成功返回0,失败返回-1

注意:mkfifo并没有在内核中生成一个管道,只是在用户空间生成了一个有名管道文件

4.3.2.3 有名管道实现进程间通信

示例程序1

创建一个有名管道文件(参考:jz2440\process_pipe\4th_create_myfifo\create_myfifo.c)

01 /**********************************************************************
02  * 功能描述: 1.创建一个有名管道
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 
16 int main(int argc, char *argv[])
17 {
18     int ret;
19 
20     ret = mkfifo("./myfifo", 0777);   //创建有名管道,文件权限为777
21     if (ret < 0) {
22         printf("create myfifo fail\n");
23         return -1;
24     }
25     printf("create myfifo sucess\n");
26     
27     return 0;
28 }

JZ2440实验

  • 编译
arm-linux-gcc create_myfifo.c -o create_myfifo
  • 拷贝到NFS文件系统
cp create_myfifo /work/nfs_root/first_fs
  • 运行
./create_myfifo

运行结果,发现在当前目录下生成一个有名管道文件myfifo(文件类型是“-p”)。

示例程序2

进程1源码(参考:jz2440\process_pipe\5th_myfifo_comm\5nd_named_pipe.c)

01 /**********************************************************************
02  * 功能描述: 1.进程1中创建一个有名管道3rd_fifo,权限是0777
03             2.以写方式打开这个有名管道文件,并向其中写入一个值
04  * 输入参数: 无
05  * 输出参数: 无
06  * 返 回 值: 无
07  * 修改日期        版本号     修改人          修改内容
08  * -----------------------------------------------
09  * 2020/05/16       V1.0      zh(ryan)        创建
10  ***********************************************************************/
11 
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <fcntl.h>
17 
18 int main(int argc, char *argv[])
19 {
20     int i, ret, fd;
21     char p_flag = 0;
22 
23     /* 创建有名管道 */
24     if (access("./3rd_fifo", 0) < 0) {   //先判断有名管道文件是否存在,不存在需要先创建
25         ret = mkfifo("./3rd_fifo", 0777);
26         if (ret < 0) {
27             printf("create named pipe fail\n");
28             return -1;
29         }
30         printf("create named pipe sucess\n");
31     }
32     
33     /* 打开有名管道,以写方式打开 */
34     fd=open("./3rd_fifo", O_WRONLY);
35     if (fd < 0) {
36         printf("open 3rd_fifo fail\n");
37         return -1;
38     }
39     printf("open 3rd_fifo sucess\n");
40 
41     for (i = 0; i < 5; i++) {
42         printf("this is first process i=%d\n", i);
43         usleep(100);
44     }
45     p_flag = 1;
46     sleep(5);
47     write(fd, &p_flag, sizeof(p_flag));
48 
49     while(1);
50     return 0;
51 }

进程2源码(参考:jz2440\process_pipe\5th_myfifo_comm\5nd_named_pipe_2.c)

01 /**********************************************************************
02  * 功能描述: 1.只读方式打开这个有名管道文件,并读取这个值
03             2.当这个值非零时,继续执行后面的打印输出语句
04  * 输入参数: 无
05  * 输出参数: 无
06  * 返 回 值: 无
07  * 修改日期        版本号     修改人          修改内容
08  * -----------------------------------------------
09  * 2020/05/16       V1.0      zh(ryan)        创建
10  ***********************************************************************/
11 
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <fcntl.h>
17 
18 int main(int argc, char *argv[])
19 {
20     int i;
21     int fd=open("./3rd_fifo", O_RDONLY);
22     char p_flag = 0;
23     
24     if (fd < 0) {
25         printf("open 3rd_fifo fail\n");
26         return -1;
27     }
28     
29     printf("open 3rd_fifo sucess\n");
30     read(fd, &p_flag, sizeof(p_flag));
31     while(!p_flag);
32     for (i = 0; i < 5; i++) {
33         printf("this is second process i=%d\n", i);
34         usleep(100);
35     }
36 
37     while(1);
38     return 0;
39 }

JZ2440实验

  • 编译
arm-linux-gcc 5nd_named_pipe.c -o 5nd_named_pipe

arm-linux-gcc 5nd_named_pipe_2.c -o 5nd_named_pipe_2
  • 拷贝到NFS文件系统
cp 5nd_named_pipe /work/nfs_root/first_fs

cp 5nd_named_pipe_2 /work/nfs_root/first_fs
  • 运行

注意我们这里都在后台运行可执行程序,方便我们在串口中断下多次输入。

./5nd_named_pipe &

./5nd_named_pipe_2 &