8.1 进程间通信概述
进程间通信(Inter-Process Communication, IPC)是指在两个或者多个不同的进程间传递或者交换信息,通过信息的传递建立几个进程间的联系,协调一个系统中的多个进程之间的行为。
8.1.1 进程间通信的工作原理
进程与进程之间是相互独立的,各自运行在自己的虚拟内存中。要想在进程与进程之间建立联系,需要通过内核,在内核中开辟一块缓冲区,两个进程的信息在缓冲区中进行交换或者传递。
进程间通信的工作原理是:进程A中的数据写入到内核中,进程B中的数据也写入到内核中,两者在内核中进行交换。交换过后,进程A读取内核中的数据,进程B也读取内核中的数据,这样两个进程间交换数据的通信就完成了。两个进程通过内核建立了联系,那么交换数据、传递数据、发送事件等行为就都可以实现了。
8.1.2 进程间通信的主要分类
在Linux系统中,常见的进程间通信主要包括管道通信、共享内存通信、信号量通信、消息队列通信、套接口通信和全双工管道通信。
Linux系统出了支持信号和管道外,还直冲SYSV(System V)子系统中的进程间通信机制,在SYSV的IPC机制中,包括贡献内存、信号量和消息队列通信。
8.2 管道与命名管道
管道与命名管道是最基本的IPC机制之一,管道主要用于父子或者兄弟进程间的数据读写,命名管道则可以在无关联的进程间进行沟通传递数据。
8.2.1 管道的基本定义
所谓管道,在进程通信意义上的管道就是传输信息或数据的工具。某一时刻只能单一方向传递数据,不能双向传递数据,这种工作模式就叫做半双工模式。半双工工作模式的管道通信是只能从一段写数据,从另一端读取数据。
全双工的工作模式是指管道一段发送数据的同时还可以接收数据,而接收数据的一段也可以读取数据。在某些版本的UNIX系统中,管道是支持全双工模式的。但是在本书中介绍的Linux系统中,管道是只支持半双工工作模式的
8.2.2 管道创建和管道关闭
管道由Linux系统提供的pipe()函数创建
#include<unistd.h>
int pipe(int filedes[2])
pipe()函数用于在内核中创建一个管道,该管道一端用于读取管道中的数据,另一端用于将数据写入到管道中。在创建一个管道之后,会获得一对文件描述符,用于读取和写入,然后将参数数组filedes中的两个值川的哥获取到的两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端。
pipe()函数调用成功,返回值为0;否则返回-1。
pipe()函数只是创建了管道,要想从管道中读取数据或者向管道中写入数据,需要使用read()和write()函数来完成。当管道通信结束后,需要使用close()函数关闭管道的两端,即读端和写端。
8.2.3 pipe()函数实现管道通信
8.2.4 命名管道基本定义
在前面介绍的使用管道进行进程间通信的方法受到很多的限制。受限之一就是两个进程进行通信必须是两个相关联的进程,如父子进程或者兄弟进程等。那么,没有关系的进程之间有时也需要进行通信该如何解决呢?
命名管道解决了这个问题。命名管道,通常被称之为FIFO,由此可知,命名管道遵循先进先出的原则。它作为特殊的设备文件,存在于文件系统中,因此,在进程中可以使用open()和close()函数打开和关闭命名管道。
命名管道与管道类似,两者的区别在于命名管道提供了一个路径名,该路径名以特殊的设备文件的形式存放在文件系统中。因此两个进程间可以通过访问该路径来建立联系,进行两个进程间的数据交换。但管道与命名文件都遵循先进先出原则,也就是指最先写入的数据添加在结尾位置,读取数据时,从开始出返回数据。
创建一个命名管道有两种方法,一种是通过函数创建命名管道,另一种是在终端输入命令创建命名管道。
8.2.5 在Shell中创建命名管道
在Shell中输入"nknod"和"mkfifo"命令可以创建一个命名管道。
8.2.6 mkfifo()函数创建命名管道
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char* pathname, mode_t mode)
该函数的参数pathname是一个文件的路径名,是创建的一个命名管道的文件名;参数mode是指文件的权限,文件的权限值取决于(mode&~umask)值。
使用mkfifo()函数创建的命名管道文件与前面介绍的管道通信相似,只是他们的创建方式不同。访问命名管道文件与访问文件系统中的其他文件一样,都是需要首先打开文件,然后对文件进行读写数据。如果在命名管道文件中读取数据时,并没有其他进程向命名管道文件中写入数据,则会出现进程阻塞状态;如果写入数据的同时,没有进程从命名管道读取数据,也会出现进程阻塞状态。
8.3 共享内存
8.3.1 SYSV子系统的基础知识
1)IPC标识符:在Linux系统中,标识符是一个整数,每一个IPC对象的标识符在系统内部都是唯一的。在系统内部,通过传递IPC对象的标识符可以访问该对象。
2)IPC键:键是一个IPC对象的外部标识,由程序员自己拟定,它主要用于多个进程都访问一个特定的IPC对象的情况。在创建一个IPC对象时,需要制定一个键值。如果该IPC键是公用的,那么系统中所有进程通过权限检查后都可以访问到相应的IPC对象;如果是私有的,那么键值通常定义为0,该键值的类型为系统定义的ket_t类型。
3)IPC对象属性
创建一个IPC对象时,除了标识该对象的唯一标识符和外部键(key)之外,还有这个对象的一些属性,如该对象的所有者或者访问权限等信息,这些属性都定义在ipc_perm结构体中。ipc_perm结构体定义如下:
struct ipc_perm
{
uid_t uid; /*拥有者的有效用户ID*/
gid_t gid; /*拥有者的有效组ID*/
uid_t cuid; /*创建者的有效用户ID*/
gid_t cgid; /*创建者的有效组ID*/
mode_t mode;
}
4)由于IPC对象是基于系统内核的,因此可以在终端通过命令查看和删除一些IPC对象的信息。可以用如下两条命令:
①ipcs
②ipcrm
8.3.2 共享内存的相关操作
共享内存就是通过两个或者多个进程共享一块内存区域来实现进程间的通信。存放在共享内存中的数据时任何进程都可以对其进行读取的。多个进程可以直接对共享内存中的数据进行操作,因此,应用共享内存所实现的进程间通信是最快速的,但是多个进程同时读写某一块共享内存时,会造成共享内存中数据的混乱。在使用共享内存进行通信时,要注意进程间的同步,控制同步的问题需要使用信号量。每一个共享内存的对象都有其指定的定义类型,该结构体类型为shmid-ds,定义形式如下:
struct shmid-ds
{
struct ipc_perm shm_perm; /*共享内存的ipc_perm结构对象*/
int shm_segsz; /*共享内存区域字节大小*/
ushort shm_lkcnt; /*共享内存区域被锁定的时间数*/
pid_t shm_cpid; /*创建该共享内存的进程ID*/
pid_t shm_lpid; /*最近一次调用shmop()函数的进程ID*/
ulong shm_nattch; /*使用该共享内存的进程数*/
time_t shm_atime; /*最近一次附加操作的时间*/
time_t shm_dtime; /*最经一次分离操作的时间*/
time_t shm_ctime; /*最近一次改变的时间*/
}
1)shmget()函数
使用共享内存实现进程通信时,需要首先调用shmget()函数创建一块共享内存区域,如果已经存在了一块共享内存区域,那么,该函数可以打开这个已经存在的共享内存。
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key, size_t, int shmflg)
2)shmat()函数
shmat()函数的功能是将共享内存区域附加到指定进程的地址空间中,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg)
3)shmdt()函数
shmdt()函数的功能是当某一进程不在使用该区域内存时,将使用shmat()函数附加的共享内存区域从该进程的地址空间分离出来。
#include<sys/types.h>
#include<sys/shm.h>
int shmdt(const void *shmaddr)
参数shmaddr为调用shmat()函数附加成功时返回的地址指针。该函数主要实现从shmaddr指针所指向的地址空间中分离出此共享区域,此共享内存区域仍然存在。
4)shmctl()函数
shmctl()函数主要实现了对共享内存区域的多种控制操作。
#include<sys/types.h>
#inlclude<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
8.3.3 共享内存实现进程间通信
8.4 信号量
在介绍共享内存实现进程间通信时,多个进程在同一块共享内存区域中进行读写,会导致数据的取值无法预测。信号量的引入可以解决这一问题。信号量是一种可以对多个进程访问共享资源进行有效控制的机制,它相当于一个整数计数器,统计了可访问的共享资源的单元个数。
8.4.1 信号量的工作原理
信号量的工作原理是:当有一个进程要求使用某一共享内存中的资源时,系统会首先判断该资源的信号量,也就是统计可以访问该资源的单元个数。如果系统判断出该资源信号量值大于0,进程就可以使用该资源,并且信号量要减1,当不再使用该资源时,信号量再加1,方便其他用书使用时,系统对其进行准确的判断。如果该信号量等于0,进程就会进入休眠状态,等候该资源有人使用结束,信号量大于0,这样进程就会被唤醒,对资源进行访问。
在一个进程间通信机制中,信号量由多个信号组成,进程通过一个信号集实现同步,因此通常将信号量称之为信号量集。一个信号量集有与其相对应的结构,用于定义信号量集的对象,这个结构存储了信号量集的各种属性,其定义形式如下:
struct semid_ds
{
struct ipc_perm *sem_perm; /*ipc_perm结构指针*/
struct sem *sem_base; /*sem结构指针*/
ushort sem_nsems; /*信号量个数*/
time_t sem_otime; /*最近一次调用semop()函数的时间*/
time_t sem_ctime; /*最近一次改变该信号量的时间*/
}
sem结构体类型中定义类信号量的一些信息,其定义内容如下:
struct sem
{
ushort semval; /*信号量值*/
pid_t sempid; /*最近一次访问资源的进程ID*/
ushort semncnt; /*等待可用资源出现的进程数*/
ushort semzcnt; /*等待全部资源可被独占的进程数*/
}
8.4.2 信号量的相关操作
由前面介绍的关于信号量的工作原理和信号量的一些属性信息可以知道,信号量并不能实现多个进程间的数据交换,只是起到了一个时间锁的功能。通过系统对信号量的检测,在通信过程中,了解该资源是否可以利用。
1)创建信号量函数semget()
在使用信号量控制进程间同步时,需要首先创建一个信号量集,semget()函数实现了创建一个新的信号量集操作和打开一个已经存在的信号量集的操作,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semget(key_t key, int nsems, int semflg)
2)信号量集操作函数semop()
semop()函数实现的功能是对信号量集中的信号量进行操作。具体的操作内容与该函数的参数的设置有关。
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid, struct sembuf* sops, unsigned nsops)
3)信号量集的控制函数semctl()
对信号量集的控制主要通过semctl()函数实现。例如,通常在使用信号量集时,都要对信号量集中的元素进行初始化,semctl()控制函数就可以实现此功能。
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semctl(int semid, int semnum, int cmd,...)
8.4.3 信号量实现进程间通信
8.5 消息队列
消息队列是一种通过链表结构组织的一组消息,消息时链表中具有一定格式及优先级的数据记录。消息队列与其他两种进程间通信对象(共享内存,信号量)相同,都存放在内核中,多个进程通过消息队列的标识符对消息数据进行传送,实现进程间的通信。
每一个消息队列都有一个与只相对应的结构,用于定义一个消息队列的对象,该结构体类型的定义形式如下:
struct msqid_ds
{
struct ipc_perm msg_perm; /*消息队列的指向ipc_perm结构的指针*/
struct msg *msg_first; /*指向消息队列中第一个消息的指针*/
struct msg *mst_last; /*指向消息队列中最后一个消息的指针*/
ulong msg_ctypes; /*当前消息队列的总字节数*/
ulong msg_qnum; /*总消息数量*/
ulong msg_qbytes; /*消息队列中字节数的上限*/
pid_t msg_lspid; /*最后一个调用msgsnd()函数的进程ID*/
pid_t msg_lrpid; /*最后一个调用msgrcv()函数的进程ID*/
time_t msg_stime; /*组后一次调用msgsnd()函数的时间*/
time_t msg_rtime; /*最后一次调用msgrcv()函数的时间*/
time_t msg_ctime; /*最后一次改变消息队列的时间*/
}
8.5.1 消息队列的相关操作
使用消息队列实现进程间通信,需要首先调用msgget()函数创建一个消息队列,然后调用msgsnd()函数向该消息队列中发送指定的消息,通过msgrcv()函数接收该消息,最后调用msgctl()函数对消息队列进行指定的控制操作。
1)msgget()函数
msgget()函数用于创建一个新的消息队列或打开一个已经存在的消息队列,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgget(key_t key, int msgflg)
2)msgsnd()函数
msgsnd()函数用于向消息队列发送消息,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
3)msgrcv()函数
msgrcv()函数用于接收消息队列中的消息数据:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgrcv(int msgqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
4)msgctl()函数
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgctl(int msgqid, int cmd, struct msqid_ds *buf)
8.5.2 消息队列实现进程间通信
8.6 小结
在本章中,对实现进程间通信的几种方法进行了详细介绍,主要包括管道和命名管道。另外,讲解了在3个System V子系统中实现IPC的方法,有共享内存、信号量和消息队列。掌握了上述几种实现进程间通信的方法,可以更加方便地实现系统内核中多个进程间的数据传输和交换。
通过本章的学习,可以实现通过信息的传递简历几个进程间的联系,更好地协调一个系统中的多个进程之间的行为。