该系列文章总纲链接:专题分纲目录 LinuxC 系统编程
本章节思维导图如下所示(思维导图会持续迭代):
第一层:
第二层:
进程间通信的意义在于怎样让多个进程相互之间访问数据,在linux下有很多种方式来实现。
进程间通信就是可以让多个进程可以相互之间访问,包括运行时的数据和对方的代码段,在实际应用中这时很常见的。IPC机制仅仅是为数据通信提供了一种数据传输通道。
进程运行期间,其地址空间相对于其他进程是不可见的(这只是传统进程的观念),在系统中它们是独立的,并且不能相互访问对方。
常见的进程间通信方式有:管道,FIFO管道,信号,信号量,消息队列,共享内存,socket。
对于管道而言有半双工管道和全双工管道:
一种常见的通信方式之一,在两个进程间实现一个数据流通的管道,该管道可以使单向的,也可以是双向的;管道简单易用,但是有很多限制。匿名双工管道在系统中是没有实名的,不可以在文件系统中以任何方式看到该管道,它只是进程的一种资源,会随着进程的结束而被系统清除。管道通信要使用grep命令查找。如下
$ls |grep ipc
管道从数据流动方向上分为全双工管道和半双工管道。全双工管道在具体的实现过程中只是在文件打开的方式上有一点区别(在操作规则上也有些不同,全双工管道要比半双工复杂的多)。
匿名管道没有名字,对于管道中使用的文件描述符没有路径名。也就是不存在任何意义的文件,只是在内存中跟某个索引点相关联的两个文件描述符,它的特点如下:
linux环境下使用pipe函数创建一个匿名半双工管道,函数原型如下:
#include
int pipe(int pipefd[2]);
详细见linux函数参考手册。对于数组fd,并没有和任何又名文件相关联,这也是匿名管道名称的由来。
一般情况下对管道操作都使用一套标准的流程,因此ANSI/ISO C中将以上操作定义在两个标准库函数popen和pclose中,在linux下的函数原型如下:
#include
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
详细见linux函数参考手册。
FIFO又称为有名管道,它是一种文件类型,在系统中可以看到。FIFO的通信方式类似于在进程中好似用文件来传输数据,只不过FIFO类型文件同时具有管道的特性,在数据读出时同时清除数据。在shell中mkfifo命令可以建立有名管道。
创建一个FIFO文件类似于创建文件,FIFO也可以通过路径名来访问。linux下使用mkfifo函数来实现FIFO,函数原型如下:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
详细见linux函数参考手册。
一般的I/O函数都可以用于FIFO文件。但是注意:在使用open函数打开一个FIFO文件时,open函数参数flag标志位的O_NONBLOCK标志,它关系到函数的返回状态。置位与否的逻辑关系如下:
当FIFO的所有进程都已经关闭,则为FIFO的读进程产生一个文件结束符。FIFO的出现,极好地解决了系统在应用过程中产生的大量的中间文件的问题。FIFO可以被shell调用使数据从一个进程到另一个进程,系统不必为该中间通道去烦恼清理不必要的垃圾,或者去释放该通道的资源,它可以被留在后来的进程所使用,并且规避了匿名管道在作用域的限制,可以应用于不相关的进程之间。
对于服务器-客户端 架构 而言,可以对其进行处理,但前提是必须预先知道服务器提供的FIFO接口,如下:
system V IPC包括三种通信机制:消息队列、信号量、共享内存。这是一个古老的方式,在最近的版本中已经被POSIX IPC取代。POSIX IPC和System V IPC 所指一样, 但是所用的函数不同, 实现也不同。IPC机制不同于管道和FIFO。管道和FIFO是基于文件系统的,而IPC是基于内核的,可以使用ipcs来查看系统当前的IPC对象的状态。
IPC对象是活动在内核级别的一种进程间通信的工具。存在的IPC对象通过它的标识符来引用和访问,这个标识符是一个非负整数,它唯一的标识了一个IPC对象,这个IPC对象可以是消息队列、信号量、共享内存中的任意一种类型。
在linux中标识符被声明为整数,所以可能存在的标识符的最大值是65535(2^16)。注意:这里的标识符与文件描述符有所不同,当用open函数打开文件的时候,返回的文件描述符的值是当前进程中最小可用的文件描述符数组的下标。IPC对象删除/创建时相应的标识符的值会不断增大,到最大值后,归零循环分配使用。
IPC的标识符只解决了内部访问一个IPC对象的问题,如何让多个进程都访问某一个特定的IPC对象还需要一个外部键(key),每一个IPC对象都与一个键相关。这样就解决了多进程在一个IPC对象上汇合的问题。
让多个进程都知道有这样的一个键值存在有以下几种方式:
函数ftok可以使用两个参数生成一个键值,函数原型如下:
#include
#include
key_t ftok(const char *pathname, int proj_id);
详细见linux函数参考手册。函数将参数pathname文件中stat结构的st_dev成员和st_ino成员的部分值与参数proj_id的第八位结合起来生成一个键值。由于只是使用st_dev成员和st_ino成员的部分值,所以会丢失信息,不排除两个不同文件使用同一个ID得到同样键值的情况。
系统为每个IPC对象保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者,每个版本的内核内容各有不同的ipc_perm结构体,该结构说明了IPC对象的权限和所有者,每个版本都不同。对于每个IPC对象,系统共用一个struct ipc_perm的数据结构来存放权限信息,以确定一个ipc操作是否可以访问该IPC对象。ipc_perm结构体实现如下:
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
只有root/创建IPC对象的进程有权改变ipc_perm结构的值。IPC对象的缺陷如下:
在shell中使用ipcs命令显示IPC状态。ipcs输出的信息:
删除一个IPC对象命令:
$ipcrm -m shmid号
查看当前系统IPC状态命令:
$ipcs -m
共享内存是所有进程空间通信方式中最快的一种,它是存在于内核级别的资源。在文件系统/proc目录下有对其描述的相应文件。
共享内存的机制所依赖的原理:在系统内核为一个进程分配一个地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存同时分配给不同的进程。对于每一个共享存储段,内核都为其维护一个shmid_ds类型的结构体,shmid_ds结构体的定义如下:
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
结构体shmid_ds会根据不同的系统内核版本而略有不同,并且在不同的系统中会对共享存储段的大小有限制,在应用时要查询相关的手册。
linux下使用shmget函数创建/打开一块共享内存区。shmget函数函数的原型如下:
#include
#include
int shmget(key_t key, size_t size, int shmflg);
详细见linux函数参考手册。
linux下使用shmat函数对一块共享内存区进行连接。shmat函数函数的原型如下:
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg)
详细见linux函数参考手册。
由于共享内存这一特殊的资源类型,操作上不同于普通文件,需要特有的操作函数。linux下使用共享内存进行多种操作。共享内存管理的函数原型如下:
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
详细见linux函数参考手册。注意:
@3 当对共享内存段操作结束时,应调用shmdt函数,断开共享内存连接,函数原型如下:
#include
#include
int shmdt(const void *shmaddr);
详细见linux函数参考手册。
信号量本身不具有数据传输的功能,它只是一种外部资源的标识,通过该标识可以判断外部资源是否可用,信号量在此过程中负责数据的互斥、同步等操作。当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值,以判断相应的资源是否可用;
当进程不再使用一个信号量控制的共享资源时,此信号量的值+1,对信号量的增减均为原子操作,这是由于信号量的主要作用是维护资源的互斥/多进程的同步访问,而在信号量的创建/初始化时,不能保证为原子操作。内核对每个信号集都会设置一个shmid_ds结构,同时用一个无名结构来标识一个信号量。定义因里linux环境的不同而不同。shmid_ds结构定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
shmid_ds数据结构表示每个新建的共享内存。当shmget()创建了一块新的共享内存后,返回一个可以用于引用该共享内存的shmid_ds数据结构的标识符。
同共享内存一样,系统中同样需要为信号量定制一系列专有的操作函数(semget、semctl等)。linux下使用函数semget创建/获得一个信号量集ID,原型如下:
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
详细见linux函数参考手册。
3个IPC对象类型中,信号量集的操作函数相对于其他两个类型的操作函数而言要复杂的多,同样信号量的使用也比其他两个更加广泛。信号量也有自己的专属操作。linux下使用semctl函数来操作,函数原型如下:
#include
#include
#include
int semctl(int semid, int semnum, int cmd, ...);
详细见linux函数参考手册。
消息队列是一种以链表式的结构组织的一组数据,存放在内核中,是由各个进程通过消息队列标识符来引用的一种数据传输方式。它也是由内核来维护,是3个IPC对象中最具有数据操作性的数据传输方式,在消息队列中可以随意根据特定的数据类型来检索消息。当然,为了维护链表,需要更多的内存资源,而且在数据读写上比起共享内存也复杂一些,时间开销也更大一些。
linux下使用msgget函数创建/打开一个队列。函数原型如下:
#include
#include
#include
int msgget(key_t key, int msgflg);
详细见linux函数参考手册。
linux下使用msgctl函数来对消息队列进行控制操作,函数原型如下:
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
详细见linux函数参考手册。
linux下使用msgsnd、msgrcv函数来对消息队列进行读写,函数原型如下:
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); //消息队列写操作
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); //消息队列读操作
详细见linux函数参考手册。