Linux系统编程——信号篇

Linux支持的信号

image.pngimage.pngimage.png
image.png
image.pngimage.png
image.pngimage.png

信号处理函数

void my_handler(int signo);

signal

sighandler_t signal(int signo,sighandler_t handler);
#include<signal.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>

//handler for SIGINT SIGTERM
static void my_handler(int signo)
{
    if(signo == SIGINT){
        printf("Caught SIGINT !\n");
    }
    else if(signo == SIGTERM){
        printf("Caught SIGTERM !\n");
    }
    else{
        fprintf(stderr,"Unexpected signal! \n");
    }
    exit(EXIT_SUCCESS);
}

int main(){
    if(signal(SIGINT,my_handler) == SIG_ERR){
        fprintf(stderr,"Cannot handle SIGINT !\n");
        exit(EXIT_FAILURE);    
    }
    if(signal(SIGTERM,my_handler) == SIG_ERR){
        fprintf(stderr,"Cannot handle SIGTERM !\n");
        exit(EXIT_FAILURE); 
    }
    if(signal(SIGPROF,my_handler) == SIG_ERR){
        fprintf(stderr,"Cannot handle SIGPROF !\n");
        exit(EXIT_FAILURE); 
    }
    if(signal(SIGHUP,my_handler) == SIG_ERR){
        fprintf(stderr,"Cannot handle SIGHUP !\n");
        exit(EXIT_FAILURE); 
    }
    for(;;)
        pause();
    return 0;
}

image.png

等待信号——pause()

#include<unistd.h>
int pause(void);

pause()只在接收到可捕获的信号时候才返回,在这种情况下该信号被处理,pause()返回-1,并将error设置为ENTER.

sigaction() – 改变信号的配置

检查或修改与指定信号相关联的处理动作(可同时两种操作)
image.png

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

signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。
◆ signum:要操作的信号。
◆ act:要设置的对信号的新处理方式。
◆ oldact:原来对信号的处理方式。
◆ 返回值:0 表示成功,-1 表示有错误发生。

** struct 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、联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。

2、由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

typedef struct siginfo_t{ 
    int si_signo;//信号编号 
    int si_errno;//如果为非零值则错误代码与之关联 
    int si_code;//说明进程如何接收信号以及从何处收到 
    pid_t si_pid;//适用于SIGCHLD,代表被终止进程的PID 
    pid_t si_uid;//适用于SIGCHLD,代表被终止进程所拥有进程的UID 
    int si_status;//适用于SIGCHLD,代表被终止进程的状态 
    clock_t si_utime;//适用于SIGCHLD,代表被终止进程所消耗的用户时间 
    clock_t si_stime;//适用于SIGCHLD,代表被终止进程所消耗系统的时间 
    sigval_t si_value; 
    int si_int; 
    void * si_ptr; 
    void* si_addr; 
    int si_band; 
    int si_fd; 
};
  • sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
    • SIG_IGN:ignore signal,忽略signum表示的信号
    • SIG_DFL:恢复到默认配置
  • sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置。 sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。

注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。

  • sa_flags 用来设置信号处理的其他相关操作。sa_flags 成员用于指定信号处理的行为,它可以是一下值的“按位或”组合。
    • SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
    • SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
    • SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
    • SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
    • **SA_NODEFER **:一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
    • SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

用法1

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
static void sig_usr(int signum)
{
    if(signum == SIGUSR1)
    {
        printf("SIGUSR1 received\n");
    }
    else if(signum == SIGUSR2)
    {
        printf("SIGUSR2 received\n");
    }
    else
    {
        printf("signal %d received\n", signum);
    }
}
 
int main(void)
{
    char buf[512];
    int  n;
    struct sigaction sa_usr;
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_usr;   //信号处理函数
    
    sigaction(SIGUSR1, &sa_usr, NULL);
    sigaction(SIGUSR2, &sa_usr, NULL);
    
    printf("My PID is %d\n", getpid());
    
    while(1)
    {
        if((n = read(STDIN_FILENO, buf, 511)) == -1)
        {
            if(errno == EINTR)
            {
                printf("read is interrupted by signal\n");
            }
        }
        else
        {
            buf[n] = '\0';
            printf("%d bytes read: %s\n", n, buf);
        }
    }
    
    return 0;
}

在这个例程中使用 sigaction 函数为 SIGUSR1 和 SIGUSR2 信号注册了处理函数,然后从标准输入读入字符。程序运行后首先输出自己的 PID,如:My PID is 5904
这时如果从另外一个终端向进程发送 SIGUSR1 或 SIGUSR2 信号,用类似如下的命令:kill -USR1 5904
则程序将继续输出如下内容:
** SIGUSR1 received** read is interrupted by signal**
这说明用 sigaction 注册信号处理函数时,不会自动重新发起被信号打断的系统调用。如果需要自动重新发起,则要设置 SA_RESTART 标志,比如在上述例程中可以进行类似一下的设置:sa_usr.sa_flags = SA_RESTART;

用法2

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

int main()
{
    struct sigaction newact, oldact;

    newact.sa_handler = SIG_IGN;   //信号忽略
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    int count = 0;
    pid_t pid = 0;

    sigaction(SIGINT,&newact,&oldact); //原来的备份到oldact里面

    pid = fork();
    if(pid == 0)
    {
        while(1)
        {
            printf("I'm child .....\n");
            sleep(1);
        }
        return 0;
    }
    while(1)
    {
        if(count++ > 3)
        {
            sigaction(SIGINT,&oldact,NULL);
            printf("pid = %d\n",pid);
            kill(pid,SIGILL);
        }
        printf("I'm father .....\n");
        sleep(1);
    }
    return 0;
}

下面用一个例程来说明 sigaction 函数的使用,代码如下

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
static void sig_usr(int signum)
{
    if(signum == SIGUSR1)
    {
        printf("SIGUSR1 received\n");
    }
    else if(signum == SIGUSR2)
    {
        printf("SIGUSR2 received\n");
    }
    else
    {
        printf("signal %d received\n", signum);
    }
}
 
int main(void)
{
    char buf[512];
    int  n;
    struct sigaction sa_usr;
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_usr;   //信号处理函数
    
    sigaction(SIGUSR1, &sa_usr, NULL);
    sigaction(SIGUSR2, &sa_usr, NULL);
    
    printf("My PID is %d\n", getpid());
    
    while(1)
    {
        if((n = read(STDIN_FILENO, buf, 511)) == -1)
        {
            if(errno == EINTR)
            {
                printf("read is interrupted by signal\n");
            }
        }
        else
        {
            buf[n] = '\0';
            printf("%d bytes read: %s\n", n, buf);
        }
    }
    
    return 0;
}

在这个例程中使用 sigaction 函数为 SIGUSR1 和 SIGUSR2 信号注册了处理函数,然后从标准输入读入字符。程序运行后首先输出自己的 PID,如:My PID is 5904
这时如果从另外一个终端向进程发送 SIGUSR1 或 SIGUSR2 信号,用类似如下的命令:kill -USR1 5904
image.png
现在在另一个终端输入Kill命令
image.png
现在再看终端1
image.png

忽略信号

image.png

sa_handler:可以是函数

image.png
处理器也可以是函数,这个函数的作用就是当按下“ctrl-c”的时候,程序没有停止,还能继续运行

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

void show_handler(int sig)
{
    printf("I got  signal %d\n",sig);
    int i;
    for(i = 0; i < 5;i++)
    {
        printf("i = %d\n",i);
        sleep(1);
    }

}
int main(void)
{
    int i = 0;
    struct sigaction act, oldact;
    act.sa_handler = show_handler;
    sigaddset(&act.sa_mask, SIGQUIT);         //ctrl+C,sig=2....ctrl+\,推出
    act.sa_flags = SA_RESETHAND | SA_NODEFER; //见注(2)
    //act.sa_flags = 0;                      //见注(3)
 
    sigaction(SIGINT, &act, &oldact);
    while(1) 
   {
        sleep(1);
        printf("sleeping %d\n", i);
        i++;
    }
}

(1)如果在信号SIGINT(Ctrl + c)的信号处理函数show_handler执行过程中,本进程收到信号SIGQUIT(Crt+),将阻塞该信号,直到show_handler执行结束才会处理信号SIGQUIT。
(2)SA_NODEFER 一般情况下, 当信号处理函数运行时,内核将阻塞<该给定信号 – SIGINT>。但是如果设置了
SA_NODEFER
标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。 SA_NODEFER是这个标记的正式的POSIX名字(还有一个名字SA_NOMASK,为了软件的可移植性,一般不用这个名字)
SA_RESETHAND 当调用信号处理函数时,将信号的处理函数重置为缺省值。 SA_RESETHAND是这个标记的正式的POSIX名字(还有一个名字SA_ONESHOT,为了软件的可移植性,一般不用这个名字)
(3) 如果不需要重置该给定信号的处理函数为缺省值;并且不需要阻塞该给定信号(无须设置sa_flags标志),那么必须将sa_flags清零,否则运行将会产生段错误。但是sa_flags清零后可能会造成信号丢失!

sa_mask——屏蔽信号

实例三:验证sigaction.sa_mask效果

#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void handler(int sig);

int main(int argc, char *argv[])
{
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT);
    act.sa_flags = 0;

    if (sigaction(SIGINT, &act, NULL) < 0)
        ERR_EXIT("sigaction error");

    struct sigaction act2;
    act2.sa_handler = handler;
    sigemptyset(&act2.sa_mask);
    act2.sa_flags = 0;

    if (sigaction(SIGQUIT, &act2, NULL) < 0)
        ERR_EXIT("sigaction error");

    for (;;)
        pause();
    return 0;
}

void handler(int sig)
{
    if(sig == SIGINT){

        printf("recv a SIGINT signal\n");
        sleep(5);
    }
    if (sig == SIGQUIT)
    {
        printf("recv a SIGQUIT signal\n");
    }
}

image.png
可知,安装信号SIGINT时,将SIGQUIT加入到sa_mask阻塞集中,则当SIGINT信号正在执行处理函数时,SIGQUIT信号将被阻塞,只有当SIGINT信号处理函数执行完后才解除对SIGQUIT信号的阻塞,由于SIGQUIT是不可靠信号,不支持排队,所以只递达一次

查看信号函数

pause()返回后,确定进程的挂起信号集(使用sigpending()),
测试哪些信号在该集合中(使用sigismember(),遍历范围1 <= s < NSIG中的所有信号),
并打印它们的描述(strsignal())。

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

int main(int argc, char* argv[])
{
    for(int sig=1;sig < NSIG; sig++)
    {
        printf("%2d: %s\n",sig, strsignal(sig));
    }
    exit(EXIT_SUCCESS);
}

image.pngimage.png

sigqueue——发送信号

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

信号没有排队

  1. 挂起的(标准)信号集是一个掩码
  2. 如果阻塞时多次产生相同的信号,则只发送一次
  3. 相比之下,实时信号是排队的

之前学过kill,raise,alarm,abort等功能稍简单的信号发送函数,现在我们学习一种新的功能比较强大的信号发送函数sigqueue.

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

int sigqueue(pid_t pid, int sig, const union sigval val)

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

  • 第一个参数是指定接收信号的进程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指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

实例:进程间的通讯

接收端:

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

void sighandler(int signo, siginfo_t *info,void *ctx);
//给自身传递信息
int main(void)
{

    struct sigaction act;
    act.sa_sigaction = sighandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;//信息传递开关
    if(sigaction(SIGINT,&act,NULL) == -1){
        perror("sigaction error");
        exit(EXIT_FAILURE);
    }
    for(; ;){
        printf("waiting a SIGINT signal....\n");
        pause();
    }
    return 0;
}

void sighandler(int signo, siginfo_t *info,void *ctx)
{
    //以下两种方式都能获得sigqueue发来的数据
    printf("receive the data from siqueue by info->si_int is %d\n",info->si_int);
    printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int);

}

发送端:

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

int main(int argc, char **argv)
{/
    
    if(argc != 2){
        fprintf(stderr,"usage:%s pid\n",argv[0]);
        exit(EXIT_FAILURE);
    }
    pid_t pid = atoi(argv[1]);    
    sleep(2);
    union sigval mysigval;
    mysigval.sival_int = 100;
    printf("sending SIGINT signal to %d......\n",pid);
    if(sigqueue(pid,SIGINT,mysigval) == -1){
        perror("sigqueue error");
        exit(EXIT_FAILURE);
    }
    return 0;
}

image.png
查找后台的接收端的Pid
image.pngimage.png
由图可知接收成功

sigpending 获取当前未决的信号

前面提到过,通过修改 信号屏蔽字可以屏蔽信号。当内核发送了一个对当前进程而言已经被屏蔽的信号,且进程不忽略该信号,那么该信号就是未决的。它并不会被捕获,但是信号也没有消失。等到进程不再屏蔽它时,它就会被捕获。
注意,屏蔽与忽略是不同的。
进程屏蔽信号,信号还在,只是没被接收;进程对信号的动作为忽略,那是进程已经接收到信号了,只是不采取任何动作。
如何获取当前进程的pending进程呢?调用sigpending来实现

#include<signal.h>
int sigpending(sigset_t *set);

我们可以通过先屏蔽一个信号,再向进程发送这个信号,最后再取消对信号的屏蔽,来观察pending的效果。

int main(){
	//1,注册信号
	if(signal(SIGUSR1, sig_usr_new) == SIG_ERR)
	{
		printf("can't catch SIGUSR1 !\n");
		exit(1);
	}
 
	//2, 屏蔽SIGUSR1
	sigset_t sigset, oldmask, pendmask;
 
	//2.1 初始化sigset
	if (sigemptyset(&sigset) < 0)
	{
		printf("sigemptyset error !\n");
		exit(1);
	}
 
	//2.2 添加要屏蔽的信号
	if (sigaddset(&sigset, SIGUSR1) < 0)
	{
		printf("add SIGUSR1 error!\n");
		exit(1);
	}
 
	//2.3 修改当前信号屏蔽字
    // 以前的屏蔽字保存进oldmask,便于后面恢复
	if (sigprocmask(SIG_BLOCK, &sigset, &oldmask) < 0)
	{
		printf("can't block SIGUSR1!\n");
		exit(1);
	}
 
	//3,休眠
	sleep(10); //在休眠期间通过kill发送SIGUSR1
 
	//4, 获取当前的pending信号
	if(sigpending(&pendmask) < 0)
	{
		printf("pend error!\n");
		exit(1);		
	}
 
	//5,判断USR1是不是在pendmask里
	if (sigismember(&pendmask, SIGUSR1))
	{
		printf("pending SIGSUR1\n");
	}
 
	//6,恢复信号屏蔽字
	printf("\nstart to unblock SIGUSR1!\n");
	if (sigprocmask(SIG_SETMASK, &oldmask, NULL))
	{
		printf("can't unblock SIGUSR1!\n");
		exit(1);
	}
	printf("unblock SIGUSR1\n");
 
	//7,死循环,为了防止进程退出
	while(1){
		pause();
	}
}

我们在休眠期间通过kill发送SIGUSR1信号,效果如下:

➜  code g++ -g -W -o study_Linux study_Linux.c
➜  code ./study_Linux &
[1] 136
➜  code kill -USR1 136
➜  code pending SIGSUR1
 
start to unblock SIGUSR1!
recived SIGUSR, signo = 10
unblock SIGUSR1

我们可以看到,发送USR1后,其对应的signal handler并没有被马上调用,因为该信号被屏蔽了,或者说被pending了。
等我们取消该信号的屏蔽之后,其马上被捕获,出发signal handler。所以被屏蔽的信号并没有消失,只是在等待被捕获。
请注意,在第二次调用sigprocmask时的返回顺序。
第二次调用sigprocmask是取消对信号的屏蔽,取消后SIGUSR1对应的信号处理程序就会被调用。该信号处理程序(sig_usr_new)先返回,打印“recived SIGUSR, signo = 10”,然后sigprocmask才返回,打印“unblock SIGUSR1”。

操作信号集——多信号

image.png

sigemptyset()——初始化set为不包含任何信号

sigfillset()——初始化set包含全部信号

sigaddset() —— 添加sig到set

sigdelset() —— 删除sig从set

sigismember()——判断sig是否在set

sigprocmask()——向呼叫者的信号掩码添加信号或从中删除信号

向呼叫者的信号掩码添加信号或从中删除信号
一个进程的信号屏蔽字规定了当前阻塞而给该进程的信号集调用函数sigprocmask可以检测或更改其信号屏蔽字,或者在一个步骤中同时执行这两个操作。
image.png
image.png
SIG_SETMASK : 给信号掩码赋值

int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );

返回值:若成功则返回0,若出错则返回-1
首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。

if(sigprocmask(SIG_BLOCK,&newmask,&oldmask) < 0) #屏蔽newmask,将当前的保存到oldmask

例如:暂时阻塞一个信号 SIG_BLOCK, SIG_UNBLOCK的使用
下面的代码片段展示了如何在执行代码块时临时阻塞信号(SIGINT)。

#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<time.h>
int main(int argc,char *argv[])
{
    sigset_t newmask,oldmask;
    int i = 1;
    while(i<=3)
    {
	sleep(1);
	i++;
    }
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGINT);
    if(sigprocmask(SIG_BLOCK,&newmask,&oldmask) < 0)
    {
		printf("newmask is not Blocked.\n");
        exit(EXIT_SUCCESS);
    }
    else
    {
		printf("newmask is Blocked.\n");
    }

    i = 1;
    while(i <= 5)
    {
		sleep(1);
		i++;
    }
    printf("---------------------------------------------\n");
    
    if(sigprocmask(SIG_UNBLOCK,&newmask,&oldmask) < 0)
    {
		printf("newmask is not  UNBlocked.\n");
    }
    else
    {
		printf("SIGINT is UnBlocked.\n");
    }
    while(1);
}

在3s内,ctrl+c
image.png
在3-8s内,ctrl+c
image.png
在这种情况下,我们看到了阻塞信号和屏蔽信号的区别。这里在SIGINT信号阻塞后再向进程发送该信号,该信号会阻塞,直到下面第一个参数为UNBLOCK的sigprocmask()函数成功执行,这时刚才的SIGINT信号会立即传送到该进程:所以,SIGINT Unblocked未输出,进程就结束了.
在8s后按下组合键Ctrl + C.
image.png

SIG_SETMASK参数的使用:

void setmask(void)
{
    int i;
    sigset_t newmask1, newmask2, oldmask;
    i = 1;
    while(i <= 3)
    {
      sleep(1);
      i += 1;
    }
    sigemptyset(&newmask1);
    sigemptyset(&newmask2);
    sigaddset(&newmask1, SIGINT);
    sigaddset(&newmask2, SIGQUIT);
    if (sigprocmask(SIG_BLOCK, &newmask1, &oldmask) < 0)  
    { 
        fprintf(stderr, "SIG_BLOCK error\n"); 
        exit(1); 
    }
    else
    {
      printf("SIGINT blocked.\n");

    }
    i = 1;
    while(i <= 5)
    {
      sleep(1);
      i += 1;
    }
    printf("--------------------------------------\n");
    if(sigprocmask(SIG_SETMASK, &newmask2, NULL) < 0)
    {
      fprintf(stderr, "SIG_SETMASK error\n");
    }
    else
    {
        printf("SIGINT unblocked and SIGQUIT blocked.\n");
    }
    while(1);
}

8s后,SIGINT信号由阻塞状态变为正常,SIGQUIT信号由正常变为阻塞状态.

Exercise

image.png

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

static void signal_handler(int signo)
{
    if(SIGUSR1 == signo){
        printf("Caught SIGUSR1.\n");
    }
    else if(SIGUSR2 == signo){
        printf("Caught SIGUSR2.\n");
    }
    else{
        printf("no SIG.\n");
    }
}

int main(int argc, char* argv[])
{
    printf("my PID is: %d\n",getpid());

    struct sigaction sa;

    sa.sa_handler = signal_handler;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask,SIGUSR1);
    if(sigaction(SIGUSR1,&sa,NULL) == -1){
        perror("sigaction SIGUSR1\n");
    }
    
    sigset_t sigset,pendmask,oldmask;
    sigemptyset(&sigset);
    sigaddset(&sigset,SIGUSR1);

    // 以前的屏蔽字保存进oldmask,便于后面恢复
    if(sigprocmask(SIG_BLOCK,&sigset,&oldmask) < 0){
        printf("newmask is not Blocked.\n");
        exit(EXIT_SUCCESS);
    }

    sleep(10);
    // pause();  //休眠

    //获取当前的pending信号
    if(sigpending(&pendmask) < 0){
        printf("pend error!\n");
        exit(EXIT_SUCCESS);
    }
    printf("sigpending success\n");

    //判断USR1是否在pendmask里
    if(sigismember(&pendmask,SIGUSR1)){
        printf("pending SIGUSR1\n");
    }
    //printf("pendingmask not has SIGUSR1\n");

    //恢复信号屏蔽字
    printf("\nStart to unblock SIGUSR1 !\n");
    if(sigprocmask(SIG_SETMASK,&oldmask,NULL) < 0){
        printf("SIGUSR1 is not unblocked.\n");
        exit(EXIT_SUCCESS);
    }
    printf("unblock SIGUSR1\n");


    //查看所有描述符
    for(int sig=1;sig < NSIG; sig++)
    {
        printf("%2d: %s\n",sig, strsignal(sig));
    }
    exit(EXIT_SUCCESS);
}

输入kill指令之前
image.png
image.png
image.pngimage.pngimage.png


信号处理——Signal handler

信号处理程序的执行步骤
以下步骤发生在信号处理程序的执行中:

  1. 发生硬件中断
  • 发生硬件中断
  • 进程从CPU调度
  • 内核获得控制并接收各种进程上下文信息,并保存这些信息(例如,寄存器值(程序计数器、堆栈指针等))
  1. 在中断处理完成后,内核选择一个进程来调度,并发现它有一个挂起信号

设计信号处理程序——sigsetjmp/siglongjmp

在信号处理程序中经常调用longjmp函数以返回到程序的主循环中,而不是从该处理程序返回。
**由于在信号处理期间自动屏蔽了正在被处理的信号,而使用setjmp/longjmp跳出信号处理程序时又不会自动将信号屏蔽码修改会原来的屏蔽码,从而引起该信号被永久屏蔽。**可以使用sigsetjmp/siglongjmp来解决这一问题。
sigsetjmp()会保存目前堆栈环境,然后将目前的地址作一个记号,而在程序其他地方调用siglongjmp()时便会直接跳到这个记号位置,然后还原堆栈,继续程序好执行。

基本语法
#include<setjmp.h>
void siglongjmp(sigjmp_buf env,int val);
int sigsetjmp(sigjmp_buf env,int savesigs);

POSIX标准没有为siglongjmp定义错误。sigsetjmp被直接激活是返回0,通过siglongjmp被激活是返回参数val的值。
参数env为用来保存目前堆栈环境,一般声明为全局变量
参数savesigs若为非0则代表搁置的信号集合也会一块保存
当sigsetjmp()返回0时代表已经做好记号上,若返回非0则代表由siglongjmp()跳转回来
返回值 :返回0代表局促存好目前的堆栈环境,随时可供siglongjmp()调用, 若返回非0值则代表由siglongjmp()返回

与setjmp和longjmp之间区别

这两个函数与setjmp和longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字调用siglongjmp时,如果带 非0 savemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字

先介绍以下setjmp和longjmp函数

int setjmp(jmp_buf env)
void longjmp(jmp_buf env, int value)

setjmp:

建立本地的jmp_buf缓冲区并且初始化,用于将来跳转回此处。这个子程序保存程序的调用环境于env参数所指的缓冲区,env将被longjmp使用。如果是从setjmp直接调用返回,setjmp返回值为0。如果是从longjmp恢复的程序调用环境返回,setjmp返回非零值。

longjmp:

恢复env所指的缓冲区中的程序调用环境上下文,env所指缓冲区的内容是由setjmp子程序调用所保存。value的值从longjmp传递给setjmp。longjmp完成后,程序从对应的setjmp调用处继续执行,如同setjmp调用刚刚完成。如果value传递给longjmp零值,setjmp的返回值为1;否则,setjmp的返回值为value。

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

jmp_buf env;  //保存待跳转位置的栈信息

void handler (int signal) {
    printf("caught exception\n");
    longjmp(env, 2);
}

int main(int argc, char* argv[]) {
    {
        struct sigaction sa = {};
        sa.sa_handler = handler;
        //sigemptyset(&sa.sa_mask);
        //sa.sa_flags = 0;
        if(sigaction(SIGFPE, &sa, NULL) == -1) {
            perror("sigaction");
        }
    }

    if (0 == setjmp(env)) {
        int a = 3/(argc - 1);
        printf("a is %d\n", a);
    } else {
        //exception
        printf("in exception\n");
    }

    return 1;
}

这种方法看起来与goto相似,但是是有区别的,区别如下:
(1)goto语句不能跳出C语言当前的函数。
(2)用longjmp只能跳回曾经到过的地方。在执行setjmp的地方仍留有一个过程活动记录。从这个角度上讲,longjmp更象是“从何处来”,而不是“要往哪去”。另外,longjmp接受一个额外的整形参数并返回它的值,这可以知道是由longjmp转移到这里的还是从上一条语句执行后自然执行到这里的。

#include<string.h>
#include<setjmp.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>

static volatile sig_atomic_t canJump = 0;

#ifdef USE_SIGSETJMP
static sigjmp_buf senv;
#else
static jmp_buf env;
#endif

void                    /* Print list of signals within a signal set */
printSigset(FILE *of, const char *prefix, const sigset_t *sigset)
{
    int sig, cnt;

    cnt = 0;
    for (sig = 1; sig < NSIG; sig++) {
        if (sigismember(sigset, sig)) {
            cnt++;
            fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig));
        }
    }

    if (cnt == 0)
        fprintf(of, "%s<empty signal set>\n", prefix);
}

int                     /* Print mask of blocked signals for this process */
printSigMask(FILE *of, const char *msg)
{
    sigset_t currMask;

    if (msg != NULL)
        fprintf(of, "%s", msg);

    if (sigprocmask(SIG_BLOCK, NULL, &currMask) == -1)
        return -1;

    printSigset(of, "\t", &currMask);

    return 0;
}

static void signal_handler(int signo){
    printf("\nReceived signal %d(%s),signal mask is:",signo,strsignal(signo));
    printSigMask(stdout, NULL);
    
    if(!canJump){
        printf("'env' buffer not yet set, doing a simple return\n");
        return;
    }

#ifdef USE_SIGSETJMP
    siglongjmp(senv,1);
#else
    longjmp(env,1);
#endif
}

int main(int argc, char *argv[]){
    struct sigaction sa;

    printSigMask(stdout, "Signal mask at startup:");
    
    sigemptyset(&sa.sa_mask);
    sa.sa_flags=0;
    sa.sa_handler=signal_handler;
    if(sigaction(SIGINT,&sa,NULL) == -1){
        perror("sigaction");
    }
#ifdef USE_SIGSETJMP
    printf("Calling sigsetjmp()\n");
    if(sigsetjmp(senv,1) == 0)
#else
    printf("Calling setjmp()\n");
    if(setjmp(env) == 0)
#endif
    canJump = 1;
    else
        printSigMask(stdout, "After jump from handler, signal mask is:" );

    for(int i = 0; i < 5; i++)
        sleep(1);
        // pause();
    return 0;
}

image.png

不可重入函数

在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果有一个函数不幸被设计成为这样:那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数

相反,肯定有一个安全的函数,这个安全的函数又叫可重入函数。那么什么是可重入函数呢?所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。

  1. 更新全局/静态变量的函数是不可重入的:

有些函数本质上是对全局数据进行操作的。
例如,malloc()和free()维护一个可用内存块的全局链表
假设主程序正在执行free(),并被一个也调用free()的信号处理程序中断…
两个“线程”同时更新链表⇒混乱!

  1. 在静态分配的内存中返回结果的函数是不可重入的

例如,getpwnam()和C库中的许多其他函数

  1. 使用静态数据结构进行内部记账的函数是不可重入的

例如,stdio函数为缓冲I/O执行此操作
保证函数的可重入性的方法:

1)在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量);
2)对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。

满足下列条件的函数多数是不可重入(不安全)的:

1)函数体内使用了静态的数据结构;
2)函数体内调用了malloc() 或者 free() 函数;
3)函数体内调用了标准 I/O 函数。

只要遵守了几条很容易理解的规则,那么写出来的函数就是可重入的:

1)不要使用全局变量。因为别的代码很可能改变这些变量值。
2)在和硬件发生交互的时候,切记执行类似 disinterrupt() 之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/ 退出核心”。
3)不能调用其它任何不可重入的函数。
4)谨慎使用堆栈。

接下来看书中的源码例子

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

static char* str2;
static int handled = 0;

static void handler(int signo){
    strcpy(str2,"xx");
    printf("handler running...\n");
    handled++;
}

int main(int argc,char *argv[])
{
    char *cr1;
    int callNum,mismatch;

    struct sigaction sa;

    if(argc != 3)
    {
        fprintf(stderr,"%s str1 str2\n",argv[0]);
    }
    str2 = argv[2];
    cr1 = strdup(strcpy(argv[1],"xx"));

    if(cr1 == NULL)
    {
        printf("strdup error.\n");
    }

    sigemptyset(&sa.sa_mask);
    sa.sa_handler = handler;
    sa.sa_flags = 0;
    if(sigaction(SIGINT,&sa,NULL) == -1)
    {
        fprintf(stderr,"SIGINT error.\n");
    }

    for(callNum=1,mismatch=0;;callNum++)
    {
        if(strcmp(strcpy(argv[1],"xx"),cr1) != 0){
            mismatch++;
            printf("Mismatch is call %d (mismatch=%d handled=%d)\n",callNum,mismatch,handled);
        }
    }
    printf("end\n");
    return 0;
}

image.png
Linux常见的可重入函数

异步信号安全函数——Async-Signal-Safe Function

可以不受信号捕获函数的限制而调用的函数。没有任何函数是异步信号安全的,除非明确地这样描述。
Reentrant Function 必然是Thread-Safe FunctionAsync-Signal-Safe Function
异步信号安全函数是可以从信号处理程序安全地调用的函数。
函数可以是异步信号安全的,因为

  1. 它是可重入的
  2. 它不能被信号处理程序中断

POSIX指定了异步信号安全所需的一组函数

  1. 参见signal-safety(7)或TLPI表21-1
  2. Set是POSIX中指定的一小部分函数

不能保证不在列表中的函数

  • stdio函数不在列表

信号处理程序本身可以是不可重入的
如果信号处理程序更新主程序使用的全局数据,它也可以是不可重入的
常见的情况是:handler调用更新errno的函数

sig_atomic_t数据类型

sig_atomic_t这个类型是定义在signal.h文件中。当把变量声明为该类型会保证该变量在使用或赋值时, 无论是在32位还是64位的机器上都能保证操作是原子的, 它会根据机器的类型自动适应。

  • 在处理信号(signal)的时候,有时对于一些变量的访问希望不会被中断,无论是硬件中断还是软件中断,这就要求访问或改变这些变量需要在计算机的一条指令内完成。
  • 通常情况下,int类型的变量通常是原子访问的,也可以认为 sig_atomic_t就是int类型的数据,因为对这些变量要求一条指令完成,所以sig_atomic_t不可能是结构体,只会是数字类型。
#include <signal.h>

typedef int __sig_atomic_t;

typedef __sig_atomic_t sig_atomic_t;

POSIX定义了一个整数数据类型,可以在handler和main()之间安全地共享:

#include<stdint.h>

typedef int __sig_atomic_t;
  • 读写保证原子性
  • 其他不保证原子性(即不安全)的操作(例如++和–)
  • 指定volatile限定符以防止优化器欺骗
volatile sig_atomic_t flag;

举例

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

static volatile sig_atomic_t gSignalStatus = 0;

static void signal_handler(int signo){
    gSignalStatus = signo;
}

int main(){
    //install a signal handler
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);
    if(sigaction(SIGINT,&sa,NULL) == -1){
        perror("sigaction");
    }

    printf("SignalValue: %d\n",gSignalStatus);
    printf("Sending signal %d\n",SIGINT);
    raise(SIGINT);  //给当前进程发送指定信号(自己给自己发信号),raise(signo)相当于kill(getpid(),signo)
    printf("SignalValue: %d\n",gSignalStatus);
    return 0;
}
// raise可以用最新的sigqueue来替代,如下所示
	union sigval mysigval;
    mysigval.sival_int = 100;

    printf("SignalValue: %d\n",gSignalStatus);
    printf("Sending signal %d\n",SIGINT);
    // raise(SIGINT);  //给当前进程发送指定信号(自己给自己发信号),raise(signo)相当于kill(getpid(),signo)
    if(sigqueue(getpid(),SIGINT,mysigval) == -1)
    {
        perror("sigqueue");
    }
    printf("sigqueue is runing.\n");
    printf("SignalValue: %d\n",gSignalStatus);

image.png

中断系统调用

自动重启系统调用:SA_RESTART

在安装处理程序时在sa_flags中指定SA_RESTART将导致系统调用自动重新启动。一旦给信号设置了SA_RESTART标记,那么当执行某个阻塞系统调用时,收到该信号时,进程不会返回,而是重新执行该系统调用。

  • SA_RESTART是单个信号标志

比手动重启更方便,但是…

  • 并非所有系统调用都会自动重启
  • 重新启动的系统调用集因UNIX系统而异

SA_SIGINFO信号处理器

使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
接收额外的信号信息:SA_SIGINFO
处理程序地址通过act传递。Sa_sigaction字段(不是通常的act.sa_handler)
image.png

信号蹦床——Signal trampoline

内核使用信号蹦床来安排在信号处理程序执行后将控制弹回内核。

sigreturn——从信号处理函数返回,并清除栈帧
int sigreturn(...);

如果 Linux 内核确定某个进程有一个未阻塞的信号等待处理,那么当该进程下一次从内核态转换回用户态时(例如,从系统调用返回时或当进程被重新调度到 CPU 上时),它会创建用户空间堆栈、或定义的备用堆栈上的一个栈帧,用于保存各种进程上下文(处理器状态字、寄存器、信号掩码和信号堆栈设置)。
之后将会调用信号处理程序,信号处理程序返回时,调用“signal trampoline”的用户空间代码,此代码实现了类似于中断返回的操作,以帮助返回执行用户程序
sigreturn() 调用将撤消已完成的所有操作:

  • 更改进程的信号掩码
  • 切换信号堆栈(请参阅 sigaltstack(2)

sigreturn() 将使用先前保存在用户空间堆栈上的信息:

  • 恢复进程的信号掩码
  • 切换堆栈
  • 恢复进程的上下文(处理器标志和寄存器,包括堆栈指针和指令指针)

以便进程恢复在被信号中断的地方执行。
注意:

sigreturn() 的存在只是为了允许信号处理程序的实现。 永远不应该直接调用它。

在当代 Linux 系统上,根据体系结构,信号蹦床代码存在于 vdso(7) 或 C 库中。 在后一种情况下,C 库的 sigaction(2) 包装函数通过将 Trampoline 代码的地址放在 sigaction 结构的 sa_restorer 字段中来通知内核该蹦床代码的位置,并在 sa_flags 字段中设置 SA_RESTORER 标志。也就是说,这个Trampoline代码是C库封装好的,并且在调用sigaction时,自动(因为sigaction函数本身也是由C库封装)将地址写入到sa_restorer中
保存的进程上下文信息放置在 ucontext_t 结构中(参见 <sys/ucontext.h>)。 该结构作为通过带有 **SA_SIGINFO **标志的 sigaction(2) 建立的处理程序的第三个参数在信号处理程序中可见。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值