1.进程间通信概述
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。
下面对常见的的IPC技术进行调研,具体如下:
1.1 管道
管道:通常指无名管道,是 UNIX 系统IPC最古老的形式。
特点:
1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
2)它只能用于具有亲缘关系的进程之间的通信。
小结:它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
函数原型:
#include <unistd.h>
int pipe(int fd[2]); // // 创建管道 返回值:若成功返回0,失败返回-1
说明:当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开;
注意:若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。
输出:
小结:只支持父子进程,兄弟进程之间的通信,作为最原始的IPC通信技术,现在略显单薄。
1.2 FIFO
FIFO定义:它是一种文件类型,在文件系统中可以看到。程序中可以查看文件stat结构中st_mode成员的值来判断文件是否是FIFO文件。创建一个FIFO文件类似于创建文件,FIFO文件就像普通文件一样。
特点:
- FIFO可以在无关的进程之间交换数据,与无名管道不同。
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
函数原型:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 创建FIFO成功返回0,出错返回-1
说明:
--pathname是你要打开的管道文件的路径。
--oflag是你要以何种模式打开,这个参数有三个宏可供选择,分别是"O_RDONLY"(只读),"O_WRONLY"(只写),"O_NONBLOCK"(不阻塞)。
1)"O_RDONLY":以只读的方式打开一个管道文件,同时进程将会阻塞,直到有另一个进程以只写的方式打开这个管道文件。
2)"O_RDONLY | O_NONBLOCK":以只读的方式打开一个管道文件,但是进程不会阻塞。
3)"O_WRONLY":以只写的方式打开一个管道文件,同时进程将会阻塞,直到有另一个进程以只读的方式打开这个管道文件。
4)"O_WRONLY | O_NONBLOCK":以只写的方式打开一个管道文件,但是进程不会阻塞。
核心源码示例:
1)client进程
#define FIFO_NAME "demofifo"
result = access( FIFO_NAME, F_OK ) //判断一下管道文件是否存在,不存在就退出程序
fifo_fd = open("fifo1", O_WRONLY)) //以写打开一个FIFO
buffer_len = write( fifo_fd, buffer, strlen( buffer ) ); //向管到文件中写入数据
close( fifo_fd );
1)server进程
#define FIFO_NAME "demofifo"
result = mkfifo( FIFO_NAME, 0777 ); //创建FIFO
fifo_fd = open( FIFO_NAME, O_RDONLY ); // 以只读的方式打开管道文件
buffer_len = read( fifo_fd, buffer, Lenbuffer );
close( fifo_fd );
小结:FIFO中可以很好地解决在无关进程间数据交换的要求,并且由于它们是存在于文件系统中的,这也提供了一种比匿名管道更持久稳定的通信办法。缺点是针对客户端类的请求时,对于服务器来说需要创建同客户端相同数量的FIFO,似的系统负载过大。
利用管道进行数据交互的最好方法就是创建两个管道,而一个管道只负责一个方向通信。这样是因为我们无法梳理数据的读写顺序,尤其是在拥有多个客户端的情况下。也就是说我们无法保证自己写入的数据不被自己读取,或者是自己想要获得数据不被他人读取。这个法则对fifo管道也同样适用。
1.3 消息队列
定义:消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
- 消息队列不同于管道,消息队列是基于数据块的,而管道是基于字节流的,并且消息队列读取不一定要先入先出,可以根据数据类型读取。
- 管道是随进程的,进程结束管道生命周期也就结束,而对于消息队列来说,是随内核的,进程退出,不主动释放,消息队列依然是存在。
函数原型:
#include<sys/msg.h>
int msgget(key_t key, int msgflg); //消息队列的创建或访问
返回值:若成功,返回消息队列ID;若出错返回-1;
说明:与其他IPC机制一样,需要提供一个键值key来命名某个特定的消息队列。可通过ftok()来生成。
msgflg表示消息队列访问权限。可与两个宏配合进行操作:
IPC_CREAT:如果不存在消息队列键值为key那么就创建一个键值为key的消息队列,如果存在,则进行打开此消息队列。
IPC_EXCL:一般与IPC_CREAT一起使用,msgget(key,IPC_CREAT|IPC_EXCL),如果该IPC已存在,则返回-1,一起使用可以保证IPC对象是新创建的不是打开已有对象。
消息队列的相关函数原型:
//将数据放在消息队列中
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
返回值:若成功,返回0;若出错,返回-1;
说明:
1)msqid:是msgget返回的消息队列ID。
2)msgp:是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型;
3)msgsz:是msgp指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msgsz是不包括长整型消息类型成员变量的长度
4)msgflg:用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1。
//读取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
返回值:若成功,返回消息数据部分的长度;若出错,返回-1;
说明:
1)msgtyp
msgtyp==0 返回队列中的第一个消息。
msgtyp>0 返回队列中消息类型为msgtyp的第一个消息。
msgtyp<0 返回队列中消息类型值小于等于msgtyp绝对值的消息,如果有多个,则取类型值最小的消息。
2)msgflg用于控制当队列中没有相应类型的消息时将发生的事情
//消息队列控制函数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
返回值:若成功,返回0;若失败,返回-1
说明:
1)msqid是msgget的返回值。
2)cmd:cmd参数指定将要执行的动作、命令。它可以取三个值。
IPC_STAT:取此消息队列的msqid_ds结构,并将它存放在buf指向的结构里面。
IPC_SET:将字段msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_qbytes从buf指向的结构复制到与这个队列相关的msqid_ds结构中。
IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所以数据。
三个命令也可用于信号量和共享存储中。
3)buf:buf是指向msqid_ds的结构的指针。
核心源码示例:
1)server进程
#define MSG_FILE "/etc/test" // 用于创建一个唯一的key
struct msg_form {long mtype;char mtext[256];};
key = ftok(MSG_FILE,'z') // 获取key值
msqid = msgget(key, IPC_CREAT|0777) // 创建消息队列
msgrcv(msqid, &msg, 256, 888, 0); // 返回类型为888的第一个消息
msg.mtype = 999; //添加消息供客户端接收999消息类型
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
2)client进程
// 消息结构
struct msg_form {long mtype;char mtext[256];};
#define MSG_FILE "/etc/test"
key = ftok(MSG_FILE, 'z') // 获取key值
msqid = msgget(key, IPC_CREAT|0666) // 打开消息队列
msg.mtype = 777;
msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 添加消息,类型为777
msgrcv(msqid, &msg, 256, 999, 0); // 读取类型为999的消息
小结:针对int msgget(key_t key, int msgflg);中的key也采用宏定义的方式直接指定,使用消息队列时,可以看到,不论是客户端还是服务器都需要对消息进程定义(定义需要报纸一直),且都要调用msgget函数,保持消息队列唯一。
优点:消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。消息队列起信箱作用,到了就挂在那里,需要的时候去取。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。
缺点:与管道一样,每个数据块有一个最大长度的限制,系统中所有队列所包含的全部数据块的总长度也有一个上限。
可用ipcs –l查看最大限制,如下如所示
1.4 共享内存
定义:共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
特点:
- 共享内存区是最快的IPC形式。
- 进程映射地址空间后直接操作内存,不再通过执行进入内核的系统调用来传递彼此的数据。
- 与管道、消息队列比较,共享内存
函数原型:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag); // 创建或获取一个共享内存
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
说明:
key:这个共享内存段名字,可以自己指定,也可以通过ftok函数来生成一个随机的key;
size:共享内存大小;
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的;
1、 共享内存的相关函数原型:
//连接共享内存到当前进程的地址空间
void *shmat(int shm_id, const void * shmaddr, int shmflg);
返回值:成功返回指向共享内存的指针,失败返回-1
说明:
shmid: 共享内存标识,就是函数shmget的返回值
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
shmaddr:指定连接的地址
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
断开与共享内存的连接
int shmdt(void *addr);
addr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
//用于控制共享内存
int shmctl(int shm_id, int cmd, struct shmid_ds *buf)
返回值:成功返回0;失败返回-1
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
IPC_STAT:获得共享内存数据结构的信息
IPC_SET :设置共享内存数据结构的信息
IPC_RMID:删除共享内存段
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
优点:
- 多进程使用共享内存,而不需要进行额外系统调用或内核操作;
- 避免了多余的内存拷贝,是效率最高、速度最快的进程间通信方式。
- 相对于上述三种方式,共享内存的空间大小很大;
通过cat命令查询如下:默认的共享内存空间达到32MB,可扩展;
缺点: - 内核并不提供任何对共享内存访问的同步机制;
- 使用共享内存一般还需要使用其他IPC机制(如信号量)进行读写同步与互斥。
小结:共享内存的效率是IPC中最高,除了创建和连接(内存映射),其他操作与单个进程操作无异,唯一需要注意的是,由于共享内存非常大的自由度,在多个进程针对同一个共享内存写操作时,需要注意信号的同步,一般需要和IPC机制(如信号量)进行读写同步与互斥。
1.5 选型小结
IPC对比概要
1)管道:速度慢,容量有限,只有父子进程能通讯,;
2)FIFO:任何进程间都能通讯,但速度慢;
3)消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
4)共享内存区:能够很容易控制容量,速度快,但要保持同步;
5)管道、FIFO、消息队列的内存拷贝需要4步,共享内存内存拷贝需要2步,访问效率共享内存更高。