首页 LINUX进程间通信

LINUX进程间通信

举报
开通vip

LINUX进程间通信LINUX进程间通信 第五章 进程间通信 在Linux系统中,以进程为单位分配和管理资源。由于保护的缘故,一个进程不能直接访问另一个进程的资源,也就是说,进程之间互相封闭。但在一个复杂的应用系统中,通常会使用多个相关的进程来共同完成一项任务,因此要求进程之间必须能够互相通信,从而来共享资源和信息。所以,一个操作系统内核必须提供进程间的通信机制(IPC)。 进程间通信有如下一些目的: , 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个 字节到几兆字节之间。 , 共享数据:多个进程想要操...

LINUX进程间通信
LINUX进程间通信 第五章 进程间通信 在Linux系统中,以进程为单位分配和管理资源。由于保护的缘故,一个进程不能直接访问另一个进程的资源,也就是说,进程之间互相封闭。但在一个复杂的应用系统中,通常会使用多个相关的进程来共同完成一项任务,因此要求进程之间必须能够互相通信,从而来共享资源和信息。所以,一个操作系统内核必须提供进程间的通信机制(IPC)。 进程间通信有如下一些目的: , 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个 字节到几兆字节之间。 , 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进 程应该立刻看到。 , 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生 了某种事件(如进程终止时要通知父进程)。 , 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁 和同步机制。 , 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控 制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态 改变。 进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux支持多种进程间通信(IPC)机制,信号和管道是其中的两种。除此之外,Linux还支持System V 的IPC机制(用首次出现的Unix版本命名)。 5.1 信号(Signals) 信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。一个键盘中断或者一个错误条件(比如进程试图访问它的虚拟内存中不存在的位置等)都有可能产生一个信号。Shell也使用信号向它的子进程发送作业控制信号。 信号是在Unix System V中首先引入的,它实现了15种信号,但很不可靠。BSD4.2解决了其中的许多问题,而在BSD4.3中进一步加强和改善了信号机制。但两者的接口不完全兼容。在Posix 1003.1标准中做了一些强行规定,它定义了一个标准的信号接口,但没有规定接口的实现。目前几乎所有的Unix变种都提供了和Posix标准兼容的信号实现机制。 一、 在一个信号的生命周期中有两个阶段:生成和传送。当一个事件发生时,需要通知一个进程,这时生成一个信号。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。在信号到来和进程对信号进行处理之间,信号在进程上挂起(pending)。 内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如 下: , 异常:进程运行过程中出现异常; , 其它进程:一个进程可以向另一个或一组进程发送信号; , 终端中断:Ctrl-C,Ctrl-\等; , 作业控制:前台、后台进程的管理; , 分配额:CPU超时或文件大小突破限制; , 通知:通知进程某事件发生,如I/O就绪等; , 报警:计时器到期。 在 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种缺省的动作: , 异常终止(abort):在进程的当前目录下,把进程的地址空间 内容 财务内部控制制度的内容财务内部控制制度的内容人员招聘与配置的内容项目成本控制的内容消防安全演练内容 、寄存器内 容保存到一个叫做core的文件中,而后终止进程。 , 退出(exit):不产生core文件,直接终止进程。 , 忽略(ignore):忽略该信号。 , 停止(stop):挂起该进程。 , 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。 进程可以对任何信号指定另一个动作或重载缺省动作,指定的新动作可以是忽略信号。进程也可以暂时地阻塞一个信号。因此进程可以选择对某种信号所采取的特定操作,这些操作包括: , 忽略信号:进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略, 必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。 , 阻塞信号:进程可选择阻塞某些信号,即先将到来的某些信号记录下来,等到 以后(解除阻塞后)再处理它。 , 由进程处理该信号:进程本身可在系统中注册处理信号的处理程序地址,当发 出该信号时,由注册的处理程序处理信号。 , 由内核进行缺省处理:信号由内核的缺省处理程序处理,执行该信号的缺省动 作。例如,进程接收到SIGFPE(浮点异常)的缺省动作是产生core并退出。大 多数情况下,信号由内核处理。 需要指出的是,对信号的任何处理,包括终止进程,都必须由接收到信号的进程来执行。而进程要执行信号处理程序,就必须等到它真正运行时。因此,对信号的处理可能需要延迟一段时间。 信号没有固有的优先级。如果为一个进程同时产生了两个信号,这两个信号会以任意顺序出现在进程中并会按任意顺序被处理。另外,也没有机制用于区分同一种类的多个信号。如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。进程无法知道它接收了1个还是42个SIGCONT信号。 二、 数据结构。Linux用存放在进程的task_struct结构中的信息来实现信号机制,其中包括如下域: int sigpending; struct signal_struct *sig; sigset_t signal, blocked; struct signal_queue *sigqueue, **sigqueue_tail; , sigpending是一个标记,表示该进程是否有待处理的信号。 , signal域是一个位图,表示该进程当前所有待处理的信号,每位表示一种信号。 某位为1表示进程收到一个相应的信号。 , blocked域也是一个位图,放着该进程要阻塞的信号掩码,如果该位图的某位为 1,表示它对应的信号目前正被进程阻塞。除了SIGSTOP和SIGKILL,所有的信 号都可以被阻塞。如果产生了一个被阻塞的信号,它将一直保留等待处理,直 到进程被解除阻塞。 , Sigqueue和sigqueue_tail描述了一个等待处理的信号队列,其中的每一项表 示一个待处理信号的具体内容:siginfo_t。 , sig 是一个signal_struct结构,其中保存进程对每一种可能信号的处理信息, 该结构的定义如下: struct 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 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是该信号的新处理程序。该 函数 excel方差函数excelsd函数已知函数     2 f x m x mx m      2 1 4 2拉格朗日函数pdf函数公式下载 所做的工作非常简单,即将signal_struct 结构中的action [sig-1]处的信号处理程序换成handler,同时将该处的老处理程序返回给用户。 四、 发送信号。向一个进程发送信号由函数send_sig_info完成。该函数的定义 如下: int send_sig_info(int sig, struct siginfo *info, struct task_struct *t) , 所做的主要工作是设置进程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是一个比较复杂的数据结构,它表示的是随着信号一起传递的 附加信息,其中的内容随信号种类的不同而不同。如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等,此处不再介绍。 信号最初的 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 目的主要是用来处理错误,内核把进程运行过程中的异常情况和硬件的信息通过信号通知进程。如果进程没有指定对这些信号的处理程序,则内核处理它们,通常是终止进程。作为一种IPC机制,信号有一些局限: , 信号的花销太大。发送信号要做系统调用;内核要中断接收进程、要管理它的 堆栈、要调用处理程序、要恢复被中断的进程等。 , 信号种类有限,只有31种,而且信号能传递的信息量十分有限。 , 信号没有优先级,也没有次数的概念。 所以,信号对于事件通知很有效,但对于复杂的交互操作却难以胜任。 5.2 管道(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)来实现同步。 图5.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的角度看,管道提供了从一个进程向另一个进程传输数据的有效方法。但是,管道有一些固有的局限性: , 因为读数据的同时也将数据从管道移去,因此,管道不能用来对多个接收者广 播数据。 , 管道中的数据被当作字节流,因此无法识别信息的边界。 , 如果一个管道有多个读进程,那么写进程不能发送数据到指定的读进程。同样, 如果有多个写进程,那么没有 办法 鲁班奖评选办法下载鲁班奖评选办法下载鲁班奖评选办法下载企业年金办法下载企业年金办法下载 判断是它们中那一个发送的数据。 5.3 系统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_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来引用,必须使用它们的引用标识符来引用。(参见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机制及其所提供的操作。 5.3.1 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 */ }; 其中包括: , 一个ipc_perm的数据结构(msg_perm域),描述该消息队列的通用认证方式。 , 一对消息指针(msg_first、msg_last),分别指向该消息队列的队头(第一个 消息)和队尾(最后一个消息)(msg)。发送者将新消息加到队尾,接收者从队 头读取消息。 , 三个时间域(msg_stime、msg_rtime、msg_ctime)用于记录队列最后一次发送 时间、接收时间和改动时间。 , 两个进程等待队列(wwait、rwait)分别表示等待向消息队列中写的进程(wwait) 和等待从消息队列中读的进程(rwait)。如果某进程向一个消息队列发送消息而 发现该队列已满,则进程挂在wwait队列中等待。从该消息队列中读取消息的 进程将从队列中删除消息,从而腾出空间,再唤醒wwait队列中等待的进程。 如果某进程从一个消息队列中读消息而发现该队列已空,则进程挂在rwait队 列中等待。向该消息队列中发送消息的进程将消息加入队列,再唤醒rwait队 列中等待的进程。 , 三个记数域(msg_cbytes、msg_qnum、msg_qbytes)分别表示队列中的当前字 节数、队列中的消息数和队列中最大字节数; , 两个PID域(msg_lspid、msg_lrpid)分别表示最后一次向该消息队列中发送 消息的进程和最后一次从该消息队列中接收消息的进程。 见图5.2。(参见include/linux/msg.h)。 图5.2 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的消息队列,如果没有找到,结果有二: , msgflg表示不创建新的队列,则错误返回。 , msgflg表示要创建新的队列,则创建新消息队列,创建过程如1)。 3) 如果在msgque向量表中找到了键值为key的消息队列,则有以下情况: , 如果msgflg表示一定要创建新的消息队列而且不允许有相同键值的队列存 在,则错误返回。 , 如果找到的队列是不能用的或已损坏的队列,则错误返回。 , 认证和存取权限检查,如果该队列不允许msgflg要求的存取,则错误返回。 , 正常,返回队列的标识符。 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搜索消息队列,情况有二: , 如果找不到所要的消息,则以可中断等待状态(TASK_INTERRUPTIBLE)将当 前进程挂起在rwait等待队列上。 , 如果找到所要的消息,则将消息从队列中摘下,调整队列参数,唤醒该消息 队列的wwait进程队列上等待写的进程,将消息内容拷贝到用户空间的消息 缓冲区msgp中,释放内核中该消息所占用的空间,返回。 4. 消息控制 在系统调用sys_ipc中call值为MSGCTL,调用的函数为sys_msgctl。该函数的定义如下: int sys_msgctl (int msqid, int cmd, struct msqid_ds *buf) 其中:msqid是消息队列的引用标识符; cmd是执行命令; buf是一个缓冲区。 该函数做如下工作: 该函数对消息队列做一些控制动作,如:释放队列,获得队列的认证信息,设置队列的认证信息等。 消息队列和管道提供相似的服务,但消息队列要更加强大并解决了管道中所存在的一些问题。消息队列传递的消息是不连续的、有格式的信息,给对它们的处理带来了很大的灵活性。可以用不同的方式解释消息的类型域,如可以将消息的类型同消息的优先级联系起来,类型域也可以用来指定接收者。 小消息的传送效率很高,但大消息的传送性能则较差。因为消息传送的过程中要经过从用户空间到内核空间,再从内核空间到用户空间的拷贝,所以,大消息的传送其性能较差。另外,消息队列不支持广播,而且内核不知道消息的接收者。 5.3.2 信号灯(Semaphores) 一般意义下,信号灯是一个具有整数值的对象,它支持两种操作P()和V()。P()操作减少信号灯的值,如果新的信号灯的值小于0,则操作阻塞;V()操作增加信号灯的值,如果结果值大于或等于0,则唤醒一个等待的进程。通常用信号灯来做进程的同步和 互斥。 最简单形式的信号灯就是内存中一个存储位置,它的取值可以由多个进程检验和设置。至少对于相关的进程来讲,对信号灯的检验和设置操作是不可中断的或者说是原子的:只要启动就不能终止。目前许多处理器提供检验和设置操作指令,如Intel处理器的sete等指令。检验和设置操作的结果是信号灯当前值与设置值的和,可以是正或者负。根据检验和设置操作的结果,一个进程可能必须睡眠直到信号灯的值被另一个进程改变。信号灯可以用于实现临界区(critical regions),就是重要的代码区,同一时刻只能有一个进程运行的代码区域。 比如,有许多协作的进程要从同一个数据文件中读写记录,并且希望对文件的访问必须严格地协调。那么,可以使用一个信号灯,将其初值设为1,用两个信号灯操作(P、V 操作),将进程中对文件操作的代码括起来。第一个信号灯操作检查并把信号灯的值减小,第二个操作检查并增加它。访问文件的第一个进程试图减小信号灯的值,如果它成功(事实上,它肯定成功),信号灯的取值将变为0,这个进程现在可以继续运行并使用数据文件。但是,如果此时另一个进程需要使用这个文件,它也试图减少信号灯的数值,它会失败,因为信号灯的值将要变成-1(但是,信号灯的值仍然保持为0,没有变成-1),这个进程会被挂起直到第一个进程处理完数据文件。当第一个进程处理完数据文件后,它会增加信号灯的值使其重新变为1。现在等待的进程会被唤醒,这次它减小信号灯的尝试会成功。 图5.3 System V IPC 机制——信号灯 与消息队列相似,Linux为系统中所有的信号灯维护一个向量表semary ,其定义如下: struct semid_ds *semary[SEMMNI]; 该向量表中的每个元素都是一个指向semid_ds数据结构的指针,而一个semid_ds 数据结构则描述了一个信号灯数组。SEMMNI的值是128,它限制了系统中同时存在的信号 灯的数组的数量。数据结构semid_ds的定义如下: struct semid_ds { struct ipc_perm sem_perm; /* permissions .. see ipc.h */ __kernel_time_t sem_otime; /* last semop time */ __kernel_time_t sem_ctime; /* last change time */ struct sem *sem_base; /* ptr to first semaphore in array */ struct sem_queue *sem_pending; /* pending operations to be processed */ struct sem_queue **sem_pending_last; /* last pending operation */ struct sem_undo *undo; /* undo requests on this array */ unsigned short sem_nsems; /* no. of semaphores in array */ }; 其中:sem_perm是一个认证; sem_otime是最后一次在该信号灯上执行操作的时间; sem_ctime是信号灯最后一次改变的时间; sem_base是一个信号灯数组,该数组中的每个元素都是一个信号灯; sem_pending和sem_pending_last描述了一个等待队列; undo调整(undo)序列; sem_nsems该信号灯数组中的信号灯数。 由此可见,每一个semid_ds数据结构都表示一个信号灯对象,而System V IPC的 每一个信号灯对象都描述了一个信号灯数组。每一个信号灯数组(semid_ds)中都有一个 域sem_nsems,表示该信号灯数组中信号灯的数量。信号灯由sem数据结构描述。sem_nsems 个sem数据结构组成一个信号灯数组,sem_base域指向该数组。 struct sem { int semval; /* current value */ int sempid; /* pid of last operation */ }; Linux在信号灯上提供三种操作。 1. 象消息队列一样,进程在使用信号灯以前必须创建信号灯或获得对已存在信号灯 的引用标识符,该工作通过系统调用sys_ipc实现,其中的call值为SEMGET。具体实现 该操作的函数是sys_semget,起定义如下: int sys_semget (key_t key, int nsems, int semflg) 该函数根据所给的键值(key)创建或获得一个信号灯引用标识符,它所做的工作如 下: 1) 如果key == IPC_PRIVATE,则: , 在数组semary中找一个空位置。 , 申请一块内存,其大小为sizeof (struct semid_ds) + nsems * sizeof (struct sem),即一个semid_ds数据结构的大小加上nsems个信号灯的大 小。该块内存区的前部用做semid_ds数据结构,剩余部分用做sem数组。 填写semid_ds数据结构数据结构的各个域。将填写好的semid_ds数据结构 加入到数组semary的空位置。唤醒在sem_lock上等待的进程。 , 返回该信号灯的引用标识符。 2) 在数组semary上查找键值为key的信号灯对象(semid_ds数据结构)。有下面 结果: A. 没找到。如果在参数semflg中指明要创建新的信号灯,则如1)所述,创 建一个新的信号灯。否则,错误返回。 B. 找到。如果在参数semflg中指明要创建新的信号灯,而且该种键值的信号 灯不能存在,则错误返回。否则,认证检查,如有错,则错误返回。否则, 返回找到的信号灯的引用标识符。 2. 所有允许在一个System V IPC信号灯对象的信号灯数组上操作的进程,都可以 通过系统调用sys_ipc对它们操作,其对应的call值为SEMOP,对应的函数为sys_semop。 对信号灯的操作,Linux只定义了一个函数,其定义如下: int sys_semop (int semid, struct sembuf *tsops, unsigned nsops) 其中semid是信号灯引用标识符,tsops是在该信号灯上要求执行的一组操作,nsops 是此次要执行的操作的个数。每一个操作都用如下的一个数据结构表示: struct sembuf { unsigned short sem_num; /* semaphore index in array */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ }; 其中sem_num是信号灯数组的一个索引,指明要做该操作的一个具体的信号灯。 sem_op是一个操作值,是要加到当前信号灯上的数值。 sem_flg是一个标志,表示对该次操作的特别要求。 一个sembuf数据结构表示对一个信号灯的一次操作,sembuf数据结构数组tsops表示 对一个信号灯对象上的信号灯数组要做的一组操作。 函数sys_semop就是要在信号灯对象(由semid表示)上完成由tsops指定的一组操 作。它所做的工作如下: 1) 检查参数nsops和semid的合法性。 2) 将tsops数组拷贝到内核。 3) 算出引用标识符semid在数组semary中对应的索引: id = (unsigned int) semid % SEMMNI 4) 检查统计操作数组tsops中指定的各个操作(合法、加、减)。 5) 认证检查。 6) Linux维护对信号灯数组的调整(undo)序列(每个信号灯对象一个)来避免可 能出现的死锁现象。基本思路是,如果实施了这些调整,信号灯就会返回到进 程对其实施操作以前的状态。这些调整被放在sem_undo数据结构中,排在semid_ds 数据结构的队列中(由 undo 域指示),同时也排在使用这些信号灯的进程的task_struct数据结构的队列中。 sem_undo数据结构的定义如下: struct sem_undo { struct sem_undo *proc_next; /* next entry on this process */ struct sem_undo *id_next; /* next entry on this semaphore set */ int semid; /* semaphore set identifier */ short * semadj; /* array of adjustments, one per semaphore */ }; 其中semid是该结构所对应的信号灯对象。Semadj是一个数组,信号灯对象的每个信号灯对应该数组的一个元素,其中记录对信号灯的累计操作的结果(在信号灯上增加或减少数值的总和取反)。 如果一个信号灯操作(sembuf数据结构)的标志sem_flg中指明SEM_UNDO标志,则在做该信号灯操作的同时还要维护一个调整动作。Linux至少为每一个进程的每一个信号灯对象都维护一个sem_undo数据结构。如果请求的进程没有该结构,就在需要的时候为它创建一个。这个新的sem_undo数据结构同时在进程的task_struct数据结构和信号灯队列的semid_ds数据结构的相应队列中排队。对信号灯队列中的信号灯执行 undo 操作的时候,和原操作值相反的值(负值)被加到该信号灯上,这个操作值记录在进程的sem_undo数据结构的调整队列中该信号灯对应的条目上。所以,如果操作值为2,那么这个信号灯的调整条目上记录的是-2。 当进程被删除,比如退出的时候,Linux遍历它的sem_undo数据结构数组,并实施对于信号灯数组的调整。如果信号灯被删除,它的sem_undo数据结构仍旧保留在进程的task_struct队列中,但是相应的信号灯数组标识符被标记为无效。在这种情况下,信号灯清除代码只是简单地废弃这个sem_undo数据结构。 函数sys_semop在此处检查该次操作有没有undo操作,如果有undo操作,则看当前进程有没有就该信号灯对象定义sem_undo数据结构,如没有定义,则申请一块内存,为其定义一个sem_undo数据结构,并将其加入到进程和信号灯对象的相应队列中。 7) 在信号灯对象上做tsops中指定的所有操作(增加指定信号灯的数值,对标志sem_flg中指明SEM_UNDO的操作记录其undo数据等)。只有操作数加上信号灯的当前值大于0或者操作值和信号灯的当前值都是0时,操作才算成功。 8) 如果任意一个信号灯操作失败,只要操作标记不要求系统调用无阻塞返回,Linux就会挂起这个进程。如果进程要挂起,Linux必须保存要进行的信号灯操作的状态并把当前进程放到等待队列中。Linux在堆栈中为每一个有进程等待的信号灯建立一个sem_queue数据结构,并填满它来实现上述过程(保存信号灯操作状态、移动进程队列等)。 struct sem_queue { struct sem_queue * next; /* next entry in the queue */ struct sem_queue ** prev; /* previous entry in the queue, *(q->prev) == q */ struct wait_queue * sleeper; /* sleeping process */ struct sem_undo * undo; /* undo structure */ int pid; /* process id of requesting process */ int status; /* completion status of operation */ struct semid_ds *sma; /* semaphore array for operations */ struct sembuf *sops; /* array of pending operations */ int nsops; /* number of operations */ int alter; /* operation will alter semaphore */ }; 这个新创建的sem_queue数据结构被放到了该信号灯对象的等待队列的结尾(由 semid_ds数据结构中的sem_pending和sem_pending_last指针指出)。当前进 程被放到了这个sem_queue数据结构的等待队列中(由它的sleeper指针指出), 然后调用调度程序,运行另外一个进程。当该进程被唤醒时,再次执行指定的那 组操作,如果此次成功,则将进程从等待队列中摘下,正常返回。 9) 如果所有的信号灯操作都成功,当前的进程就不需要被挂起。Linux继续向前并 把这些操作施加到信号灯数组的合适的成员上。现在Linux必须检查任何睡眠或 者挂起的进程,它们的操作现在可能可以实施。Linux顺序查找在该信号灯上的 操作等待队列(由sem_pending指出)中的每一个成员,检查它的信号灯操作现 在是否可以成功。如果可以,它就把这个sem_queue数据结构从操作等待表中删 除,并把这种信号灯操作施加到信号灯数组的合适的成员上。它唤醒睡眠的进程, 让它在下次调度程序运行的时候可以继续运行。Linux从头到尾检查操作等待队 列,直到无法执行信号灯操作,从而无法唤醒更多的进程为止。 参见include/linux/sem.h,linux/ipc/sem.c 3. Linux在信号灯上实现的第三种操作是对信号灯的控制(call值为SEMCTL的 sys_ipc调用),它由函数sys_semctl实现。控制操作包括获得信号灯的状态、 获得信号灯的值,设置信号灯的值,释放信号灯对象资源等。 信号灯机制为互相操作的进程提供了一种复杂的同步方法。但信号灯也存在一些问 题,如死锁(deadlock)。这发生在一个进程改变了信号灯的值,从而进入一个临界区 (critical region),但是因为崩溃或者被kill而没有离开这个临界区域的情况下。Linux 使用undo操作来避免死锁,如上所述。另一个问题是所有System V IPC机制同时存在的 问题,即必须显式地释放IPC资源,如果某进程忘记释放某IPC资源,则会造成内存垃圾。 5.3.3 共享内存(Shared Memory) 共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域 映射到自己的虚拟地址空间,则这些进程就都可以直接访问该共享内存区域,从而可以通过该区域进行通信。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。这块共享虚拟内存的页面,出现在每一个共享该页面的进程的页表中。但是它不需要在所有进程的虚拟内存中都有相同的虚拟地址。 进程A 进程B 共享内存区域 0x5000 0x7000 0x5000 0x3000 图5.4 共享内存映射图 象所有的 System V IPC对象一样,对于共享内存对象的访问由key控制,并要进行访问权限检查。内存共享之后,对进程如何使用这块内存就不再做检查。它们必须依赖于其它机制,比如System V的信号灯来同步对于共享内存区域的访问。 每一个新创建的共享内存对象都用一个shmid_kernel数据结构来表达。系统中所有的shmid_kernel数据结构都保存在shm_segs向量表中,该向量表的每一个元素都是一个指向shmid_kernel数据结构的指针。shm_segs向量表的定义如下: struct shmid_kernel *shm_segs[SHMMNI]; SHMMNI为128,表示系统中最多可以有128个共享内存对象。 数据结构shmid_kernel的定义如下: struct shmid_kernel { struct shmid_ds u; /* the following are private */ unsigned long shm_npages; /* size of segment (pages) */ unsigned long *shm_pages; /* array of ptrs to frames -> SHMMAX */ struct vm_area_struct *attaches; /* descriptors for attaches */ }; 其中:shm_pages是该共享内存对象的页表,每个共享内存对象一个,它描述了如何把该共享内存区域映射到进程的地址空间的信息。 shm_npages是该共享内存区域的大小,以页为单位。 shmid_ds是一个数据结构,它描述了这个共享内存区的认证信息,字节大小,最后 一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次对它操作的进程, 当前有多少个进程在使用它等信息。其定义如下: struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment (bytes) */ __kernel_time_t shm_atime; /* last attach time */ __kernel_time_t shm_dtime; /* last detach time */ __kernel_time_t shm_ctime; /* last change time */ __kernel_ipc_pid_t shm_cpid; /* pid of creator */ __kernel_ipc_pid_t shm_lpid; /* pid of last operator */ unsigned short shm_nattch; /* no. of current attaches */ unsigned short shm_unused; /* compatibility */ void *shm_unused2; /* ditto - used by DIPC */ void *shm_unused3; /* unused */ }; attaches描述被共享的物理内存对象所映射的各进程的虚拟内存区域。每一个希望 共享这块内存的进程都必须通过系统调用将其粘附(attach)到它的虚拟内存中。这一过 程将为该进程创建了一个新的描述这块共享内存的vm_area_struct数据结构。进程可以 选择共享内存在它的虚拟地址空间的位置,也可以让Linux为它选择一块足够的空闲区域。 这个新的vm_area_struct结构除了要连接到进程的内存结构mm中以外,还被连接 到共享内存数据结构shmid_kernel的一个链表中,该结构中的attaches指针指向该链表。 vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和vm_prev_shared, 用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。其实,虚 拟内存并没有在粘附的时候创建,而要等到第一个进程试图访问它的时候才创建。 图5.5 System V IPC 机制——共享内存 Linux为共享内存提供了四种操作。 1. 共享内存对象的创建或获得。与其它两种IPC机制一样,进程在使用共享内存区域以前,必须通过系统调用sys_ipc (call值为SHMGET)创建一个键值为key的共享内存对象,或获得已经存在的键值为key的某共享内存对象的引用标识符。以后对共享内存对象的访问都通过该引用标识符进行。对共享内存对象的创建或获得由函数sys_shmget完成,其定义如下: int sys_shmget (key_t key, int size, int shmflg) 这里key是表示该共享内存对象的键值,size是该共享内存区域的大小(以字节为单位),shmflg是标志(对该共享内存对象的特殊要求)。 它所做的工作如下: 1) 如果key == IPC_PRIVATE,则创建一个新的共享内存对象。 , 算出size对应的页数,检查其合法性。 , 搜索向量表shm_segs,为新创建的共享内存对象找一个空位置。 , 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区 域大小不包括真正的共享内存区,实际上,要等到第一个进程试图访问 它的时候才真正创建共享内存区。 , 根据该共享内存区所占用的页数,为其申请一块空间用于建立页表(每 页4个字节),将页表清0。 , 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的 空位置。 , 返回该共享内存对象的引用标识符。 2) 在向量表shm_segs中查找键值为key的共享内存对象,结果有三: , 如果没有找到,而且在操作标志shmflg中没有指明要创建新共享内存, 则错误返回,否则创建一个新的共享内存对象。 , 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么 错误返回。 , 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对 象的引用标识符。 共享内存对象的创建者可以控制对于这块内存的访问权限和它的key是公开还是私有。如果有足够的权限,它也可以把共享内存锁定在物理内存中。 参见include/linux/shm.h 2. 粘附。在创建或获得某个共享内存区域的引用标识符后,还必须将共享内存区域映射(粘附)到进程的虚拟地址空间,然后才能使用该共享内存区域。系统调用sys_ipc(call值为SHMAT)用于共享内存区到进程虚拟地址空间的映射,而真正完成粘附动作的是函数sys_shmat,其定义如下: int sys_shmat (int shmid, char *shmaddr, int shmflg, ulong *raddr) 其中:shmid是共享内存对象的引用标识符; shmaddr该共享内存区域在进程的虚拟地址空间对应的虚拟地址; shmflg是映射标志; raddr是实际映射的虚拟空间地址。 该函数所做的工作如下: 1) 根据shmid找到共享内存对象。 2) 如果shmaddr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位 置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则, 就用shmaddr作为映射的虚拟地址。 3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小—3G,不能太接 近堆栈栈顶)。 4) 认证检查。 5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。 6) 检查该内存区域,将其加入到进程的mm结构和该共享内存对象的 vm_area_struct队列中。 共享内存的粘附只是创建一个vm_area_struct数据结构,并将其加入到相应的队列中,此时并没有创建真正的共享内存页。 当进程第一次访问共享虚拟内存的某页时,因为所有的共享内存页还都没有分配,所以会发生一个page fault异常。当Linux处理这个page fault的时候,它找到发生异常的虚拟地址所在的vm_area_struct数据结构。在该数据结构中包含有这类共享虚拟内存的一组处理例程,其中的nopage操作用来处理虚拟页对应的物理页不存在的情况。对共享内存,该操作是shm_nopage(定义在ipc/shm.c中)。该操作在描述这个共享内存的shmid_kernel数据结构的页表 shm_pages中查找发生page fault异常的虚拟地址所对应的页表条目,看共享页是否存在(页表条目为0,表示共享页是第一次使用)。如果不存在,它就分配一个物理页,并为它创建一个页表条目。这个条目不但进入当前进程的页表,同时也存到shmid_kernel数据结构的页表shm_pages中。 当下一个进程试图访问这块内存并得到一个page fault的时候,经过同样的路径,也会走到函数shm_nopage。此时,该函数查看shmid_kernel数据结构的页表shm_pages时,发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问共享内存页的进程使得这一页被创建,而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。 3. 分离。当进程不再需要共享虚拟内存的时候,它们与之分离(detach)。只要仍旧有其它进程在使用这块内存,这种分离就只会影响当前的进程,而不会影响其它进程。当前进程的vm_area_struct数据结构被从shmid_ds中删除,并被释放。当前进程的页表也被更新,共享内存对应的虚拟内存页被标记为无效。当共享这块内存的最后一个进程与之分离时,共享内存页被释放,同时,这块共享内存的shmid_kernel数据结构也被释放。 系统调用sys_ipc(call值为SHMDT)用于共享内存区与进程虚拟地址空间的分离,而真正完成分离动作的是函数sys_shmdt,其定义如下: int sys_shmdt (char *shmaddr) 其中shmaddr是进程要分离的共享页的开始虚拟地址。 该函数搜索进程的内存结构中的所有vm_area_struct数据结构,找到地址shmaddr对应的一个,调用函数do_munmap将其释放。 在函数do_munmap中,将要释放的vm_area_struct数据结构从进程的虚拟内存中摘下,清除它在进程页表中对应的页表项(可能占多个页表项),调用该共享内存数据结构vm_area_struct的操作例程中的close操作(此处为shm_close)做进一步的处理。 在函数shm_close(定义在ipc/shm.c中)中,找到该共享内存对象在向量表shm_segs中的索引,从而找到该共享内存对象,将该共享内存在当前进程中对应的vm_area_struct数据结构从对象的共享内存区域链表(由vm_next_share和vm_pprev_share指针连接)中摘下。如果目前与该共享内存对象粘附的进程数变成了0,则释放共享内存页,释放共享内存页表,释放该对象的shmid_kernel数据结构,将向量表shm_segs中该共享内存对象所占用的项改为IPC_UNUSED。 如果共享的虚拟内存没有被锁定在物理内存中,分离会更加复杂。因为在这种情况下,共享内存的页可能在系统大量使用内存的时候被交换到系统的交换磁盘。为了避免这种情况,可以通过下面的控制操作,将某共享内存页锁定在物理内存不允许向外交换。共享内存的换出和换入,已在第3章中讨论。 4. 控制。Linux在共享内存上实现的第四种操作是共享内存的控制(call值为SHMCTL的sys_ipc调用),它由函数sys_shmctl实现。控制操作包括获得共享 内存对象的状态,设置共享内存对象的参数(如uid、gid、mode、ctime等), 将共享内存对象在内存中锁定和释放(在对象的mode上增加或去除SHM_LOCKED 标志),释放共享内存对象资源等。 共享内存提供了一种快速灵活的机制,它允许进程之间直接共享大量的数据,而无须使用拷贝或系统调用。共享内存的主要局限性是它不能提供同步,如果两个进程企图修改相同的共享内存区域,由于内核不能串行化这些动作,因此写的数据可能任意地互相混合。所以使用共享内存的进程必须设计它们自己的同步协议,如用信号灯等。
本文档为【LINUX进程间通信】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_014457
暂无简介~
格式:doc
大小:141KB
软件:Word
页数:39
分类:互联网
上传时间:2017-09-21
浏览量:31