- 4 Linux进程间通信应用开发
- 4.1 初识进程
- 4.1.1 进程的概念
- 4.1.1.1 程序
- 4.1.1.2 进程
- 4.1.1.3 进程和程序的联系
- 4.1.1.4 进程和程序的区别
- 4.1.2 进程的操作(创建、结束、回收)
- 4.1.2.1 创建进程
- 4.1.2.2 结束进程
- 4.1.2.3 回收进程
- 4.2 进程为什么需要通信
- 4.3 进程通信之管道通信
- 4.3.1 无名管道
- 4.3.1.1 特点
- 4.3.1.2 创建无名管道
- 4.3.1.3 读、写、关闭管道
- 4.3.1.4 无名管道实现进程间通信
- 4.3.2 有名管道
- 4.3.2.1 特点
- 4.3.2.2 创建有名管道
- 4.3.2.3 有名管道实现进程间通信
- 4.4 进程通信之IPC通信
- 4.4.1 共享内存
- 4.4.1.1 特点
- 4.4.1.2创建共享内存
- 4.4.1.3 应用程序如何访问共享内存
- 4.4.1.4 共享内存实现进程间通信
- 4.4.2 消息队列
- 4.4.2.1 什么是消息队列
- 4.4.2.2 特点
- 4.4.2.3 消息队列函数
- 4.4.2.4 消息队列实现进程间通信
- 4.4.3 信号量灯
- 4.4.3.1 什么是P、V操作
- 4.4.3.2 什么是信号量灯
- 4.4.3.3 信号量灯函数
- 4.4.3.4 信号量灯实现进程间同步/互斥
- 4.5 进程通信之信号通信
- 4.5.1 信号机制
- 4.5.2 常见信号类型
- 4.5.3 信号发送函数
- 4.5.4 进程捕捉信号
- 4.6 进程通信之socket通信
- 4.6.1 什么是socket
- 4.6.2 相关函数
- 4.6.3 socket实现进程间通信
- 4.6.4 一个server和多个client之间的通信
在日常工作/学习中,读者可能会经常听到如下一些词:“作业”,“任务”,“开了几个线程”,“创建了几个进程”,“多线程”,“多进程”等等。如果系统学习过《操作系统》这门课程,相信大家对这些概念都十分了解。但对很多电子、电气工程专业(或是其他非计算机专业)的同学来说,由于这门课程不是必修课程,我们脑海中可能就不会有这些概念,听到这些概念的时候就会不知所云,不过没有关系,先让我们克服对这些概念的恐惧。比如小时候刚开始学习数学的时候,先从正整数/自然数开始学习,然后逐步接触到分数、小数、负数、有理数、无理数、实数,再到复数等等。这些操作系统中的概念也是这样,让我们从初级阶段开始学起,逐步攻克这些新概念背后的真正含义。
本篇主要讨论linux进程间通信方式,这个主题拆分开始来看,分为三个部分:linux(操作系统)、进程、进程间通信。Linux操作系统本篇暂且不谈,我们主要来关注后两个部分:进程,以及进程间通信。在探讨进程间通信之前,让我们先关注一个知识点概念----进程。
4.1.1 进程的概念 4.1.1.1 程序 在探讨进程之前,先思考一个问题:什么是程序?
嵌入式软件工程师每天的工作/学习内容就是看C/C++源代码、分析C/C++源代码、编写C/C++源代码(有人会说,应该还有最重要的调试程序,我每天的工作日常是三分写程序,七分调试程序,调试程序去哪里了,大家别着急,这里先卖一个关子)。这些独立的源代码就是一个个程序。它们有一个共同特点,在我们阅读、分析、编写的过程中,此刻都是静态的,它们存储在我们的硬盘上、公司的服务器上。
程序:存储在磁盘上的指令和数据的有序集合。如下就是一个程序,此刻它正安静地躺在硬盘上。
01 #include
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.有一位计算机科学家,他的女儿要过生日了,他准备给女儿做一个生日蛋糕,于是他去找了一本菜谱,跟着菜谱学习做蛋糕。
菜谱=程序 科学家=CPU 做蛋糕的原材料=数据 做蛋糕的过程=进程
2.科学家正在做蛋糕的时候,突然他的小儿子跑过来,说他的手被扎破了,于是科学家又去找了一本医疗手册,给小儿子处理伤口,处理完伤口之后,继续做生日蛋糕
医疗手册=新程序 给小儿子处理伤口=新进程
从做蛋糕切换到优先包扎伤口=进程切换 处理完伤口继续做生日蛋糕=进程恢复
介绍到这里,希望读者对进程已经建立起一些基础概念了,有关进程的深入部分,我们在这里暂且先不介绍,比如进程的组成包括哪些(代码段,用户数据段,系统数据段)?进程的类型有哪些?进程的状态有哪些等等?这些深入内容,在我们掌握了进程的基础知识之后,读者有兴趣的话,可以查阅相关书籍资料。
4.1.2 进程的操作(创建、结束、回收) 4.1.2.1 创建进程使用fork函数来创建一个进程
头文件: #include
函数原型: pid_t fork(void);
返回值: 成功时,父进程返回子进程的进程号(>0的非零整数),子进程中返回0;通过fork函数的返回值区分父子进程。
父进程: 执行fork函数的进程。
子进程: 父进程调用fork函数之后,生成的新进程。
请重点注意:这个函数的返回值和我们接触的绝大部分函数的返回值不一样。
一般地,一个函数的返回值只有一个值,但是该函数的返回值却有两个。实际上关于这个函数的返回值究竟有几个,可以换一种方式来理解,因为这个函数执行之后,系统中会存在两个进程----父进程和子进程,在每个进程中都返回了一个值,所以给用户的感觉就是返回了两个值。
进程的特点:
-
在linux中,一个进程必须是另外一个进程的子进程,或者说一个进程必须有父进程,但是可以没有子进程。
-
子进程继承了父进程的内容,包括父进程的代码,变量,pcb,甚至包括当前PC值。在父进程中,PC值指向当前fork函数的下一条指令地址,因此子进程也是从fork函数的下一条指令开始执行。父子进程的执行顺序是不确定的,可能子进程先执行,也可能父进程先执行,取决于当前系统的调度。
-
父子进程有独立的地址空间、独立的代码空间,互不影响,就算父子进程有同名的全局变量,但是由于它们处在不同的地址空间,因此不能共享。
-
子进程结束之后,必须由它的父进程回收它的一切资源,否则就会成为僵尸进程。
-
如果父进程先结束,子进程会成为孤儿进程,它会被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
12 #include
13 #include
14 #include
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 0) { // 父进程
31 int i = 0;
32 for (i = 0; i 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
运行结果
先让我们看如下两个简单的程序,这两个程序中都有一个同名全局变量“global”,唯一的区别是这个全局变量的初始值不同。说明:以下两个示例程序是为了让我们理解进程的一个特点,因此实验环境是Ubuntu虚拟机。
程序1:
01 #include
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
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
程序1运行结果
程序2运行结果
我们发现,两个程序运行之后,当前进程中的全局变量global的值并不会改变,它不会被改变成另外一个进程中的值,由此引出的进程的一个特点:**进程资源的唯一性,不共享性,它不能访问别的进程中的数据(地址空间),也不能被别的进程访问本身的数据(地址空间)。**每个进程对其他进程而言,就是一个黑盒(后面读者学习到线程的时候,会发现在这个特性上,线程是有别于进程的)。
那么为什么会这样呢?这是因为操作系统为了保证系统的安全(进程A奔溃不会影响进程B,进程B仍然会继续运行),它会为每个进程分配特定的地址空间,每个进程只能在这个特定的地址空间执行指令、访问数据,如下图所示。程序需要访问某个变量时,都是通过变量地址去访问该变量的,在不同的进程中,同名变量对应不同的地址(处在当前进程地址空间范围内),进程无法访问分配给它的地址范围之外的地址空间,自然就无法获得其他进程中的变量值。
进程间为何需要通信呢?从上面的两个示例程序中,可以得知:不同进程之间无法互相访问对方的地址空间。但是在我们实际的项目开发中,为了实现各种各样的功能,不同进程之间一定需要数据交互,那么我们应该如何实现进程间数据交互呢?这就是进程间通信的目的:实现不同进程之间的数据交互。
在linux下,内存空间被划分为用户空间和内核空间,应用程序开发人员开发的应用程序都存在于用户空间,绝大部分进程都处在用户空间;驱动程序开发人员开发的驱动程序都存在于内核空间。
在用户空间,不同进程不能互相访问对方的资源,因此,在用户空间是无法实现进程间通信的。为了实现进程间通信,必须在内核空间,由内核提供相应的接口来实现,linux系统提供了如下四种进程通信方式。
进程间通信方式分类管道通信无名管道、有名管道IPC通信共享内存、消息队列、信号灯信号通信信号发送、接收、处理socket通信本地socket通信,远程socket通信 linux有一个最基本的思想----“一切皆文件”,内核中实现进程间通信也是基于文件读写思想。不同进程通过操作内核里的同一个内核对象来实现进程间通信,如下图所示,这个内核对象可以是管道、共享内存、消息队列、信号灯、信号,以及socket。
管道分为无名管道和有名管道,其特点如下
类型特点无名管道在文件系统中没有文件节点,只能用于具有亲缘关系的进程间通信(比如父子进程)有名管道在文件系统中有文件节点,适用于在同一系统中的任意两个进程间通信 4.3.1 无名管道 4.3.1.1 特点 无名管道实际上就是一个单向队列,在一端进行读操作,在另一端进行写操作,所以需要两个文件描述符,描述符fd[0]指向读端,fd[1]指向写端。它是一个特殊的文件,所以无法使用简单open函数创建,我们需要pipe函数来创建。它只能用于具有亲缘关系的两个进程间通信。
1.头文件#include
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
12 #include
13 #include
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
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?