此系列笔记参考华清远见《嵌入式 Linux 应用程序开发标准教程》
Linux 下的进程通信手段基本上是从 UNIX 平台上的进程通信手段继承而来的。而对 UNIX 发展做出重大贡献的两大主力 AT&T 的贝尔实验室及 BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间的通信方面的侧重点有所不同。前者是对 UNIX 早期的进程间通信手段进行了系统的改进和扩充,形成了“ system V IPC”, 其通信进程主要局限在单个计算机内; 后者则跳过了该限制, 形成了基于套接口( socket)的进程间通信机制。
- UNIX 进 程间通信( IPC)方式包括管道、FIFO 以及信号。
- System V 进程间通信( IPC)包括 System V 消息队列、 System V 信号量以及 System V 共享内存区。
- Posix 进程间通信( IPC)包括 Posix 消息队列、 Posix 信号量以及 Posix 共享内存区。
现在在 Linux 中使用较多的进程间通信方式主要有以下几种。
( 1)管道( Pipe)及有名管道( named pipe):管道可用于具有亲缘关系进程间的通信,有名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
( 2)信号( Signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。
( 3)消息队列( Messge Queue):消息队列是消息的链接表,包括 Posix 消息队列 SystemV 消息队列。
它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。
( 4)共享内存( Shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块
内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。
( 5)信号量( Semaphore):主要作为进程之间以及同一进程的不同线程之间的同步和互斥手段。
( 6)套接字( Socket):这是一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
管道是 Linux 中一种很重要的通信方式,它是把一个程序的输出直接连接到另一个程序的输入。管道是 Linux 中进程间通信的一种方式。
这里所说的管道主要指无名管道,它具有如下特点。
- 它只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)。
- 它是一个半双工的通信模式,具有固定的读端和写端。
- 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read()和 write()等函数。但 是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中。
管道读写注意点
前面介绍的管道是无名管道,它只能用于具有亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制, 它可以使互不相关的两个进程实现彼此通信。该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。不过值得注意的是, FIFO 是严格地遵循先进先出规则的,对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如 lseek()等文件定位操作。
- 有名管道的创建可以使用函数 mkfifo(),该函数类似文件中的 open()操作,可以指定管道的路径和打开的模式。
- 用户还可以在命令行使用“ mknod 管道名 p”来创建有名管道。
在创建管道成功之后,就可以使用 open()、 read()和 write()这些函数了。与普通文件的开发设置一样,对于为读而打开的管道可在open()中设置 O_RDONLY, 对于为写而打开的管道可在 open()中设置 _WRONLY,在这里与普通文件不同的是阻塞问题。由于普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志可以在 open()函数中设定为O_NONBLOCK。下面分别对阻塞打开和非阻塞打开的读写进行讨论。
( 1)对于读进程。
信号是 UNIX 中所使用的进程通信的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
信号值在 32 之前的则有不同的名称,而信号值在 32 以后的都是用“ SIGRTMIN”或“ SIGRTMAX”开头的,这就是两类典型的信号。前者是从 UNIX 系统中继承下来的信号,为不可靠信号(也称为非实时信号);后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号,称为“可靠信号”(也称为实时信号)。
那么为什么之前的信号不可靠呢?这里首先要介绍一下信号的生命周期。一个完整的信号生命周期可以分为 3 个重要阶段,这 3 个阶段由 4 个重要事件来刻画的:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数。相邻两个事件的时间间隔构成信号生命周期的一个阶段。
一个不可靠信号的处理过程是这样的: 如果发现该信号已经在进程中注册, 那么就忽略该信号。因此,若前一个信号还未注销又产生了相同的信号就会产生信号丢失。而当可靠信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此信号就不会丢失。所有可靠信号都支持排队,而所有不可靠信号都不支持排队。
用户进程对信号的响应可以有 3 种方式。
发送信号的函数主要有 kill()、 raise()、 alarm()以及 pause(),
信号处理的主要方法有两种,一种是使用简单的 signal()函数,另一种是使用信号集函数组。
在多任务操作系统环境下,多个进程会同时运行,并且一些进程之间可能存在一定的关联。多个进程可能为了完成同一个任务会相互协作,这样形成进程之间的同步关系。而且在不同进程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程之间的互斥关系。
进程之间的互斥与同步关系存在的根源在于临界资源。临界资源是在同一个时刻只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段,共享结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会成为临界资源。
信号量是用来解决进程之间的同步与互斥问题的一种进程之间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作( PV 操作)。其中信号量对应于某一种资源,取一个非负的整型值。
信号量值指的是当前可用的该资源的数量,若它等于 0 则意味着目前没有可用的资源。
PV 原子操作的具体定义如下:
- P 操作:如果有可用的资源(信号量值>0),则占用一个资源(给信号量值减去一,进入临界区代码) ;如果没有可用的资源(信号量值等于 0),则被阻塞到,直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
- V 操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程。如果没有进程等待它,则释放一个资源(给信号量值加一)。
使用信号量访问临界区的伪代码所下所示:
{
/* 设 R 为某种资源, S 为资源 R 的信号量*/
INIT_VAL(S); /* 对信号量 S 进行初始化 */
非临界区;
P(S); /* 进行 P 操作 */
临界区(使用资源 R) ; /* 只有有限个(通常只有一个)进程被允许进入该区*/
V(S); /* 进行 V 操作 */
非临界区;
}
最简单的信号量是只能取 0 和 1 两种值,这种信号量被叫做二维信号量。在本节中,主要讨论二维信号量。二维信号量的应用比较容易地扩展到使用多维信号量的情况。
在 Linux 系统中,使用信号量通常分为以下四个步骤。
- 创建信号量或获得在系统已存在的信号量,此时需要调用 semget()函数。不同进程通过使用同一个信号量键值来获得同一个信号量。
- 初始化信号量,此时使用 semctl()函数的 SETVAL 操作。当使用二维信号量时,通常将信号量初始化 为 1。
- 进行信号量的 PV 操作,此时调用 semop()函数。这一步是实现进程之间的同步和互斥的核心工作部分。
- 如果不需要信号量,则从系统中删除它,此时使用 semclt()函数的 IPC_RMID 操作。此时需要注意,在程序中不应该出现对已经被删除的信号量的操作。
共享内存是一种最为高效的进程间通信方式。因为进程可以直接读写内存,不需要任何数据的复制。为了在多个进程间交换信息,内核专门留出了一块内存区。这段内存区可以由需要访问的进程将其映射到自己的私有地址空间。因此,进程就可以直接读写这一内存区而不需要进行数据的复制,从而大大提高了效率。当然,由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。
共享内存的实现分为两个步骤:
- 第一步是创建共享内存,这里用到的函数是shmget(),也就是从内存中获得一段共享内存区域;
- 第二步映射共享内存,也就是把这段创建的共享内存映射到具体的进程空间中,这里使用的函数是shmat()。
到这里,就可以使用这段共享内存了,也就是可以使用不带缓冲的 I/O 读写命令对其进行操作。
消息队列就是一些消息的列表,用户可以从消息队列中添加消息和读取消息等。从这点上看,消息队列具有一定的 FIFO 特性,但是它可以实现消息的随机查询,比 FIFO 具有更大的优势。同时,这些消息又是存在于内核中的,由“队列 ID”来标识。
消息队列的实现包括创建或打开消息队列、添加消息、读取消息和控制消息队列这 4 种操作。
- 创建或打开消息队列使用的函数是 msgget(),这里创建的消息队列的数量会受到系统消息队列数量的限制; 添加消息使用的函数是
- msgsnd()函数,它把消息添加到已打开的消息队列末尾; 读取消息使用的函数是msgrcv(),它把消息从消息队列中取走,与 FIFO
- 不同的是,这里可以指定取走某一条消息; 最后控制消息队列使用的函数是 msgctl(),它可以完成多项功能。