Linux 进程间通信(IPC)简介
在Linux系统中,以进程为单位分配和管理资源。由于保护的缘故,一个进程不能直接访问另一个进程的资源,也就是说,进程之间互相封闭。但在一个复杂的应用系统中,通常会使用多个相关的进程来共同完成一项任务,因此要求进程之间必须能够互相通信,从而来共享资源和信息。所以,一个操作系统内核必须提供进程间的通信机制(IPC)。进程间通信(IPC: Inter-process communication)有如下一些目的:数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。通知事件���一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。Linux下进程间通信的几种主要手段简介:1. 管道(Pipe)及有名管道(named pipe): 管道可用于具有亲缘关系进程间的通信,有名管道(FIFO)克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可���信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);3. 报文(Message)队列(消息队列): 消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。4. 共享内存(Shared Memory): 使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。6. 套接口(Socket): 更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。PS: 一般来说,linux下的进程包含以下几个关键要素:
- 有一段可执行程序;
- 有专用的系统堆栈空间;
- 内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;
- 具有独立的存储空间
进程和线程有时候并不完全区分,而往往根据上下文理解其含义。
5.1 共享内存(Shared Memory) * 共享内存是最快捷的进程间通信方式。访问共享内存的效率和访问进程自己的非共享内存的效率是相同的,而且这种通信方式不需要任何额外的系统调用。 * 系统不会自动为共享内存处理同步问题,这个问题必须由用户自己解决。 * 共享内存的步骤通常是: o 一个进程申请一块共享内存,即在它的页表中加入新的一项 o 所有进程Attach该共享内存,即从申请内存的进程中拷贝对应的页表 o 使用该内存进行通讯 o 结束后所有进程detach该共享内存 o 申请共享内存的进程在确定所有进程都detach后,释放该内存 * 由于共享内存是通过页表来实现的,我们可以得出一个结论:共享内存的大小是页面大小的整数倍,页面的大小可以通过getpagesize()来得到,通常在Linux下该值是4KB * 相关的API函数: o 申请共享内存:shmget,返回共享内存segment的id o Attach,Detach函数:shmat,shmdt。需要共享内存segment的id o 释放申请的内存:shmctl。一定要记得释放!调用exit和exec会自动detach,但不会自动释放。 * 使用 ipcs -m来观看当前系统存在的共享内存5.2 进程信号量 * 信号量(Semaphore)的概念前面已经介绍过了。Linux对用来同步进程的信号量采取了一种特别的实现方式。这些信号量也就被称为进程信号量(Process Semaphore)。(这一节下面所提到的所有信号量默认都是指进程信号量) * 相关的API函数: o 申请:semget o 释放:semctl。需要注意的是信号量不会被自动释放,我们必须显式释放它。 o Wait和Post:semop * 使用ipcs -s来观看当前系统存在的信号量5.3 内存映射(Mapped Memory) --本文开篇并没提到,可将其归入"共享内存" * 内存映射使得不同的进程可以通过一个共享文件来互相通信。和共享内存几乎相同,除了特工们把地点从内存改成了文件系统。
* 相关的API函数: o 映射:mmap o 同步:msync。用来指定对文件的修改是否被buffer。 o 释放:munmap。在程序结束的时候会自动unmap * mmap的其他用法: o 可以替代read和write,有时使用内存映射后的效率比单纯使用I/O操作来的更快 o 在内存映射文件中构建structure,修改structure再次将文件映射到内存中可以快速的将structure恢复到原来的状态 o 把/dev/zero文件映射到内存中。该文件可以提供无限的0,并且写到该文件的所有内容将被直接丢弃5.4 管道(Pipes) * 管道是单向的,即一个线程写,另一个线程读,无法互换 * 如果写的速度太快,造成管道满了,那么写的线程就会被block;如果读的速度太快,造成管道空了,那么读的进程就会被block。因此事实上我们可以说管道自动实现了同步机制 * 我们可以通过调用pipe函数来生成一对pipe file description。(为什么是一对?因为一个读一个写)。可是,生成的pipe file description无法传送给不相关的进程(因为做为file descriptor即使它拿到了也没法用)。但是我们注意到fork之后父进程所有的file descriptor在子进程中依然有效,因此管道最大的作用是在父子进程之间通信。或者更确切的说,是在有共同祖先的进程之间通信。 * 典型的创建管道的流程如下: o 用pipe生成2个pipe file description(简称fds)。然后调用fork o 在父进程关闭fds[0](或fds[1]),并以只读(或只写)方式打开fds[1](或fds[0])。在子进程中关闭fds[1](或fds[0]),并以只写(或只读)���式打开fds[0](或fds[1])。打开的函数是fdopen。 o 开始通信。结束后用close函数关闭剩下的fds。 * 这里有一个技巧:可以利用管道来达成重定向stdin, stdout和stderr。注意到dup2这个API可以把一个file descriptor复制到另一个上。 * 事实上,我们有一对更为简洁的函数popen/pclose来完成上面的一系列复杂的操作。popen有两个参数: o 第一个参数接受一个exec,子进程将执行这个exec o 第二个参数为”w”或者”r”,”w”表示父进程写子进程读,”r”则反之 o 返回值为管道的一端,也就是一个file descriptor o pclose用来关闭popen返回的file descriptor * FIFO(First In First Out)文件事实上是一个有名字的管道,换句话说,他可以用来让“不相干”的程序互相通信。 o 我们使用mkfifo函数来创建一个FIFO文件 o 我们可以使用任何的低级I/O函数(open, write, read, close等)以及C库I/O函数(fopen, fprintf, fscanf, fclose等)来操作FIFO文件。 * Linux的管道和Windows下的命名管道(Named Pipes)的区别 o Windows的命名管道更像一个套接字(sockets),它可以通过网络让不同主机上的程序进行通信 o Linux的管道允许有多个reader和writer,每个reader和writer进行读/写的最大容量为 PIPE_BUF(4KB),如果有多个writer同时写,他们写的东西会被分为一个一个的chunk(每个4KB)并允许交错写。(例如进程A有两个 chunk,A1,A2。进程B也有两个chunk,B1,B2。A和B同时写,则顺序可能为A1,B1,A2,B2) Windows的管道允许在同一个管道上有多个reader/writer对,他们之间读写的数据没有交叉。5.5 套接字(Sockets) * 套接字的特点: o 它是双向通信的 o 它是进程间通信的,包括其他机器上的进程 * 套接字有三个参数: o 通讯类型(communication style) + 连接(connection)类型:保证所有的包按发送的顺序到达接受方。(类似于电话)如果包丢失或者抵达顺序错误,会自动重发。 + datagram类型:所有包单独发送,可能会出现丢失或者晚发早到的现象。(类似于邮寄) o 命名空间(namespace):描述套接字的地址是如何表示的,例如本地就是文件名,internet上就是ip地址。 o 协议(protocol):通讯协议,常用的有TCI/IP,AppleTalk等。 * 相关的API(套接字也是通过file descriptor来表示的): o socket:创建一个socket o closes:销毁一个socket o connect:在两个socket之间创建一个连接。这个API通常由客户端调用。 o bind:给服务器的一个套��字绑定一个地址,服务器端调用。 o listen:让一个套接字开始侦听,准备接受请求,服务器端调用。 o accept:接受一个连接请求,并且为该连接创建一个新的套接字,服务器端调用。 * 服务器端的生命流程: o 创建一个connection类型的socket o 给该socket绑定一个地址 o 调用listen来enable该socket(listen可以指定最多有多少个请求在等待队列中,如果等待队列满了,又有新的请求到达的时候,则该请求被拒绝) o 对于收到的连接请求调用accept来接受 o 关闭socket * 本地socket(local socket) o 如果是同一台电脑上的两个进程需要通信的话,可以使用本地socket。这种情况下socket的地址是文件路径。注意进程必须对该路径拥有可写权限,否则无法建立连接 o 完成之后使用unlink来关闭一个socket5.6 信号信号本质:信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号来源:信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。进程对信号的响应:进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作,详细情况请参考[2]以及其它资料。注意,进程对实时信号的缺省反应是进程终止。Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。5.7 消息队列:消息队列(也叫做报文队列)能够克服早期unix通信机制的一些缺点。作为早期unix通信机制之一的信号能够传送的信息量有限,后来虽然 POSIX 1003.1b在信号的实时性方面作了拓广,使得信号在传递信息量方面有了相当程度的改进,但是信号这种通信方式更像"即时"的通信方式,它要求接受信号的进程在某个时间范围内对信号做出反应,因此该信号最多在接受信号进程的生命周期内才有意义,信号所传递的信息是接近于随进程持续的概念(process-persistent);管道及有名管道及有名管道则是典型的随进程持续IPC,并且,只能传送无格式的字节流无疑会给应用程序开发带来不便,另外,它的缓冲区大小也受到限制。消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的。目前主要有两种类型的消息队列:POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用。考虑到程序的可移植性,新开发的应用程序应尽量使用POSIX消息队列。消息队列与管道以及有名管道相比,具有更大的灵活性,首先,它提供有格式字节流,有利于减少开发人员的工作量;其次,消息具有类型,在实际应用中,可作为优先级使用。���两点是管道以及有名管道所不能比的。同样,消息队列可以在几个进程间复用,而不管这几个进程是否具有亲缘关系,这一点与有名管道很相似;但消息队列是随内核持续的,与有名管道(随进程持续)相比,生命力更强,应用空间更大。本文仅是一些简单的基础概念,如需了解更多细节的知识,可以到http://www.ibm.com/developerworks/cn
搜索“Linux环境进程间通信”来阅读相关的一些列
该贴被zhou编辑于2012-11-22 9:45:42