ECF机制:信号处理

简介: ECF机制:信号处理

 



0x00 观察接收信号

假设内核从异常处理程序中返回,并准备将控制权交给进程

代码层级上看:假设内核从异常处理程序中返回,并准备将控制权交给进程 ,内核计算:

(进程 的待处理的非阻塞信号集合)

如果 pnb == 0:

将控制权传递给进程 在逻辑流程中的下一条指令。

否则:

  • 中选择最小的非零位 ,并强制进程 接收信号
  • 信号的接收将触发 执行某些动作。
  • 对于 中的所有非零 ,重复上述步骤。
  • 将控制权传递给 在逻辑流程中的下一条指令

默认操作:每种信号类型都有预定义的默认操作,包括以下几种:

  • 进程终止
  • 进程暂停,直到通过 SIGCONT 信号重新启动
  • 进程忽略该信号

安装信号处理程序

下面的函数可以修改与接收到信号signum相关联的操作(使用man命令获取详细信息):

// 设置信号 signum 的处理函数,也称为信号处理程序或信号处理函数
sighandler_t signal(int signum, sighandler_t handler)
// 设置信号 signum 的处理行为,并且可以获取先前的处理行为
int sigaction(int signum, const struct sigaction *act, 
              struct sigaction *oldact);

signal() 函数中 handler 参数的可能取值:

  • SIG_IGN:忽略类型为 signum 的信号
  • SIG_DFL:恢复类型为 signum 的信号的默认操作
  • 否则,handler 是用户级别函数的地址:当进程接收到类型为 signum 的信号时调用该函数这被称为 "安装" 信号处理程序,执行 handler 被称为 "捕获" 或 "处理" 信号,当 handler 返回时,控制权会回到被信号中断的进程指令处。

信号处理的例子:

void sigint_handler(int sig) /* SIGINT handler */
{
    printf("So you think you can stop this with ctrl-c?\n");
    sleep(2);
}
int main(int argc, char** argv)
{
    /* Install the SIGINT handler */
    if (signal(SIGINT, sigint_handler) == SIG_ERR)
    perror("signal error");
    /* Wait for the receipt of a signal */
    while(1);
    return 0;
}

接收信号:

信号处理器作为并发流,信号处理器是与主程序同时运行的独立逻辑流(而不是进程):

嵌套的信号处理器,处理器可以被其他处理器中断:

0x01 阻塞和解除阻塞信号

隐式阻塞机制

  • 内核会阻塞当前正在处理的类型的任何未决信号
  • 例如,SIGINT处理器不能被另一个SIGINT中断

显式阻塞和解除阻塞机制

  • 使用 sigprocmask 函数

支持的函数

  • sigemptyset - 创建一个空的信号集合
  • sigfillset - 将所有信号号码添加到集合中
  • sigaddset - 将信号号码添加到集合中
  • sigdelset - 从集合中删除信号号码

阻塞和解除阻塞的例子:

sigset_t mask, prev_mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
/* Block SIGINT and save previous blocked set */
sigprocmask(SIG_BLOCK, &mask, &prev_mask);
.
.     /* Code region that will not be interrupted by SIGINT */
.
/* Restore previous blocked set, unblocking SIGINT */
sigprocmask(SIG_SETMASK, &prev_mask, NULL);

0x02 安全的信号处理

处理器因为与主程序并发运行并共享相同的全局数据结构,所以比较棘手。

  • 共享的数据结构可能会被破坏

我们将在后面深入探讨并发性问题!

现在,以下是一些指导原则,以帮助您编写安全的信号处理器。

编写安全处理器的指导原则:

G0:保持您的处理器尽可能简单

  • 例如,设置一个全局标志并返回

G1:在处理器中只调用异步信号安全的函数

  • 如果函数是可重入的(例如,所有变量都存储在栈帧上,参见CS:APP 12.7.2),或者不会被信号中断,则该函数是异步信号安全的。
  • 常用的异步信号安全的函数有:_exit、write、wait、waitpid、sleep、kill
  • 不安全的函数包括:exit、printf、sprintf、malloc

G2:在进入和退出时保存和恢复 errno 值

  • 这样,您的处理器不会影响主程序中观察到的 errno 值

G3:通过临时阻塞所有信号来保护对共享数据结构的访问

防止可能的数据结构损坏,如何实现?

  • 在处理器中调用 sigprocmask 函数(可能不稳定)
  • 当使用sigaction函数时,可以指定在处理器执行时额外阻塞哪些信号

G4:将全局变量声明为 volatile

  • 防止编译器将其存储在寄存器中
  • 如果变量存储在寄存器中,对该变量的更新可能对读取者不可见

0x03 在信号处理器中使用安全的 I/O 函数

在信号处理器中考虑使用可重入的 SIO (Safe I/O) 库,例如 sio.c 中提供的库函数。

ssize_t sio_puts(char s[])   /* Put string */
ssize_t sio_putl(long v)     /* Put long */
void sio_error(char s[])     /* Put msg & exit */

SIO库是一个安全的I/O库,专门为信号处理器设计,用于处理信号处理器中的并发问题。它提供了一组可重入的I/O函数,用于在信号处理器中进行安全的I/O操作,避免了潜在的并发问题和数据损坏。

代码例子:

ssize_t sio_puts(char s[])
{
    return write(STDOUT_FILENO, s, sio_strlen(s));
}
void sio_error(char s[])
{
    sio_puts(s);
    _exit(1);
}

0x04 便携式信号处理

在不同的UNIX操作系统中,信号处理的实现可能会有所不同。

  • 一些旧系统在捕捉信号后会将动作恢复为默认值
  • 一些系统不会阻塞正在处理的类型的信号
  • 一些被中断的系统调用可能会返回errno == EINTR

解决方案:使用 sigaction 函数

handler_t *Signal(int signum, handler_t *handler)
{
    struct sigaction action, old_action;
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask);  /* Which signal will be additionally blocked */
    action.sa_flags = SA_RESTART;  /* Restart syscalls if possible */
    if (sigaction(signum, &action, &old_action) < 0)
    unix_error("Signal error");
    return (old_action.sa_handler);
}

常见错误:

volatile int ccount = 0;
void child_handler(int sig) {
  int olderrno = errno;
  pid_t pid;
  if ((pid = wait(NULL)) < 0)
    sio_error("wait error");
  ccount--;
  sio_puts("Handler reaped child ");
  sio_putl((long)pid);
  sio_puts("\n");
  sleep(1);
  errno = olderrno;
}
int main(void) {
  pid_t pid[N];
  int i;
  ccount = N;
  signal(SIGCHLD, child_handler);
  for (i = 0; i < N; i++) {
    if ((pid[i] = fork()) == 0) {
      sleep(1);
      exit(0); /* Child exits */
    }
  }
  while (ccount > 0); /* Parent spins */
}

未决信号未排队:对于每种信号类型,一位指示信号是否处于未决状态,因此任何特定类型的至多一个未决信号。所以不能使用信号来计算事件,例如 child 的终止。

修复错误:必须等待所有终止的子进程,在循环中加入 wait 以获取所有终止的子进程。

void child_handler(int sig)
{
  int olderrno = errno;
  pid_t pid;
  while ((pid = wait(NULL)) > 0) {
    ccount--;
    sio_puts("Handler reaped child ");
    sio_putl((long)pid);
    sio_puts("\n");
  }
  if (errno != ECHILD)
    sio_error("wait error");
  errno = olderrno;
}

另一个细微的错误:带有细微同步错误的简易 shell

int main(void) {
  int pid;
  sigset_t mask_all, prev_all;
  Sigfillset(&mask_all);
  Signal(SIGCHLD, handler);
  initjobs(); /* Initialize the job list */
  while (1) {
    if ((pid = Fork()) == 0) { /* Child */
      Execve("/bin/date", argv, NULL);
    }
    /* Parent */
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    addjob(pid); /* Add the child to the job list */
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
  }
  exit(0);
}
void handler(int sig)
{
  int olderrno = errno;
  sigset_t mask_all, prev_all;
  pid_t pid;
  Sigfillset(&mask_all);
  while ((pid = waitpid(-1, NULL, 0)) > 0) {
    /* Reap child */
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    deletejob(pid); /* Delete child from the job list */
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
  }
  if (errno != ECHILD) /* ECHILD: child does not exist */
    sio_error("waitpid error");
  errno = olderrno;
}

修复该错误:

int main(void) {
  int pid;
  sigset_t mask_all, mask_one, prev_one;
  Sigfillset(&mask_all);
  Sigemptyset(&mask_one);
  Sigaddset(&mask_one, SIGCHLD);
  Signal(SIGCHLD, handler);
  initjobs(); /* Initialize the job list */
  while (1) {
    Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
    if ((pid = Fork()) == 0) { /* Child process */
      Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
      Execve("/bin/date", argv, NULL);
    }
    Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
    addjob(pid); /* Add the child to the job list */
    Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
  }
  exit(0);
}

0x05 显式信号等待

显式地等待 SIGCHLD 到来的程序的处理程序。

int main(void) {
  sigset_t mask, prev;
  Signal(SIGCHLD, sigchld_handler);
  Signal(SIGINT, sigint_handler);
  Sigemptyset(&mask);
  Sigaddset(&mask, SIGCHLD);
  while (1) {
    Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
    if (Fork() == 0) /* Child */
      exit(0);
    /* Parent */
    pid = 0;
    Sigprocmask(SIG_SETMASK, &prev, NULL); /* Unblock */
    /* Wait for SIGCHLD to be received (wasteful!) */
    while (!pid);
    /* Do some work after receiving SIGCHLD */
    printf(".");
  }
  exit(0);
}

如果程序是正确的,那么将会导致浪费,程序会进入忙等待循环:

while (!pid);

可能的竞争条件,可能在检查 pid 和启动暂停信号之间接收信号:

while (!pid) /* Race! */
    pause();

安全,但速度慢:最多需要一秒钟才能做出响应

while (!pid) /* Too slow! */
    sleep(1);

解决方案:sigsuspend

int sigsuspend(const sigset_t *mask)

不间断版本:

sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

💬 代码演示:使用 sigsuspend 等待信号

int main(int argc, char** argv) {
  sigset_t mask, prev;
  Signal(SIGCHLD, sigchld_handler);
  Signal(SIGINT, sigint_handler);
  Sigemptyset(&mask);
  Sigaddset(&mask, SIGCHLD);
  while (1) {
    Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
    if (Fork() == 0) /* Child */
      exit(0);
    /* Wait for SIGCHLD to be received */
    pid = 0;
    while (!pid)
      Sigsuspend(&prev);
    /* Optionally unblock SIGCHLD */
    Sigprocmask(SIG_SETMASK, &prev, NULL);
    /* Do some work after receiving SIGCHLD */
    printf(".");
  }
  exit(0);
}

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.3.
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. Linux[EB/OL]. 2021[2021.8.31 xi

相关文章
|
7月前
|
Unix Linux C++
c++信号处理
c++信号处理
65 0
|
设计模式 Unix Shell
ECF机制:信号 (Signal)
ECF机制:信号 (Signal)
194 0
|
7月前
|
存储 Linux 编译器
Linux进程信号【信号处理】
Linux进程信号【信号处理】
114 0
|
3月前
|
Linux 程序员 API
信号的机制——信号处理函数的注册
【9月更文挑战第17天】在 Linux 系统中,信号用于响应各种事件,可通过 `kill -l` 查看所有信号。每个信号有唯一 ID 及默认操作,如终止(Term)或生成核心转储(Core)。进程可执行默认操作、捕获信号或忽略信号,但无法忽略 SIGKILL 和 SIGSTOP。常用 `signal` 或 `sigaction` 函数注册信号处理函数,后者更灵活且推荐使用。信号处理涉及系统调用和内核设置,建议根据需求定制参数。
|
6月前
信号处理与 signal.h 库
信号处理与 signal.h 库
|
7月前
|
NoSQL Linux 程序员
【linux进程信号(一)】信号的概念以及产生信号的方式
【linux进程信号(一)】信号的概念以及产生信号的方式
|
7月前
|
存储 Linux
【linux进程信号(二)】信号的保存,处理以及捕捉
【linux进程信号(二)】信号的保存,处理以及捕捉
|
7月前
|
Linux C语言
Linux系统编程(信号处理机制)
Linux系统编程(信号处理机制)
65 0
|
7月前
|
存储 Unix Linux
Linux系统编程(传统信号和实时信号)
Linux系统编程(传统信号和实时信号)
79 0
|
Linux
Linux 进程信号的基本概念、信号类型、信号处理方式、信号传递机制以及如何使用进程信号进行进程间通信、异常处理
Linux 进程信号的基本概念、信号类型、信号处理方式、信号传递机制以及如何使用进程信号进行进程间通信、异常处理
689 0