学习笔记-第六章

一、梗概

  • 本章讲述了信号和信号处理;介绍了信号和中断的统一处理,有助于从正确的角度看待信号;将信号视为进程中断,将进程从正常执行转移到信信号处理;解释了信号的来源,包括来自硬件、异常和其他进程的信号;然后举例说明了信号在 Unix/Linux 中的常见用法;
    详细解释了 Unix/Linux 中的信号处理,包括信号类型、 信号向量位、信号掩码位、进程 PROC 结构体中的信号处理程序以及信号处理步骤;用示例展示了如何安装信号捕捉器来处理程序异常,如用户模式下的段错误;还讨论了将信号月作进程间通信(IPC)机制的适用性。读者可借助该编程项目,使用信号和管道来实现用于进程交换信息的进程间通信机制。

二、知识点总结

1、信号和中断

1)中断的基本概念

中断是指计算机在执行期间,系统内发生任何非寻常的或非预期的急需处理事件,使得CPU暂时中断当前正在执行的程序而转去执行相应的事件处理程序,待处理完毕后又返回原来被中断处继续执行或调度新的进程执行的过程。引起中断发生的事件被称为中断源。中断源向CPU发出的请求中断处理信号称为中断请求,而CPU收到中断请求后转到相应的事件处理程序称为中断响应。

2)中断的分类与优先级

根据系统对中断处理的需要,操作系统一般对中断进行分类并对不同的中断赋予不同的处理优先级,以便在不同的中断同时发生时,按轻重缓急进行处理。

根据中断源产生的条件,可把中断分为外中断和内中断。外中断是指来自处理器和内存外部的中断,包括I/0设备发出的I/O中断、外部信号中断(例如用户键人ESC键)。各种定时器引起的时钟中断以及调试程序中设置的断点等引起的调试中断等。外中断在狭义上一般被称为中断。

内中断主要指在处理器和内存内部产生的中断。内中断一般称为陷阱(trap)或异常。它包括程序运算引起的各种错误,如地址非法、校验错、页面失效、存取访问控制错、算术操作溢出、数据格式非法、除数为零、非法指令、用户程序执行特权指令、分时系统中的时间片中断以及从用户态到核心态的切换等都是陷阱的例子。

为了按中断源的轻重缓急处理响应中断,操作系统为不同的中断赋予不同的优先级。例如在UNIX系统中,外中断和陷阱的优先级共分为8级。为了禁止中断或屏蔽中断,CPU的处理器状态字PSW中也设有相应的优先级。如果中断源的优先级高于PSW的优先级,则CPU响应该中断源的请求;反之,CPU屏蔽该中断源的中断请求。

各中断源的优先级在系统设计时给定,在系统运行时是固定的。而处理器的优先级则根据执行情况由系统程序动态设定。

除了在优先级的设置方面有区别之外,中断和陷阱还有如下主要区别:

陷阱通常由处理器正在执行的现行指令引起,而中断则是由与现行指令无关的中断源引起的。陷阱处理程序提供的服务为当前进程所用,而中断处理程序提供的服务则不是为了当前进程的。

CPU执行完一条指令之后,下一条指令开始之前响应中断,而在一条指令执行中也可以响应陷阱。例如执行指令非法时,尽管被执行的非法指令不能执行结束,但CPU仍可对其进行处理。

3)什么是信号?

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号来源

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

2、linux的信号处理

1)

*对于大部分的信号,Linux系统都有默认的处理方式。而大部分默认的处理方式是终止程序并转储core文件。要处理信号,Linux系统处理信号的接口有两个sigaction(),signal(),较简单的是signal()函数,其形式如下:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

  • siganl()函数有两个参数其中有一个int的参数便是要处理的信号,诸如SIGINT的宏。另一个参数类型为sighandler_t的函数指针,handler指针对应的函数我们称之为:信号处理函数(signal-handler function)。可见signal()的第二个参数是一个信号处理函数,返回值也是一个信号处理函数,失败返回宏SIG_ERR(SIGKILL和SIGSTOP的默认行为分别是杀死和停止一个进程,任何试图改变这两个信号的处理方式的行为都将返回错误)。这样经典形式的函数在Linux上我们经常会经常碰到。signal()函数的作用就是建立一个signum信号的处理函数(establish a signal handler function)。通俗一点来说就是当signum信号到来时,进程会保存当前的进程栈,转去执行siganl()中指定的handler函数。之前提到过,信号的响应方式有多种,因此handler不仅可以是一个函数指针也可以是ISO C为我们定义的宏:SIG_IGN,SIG_DEL,和他们的名字一样SIG_IGN是忽略这个信号,SIG_DEL是保持这个信号的默认处理方式(默认处理方式也可以可以是SIG_IGN ,比较绕,但是合理)。前文提到的三个宏定义分别如下(/usr/include/bits/signum.h):
#define SIG_ERR ((__sighandler_t) -1) 
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

2)进程对信号的响应

  • 进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作,详细情况请参考[2]以及其它资料。注意,进程对实时信号的缺省反应是进程终止。
    Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。

3)信号的发送

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

1、kill()
*#include <sys/types.h>
*#include <signal.h>
int kill(pid_t pid,int signo)

参数pid的值 信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程
Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

Kill()最常用于pid>0时的信号发送,调用成功返回 0; 否则,返回 -1。 注:对于pid<0时的情况,对于哪些进程将接受信号,各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c即可,上表中的规则是参考red hat 7.2。

2、raise()

#include <signal.h>
int raise(int signo)

向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。

3、sigqueue()

#include <sys/types.h>
#include <signal.h> 
int sigqueue(pid_t pid, int sig, const union sigval val)

调用成功返回 0;否则,返回 -1。

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
typedef union sigval { int sival_int; void *sival_ptr; }sigval_t;
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指的是信号处理函数由sigaction安装,并设定了sa_sigaction指针,稍后将阐述)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数; sigqueue()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。

4、alarm()

#include <unistd.h> 
unsigned int alarm(unsigned int seconds) 

专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。
返回值,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

5、setitimer()

#include <sys/time.h> 
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue)); 

setitimer()比alarm功能强大,支持3种类型的定时器:

ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;
Setitimer()第一个参数which指定定时器类型(上面三种之一);第二个参数是结构itimerval的一个实例,结构itimerval形式见附录1。第三个参数可不做处理。

Setitimer()调用成功返回0,否则返回-1。

6、abort()

#include <stdlib.h> 
void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

3、信号的安装(设置信号关联动作)

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现, 是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。

1、signal()

#include <signal.h> 
void (*signal(int signum, void (*handler))(int)))(int); 

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int); 
sighandler_t signal(int signum, sighandler_t handler)); 

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。
如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

2、sigaction()

#include <signal.h> 
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。

sigaction结构定义如下:

 struct sigaction {
          union{
            __sighandler_t _sa_handler;
            void (*_sa_sigaction)(int,struct siginfo *, void *);
            }_u

            sigset_t sa_mask;
            unsigned long sa_flags; 
           }

1、联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。
2、由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

  siginfo_t {
        int      si_signo;  /* 信号值,对所有信号有意义*/
        int      si_errno;  /* errno值,对所有信号有意义*/
        int      si_code;   /* 信号产生的原因,对所有信号有意义*/
        pid_t    si_pid;    /* 发送信号的进程ID,对kill(2),实时信号以及SIGCHLD有意义 */
        uid_t    si_uid;    /* 发送信号进程的真实用户ID,对kill(2),实时信号以及SIGCHLD有意义 */
        int      si_status; /* 退出状态,对SIGCHLD有意义*/
        clock_t  si_utime;  /* 用户消耗的时间,对SIGCHLD有意义 */
        clock_t  si_stime;  /* 内核消耗的时间,对SIGCHLD有意义 */
        sigval_t si_value;  /* 信号值,对所有实时有意义,是一个联合数据结构,
                          /*可以为一个整数(由si_int标示,也可以为一个指针,由si_ptr标示)*/
	
        void *   si_addr;   /* 触发fault的内存地址,对SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义*/
        int      si_band;   /* 对SIGPOLL信号有意义 */
        int      si_fd;     /* 对SIGPOLL信号有意义 */
  }

4、信号集及信号集操作函数:

信号集被定义为一种数据类型:

	typedef struct {
			unsigned long sig[_NSIG_WORDS];
			} sigset_t

信号集用来描述信号的集合,linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum)
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;
sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;
sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;
sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;
sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。

5、信号用作IPC

在许多操作系统的书籍中,信号被归类为进程间的通信机制。基本原理是一个进程可以向另一个进程发送信号,使它执行预先安装的信号处理函数。由于以下原因,这种分类即使不算不恰当也颇具争议。

  • 该机制并不可靠,因为可能会丢失信号。每个信号由位向量中的一个位表示,只能记录一个信号的一次出现。如果某个进程向另一个进程发送两个或多个相同的信号,它们可能只在接收 PROC 中出现一次。实时信号被放入队列,并保证按接收顺序发送但操作系统内核可能不支持实时信号。
  • 竞态条件:在处理信号之前,进程通常会将信号处理函数重置为 DEFault。要想捕捉同一信号的再次出现,进程必须在该信号再次到来之前重新安装捕捉函数。否则,下一个信号可能会导致该进程终止。在执行信号捕捉函数时,虽然可以通过阻塞同一信号来防止竞态条件,但是无法防止丢失信号。
  • 大多数信号都有预定义的含义。不加区别地任意使用信号不仅不能达到通信的目的,反而会造成混乱。例如,向循环进程发送 SIGSEGV(1)段错误信号,就像对水里游泳的人大喊:“你的裤子着火了!”
    因此,试图将信号用作进程间通信手段实际上是对信号预期用途的过度延伸,应避免出现这种情况。

三、实践内容

  • 该实例首先把SIGQUIT、SIGINT两个信号加入信号集,然后将该信号集设为阻塞状态,并进入用户输入状态。用户只需按任意键,就可以立刻将信号集设置为非阻塞状态,再对这两个信号分别操作,其中SIGQUIT执行默认操作,而SIGINT执行用户自定义函数的操作。源代码如下:
    /* sigset.c */
    #include <sys/types.h>
    #include <unistd.h>
    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>

    /* 自定义的信号处理函数 */
    void my_func(int signum)
    {
        printf("If you want to quit,please try SIGQUIT\n");
    }

    int main()
    {
        sigset_t set,pendset;
        struct sigaction action1,action2;

        /* 初始化信号集为空 */
        if (sigemptyset(&set) < 0)
        {
            perror("sigemptyset");
            exit(1);
        }

        /* 将相应的信号加入信号集 */
        if (sigaddset(&set, SIGQUIT) < 0)
        {
            perror("sigaddset");
            exit(1);
        }

        if (sigaddset(&set, SIGINT) < 0)
        {
            perror("sigaddset");
            exit(1);
        }

        if (sigismember(&set, SIGINT))
        {
            sigemptyset(&action1.sa_mask);
            action1.sa_handler = my_func;
            action1.sa_flags = 0;
            sigaction(SIGINT, &action1, NULL);
        }

        if (sigismember(&set, SIGQUIT))
        {
            sigemptyset(&action2.sa_mask);
            action2.sa_handler = SIG_DFL;
            action2.sa_flags = 0;
            sigaction(SIGQUIT, &action2,NULL);
        }

        /* 设置信号集屏蔽字,此时set中的信号不会被传递给进程,暂时进入待处理状态 */
        if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
        {
            perror("sigprocmask");
            exit(1);
        }
        else
        {
            printf("Signal set was blocked, Press any key!");
            getchar();
        }
        /* 在信号屏蔽字中删除set中的信号 */
        if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
        {
            perror("sigprocmask");
            exit(1);
        }
        else
        {
            printf("Signal set is in unblock state\n");
        }

        while(1);
        exit(0);
    }

该程序的运行结果如下,可以看见,在信号处于阻塞状态时,所发出的信号对进程不起作用,并且该信号进入待处理状态。读者按任意键,并且信号脱离了阻塞状态后,用户发出的信号才能正常运行。这里SIGINT已按照用户自定义的函数运行,请读者注意阻塞状态下SIGINT的处理和非阻塞状态下SIGINT的处理有何不同。

四、问题与解决

  • 1、在一个多线程程序中,线程A中会设置定时器,如果超时就会触发SIGALRM的信号处理函数sig_alarm_func,该函数执行了pthread_cancel(A);pthread_create(B);的操作。在测试过程中发现进程中同时存在A, B两个线程。查看pthread_cancel 说明,phtread_cancel是个异步的,需要等到线程A执行到cancellation point才能结束退出。利用gdb查看A的函数调用栈发现,阻塞到了信号处理函数sig_alarm_func中,即发生了“自己取消自己”的问题。根据第二部分讲到的信号通告机制,定时器信号被发往了调用定时器的线程,因而信号处理函数也是在调用线程的上下文中执行,所以出现了异常。

解决方法:

单独设置一个信号处理线程,阻塞除该线程外的其他所有线程的信号。在信号处理线程中,利用 while+sigwait 对信号进行同步处理代替注册信号处理函数的异步处理方式。

  • 2、在处理一个程序堆栈时,发现程序在malloc函数中发生了死锁。进一步分析发现信号处理函数在保存函数调用堆栈时调用了malloc,而信号产生时正好也在执行malloc操作。通过查看malloc的相关文档发现,malloc在申请内存的时候,有加锁操作。

解决方法:

信号处理函数中取消malloc这类不可重入的有锁函数。以后编写信号处理函数的时候,在函数内部尽少做一些耗时处理尽快返回,在调用函数时必须调用可重入(reentrant)函数(即不可以有static、global等全局变量,不可以分配、释放内存,不要修改errno等)。

posted @ 2021-11-13 18:45  20191223张俊怡  阅读(32)  评论(0编辑  收藏  举报