进程通信
进程通信指的是进程间的数据传输。进程通信根据交换信息量的多少和效率的高低,分为低级通信(只能传递状态或整数值)和高级通信(提高信号通过的效率,传递大量数据,减轻程序编制的复杂度),其中高级进程的通信分为三种:
- 共享内存模式
- 消息传递模式
- 共享文件模式
所以进程共享的方式可分为以下五种:
1.管道
管道一般是指两个不同的进程之间的通信。当一个进程创建管道,并且调用fork创建自己的子进程之后,父进程关闭读管道端,这样提供了两个进程间数据的流动方式;
特点:
1.它是半双工的,具有固定的读端和写端;
2.它只能用于具有亲缘关系的进程之间通信;
3.它可以看成是一个特殊的文件,对于它的读写也可以使用read、write等函数。但它不属于任何的文件系统,而是只存放在内存上;
如图,fd[0]为读端,fd[1]为写端,父进程通过fd[1]写进管道之后,子进程通过fd[0]读出来;反之,则可以将子进程的数据流入父进程;
2.FIFO(命名管道)
FIFO是一个先进先出的队列。它类似于一个管道,只允许数据单向流通。因为每个FIFO都有一个自己的名字,允许不相关的进程访问同一个FIFO。因此也叫做命名管道;
特点:
- FIFO可以在没有关联的进程之间完成数据流动;
- FIFO有特定的路径名与之相关联,它以一种特殊设备文件存储在文件系统中;
当系统打开一个FIFO时,是否设置了阻塞标志(0_NONBLOCK)的区别:
- 当没有设置0_NONBLOCK(默认),只读open 要阻塞到某个其他进程为写而打开此FIFO;类似的只写open 要阻塞到某个其他进程为读而打开此FIFO;
- 当设置了0_NONBBLOCK时,则只读open立即返回,只写open将出错返回-1,如果没有进程已经为读而打开该FIFO,其error置ENXIO;
服务器相当于一个read_FIFO并实时监控着FIFO的write端,客户机想到于一个write_FIFO,当打开多个客户机向服务器发送数据的时候,服务器会打开读端并将客户机写入的数据进行读取并处理。
3.消息队列
UNIX下不同的进程之间可实现共享资源的一种机制:UNIX允许不同的进程将格式化的数据流以消息形式发送给任意的进程。对消息队列具有操作权限的进程都可以使用msget完成对消息队列的操作控制。通过使用消息队列,进程可以按照任何的顺序读取消息,或为消息安排优先级顺序。
消息队列是消息的链表,存放在内核中。一个消息队列由一个标识符来标识;
特点:
- 消息队列是面向记录的,其中消息具有特定的格式和优先级
- 消息队列独立于发送和接收进程。进程终止时,消息队列及其内容不会被清除;
- 消息队列可以实现消息的随机查询,也可以按照消息的类型查询;
1 #include <sys/msg.h>
2 // 创建或打开消息队列:成功返回队列ID,失败返回-1
3 int msgget(key_t key, int flag);
4 // 添加消息:成功返回0,失败返回-1
5 int msgsnd(int msqid, const void *ptr, size_t size, int flag);
6 // 读取消息:成功返回消息数据的长度,失败返回-1
7 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
8 // 控制消息队列:成功返回0,失败返回-1
9 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
函数msgrcv在读取消息的时候,参数type共有以下几种类型:
- type<0 返回的消息类型值小于或等于type绝对值的消息,如果有多个,则取类型值的最小消息。
- type=0 返回消息队列中第一个消息。
- type>0 返回消息队列中第一个类型为type的消息。
函数msgget在以下两种情况下创建新的消息队列:
- 如果没有与键值对key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
- key的参数为IPC_PRIVATE。
4.信号量
作为进程间通信的一种方式,它不是用于交换大批量的数据,而是用于多进程之间的同步(协调对共享存储段的存取)。他是一个计数器,用于实现进程的同步或互斥,而不是用来存储进程间通信数据。一般和共享内存一起使用
特点:
1.信号量用于进程间的同步,若要在进程间传递数据则需要共享内存;
2.信号量基于操作系统的PV操作,程序对信号量的操作都是原子性的;
3.每次对信号量的操作不限于+1或者-1(二值信号量),可以加减任意的信号量(通用信号量);
4.支持信号量组;
1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int num_sems, int sem_flags);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);
当semget创建新的信号量集合的时候,必须指定集合中的信号量个数(num_sems),通常为1;如果引用一个现有的集合,则将num_sems置为0;
在对信号量操作的函数semop()中,sembuf的定义如下:
1 struct sembuf
2 {
3 short sem_num; // 信号量组中对应的序号,0~sem_nums-1
4 short sem_op; // 信号量值在一次操作中的改变量
5 short sem_flg; // IPC_NOWAIT, SEM_UNDO
6 }
对于参数sem_op,分为以下三种情况:
- sem_op<0 请求sem_op 的绝对值资源;
- sem_op=0 进程阻塞到信号量的相应的值为0;
- sem_op>0 表示进程释放的资源数,将sem_op的值加到信号量上。如果有进程正在休眠等待该信号量,则换成它们;
5.共享内存
通过信号量实现存储共享;(类似于红灯停,绿灯行)
特点:
1.共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
2.因为多个进程可以同时操作,所以需要进行同步。
3.信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
1 #include <sys/shm.h>
2 // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
3 int shmget(key_t key, size_t size, int flag);
4 // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
5 void *shmat(int shm_id, const void *addr, int flag);
6 // 断开与共享内存的连接:成功返回0,失败返回-1
7 int shmdt(void *addr);
8 // 控制共享内存的相关信息:成功返回0,失败返回-1
9 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
- 当shmget创建一段共享内存的时候必须指定size,而如果引用一个存在的共享内存时,指定size=0;
- 当一段共享内存被创建之后,它并不能被任何的进程访问,必须使用shmat函数将共享内存连接到当前进程的地址空间,连接成功后把共享内存的对象映射到调用进程的地址空间,随后就可以向本地进程空间一样访问;
- shmdt用来断开由shmat连接上的地址空间;这并不是指删除地址空间,而是指当前共享空间该进程不能使用;
6.总结
在一般情况下,系统都会使用共享内存+信号量+消息队列的形式来实现进程共享;大概的原理如下:
- 系统开辟一个共享的地址空间供进程使用;
- 进程通过msgrcv/msgsnd给消息队列中读/写共享消息;
- 信号量用来控制进程间的同步;(通过对信号量的加减来控制是同步还是阻塞)
优点 | 缺点 | |
---|---|---|
无名管道 | 简单方便 | 只局限于单项通信且父子进程通信 |
有名管道FIFO | 是无名管道的升级版,可以提供给任何的进程使用 | 长期存在系统内核中,使用不当容易出错 |
消息缓冲 | 不局限于父子进程,允许任意进程通过共享消息队列来实现进程间的通信,并由系统调用函数实现消息接收和发送,从而使得用户在使用消息队列时不用再去考虑同步问题,使用方便 | 信息的复制额外消耗CPU,不宜于信息量大或操作频繁的场合 |
共享内存 | 利用内存缓冲区直接交换消息,无需复制,快捷、信息量大 | 这些进程之间的读写的操作同步问题无法保证,必须在利用其他的同步工具来实现;并且由于系统实体存在于计算机系统中,所以只能处于一个计算机进程的系统共享,不方便网络通信 |
信号量 | 只是一个控制系统进程同步的工具,一般于共享内存一起使用 |