Linux信号

RayCongLiang 2020-06-01

学习目标:

  • 了解Linux中信号的作用,以及信号的产生和处理机制
  • 能够使用Linux中提供的信号接口函数进行程序设计

1、信号的概念及机制

       信号在我们生活中随处可见,如:古代的烽火传信、摔杯为号,现代的体育比赛使用的信号枪,它总是带有信息,当信号发生时人们将闻号而动。Linux系统中的信号和我们生活中的信号作用相似,为响应某些条件而产生的一个事件,接受到该信号的进程会相应地采取一些动作。例如:A给B发信号,B收到信号之前在执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行处理信号,处理完毕再继续运行。

理解信号时,要知道它的4要素:信号的编号、信号名、产生事件、默认处理动作

2、信号的产生、处理方式

产生信号的方式有以下几种:

  1. 按键产生,如:ctrl+c、ctrl+z
  2. 系统调用产生,如:kill、raise、abort
  3. 硬件异常产生,如:非法访问内存(段错误)、除零操作、内存对齐出错(总线错误)
  4. 软件条件产生,如:定时器alarm
  5. 命令产生,如shell中执行kill命令

信号处理方式:

  1. 执行默认动作
  2. 忽略
  3. 捕获,执行用户自定义操作

(注:SIGKILL和SIGSTOP不能被捕捉、阻塞和忽略,只能执行默认动作)

内核处理信号的机制:

信号是由内核发出和捕获的,当某信号被内核捕获,并不会立刻执行,而是要等到用户程序由系统调用、中断或异常进入内核处理完相应操作准备返回用户模式时才会被检测调用。具体执行细节如下图2-1所描述

Linux信号

图2-1 内核处理信号机制

3、信号的编号

在shell环境中执行kill -l命令查看当前系统中支持的信号,如下图3-1所示,信号中不存在编号为0的信号,其中1~31信号为普通信号,34~64之间称之为实时信号(驱动编程中使用,和硬件相关)

Linux信号

 图3-1:linux系统中支持信号

4、信号的默认动作

当信号产生时,系统都有与之对应的默认处理动作。通过man 7 signal命令可以在man page中查看到具体帮助文档:

Linux信号

  图4-1 各信号描述信息

由上图可以看出,信号的默认动作分为四种:

Term:终止进程

Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试)

Ign:忽略信号

Cont:继续运行进程

Stop:暂停进程

仅举一些常见的信号来看:

1)SIGHUP,Action为Term,当用户退出shell时,由该shell启动的所有进程将收到该信号,默认动作为终止进程。

2)SIGINT,Action为Term,当用户按下<ctrl+c>组合键时,用户终端向正在运行的由当前终端启动的程序发信号。当应用程序收到该信号,默认情况进程终止。

14)SIGALRM,Action为Term,定时器超时,超时时间由系统调用函数alarm来设置,默认情况下终止进程。

17)SIGCHLD,Action为Ign,当程序的子进程退出时,会向其父进程发送SIGCHLD信号,默认情况下父进程忽略该信号。

5、linux系统提供信号操作函数

5.1 信号的产生

可以通过kill函数给指定进程发送指定信号,kill函数的函数原型如下:

int kill(pid_t pid, int sig); //成功时返回0,失败时返回-1并设置errno

 sig:信号名,应使用宏名,如要发送SIGKILL,将sig设置为SIGKILL

 pid:目标进程id, pid > 0发送信号给指定进程;pid = 0,发送信号给和执行该系统调用的进程所在进程组中的所有进程;pid < -1,取|pid|发送给对应进程;pid = -1,发送给进程有权限发送的系统中所有进程

5.2 信号捕捉

5.2.1 注册一个信号捕捉函数

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); //成功:返回函数指针;失败返回SIG_ERR,并设置errno

handler:函数指针,指向“void sig_cb(int sig)“类型函数,捕获信号后,执行该自定义的处理函数

signum:捕获信号名

5.2.2 一个更健壮的信号捕获接口

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);//成功:返回0,失败返回-1,并设置errno
 struct sigaction {
      ...
      void     (*sa_handler)(int);
      sigset_t   sa_mask;
      int       sa_flags;
      ...
 };

signum:要修改处理动作的信号

act:传入参数,接收到该信号新的处理方式

oldact:传出参数,旧的信号处理方式

struct sigaction结构体:

   sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作

  sa_mask:调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。

  sa_flag:通常设置为0,表使用默认属性。

5.3 信号集操作

        linux的进程控制块中,维护了两个非常重要信号集,一个是“阻塞信号集”,另一个是“未决信号集”,这两个信号集都是内核通过位图来实现的。阻塞信号集,当某信号加入该集合,对他们设置屏蔽。当阻塞了x信号,再收到该信号,该信号的处理将推后,此时未决信号集的该位被置1;当阻塞结束,执行x信号后,对应的未决信号位翻转回0。操作系统不允许我们直接对pcb控制块内的信号集进行直接操作,需自定义另外一个集合,借助信号集操作函数对pcb进程块中的信号集进行操作。

int sigemptyset(sigset_t *set);                   //将某个信号集清0             成功:0;失败:-1,设置errno
int sigfillset(sigset_t *set);                    //将某个信号集置1             成功:0;失败:-1,设置errno
int sigaddset(sigset_t *set, int signum);         //将某个信号加入信号集合中      成功:0;失败:-1,设置errno
int sigdelset(sigset_t *set, int signum);         //将某信号从信号清出信号集      成功:0;失败:-1,设置errno
int sigismember(const sigset_t *set, int signum); //判断某个信号是否在信号集中存在, 存在返回1,不在返回0,出错:-1,设置errno

5.4 设置、解除信号屏蔽

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);    //成功:0;失败:-1,设置errno

how:假设当前的信号屏蔽字为mask

SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set

SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set

SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

set:传入参数,是一个自定义信号集,和how一起进行设置当前信号屏蔽集

oldset:传出参数,保存旧的信号屏蔽集

5.5 读取当前未决信号集

int sigpending(sigset_t *set);    //set传出参数。   返回值:成功:0;失败:-1,设置errno

5.6 替换进程屏蔽信号集、挂起当前进程

int sigsuspend(const sigset_t *sigmask); //如果接收到信号终止了程序,sigsuspend不会返回,如果接受的信号没有终止程序sigsuspend返回-1,并设置errno为EINTR

 sigsuspend函数将进程的屏蔽字替换为由参数sigmask传入的信号集,然后挂起执行程序。程序将在信号处理函数执行完毕后继续执行。

6、信号接口函数测试

6.1 signal函数修改SIGINT信号默认动作

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

#include <stdio.h>
 
void sigint_handler(int sig)
{
    printf("Current Process Id = %d\n", getpid());
}
 
int main(int argc, char **argv)
{
    signal(SIGINT, sigint_handler);

    while(1)
    {
        sleep(10);
    }
}

修改了捕获SIGINT信号的默认动作,当捕获到组合键<ctrl+c>时,该进程不会终止,而是打印当前进程ID。值得注意的是,该测试程序执行时,需通过重新打开shell通过kill命令将其杀掉。

6.2 实现my_sleep函数

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

void sig_alrm(int signo)
{
    /* nothing to do */
}

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

    //1.为SIGALRM设置捕捉函数,一个空函数
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    //2.设置阻塞信号集,阻塞SIGALRM信号
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);

    //3.定时n秒,到时后可以产生SIGALRM信号
    alarm(nsecs);

    /*4.构造一个调用sigsuspend临时有效的阻塞信号集,
     *  在临时阻塞信号集里解除SIGALRM的阻塞*/
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);

    /*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
     *  这个信号集中不包含SIGALRM信号,同时挂起等待,
     *  当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/
    sigsuspend(&suspmask); 

    unslept = alarm(0);
    //6.恢复SIGALRM原有的处理动作,呼应前面注释1
    sigaction(SIGALRM, &oldact, NULL);

    //7.解除对SIGALRM的阻塞,呼应前面注释2
    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return(unslept);
}

int main(void)
{
    while(1){
        mysleep(2);
        printf("Two seconds passed\n");
    }

    return 0;
}

相关推荐