Linux进程间通信方式

1.管道

管道简介

管道包括三种: 
- 普通管道pipe: 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用. 
- 流管道s_pipe: 去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输. 
- 命名管道:name_pipe:去除了第二种限制,可以在许多并不相关的进程之间进行通讯.

管道原理

管道如何通信

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。 
这里写图片描述

管道如何创建

从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。 
这里写图片描述 
那么管道创建后其对应的数据结构是什么呢?

在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图: 
管道数据结构 
有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

流管道

命名管道

由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做命名管道(named PIPE)。

实现原理

FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。

匿名管道和有名管道总结

(1)管道是特殊类型的文件,在满足先入先出的原则条件下可以进行读写,但不能进行定位读写。 
(2)匿名管道是单向的,只能在有亲缘关系的进程间通信;有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 
(3)无名管道阻塞问题:无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则将退出。如果当前进程向无名管道的一端写数据,必须确定另一端有某一进程。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将自动退出。 
(4)有名管道阻塞问题:有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。

信号

信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。

  • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  • 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,直到该进程恢复执行并传递给它为止。
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时信号才被传递给进程。 

    Linux系统中常用信号: 
    (1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。 
    (2)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。 
    (3)SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\\键将产生该信号。 
    (4)SIGBUS和SIGSEGV:进程访问非法地址。 
    (5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。 
    (6)SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。 
    (7)SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。 
    (8)SIGALRM:定时器信号。 
    (9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。 

信号来源

信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源: 
- 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。 
- 软件来源:终止进程信号、其他进程调用kill函数、软件异常产生信号。

信号生命周期和处理流程

(1)信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统; 
(2)操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。 
(3)目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后再恢复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。

消息队列

消息队列是Linux IPC中很常用的一种通信方式,它通常用来在不同进程间发送特定格式的消息数据。 
消息队列和之前讨论过的管道和FIFO有很大的区别,主要有以下两点: 
一个进程向消息队列写入消息之前,并不需要某个进程在该队列上等待该消息的到达,而管道和FIFO是相反的,进程向其中写消息时,管道和FIFO必需已经打开来读,那么内核会产生SIGPIPE信号。 
IPC的持续性不同。管道和FIFO是随进程的持续性,当管道和FIFO最后一次关闭发生时,仍在管道和FIFO中的数据会被丢弃。消息队列是随内核的持续性,即一个进程向消息队列写入消息后,然后终止,另外一个进程可以在以后某个时刻打开该队列读取消息。只要内核没有重新自举,消息队列没有被删除。 
消息队列中的每条消息通常具有以下属性: 
- 一个表示优先级的整数; 
- 消息的数据部分的长度; 
- 消息数据本身

共享内存

实现原理

管道,FIFO,消息队列,他们的共同特点就是通过内核来进行通信(假设POSIX消息队列也是在内核中实现的,因为POSIX标准并没有限定它的实现方式)。向管道,FIFO,消息队列写入数据需要把数据从进程复制到内核,从这些IPC读取数据的时候又需要把数据从内核复制到进程。所以这种IPC方式往往需要2次在进程和内核之间进行数据的复制,即进程间的通信必须借助内核来传递。如下图所示: 
内核IPC通信
共享内存也是一种IPC,它是目前可用IPC中最快的,它是使用方式是将同一个内存区映射到共享它的不同进程的地址空间中,这样这些进程间的通信就不再需要通过内核,只需对该共享的内存区域进程操作就可以了,和其他IPC不同的是,共享内存的使用需要用户自己进行同步操作。下图是共享内存区IPC的通信: 
共享内存IPC通信

api和应用

Linux下有三种共享内存的IPC技术:System V共享内存、共享文件映射(mmap)、POSIX共享内存。 

system V共享内存

共享文件映射

mmap函数主要的功能就是将文件或设备映射到调用进程的地址空间中,当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用read,write等系统调用。在很大程度上提高了系统的效率和代码的简洁性。 
使用mmap函数的主要目的是: 
- 对普通文件提供内存映射I/O,可以提供无亲缘进程间的通信; 
- 提供匿名内存映射,以供亲缘进程间进行通信。 
- 对shm_open创建的POSIX共享内存区对象进程内存映射,以供无亲缘进程间进行通信。 
下面是mmap函数的接口以及说明:

#include <sys/mman.h>  
void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);  
               //成功返回映射到进程地址空间的起始地址,失败返回MAP_FAILED  
start:指定描述符fd应被映射到的进程地址空间内的起始地址,它通常被设置为空指针NULL,这告诉内核自动选择起始地址,该函数的返回值即为fd映射到内存区的起始地址。
len:映射到进程地址空间的字节数,它从被映射文件开头的第offset个字节处开始,offset通常被设置为0。

prot:内存映射区的保护由该参数来设定,通常由以下几个值组合而成:
PROT_READ:数据可读;
 PROT_WRITE:数据可写;
 PROT_EXEC:数据可执行;
 PROT_NONE:数据不可访问;
flags:设置内存映射区的类型标志,POSIX标志定义了以下三个标志:
MAP_SHARED:该标志表示,调用进程对被映射内存区的数据所做的修改对于共享该内存区的所有进程都可见,而且确实改变其底层的支撑对象(一个文件对象或是一个共享内存区对象)。
 MAP_PRIVATE:调用进程对被映射内存区的数据所做的修改只对该进程可见,而不改变其底层支撑对象。
 MAP_FIXED:该标志表示准确的解释start参数,一般不建议使用该标志,对于可移植的代码,应该把start参数置为NULL,且不指定MAP_FIXED标志。
上面三个标志是在POSIX.1-2001标准中定义的,其中MAP_SHARED和MAP_PRIVATE必须选择一个。在Linux中也定义了一些非标准的标志,例如MAP_ANONYMOUS(MAP_ANON),MAP_LOCKED等,具体参考Linux手册。
fd:有效的文件描述符。如果设定了MAP_ANONYMOUS(MAP_ANON)标志,在Linux下面会忽略fd参数,而有的系统实现如BSD需要置fd为-1;
offset:相对文件的起始偏移。
mmap成功后,可以关闭fd,一般也是这么做的,这对该内存映射没有任何影响。 
从进程的地址空间中删除一个映射关系,需要用到下面的函数:
#include <sys/mman.h>  
int munmap(void *start, size_t len);  
                           //成功返回0,出错返回-1  
start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
len:映射区的大小。

对于一个MAP_SHARED的内存映射区,内核的虚拟内存算法会保持内存映射文件和内存映射区的同步,也就是说,对于内存映射文件所对应内存映射区的修改,内核会在稍后的某个时刻更新该内存映射文件。如果我们希望硬盘上的文件内容和内存映射区中的内容实时一致,那么我们就可以调用msync开执行这种同步:

#include <sys/mman.h>  
int msync(void *start, size_t len, int flags);  //成功返回0,出错返回-1  
start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。
len:映射区的大小。
flags:同步标志,有一下三个标志:
MS_ASYNC:异步写,一旦写操作由内核排入队列,就立刻返回;
MS_SYNC:同步写,要等到写操作完成后才返回。
MS_INVALIDATE:使该文件的其他内存映射的副本全部失效。
  • mmap内存映射区的大小 
    Linux下的内存是采用页式管理机制。通过mmap进行内存映射,内核生成的映射区的大小都是以页面大小PAGESIZE为单位,即为PAGESIZE的整数倍。如果mmap映射的长度不是页面大小的整数倍,那么多余空间也会被闲置浪费。
  • mmap实现进程间通信 
    mmap本身提供的进程间通信的两种方式,分别用于无亲缘和亲缘进程间的通信。 
    (1)通过匿名内存映射提供亲缘进程间的通信 
    我们可以通过在父进程fork之前指定MAP_SHARED调用mmap,通过映射一个文件来实现父子进程间的通信,POSIX保证了父进程的内存映射关系保留到子进程中,父子进程对内存映射区的修改双方都可以看到。 
    在Linux 2.4以后,mmap提供匿名内存映射机制,即将mmap的flags参数指定为:MAP_SHARED | MAP_ANON。这样就彻底避免了内存映射文件的创建和打开,简化了对文件的操作。匿名内存映射机制的目的就是为了提供一个穿越父子进程间的内存映射区,很方便的提供了亲缘进程间的通信,

信号量

原始套接字

套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。 
这里写图片描述 
socket是应用层和传输层之间的桥梁

套接字特性

套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。 
(1)套接字的域 
它指定套接字通信中使用的网络介质,最常见的套接字域有两种: 
一是AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。 
另一个域AF_UNIX(AF_LOCAL),表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。 
(2)套接字的端口号 
每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口,这样形成的整体就可以区别每一个套接字。 
(3)套接字协议类型 
因特网提供三种通信机制,

  1. 流套接字(SOCK_STREAM) 
    流套接字在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。
  2. 数据报套接字(SOCK_DGRAM) 
    它不需要建立连接和维持一个连接,它们在域中通常是通过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。
  3. 原始套接字(SOCK_RAW) 
    原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW。 
    原始套接字与标准套接字的区别在于: 
    原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

 

转自:https://blog.csdn.net/godleading/article/details/78391159

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值