通过查看书籍和博客对IPC(进程间通信)进行梳理。
首先我们明白一个进程其实就是一个狭义上的程序。
管道 (pipe)
管道是UNIX IPC的最老形式,并且所有U N I X系统都提供此种通信机制,管道有两种限制;
(1) 它们是半双工的。数据只能在一个方向上流动。
(2) 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道原型:
管道是由调用pipe函数而创建的。
#include <unistd.h>
int pipe(int filedes [ 2 ]) ;
返回:若成功则为0,若出错则为-1
经由参数 filedes 返回两个文件描述符:filedes [ 0 ]为读而打开,filedes [ 1 ]为写而打开。filedes [ 1 ]
的输出是 filedes [ 0 ]的输入。
有两种方法来描绘一个管道
如下左半图显示了管道的两端在一个进程中相互连接,右半图则说明数据通过内核在管道中流动。
用fork来创建父进程与子进程之间的 IPC 原理
当管道的一端被关闭后,下列规则起作用:
(1) 当读一个写端已被关闭的管道时,在所有数据都被读取后, read返回0,以指示达到了文件结束处(从技术方面考虑,管道的写端还有进程时,就不会产生文件的结束。可以复制一个管道的描述符,使得有多个进程具有写打开文件描述符。但是,通常一个管道只有一个读进程,一个写进程。下一节介绍FIFO时,我们会看到对于一个单一的FIFO常常有多个写进程)。
(2) 如果写一个读端已被关闭的管道,则产生信号 SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write出错返回,errno设置为EPIPE。在写管道时,常数PIPE _ BUF规定了内核中管道缓存器的大小。如果对管道进行 write调用,而且要求写的字节数小于等于 PIPE _ BUF,则此操作不会与其他进程对同一管道(或 FIFO)的write操作穿插进行。但是,若有多个进程同时写一个管道(或 FIFO),而且某个或某些进程要求写的字节数超过PIPE _ BUF字节数,则数据可能会与其他写操作的数据相穿插。
实现代码
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int fd[2]; // 两个文件描述符
7 pid_t pid;
8 char buff[20];
9
10 if(pipe(fd) < 0) // 创建管道
11 printf("Create Pipe Error!\n");
12
13 if((pid = fork()) < 0) // 创建子进程
14 printf("Fork Error!\n");
15 else if(pid > 0) // 父进程
16 {
17 close(fd[0]); // 关闭读端
18 write(fd[1], "hello world\n", 12);
19 }
20 else
21 {
22 close(fd[1]); // 关闭写端
23 read(fd[0], buff, 20);
24 printf("%s", buff);
25 }
26
27 return 0;
28 }
特点:
1、它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。(写端流入,读端流出)
2、它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
3、它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
4、数据一旦被读走,便不存在于管道中。
有名管道 (FIFO)
FIFO有时被称为命名管道。管道只能由相关进程使用,它们共同的祖先进程创建了管道。但是,通过FIFO,不相关的进程也能交换数据。及FIFO是一种文件类型。
原型
1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);
创建命名管道代码:
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
FIFO有两种用途:
(1) FIFO由s h e l l命令使用以便将数据从一条管道线传送到另一条,为此无需创建中间临时
文件。
(2) FIFO用于客户机-服务器应用程序中,以在客户机和服务器之间传递数据。
用FIFO复制输出流
FIFO可被用于复制串行管道命令之间的输出流,于是也就不需要写数据到中间磁盘文件中(类似于使用管道以避免中间磁盘文件)。管道只能用于进程间的线性连接,然而,因为FIFO具有名字,所以它可用于非线性
连接。使用F I F O以及U N I X程序t e e ( 1 ),就可以实现这样的过程而无需使用临时文件。(t e e程序将其标准输入同时复制到其标准输出以及其命令行中包含的命名文件中。)
客户-服务器使用F I F O进行通信
上述例子可以扩展成 客户进程—服务器进程 通信的实例,write_fifo的作用类似于客户端,可以打开多个客户端向一个服务器发送请求信息,read_fifo类似于服务器,它适时监控着FIFO的读端,当有数据时,读出并进行处理,但是有一个关键的问题是,每一个客户端必须预先知道服务器提供的FIFO接口,下图显示了这种安排:
特点:
1、FIFO可以在无关的进程之间交换数据,与无名管道不同。
2、FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
消息队列 (MessageQueue)
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
两个指针msg_first和msg_last分别指向相应消息在内核中的存放位置,所以它们对用户进程而言是无价值的。结构的其他成员是自定义的。
原型
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);
特点
1、消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2、消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3、消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
4、消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享存储
共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种 IPC。使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。若服务器将数据放入共享存储区,则在服务器做完这一操作之前,客户机不应当去取这些数据。
调用函数
一、第一个函数通常是shmget,它获得一个共享存储标识符。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t k e y, int s i z e, int f l a g) ;
返回:若成功则为共享内存 I D,若出错则为-1
• shm_lpid、s h m _ n a t t a c h、s h m _ a t i m e、以及s h m _ d t i m e都设置为0。
• shm_ctime设置为当前时间。
s i z e是该共享存储段的最小值。如果正在创建一个新段(一般在服务器中),则必须指定其s i z e。如果正在存访一个现存的段(一个客户机),则将s i z e指定为0。
二、shmctl函数对共享存储段执行多种操作。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int s h m i d, int c m d, struct shmid_ds * b u f) ;
返回:若成功则为0,若出错则为- 1
• IPC_STAT 对此段取s h m i d _ d s结构,并存放在由b u f指向的结构中。
• IPC_SET 按 b u f指向的结构中的值设置与此段相关结构中的下列三个字段:s h m _ p e r m . u i d、s h m _ p e r m . g i d以及s h m _ p e r m . m o d e。此命令只能由下列两种进程执行:一种是
其有效用户I D等于s h m _ p e r m . c u i d或s h m _ p e r m . u i d的进程;另一种是具有超级用户特权的进程。
• IPC_RMID 从系统中删除该共享存储段。因为每个共享存储段有一个连接计数(s h m _ n a t t c h在s h m i d _ d s结构中),所以除非使用该段的最后一个进程终止或与该段脱接,否则
不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除 ,所以不能再用s h m a t与该段连接。此命令只能由下列两种进程执行 :一种是其有效用户I D等于s h m _ p e r m . c u i d或s h m _ p e r m . u i d的进程;另一种是具有超级用户特权的进程。
• SHM_LOCK 锁住共享存储段。此命令只能由超级用户执行。
• SHM_UNLOCK 解锁共享存储段。此命令只能由超级用户执行。
三、shmat函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int s h m i d, void *a d d r, int f l a g) ;
返回:若成功则为指向共享存储段的指针,若出错则为 -1
(1) 如果a d d r为0,则此段连接到由内核选择的第一个可用地址上。
(2) 如果a d d r非0,并且没有指定S H M _ R N D,则此段连接到a d d r所指定的地址上。
(3) 如果a d d r非0,并且指定了S H M _ R N D,则此段连接到(a d d r-(a d d r mod SHMLBA))所表示的地址上。S H M _ R N D命令的意思是:取整。S H M L B A的意思是:低边界地址倍数,它总是2的乘方。该算式是将地址向下取最近1个S H M L B A的倍数。
信号量 (Semaphore)
信号量与已经介绍过的I P C机构(管道、F I F O以及消息列队)不同。它是一个计数器,用于多进程对共享数据对象的存取。为了获得共享资源,进程需要执行下列操作:
(1) 测试控制该资源的信号量。
(2) 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减 1,表示它使用了一个资源单位。
(3) 若此信号量的值为 0,则进程进入睡眠状态,直至信号量值大于 0。若进程被唤醒后,它返回至(第( 1 )步)。
当进程不再使用由一个信息量控制的共享资源时,该信号量值增 1。如果有进程正在睡眠等待此信号量,则唤醒它们。
为了正确地实现信息量,信号量值的测试及减 1操作应当是原子操作。为此,信号量通常是在内核中实现的。
函数原型
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, ...);
调用函数
一、s e m g e t()以获得一个信号量I D
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t k e y, int n s e m s, int f l a g) ;
返回:若成功则返回信号量 I D,若出错则为-1
将k e y变换为标识符的规则,讨论了是否创建一个新集合,或是引用一个现存的集合。但创建一个新集合时,对s e m i d _ d s结构的下列成员赋初值:
对i p c _ p e r m结构赋初值。该结构中的m o d e被设置为f l a g中的相应许可
权位。这些许可权是常数设置的。
• sem_otime设置为0。
• sem_ctime设置为当前时间。
• sem_nsems设置为n s e m s。
二、s e m c t l函数包含了多种信号量操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int s e m i d, int s e m n u m, int c m d, union semun a rg) ;
最后一个参数是个联合(u n i o n),而非指向一个联合的指针。
s e m n u m值在0和n s e m s-1之间(包括0和n s e m s-1)。
• IPC_STAT 对此集合取s e m i d _ d s结构,并存放在由a rg . b u f指向的结构中。
• IPC_SET 按由a rg . b u f指向的结构中的值设置与此集合相关结构中的下列三个字段值:s e m _ p e r m . u i d , s e m _ p e r m . g i d和s e m _ p e r m . m o d e。此命令只能由下列两种进程执行:一种是其有效用户I D等于s e m _ p e r m . c u i d或s e m _ p e r m . u i d的进程;另一种是具有超级用户特权的进程。
• IPC_RMID 从系统中删除该信号量集合。这种删除是立即的。仍在使用此信号量的
其他进程在它们下次意图对此信号量进行操作时,将出错返回 E I D R M。此命令只能由下列两
种进程执行:一种是具有效用户I D等于s e m _ p e r m . c u i d或s e m _ p e r m . u i d的进程;另一种是具有超
级用户特权的进程。
• GETVAL 返回成员s e m n u m的s e m v a l值。
• SETVAL 设置成员s e m n u m的s e m v a l值。该值由a rg . v a l指定。
• GETPID 返回成员s e m n u m的s e m p i d值。
• GETNCNT 返回成员s e m n u m的s e m n c n t值。
• GETZCNT 返回成员s e m n u m的s e m z c n t值。
• GETALL 取该集合中所有信号量的值,并将它们存放在由a rg . a rr a y指向的数组中。
• SETALL 按a rg . a rr a y指向的数组中的值设置该集合中所有信号量的值。
三、s e m o p自动执行信号量集合上的操作数组
#include <sys/types.h>
# i n c l u d e < s y s / i p c . h >
# i n c l u d e < s y s / s e m . h >
int semop(int s e m i d, struct sembuf s e m o p a rr a y[], size_t n o p s) ;
返回:若成功则为0,若出错则为- 1
对集合中每个成员的操作由相应的s e m _ o p规定。此值可以是负值、0或正值。(下面的讨论将提到信号量的u n d o标志。此标志对应于相应s e m _ f l g成员的S E M _ U N D O位。)
(1) 最易于处理的情况是s e m _ o p为正。这对应于返回进程占用的资源。 s e m _ o p值加到信号量的值上。如果指定了u n d o标志,则也从该进程的此信号量调整值中减去 s e m _ o p。
(2) 若s e m _ o p为负,则表示要获取由该信号量控制的资源。
如若该信号量的值大于或等于 s e m _ o p的绝对值(具有所需的资源),则从信号量值中减去s e m _ o p的绝对值。这保证信号量的结果值大于或等于 0。如果指定了u n d o标志,则s e m _ o p的绝对值也加到该进程的此信号量调整值上。
特点
1、信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2、信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
3、每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
4、支持信号量组。
信号 (Signal)
信号是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。一个键盘中断或者一个错误条件(比如进程试图访问它的虚拟内存中不存在的位置等)都有可能产生一个信号。Shell也使用信号向它的子进程发送作业控制信号。
套接字(Socket)
套接字( socket ) : 套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
Socket通信流程
1命名socket
SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用 struct sockaddr_un 类型的变量。
2 绑定
OCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用 struct sockaddr_un 类型的变量,将相应字段赋值,再将其绑定在创建的服务器套接字上,绑定要使用 bind 系统调用。
3 监听
服务器端套接字创建完毕并赋予本地地址值(名称,本例中为Server Socket)后,需要进行监听,等待客户端连接并处理请求,监听使用 listen 系统调用,接受客户端连接使用accept系统调用。
4 连接服务器
客户端套接字创建完毕并赋予本地地址值后,需要连接到服务器端进行通信,让服务器端为其提供处理服务。对于SOCK_STREAM类型的流式套接字,需要客户端与服务器之间进行连接方可使用。
5 相互发送接收数据
无论客户端还是服务器,都要和对方进行数据上的交互,这种交互也正是我们进程通信的主题。一个进程扮演客户端的角色,另外一个进程扮演服务器的角色,两个进程之间相互发送接收数据,这就是基于本地套接字的进程通信。发送和接收数据要使用 write 和 read 系统调用。
6 断开连接
交互完成后,需要将连接断开以节省资源,使用close系统调用。
参考书籍、博客
https://blog.csdn.net/IT_10/article/details/90174175
https://blog.csdn.net/qq_38880380/article/details/78527115
https://www.cnblogs.com/wxmdevelop/p/6855068.html
《Unix环境高级编程》