进程间通信

65 篇文章 1 订阅

进程间通信


进程间通信就是在不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问的介质呢?进程的用户空间是互相独立的,一般而言是不能互相访问的,唯一的例外是共享内存区。但是,系统空间却是“公共场所”,所以内核显然可以提供这样的条件。除此以外,那就是双方都可以访问的外设了。在这个意义上,两个进程当然也可以通过磁盘上的普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上这也是进程间通信的手段,但是一般都不把这算作“进程间通信”。

目录

主要分类
  1. 识别
  2. Linux
进程间通信目的:
信号(Signals)
管道(Pipes)
系统V IPC机制(System V IPC Mechanisms)
  1. Message Queues(消息队列)
操作
  1. 举例
  2. 共享内存
  3. 获得共享资源
  4. 其他信息
进程间通信各种方式效率比较
展开

编辑本段主要分类

   进程间通信主要包括管道, 系统IPC(包括消息队列, 信号量,共享存储), SOCKET.
  管道包括三种:1)普通管道PIPE, 通常有种限制,一是半双工,只能单向传输;二是只能在 父子或者兄弟进程间使用. 2)流管道s_pipe: 去除了第一种限制,可以双向传输. 3) 命名管道:name_pipe, 去除了第二种限制,可以在许多并不相关的进程之间进行通讯.

识别

  系统IPC的三种方式类同,都是使用了内核里的 标识符来识别. FAQ1: 管道与 文件描述符,文件指针的关系?
  答: 其实管道的使用方法与文件类似,都能使用read,write,open等普通IO函数. 管道描述符来类似于文件描述符. 事实上, 管道使用的描述符, 文件指针和文件描述符最终都会转化成系统中SOCKET描述符. 都受到系统内核中SOCKET描述符的限制. 本质上LINUX内核源码中管道是通过空文件来实现.
  FAQ2: 管道的使用方法?
  答: 主要有下面几种方法: 1)pipe, 创建一个管道,返回2个管道描述符.通常用于父子进程之间通讯. 2)popen, pclose: 这种方式只返回一个管道描述符,常用于通信另一方是stdin or stdout; 3)mkpipe: 命名管道, 在许多进程之间进行交互.
  FAQ3: 管道与系统IPC之间的优劣比较?
  答: 管道: 优点是所有的UNIX实现都支持, 并且在最后一个访问管道的进程终止后,管道就被完全删除;缺陷是管道只允许单向传输或者用于父子进程之间.
  系统IPC: 优点是功能强大,能在毫不相关进程之间进行通讯; 缺陷是关键字KEY_T使用了内核标识,占用了内核资源,而且只能被显式删除,而且不能使用SOCKET的一些机制,例如select,epoll等.
  FAQ4: WINDOS 进程间通信与LINUX 进程间通信的关系?
  答: 事实上,WINDOS的 进程通信大部分移植于UNIX, WINDOS的 剪贴板,文件映射等都可从UNIX进程通信的共享存储中找到影子.
  FAQ5: 进程间通信与线程间通信之间的关系?
  答: 因为WINDOWS运行的实体是线程, 狭义上的 进程间通信其实是指分属于不同进程的线程之间的通讯.而单个进程之间的线程同步问题可归并为一种特殊的进程通信.它要用到内核支持的 系统调用来保持线程之间同步. 通常用到的一些线程同步方法包括:Event, Mutex, 信号量Semaphore, 临界区资源等.

Linux  的进程间通信(IPC,InterProcess Communication)通信方法有管道、消息队列、信号量、共享内存、套接口等。

编辑本段进程间通信目的:

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

编辑本段信号(Signals)

  信号(Signals )是Unix系统中使用的最古老的 进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。一个键盘中断或者一个错误条件(比如进程试图访问它的虚拟内存中不存在的位置等)都有可能产生一个信号。Shell也使用信号向它的子进程发送作业控制信号。
  信号是在Unix System V中首先引入的,它实现了15种信号,但很不可靠。BSD4.2解决了其中的许多问题,而在BSD4.3中进一步加强和改善了信号机制。但两者的接口不完全兼容。在Posix 1003.1标准中做了一些强行规定,它定义了一个标准的信号接口,但没有规定接口的实现。目前几乎所有的Unix变种都提供了和Posix标准兼容的信号实现机制。
  一、 在一个信号的生命周期中有两个阶段:生成和传送。当一个事件发生时,需要通知一个进程,这时生成一个信号。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。在信号到来和进程对信号进行处理之间,信号在进程上挂起(pending)。
  内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如下:
  l 异常:进程运行过程中出现异常;
  l 其它进程:一个进程可以向另一个或一组进程发送信号;
  l 终端中断:Ctrl-C,Ctrl-\等;
  l 作业控制:前台、后台进程的管理;
  l 分配额:CPU超时或文件大小突破限制;
  l 通知:通知进程某事件发生,如I/O就绪等;
  l 报警:计时器到期。
  在 Linux 中,信号的种类和数目与硬件平台有关。内核用一个字代表所有的信号,每个信号占一位,因此一个字的位数就是系统可以支持的最多信号种类数。i386 平台上有32 种信号,而Alpha AXP 平台上最多可有 64 种信号。系统中有一组定义好的信号,它们可以由内核产生,也可以由系统中其它有权限的进程产生。可以使用kill命令(kill –l)列出系统中的信号集。下面是Linux 在Intel系统中的信号:
  1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
  5) SIGTRAP 6) SIGIOT 7) SIGBUS 8) SIGFPE
  9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
  13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
  18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
  22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
  26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
  30) SIGPWR
  在Alpha AXP Linux系统上,信号的编号有些不同。
  下面是几个常见的信号。
  SIGHUP: 从终端上发出的结束信号;
  SIGINT: 来自键盘的中断信号(Ctrl-C);
  SIGQUIT:来自键盘的退出信号(Ctrl-\);
  SIGFPE: 浮点异常信号(例如浮点运算溢出);
  SIGKILL:该信号结束接收信号的进程;
  SIGALRM:进程的定时器到期时,发送该信号;
  SIGTERM:kill 命令发出的信号;
  SIGCHLD:标识子进程停止或结束的信号;
  SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号;
  …………
  每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:
  l 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。
  l 退出(exit):不产生core文件,直接终止进程。
  l 忽略(ignore):忽略该信号。
  l 停止(stop):挂起该进程。
  l 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。
  进程可以对任何信号指定另一个动作或重载缺省动作,指定的新动作可以是忽略信号。进程也可以暂时地阻塞一个信号。因此进程可以选择对某种信号所采取的特定操作,这些操作包括:
  l 忽略信号:进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略,必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。
  l 阻塞信号:进程可选择阻塞某些信号,即先将到来的某些信号记录下来,等到以后(解除阻塞后)再处理它。
  l 由进程处理该信号:进程本身可在系统中注册处理信号的处理程序地址,当发出该信号时,由注册的处理程序处理信号。
  l 由内核进行缺省处理:信号由内核的缺省处理程序处理,执行该信号的缺省动作。例如,进程接收到SIGFPE(浮点异常)的缺省动作是产生core并退出。大多数情况下,信号由内核处理。
  需要指出的是,对信号的任何处理,包括终止进程,都必须由接收到信号的进程来执行。而进程要执行信号处理程序,就必须等到它真正运行时。因此,对信号的处理可能需要延迟一段时间。
  信号没有固有的优先级。如果为一个进程同时产生了两个信号,这两个信号会以任意顺序出现在进程中并会按任意顺序被处理。另外,也没有机制用于区分同一种类的多个信号。如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。进程无法知道它接收了1个还是42个SIGCONT信号。
  二、 数据结构。Linux用存放在进程的task_struct结构中的信息来实现信号机制,其中包括如下域:
  int sigpending;
  struct signal_struct *sig;
  sigset_t signal, blocked;
  struct signal_queue *sigqueue, **sigqueue_tail;
  l sigpending是一个标记,表示该进程是否有待处理的信号。
  l signal域是一个位图,表示该进程当前所有待处理的信号,每位表示一种信号。某位为1表示进程收到一个相应的信号。
  l blocked域也是一个位图,放着该进程要阻塞的信号掩码,如果该位图的某位为1,表示它对应的信号目前正被进程阻塞。除了SIGSTOP和SIGKILL,所有的信号都可以被阻塞。如果产生了一个被阻塞的信号,它将一直保留等待处理,直到进程被解除阻塞。
  l Sigqueue和sigqueue_tail描述了一个等待处理的信号队列,其中的每一项表示一个待处理信号的具体内容:siginfo_t。
  l sig 是一个signal_struct结构,其中保存进程对每一种可能信号的处理信息,该结构的定义如下:
  struct signal_struct {
  atomic_t count;
  struct k_sigactionaction[_NSIG];
  spinlock_t siglock;
  };
  其关键是action数组,它记录进程对每一种信号的处理信息。其中:
  struct k_sigaction {
  struct sigaction sa;
  };
  struct sigaction {
  __sighandler_t sa_handler;
  unsigned long sa_flags;
  void (*sa_restorer)(void);
  sigset_t sa_mask; /* mask last for extensibility */
  };
  数据结构sigaction中描述的是一个信号处理程序的相关信息,其中:sa_handler是信号处理程序的入口地址,当进程要处理该信号时,它调用这里指出的处理程序;sa_flags是一个标志,告诉Linux该进程是希望忽略这个信号还是让内核处理它;sa_mask是一个阻塞掩码,表示当该处理程序运行时,进程对信号的阻塞情况。即当该信号处理程序运行时,系统要用sa_mask替换进程blocked域的值。
  三、 修改信号处理程序。进程通过执行系统调用sys_signal(定义在kernel/signal.c)可以改变缺省的信号处理例程,这些调用同时改变相应信号的sa_flags和sa_mask。sys_signal的定义如下:
  unsigned long sys_signal(int sig, __sighandler_t handler)
  其中sig是信号类型,handler是该信号的新处理程序。该函数所做的工作非常简单,即将signal_struct 结构中的action [sig-1]处的信号处理程序换成handler,同时将该处的老处理程序返回给用户。
  四、发送信号。向一个进程发送信号由函数send_sig_info完成。该函数的定义如下:
  int send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
  l 所做的主要工作是设置进程t的signal位图中信号sig所对应的位。
  l 如果该信号没有被阻塞(位图blocked中的sig位为0),则将进程t的sigpending域设为1,表示该进程有待处理的信号。
  l 对有些信号,仅仅在signal上设置一位无法将信号的内容完全传达给接收进程,此时就需要用另外一个数据结构来记录这些附加信息。Linux用数据结构signal_queue和siginfo_t来描述这些附加信息。数据结构signal_queue的定义如下:
  struct signal_queue
  {
  struct signal_queue *next;
  siginfo_t info;
  };
  其中的siginfo_t是一个比较复杂的数据结构,它表示的是随着信号一起传递的附加信息,其中的内容随信号种类的不同而不同。如SIGCHLD是子进程用来通知父进程自己要终止的一个信号,该信号就要有附加信息告诉父进程自己的pid、状态等信息。信号处理程序使用该附加信息对相应的信号做适当的处理。
  发送信号所做的第三个工作是为信号的附加信息创建一个signal_queue数据结构,将信息内容记录在该结构的info域中,并将该结构挂在进程t的待处理信号信息结构队列中(由sigqueue和sigqueue_tail表示)。
  并非系统中所有的进程都可以向其它每一个进程发送信号。事实上,只有内核和超级用户可以向任一进程发送信号,普通进程只可以向拥有相同uid和gid的进程或者在相同进程组中的进程发送信号。如上所述,通过设置进程task_struct数据结构中signal域中的适当位来产生信号。如果进程不阻塞该信号,而且它正在等待但是可以中断(状态是Interruptible),那么它的状态被改为Running并被放到运行队列,以此来唤醒该进程。这样调度程序在系统下次调度的时候会把它当作一个运行的候选。如果接收信号的进程处于其它状态(如TASK_UNINTERRUPTIBLE),则只做标记,不立刻唤醒进程。如果需要缺省的处理,Linux可以将对信号的处理优化。例如,如果信号SIGWINCH(X window改变焦点)发生并且使用的是缺省处理程序,则不需要做任何事情。
  五、 处理信号。信号不会一产生就立刻出现在进程中,事实上,它们必须等待直到进程下次运行。在进程从系统调用返回到用户态之前,在进程从中断返回到用户态之前,系统都要检查进程的sigpending标记,如果它非0,说明进程有待处理的信号,于是系统就调用函数do_signal去处理它接收到的信号。这看起来好像非常不可靠,但是,系统中的每一个进程都总是在调用系统调用(如向终端写一个字符等),也总在被中断(如时钟中断等),所以进程处理信号的机会很多。如果愿意,进程可以选择等待信号,它可以在Interruptible状态下挂起,直到有了一个信号到来被唤醒。Linux信号处理代码为每一个当前未阻塞的信号检查sigaction结构,以确定如何处理它。
  函数do_signal的定义如下:
  int do_signal(struct pt_regs *regs, sigset_t *oldset)
  该函数根据当前进程的signal域,确定进程收到了那些信号。对进程收到的每一个信号,从进程的信号等待队列中找到该信号对应的附加信息,从进程的sig域的action数组中找到信号的处理程序及其相关的信息,然后,处理信号。
  如果信号处理程序被设置为缺省动作,则内核会处理它。如SIGSTOP信号的缺省处理是把当前进程的状态改为Stopped,然后运行调度程序,选择一个新的进程来运行。SIGFPE信号的缺省动作是让当前进程产生core(core dump),然后让它退出。
  如果进程自己设置了信号处理程序,则系统调用该处理程序,处理信号。
  有一点必须注意:当前进程运行在核心态,并正准备返回到用户态。因此系统对信号处理程序的调用方法与通常对子程序的调用方法不同,它利用当前进程的堆栈和寄存器。进程的程序计数器被设为它的信号处理程序的首地址,处理程序的参数被加到调用框架结构(call frame )中或者通过寄存器传递。当进程恢复运行的时候就象信号处理程序是正常的子程序调用一样。
  Linux是POSIX兼容的,所以进程可以指定当调用特定的信号处理程序的时候要阻塞的信号。这意味着在调用进程的信号处理程序的时候改变blocked掩码。当信号处理程序结束的时候,blocked掩码必须恢复到它的初始值。因此,Linux在收到信号的进程的堆栈中增加了一个对整理例程的调用,该例程用于把blocked掩码恢复到初始值。Linux也优化了这种情况:如果同时有几个信号处理例程需要调用,就把它们堆积在一起,每次退出一个处理例程的时候就调用下一个,直到最后才调用整理例程。
  六、 除了上述的操作以外,Linux还提供了另外几种对信号的操作,如sys_sigsuspend、sys_rt_sigsuspend、sys_sigaction、sys_sigpending 、sys_sigprocmask 、sys_sigaltstack、sys_sigreturn、sys_rt_sigreturn等,此处不再介绍。
  信号最初的设计目的主要是用来处理错误,内核把进程运行过程中的异常情况和硬件的信息通过信号通知进程。如果进程没有指定对这些信号的处理程序,则内核处理它们,通常是终止进程。作为一种IPC机制,信号有一些局限:
  l 信号的花销太大。发送信号要做系统调用;内核要中断接收进程、要管理它的堆栈、要调用处理程序、要恢复被中断的进程等。
  l 信号种类有限,只有31种,而且信号能传递的信息量十分有限。
  l 信号没有优先级,也没有次数的概念。
  所以,信号对于事件通知很有效,但对于复杂的交互操作却难以胜任。

编辑本段管道(Pipes)

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

管道示意图 (1张)
  右图 --管道示意图所示
  参见include/linux/inode_fs.h
  当写进程向管道写的时候,它使用标准的write库函数。这些库函数(read、write等)要求传递一个文件描述符作为参数。文件描述符是该文件对应的file数据结构在进程的file数据结构数组中的索引,每一个都表示一个打开的文件,在这种情况下,是打开的管道。Linux系统调用使用描述这个管道的file数据结构中f_op所指的write例程,该write例程使用表示管道的VFS I 节点中存放的信息,来管理写请求。如果共享数据页中有足够的空间能把所有的字节都写到管道中,而且管道没有被读进程锁定,则Linux就在管道上为写进程加锁,并把字节从进程的地址空间拷贝到共享数据页。如果管道被读进程锁定或者共享数据页中没有足够的空间,则当前进程被迫睡眠,它被挂在管道I节点的等待队列中等待,而后调用调度程序,让另外一个进程运行。睡眠的写进程是可以中断的(interruptible),所以它可以接收信号。当管道中有了足够的空间可以写数据,或者当锁定解除时,写进程就会被读进程唤醒。当数据写完之后,管道的VFS I 节点上的锁定解除,在管道I节点的等待队列中等待的所有读进程都会被唤醒。
  参见fs/pipe.c pipe_write()
  从管道中读取数据和写数据非常相似。Linux允许进程无阻塞地读文件或管道(依赖于它们打开文件或者管道的模式),这时,如果没有数据可读或者管道被锁定,系统调用会返回一个错误。这意味着进程会继续运行。另一种方式是阻塞读,即进程在管道I节点的等待队列中等待,直到写进程完成。
  如果所有的进程都完成了它们的管道操作,则管道的I节点和相应的共享数据页会被废弃。
  参见fs/pipe.c pipe_read()
  Linux也支持命名管道(也叫FIFO,因为管道工作在先入先出的原则下,第一个写入管道的数据也是第一个被读出的数据)。与管道不同,FIFO不是临时的对象,它们是文件系统中真正的实体,可以用mkfifo命令创建。只要有合适的访问权限,进程就可以使用FIFO。FIFO的打开方式和管道稍微不同。一个管道(它的两个file数据结构、VFS I节点和共享数据页)是一次性创建的,而FIFO已经存在,可以由它的用户打开和关闭。Linux必须处理在写进程打开FIFO之前读进程对它的打开,也必须处理在写进程写数据之前读进程对管道的读。除此以外,FIFO几乎和管道的处理完全一样,而且它们使用一样的数据结构和操作。
  从IPC的角度看,管道提供了从一个进程向另一个进程传输数据的有效方法。但是,管道有一些固有的局限性:
  l 因为读数据的同时也将数据从管道移去,因此,管道不能用来对多个接收者广播数据。
  l 管道中的数据被当作字节流,因此无法识别信息的边界。
  l 如果一个管道有多个读进程,那么写进程不能发送数据到指定的读进程。同样,如果有多个写进程,那么没有办法判断是它们中那一个发送的数据。

编辑本段系统V IPC机制(System V IPC Mechanisms)

  前面讨论的信号和管道虽然可以在进程之间通信,但还有许多应用程序的IPC需求它们不能满足。因此在System V UNIX(1983)中首次引入了另外三种 进程间通信机制(IPC)机制:消息队列、信号灯和共享内存(message queues,semaphores and shared memory)。它们最初的设计目的是满足事务式处理的应用需求,但目前大多数的UNIX供应商(包括基于BSD的供应商)都实现了这些机制。 Linux完全支持Unix System V中的这三种IPC机制。
  System V IPC机制共享通用的认证方式。进程在使用某种类型的IPC资源以前,必须首先通过系统调用创建或获得一个对该资源的引用标识符。进程只能通过系统调用,传递一个唯一的引用标识符到内核来访问这些资源。在每一种机制中,对象的引用标识符都作为它在资源表中的索引。但它不是直接的索引,需要一个简单的操作来从引用标识符产生索引。对于System V IPC对象的访问,使用访问许可权检查,这很象对文件访问时所做的检查。System V IPC对象的访问权限由对象的创建者通过系统调用设置。
  系统中表示System V IPC对象的所有Linux数据结构中都包括一个ipc_perm数据结构,用它记录IPC资源的认证信息。其定义如下:
  struct ipc_perm
  {
  __kernel_key_t key;
  __kernel_uid_t uid;
  __kernel_gid_t gid;
  __kernel_uid_t cuid;
  __kernel_gid_t cgid;
  __kernel_mode_tmode;
  unsigned short seq;
  };
  在ipc_perm数据结构中包括了创建者进程的用户和组标识、所有者进程的用户和组标识、对于这个对象的访问模式(属主、组和其它)以及IPC对象的键值(key)。Linux通过key 来定位System V IPC对象的引用标识符,每个IPC对象都有一个唯一的key。Linux支持两种key:公开的和私有的。如果key是公开的,那么系统中的任何进程,只要通过了权限检查,就可以找到它所对应的System V IPC对象的引用标识符。System V IPC对象不能直接使用key来引用,必须使用它们的引用标识符来引用。(参见include/linux/ipc.h)每种IPC机制都提供一种系统调用,用来将键值(key)转化为对象的引用标识符。
  对所有的System V IPC,Linux提供了一个统一的系统调用:sys_ipc,通过该函数可以实现对System V IPC的所有操作。函数sys_ipc的定义如下:
  int sys_ipc (uint call, int first, int second,
  int third, void *ptr, long fifth)
  这里call是一个32位的整数,其低16位指明了此次调用所要求的工作。对不同的call值,其余各参数的意义也不相同。以下将分别介绍各IPC机制及其所提供的操作。

Message Queues(消息队列)

  消息队列就是消息的一个链表,它允许一个或多个进程向它写消息,一个或多个进程从中读消息。Linux维护了一个消息队列向量表:msgque,来表示系统中所有的消息队列。其定义如下:
  struct msqid_ds *msgque[MSGMNI];
  该向量表中的每一个元素都是一个指向msqid_ds数据结构的指针,而一个msqid_ds数据结构完整地描述了一个消息队列。
  MSGMNI的值是128,就是说,系统中同时最多可以有128个消息队列。
  msqid_ds数据结构的定义如下:
  struct msqid_ds {
  struct ipc_perm msg_perm;
  struct msg *msg_first; /* first message on queue */
  struct msg *msg_last; /* last message in queue */
  __kernel_time_t msg_stime; /* last msgsnd time */
  __kernel_time_t msg_rtime; /* last msgrcv time */
  __kernel_time_t msg_ctime; /* last change time */
  struct wait_queue *wwait;
  struct wait_queue *rwait;
  unsigned short msg_cbytes; /* current number of bytes on queue */
  unsigned short msg_qnum; /* number of messages in queue */
  unsigned short msg_qbytes; /* max number of bytes on queue */
  __kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
  __kernel_ipc_pid_t msg_lrpid; /* last receive pid */
  };
  其中包括:
  l 一个ipc_perm的数据结构(msg_perm域),描述该消息队列的通用认证方式。
  l 一对消息指针(msg_first、msg_last),分别指向该消息队列的队头(第一个消息)和队尾(最后一个消息)(msg)。发送者将新消息加到队尾,接收者从队头读取消息。
  l 三个时间域(msg_stime、msg_rtime、msg_ctime)用于记录队列最后一次发送时间、接收时间和改动时间。
  l 两个进程等待队列(wwait、rwait)分别表示等待向消息队列中写的进程(wwait)和等待从消息队列中读的进程(rwait)。如果某进程向一个消息队列发送消息而发现该队列已满,则进程挂在wwait队列中等待。从该消息队列中读取消息的进程将从队列中删除消息,从而腾出空间,再唤醒wwait队列中等待的进程。如果某进程从一个消息队列中读消息而发现该队列已空,则进程挂在rwait队列中等待。向该消息队列中发送消息的进程将消息加入队列,再唤醒rwait队列中等待的进程。
  l 三个记数域(msg_cbytes、msg_qnum、msg_qbytes)分别表示队列中的当前字节数、队列中的消息数和队列中最大字节数;
  l 两个PID域(msg_lspid、msg_lrpid)分别表示最后一次向该消息队列中发送消息的进程和最后一次从该消息队列中接收消息的进程。
  见右图(参见include/linux/msg.h)
  

System V IPC 机制——消息队列

[1]
图 System V IPC 机制——消息队列
  当创建消息队列时,一个新的msqid_ds数据结构被从系统内存中分配出来,并被插入到msgque 向量表中。
  每当进程试图向消息队列写消息时,它的有效用户和组标识符就要和消息队列的ipc_perm数据结构中的模式域比较。如果进程可以向这个消息队列写(比较成功),则消息会从进程的地址空间拷贝到一个msg数据结构中,该msg数据结构被放到消息队列的队尾。每一个消息都带有进程间约定的、与应用程序相关的类型标记。但是,因为Linux限制了可以写的消息的数量和长度,所以可能会没有空间来容纳该消息。这时,进程会被放到消息队列的写等待队列中,然后调度程序会选择一个新的进程运行。当一个或多个消息从这个消息队列中读出去时,等待的进程会被唤醒。
  从队列中读消息与向队列中写消息是一个相似的过程。进程对消息队列的访问权限一样要被检查。读进程可以选择从队列中读取第一条消息而不管消息的类型,也可以选择从队列中读取特殊类型的消息。如果没有符合条件的消息,读进程会被加到消息队列的读等待队列,然后运行调度程序。当一个新的消息写到消息队列时,这个进程会被唤醒,继续它的运行。
  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是一个缓冲区。
  该函数做如下工作:
  该函数对消息队列做一些控制动作,如:释放队列,获得队列的认证信息,设置队列的认证信息等。
  消息队列和管道提供相似的服务,但消息队列要更加强大并解决了管道中所存在的一些问题。消息队列传递的消息是不连续的、有格式的信息,给对它们的处理带来了很大的灵活性。可以用不同的方式解释消息的类型域,如可以将消息的类型同消息的优先级联系起来,类型域也可以用来指定接收者。
  小消息的传送效率很高,但大消息的传送性能则较差。因为消息传送的过程中要经过从用户空间到内核空间,再从内核空间到用户空间的拷贝,所以,大消息的传送其性能较差。另外,消息队列不支持广播,而且内核不知道消息的接收者。

编辑本段操作

  管道分为有名管道和无名管道,无名管道只能用于亲属进程之间的通信,而有名管道则可用于无亲属关系的进程之间。

举例

  #define INPUT 0
  #define OUTPUT 1
  void main()
  {
  int file_descriptors[2];
  /*定义子进程号 */
  pid_t pid;
  char buf[BUFFER_LEN];
  int returned_count;
  /*创建无名管道*/
  pipe(file_descriptors);
  /*创建子进程*/
  if ((pid = fork()) == - 1)
  {
  printf("Error in fork\n");
  exit(1);
  }
  /*执行子进程*/
  if (pid == 0)
  {
  printf("in the spawned (child) process...\n");
  /*子进程向父进程写数据,关闭管道的读端*/
  close(file_descriptors[INPUT]);
  write(file_descriptors[OUTPUT], "test data", strlen("test data"));
  exit(0);
  }
  else
  {
  /*执行父进程*/
  printf("in the spawning (parent) process...\n");
  /*父进程从管道读取子进程写的数据,关闭管道的写端*/
  close(file_descriptors[OUTPUT]);
  returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
  printf("%d bytes of data received from spawned process: %s\n",
  returned_count, buf);
  }
  }
  上述程序中,无名管道以int pipe(int filedis[2]);方式定义,参数filedis返回两个文件描述符filedes[0]为读而打开,filedes[1]为写而打开,filedes[1]的输出是filedes[0]的输入;
  在Linux系统下,命名管道可由两种方式创建(假设创建一个名为“fifoexample”的有名管道):
  (1)mkfifo("fifoexample","rw");
  (2)mknod fifoexample p
  mkfifo是一个函数,mknod是一个系统调用,即我们可以在shell下输出上述命令。
  有名管道创建后,我们可以像读写文件一样读写之:
  /* 进程一:读有名管道*/
  void main()
  {
  FILE *in_file;
  int count = 1;
  char buf[BUFFER_LEN];
  in_file = fopen("pipeexample", "r");
  if (in_file == NULL)
  {
  printf("Error in fdopen.\n");
  exit(1);
  }
  while ((count = fread(buf, 1, BUFFER_LEN, in_file)) > 0)
  printf("received from pipe: %s\n", buf);
  fclose(in_file);
  }
  /* 进程二:写有名管道*/
  void main()
  {
  FILE *out_file;
  int count = 1;
  char buf[BUFFER_LEN];
  out_file = fopen("pipeexample", "w");
  if (out_file == NULL)
  {
  printf("Error opening pipe.");
  exit(1);
  }
  sprintf(buf, "this is test data for the named pipe example\n");
  fwrite(buf, 1, BUFFER_LEN, out_file);
  fclose(out_file);
  }
  消息队列用于运行于同一台机器上的 进程间通信,与管道相似;

共享内存

  通常由一个进程创建,其余进程对这块内存区进行读写。得到共享内存有两种方式:映射/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本身无法对其做同步,需要程序自己来对共享的内存做出同步计算,而这种同步很多时候就是用信号量实现。

获得共享资源

  本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。信号量,分为互斥信号量,和条件信号量。一般说来,为了获得共享资源,进程需要执行下列操作:
  (1)测试控制该资源的信号量;
  (2)若此信号量的值为正,则允许进行使用该资源,进程将进号量减去所需的资源数;
  (3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1);
  (4)当进程不再使用一个信号量控制的资源时,信号量值加其所占的资源数,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
  下面是一个使用信号量的例子,该程序创建一个特定的IPC结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后清除信号量:
  #include <stdio.h>
  #include <sys/types.h>
  #include <sys/sem.h>
  #include <sys/ipc.h>
  void main()
  {
  key_t unique_key; /* 定义一个IPC关键字*/
  int id;
  struct sembuf lock_it;
  union semun options;
  int i;
  unique_key = ftok(".", 'a'); /* 生成关键字,字符'a'是一个随机种子*/
  /* 创建一个新的信号量集合*/
  id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);
  printf("semaphore id=%d\n", id);
  options.val = 1; /*设置变量值*/
  semctl(id, 0, SETVAL, options); /*设置索引0的信号量*/
  /*打印出信号量的值*/
  i = semctl(id, 0, GETVAL, 0);
  printf("value of semaphore at index 0 is %d\n", i);
  /*下面重新设置信号量*/
  lock_it.sem_num = 0; /*设置哪个信号量*/
  lock_it.sem_op = - 1; /*定义操作,信号量需要资源数1*/
  lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/
  if (semop(id, &lock_it, 1) == - 1)
  {
  printf("can not lock semaphore.\n");
  exit(1);
  }
  i = semctl(id, 0, GETVAL, 0);
  printf("value of semaphore at index 0 is %d\n", i);
  /*清除信号量*/
  semctl(id, 0, IPC_RMID, 0);
  }

其他信息

  套接字通信并不为Linux所专有,在所有提供了TCP/IP协议栈的 操作系统中几乎都提供了socket,而所有这样操作系统,对套接字的编程方法几乎是完全一样的。

编辑本段进程间通信各种方式效率比较

  
类型无连接可靠流控制记录消息类型优先级
普通PIPENYY N
流PIPENYY N
命名PIPE(FIFO)NYY N
消息队列NYY Y
信号量NYY Y
共享存储NYY Y
UNIX流SOCKETNYY N
UNIX数据包SOCKETYYN N
词条图册 更多图册
参考资料
  • 1

    System V IPC 机制——消息队列  

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值