Linux信号

信号

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

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

Linux信号概述

发送信号

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

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

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

image-20220822113352089

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

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

image-20220822114203553

信号处理方式

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

#include <signal.h>
/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);

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

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

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

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

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

#include <signal.h>

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是否在信号集中 */	

进程掩码

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

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

#include <signal.h>

/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

set参数指定新的信号掩码

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

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

image-20220825111927286

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

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

被挂起的信号

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

#include <signal.h>

int sigpending(sigset_t *set);

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

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

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

信号集这几个函数举例

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

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

image-20220825181937952

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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;
}

image-20220825215756905

信号函数

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

signal系统调用

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

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.  */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>

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;
}

image-20220825182345010

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

sigaction系统函数

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

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

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);
};

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

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

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

image-20220824165327097

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

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

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

简单是使用例子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

/*自定义的信号捕捉函数*/
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;
}

image-20220826155323696

一些疑惑

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

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

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/*自定义的信号捕捉函数*/
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;
}

image-20220826161922094

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

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

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

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/*自定义的信号捕捉函数*/
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;
}

image-20220826163813060

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

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

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

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/*自定义的信号捕捉函数*/
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;
}

image-20220826165823734

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值