天天看点

信号基础

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

信号的详细信息可通过man 7 signal

1. 信号分类

信号可从两个不同分类角度对信号进行分类,1)可靠性方面:可靠信号与不可靠信号;2)与时间的关系上:实时信号与非实时信号。

常见的信号1-31都是不可靠信号,都是非实时信号,也称标准信号。

kill -l 显示所有信号

信号9(SIGKILL),19(SIGSTOP)不能被阻塞(进程中可屏蔽两信号sigaddset()不报错,但不起作用),不能被忽略,也不能注册信号处理函数(直接报错)。

信号共计SIGRTMAX个,其中32,33无信号,

if(sigi == SIGKILL || sigi == SIGSTOP || sigi == 32 || sigi == 33)

2. 信号发送

kill -signal PID // kill命令默认发送信号15(SIGTERM)。

killall -signal name

ctrl+c => SIGINT

ctrl+\ => SIGQUIT

ctrl+z => SIGTSTP

(SIGTTIN/SIGTTOU:unix环境下,当一个进程以后台形式启动,但尝试去读写控制台终端时,将会触发 SIGTTIN(读) 和 SIGTTOU(写)信号量,

接着,进程将会暂停(linux默认),read/write将会返回错误。

前台转后台:ctrl+z,后台转前台:fg)

发送信号的函数有raise(), kill(), killpg(), pthread_kill(), tgkill(), sigueue()等:

#include <signal.h>
int kill(pid_t pid, int signo);

raise自发信号
int raise(int signo);

abort 自发SIGABRT信号,终止
void abort(void);

alarm闹钟
unsigned int alarm(unsigned int sec);
原来没有调度alarm返回0或以前设定的闹钟时间还剩多少秒。如果sec为0,表示取消以前设定的闹钟,返回剩余的秒数。      

3. 信号阻塞

-------------------pending(未决)

信号生成 ---- (信号阻塞) ---- 信号递达 ---- 信号处理(默认、忽略、捕获)

A signal may be blocked, which means that it will not be delivered until it is later unblocked. Between the time when it is generated and when it is delivered a signal is said to be pending.

信号阻塞指信号不能递达进程,被屏蔽(signal mask),只有信号非阻塞后才能递达进程进一步处理。

每一个线程都有独立的信号屏蔽(signal mask),在信号屏蔽中的信号被当前线程阻塞,不能递达线程处理。Each thread in a process has an independent signal mask, which indicates the set of signals that the thread is currently blocking.

#include <signal>
int sigemptyset(sigset_t *set);置0
int sigfillset(sigset_t *set);置1
int sigaddset(sigset_t *set, int signum);某位置1
int sigdelset(sigset_t *set, int signum);某位置0
成功0,失败-1.
int sigismember(const sigset_t *set, int signum);
包含该位返回1,不包含返回0,错误返回-1.      
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpromask(int how, const sigset_t *set, sigset_t *oset);
成功返回0,失败返回-1;
how:SIG_BLOCK +, SIG_UNBLOCK -, SIG_SETMASK.      
int sigpending(sigset_t *set);
读取未决信号集。
成功0,失败-1。      
int pause(void);
pause函数使调用进程挂起直到有信号递达。信号的处理动作是终止进程,则进程终止,pause不能返回;      
忽略,则进程处于挂起状态,pause不返回;捕抓,则调用信号处理函数后pause返回-1,errno置为EINTR。
pause只有出错的返回值。

int sigsuspend(const sigset_t *sigmask);
和pause一样,无成功返回值。
只有执行了一个信号处理函数后,sigsuspend才返回,返回-1,errno置为EINTR。
注:sigsuspend临时用sigmask取代信号屏蔽字,挂起进程。调用sigsuspend时应将想获取的信号从原来的信号屏蔽子中移除填充到sigmask中。      

4.信号处理

Each signal has a current disposition, which determines how the process behaves when it is delivered the signal.

当信号递达时,信号的处理方式有三种:默认、忽略和捕获。

The signal disposition is a per-process attribute: in a multithreaded application, the disposition of a particular signal is the same for all threads.

A child created via fork(2) inherits a copy of its parent's signal dispositions. During an execve(2), the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged.

信号处理是单进程属性,多线程中,所有信号处理都一样。execve时,信号处理恢复为默认。

  • linux对每种信号都规定了默认动作,具体可参考man 7 signal
  • SIGCHLD 忽略(注意:需要注意的是,虽然进程对于 `SIGCHLD`的默认动作是忽略,但是还是显示写出来,才能有效(不显示写出来无效);​

    ​signal(SIGCHLD, SIG_IGN)​

    ​,这样子进程直接会退出,不会变成僵尸。)
  • 实时信号的缺省反应是结束进程。
  • 如果不想程序采用默认动作处理进程,需要捕捉函数(为想要特殊处理的函数指定信号处理函数)。如发生SIGALARM或SIGPIPE,进行超时处理即可,不必终止进程。此外若想发生信号时做特殊处理也应指定信号处理函数,如发生段错误时,提示用户等。

5. 信号捕获

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,称为捕抓信号。

除了SIGSTOP(19)和SIGKILL(9)外,进程能够忽略或捕获其他的全部信号。

sighandler和main函数默认使用相同的堆栈空间(多线程时每个线程与该线程的信号处理函数共享栈空间,虽然各个线程处理函数相同,堆空间所有线程和信号处理函数共享,但每个线程执行信号处理函数时是在线程的栈空间),所有函数或变量均可使用。但为了程序稳定性,在信号处理函数中应使用可重入函数(如sleep)。

sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。内核态切换到用户态执行main函数前要先扫描信号,处理信号后再执行main函数部分。

在信号处理函数中,或关键处理时,不能被其他信号打断而处理其他信号,所以需要信号屏蔽。

注:多线程中,信号处理函数和main或线程函数等同,都可被阻塞。

注:信号是一种异步处理机制,所以需要防止竞争条件。

信号处理函数编程注意

在用sigaction函数登记的信号处理函数中可以做的处理是被严格限定的,仅仅允许做下面的三种处理:

1. 局部变量的相关处理

2. “volatile sig_atomic_t”类型的全局变量的相关操作

3. 调用异步信号安全的相关函数

以外的其他处理不要做!若要使用其他请慎重考虑!

参考:​​准则2: 要知道信号处理函数中可以做那些处理​​

int kill(pid_t pid, int sig);

int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
     int   sival_int;
     void *sival_ptr;
};      
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

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

struct sigaction {
    void (*sa_handler)(int); /* or SIG_IGN, SIG_DFL */

    sigset_t sa_mask; /*addition signals to block*/

    int sa_flags; /*0则信号处理函数为sa_handler();SA_SIGINFO,信号处理函数为sa_sigaction*/

    void (*sa_sigaction)(int, siginfo_t *, void *);
};      

sa_mask specifies a mask of signals which should be blocked (i.e., added to the signal mask of the thread in which the signal handler is invoked) during execution of the signal handler.

In addition, the signal which triggered the handler will be blocked, unless the SA_NODEFER flag is used.

示例:主程序所有信号都注册新捕捉函数,然后捕抓函数中恢复默认动作。

    void do_signal(int signo)
    {
        struct sigaction act;
        int ret;

        printf("catch signo %d.\n", signo);

        act.sa_handler = SIG_DFL;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        ret = sigaction(signo, &act, NULL);

        if(ret < 0){
            perror("sigaction error");
            exit(EXIT_FAILURE);
        }
        raise(signo);
    }

    int sigi;
    int ret;
    struct sigaction new_act;

    new_act.sa_handler = do_signal;
    sigemptyset(&new_act.sa_mask);
    new_act.sa_flags = 0;

    for(sigi=1; sigi<=SIG_NUM; sigi++){
        if((sigi == SIGKILL) || (sigi == SIGSTOP))
            continue; 

        ret = sigaction(sigi, &new_act, NULL);
        if(ret < 0){
            perror("main sigaction error");
            exit(EXIT_FAILURE);
        }else{
            // printf("%d signal is registered.\n", sigi);
        }
    }      

通过编程观察到常用信号(1-31)有如下执行行为:

1)没有屏蔽任何信号时,任何信号都可以到达,并首先完成最后一个信号的执行,然后再完成原来信号函数剩余部分。(信号嵌套)

2)在信号执行过程中,再次(或多次)触发相同信号,则该信号不会立即执行(阻塞相同信号),在原来信号执行完后,仅再执行一次信号处理函数。

3)在一个信号执行周期内(程序未返回main函数执行),若执行过程中触发了多个信号,则首先执行完最后一个信号,然后倒序依次执行信号一次。

通过上述观察,可知:重复的信号仅执行一次。

究其原因,是因为非实时信号的实现方式。

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)如果进程的未决信号中没有相同信号,则在进程中注册自己)。

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)。

异常信号捕获

一般情况下,程序应对下面信号捕获,一方面探测程序bug,另一方面阻止一些异常信号促使程序不能正常运行。

SIGQUIT:停止

SIGILL:illegal instruction

SIGABRT:Abort

SIGFPE:Float point exception

SIGPIPE:Broken pipe

SIGBUS:总线错误(访问mem)

SIGSEGV:段错误

6. 系统调用和库函数被信号处理中断

Interruption of system calls and library functions by signal handlers

当系统调用或库函数阻塞时,一个信号触发了,此时系统有两种处理方式:1)信号处理后调用自动恢复;2)调用失败并置errno为EINTR。

具体采用哪种处理方式取决于call和信号处理是否设置SA_RESTART标记。不同系统,甚至不同版本都有可能变化。

If a signal handler is invoked while a system call or library function call is blocked, then either:

* the call is automatically restarted after the signal handler returns; or

* the call fails with the error EINTR.

Which of these two behaviors occurs depends on the interface and

whether or not the signal handler was established using the SA_RESTART flag (see sigaction(2)).

The details vary across UNIX systems; below, the details for Linux.

一般情况下,read和write系列函数作用于slow设备时,或socket接口函数,会自动恢复。

If a blocked call to one of the following interfaces is interrupted by a signal handler,

then the call is automatically restarted after the signal handler returns

if the SA_RESTART flag was used; otherwise the call fails with the error EINTR.

epoll、poll和select相关函数绝不重启,总是返回errno为EINTR。

The following interfaces are never restarted after being interrupted by a signal handler,

regardless of the use of SA_RESTART; they always fail with the error EINTR

when interrupted by a signal handler

7. 线程信号

1)根据APUE 12.8,进程的处理函数与处理方式是进程中所有线程共享的。

2)根据APUE 12.8,如果进程接收到信号,该信号只会被递送到某一个单独线程。一般情况下由那个线程引起信号则递送到那个线程。如果没有线程引发信号,信号被发送到任意线程。

线程信号处理函数编程时要注意死锁问题(同一线程如果重复申请同一个互斥锁那么必然会死锁)。

可参考:​​关于线程与信号处理函数获得同一把互斥锁的问题 (原)​​

信号处理函数和线程锁在同一线程申请锁时死锁示例:

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

pthread_mutex_t mmutex = PTHREAD_MUTEX_INITIALIZER;

void *thread_run(void *p)
{
    printf("thread...\n");
    pthread_mutex_lock(&mmutex);
    int i;
    for(i = 0; i < 10; i++){
        sleep(1);
        printf("thread run [%d]!\n", i);
    }
    pthread_mutex_unlock(&mmutex);

    return NULL;
}

void signal_handler(int signo)
{
    printf("signal...\n");

    pthread_mutex_lock(&mmutex);
    int i;
    for(i = 0; i < 5; i++){
        sleep(1);
        printf("signal run [%d]!\n", i);
    }
    pthread_mutex_unlock(&mmutex);
}


int main()
{
    signal(SIGUSR1, signal_handler);
    pthread_t p;
//    pthread_create(&p, NULL, thread_run, NULL);
    sleep(2);
    //raise(SIGUSR1);
    pthread_create(&p, NULL, thread_run, NULL);
    pthread_kill(p, SIGUSR1);

    while(1){
        printf("main...\n");
        sleep(1);
    }

    pthread_join(p, NULL);
    pthread_mutex_destroy(&mmutex);

    return 0;
}      

最好不要在信号处理函数中访问共享资源,互斥锁可能死锁。可通过其他方式避免:

1)任务在屏蔽临界区时先阻塞信号,出临界区时开放信号。

2)自己另外起一个线程,将信号中需要处理的临界区由线程来操作,信号只是给该线程以通知。这样就可以使用线程间的互斥机制。(推荐)

参考:​​Linux 多线程应用中如何编写安全的信号处理函数​​ 使用sigwait()在指定的线程中以同步的方式处理异步信号

int sigwait(const sigset_t *set, int *sig);

继续阅读