Linux 信号量大全

Linux 信号量大全

一、内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

1. 用户程序注册了SIGQUIT信号的处理函数sighandler。

2. 当前正在执行main函数,这时发生中断或异常切换到内核态。

3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。

4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

(By default,  the  signal  handler  is invoked on the normal process stack.  It is possible to arrange that the signal handler uses an alternate stack; see sigaltstack(2) for a discussion of how to do this and when it might be useful.)

5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。

6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

 上图出自ULK

二、sigaction函数

#include <signal.h>

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

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。

Signo是指定要操作信号的编号。

若act 指针非空,则根据act修改该信号的处理动作。

若oact指针非空,则通过oact传出该信号原来的处理动作。

Act和oact指向sigaction结构体:

struct sigaction {

          void  (*sa_handler)(int );    /*//老类型的信号处理函数指针,与single函数一样*/

          void  (*sa_sigaction)(int, siginfo_t *, void*);  /* // 新类型的信号处理函数指针*/

           sigset_t   sa_mask;                     // 将要被阻塞的信号集合

           int        sa_flags;                   // 信号处理方式

           void     (*sa_restorer)(void);     // 保留

           };

       void  (*sa_handler)(int):将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

sigset_t  sa_mask;:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

int  sa_flags:sa_flags有几个选项,比较重要的有两个:SA_NODEFER 和SA_SIGINFO,当SA_NODEFER设置时在信号处理函数执行期间不会屏蔽当前信号;当SA_SIGINFO设置时与sa_sigaction 搭配出现,sa_sigaction函数的第一个参数与sa_handler一样表示当前信号的编号,第二个参数是一个siginfo_t 结构体,第三个参数一般不用。当使用sa_handler时sa_flags设置为0即可。

void   (*sa_restorer)(void):

需要注意的是sa_restorer 参数已经废弃不用,sa_handler主要用于不可靠信号(实时信号当然也可以,只是不能带信息),sa_sigaction用于实时信号可以带信息(siginfo_t),两者不能同时出现。

 siginfo_t {

               int      si_signo;    /* Signal number */

               int      si_errno;    /* An errno value */

               int      si_code;     /* Signal code */

               int      si_trapno;   /* Trap number that caused

                                        hardware-generated signal

                                        (unused on most architectures) */

               pid_t    si_pid;      /* Sending process ID */

               uid_t    si_uid;      /* Real user ID of sending process */

               int      si_status;   /* Exit value or signal */

               clock_t  si_utime;    /* User time consumed */

               clock_t  si_stime;    /* System time consumed */

               sigval_t si_value;    /* Signal value */

               int      si_int;      /* POSIX.1b signal */

               void    *si_ptr;      /* POSIX.1b signal */

               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */

               int      si_timerid;  /* Timer ID; POSIX.1b timers */

               void    *si_addr;     /* Memory location which caused fault */

               long     si_band;     /* Band event (was int in

                                        glibc 2.3.2 and earlier) */

               int      si_fd;       /* File descriptor */

               short    si_addr_lsb; /* Least significant bit of address

                                        (since kernel 2.6.32) */

           }

需要注意的是并不是所有成员都在所有信号中存在定义,有些成员是共用体,读取的时候需要读取对某个信号来说恰当的有定义的部分。

下面用sigaction函数举个小例子:

#include<sys/types.h>

#include<sys/stat.h>

#include<unistd.h>

#include<fcntl.h>

#include<stdio.h>

#include<stdlib.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);

    act.sa_flags = 0;

    if (sigaction(SIGINT, &act, NULL) < 0)

        ERR_EXIT("sigaction error");

    for (; ;)

        pause();

    return 0;

}

 

void handler(int sig)

{

    printf("rev sig=%d\n", sig);

}

simba@ubuntu:~/Documents/code/linux_programming/APUE/signal$ ./sigaction

^Crev sig=2

^Crev sig=2

^Crev sig=2

即按下ctrl+c 会一直产生信号而被处理打印recv语句。

 

其实我们在前面文章说过的signal 函数是调用sigaction 实现的,而sigaction函数底层是调用 do_sigaction() 函数实现的。可以自己实现一个my_signal 函数,如下:

#include<sys/types.h>

#include<sys/stat.h>

#include<unistd.h>

#include<fcntl.h>

#include<stdio.h>

#include<stdlib.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);

/* 系统调用signal()实际上调用了sigaction() */

__sighandler_t my_signal(int sig, __sighandler_t handler);

int main(int argc, char *argv[])

{

    my_signal(SIGINT, handler);

    for (; ;)

        pause();

    return 0;

}

__sighandler_t my_signal(int sig, __sighandler_t handler)

{

    struct sigaction act;

    struct sigaction oldact;

    act.sa_handler = handler;

    sigemptyset(&act.sa_mask);

    act.sa_flags = 0;

    if (sigaction(sig, &act, &oldact) < 0)

        return SIG_ERR;

    return oldact.sa_handler; // 返回先前的处理函数指针

}

void handler(int sig)

{

    printf("rev sig=%d\n", sig);

}

输出测试是一样的,需要注意的是 signal函数成功返回先前的handler,失败返回SIG_ERR。而sigaction 是通过oact 参数返回先前的handler,成功返回0,失败返回-1。

 

三、Linux信号量大全

编号

信号名称

缺省动作

说明

1

SIGHUP

终止

终止控制终端或进程

2

SIGINT

终止

键盘产生的中断(Ctrl-C)

3

SIGQUIT

dump

键盘产生的退出

4

SIGILL

dump

非法指令

5

SIGTRAP

dump

debug中断

6

SIGABRT/SIGIOT

dump

异常中止

7

SIGBUS/SIGEMT

dump

总线异常/EMT指令

8

SIGFPE

dump

浮点运算溢出

9

SIGKILL

终止

强制进程终止

10

SIGUSR1

终止

用户信号,进程可自定义用途

11

SIGSEGV

dump

非法内存地址引用

12

SIGUSR2

终止

用户信号,进程可自定义用途

13

SIGPIPE

终止

向某个没有读取的管道中写入数据

14

SIGALRM

终止

时钟中断(闹钟)

15

SIGTERM

终止

进程终止

16

SIGSTKFLT

终止

协处理器栈错误

17

SIGCHLD

忽略

子进程退出或中断

18

SIGCONT

继续

如进程停止状态则开始运行

19

SIGSTOP

停止

停止进程运行

20

SIGSTP

停止

键盘产生的停止

21

SIGTTIN

停止

后台进程请求输入

22

SIGTTOU

停止

后台进程请求输出

23

SIGURG

忽略

socket发生紧急情况

24

SIGXCPU

dump

CPU时间限制被打破

25

SIGXFSZ

dump

文件大小限制被打破

26

SIGVTALRM

终止

虚拟定时时钟

27

SIGPROF

终止

profile timer clock

28

SIGWINCH

忽略

窗口尺寸调整

29

SIGIO/SIGPOLL

终止

I/O可用

30

SIGPWR

终止

电源异常

31

SIGSYS/SYSUNUSED

dump

系统调用异常

四、SIGALRM信号

信号有好多种,为什么偏偏要讲SIGALRM信号,因为这种信号在实际研发中的实用价值高,在使用中有两个函数可以产生这个信号,它们是alarm和setitimer,它们的区别是alarm相当于单次定时器,setitimer相当于循环定时器。

具体实现如下:

4.1 // alarm函数原型

        unsigned int alarm(unsigned int seconds);

        // alarm函数使用

        alarm(6);

        // 6秒以后给当前进程发SIGALRM信号。

 4.2 // setitimer函数原型

        int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

        // setitimer函数使用

        struct itimerval new_value;

        struct itimerval old_value;

        new_value.it_interval.tv_sec = 1;

        new_value.it_interval.tv_usec = 0;

        // 定时器间隔,定时器到0以后,重新填充的值。

        new_value. it_value.tv_sec = 1;

        new_value. it_value.tv_usec = 0;

        // 当前值,定时器从当前值开始递减到0。

        setitimer(ITIMER_REAL, &new_value, &old_value);

        // 如果已经设置了定时器,那么旧值存到old_value中。

4.3 signal 系统调用

系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下:

void (*signal(int signum, void (*handler)(int)))(int);

上述声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义):

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

但这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册。

在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是

SIG_IGN:忽略参数signum所指的信号。

SIG_DFL:恢复参数signum所指信号的处理方法为默认值。

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal返回值是指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR。下面来看一个简单的例子:

4.4 Kill系统调用

系统调用kill用来向进程发送一个信号。该调用声明的格式如下:

int kill(pid_t pid, int sig);

该系统调用可以用来向任何进程或进程组发送任何信号。如果参数pid是正数,那么该调用将信号sig发送到进程号为pid的进程。如果pid等于0,那么信 号sig将发送给当前进程所属进程组里的所有进程。如果参数pid等于-1,信号sig将发送给除了进程1和自身以外的所有进程。如果参数pid小于- 1,信号sig将发送给属于进程组-pid的所有进程。如果参数sig为0,将不发送信号。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应 的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。

EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误 表示组中有成员进程不能接收该信号。

4.5 pause系统调用 

系统调用pause的作用是等待一个信号。该调用的声明格式如下: 

int pause(void); 

该调用使得发出调用的进程进入睡眠,直到接收到一个信号为止。该调用总是返回-1,并设置错误代码为EINTR(接收到一个信号)。

4.6 alarm系统调用

系统调用alarm的功能是设置一个定时器,当定时器计时到达时,将发出一个信号给进程。该调用的声明格式如下:

unsigned int alarm(unsigned int seconds);

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

4.7 关于signal函数的返回值问题

今天看到了信号有关的章节,对其中的返回值的描述不太理解,也包括后面例程中提供的正规写法:

if (signal(SIGINT, sigint_handler)) == SIG_ERR)

觉得这条if语句也有点搞不懂 原文说的是:若成功则返回指向前次处理程序的指针,若出错则为SIG_ERR 。当时想不清楚signal调用后到底返回了什么,这条if语句怎么判断,后来用google找到一篇国外的博客解释的不错。其实主要是记住signal返回的是一个指向某个函数的指针,而这个函数就是上次处理这个信号的信号处理函数,如果未处理过,那就是NULL。测试代码如下:

#include <signal.h>

#include <assert.h>

#include <stdio.h>

void catch1(int signo) {

  printf("catch1 received signal %d\n", signo);

}

void catch2(int signo) {

  printf("catch2 received signal %d\n", signo);

}

int main(void) {

  sig_t prev_sigint_handler1 = signal(SIGINT, catch1);

  assert(prev_sigint_handler1 == NULL);

  sig_t prev_sighup_handler1 = signal(SIGHUP, catch2);

  assert(prev_sighup_handler1 == NULL);

raise(SIGINT);  // calls catch1

  raise(SIGHUP);  // calls catch2

  // Now let's swap the handlers

  sig_t prev_sigint_handler2 = signal(SIGINT, catch2);

  assert(prev_sigint_handler2 == catch1);

 

  sig_t prev_sighup_handler2 = signal(SIGHUP, catch1);

  assert(prev_sighup_handler2 == catch2);

 

  raise(SIGINT);  // calls catch2

  raise(SIGHUP);  // calls catch1

 

  return 0;

}

% ./a.out

catch1 received signal 2

catch2 received signal 1

catch2 received signal 2

catch1 received signal 1

不难看出一开始两个handler1都为NULL,分别发送SIGINT和SIGHUP信号后,就指向了catch1和catch2。而再次调用signal后会分别得到两个handler,于是可以让新的handler2交换捕获的信号,就和指针的赋值一样了。

 

posted @ 2020-09-14 10:46  陈木  阅读(1200)  评论(0)    收藏  举报