学习笔记38-进程间通信

来源:互联网 发布:淘宝ar buy怎么使用 编辑:程序博客网 时间:2024/06/03 15:41

IPC(Inter Process Communication)

进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。

IPC目的

1)数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
2)共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
4)资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
5)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux支持多种进程间通信(IPC)机制,信号和管道是其中的两种。除此之外,Linux还支持System V 的IPC机制(用首次出现的Unix版本命名)。

IPC方式

1. 无名管道通信
首先,管道是一种半双工的通信方式,数据只能单向流通。
无名管道(pipe)只能在具有亲缘关系的进程间使用。亲缘关系通常是指父子进程关系。
2. 有名管道通信
有名管道(named pipe)也是半双工的通信方式,但是它允许无亲缘关系的进程间通信。
3. 高级管道通信
高级管道(popen)是将另一个程序当做一个新的进程在当前程序中启动,也就是把它当成是当前程序的子进程。

管道
管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
传统上有很多种实现管道的方法,如利用文件系统、利用套接字(sockets)、利用流等。在Linux中,使用两个file数据结构来实现管道。这两个file数据结构中的f_inode(f_dentry)指针指向同一个临时创建的VFS I节点,而该VFS I节点本身又指向内存中的一个物理页,如图5.1所示。两个file数据结构中的f_op指针指向不同的文件操作例程向量表:一个用于向管道中写,另一个用于从管道中读。这种实现方法掩盖了底层实现的差异,从进程的角度来看,读写管道的系统调用和读写普通文件的普通系统调用没什么不同。当写进程向管道中写时,字节被拷贝到了共享数据页,当读进程从管道中读时,字节被从共享页中拷贝出来。Linux必须同步对于管道的存取,必须保证管道的写和读步调一致。Linux使用锁、等待队列和信号(locks,wait queues and signals)来实现同步。

4. 共享内存
共享内存(shared memory)就是映射一段能被其他进程所访问的内存,这段内存是由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。
共享内存通常与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
通常由一个进程创建,其余进程对这块内存区进行读写。得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的是实际的物理内存;常用的方式是通过shmXXX函数族来实现共享内存:
int shmget(key_t key, int size, int flag); /* 获得一个共享存储标识符*/
该函数使得系统分配size大小的内存用作共享内存;
void shmat(int shmid, void *addr, int flag); / 将共享内存连接到自身地址空间中*/
如果一个进程通过fork创建了子进程,则子进程继承父进程的共享内存,既而可以直接对共享内存使用,不过子进程可以自身脱离共享内存。
shmid为shmget函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址。此后,进程可以对此地址进行读写操作访问共享内存。
对于共享内存,linux本身无法对其做同步,需要程序自己来对共享的内存做出同步计算,而这种同步很多时候就是用信号量实现。

5. 信号
信号(signal)用于通知接收进程某个事件已经发生。
信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。一个键盘中断或者一个错误条件(比如进程试图访问它的虚拟内存中不存在的位置等)都有可能产生一个信号。Shell也使用信号向它的子进程发送作业控制信号。
信号是在Unix System V中首先引入的,它实现了15种信号,但很不可靠。BSD4.2解决了其中的许多问题,而在BSD4.3中进一步加强和改善了信号机制。但两者的接口不完全兼容。在Posix 1003.1标准中做了一些强行规定,它定义了一个标准的信号接口,但没有规定接口的实现。目前几乎所有的Unix变种都提供了和Posix标准兼容的信号实现机制。
在一个信号的生命周期中有两个阶段:生成和传送。当一个事件发生时,需要通知一个进程,这时生成一个信号。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。在信号到来和进程对信号进行处理之间,信号在进程上挂起(pending)。
内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如下:

  1. 异常:进程运行过程中出现异常;
  2. 其它进程:一个进程可以向另一个或一组进程发送信号;
  3. 终端中断:Ctrl-C,Ctrl-\等;
  4. 作业控制:前台、后台进程的管理;
  5. 分配额:CPU超时或文件大小突破限制;
  6. 通知:通知进程某事件发生,如I/O就绪等;
  7. 报警:计时器到期。

每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:

  1. 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。
  2. 退出(exit):不产生core文件,直接终止进程。
  3. 忽略(ignore):忽略该信号。
  4. 停止(stop):挂起该进程。
  5. 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。

进程可以对任何信号指定另一个动作或重载缺省动作,指定的新动作可以是忽略信号。进程也可以暂时地阻塞一个信号。因此进程可以选择对某种信号所采取的特定操作,这些操作包括:
忽略信号:进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略,必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。
阻塞信号:进程可选择阻塞某些信号,即先将到来的某些信号记录下来,等到以后(解除阻塞后)再处理它。
由进程处理该信号:进程本身可在系统中注册处理信号的处理程序地址,当发出该信号时,由注册的处理程序处理信号。
由内核进行缺省处理:信号由内核的缺省处理程序处理,执行该信号的缺省动作。例如,进程接收到SIGFPE(浮点异常)的缺省动作是产生core并退出。大多数情况下,信号由内核处理。
需要指出的是,对信号的任何处理,包括终止进程,都必须由接收到信号的进程来执行。而进程要执行信号处理程序,就必须等到它真正运行时。因此,对信号的处理可能需要延迟一段时间。
信号没有固有的优先级。如果为一个进程同时产生了两个信号,这两个信号会以任意顺序出现在进程中并会按任意顺序被处理。另外,也没有机制用于区分同一种类的多个信号。如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。进程无法知道它接收了1个还是42个SIGCONT信号。

6. 信号量
信号量(semophore)是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间一级同一进程内不同线程之间的同步手段。
本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。信号量,分为互斥信号量,和条件信号量。一般说来,为了获得共享资源,进程需要执行下列操作:

(1)测试控制该资源的信号量;
(2)若此信号量的值为正,则允许进行使用该资源,进程将信号量减去所需的资源数;
(3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1);
(4)当进程不再使用一个信号量控制的资源时,信号量值加其所占的资源数,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。

7. 消息队列
消息队列(message queue)是由消息的链表存放在内核中,并由消息队列标示符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓存区大小受限等缺点。
消息队列就是消息的一个链表,它允许一个或多个进程向它写消息,一个或多个进程从中读消息。Linux维护了一个消息队列向量表:msgque,来表示系统中所有的消息队列。
一个消息队列包括:

  1. 一个ipc_perm的数据结构(msg_perm域),描述该消息队列的通用认证方式。
  2. 一对消息指针(msg_first、msg_last),分别指向该消息队列的队头(第一个消息)和队尾(最后一个消息)(msg)。发送者将新消息加到队尾,接收者从队头读取消息。
  3. 三个时间域(msg_stime、msg_rtime、msg_ctime)用于记录队列最后一次发送时间、接收时间和改动时间。
  4. 两个进程等待队列(wwait、rwait)分别表示等待向消息队列中写的进程(wwait)和等待从消息队列中读的进程(rwait)。如果某进程向一个消息队列发送消息而发现该队列已满,则进程挂在wwait队列中等待。从该消息队列中读取消息的进程将从队列中删除消息,从而腾出空间,再唤醒wwait队列中等待的进程。如果某进程从一个消息队列中读消息而发现该队列已空,则进程挂在rwait队列中等待。向该消息队列中发送消息的进程将消息加入队列,再唤醒rwait队列中等待的进程。
  5. 三个记数域(msg_cbytes、msg_qnum、msg_qbytes)分别表示队列中的当前字节数、队列中的消息数和队列中最大字节数;
  6. 两个PID域(msg_lspid、msg_lrpid)分别表示最后一次向该消息队列中发送消息的进程和最后一次从该消息队列中接收消息的进程。

Linux提供了四个消息队列操作。

1.创建或获得消息队列(MSGGET)

在系统调用sys_ipc中call值为MSGGET,调用的函数为sys_msgget。该函数的定义如下:
int sys_msgget (key_t key, int msgflg)
其中key是一个键值,而msgflg是一个标志。
该函数的作用是创建一个键值为key的消息队列,或获得一个键值为key的消息队列的引用标识符。这是使用消息队列的第一步,即获得消息队列的引用标识符,以后就通过该标识符使用这个消息队列。、
工作过程如下:
1) 如果key == IPC_PRIVATE,则申请一块内存,创建一个新的消息队列(数据结构msqid_ds),将其初始化后加入到msgque向量表中的某个空位置处,返回标识符。
2) 在msgque向量表中找键值为key的消息队列,如果没有找到,结果有二:
l msgflg表示不创建新的队列,则错误返回。
l msgflg表示要创建新的队列,则创建新消息队列,创建过程如1)。
3) 如果在msgque向量表中找到了键值为key的消息队列,则有以下情况:
l 如果msgflg表示一定要创建新的消息队列而且不允许有相同键值的队列存在,则错误返回。
l 如果找到的队列是不能用的或已损坏的队列,则错误返回。
l 认证和存取权限检查,如果该队列不允许msgflg要求的存取,则错误返回。
l 正常,返回队列的标识符。

2.发送消息

在系统调用sys_ipc中call值为MSGSND,调用的函数为sys_msgsnd。该函数的定义如下:
int sys_msgsnd (int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg)
其中:msqid是消息队列的引用标识符;
msgp是消息内容所在的缓冲区;
msgsz是消息的大小;
msgflg是标志。
工作过程如下:
1) 该消息队列在向量msgque中的索引是id = (unsigned int) msqid % MSGMNI,认证检查(权限、模式),合法性检查(类型、大小等)。
2) 如果队列已满,以可中断等待状态(TASK_INTERRUPTIBLE)将当前进程挂起在wwait等待队列上。
3) 申请一块空间,大小为一个消息数据结构加上消息大小,在其上创建一个消息数据结构struct msg,将消息缓冲区中的消息内容拷贝到该内存块中消息头的后面(从用户空间拷贝到内核空间)。
4) 将消息数据结构加入到消息队列的队尾,修改队列的相应参数(大小等)。
5) 唤醒在该消息队列的rwait进程队列上等待读的进程。
6) 返回

3.接收消息

在系统调用sys_ipc中call值为MSGRCV,调用的函数为sys_msgrcv。该函数的定义如下:
int sys_msgrcv (int msqid, struct msgbuf *msgp, size_t msgsz,
long msgtyp, int msgflg)
其中:msqid是消息队列的引用标识符;
msgp是接收到的消息将要存放的缓冲区;
msgsz是消息的大小;
msgtyp是期望接收的消息类型;
msgflg是标志。
工作过程如下:
1) 该消息队列在向量msgque中的索引是id = (unsigned int) msqid % MSGMNI,认证检查(权限、模式),合法性检查。
2) 根据msgtyp和msgflg搜索消息队列,情况有二:
l 如果找不到所要的消息,则以可中断等待状态(TASK_INTERRUPTIBLE)将当前进程挂起在rwait等待队列上。
l 如果找到所要的消息,则将消息从队列中摘下,调整队列参数,唤醒该消息队列的wwait进程队列上等待写的进程,将消息内容拷贝到用户空间的消息缓冲区msgp中,释放内核中该消息所占用的空间,返回。

4.消息控制

在系统调用sys_ipc中call值为MSGCTL,调用的函数为sys_msgctl。该函数的定义如下:
int sys_msgctl (int msqid, int cmd, struct msqid_ds *buf)
其中:msqid是消息队列的引用标识符;
cmd是执行命令;
buf是一个缓冲区。
工作过程如下:
该函数对消息队列做一些控制动作,如:释放队列,获得队列的认证信息,设置队列的认证信息等。

消息队列和管道提供相似的服务,但消息队列要更加强大并解决了管道中所存在的一些问题。消息队列传递的消息是不连续的、有格式的信息,给对它们的处理带来了很大的灵活性。可以用不同的方式解释消息的类型域,如可以将消息的类型同消息的优先级联系起来,类型域也可以用来指定接收者。

小消息的传送效率很高,但大消息的传送性能则较差。因为消息传送的过程中要经过从用户空间到内核空间,再从内核空间到用户空间的拷贝,所以,大消息的传送其性能较差。另外,消息队列不支持广播,而且内核不知道消息的接收者。

8. 套接字
套接字(socket)通常用于计算机网络中不同机器之间的通信。套接字通信并不为Linux所专有,在所有提供了TCP/IP协议栈的操作系统中几乎都提供了socket,而所有这样操作系统,对套接字的编程方法几乎是完全一样的。

最后给出各种IPC方式的效率比较:

这里写图片描述

原创粉丝点击