linux-ipc

1. 共享内存

现代Linux有两种共享内存机制:

POSIX共享内存(shm_open()、shm_unlink()) System V共享内存(shmget()、shmat()、shmdt()) 其中,System V共享内存历史悠久,一般的UNIX系统上都有这套机制;而POSIX共享内存机制接口更加方便易使用,一般是结合内存映射mmap用。

mmap和System V共享内存的主要区别在于: sysv shm是持久化的,除非被一个进程明确的删除,否则它始终存在于内存里,直到系统关机; mmap映射的内存在不是持久化的,假如进程关闭,映射随即失效,除非事前已经映射到了一个文件上。 内存映射机制mmap是POSIX标准的系统调使用,有匿名映射和文件映射两种。

匿名映射用进程的虚拟内存空间,它和malloc(3)相似,实际上有些malloc实现会用mmap匿名映射分配内存,不过匿名映射不是POSIX标准中规定的。 文件映射有MAP_PRIVATE和MAP_SHARED两种。前者用COW的方式,把文件映射到当前的进程空间,修改操作不会改动源文件。后者直接把文件映射到当前的进程空间,所有的修改会直接反应到文件的page cache,而后由内核自动同步到映射文件上。

相比于IO函数调使用,基于文件的mmap的一大优点是把文件映射到进程的地址空间,避免了数据从使用户缓冲区到内核page cache缓冲区的复制过程;当然还有一个优点就是不需要频繁的read/write系统调使用。

因为接口易使用,且可以方便的persist到文件,避免主机shutdown丢失数据的情况,所以在现代操作系统上一般偏向于用mmap而不是传统的System V的共享内存机制。

建议仅把mmap使用于需要大量内存数据操作的场景,而不使用于IPC。由于IPC总是在多个进程之间通信,而通信则涉及到同步问题,假如自己手工在mmap之上实现同步,容易滋生bug。推荐用socket之类的机制做IPC,基于socket的通信机制相对健全很多,有很多成熟的机制和模式,比方epoll, reactor等。

sysv shm的实现可以参考glibc源码,shm_open(3) 打开一个名为abc的共享内存,等价于open(“/dev/shm/abc”, ..),其中 /dev/shm 是Linux下sysv共享内存的默认挂载点。shm_open调使用返回一个文件形容符,其实也可以给 mmap(2) 用,作为named share memory用。

第一种:mmap方式,适用场景:非持久化, 父子进程之间,创建的内存非常大时 第二种:shmget方式,适用场景:持久化, 同一台电脑上不同进程之间,创建的内存相对较小时

2. 管道 (PIPE)

管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。 管道的特点: 1、管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; 2、只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。 3、单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 4、数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 管道的实现机制:

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

管道只能在本地计算机中使用,而不可用于网络间的通信。

3. 信号 (signal)

信号机制是unix系统中最为古老的进程之间的通信机制,用于一个或几个进程之间传递异步信号。信号可以有各种异步事件产生,比如键盘中断等。shell也可以使用信号将作业控制命令传递给它的子进程。

4. 消息队列(Message queues)

消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容,消息顺序地发送到消息队列中,并以几种不同的方式从队列中获得,每个消息队列可以用IPC标识符唯一地进行识别。内核中的消息队列是通过IPC的标识符来区别,不同的消息队列直接是相互独立的。每个消息队列中的消息,又构成一个独立的链表。 消息队列克服了信号承载信息量少,管道只能承载无格式字符流。

5. 信号量(Semaphore)

信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。 信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号) p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行 V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1 简单理解就是P相当于申请资源,V相当于释放资源

6. 套接字(socket)

套接字机制不但可以单机的不同进程通信,而且使得跨网机器间进程可以通信。 套接字的创建和使用与管道是有区别的,套接字明确地将客户端与服务器区分开来,可以实现多个客户端连到同一服务器。 服务器套接字连接过程描述: 首先,服务器应用程序用socket创建一个套接字,它是系统分配服务器进程的类似文件描述符的资源。 接着,服务器调用bind给套接字命名。这个名字是一个标示符,它允许linux将进入的针对特定端口的连接转到正确的服务器进程。 然后,系统调用listen函数开始接听,等待客户端连接。listen创建一个队列并将其用于存放来自客户端的进入连接。 当客户端调用connect请求连接时,服务器调用accept接受客户端连接,accept此时会创建一个新套接字,用于与这个客户端进行通信。 客户端套接字连接过程描述: 客户端首先调用socket创建一个未命名套接字,让后将服务器的命名套接字作为地址来调用connect与服务器建立连接。 只要双方连接建立成功,我们就可以像操作底层文件一样来操作socket套接字实现通信。