Unix环境编程: 信号

Table of Contents

1 信号处理函数的注册

2 信号的发生

3 信号集合

4 信号掩码

5 信号挂起

6 最新的sigaction

7 sigsuspend(sigset_t *set)

8 信号的字符串信息

7. 多线程中的信号处理

总结


reference:https://stackoverflow.com/questions/11679568/signal-handling-with-multiple-threads-in-linux

Linux系统中支持各种各样的信号, 总共有31个之多,除此之外它还允许用户自定义信号,总之信号很多,用到的时候记得可以去 /usr/include/bits/signum.h 中去查看哦。

1 信号处理函数的注册

信号跟中断很类似,可以看做是一种软件中断,既然是中断,那么就应该有中断处理函数,没有我们使用下面的函数来注册一个信号处理函数:

 

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

signal 函数用来注册为一个信号注册一个处理函数,也就是参数中的handler啦,这个handler可以设置为 a) SIG_IGN,来忽略这个信号, b)SIG_DFL,来使用系统默认的处理方式处理信号,c)一个自定义的函数地址,也就是用户自己的信号处理函数。signal函数的返回值是之前设置的信号处理函数,或者出现错误时返回SIG_ERR。

 

2 信号的发生

信号一般会在发生硬件异常(除以零)或者在终端上我们按了特殊的按键或者使用以下函数来产生信号

int kill(pid_t pid, int signo) : 向指定的进程发送一个特定的信号。根据pid的不同这个函数有不同的变现

    pid > 0: 发送信号到pid指定的进程

    pid == 0:发送信号到发送者所在进程组的所有进程,init进程与内核进程除外

    pid < 0:发送信号到发送者所在进程组的所有进程(进程组ID为pid的绝对值),依然init进程与内核进程除外

    pid == -1:发送信号到系统 上的所有进程,依然init与内核进程除外

关于kill,另一个用途就是检查一个进程是否存在。由于所有有效的信号值都是大于 0 的,那么 0 就光荣的来完成这个任务吧。

如果我们想检查pid为12345的进程是否存在,我们就这样写:

 

if ((ret = kill(12345, 0)) == -1) {
    if (errno == ESRCH) {
        printf("The prcess is not exist\n");
    }
}

PS:(你想发送信号到进程当然首先确保你有权限才行,root用户可以向所有进程发信号,普通进程只可以向real UID 或者 effective UID 相同的进程发送信号)。

int raise(int signo): 向本进程发送一个特定的信号

unsigned int alarm(unsigned int seconds):定时器,到时之后会产生SIGALRM信号。其返回值为上次设置的定时器剩余时间,参数为定时的时间,如果为 0 则设置的定时器被取消

int pause(void):挂起当前进程,直到有一个信号处理函数返回。

void abort(void):产生信号SIGABRT给当前进程,进程不可以忽略该信号,而且即使用户捕获了该信号并处理啦,当处理函数返回的时候,进程也会结束。

 

3 信号集合

有时候我们需要告诉内核不要将特定的信号发送给本进程,这就需要用一个数据来标识我们允许那些信号发生,不允许哪些信号发生,OK,其实是可以用一个整型数据的32位老表示各个信号的开与关啦,不过由于系统上允许的信号数目可能超过32个,所以才定义了这个概念,信号集合,以及操作信号集合的方法:

 

int sigempty(sigset_t *set)
int sigfillset(sigset_t *set)
int sigaddset(sigset_t *set, int signo)
int sigdelset(sigset_t *set, int signo)
int sigismember(const sigset_t *set, int signo)


在这里假定信号的总个数不超过31个,这样我们就可以用一个int来标识所有的信号,以上几个函数就可以这样实现

typedef sigset_t int;

#define sigemptyset(ptr) (*(ptr) = 0)
#define sigfillset(ptr) ((*(ptr) = ~(sigset_t)0), 0)


int sigaddset(sigset_t *set, int signo)
{
    //FIXME check if set and signo is valid

    int sig = 1 << (signo - 1);

    *set |= sig;

    return 0;
}

int sigdelset(sigset_t *set, int signo)
{
    //FIXME check if set and signo is valid

    int sig = 1 << (signo - 1);

    *set &= ~sig;

    return 0;
}

int sigismember(const sigset_t *set, int signo)
{
    //FIXME check if set and signo is vallid

    return ((*set & (1<<(signo - 1))) != 0);
}

 

 

 

4 信号掩码

一个进程的信号掩码是当前被阻塞而不能递送给进程的信号集合,使用下面的系统调用可以来查看或改变信号掩码

 

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

如果oset 不为NULL, 那么oset作为返回值,这里面存放着当前的信号掩码

 

如果set不为NULL,那么参数how指定了对现在的信号掩码如何修改:

--------------------------------------------------------------------------------------------------------------

how                                             描述

---------------------------------------------------------------------------------------------------------------

SIG_BLOCK                             新的信号掩码是当前的值与set指向的信号集合的并,也就是说set里面是我们想阻塞的额外的信号

SIG_UNBLOCK                       新的信号掩码是当前的值与set补集的交集,也就说set里面是我们想要unblock的信号

SIG_SETMASK                        新的信号掩码就是set里面的所有信号

---------------------------------------------------------------------------------------------------------------

如果set是NULL, 信号掩码值不变,参数how被忽略

 

5 信号挂起

我们再来讲一个概念,就是信号挂起(pending)。一个信号处理函数由于特定信号被调用,我们程该信号被delivered to 进程,在信号产生后被递送前的这段时间,被称为信号挂起(pending)。从前面的描述知道,我们可以block一个信号,如果被block的信号产生了,那么这个信号就处在pending状态,除非我们给它unblock。

也就是说,如果一个信号处在pending 状态,这个时候我们使用sigprocmask来将这个信号unblock啦,那么这个信号在sigprocmask返回之前就被delivered 到进程啦。

如果一个信号处在pending状态,然后又来了一个相同的信号,那么后续的信号不会被保存起来(通常是个队列),也就是说,当unblock这个信号的时候,处理函数只会被调用一次。

我们可以使用一下函数来获得当前进程挂起了那些信号:

 

int sigpending(sigset_t *set)

 

6 最新的sigaction

是时候来介绍一下新版本Linux中使用的sigaction啦,这个函数同signal()的作用类似,也是用来注册信号处理函数的,不过它更安全,功能更多,是用来取代signal()的。

 

int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact)

其中的结构体为

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (sa_sigaction*)(int, siginfo_t *, void *);
}

在这个结构体中,sa_handler就是要注册的信号处理函数,sa_mask为要添加的信号掩码,sa_flags指定了处理信号的许多选项,其中如果sa_flags被设置成SA_SIGINFO,那么信号处理函数将会使用sa_sigaction而不是sa_handler,其中的siginfo_t中包含了很多的关于信号的信息。

在使用这个函数的时候,只需要将struct sigaction结构体赋于正确的值就可以啦。



7 sigsuspend(sigset_t *set)

考虑这样一种情况,我们想要使用sigprocmask来保护临界区内的代码不被信号SIGINT打断,临界区内的代码执行完毕后我们恢复信号掩码并等待之前block的信号发生,然后再继续运行。我们可以这样做

 

sigset_t newset, oldset;

sigemptyset(&newset);
sigaddset(&newset, SIGINT);

sigprocmask(SIG_BLOCK, &newset, &oldset);

/*critical region of code */

sigprocmask(SIG_SETMASK, &oldset, NULL);

pause();

/* continue... */


上面的代码有些问题,假如SIGINT信号发生在sigprocmask与pause之间的话就有可能导致pause永远阻塞在那里。怎么办呢,不用怕,Linux系统考虑到了这种情况并将设置信号掩码与pause函数作为一个原子操作实现了sigsuspend函数:

 

 

int sigsuspend(sigset_t *set)

这个函数将当前进程的信号掩码设置为set,并等到信号发生再返回,不过它的返回值永远为-1!当此函数返回后,进程的信号掩码又恢复到调用之前的值。

 

8 信号的字符串信息

信号也有与errno类似的特性,就是转化成字符串,更具有可读性,主要有以下几个函数

 

<span style="font-family:Courier New;font-size:12px;">void psignal(int signo, const char *msg) : the output format is "msg: XXX"

char *strsignal(int signo) //same as strerror

/* 此外还有信号值与字符串的对应关系函数 */
int sig2str(int signo, char *str)
int str2sig(const char *str, int *signo)</span>

7. 多线程中的信号处理

Signal handlers are per process, signal masks are per thread!

通常情况下,Linux中,当一个异步信号(例如kill产生的信号)发生时,系统选择一个thread去执行处理函数,但是无法预测哪一个thread会被选中。但是如果是同步信号(例如一些异常:SIGSEGV, SIGFPE, SIGBUS, SIGILL, …),那么产生这些异常的线程负责处理这些信号。

但是为了让特定的线程去处理信号,我们可以

1. 在其他所有线程中block signal,只留下选中的线程不block signal。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

void mask_sig(void)
{
	sigset_t mask;
	sigemptyset(&mask); 
        sigaddset(&mask, SIGRTMIN+3); 
                
        pthread_sigmask(SIG_BLOCK, &mask, NULL);
        
}

void *threadfn1(void *p)
{
	mask_sig();
	while(1){
		printf("thread1\n");
		sleep(2);
	}
	return 0;
}

void *threadfn2(void *p)
{
	mask_sig();
	while(1){
		printf("thread2\n");
		sleep(2);
	}
	return 0;
}

void *threadfn3(void *p)
{
	while(1){
		printf("thread3\n");
		sleep(2);
	}
	return 0;
}


void handler(int signo, siginfo_t *info, void *extra) 
{
	int i;
	for(i=0;i<10;i++)
	{
		puts("signal");
		sleep(2);
	}
}

void set_sig_handler(void)
{
        struct sigaction action;


        action.sa_flags = SA_SIGINFO; 
        action.sa_sigaction = handler;

        if (sigaction(SIGRTMIN + 3, &action, NULL) == -1) { 
            perror("sigusr: sigaction");
            _exit(1);
        }

}

int main()
{
	pthread_t t1,t2,t3;
	set_sig_handler();
	pthread_create(&t1,NULL,threadfn1,NULL);
	pthread_create(&t2,NULL,threadfn2,NULL);
	pthread_create(&t3,NULL,threadfn3,NULL);
	pthread_exit(NULL);
	return 0;
}

2. 创建一个专门用于处理信号的线程,使用`sigwait`阻塞的等待信号。

为什么所有的线程都可能去处理信号呢? 这跟LWP的实现有关。每一个LWP都有一个task_struct,内部关于信号处理的结构体指针都指向同一个地址(但是sigmask是每个LWP独有的),所以process会在这些LWP(task_struct)中选择一个去处理信号。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info		thread_info;
#endif
	/* -1 unrunnable, 0 runnable, >0 stopped: */
	volatile long			state;
...
...
...
	/* Signal handlers: */
	struct signal_struct		*signal;
	struct sighand_struct		*sighand;
	sigset_t			blocked;
	sigset_t			real_blocked;
	/* Restored if set_restore_sigmask() was used: */
	sigset_t			saved_sigmask;
	struct sigpending		pending;
	unsigned long			sas_ss_sp;
	size_t				sas_ss_size;
	unsigned int			sas_ss_flags;
...
...
}

可以在代码中用pthread_kill向特定线程发送信号。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
 
 
void *threadfn1(void *p)
{
	while(1){
		printf("thread1\n");
		sleep(2);
	}
	return 0;
}
 
void *threadfn2(void *p)
{
	while(1){
		printf("thread2\n");
		sleep(2);
	}
	return 0;
}
 
void *threadfn3(void *p)
{
	while(1){
		printf("thread3\n");
		sleep(2);
	}
	return 0;
}
 
 
void handler(int signo, siginfo_t *info, void *extra) 
{
	int i;
	for(i=0;i<5;i++)
	{
		puts("signal");
		sleep(2);
	}
}
 
void set_sig_handler(void)
{
        struct sigaction action;
 
 
        action.sa_flags = SA_SIGINFO; 
        action.sa_sigaction = handler;
 
        if (sigaction(SIGRTMIN + 3, &action, NULL) == -1) { 
            perror("sigusr: sigaction");
            _exit(1);
        }
 
}
 
int main()
{
	pthread_t t1,t2,t3;
	set_sig_handler();
	pthread_create(&t1,NULL,threadfn1,NULL);
	pthread_create(&t2,NULL,threadfn2,NULL);
	pthread_create(&t3,NULL,threadfn3,NULL);
	sleep(3);
	pthread_kill(t1,SIGRTMIN+3);
	sleep(15);
	pthread_kill(t2,SIGRTMIN+3);
	pthread_kill(t3,SIGRTMIN+3);
	pthread_exit(NULL);
	return 0;
}


总结

 

这篇文章主要介绍了Linux上的信号以及处理,主要涉及到了信号的发生,信号的递送(信号处理),信号掩码与信号集的用途以及一些基于信号实现的系统调用。在使用信号时必须非常细致,何时屏蔽那种信号,何时恢复信号处理等等都需要非常小心,复杂但是有用。作为实例最后再来一个由信号实现的sleep函数吧:

 

static void sig_alrm(int signo)
{
    printf("signal alarm handler\n");

    return;
}

unsigned int sleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    /* set signal handler */
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    /* block signal alrm and save current signal mask */
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);

    alarm(nsecs);

    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);
    sigsuspend(&suspmask);

    unslept = alarm(0);
    sigaction(SIGALRM, &oldact, NULL);

    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return unslept;
}

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值