1、linux进程间通信ARM开发板第五章 进程间通信在Linux系统中,以进程为单位分配和管理资源。由于保护的缘故,一个进程不能直接访问另一个进程的资源,也就是说,进程之间互相封闭。但在一个复杂的应用系统中,通常会使用多个相关的进程来共同完成一项任务,因此要求进程之间必须能够互相通信,从而来共享资源和信息。所以,一个操作系统内核必须提供进程间的通信机制(IPC)。进程间通信有如下一些目的: 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。 通知事件:一个进程需要向另一个或
2、一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux支持多种进程间通信(IPC)机制,信号和管道是其中的两种。除此之外,Linux还支持System V 的IPC机制(用首次出现的Unix版本命名)。5.1 信号(Signals)信号(Signals )是Unix系统中使用的最
3、古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。一个键盘中断或者一个错误条件(比如进程试图访问它的虚拟内存中不存在的位置等)都有可能产生一个信号。Shell也使用信号向它的子进程发送作业控制信号。信号是在Unix System V中首先引入的,它实现了15种信号,但很不可靠。BSD4.2解决了其中的许多问题,而在BSD4.3中进一步加强和改善了信号机制。但两者的接口不完全兼容。在Posix 1003.1标准中做了一些强行规定,它定义了一个标准的信号接口,但没有规定接口的实现。目前几乎所有的U
4、nix变种都提供了和Posix标准兼容的信号实现机制。一、 在一个信号的生命周期中有两个阶段:生成和传送。当一个事件发生时,需要通知一个进程,这时生成一个信号。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。在信号到来和进程对信号进行处理之间,信号在进程上挂起(pending)。内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如下: 异常:进程运行过程中出现异常; 其它进程:一个进程可以向另一个或一组进程发送信号; 终端中断:Ctrl-C,Ctrl-等; 作业控制:前台、后台进程的管理; 分配额:CPU超时或文件大小突破限制; 通知:通知进程某事件发生,如I/O
5、就绪等; 报警:计时器到期。在 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) SIGF
6、PE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR213) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO30) SIGPWR 在Alpha AXP Linux系统上,信号的编号有些不同。下面是几个常见的信号。SIGHUP: 从终端
7、上发出的结束信号;SIGINT: 来自键盘的中断信号(Ctrl-C);SIGQUIT:来自键盘的退出信号(Ctrl-);SIGFPE: 浮点异常信号(例如浮点运算溢出);SIGKILL:该信号结束接收信号的进程;SIGALRM:进程的定时器到期时,发送该信号;SIGTERM:kill 命令发出的信号;SIGCHLD:标识子进程停止或结束的信号;SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号;每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作: 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到
8、一个叫做core的文件中,而后终止进程。 退出(exit):不产生core文件,直接终止进程。 忽略(ignore):忽略该信号。 停止(stop):挂起该进程。 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。进程可以对任何信号指定另一个动作或重载缺省动作,指定的新动作可以是忽略信号。进程也可以暂时地阻塞一个信号。因此进程可以选择对某种信号所采取的特定操作,这些操作包括: 忽略信号:进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略,必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。 阻塞信号:进程可选择阻塞某些信号
9、,即先将到来的某些信号记录下来,等到以后(解除阻塞后)再处理它。 由进程处理该信号:进程本身可在系统中注册处理信号的处理程序地址,当发出该信号时,由注册的处理程序处理信号。 由内核进行缺省处理:信号由内核的缺省处理程序处理,执行该信号的缺省动作。例如,进程接收到SIGFPE(浮点异常)的缺省动作是产生core并退出。大多数情况下,信号由内核处理。需要指出的是,对信号的任何处理,包括终止进程,都必须由接收到信号的进程来执行。而进程要执行信号处理程序,就必须等到它真正运行时。因此,对信号的处理可能需要延迟一段时间。信号没有固有的优先级。如果为一个进程同时产生了两个信号,这两个信号会以任意顺序出现在
10、进程中并会按任意顺序被处理。另外,也没有机制用于区分同一种类的多个信号。如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。进程无法知道它接收了1个还是42个SIGCONT信号。二、 数据结构。Linux用存放在进程的task_struct结构中的信息来实现信号机制,其中包括如下域:int sigpending;struct signal_struct *sig;sigset_t signal, blocked;struct signal_queue *sigqueue, *sigqueue_tail; sigpending是一个标记,表示该进程是否有待处理的信号。 si
11、gnal域是一个位图,表示该进程当前所有待处理的信号,每位表示一种信号。某位为1表示进程收到一个相应的信号。 blocked域也是一个位图,放着该进程要阻塞的信号掩码,如果该位图的某位为1,表示它对应的信号目前正被进程阻塞。除了SIGSTOP和SIGKILL,所有的信号都可以被阻塞。如果产生了一个被阻塞的信号,它将一直保留等待处理,直到进程被解除阻塞。 Sigqueue和sigqueue_tail描述了一个等待处理的信号队列,其中的每一项表示一个待处理信号的具体内容:siginfo_t。 sig 是一个signal_struct结构,其中保存进程对每一种可能信号的处理信息,该结构的定义如下:s
12、truct signal_struct atomic_t count; struct k_sigaction action_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 extensibilit
13、y */;数据结构sigaction中描述的是一个信号处理程序的相关信息,其中:sa_handler是信号处理程序的入口地址,当进程要处理该信号时,它调用这里指出的处理程序;sa_flags是一个标志,告诉Linux该进程是希望忽略这个信号还是让内核处理它;sa_mask是一个阻塞掩码,表示当该处理程序运行时,进程对信号的阻塞情况。即当该信号处理程序运行时,系统要用sa_mask替换进程blocked域的值。三、 修改信号处理程序。进程通过执行系统调用sys_signal(定义在kernel/signal.c)可以改变缺省的信号处理例程,这些调用同时改变相应信号的sa_flags和sa_mas
14、k。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)
15、所做的主要工作是设置进程t的signal位图中信号sig所对应的位。 如果该信号没有被阻塞(位图blocked中的sig位为0),则将进程t的sigpending域设为1,表示该进程有待处理的信号。 对有些信号,仅仅在signal上设置一位无法将信号的内容完全传达给接收进程,此时就需要用另外一个数据结构来记录这些附加信息。Linux用数据结构signal_queue和siginfo_t来描述这些附加信息。数据结构signal_queue的定义如下:struct signal_queue struct signal_queue *next; siginfo_t info;其中的siginfo_t
16、是一个比较复杂的数据结构,它表示的是随着信号一起传递的附加信息,其中的内容随信号种类的不同而不同。如SIGCHLD是子进程用来通知父进程自己要终止的一个信号,该信号就要有附加信息告诉父进程自己的pid、状态等信息。信号处理程序使用该附加信息对相应的信号做适当的处理。发送信号所做的第三个工作是为信号的附加信息创建一个signal_queue数据结构,将信息内容记录在该结构的info域中,并将该结构挂在进程t的待处理信号信息结构队列中(由sigqueue和sigqueue_tail表示)。并非系统中所有的进程都可以向其它每一个进程发送信号。事实上,只有内核和超级用户可以向任一进程发送信号,普通进程
17、只可以向拥有相同uid和gid的进程或者在相同进程组中的进程发送信号。如上所述,通过设置进程task_struct数据结构中signal域中的适当位来产生信号。如果进程不阻塞该信号,而且它正在等待但是可以中断(状态是Interruptible),那么它的状态被改为Running并被放到运行队列,以此来唤醒该进程。这样调度程序在系统下次调度的时候会把它当作一个运行的候选。如果接收信号的进程处于其它状态(如TASK_UNINTERRUPTIBLE),则只做标记,不立刻唤醒进程。如果需要缺省的处理,Linux可以将对信号的处理优化。例如,如果信号SIGWINCH(X window改变焦点)发生并且使
18、用的是缺省处理程序,则不需要做任何事情。五、 处理信号。信号不会一产生就立刻出现在进程中,事实上,它们必须等待直到进程下次运行。在进程从系统调用返回到用户态之前,在进程从中断返回到用户态之前,系统都要检查进程的sigpending标记,如果它非0,说明进程有待处理的信号,于是系统就调用函数do_signal去处理它接收到的信号。这看起来好像非常不可靠,但是,系统中的每一个进程都总是在调用系统调用(如向终端写一个字符等),也总在被中断(如时钟中断等),所以进程处理信号的机会很多。如果愿意,进程可以选择等待信号,它可以在Interruptible状态下挂起,直到有了一个信号到来被唤醒。Linux信
19、号处理代码为每一个当前未阻塞的信号检查sigaction结构,以确定如何处理它。函数do_signal的定义如下:int do_signal(struct pt_regs *regs, sigset_t *oldset)该函数根据当前进程的signal域,确定进程收到了那些信号。对进程收到的每一个信号,从进程的信号等待队列中找到该信号对应的附加信息,从进程的sig域的action数组中找到信号的处理程序及其相关的信息,然后,处理信号。如果信号处理程序被设置为缺省动作,则内核会处理它。如SIGSTOP信号的缺省处理是把当前进程的状态改为Stopped,然后运行调度程序,选择一个新的进程来运行。S
20、IGFPE信号的缺省动作是让当前进程产生core(core dump),然后让它退出。如果进程自己设置了信号处理程序,则系统调用该处理程序,处理信号。有一点必须注意:当前进程运行在核心态,并正准备返回到用户态。因此系统对信号处理程序的调用方法与通常对子程序的调用方法不同,它利用当前进程的堆栈和寄存器。进程的程序计数器被设为它的信号处理程序的首地址,处理程序的参数被加到调用框架结构(call frame )中或者通过寄存器传递。当进程恢复运行的时候就象信号处理程序是正常的子程序调用一样。Linux是POSIX兼容的,所以进程可以指定当调用特定的信号处理程序的时候要阻塞的信号。这意味着在调用进程的
21、信号处理程序的时候改变blocked掩码。当信号处理程序结束的时候,blocked掩码必须恢复到它的初始值。因此,Linux在收到信号的进程的堆栈中增加了一个对整理例程的调用,该例程用于把blocked掩码恢复到初始值。Linux也优化了这种情况:如果同时有几个信号处理例程需要调用,就把它们堆积在一起,每次退出一个处理例程的时候就调用下一个,直到最后才调用整理例程。六、 除了上述的操作以外,Linux还提供了另外几种对信号的操作,如sys_sigsuspend、sys_rt_sigsuspend、sys_sigaction、sys_sigpending 、sys_sigprocmask 、sy
22、s_sigaltstack、sys_sigreturn、sys_rt_sigreturn等,此处不再介绍。信号最初的设计目的主要是用来处理错误,内核把进程运行过程中的异常情况和硬件的信息通过信号通知进程。如果进程没有指定对这些信号的处理程序,则内核处理它们,通常是终止进程。作为一种IPC机制,信号有一些局限: 信号的花销太大。发送信号要做系统调用;内核要中断接收进程、要管理它的堆栈、要调用处理程序、要恢复被中断的进程等。 信号种类有限,只有31种,而且信号能传递的信息量十分有限。 信号没有优先级,也没有次数的概念。所以,信号对于事件通知很有效,但对于复杂的交互操作却难以胜任。5.2 管道(Pi
23、pes)普通的Linux shell都允许重定向,而重定向使用的就是管道。例如:$ ls | pr | lpr把命令ls(列出目录中的文件)的输出通过管道连接到命令pr的标准输入上进行分页。最后,命令pr的标准输出通过管道连接到命令lpr的标准输入上,从而在缺省打印机上打印出结果。进程感觉不到这种重定向,它们和平常一样地工作。正是shell建立了进程之间的临时管道。管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供
24、了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。传统上有很多种实现管道的方法,如利用文件系统、利用套接字(sockets)、利用流等。在Linux中,使用两个file数据结构来实现管道。这两个file数据结构中的f_inode(f_dentry)指针指向同一个临时创建的VFS I节点,而该VFS I节点本身又指向内存中的一个物理页,如图5.1所示。两个file数据结构中的f_op指针指向不同的文件操作例程向量表:一个用于向管道中写,另一个用于从管道中读。这种实现方法掩盖了底层实现
25、的差异,从进程的角度来看,读写管道的系统调用和读写普通文件的普通系统调用没什么不同。当写进程向管道中写时,字节被拷贝到了共享数据页,当读进程从管道中读时,字节被从共享页中拷贝出来。Linux必须同步对于管道的存取,必须保证管道的写和读步调一致。Linux使用锁、等待队列和信号(locks,wait queues and signals)来实现同步。图5.1 管道示意图参见include/linux/inode_fs.h当写进程向管道写的时候,它使用标准的write库函数。这些库函数(read、write等)要求传递一个文件描述符作为参数。文件描述符是该文件对应的file数据结构在进程的file
26、数据结构数组中的索引,每一个都表示一个打开的文件,在这种情况下,是打开的管道。Linux系统调用使用描述这个管道的file数据结构中f_op所指的write例程,该write例程使用表示管道的VFS I 节点中存放的信息,来管理写请求。如果共享数据页中有足够的空间能把所有的字节都写到管道中,而且管道没有被读进程锁定,则Linux就在管道上为写进程加锁,并把字节从进程的地址空间拷贝到共享数据页。如果管道被读进程锁定或者共享数据页中没有足够的空间,则当前进程被迫睡眠,它被挂在管道I节点的等待队列中等待,而后调用调度程序,让另外一个进程运行。睡眠的写进程是可以中断的(interruptible),所
27、以它可以接收信号。当管道中有了足够的空间可以写数据,或者当锁定解除时,写进程就会被读进程唤醒。当数据写完之后,管道的VFS I 节点上的锁定解除,在管道I节点的等待队列中等待的所有读进程都会被唤醒。参见fs/pipe.c pipe_write()从管道中读取数据和写数据非常相似。Linux允许进程无阻塞地读文件或管道(依赖于它们打开文件或者管道的模式),这时,如果没有数据可读或者管道被锁定,系统调用会返回一个错误。这意味着进程会继续运行。另一种方式是阻塞读,即进程在管道I节点的等待队列中等待,直到写进程完成。如果所有的进程都完成了它们的管道操作,则管道的I节点和相应的共享数据页会被废弃。参见f
28、s/pipe.c pipe_read()Linux也支持命名管道(也叫FIFO,因为管道工作在先入先出的原则下,第一个写入管道的数据也是第一个被读出的数据)。与管道不同,FIFO不是临时的对象,它们是文件系统中真正的实体,可以用mkfifo命令创建。只要有合适的访问权限,进程就可以使用FIFO。FIFO的打开方式和管道稍微不同。一个管道(它的两个file数据结构、VFS I节点和共享数据页)是一次性创建的,而FIFO已经存在,可以由它的用户打开和关闭。Linux必须处理在写进程打开FIFO之前读进程对它的打开,也必须处理在写进程写数据之前读进程对管道的读。除此以外,FIFO几乎和管道的处理完全
29、一样,而且它们使用一样的数据结构和操作。从IPC的角度看,管道提供了从一个进程向另一个进程传输数据的有效方法。但是,管道有一些固有的局限性: 因为读数据的同时也将数据从管道移去,因此,管道不能用来对多个接收者广播数据。 管道中的数据被当作字节流,因此无法识别信息的边界。 如果一个管道有多个读进程,那么写进程不能发送数据到指定的读进程。同样,如果有多个写进程,那么没有办法判断是它们中那一个发送的数据。5.3 系统V IPC机制(System V IPC Mechanisms)前面讨论的信号和管道虽然可以在进程之间通信,但还有许多应用程序的IPC需求它们不能满足。因此在System V UNIX(
30、1983)中首次引入了另外三种进程间通信机制(IPC)机制:消息队列、信号灯和共享内存(message queues,semaphores and shared memory)。它们最初的设计目的是满足事务式处理的应用需求,但目前大多数的UNIX供应商(包括基于BSD的供应商)都实现了这些机制。 Linux完全支持Unix System V中的这三种IPC机制。System V IPC机制共享通用的认证方式。进程在使用某种类型的IPC资源以前,必须首先通过系统调用创建或获得一个对该资源的引用标识符。进程只能通过系统调用,传递一个唯一的引用标识符到内核来访问这些资源。在每一种机制中,对象的引用标
31、识符都作为它在资源表中的索引。但它不是直接的索引,需要一个简单的操作来从引用标识符产生索引。对于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_t mode; unsigned short seq;在ipc_perm数据结构中包括了创建者进程的用户和组标识、所有者进程的用户和组标识、对于这个对象的访问模式(属主、组和其它)以及IPC对象的键值(key)。Linux通过key 来定位System V IPC对象的引用标识符,每个IPC对象都有一个唯一的key。Linux支持两种key:公开的和私有的。如果key是公开的,那么系统中的任何进程,只要通过了权限检查,就可以找到它所对应的System V IPC对象的引用标识符。System V IPC对象不能直接使用key来引用,必须使用它们
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1