20191227甘泞与第六章学习笔记

第6章 信号和信号处理

6.1信号和中断

信号:发给进程的请求,将进程从正常执行转移到中断处理。
中断:是从I/O设备或协处理器发送到CPU的外部请求,它将CPU从正常执行转移到中断处理。
“中断”是发送给“进程”的事件,它将“进程”从正常活动转移到其他活动,称为“中断处理”。“进程”可在完成“中断”处理后恢复正常活动。
终端主要有以下几种类型:

  • 1、人员中断
  • 2、进程中断
  • 3、硬件中断
  • 4、进程的陷阱错误

Unix/Linux的信号处理

(1)按Ctrl+C组合键通常会导致当前运行的进程终止。原因如下:Ctrl+C组合键会生成一个键盘硬件中断。键盘中断处理程序将Ctrl+C组合键转换为SIGINT(2)信号,发送给终端上的所有进程,并唤醒等待键盘输入的进程。在内核模式下,每个进程都要检查和处理未完成的信号。进程对大多数信号的默认操作是调用内核的kexit(exitValue)函数来终止。在Linux中,exitValue的低位字节是导致进程终止的信号编号。
(2)用户可使用nohup a.out &命令在后台运行一个程序。即使在用户退出后,进程仍将继续运行。
(3)用户再次登录时也许会发现(通过ps-u LTD)后台进程仍在运行。用户可以使用sh命令kill pid (or kill -s 9 pid)杀死该进程。

信号处理函数

每一个进程块都有一个信号处理数组int sig【32】。sig[32]数组的每个条目都指定了如何处理相应的信号,其中0表示DEFault(默认),1表示 IGNore(忽略)。下图给出了信号位向量、屏蔽位向量和信号处理函数。

如果信号位向量中的位I为1,则会生成一个信号I或将其发送给进程。如果屏蔽位向量的位I为1,则信号会被阻塞或屏蔽。否则,信号未被阻塞。只有当信号存在并且未被阻塞时,信号才会生效或传递给进程。

信号的发送

发送信号的主要函数有: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仍然能被进程接收。该函数无返回值。

信号捕捉函数

进程可使用系统调用:int r =signal(int signal_numberr void *handler);来修改选定信号编号的处理函数,SIGKILL(19)除外,它们不能修改。signal()系统调用在所有类Unix系统中均可用,但它有一些缺点:

  • 在执行已安装的信号捕捉函数之前,通常将信号处理函数重置为DEFault。为捕捉下一次出现相同的信号,必须重新安装捕捉函数。
  • signal()不能阻塞其他信号
  • 不同Unix版本的signal。可能会有所不通不同。
    所以现在signal()已经被sigaciton()所代替,它的原型是int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact);,sigaction结构体的定义为


其中重要的字段如下:

sa_handler :该字段是指向处理函数的指针,该函数与signal()的处理函数有相同的原型。
sa_sigaction:该字段是运行信号处理函数的另一种方法。它的信号编号旁边有两个额外参数,其中siginfo t *提供关于所接收信号的更多信息。
sa_mask:可在处理函数执行期间设置要阻塞的信号。
sa_flags :可修改信号处理进程的行为。若要使用sa_sigaction处理函数,必须将sa_flags设置为SA_SIGINFO。

信号用作IPC

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

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

(1)管道和FIFO
一个管道有一个读取端和一个写入端。管道的主要用途是连接一对管道写进程和读进程。管道写进程可将数据写入管道,读进程可从管道中读取数据。管道控制机制要对管道读写操作进行同步控制。未命名管道供相关进程 使用,命名管道是FIFO的,可供不相关进程使用。在Linux中的管道读取操作为同步和阻塞。如果管道仍有写进程但没有数据,读进程会进行等待。

(2)信号
进程可使用kill系统调用向其他进程发送信号.其他进程使用信号捕捉函数处理信号,将信号用作IPC的一个主要缺点是信号只是用作通知,不含任何信息内容。

(3)System V IPC
包括共享内存、信号址和消息队列。在Linux中,多种 System V 1PC函数,例如用于添加/移除共享内存的shmat/shmdt、用于获取/操作信号反的semget/semop和用于发送/接收消息的msgsnd/msgrcv,都是库包装函数,它们都会向 Linux内核发出一个ipc()系统调用。ipc()的实现是Linux所特有的,不可移植。

(4)POSIX消息队列
(5)线程同步机制
进程是共享某些公共资源的线程。如果是使用有共享地址空间的clone()系统调用创建的进程,它们可使用互斥量和条件变量通过共享内存进行同步通信。另外,常规进程可添加到共享内存,使它们可作为线程进行同步。

(6)套接字
用于跨网络进程通信的IPC机制。

实践内容

/* 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-14 11:01  氧气2019  阅读(18)  评论(0编辑  收藏  举报