现在的位置: 首页 > 综合 > 正文

信号和信号处理

2013年10月19日 ⁄ 综合 ⁄ 共 10789字 ⁄ 字号 评论关闭

1. 信号的基础

       信号时一种常用的Linux进程间的通信方式,用户进程使用的Linux命令中有许多包含对信号的处理。深入讨论常用的Linux信号时有必要的,可以通过查询帮助文档的方式查询。

1.1 信号的基本概念

       信号时一种进程通信的方式,又称为软中断,一个进程一旦收到信号就会打断原来的程序执行流程来处理该信号。由于进程不知道你是否收到信号,因此,信号通过这种进程间的通信方式是异步的。任何一个进程都可以发送信号并且接收信号。使用 kill -l 命令可以查看系统所支持的信号列表。

       kill-l 能够查看信号的编号以及信号对应的宏。这些宏定义在signal.h中。信号编号从1号开始,所以没有0号信号。每个信号的具体意义可以使用Linux系统内带有的man命令查询。

1.2 产生信号

       Linux环境下共有以下5种方式可以产生信号:

       1. 用户按下某些终端键时,终端驱动程序会发送信号给前台进程,例如 Ctrl+C 产生SIGINT信号,Ctrl+\ 产生SIGQUIT 信号。在用户需要终止一个进程时经常使用这些信号。

       2. 硬件异常产生信号,这些条件由硬件检测到并通过内核,然后内核向当进程发送适当的信号。例如,被0 除后产生SIGFPE 异常信号,无效内存访问产生SIGSEGV 信号,这就是在Linux环境下开发应用程序经常遇到的段错误。

       3. 一个进程调用 kil(2) 函数可以发送信号给另一个进程

       4. 可以用 kill(1) 命令发送信号给某个进程,kill(1) 命令也是调用 kill(2)函数实现的,如果不明确指定信号则发送 SIGTERM信号。kill 命令常用的使用方法是杀掉一个进程,许多人根据其命令的名判断它的功能。其实杀死一个进程只是kill 命令功能极小的一部分,是其向指定进程发送SIGTERM信号的结束。

       5. 当内核检测到某软件条件发生时,也可以通过信号通知进程,例如,闹钟时间超时产生SIGALRM,该信号是由alarm函数所设置的定时器发送的。

       不论是上述5种方法中那一种发送了信号,接收信号的进程都会暂停执行程序,转而处理接收到的信号。如果进程处于就绪状态,那么一旦进程得到CPU时间片将首先处理信号。如果进程处于挂起状态,那么接收信号将唤醒挂起进程,进程将首先处理信号。

1.3 处理信号

       对于一个信号,Linux环境下的进程只有三种处理方式:

        1. 忽略此信号,对其置之不理

        2. 注册一个信号处理函数,并要求内核在接收到信号时切换到用户态用该处理函数,这种方式称为捕捉到一个信号。用户程序经常需要对某些信号做一些自定义的处理,例如,如果进程创建了临时文件,那么可能要为SIGTERM 信号编写一个信号处理函数以清除临时文件,这样被 kill 时也可以比较干净地终止。

        3. 执行系统默认动作。不同的信号有不同的系统默认动作,系统所使用的默认动作只有两种。终结进程或者忽略信号。

 

2. 信号的影响

       信号是一种传统的、方便的通信方式。当进程捕捉到信号时,不论进程当前执行到何处,都会先跳到信号处理函数中执行,从信号处理函数返回后再恢复先前的代码位置继续执行。因此信号也会产生一个副作用。

2.1 重入

       当进程捕捉到信号时,不论进程当前执行到何处,都会跳到信号处理函数中执行,从信号处理函数返回后再恢复先前的代码位置继续执行。信号处理函数此时进程执行到何处。

       在同一进程中,main函数(主线程)、其他线程以及信号处理函数都是各自独立的执行流程,它们是并行。如果一个进程有多个执行流程,并且这些执行流程访问相同的全局资源(全局变量等),就有可能出现冲突。

       如果一个函数被不同的执行流程调用,有可能第一次调用还没返回就又一次进入该函数,则称为重入,这个函数访问一个全局的链表,有可能因为重入而造成错乱,称为不可重入函数。反之,如果一个函数只访问局部变量或参数,则称为可重入(reentrant)函数或者纯代码,或称为线程安全(thread-safe) 的函数。

       如果一个函数符号以下条件之一则是不可重入的:

       1. 使用了全局的数据,例如全局变量或静态变量

       2. 调用了动态方法得到内存(调用malloc函数),因为动态分配内存的方法也是以链表来管理内存分配的,这种数据也是全局作用域的

       3. 使用了标准 I/O 库,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构

       所以,归根结底所有使用具有全局作用域的数据都是函数,都是不可重入的,这种函数代码被chengwi非纯代码。

2.2 原子操作

       原子类型 sig_atomic_t

       C语言提供了volatile 限定符,如果将变量定义为volatile sig_atmoic_t a=0;那么即使制定了优化选项,编译器不会优化选项,编译器也不会优化掉对变量a内存单元的读写。

       由于被多个流程访问的全局资源都应当使用 volatile限定符,所以可以得出结论,sig_atomic_t 类型的变量应该总是和volatile 限定符一起使用的。因为使用 volatile 限定符一起使用的。因为使用sig_atomic_t 类型的数据基本上都是被多个执行流程访问的全局变量。 

2.3 中断系统调用

       信号的到来会中断进程,使得进程跳转到信号处理程序处执行,然后继续执行该进程的其他部分。如果在信号到来的时候正在执行系统调用,则该系统调用会被中断,当信号处理程序处理结束后重新启动被中断的系统调用。使用signal函数加载的 信号处理程序总是会在信号处理结束后重新启动被中断的系统调用;而使用sigaction 函数可以设置信号处理程序返回时是否重启被中断的系统调用。

       如果设置不重启动系统调用,这将是一个使外设的读写函数从低速设备的阻塞中解放出来的好方法。

 

3. 信号处理函数

      当信号接收到信号时,会调用相应的处理函数对信号进行处理。Linux 允许用户自行设置信号处理函数。此外系统还提供接口,供调用者产生一个信号。主动通信的一方送信号,而被动通信的一方接收信号。

3.1 设置信号处理函数

        当进程接收到信号需要跳转到信号处理函数的代码执行。系统为每个信号提供了多种默认的处理方式,这种默认的处理方式其中一种就是结束执行接收到信号的进程。Linux允许用户提供自己的信号函数,使用signal 函数将处理函数加载,并且通知系统,其函数原型如下:

              #include<signal.h>

              void (*signal (int signo, void (*func) (int)) (int);

       signal函数的第1个参数是需要加载处理的信号的编号,例如SIGKILL等。该编号是一个宏,其本质是一个整数,定义在signal.h文件中。

       第2个参数是一个函数的指针,这个函数捕捉第一个函数指定的信号,并对其进行处理。该参数可以是以下三个值中的一个:

       SIG_IGN:表示忽略该信号,即捕捉到该信号后不做任何处理。该宏定义在signal.h文件中:#define SIG_IGN ((void *)(*)())  1。STGKILL 和 SIGSTOP信号不能忽略。如果一个程序可以忽略以上两种信号,系统将无法“杀死”它。如果这个程序恰好是一个恶意程序,那么系统只有等待崩溃了。

       SIG_DFL:表示使用默认的信号处理方式,这样可以对此信号恢复系统的默认处理方式。该宏定义在signal.h文件中:

#define SIG_DFL ((void *)(*)())  0.

       其他已定义的函数的指针:表示使用用户自己的处理函数处理此信号,该函数称为信号处理程序(signal handler)。信号处理函数原型为。信号处理函数原型为:void handler(int).

       signal 函数的返回值时也是一个函数的指针,这个函数指向上一次信号处理程序,因此这个函数和signal 的第2个参数表示的函数的原型一致。如果出错,signal函数返回SIG_ERR,该宏定义在signal.h文件中: #define SIG_ERR ((void *)(*) ())  -1 

       信号是异步的通信方式,其到来由系统通知应用程序,所以信号处理程序是由系统调用的,其参数表示当前所捕获到信号的编号,也就是说该参数其实和signal的第1个参数signo是等效的。这样的好处是多余的多哥信号可以共用一个处理程序,处理程序内部的流程只需要根据参数来判断就可以了。

        void signal_handler(int signo)

        {                  switch(signo) {

                           case SIG1:    ......

                           case SIG2:    ......

                           case SIGn:    ......

                           }

        }

        signal 函数的原型过于复杂,可以使用 typedef 关键字对其进行简化。

                 typedef void HANDLER (int);

                 HANDLER *signal (int signo, HANDLER *handler);

        Linux 系统不允许用户创建新的信号,但是提供两个信号 SIGUSR1 和 SIGUSR2 专门用于应用程序之间进行信号通信。这两个没有特殊的含义,系统的默认处理方式是忽略。

3.2 发送信号

        一个进程可以向另一个进程发送信号,因此信号可以很好地用于进程之间的通信。通过信号输出的信号,使多个进程可以写作完成一个任务。Linux 环境使用kill 函数向进程或者进程组发送信号,其函数原型如下:

         #include<signal.h>

         int kill (pid_t pid, int signo);

       kill函数的第2个参数signo 表示要发送的信号和编号;第一个参数pid 有4种不同的取值情况: pid>0:将此信号发送给进程ID 为pid 的进程;pid==0:将此信号发送给进程组ID和该进程相同的进程;pid<0:将此信号发送给进程组内进程ID 为pid的进程;pid=-1:将此信号发送给系统所有的进程。

       一个进程向另一个进程发送信号必须注意以下两点:1. 该进程有向指定进程发信号的权限。这一点很显然,如果信号可以随便发送,则会给恶意程序留下可乘之机,试想一个恶意程序可以随便向其他用户的进程发送SIGKILL 信号,使这些进程终止运行。2. 系统进程不能接收信号,例如init 进程。这一点也很好理解,如果进程可以随便发送信号终结init进程,那么以后系统中的孤儿进程将不再被收养,系统出现大量僵尸进程直至进程直至系统崩溃。

       进程之间发送信号需要权限检查:

      1. 发送者UID==接收者UID:同一用户的进程之间的信号通信

      2. 发送者UID==接收者EID:接收者得到了发送者的授权,暂时变成与发送者同一用户的进程

      3. 发送者EID==接收者UID:发送者得到了接收者的授权,暂时成为接收者同一个用户的进程

      4. 发送者EID==接收者EID:发送者和接收者都暂时属于同一用户,等同于第一种情况

      跟用户可以向系统内任意进程发信号,因此跟用户可以杀死任何恶意进程,同时得到根用户权限的恶意进程也可以杀死系统内其他的进程。

      如果成功发送信号,kill 函数返回0,失败则返回-1。失败的原因有很多,例如,指定的进程或者进程组不存在,发送信号的权限不够等。如果kill函数发送的信号不被接受进程屏蔽,当该函数返回时,此信号已经成功发送给指定的接收进程。该进程共3次调用kill函数,分别是子进程向父进程发送信号SIGUSR1,父进程向子进程发送信号SIGUSR2 和SIGKILL。进程可以调用 kill函数向自身发送一个信号。

       kill (getpid(), signo);

3.3 向进程本身发送信号

       Linux 同样提供可以向进程本身发送信号的函数,该函数可以替代上述形式,其函数原型如下:

                #include<signal.h>

                int raise(int signo);

       raise 函数的参数是要发送的信号编号,成功返回0,失败返回-1。raise函数可以实现exit 函数的功能使进程退出,所不同的是此用法不做任何善后处理(例如,冲洗流,关闭文件等)。

       值得一体的是,如果信号的粗合理方式是终结进程,那么当进程捕捉到该信号时就会终结退出,同样不做任何的善后工作。因此,如果一个进程有可能向其他进程发送信号终结的时候,应该编写一个处理所有善后事宜的函数作为此信号的信号处理程序,使用signal函数设置后使用。

3.4 设置Linux定时器

       时间是编程中的一个重要的概念,有些场合需要一个定时器,在经过若干时间后通知设置定时器的进程。

       Linux 环境下使用alarm函数设置一个定时器,其函数原型如下:

               #include<unistd.h>

               unsigned int alarm(unsigned int seconds);

       alarm函数的参数表示定时器设置的秒数,从设置定时器开始,如果系统时间超过该时间后就会向alarm函数的进程发送一个SIGALRM 的信号,这个信号的默认动作是终止调用alarm函数的进程。

       如果此前没有设置过定时器,或者设置过定时器但该定时器已经超时,alarm 的返回值为0;如果此前设置过定时器且该定时器没有超时,则返回该定时器剩余的秒数。当alarm的参数是0时可以取消一个定时器,返回值是之前的定时器的剩余秒数。

       alarm函数所设置的不是一个十分精确的定时器。

       首先,该函数设置的定时器的精确是秒,秒的级别时间在计算机内部而言是相当长的,因为一条计算机指令的执行时间是纳秒级。其次,在定时器超时发送了SIGALRM信号后,进程从捕捉该信号到进行处理还会有一定的延时,因为只有被调度或者从内核态返回到用户态的时候进程才能做检查信号的操作。

       alarm 函数用来设置精确的定时器还未够资格,同时也说明1秒钟对于计算计而言是一个很长的时间。

3.5 定时等待I/O

       对外设进行读写操作的时候,外设有可能处于一个不可用的状态。例如,网络设备中指定的数据包未到达等。这种时候往往会导致读写操作的阻塞,这种阻塞是没有时间限制的,直到所请求的设备准备好为止。

       alarm函数可以用来实现定时阻塞,当需要读写的设备未就绪时,只等待有限的时间。

3.6 挂起进程

       运行、就绪和阻塞是进程的三个基本状态,任一时刻只有一个进程处于运行状态。当进程的运行条件已经具备时进程处于就绪态,当进程运行条件尚不具备进程处于阻塞态。就绪态的进程可以被调度,阻塞态的进程由于不能运行所以不参与调度。

       有时当一个进程的运行条件已经具备时仍需要使进程阻塞(例如期望进程延迟执行,sleep函数经常起到这个作用),这种是由进程自愿进入阻塞态的情况成为进程挂起。Linux环境下使用pause函数挂起一个进程,其函数原型如下:

       #include<unistd.h>

       int pause(void);

       pause 函数使调用该函数的进程进入挂起状态,直到一个信号到来,并且执行了一个信号处理程序从其返回后,pause函数才返回,返回值是-1。pause函数的返回值只有 -1 一种情况,errno被设置为EINTER,返回-1表示执行正确。

3.7 进程休眠

      pause 使进程无时间限制的挂起,如想使进程在一定时间内恢复运行则使用sleep函数,其函数原型如下:

                 #include<unistd.h>

                 unsigned int sleep(unsigned int nsec);

       sleep函数的参数表示进程挂起的时间,精确为妙。sleep函数的返回的可能有2种,第一种情况是挂起的时间超过了指定的时间,这时sleep函数返回0;第2种情况是挂起的期间被信号唤醒,这时sleep函数返回指定的时间—挂起以来的时间。

       sleep 函数的实现通常封装了pause函数 和alarm函数。该程序首先定义了2个信号处理函数。第一个处理定时器信号,这个函数什么都不做。第2个处理试验用的SIGUSR1信号,这个函数只是输出提示信息。

 

4. 信号集与屏蔽信号

        屏蔽信号是Linux系统一个重要的功能,其允许用户进程有选择的接收并且处理信号。需要屏蔽的信号使用信号集表示。这种集合时一种位向量,其中的每一位对应一个信号。

4.1 信号集和信号集处理函数

       进程所能够捕捉并且处理的信号集合称为信号集。信号集的实现通常是一个位向量,其中的每一位对应一个Linux系统中的信号。从使用的角度来讲,信号集向量实现用户并不关系,只需要提供一种数据类型,用户可以直接使用即可。

       这种数据类型就是sigset_t,其本质是一个位向量,信号的编码就是此信号在信号集类型变量中所使用的位。例如 SIGKILL的编号为13,那么sigset_t类型数据中的第13位就表示SIGKILL信号。

       由于屏蔽了 sigset_t 数据类型的实现细节,所以即使其本质是一个位向量也不能使用简单的移位操作对sigset_t型数据进行操作,而应该使用系统提供的一组信号集处理函数,这些函数的原型如下:

               #include<signal.h>

               int sigemptyset(signal_t *set);

               int sigfillset(sigset_t *set);

               int sigaddset(sigset_t *set, int signo);

               int sigdelset(sigset_t *set, int signo);

       sigemptyset 函数的参数是一个信号集数据,该函数将其清空,即所有的位都设置为0。sigfillset 函数的参数和sigemptyset 函数意义一样,只是功能正好和其相反,该函数将所有的位设置为1。

       sigaddset函数的第1个参数set是一个信号集数据,第2个参数signo 则指定信号编号。sigaddset函数将参数signo指定的信号所对应的位设置为1;而sigdelset函数正好相反,将signo 对应的位设置为0。以上4个函数的返回值意义一样,如果操作成功则返回0,失败返回-1。

       Linux 环境下使用sigismember函数测试某个信号所对应的位是否被设置,其函数原理如下:

               #include<signal.h>

               int sigismember(sigset_t *set, int signo);

       sigismember 函数的参数的意义和sigaddset函数一致。如果signo 所对应的位被设置则返回1,未设置返回0,失败则返回-1。

       信号的编号是从1开始的,而信号集处理函数中的位编号是从0 开始的,所以signo 的值应该是信号编码减去1。

4.2 屏蔽信号

       有时候希望阻塞一些信号,使进程即使接收到该信号,也不用做处理。阻塞一个信号称为信号屏蔽,每个进程内部都有一个信号屏蔽字,标记被屏蔽的信号。信号屏蔽字的本质同信号集以一样,是一个位向量。信号编码对应的位为1 表示屏蔽该信号,对应的位为0 表示处理该信号。

       阻塞区别于默认处理动作为SIG_ING 的信号。后者可以处理信号,只是忽略它;前者根不处理该信号。

       Linux环境下sigprocmask 函数设置信号屏蔽字,其函数原型如下:

       #include<signal.h>

       int sigprocmask(int how, cosnt sigset_t *restrict set, sigset_t *restrict oset);

       sigprocmask函数的第2个参数set 是一个信号集,该信号集中被设置的位表示需要被屏蔽的信号。在函数返回时,第3个参数oset 被设置为原来的信号屏蔽字。第1个参数how 有3种情况:SIG_BLOCK:set包含了希望添加当前屏蔽字的信号,相当于mask=mask | set;SIG_UNBLOCK:set包含了希望从当前屏蔽信号集中解除阻塞的信号,相当于mask=mask&~set;SIG_SETMASK:设置当前屏蔽信号集为set 所指向的值,相当于mask=set。mask表示屏蔽字,set表示信号集。

       如果参数set 的值是NULL,则无论how是何值都不会更改信号屏蔽字。这种方法用于得到当前进程信号屏蔽字。

              sigset_t oset;

              sigprocmask(0, NULL, &oset);

       如果参数oset 的值是NULL,则表示忽略原来的信号屏蔽字。如果成功操作,sigprocmask 函数返回0,否则返回-1。

       SIGKILL 和 SIGSTOP这两个信号不能被屏蔽。如果SIGKILL 和SIGSTOP被屏蔽,恶意进程就不用担心被用户的kill命令杀死,而可以肆意破坏了。

4.3 处理未决信号

       如果屏蔽了一个信号,但是进程还是从某处接收到了此信号,这种信号叫做未决的。之所以这么称呼是因为这种信号被当前进程阻塞,但是事实上又有人发给它,不能处理,也不能够清除,只能悬而未决。

        如果调用 sigprocmask 后有任何未决的但是已经不再阻塞的信号时,在该函数返回之前,至少会将这些解放了的味觉信号的一个发送给该进程。Linux环境下sigpending函数检查未决信号,其函数的原型如下:

                #include<signal.h>

                int sigpending(sigset_t *set);

        sigpending函数的参数是一个信号集变量的指针,该信号集表示当前进程所有未决的信号。如果成功得到未决信号集,sigpending函数返回0,否则返回-1。

4.4 高级信号注册函数

        现在Linux系统中提供了一个改进版的信号注册函数,其函数原型如下:

                #include<signal.h>

                int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

        sigaction函数的第1个参数表示注册的信号编号,这个参数的意义和signal函数一样。后面2个参数是两个sigaction结构的指针,内核使用参数act指向结构设置信号处理函数,将原来的值存储在oact所指向的内存区域内。

        Sigaction 结构在signal.h文件中声明。

                        struct sigaction{

                                  void (*sa_handler)(int);  //信号处理函数

                                  sigset_t sa_mask;            //屏蔽信号集

                                  int sa_flags;                   //信号选项

                                  void (*sa_sigaction)(int, siginfo_t, void *)      //替代sa_handler的信号处理函数

                                  }

       

       sigaction 结构的第1个成员变量表示信号出来函数,其意义与signal函数的第2个参数相同。Sigaction结构的第2个成员变量表示屏蔽的信号集,在设置信号处理函数时,sa_mask中表示的信号将添加到信号屏蔽字中。

       当进程捕捉到了该信号,调用的信号处理函数返回之后,信号屏蔽字将被还原为设置前的摸样。这样做的好处是可以防止当一个进程在处理该信号时又会被同样的信号再次中断,从而陷入信号处理函数中不能返回。sigaction 结构的第3个成员变量表示信号选项:

        SA_INTERRUPT:被此信号中断的系统调用不会自动启动

        SA_NOCIDSTOP:当信号为SIGCHILD时,调用进程的子进程被停止时不发送不产生此信号;调用进程的子进程终止时产生此信号

        SA_NOCLDWAIT:当信号为SIGCHILD时,则调整进程的子进程终止时,立即释放系统资源。如果调用进程调用wait函数,会导致该进程阻塞,直到其所有子进程全部结束后wait 函数才返回-1,并将errno错位号设置为ECHILD。这个 选项可以用来避免僵尸进程的产生。

        SA_NODEFFER:当捕捉到该信号正在执行处理函数时,不阻塞该信号,除非sa_mask中指定阻塞该信号

       SA_RESETHAND:用户注册的信号处理函数被调用后就会被清除注册,恢复为默认的处理函数

       SA_RESTART:被此信号中断的系统调用会重新启动

       SA_SIGINFO:该选项使用sa_sigaction所指向的函数处理信号,而不是使用sa_handler所指向的函数处理信号

       这些宏定义在signal.h 中,在设置信号选项时使用多个宏用“与”的方法。

       sigaction结构的第4个成员变量表示一个替代的信号处理函数,当信号选项SA_SIGINFO被设置时使用此函数处理信号。该函数有3个参数,此3个参数都由内核传递,分别是信号编号、信号产生的详细信息和进程的上下文。

       第1个参数的类型和sa_handler 指针所指向函数的相同。第2个参数是一个siginfo_t 结构类型,不同的信号可以得到不同的信息,例如,对于SIDCHLD信号,可以得到子进程ID、子进程所有者ID以及进程的退出状态。第3个参数是一个任类型的指针,内核通常将其转换为ucntext_t 类型的结构,用以传入进程的上下文。

       如果成功注册了信号的处理函数,sigaction函数返回0,否则返回-1。sigaction函数可以完全替代signal函数,其功能的强大是signal 函数所部能相比的。但是功能的强大势必会带来编码的复杂,所以在有些场合还是会选择signal函数,使用简洁的信号注册函数。 

抱歉!评论已关闭.