『 Linux 』信号的捕捉及部分子问题

31 篇文章 2 订阅
4 篇文章 0 订阅


信号的捕捉

请添加图片描述

该图为基于信号处理为用户自定义动作的图解;

  • 信号的捕捉

    当一个信号被递达时,如果该信号的处理动作是用户自定义的函数(如int sighandler(int))时就会调用这个函数,该步骤被称为捕捉信号;

  • 用户程序注册新号处理函数

    用户程序中注册了SIGQUIT信号的处理函数sighandler;

  • 切换到内核态

    当前正在执行main函数时发生了中断或是异常,这导致程序从用户态切换到内核态(如图中标注的12);

  • 内核态处理信号

    内核在处理完中断或异常之后,在返回用户态的main函数前检测到有信号SIGQUIT递达(图中标注2);

  • 独立控制流程

    sighandlermain函数使用不同的堆栈空间,之间没有调用和被调用的关系,是两个独立的控制流程(图中标注4);

  • 返回用户态

    sighandler函数返回后通过执行的特殊的系统调用sys_sigreturn再次进入内核(图中标注4);

  • 恢复主流程

    如果没有新的信号被注册,这次返回用户态时将恢复main函数中断前的上下文继续执行(图中标注5);


sigaction函数

请添加图片描述

sigaction()函数与signal()函数相同,功能都为捕捉信号;

NAME
       sigaction - examine and change a signal action

SYNOPSIS
       #include <signal.h>

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

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigaction(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

       siginfo_t: _POSIX_C_SOURCE >= 199309L

RETURN VALUE
       sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.

该函数的具体功能用于检查和改变指定型号的处理方式;

它允许程序定义或更改信号处理函数,也可以获取当前信号处理函数的信息;

函数调用成功时返回0,调用失败时返回-1并设置errno;

参数如下:

  • int signum

    表示传入一个int类型的参数,该参数表明需要传入的信号编号;

  • const struct sigaction *act

    传入一个struct sigaction *参数,其中加了const作修饰表示是一个传入型参数;

    该参数用于指定新的信号处理动作,其结构体包含以下成员:

    struct sigaction {
        void (*sa_handler)(int);      // 信号处理函数指针
        void (*sa_sigaction)(int, siginfo_t *, void *); // 另一个信号处理函数指针,用于接收额外信息
        sigset_t sa_mask;             // 在信号处理期间需要阻塞的信号集
        int sa_flags;                 // 影响信号处理行为的标志位
        void (*sa_restorer)(void);    // 不常用的字段,一般设置为NULL
    };
    

    在处理普通信号时只需要关注void (*sa_handler)(int)成员与sigset_t sa_masksigset_t sa_mask成员即可;

    • void (*sa_handler)(int)

      该成员为一个函数指针类型,用于指向信号处理函数,如SIG_IGN,SIG_DFL或是用户自定义函数void userhandler(int);

    • sigset_t sa_mask

      该成员为一个sigset_t类型的数据,其中sigset_t类型为操作系统封装的一个位图结构用于依靠该结构对信号集进行操作;

      当一个进程在处理一个信号的时候会将对应的信号添加到其信号屏蔽字(阻塞信号集)中以避免信号方法重复调用;

      设置该成员可以使得进程在处理一个信号时同时将多个信号添加至信号屏蔽字中;

    其余成员可默认设为0;

  • struct sigaction *oldact

    该参数类型与act类型相同,不被const修饰为一个输出型参数;

    该参数用于保存之前的信号处理动作,如果不需要保存旧的信号处理则传递nullptr;

void sighandler(int signo) {
  // 自定义处理动作
  printf("sighandler get a signal: %d\n", signo);
}

int main() {
  struct sigaction act, oldact;

  memset(&act, 0, sizeof(act));  // 利用memset()初始化结构体
  memset(&act, 0, sizeof(oldact));

  act.sa_handler = sighandler;  // 为成员赋值 设置自定义处理动作
  int n = sigaction(SIGINT, &act, &oldact);  // 函数调用
  while (!n) {                               // 循环打印
    cout << "I am a Process ,id : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}

该例子未使用可以同时阻塞多个信号的特性;

运行结果为:

$ ./mysignal 
I am a Process ,id : 29791
^Csighandler get a signal: 2
I am a Process ,id : 29791
^Csighandler get a signal: 2
I am a Process ,id : 29791
^\Quit

当使用Ctrl + C对进程发送SIGINT信号时被捕捉而后执行自定义动作;


未决信号集的置零时机

请添加图片描述

当信号被处理后未决信号集对应位置应置零从而表示该信号已经被处理完毕为递达状态;

实际上未决信号集的置零时机为执行信号处理前,即先将未决信号集对应位置置零再执行处理信号的函数;

以上文代码为基础进行修改:

void PrintPending() {
  sigset_t pending;
  sigemptyset(&pending);
  sigpending(&pending);

  for (int i = 31; i > 0; --i) {
    if (sigismember(&pending, i))
      cout << "1";
    else
      cout << "0";
  }
  cout << endl;
}

void sighandler(int signo) {
  // 自定义处理动作
  PrintPending();
  printf("sighandler get a signal: %d\n", signo);
}

int main() {
  struct sigaction act, oldact;

  memset(&act, 0, sizeof(act));  // 利用memset()初始化结构体
  memset(&act, 0, sizeof(oldact));

  act.sa_handler = sighandler;  // 为成员赋值 设置自定义处理动作
  int n = sigaction(SIGINT, &act, &oldact);  // 函数调用
  while (!n) {                               // 循环打印
    cout << "I am a Process ,id : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}

添加了PrintPending()函数用于打印整张Pending位图;

运行结果为:

$ ./mysignal 
I am a Process ,id : 29858
^C0000000000000000000000000000000
sighandler get a signal: 2
I am a Process ,id : 29858
^C0000000000000000000000000000000
sighandler get a signal: 2
I am a Process ,id : 29858
^C0000000000000000000000000000000

当进行信号处理并对Pending位图进行打印时并未出现SIGINT位置变为1的情况;

这意味着实际上在进行信号处理函数前对应的Pending位图已经被置零;


信号处理过程的阻塞

请添加图片描述

当一个信号在被进行处理时将会把正在处理的信号添加进信号屏蔽字以阻塞下一个相同的信号;

本质上是防止同一个信号处理函数被重复调用;

void PrintPending() {
  sigset_t pending;
  sigemptyset(&pending);
  sigpending(&pending);

  for (int i = 31; i > 0; --i) {
    if (sigismember(&pending, i))
      cout << "1";
    else
      cout << "0";
  }
  cout << endl;
}

void sighandler(int signo) {
  // 自定义处理动作
  //   PrintPending();
  printf("sighandler get a signal: %d\n", signo);
  while (1) {
    PrintPending();
    cout << endl;
    sleep(1);
  }
}

int main() {
  struct sigaction act, oldact;

  memset(&act, 0, sizeof(act));  // 利用memset()初始化结构体
  memset(&act, 0, sizeof(oldact));

  act.sa_handler = sighandler;  // 为成员赋值 设置自定义处理动作
  int n = sigaction(SIGINT, &act, &oldact);  // 函数调用
  while (!n) {                               // 循环打印
    cout << "I am a Process ,id : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}

该程序中当进程捕获到一个信号时将会调用用户自定义动作进行信号处理;

用户自定义动作为打印对应捕捉到的信号而后无限循环打印pending表以验证相同信号在第二次注册时是否会被阻塞保留其未决状态;

运行并在另一个窗口中使用kill -signo <pid>命令向进程发送SIGINT信号,结果为:

$ ./mysignal 
I am a Process ,id : 29921
sighandler get a signal: 2

0000000000000000000000000000000

0000000000000000000000000000010

0000000000000000000000000000010

0000000000000000000000000000010

^\Quit

当第一次注册SIGINT信号时将执行用户自定义动作,即先打印sighandler get a signal: 2再循环打印pending表;

一开始的pendingSIGINT信号处已经置零表示正在对该信号进行处理;

当再次注册SIGINT信号时由于上一个相同信号未被处理完成,处于未递达状态,相同的信号被阻塞,停留在未决信号集中;

可调用sigaction()函数并设置struct sigaction *act成员中的sigset_t sa_mask位图以能够在进行一个信号的处理时阻塞多个信号;

void PrintPending() {
  sigset_t pending;
  sigemptyset(&pending);
  sigpending(&pending);

  for (int i = 31; i > 0; --i) {
    if (sigismember(&pending, i))
      cout << "1";
    else
      cout << "0";
  }
  cout << endl;
}

void sighandler(int signo) {
  // 自定义处理动作
  //   PrintPending();
  printf("sighandler get a signal: %d\n", signo);
  while (1) {
    PrintPending();
    cout << endl;
    sleep(1);
  }
}

int main() {
  struct sigaction act, oldact;

  memset(&act, 0, sizeof(act));  // 利用memset()初始化结构体
  memset(&act, 0, sizeof(oldact));

  sigset_t mask;
  sigemptyset(&mask);

  // 使用sigaddset()设置信号集
  sigaddset(&mask, 3);
  sigaddset(&mask, 4);
  sigaddset(&mask, 1);
  sigaddset(&mask, 5);

  act.sa_mask = mask;  // 将设置好的信号集进行赋值从而设置处理时阻塞

  act.sa_handler = sighandler;  // 为成员赋值 设置自定义处理动作
  int n = sigaction(SIGINT, &act, &oldact);  // 函数调用
  while (!n) {                               // 循环打印
    cout << "I am a Process ,id : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}

该函数中定义了一个sigset_t类型的mask,并用sigaddset()将信号1,3,4,5分别加入了信号集,并将该sigset_t类型赋值给sa_mask成员,使其阻塞多个信号;

运行程序并在另一个终端中使用kill命令分别以2,1,2,3,4,5的顺序依次注册信号并观察结果;

对应的结果为:

$ ./mysignal 
I am a Process ,id : 30083
I am a Process ,id : 30083
sighandler get a signal: 2
0000000000000000000000000000010

0000000000000000000000000000011

0000000000000000000000000000111

0000000000000000000000000001111

0000000000000000000000000011111

结果为首先处理2号信号SIGINT并进入死循环打印pending表;

向进程再次发送1-5号信号其都被阻塞至未决信号集中;


可重入函数

请添加图片描述

当一个进程在调用一个函数,在函数执行过程中接收到了一个信号发生了中断;

而信号的处理中又需要调用一次该函数,若是出现了数据错误或是数据不一致等错误问题则称该函数为不可重入函数,反之则称为可重入函数;

以该图为例;

全局环境中存在一个链表并对其进行一次头插操作,其头插操作的核心代码为:

	/* 伪代码 */
insert(...){
    // ...
    NodeA->next = head;
	head = &NodeA;
    // ...
}

即将新头插的节点的next指针指向head所指向的节点;

head重新指向新插入的头结点;

若是在插入过程中,即执行完了NodeA -> next = head;后进程接收到了一个信号并对信号进行处理;

而在信号的处理中需要调用insert()函数进行头插而再次执行该函数时这个过程被称为 “重入” ,即相同函数重复进入;

当执行主控制流时执行insert()函数,在调用insert()时接收信号并处理型号,在处理信号中再次调用了一次insert();

流程图如下:

结果为main()函数和sighandler()先后向链表中插入两个头结点;

而最后只有一个节点被真正插入链表中,使得另一个节点出现 节点丢失 的内存泄漏问题;

这意味着该insert()函数为一个不可重入函数;

可重入函数 , 不可重入函数 都只为一个函数的特点;

如果一个函数符合以下条件之一的则是不可重入函数:

  • 调用了mallocfree

    malloc也是用全局链表来管理堆的;

  • 调用了标准I/O库函数

    标准I/O库的很多实现都以不可重入的方式使用全局数据堆;


volatile 关键字

请添加图片描述

编译器在编译时可以将代码进行优化;

对应的使用不同优化级别使编译时对代码进行优化,常见的有-O0,-O1,-O2,-O3等选项,其中-O0为默认优化,即表示不进行优化;

volatile关键字是用来修饰一个变量以防止编译器过度优化;

int flag = 1;

void sighandler(int signo) {
  printf("sighandler get a signal: %d\n", signo);
  flag = 0;
  printf("falg : %d\n", flag);
}

int main() {
  signal(SIGINT, sighandler);
  while (flag);

  cout << "process quit sucess" << endl;
}

在这段代码中定义了一个全局变量,且设置了对SIGINT信号的捕获;

在主控制流main函数中使用whileflag为条件进行循环,当条件为真时循环,条件为假时跳出循环;

当捕捉到SIGINT信号时对该信号进行处理,处理方案为使用自定义动作,为将全局变量flag设为0,即条件为假并对当前flag进行一次打印;

使用g++-O3选项进行编译并进行编译优化;

g++ -o mysignal mysignal.cc -g -O3 -Wall -std=c++11

运行该程序并向该程序发送2号信号SIGINT;

$ ./mysignal 
^Csighandler get a signal: 2
falg : 0
^Csighandler get a signal: 2
falg : 0
^Csighandler get a signal: 2
falg : 0

从结果看出,即使注册了SIGINT信号且执行了自定义动作将全局变量flag设为了0但进程仍不退出;

本质原因是由于进行了优化后,其flag参数将被存放至寄存器当中,而main()函数中的循环条件始终以寄存器中的flag进行条件判断,修改的flag确实内存中的flag;

两者出现了隔离,寄存器不会向内存再去读取flag变量而是由于while循环不停判断寄存器中的变量从而使得进程无法正常退出;

该行为即为编译器的一种编译过度优化;

可通过使用volatile关键字修饰来放置过度优化行为;

int flag修改为volatile int flag;

重新使用-O3选项编译并运行,并使用Ctrl + C向进程发送一个2号信号SIGINT信号;

$ ./mysignal 
^Csighandler get a signal: 2
falg : 0
process quit sucess
$ 

结果为使用Ctrl + C发送2号信号SIGINT时进程被终止;


SIGCHLD 信号

请添加图片描述

当一个子进程退出时将会为其父进程发送一个信号,该信号为17号信号SIGCHLD;

该信号默认行为SIG_DFL为忽略;

可在父进程中调用signal()接口捕捉SIGCHLD函数进行验证;

void sighandler(int signo) {
  sleep(2);
  printf("parent process catch a signal: %d\n", signo);
  cout << endl;
  waitpid(-1, nullptr, WNOHANG); // 不考虑获取进程退出信息 参数2设置为nullptr
}

int main() {
  signal(SIGCHLD, sighandler);

  pid_t id = fork();
  if (id == 0) {
    // child
    int cnt = 2;
    while (cnt--) {
      printf("I am child process,the PID is %d\n", getpid());
      cout << endl;
      sleep(1);
    }
    cout << "child process quit...." << endl<<endl;;
    exit(-1);
  }
  // parent
  while (1) {
    printf("I am parent process,the PID is %d\n", getpid());
    cout << endl;

    sleep(1);
  }

  return 0;
}

在父进程中使用signal()设置捕获17号信号SIGCHLD信号,并设置自定义动作为调用waitpid(-1,nullptr,WNOHANG)以非阻式来等待子进程退出;

fork()创建子进程,子进程在2s后退出进程并向父进程发送17号信号SIGCHLD;

当父进程获取到子进程所发的信号时将该信号进行捕获,而后调用自定义动作对已经僵尸的子进程进行等待处理;

运行程序并在另一个窗口使用shell脚本:

$ while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep ; echo "----------------------------" ; sleep 1 ; done

观察父子进程的状态;

运行结果为:

$ ./mysignal 
I am parent process,the PID is 32131

I am child process,the PID is 32132

I am parent process,the PID is 32131

I am child process,the PID is 32132

I am parent process,the PID is 32131

child process quit....

parent process catch a signal: 17

I am parent process,the PID is 32131

I am parent process,the PID is 32131

2s过后子进程退出,父进程捕获到17号信号并对子进程进行waitpid()清理;

另一个会话显示的结果为:

 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32131 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
32131 32132 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32131 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
32131 32132 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32131 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
32131 32132 32131 29111 pts/0    32131 Z+    1001   0:00 [mysignal] <defunct>
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32131 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
32131 32132 32131 29111 pts/0    32131 Z+    1001   0:00 [mysignal] <defunct>
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32131 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32131 32131 29111 pts/0    32131 S+    1001   0:00 ./mysignal
----------------------------
^C

子进程僵尸了2s后被父进程回收;

若是不需要子进程的退出信息时父进程可对子进程发出的信号使用signal()进行忽略从而避免子进程停留在僵尸;

以上述代码为基础进行修改:

int main() {
//   signal(SIGCHLD, sighandler);
  signal(SIGCHLD, SIG_IGN);

  pid_t id = fork();
  if (id == 0) {
    // child
    int cnt = 2;
    while (cnt--) {
      printf("I am child process,the PID is %d\n", getpid());
      cout << endl;
      sleep(1);
    }
    cout << "child process quit...." << endl<<endl;;
    exit(-1);
  }
  // parent
  while (1) {
    printf("I am parent process,the PID is %d\n", getpid());
    cout << endl;

    sleep(1);
  }

  return 0;
}

对应结果为:

 # 程序所在会话
 
$ ./mysignal 
I am parent process,the PID is 32213

I am child process,the PID is 32214

I am parent process,the PID is 32213

I am child process,the PID is 32214

I am parent process,the PID is 32213

child process quit....

I am parent process,the PID is 32213

I am parent process,the PID is 32213

^C

 # 脚本所在会话
 
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32213 32213 29111 pts/0    32213 S+    1001   0:00 ./mysignal
32213 32214 32213 29111 pts/0    32213 S+    1001   0:00 ./mysignal
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32213 32213 29111 pts/0    32213 S+    1001   0:00 ./mysignal
32213 32214 32213 29111 pts/0    32213 S+    1001   0:00 ./mysignal
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32213 32213 29111 pts/0    32213 S+    1001   0:00 ./mysignal
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29111 32213 32213 29111 pts/0    32213 S+    1001   0:00 ./mysignal
----------------------------

结果表示子进程退出时直接退出,其父进程并未接收到子进程的SIGCHLD而是直接将其进行忽略处理;

shell脚本所在会话显示子进程并未在僵尸状态下进行停留而是直接退出;

  • SIGCHLD信号的忽略行为

    SIGCHLD的默认动作是忽略与对SIGCHLD信号进行忽略是两种概念;

    第一种为默认动作实际上是调用signal(SIGCHLD,SIG_DFL),而其对应的行为为忽略;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dio夹心小面包

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值