简 述: 在上一篇中,讲解了 Linux 的系统中基本信号概念入门。这里就说一下两个重要的概念,系统内核里面的 未决信号集阻塞信号集 的状态关系,以及处于用户区域的 自定义的信号集 ,如何处理这三者之间的关系。和 Linux 中的信号捕捉 ,以及捕捉函数相关函数相关的使用。

  • 自定义信号集:
    • sigaddset() //将指定信号置为 1,添加到自定义集中
    • sigdelset() //将指定信号置为 0,添加到自定义集中
    • sigemptyset() //将所有信号置为 0,清空
    • sigfillset() //将所有信号置为 1,填充
    • sigismember() //判断指定信号是否存在,是否为 1
  • 系统信号集:
    • sigprocmask() //将自定义信号集设置给阻塞信号集。
    • sigprocmask() //读取当前信号的未决信号集。参数为输出参数,内核将未决信号集写入 set
  • 信号捕捉:
    • signal() //实现信号捕捉的功能;最简单使用一个函数。
    • sigaction() //同上,多一个额外功能,运行期间能够临时 屏蔽指定信号

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境:

  💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

  💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


未决信号集:

  • 概念: 没有被当前进程处理的信号

阻塞信号集:

  • 概念: 将某个信号放到阻塞信号集中,这个信号就不会被进程处理;当阻塞解除后,信号就会被处理。

绘画一个草图理解一下这几则之间的关系,当一个程序跑起来之后,会生成一个进程,有自己虚拟地址控件,然后在内核区域的 PCB 中,有着未决信号集合阻塞信号集两个,被系统内核所控制,包括其和自定义信号集的类型都是一样的。左侧的数值是系统信号 1-64(或更多)对应的,然后格子里面的是该 n 号信号对应的值,值为 0 或 1;当发送一个 2 号信号在未决信号集里面时候,系统就会去阻塞信号集里面照片 2 号信号的值,若是为 1,表示阻塞,不作处理,让其一直待在未决信号集中;若是为 0,则将 2 号信号在未决信号集中的值修改为 1,且对出对应的处理。

若是要修改阻塞信号集合里面的值,只能够先赋值号自顶一个信号集,然后再讲自定义的这个值设置到系统的阻塞信号中。


自定义信号集:

查看 man 文档,里面常见的 自定义信号集 的接口函数为如下:

int sigaddset(sigset_t *set, int signo);          //将指定信号置为 1,添加到自定义集中 

int sigdelset(sigset_t *set, int signo);          //将指定信号置为 0,添加到自定义集中 

int sigemptyset(sigset_t *set);                   //将所有信号置为 0,清空

int sigfillset(sigset_t *set);                    //将所有信号置为 1,填充

int sigismember(const sigset_t *set, int signo);  //判断指定信号是否存在,是否为 1

sigprocmask() 函数:

  • 作用: 将自定义信号集设置给阻塞信号集。
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
  • 参数:
    • how: 假设当前屏蔽的信号屏蔽字符为 mask;
      • SIG_BLOCK: 相当于 mask = mask | set (set 为需要屏蔽的信号集)
      • SIG_UNBLOCK: 相当于 mask = mask & ~set (set 为需要解除屏蔽的信号集)
      • SIG_SETMASK: 相当于 mask = set (set 为用于替代原始屏蔽集的新屏蔽集)

sigpending() 函数:

  • 作用: 读取当前信号的未决信号集。参数为输出参数,内核将未决信号集写入 set
int sigpending(sigset_t *set);

写一个小的例子:

编写一个小的例子来使用一下,设置阻塞信号集,并把所有常规的信号的未决状态打印到终端。

  • 代码实现:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/time.h>
    #include <signal.h>
    
    int main(int argc, char *argv[])
    {
        while (true) {
            sigset_t pendest;
            sigpending(&pendest);
    
            for (int i = 0; i < 64; i++) {    //每隔一秒,对系统信号阻塞集的信号做一次校验
                if (sigismember(&pendest, i))
                    printf("1");
                else
                    printf("0");
            }
    
            printf("\n"); 
            sleep(1);
        }
    
        return 0;
    }
  • 运行效果:

    这里的 1-64 号信号,在系统的阻塞信号集中,都是 0,说明该进程没有设置信号阻塞。


信号捕捉:

在 Linux 中, 系统会释放信号,然后内核又会根据这个信号对对应的进程做相应的动作。然后对于信号捕捉常用有如下函数的使用。

其中分为简单的 signal() 函数,和 sigaction() 函数,这两个都能够捕捉指定的信号,也比较常使用

signal() 函数:

实现信号捕捉的功能;最简单一个函数。

typedef void (*sig_t) (int);
sig_t signal(int sig, sig_t func);
  • 这个函数比较特别,当然,也可以简写为 void (* signal(int sig, void (*func)(int));)(int); 这种复杂形式的。

    • 第一行是一个函数指针,作为后面的函数的回调函数使用。
    • 第二行是真正的信号捕捉函数调用
  • 代码例子:

    写一个例子,捕捉信号 SIGINT 函数,键盘按下 ctrl + c 会发射此信号,然后保证此进程没有终止或者死亡的情况下,捕捉这个函数,做出自定义的行为,具体的在 func() 函数里面实现。

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/time.h>
    #include <signal.h>
    
    void func(int no); //用作回调函数,给 signal() 调用
    
    int main(int argc, char *argv[])
    {
        signal(SIGINT, func); //设置信号捕捉函数, 捕捉 ctrl + c
    
        while (true) {
            printf("Keep the thread running for the non-death state.\n"); 
            sleep(1);
        }
    
        return 0;
    }
    
    void func(int no) 
    {
        printf("捕捉的信号为: %d\n", no);
    }
  • 运行效果:


Unix 中的 sigaction() 函数:

捕捉信号的函数,比上面使用略微复杂点,但是有一个额外的功能,可以在程序运行期间,临时屏蔽指定的信号。

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

/* union for signal handlers */
union __sigaction_u {
	void    (*__sa_handler)(int);
	void    (*__sa_sigaction)(int, struct __siginfo *,
	    void *);
};

/* Signal vector template for Kernel user boundary */
struct  __sigaction {
	union __sigaction_u __sigaction_u;  /* signal handler */
	void    (*sa_tramp)(void *, int, int, siginfo_t *, void *);  //已被废弃的参数,不填
	sigset_t sa_mask;               /* signal mask to apply */
	int     sa_flags;               /* see signal options below */ //在信号处理函数执行中,是 "临时" 屏蔽指定信号
};

/* if SA_SIGINFO is set, sa_sigaction is to be used instead of sa_handler. */
#define sa_handler      __sigaction_u.__sa_handler
#define sa_sigaction    __sigaction_u.__sa_sigaction


//--------------------------------------------------------------------
//__sigaction() 函数也可以 改为使用 sigaction() 函数
/*
 * Signal vector "template" used in sigaction call.
 */
struct  sigaction {
	union __sigaction_u __sigaction_u;  /* signal handler */
	sigset_t sa_mask;               /* signal mask to apply */
	int     sa_flags;               /* see signal options below */
};
  • 参数:
    • sig: 捕捉的信号
    • act: 新的,将要执行的自定义动作的设定操作
    • oact: 被设置之前的旧的设定的操作吗,一般不需要,传入 NULL

restrict是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码.如 int *restrict ptr, ptr 指向的内存单元只能被 ptr 访问到,任何同样指向这个内存单元的其他指针都是未定义的,直白点就是无效指针(野指针)。这个关键字只能在C99标准的C程序里使用,C++程序不支持,restrict的起源最早可以追溯到Fortran。

  • 写一个例子:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/time.h>
    #include <signal.h>
    
    void func(int no); //用作回调函数
    
    int main(int argc, char *argv[])
    {
        __sigaction_u sigactu;
        sigactu.__sa_handler = func; //另一个变量 sigactu.__sa_sigaction 不用赋值
    
        struct sigaction act;
        act.sa_flags = 0;     //通常给 0
        act.__sigaction_u = sigactu;
        sigemptyset(&act.sa_mask);  //清空 自定义信号集
        sigaddset(&act.sa_mask, SIGQUIT);  //向指定 系统阻塞集 中写入 自定义的信号集,添加需要屏蔽的信号(ctrl + 反斜杠 触发)
    
        sigaction(SIGINT, &act, NULL);
    
        while (true) {
            printf("Keep the thread running for the non-death state.\n");
            sleep(1);
        };
        
    }
    
    void func(int no) 
    {
        printf("捕捉的信号为: %d\n", no);
        sleep(4);
        printf("醒了\n");
    }
  • 分析运行:

    这里故意运行两次:

    第一次: 先按下 ctrl + c 发射 2 号信号 SIGINT, 等待其收到信号后,执行 3s 后 ,将 func() 内容跑完,再次按下 ctrl + \ 发射其他信号 SIGINT,系统收到后,程序立即死亡(此时该 SIGINT 已经过了临死屏蔽的状态)

    第二次: 先按下 ctrl + c 发射 2 号信号 SIGINT, 等待其收到信号后,执行不到 3s ,func() 内容还没跑完,立即按下 ctrl + \ 发射其他信号 SIGINT,系统收到后,程序没有立即死亡(此时该 SIGINT 正处于临死屏蔽的状态), 而是过了几秒钟在死亡。

  • 运行效果:


Linux 中的 sigaction() 函数:

Linux 中的该函数,和 Unix 有点不一致,但主要填写的四个参数是一样的,这是不变的;

这里贴出 Linux 下的函数原型,可以和 Unix 下的 sigaction() 比较一下:


下载地址:

15_sys_usr_signal

欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。