linux信号 linux signal

http://blog.csdn.net/ruglcc/article/details/7876146

Linux Signal (1): 基本概念

 1. 信号是软件中断:

信号提供了一种处理异步事件的方法. 每个信号都有一个名字, 他们以SIG开头, 如SIGALRM是闹钟信号, 当由alarm函数设置的计时器超时后产生此信号, 然后由SIGALRM的信号处理函数接管处理, 处理之后返回调用alarm函数的应用程序中.


2. 信号编号:

在头文件<signal.h>中, 信号都用宏定义为正整数的信号编号, 不存在编号为0的信号, kill函数对编号为0的信号有特殊的作用. POSIX.1将编号为0的信号定义为空信号, 如果kill中的signo参数为0, 则kill仍执行正常的错误检查, 但不发送信号. 这常被用来通过kill的返回值确定一个特定进程是否存在.


3. 不可忽略/捕捉的信号:

大多数信号都可以通过忽略进行处理, 但SIGKILL和SIGSTOP这两个信号不能被忽略. 因为它们是向超级用户提供使进程终止或停止的可靠方法. 另外, 如果忽略某些硬件异常产生的信号(例如非法内存引用或除0), 则进程的行为是未定义的.


4. 介绍几个常见信号:

SIGINT: 当用户按某些终端键时, 引发终端产生的信号. 如Ctrl+C键, 这将产生中断信号(SIGINT). 它将停止一个已失去控制的程序.

SIGSEGV: 由硬件异常(除数为0, 无效的内存引用等等)产生的信号. 这些条件通常由硬件检测到, 并将其通知内核. 然后内核为该条件发生时正在运行的进程产生该信号.

SIGURG: 在网络连接上传来带外数据时产生.

SIGPIPE: 在管道的读进程已终止后, 一个进程写此管道时产生. 当类型为SOCK_STREAM的socket已不再连接时, 进程写到该socket也产生此信号.

SIGALRM: 进程所设置的闹钟时钟超时的时候产生.

SIGABRT: 进程调用abort函数时产生此信号, 进程异常终止.

SIGCHLD: 在一个进程终止或停止时, 它将把该信号发送给其父进程. 按系统默认, 将忽略此信号. 如果父进程希望被告知其子进程的这种状态改变, 则应该捕捉此信号. 通常是用wait系列函数捕捉, 如果不wait的话, 子进程将成为一个僵尸进程.

SIGIO: 此信号指示一个异步I/O事件.

SIGSYS: 该信号指示一个无效的系统调用.

SIGTSTP: 交互式停止信号. Ctrl+Z, 按下时, 终端将产生此信号, 进程被挂起.

signal函数

 1. 原型:

#include <signal.h>
void (*signal(int signo, void (*func)(int))(int);

成功则返回该信号以前的处理配置, 出错则返回SIG_ERR.
参数说明:

signo: 信号名, 如SIGINT. 
func: 对应signo的信号处理函数的函数名, 这个函数没有返回值, 有一个整型参数, 这是捕捉的情况, 当然也可以是以下两种宏: 
SIG_IGN: 忽略. 
SIG_DFL: 默认动作.

2. 改写原型:

typedef void (*sigfunc)(int);

sigfunc *signal(int, sigfunc);
 

3. 三个宏定义:

#define SIG_ERR (void (*)()) -1 // 错误编号

#define SIG_DFL (void (*)()) 0  // 默认动作编号

#define SIG_IGN (void (*)()) 1  // 忽略编号


4. kill命令:

在shell里面执行kill命令可以向进程发送信号:

kill -USR1 7216 ;向pid为7216的进程发送SIGUSR1信号.

kill 7216       ;向pid为7216的进程发送SIGTERM信号.


5. 注意事项:

exec函数执行后, 把该进程所有信号设为默认动作.

exec函数执行后, 把原先要捕捉的信号设为默认, 其他不变.

fork之后, 子进程继承父进程的信号处理方式.


Linux Signal (3): kill和raise

1. 函数说明:

kill和raise是用来发送信号的:

kill把信号发送给进程或进程组;

raise把信号发送给(进程)自身.

他们的原型如下:

#include <signal.h>

int kill(pid_t pid, int signo);
int raise(int signo);

成功则返回0, 出错则返回-1
 从原型上可以看出, raise函数是可以通过kill实现的.

raise(signo);

等价于:

kill(getpid(), signo);


2. pid参数:

kill函数中的pid参数, 它有以下4种情况:

pid > 0: 将该信号发送给进程ID为pid的进程. 
pid == 0: 将该信号发送给与发送进程属于同一进程组的所有进程(不包括内核进程和init进程). 此时, 发送进程必须具有向这些进程发送信号的权限. 
pid < 0: 将该信号发给其进程组ID等于pid绝对值的所有进程(不包括内核进程和init进程). 此时, 发送进程必须具有向这些进程发送信号的权限. 
pid == -1: 将该信号发送给发送进程有权限向它们发送信号的系统上的所有进程.(不包括内核进程和init进程).

3. signo参数:

POSIX.1将编号为0的信号定义为空信号. 如果signo参数是0, 则kill仍执行正常的错误检查, 但不发送信号. 这被用来确定一个进程是否存在.


  

Linux Signal (4): alarm和pause

1. alarm函数:

alarm函数是设置一个计时器, 在计时器超时的时候, 产生SIGALRM信号. 如果不忽略或捕捉此信号, 它的默认操作是终止调用该alarm函数的进程.

原型如下:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

返回0或余留秒数
说一下alarm的返回值问题, 每个进程只能有一个alarm维护的"闹钟".

如果该"闹钟"顺利超时, 则返回0;

如果该"闹钟"在计时过程中, 调用了另一个alarm函数, 则该"闹钟"的余留秒数作为该次alarm的返回值, 并且新的"闹钟"开始计时.(实际上是新的闹钟替代了以前的闹钟)

代码举例:

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


/* My alarm func for print */
static unsigned int my_alarm(unsigned int nsec)
{
    printf("Wait for %u secs to alarm ", nsec);
    return alarm(nsec);
}

/* My sleep func for print */
static unsigned int my_sleep(unsigned int nsec)
{
    printf("Sleep for %u secs ", nsec);
    return sleep(nsec);
}

/* SIGALRM handler */
static void sig_alarm(int signo)
{
    printf("SIGALRM ");
}

int main()
{
    /* Check alarm return value */
    unsigned int ret1, ret2;

    /* Signal handle */
    if (signal(SIGALRM, sig_alarm) < 0)
        perror("signal");

    printf("Alarm start: ");

    /* First alarm */
    ret1 = my_alarm(5);
    my_sleep(3);
    
    printf("New alarm: ");
    
    /* Second alarm */
    ret2 = my_alarm(2);
    my_sleep(4);

    printf("Alarm end ");

    /* Show the two return values */
    printf("First  return: %u ", ret1);
    printf("Second return: %u ", ret2);

    return 0;
}这段代码中我自己封装了my_alarm和my_sleep, 在其中添加了printf的代码用于跟踪. 
程序的运行结果如下:

Alarm start:
Wait for 5 secs to alarm
Sleep for 3 secs
New alarm:
Wait for 2 secs to alarm
Sleep for 4 secs
SIGALRM
Alarm end
First  return: 0
Second return: 2由上面这个结果, 我想我对这个程序就不用多解释了.

由此可见alarm的返回值问题, 一目了然.

 


2. pause函数:

pause函数使调用进程挂起, 直到捕捉到一个信号. 它的原型如下:


#include <unistd.h>

int pause();

返回-1, 并将errno设置为EINTR.这个函数很简单, 由字面意思就可以理解出来"暂停". pause只有在执行了一个信号处理程序并从其返回时, pause才返回.


  

Linux Signal (5): 信号集

 信号集给我们提供了一个能表示多个信号的是数据类型(sigset_t), 它将在sigprocmask, sigpending, sigsuspend之类的函数中用到, 这些函数我会在以后的文章中介绍.

1. 信号集相关函数:

#include <signal.h>

int sigemptyset(sigset_t *set);

成功则返回0, 出错则返回-1.
 这个函数用作初始化set指向的信号集, 清空其中的所有信号.

 

#include <signal.h>

int sigfillset(sigset_t *set);

成功则返回0, 出错则返回-1.
这个函数用作初始化set指向的信号集, 填充其中的所有信号.

 

#include <signal.h>

int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);

成功则返回0, 出错则返回-1.
这两个函数用作向set指向的信号集中, 增加/删除一个signo代表的信号.


#include <signal.h>

int sigismember(const sigset_t *set, int signo);

真则返回1, 假则返回0, 出错则返回-1.
这个函数用作判断signo信号是否在set指向的信号集中.


2. 宏:

在signal.h中有两个宏:

#define sigemptyset(ptr)  (*(ptr) = 0)
#define sigfillset(ptr)  (*ptr = ~(sigset_t)0, 0)
这两个宏分别定义了sigemptyset和sigfillset两个函数的行为.

sigemptyset: 把ptr指向的地址的内容设为0. 
sigfillset: 这是一个逗号表达式, 把0转换为sigset_t类型, 然后按位取反, 并返回0. 
通过以上两个宏, 我们可以确切地说, sigset_t是用多位(比信号总数更多的位数)来表示信号集概念的.

因此,

sigaddset: 将某一位设置为1.

sigdelset: 将某一位设置为0.

sigismember: 测试某一个指定位.

下面我们来实现这些函数.


3. 实例:

#include <signal.h>
#include <errno.h>

/* NSIG defined in <signal.h> */
#define SIGBAD(signo) ((signo) <= 0 || (signo) >= NSIG)

int sigaddset(sigset_t *set, int signo)
{
    if (SIGBAD(signo))
    {
        errno = EINVAL;
        return (-1);
    }
    *set |= 1 << (signo -1);
    
    return 0;
}

int sigdelset(sigset_t *set, int signo)
{
    if (SIGBAD(signo))
    {
        errno = EINVAL;
        return (-1);
    }
    *set &= ~(1 << (signo -1));

    return 0;
}

int sigismember(const sigset_t *set, int signo)
{
    if (SIGBAD(signo))
    {
        errno = EINVAL;
        return (-1);
    }
    
    return ((*set & (1 << (signo -1))) != 0);
}
说明一下里面的几个细节:

signo - 1: 因为不存在编号为0的信号, 也就是第0位与编号为1的信号是对应的, 所以减1.

SIGBAD: 小于等于0, 或者大于最大信号编号NSIG.

1 << : 1的左移位操作, 右边补0, 所以执行或操作时直接实现添加; 执行与操作时需要先取反, 以保证本位为0, 其他位不变, 这样来实现删除.

sigismember: 最后一个return语句看上去有些复杂. 想起来很难想到, 但看起来应该不难. 执行移位后的与操作, 当存在时使本位保持不变, 其他位清零; 当不存在时, 全部清零. 然后判断是否为0, 这样来实现判断存在性.

Linux Signal (6): 信号屏蔽字

 1. 概念:

信号屏蔽字就是进程中被阻塞的信号集, 这些信号不能发送给该进程, 它们在该进程中被"屏蔽"了. 后面我们会提到, 实际上它们是被阻塞了.


2. 信号屏蔽函数:

#include <signal.h>

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

成功则返回0, 出错则返回-1.
sigprocmask函数有3个参数:

how: 修改信号屏蔽字的方式. 
set: 把这个信号集设为新的当前信号屏蔽字. 如果为NULL则不改变. 
oset: 保存进程旧的信号屏蔽字. 如果为NULL则不保存. 
参数中的how可以取3个值:

sigprocmask中的how参数 how 说明 
SIG_BLOCK 修改后, 该进程新的信号屏蔽字是其当前屏蔽字和set指向的信号集的并集. 
SIG_UNBLOCK 修改后, 该进程新的信号屏蔽字是其当前屏蔽字和set指向的信号集的补集的交集. 
SIG_SETMASK 修改后, 该进程新的信号屏蔽字将被set指向的信号集的值代替


 另外要说的是, sigprocmask只为单线程定义的, 在多线程中要使用pthread_sigmask.


3. 未处理的信号:

在调用信号屏蔽的相关函数后, 被屏蔽的信号对于调用进程是阻塞的, 不能发送给调用进程, 因此是未决的. 取得这些阻塞的信号集, 可以通过调用sigpending函数.

#include <signal.h>

int sigpending(sigset_t *set);

成功则返回0, 出错则返回-1.

4. 实例:

下面通过一个简单的实例来说明这篇文章中所讲到的两个函数.

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

/* SIGQUIT handler */
static void sig_quit(int signo)
{
    printf("SIGQUIT is caught ");
}

int main()
{
    sigset_t new, old, pend;

    /* Handle SIGQUIT */
    if (signal(SIGQUIT, sig_quit) == SIG_ERR)
    {
        perror("signal");
        exit(1);
    }

    /* Add SIGQUIT to sigset */
    if (sigemptyset(&new) < 0)
        perror("sigemptyset");
    if (sigaddset(&new, SIGQUIT) < 0)
        perror("sigaddset");

    /* Mask SIGQUIT */
    if (sigprocmask(SIG_SETMASK, &new, &old) < 0)
    {
        perror("sigprocmask");
        exit(1);
    }

    printf("SIGQUIT is blocked ");
    printf("Now try Ctrl /  ");

    sleep(5); /* SIGQUIT will pending */

    /* Get pending */
    if (sigpending(&pend) < 0)
        perror("sigpending");

    if (sigismember(&pend, SIGQUIT))
        printf("SIGQUIT pending ");

    /* Restore signal mask */
    if (sigprocmask(SIG_SETMASK, &old, NULL) < 0)
    {
        perror("sigprocmask");
        exit(1);
    }

    printf(" SIGQUIT unblocked ");
    printf("Now try Ctrl /  ");

    sleep(5);

    return 0;
}

这个程序在开始的时候用sigprocmask屏蔽了SIGQUIT(ctrl+/触发), 在5秒内触发的该信号将可以从sigpending中获得; 然后程序把SIGQUIT解除屏蔽(恢复以前的屏蔽字), 此时再触发该信号将调用sig_quit信号处理函数.


Linux Signal (7): sigaction

sigaction函数是用作检查/修改与指定信号相关联的处理动作. 在UNIX早期版本中使用signal, 后来改用了sigaction, 可见它的功能比signal要强大. 另外, signal函数也是可以用sigaction实现的.

1. sigaction原型:

#include <signal.h>

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

成功则返回0, 出错则返回-1.
首先说一下struct sigaction这个结构:

struct sigaction
{
    void (*sa_handler)(int); /* addr of signal handler or 
                                SIG_IGN, SIG_DFL */
    sigset_t sa_mask;        /* additional signals to block */
    int sa_flags;            /* signal options */

    /* alternate handler */
    void (*sa_sigaction)(int, siginfo_t *, void *);
};
sa_hanlder: 一个带有int参数的函数指针, 或者SIG_IGN(忽略), 或者SIG_DFL(默认). 
sa_mask: 信号屏蔽字(集). 当该信号处理函数返回时, 屏蔽字恢复. 
sa_sigaction: 替代的信号处理程序, 当使用了SA_SIGINFO标志时, 使用该信号处理程序. 
对于sa_flags和siginfo结构, 具体参考APUE 262页.


2. 实例:

用sigaction实现signal:

typedef void (*sig_func)(int);

sig_func *signal(int signo, sig_func *func);
{
    struct sigaction act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (sigaction(signo, &act, &oact) < 0)
        return SIG_ERR;

    return oact.sa_hanlder;
}


Linux Signal (8): sigsetjmp和siglongjmp

 进程部分介绍过了setjmp和longjmp函数, 这两个函数在跳转时会带信号屏蔽字跳转, 在信号处理程序(hanlder)中使用longjmp会导致后来产生的这种信号被屏蔽.

POSIX.1 也没有具体说明setjmp和longjmp对信号屏蔽字的作用, 而是定义了两个新函数: sigsetjmp和siglongjmp.

1. 原型:

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savemask);
直接调用则返回0, 从siglongjmp调用返回则返回非0值.

void siglongjmp(sigjmp_buf env, int val);
可见发现sigsetjmp比setjmp多了一个参数savemask, 如果非0, 则sigsetjmp在env中保存进程的当前信号屏蔽字.


2. 实例:

还是老习惯, 用代码来验证

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

/* Jump buffer */
static sigjmp_buf jmpbuf;

/* Signal handler */
static void myfunc(int signo)
{
    printf("SIGQUIT ");
    sleep(1);
    siglongjmp(jmpbuf, 1);
}

int main()
{
    char *p = NULL;
    struct sigaction act;
    act.sa_handler = myfunc;
    act.sa_flags = SA_INTERRUPT;
    sigemptyset(&act.sa_mask);

    if (sigaction(SIGQUIT, &act, NULL) < 0)
        perror("sigaction");

    if (sigsetjmp(jmpbuf, 1) == 1)
    {
        printf("I'm jumped ");
    }
    else
    {
        /* SIGSEGV */
        raise(SIGQUIT);
    }

    /* JUMP */
    printf("I'm here ");

    return 0;
}

这段代码首先用sigaction设定SIGQUIT信号的处理函数, 然后sigsetjmp设定sigjmp_buf, 当它从siglongjmp返回时显示I'm jumped, 第一次设定完成后产生SIGQUIT信号.

运行结果:

SIGQUIT
I'm jumped
I'm here


inux Signal (9): sigsuspend函数

手册:
       #include <signal.h>

       int sigsuspend(const sigset_t *sigmask);

The   sigsuspend()   function   shall replace the current signal mask of the calling thread with the set of signals pointed to by sigmask and then suspend the thread until delivery of a   signal   whose   action   is either to execute a signal-catching function or to terminate the process. This shall not cause any other signals that may have been pending on the process to become pending on the thread.

If the action is to terminate the process then sigsuspend() shall never return. If the action is to execute   a   signal-catching   function,   then   sigsuspend()   shall return after the signal-catching function returns, with the signal mask restored to the set that existed prior to the sigsuspend() call.

It is not possible to block signals that cannot be ignored. This is enforced by the system without causing an error to be indicated.

也就是说,sigsuspend后,进程就挂在那里,等待着开放的信号的唤醒。系统在接受到信号后,马上就把现在的信号集还原为原来的,然后调用处理函数。


Unix提供了等待信号的系统调用,sigsuspend就是其中一个,下面我摘录的解释不错

CU 网友讨论的问题的核心就是到底sigsuspend先返回还是signal handler先返回。这个问题Stevens在《Unix环境高级编程》一书中是如是回答的“If a signal is caught and if the signal handler returns, then sigsuspend returns and the signal mask of the process is set to its value before the call to sigsuspend.”,由于sigsuspend是原子操作,所以这句给人的感觉就是先调用signal handler先返回,然后sigsuspend再返回。但其第一个例子这么讲又说不通,看下面的代码:
CU上讨论该问题起于中的该例子:

int main(void) { 
   sigset_t   newmask, oldmask, zeromask;
   if (signal(SIGINT, sig_int) == SIG_ERR) 
       err_sys("signal(SIGINT) error");
   sigemptyset(&zeromask);
   sigemptyset(&newmask); 
   sigaddset(&newmask, SIGINT); 
   /* block SIGINT and save current signal mask */ 
   if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) 
       err_sys("SIG_BLOCK error");
   /* critical region of code */ 
   pr_mask("in critical region: ");
   /* allow all signals and pause */ 
   if (sigsuspend(&zeromask) != -1) 
       err_sys("sigsuspend error"); 
   pr_mask("after return from sigsuspend: ");
   /* reset signal mask which unblocks SIGINT */ 
   if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) 
       err_sys("SIG_SETMASK error");
   /* and continue processing ... */ 
   exit(0); 
}
static void sig_int(int signo) { 
   pr_mask("/nin sig_int: "); 
   return; 
}

结果:
$a.out 
in critical region: SIGINT 
^C 
in sig_int: SIGINT 
after return from sigsuspend: SIGINT
如果按照sig_handler先返回,那么SIGINT是不该被打印出来的,因为那时屏蔽字还没有恢复,所有信号都是不阻塞的。那么是Stevens说错了么?当然没有,只是Stevens没有说请在sigsuspend的原子操作中到底做了什么?
sigsuspend的整个原子操作过程为:
(1) 设置新的mask阻塞当前进程;
(2) 收到信号,调用该进程设置的信号处理函数;
(3) 待信号处理函数返回后,恢复原先mask;
(4) sigsuspend返回。

大 致就是上面这个过程,噢,原来signal handler是原子操作的一部分,所以上面的例子是没有问题的,Stevens说的也没错。由于Linux和Unix的千 丝万缕的联系,所以在两个平台上绝大部分的系统调用的语义是一致的。上面的sigsuspend的原子操作也是从《深入理解Linux内核》一书中揣度出 来的。书中的描述如下:
The sigsuspend( ) system call puts the process in the TASK_INTERRUPTIBLE state, after having signaspecifie blocked the standard sid by a bit mask array to which the mask parameter points. The process will wake up only when a nonignored, nonblocked signal is sent to it. The corresponding sys_sigsuspend( ) service routine executes these statements:
mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP)); 
spin_lock_irq(¤t->sigmask_lock);
saveset = current->blocked; 
siginitset(¤t->blocked, mask);
recalc_sigpending(current); 
spin_unlock_irq(¤t->sigmask_lock);
regs->eax = -EINTR; 
while (1) { 
     current->state = TASK_INTERRUPTIBLE; 
     schedule( ); 
     if (do_signal(regs, &saveset)) 
         return -EINTR; 

而最后的do_signal函数调用则是负责调用User Signal Handler的家伙。我想到这CU上的那个问题该被解疑清楚了吧。

-----------------------------------补充:
清晰且可靠的等待信号到达的方法是先阻塞该信号(防止临界区重入),然后使用 sigsuspend 放开此信号并等待句柄设置信号到达标志。

转自


Linux Signal (10): abort函数

#include <stdlib.h>

函数名: abort

 功 能: 异常终止一个进程

 用 法: void abort(void);

 

abort()是使异常程序终止,同时发送SIGABRT信号给调用进程。

 

程序例:

#include  <stdio.h>
#include  <stdlib.h>
void main( void )
{
   FILE *stream;
   if( (stream = fopen( "NOSUCHF.ILE", "r" )) == NULL )
   {
      perror( "Couldn't open file" );
      abort();
   }
   else
      fclose( stream );
}

输出:
Couldn't open file: No such file or directory
abnormal program termination

posted @ 2013-05-10 09:33  ArcherDev  阅读(1888)  评论(0编辑  收藏  举报