• Linux信号


    信号

    学习《Linux高性能服务器编程》第十章信号,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中

    Linux信号概述、信号集、信号函数和一些疑惑。

    Linux信号概述

    发送信号

    Linux 下,一个进程给其他进程发送信号的API是kill函数。其定义如下:

    #include 
    #include 
    
    int kill(pid_t pid, int sig);
    
    • 1
    • 2
    • 3
    • 4

    这个函数把信号sig发送给目标进程;目标进程pid参数指定,其可能的取值以及含义如表所展示:

    image-20220822113352089

    Linux当中信号都大于0,如果sig取值为0,则kill函数不发送任何信号。这种方法可以用来检测目标进程或进程组是否存在,但是这种方法是不可靠的(这种方法不是原子操作)。

    该函数成功时返回0,失败则返回-1并设置errno。几种可能的errno如表所示。

    image-20220822114203553

    信号处理方式

    在目标进程收到信息时,需要定义一个接收函数来处理。信号处理函数的原则如下:

    #include 
    /* Type of a signal handler.  */
    typedef void (*__sighandler_t) (int);
    
    • 1
    • 2
    • 3

    信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。

    除了用户自己定义信号处理函数之外,Linux当中还定义了信息号的两种其他处理方式:

    #define	SIG_DFL	 ((__sighandler_t)  0)	/* Default action.  */
    #define	SIG_IGN	 ((__sighandler_t)  1)	/* Ignore signal.  */
    
    • 1
    • 2

    SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号默认处理方式。信号默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件(Core)、暂停进程(Stop),以及继续进程(Cont)。

    Linux信号

    在linux上,可以使用kill -l命令看到所有的信号,但是我们并不关心这些所有的信号,只用重点关心SIGHUPSIGPIPESIGURGSIGALRMSIGCHLD等几个信号即可。

    信号起源默认行为含义
    SIGHUPPOSIXTerm控制终端挂起
    SIGPIPEPOSIXTerm往读端被关闭的管道或者socket连接中些数据
    SIGURG4.2BSDIgnsocket连接上接收到紧急数据
    SIGALRMPOSIXTerm由alarm 或setitimer设置的实时闹钟超时引起
    SIGCHLDPOSIXIgn子进程状态发生变化(退出或者暂停)

    信号集

    信号集函数

    信号集sigset_t 的定义如下

    #define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
    typedef struct
    {
      unsigned long int __val[_SIGSET_NWORDS];
    } __sigset_t;
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由该定义可见,sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号(虽然不知道为啥定义多个元素)。这种定义方式和文件描述符集fd_set类似。Linux提供了如下一组函数来设置、修改、删除和查询信号集:

    #include 
    
    int sigemptyset(sigset_t *set);						/* 清空信号集 */
    
    int sigfillset(sigset_t *set);						/* 在信号集中设置所有的信息 */
    
    int sigaddset(sigset_t *set, int signum);			/* 将信号signum添加到信号集中 */	
    
    int sigdelset(sigset_t *set, int signum);			/* 将信号signum从到信号集中删除 */	
    
    int sigismember(const sigset_t *set, int signum);	/* 测试信号signum是否在信号集中 */	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    进程掩码

    内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程的传递。如果将遭阻塞的信号发给某进程,那么对该信号的传递将延后,直至从进程信号掩码中移除该信号,从而解除阻塞为止。(信号掩码实际属于线程属性,在多线程进程中,每个线程都可使用 pthread_sigmask() 函数来独立检查和修改其信号掩码。)

    下面的函数可以用于设置或查看进程的信号掩码:

    #include 
    
    /* Prototype for the glibc wrapper function */
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    
    • 1
    • 2
    • 3
    • 4

    set参数指定新的信号掩码

    oldset参数输出原来的信号掩码(不过不为NULL)

    如果set参数不为NULL,则how参数指定设置进程信号掩码的方式,其可选值如表所示。

    image-20220825111927286

    如果set为 NULL,则进程信号掩码不变,此时我们仍然可以利用oldset参数来获得进程当前的信号掩码。

    sigprocmask成功时返回0,失败则返回-1并设置errno

    被挂起的信号

    设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:

    #include 
    
    int sigpending(sigset_t *set);
    
    • 1
    • 2
    • 3

    set用于保存被挂起的信号集。

    sigpending成功时返回0,失败则返回-1并设置errno

    进程多次接收到同一个被挂起的信号,sigpending 函数也只能反映一次。并且,当我们再次使用sigprocmask使能该挂起的信号时,该信号的处理函数也只被触发一次。

    信号集这几个函数举例

    我们以SIGINTSIGQUIT为例,就是键盘上按下(Ctrl+C)和(Ctrl+\)为例。

    通过sigprocmask设置这两个信号被挂起,然后分别按下(Ctrl+C)和(Ctrl+\),再通过sigpending查看那些进程被挂起(这个进程可以通过kill杀死)。

    image-20220825181937952

    #include 
    #include 
    #include 
    #include 
    void printset(sigset_t *ped)
    {
        int i;
        for (i = 1; i < 32; i++)
        {
    
            if ((sigismember(ped, i) == 1))
            {
                putchar('1');
            }
            else
            {
                putchar('0');
            }
        }
        printf("\n");
    }
    
    int main(int argc, char const *argv[])
    {
        sigset_t set, oldset, ped;
        sigemptyset(&set);
        sigaddset(&set, SIGINT);
        sigaddset(&set, SIGQUIT);
        sigprocmask(SIG_BLOCK, &set, &oldset);
        printf("进程信号掩码:");
        printset(&set);
        while (1)
        {
            sigpending(&ped); //获取信号集
            printf("被挂起的信号掩码:");
            printset(&ped);
            sleep(1);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    image-20220825215756905

    信号函数

    处理或者说捕捉信号的函数有signalsigaction

    signal系统调用

    #include 
    
    typedef void (*sighandler_t)(int);
    
    sighandler_t signal(int signum, sighandler_t handler);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    signum参数指出要捕获的信号类型。

    handler参数是sighandler_t类型的函数指针,用于指定信号signum的处理函数。

    signal函数成功时返回一个函数指针,该函数指针的类型也是sighandler_t。这个返回值是前一次调用signal函数时传入的函数指针,或者是信号signum对应的默认处理函数指针SIG_DEF(如果是第一次调用signal的话)。

    signal系统调用出错时返回SIG_ERR,并设置errno

    #define	SIG_ERR	 ((__sighandler_t) -1)	/* Error return.  */
    
    • 1
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void do_sig(int a)
    {
        printf("Hi, SIGINT, how do you do !\n");
    }
    
    int main(int argc, char const *argv[])
    {
        // 设置SIGINT信号(ctrl+C)对应的事件
        if (signal(SIGINT, do_sig) == SIG_ERR)
        {
            perror("signal");
            exit(1);
        }
    
        while (1)
        {
            printf("---------------------\n");
            sleep(1);
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    image-20220825182345010

    可以看的按下ctrl+c之后是杀不死这个进程的,但是可以通过“ctrl+\”或者关闭shell或者通过kill命令进行杀死。

    sigaction系统函数

    设置信号处理函数更为健壮的方法如下

    #include 
    
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    
    • 1
    • 2
    • 3

    signum参数指出要捕获的信号类型

    act参数指定新的信号处理方式

    oldact参数输出信号之前处理的方式(如果不为NULL的话)。

    actoldact都是sigaction结构体类型的指针,sigaction结构体定义如下:

    struct sigaction {
       void     (*sa_handler)(int);
       void     (*sa_sigaction)(int, siginfo_t *, void *);
       sigset_t   sa_mask;
       int        sa_flags;
       void     (*sa_restorer)(void);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    该结构体中的sa_handler成员指定信号处理函数。

    sa_mask成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask是信号集sigset_t (_sigset_t的同义词)类型,该类型指定一组信号。

    sa_flags成员用于设置程序收到信号时的行为,其可选值如表所示。

    image-20220824165327097

    sa_restorer成员已经过时,最好不要使用。

    sigaction成功时返回0,失败则返回-1并设置errno

    sigaction中有信号集,所以最好配合信号集函数一起使用。

    简单是使用例子

    #include 
    #include 
    #include 
    #include 
    
    /*自定义的信号捕捉函数*/
    void sig_int(int signo)
    {
    	printf("catch signal SIGINT\n"); //单次打印
    }
    
    int main(int argc, char const *argv[])
    {
    	struct sigaction act;
    
    	act.sa_handler = sig_int;
    	act.sa_flags = 0;
    	sigemptyset(&act.sa_mask); //不屏蔽任何信号
    
    	sigaction(SIGINT, &act, NULL);
    
    	while (1)
    	{
    		printf("---------------------\n");
    		sleep(1);
    	};
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    image-20220826155323696

    一些疑惑

    第一个疑惑是进程在处理一个信号的过程中,能接收另一个信号吗?

    答案是可以的,下面的代码接收了两个SIGINTSIGQUIT两个信号,在按下(Ctrl+C)后立刻按下(Ctrl+\),都能进行输出,说明进程在处理一个信号的过程中,能接收另一个信号

    #include 
    #include 
    #include 
    
    /*自定义的信号捕捉函数*/
    void sig_int(int signo)
    {
        printf("catch signal SIGINT\n");
        sleep(10); //模拟信号处理函数执行很长时间
        printf("end of SIGINT handler\n");
    }
    
    void sig_quit(int signo)
    {
        printf("catch signal SIGQUIT\n");
        sleep(10); //模拟信号处理函数执行很长时间
        printf("end of SIGQUIT handler\n");
    }
    
    int main(int argc, char const *argv[])
    {
        struct sigaction act1, act2;
    
        act1.sa_handler = sig_int;
        sigemptyset(&act1.sa_mask); //不屏蔽任何信号
        act1.sa_flags = 0;
    
        act2.sa_handler = sig_quit;
        sigemptyset(&act2.sa_mask); //不屏蔽任何信号
        act2.sa_flags = 0;
    
        sigaction(SIGINT, &act1, NULL);  //注册信号处理函数
        sigaction(SIGQUIT, &act2, NULL); //注册信号处理函数
    
        while (1)
        {
            printf("---------------------\n");
            sleep(1);
        };
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    image-20220826161922094

    第二个疑惑是信号在处理一个信号的过程中,会阻塞(挂起)这个信号吗?

    个人感觉是阻塞了这个信号的。也就是第1个信号在处理的过程中,收到再多这个信号也是不会进行处理的,知道第2个信号处理完毕,后面的第2到n个信号当作一次信号进行处理(这里的信号指相同一种信息)。

    我们以SIGINT为例,当我们按下(Ctrl+C)后,SIGINT信号的回调函数在进行处理,处理的过程中我们疯狂的按(Ctrl+C),最终后续的SIGINT信息只当作一次信息进行处理了。

    #include 
    #include 
    #include 
    
    /*自定义的信号捕捉函数*/
    void sig_int(int signo)
    {
    	printf("catch signal SIGINT\n");
    	sleep(5); //模拟信号处理函数执行很长时间
    	printf("end of SIGINT handler\n");
    }
    
    int main(int argc, char const *argv[])
    {
    	struct sigaction act;
    	act.sa_handler = sig_int;
    	sigemptyset(&act.sa_mask); //不屏蔽任何信号
    	act.sa_flags = 0;
    
    	sigaction(SIGINT, &act, NULL); //注册信号处理函数
    
    	while (1)
    	{
    		printf("---------------------\n");
    		sleep(1);
    	};
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    image-20220826163813060

    第三个疑惑是信号被屏蔽之后,我们多次发送该信号,信号被挂起了,再“解挂”或者叫取消屏蔽情况会如何?

    实际情况是,取消屏蔽后只会执行一次信号处理,后续的信号处理和普通信号处理相同。

    我们以SIGINT为例,先屏蔽SIGINT这个信息,在此期间我们不停的发信息,后续取消屏蔽后,信号的回调函数被处理了一次。再后续的信息处理和普通信号类似。

    #include 
    #include 
    #include 
    #include 
    
    /*自定义的信号捕捉函数*/
    void sig_int(int signo)
    {
    	printf("catch signal SIGINT\n");
    }
    
    int main(int argc, char const *argv[])
    {
    	struct sigaction act;
    	act.sa_handler = sig_int;
    	sigemptyset(&act.sa_mask); //不屏蔽任何信号
    	act.sa_flags = 0;
    	sigaction(SIGINT, &act, NULL); //注册信号处理函数
    
    	sigset_t set, oldset, ped;
    	sigemptyset(&set);
    	sigaddset(&set, SIGINT); // 将SIGINT进行屏蔽
    	sigprocmask(SIG_BLOCK, &set, &oldset);
    	printf("-----begin sleep 10s--\n");
    	sleep(10);
    	printf("-----end sleep 10s--\n");
    	sigprocmask(SIG_UNBLOCK, &set, &oldset);
    	while (1)
    	{
    		printf("---------------------\n");
    		sleep(1);
    	}
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    image-20220826165823734

  • 相关阅读:
    MySQL的表格去重,史上最简便的算法,一看就会
    机器人EV3初级、中级、高级课程课件
    谁懂万方检索的高级检索嘛
    Python xlwings打开excel表格进行最大化
    动态调宽箱式机器人HEGERLS A42-FW|仓储存储密度再提高60%|刷新高密度的“天花板”
    静态代理和动态代理
    【毕业设计】基于单片机的室内环境温湿度检测系统 - 物联网 嵌入式 stm32
    C# 实现 Linux 视频会议(源码,支持信创环境,银河麒麟,统信UOS)
    Maven各方面配置好了却无法显示版本
    解决flask重启后端口被占用的问题(非kill方法)
  • 原文地址:https://blog.csdn.net/qq_41474648/article/details/126556777