linux c语言学习笔记之IPC-信号

哈尔滨理工大学软件工程专业08-7李万鹏原创作品,转载请标明出处

http://blog.csdn.net/woshixingaaa/archive/2010/06/10/5660617.aspx

 

在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标准兼容的信号实现机制。
1、在一个信号的生命周期中有两个阶段:生成和传送。当一个事件发生时,需要通知一个进程,这时生成一个信号。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。在信号到来和进程对信号进行处理之间,信号在进程上挂起(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信号。
1、数据结构。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域的值。
1、修改信号处理程序。进程通过执行系统调用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,同时将该处的老处理程序返回给用户。
2、发送信号。向一个进程发送信号由函数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改变焦点)发生并且使用的是缺省处理程序,则不需要做任何事情。
3、处理信号。信号不会一产生就立刻出现在进程中,事实上,它们必须等待直到进程下次运行。在进程从系统调用返回到用户态之前,在进程从中断返回到用户态之前,系统都要检查进程的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也优化了这种情况:如果同时有几个信号处理例程需要调用,就把它们堆积在一起,每次退出一个处理例程的时候就调用下一个,直到最后才调用整理例程。
4、除了上述的操作以外,Linux还提供了另外几种对信号的操作,如sys_sigsuspend、sys_rt_sigsuspend、sys_sigaction、sys_sigpending 、sys_sigprocmask 、sys_sigaltstack、sys_sigreturn、sys_rt_sigreturn等,此处不再介绍。
信号最初的设计目的主要是用来处理错误,内核把进程运行过程中的异常情况和硬件的信息通过信号通知进程。如果进程没有指定对这些信号的处理程序,则内核处理它们,通常是终止进程。作为一种IPC机制,信号有一些局限:
信号的花销太大。发送信号要做系统调用;内核要中断接收进程、要管理它的堆栈、要调用处理程序、要恢复被中断的进程等。
信号种类有限,只有31种,而且信号能传递的信息量十分有限。
信号没有优先级,也没有次数的概念。
所以,信号对于事件通知很有效,但对于复杂的交互操作却难以胜任。

发布了18 篇原创文章 · 获赞 0 · 访问量 48万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览