当前位置: 首页 > news >正文

Linux信号-1-信号处理-1-sigaction() - Hello

Linux信号-1-信号处理-1-sigaction()


一、signation简介

sigaction() 系统调用用于修改进程在收到某个特定信号时采取的动作。在 Linux 里,sigaction 是比 signal 更推荐的信号处理接口,原因是行为更稳定、可控性更强。

由 fork(2) 创建的子进程会继承父进程信号处置的副本####。在 execve(2) 期间,已安装处理函数的信号会恢复为默认处置;被忽略的信号保持不变。


1. 基本作用

sigaction 用来为某个信号注册处理方式,比如:
(1) 忽略信号;
(2) 使用自定义处理函数;
(3) 恢复默认行为;
(4) 设置处理时的附加选项(是否自动重启系统调用、是否拿到更多上下文等);


2. 常见结构和调用

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);struct sigaction {void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void);
};

(1) 函数成员介绍:

signum: 要处理的信号,可以是任意有效信号,但 SIGKILL 和 SIGSTOP 除外。
act: 新的处理配置。如果 act 非 NULL,则从 act 安装 signum 的新处理动作。
oldact: 可选,保存旧配置(便于恢复)。如果 oldact 非 NULL,则把此前动作保存到 oldact。
sa_restorer 字段不供应用程序使用,其用途可见 sigreturn(2)。

sigaction() 成功时返回 0;出错时返回 -1,并设置 errno 指示错误。


(2) 核心结构体成员介绍:

sa_handler: 普通处理函数, 通过参数只能拿到信号编号。还可以是一些特殊值,如 SIG_DFL(默认动作)、SIG_IGN(忽略该信号)。

sa_sigaction: 扩展处理函数, 可拿到发送者 PID、附带值等,包括各个寄存器值####。这个函数和上面那个函数只能二选一(实际是联合体),在 sa_flags 中指定 SA_SIGINFO 标志才能选择使用 sa_sigaction。
其第三个参数是当前线程在进入信号处理函数前的上下文(信号发生时的上下文),类型上通常要强转成 ucontext_t * 来用。它主要用于"查看"当时的寄存器、栈指针、程序计数器等现场信息,做调试、崩溃分析、采样比较常见。

sa_mask: 处理期间额外屏蔽哪些信号。默认触发处理函数的那个信号也会被阻塞,除非使用 SA_NODEFER。

sa_flags: 指定一组用于改变信号行为的标志,可由以下标志按位或组成:

SA_NOCLDSTOP: 若 signum 为 SIGCHLD,则当子进程停止(收到 SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU)或继续执行(收到 SIGCONT)时不接收通知(见 wait(2))。仅在为 SIGCHLD 建立处理函数时有意义。

SA_NOCLDWAIT: 若 signum 为 SIGCHLD,则子进程终止时不转为僵尸进程。另见 waitpid(2)。该标志仅在为 SIGCHLD 建立处理函数或将其处置设为 SIG_DFL 时有意义。当为 SIGCHLD 设置处理函数时同时启用 SA_NOCLDWAIT,POSIX.1 对“子进程终止时是否生成 SIGCHLD”未作规定。Linux 会生成 SIGCHLD;某些其他实现不会。

SA_NODEFER: 在处理函数执行期间,不自动阻塞当前正在处理的这个信号。仅在建立信号处理函数时有意义。SA_NOMASK 是其过时且非标准的同义名。

SA_ONSTACK: 在由 sigaltstack(2) 提供的备用信号栈上调用处理函数;若备用栈不可用,则使用默认栈。仅在建立信号处理函数时有意义。

SA_RESETHAND: 进入处理函数时将该信号动作恢复为默认动作。仅在建立信号处理函数时有意义。SA_ONESHOT 是其过时且非标准的同义名。

SA_RESTART: 提供与 BSD 信号语义兼容的行为,使某些系统调用在被信号打断后可自动重启。仅在建立信号处理函数时有意义。系统调用重启讨论见 signal(7)。

SA_RESTORER: 不供应用程序使用。C 库使用该标志表示 sa_restorer 字段中保存了“signal trampoline”的地址。详见 sigreturn(2)。

SA_SIGINFO: 处理函数接收三个参数,而不是一个。此时应设置 sa_sigaction 而不是 sa_handler。仅在建立信号处理函数时有意义。


(3) sa_sigaction 回调介绍

void (*sa_sigaction)(int sig, siginfo_t *info, void *ucontext);

三个参数含义如下:

sig: 触发该处理函数的信号编号。

info: 指向 siginfo_t 的指针,结构中含有该信号的更多信息。siginfo_t 数据类型结构如下:

siginfo_t {int      si_signo;     /* 信号编号 */int      si_errno;     /* errno 值 */int      si_code;      /* 信号代码 */int      si_trapno;    /* 触发硬件信号的陷阱号(多数架构未使用) */pid_t    si_pid;       /* 发送进程 PID */uid_t    si_uid;       /* 发送进程真实 UID */int      si_status;    /* 退出值或信号 */clock_t  si_utime;     /* 消耗的用户态时间 */clock_t  si_stime;     /* 消耗的内核态时间 */sigval_t si_value;     /* 信号值 */int      si_int;       /* POSIX.1b 信号值 */void    *si_ptr;       /* POSIX.1b 信号值 */int      si_overrun;   /* 定时器超限次数;POSIX.1b 定时器 */int      si_timerid;   /* 定时器 ID;POSIX.1b 定时器 */void    *si_addr;      /* 导致故障的内存地址 */long     si_band;      /* band 事件*/int      si_fd;        /* 文件描述符 */short    si_addr_lsb;  /* 地址最低有效位 */void    *si_lower;     /* 地址越界时下界 */void    *si_upper;     /* 地址越界时上界 */int      si_pkey;      /* 引发故障的 PTE 保护键(Linux 4.6 起) */void    *si_call_addr; /* 系统调用指令地址 */int      si_syscall;   /* 尝试调用的系统调用号 */unsigned int si_arch;  /* 尝试调用时的架构 */
}

这里重点说下 si_code 字段:

a. si_code 是一个"值"(不是位掩码),用于说明信号产生原因。对 ptrace(2) 事件,si_code 会包含 SIGTRAP。
b. 对普通信号,si_code 可取以下值 SI_USER: kill(2); SI_KERNEL: 由内核发送; SI_QUEUE: sigqueue(3); SI_TIMER: POSIX 定时器到期; SI_MESGQ: POSIX 消息队列状态变化, 见 mq_notify(3)。SI_ASYNCIO: 异步 I/O 完成; SI_SIGIO: 排队的 SIGIO。
c. SIGILL 的 si_code 可取:ILL_ILLOPC 非法操作码。ILL_ILLOPN 非法操作数。ILL_ILLADR 非法寻址模式。ILL_ILLTRP 非法陷阱。ILL_PRVOPC 特权操作码。ILL_PRVREG 特权寄存器。ILL_COPROC 协处理器错误。ILL_BADSTK 内部栈错误。
d. SIGFPE 的 si_code 可取:FPE_INTDIV 整数除零。FPE_INTOVF 整数溢出。FPE_FLTDIV 浮点除零。FPE_FLTOVF 浮点上溢。FPE_FLTUND 浮点下溢。FPE_FLTRES 浮点结果不精确。FPE_FLTINV 浮点非法操作。FPE_FLTSUB 下标越界。
e. SIGSEGV 的 si_code 可取:SEGV_MAPERR 地址未映射到对象####。SEGV_ACCERR 对映射对象的访问权限无效。SEGV_BNDERR 地址边界检查失败。SEGV_PKUERR 被内存保护键拒绝访问。见 pkeys(7),对应保护键可通过 si_pkey 获取。
f. SIGBUS 的 si_code 可取:BUS_ADRALN 地址对齐无效。BUS_ADRERR 物理地址不存在。BUS_OBJERR 对象相关硬件错误。BUS_MCEERR_AR 在机器检查中消费到硬件内存错误;需要处理。BUS_MCEERR_AO: 进程中检测到硬件内存错误但未消费,可选处理。
g. SIGTRAP 的 si_code 可取:TRAP_BRKPT 进程断点。TRAP_TRACE 进程跟踪陷阱。TRAP_BRANCH 进程分支陷阱。TRAP_HWBKPT 硬件断点/观察点。
h. SIGCHLD 的 si_code 可取:CLD_EXITED 子进程已退出。CLD_KILLED 子进程被杀死。CLD_DUMPED 子进程异常终止。CLD_TRAPPED 被跟踪子进程触发陷阱。CLD_STOPPED 子进程已停止。CLD_CONTINUED 停止的子进程已继续执行。
i. SIGIO/SIGPOLL 的 si_code 可取:POLL_IN 可读数据可用。POLL_OUT 输出缓冲可用。POLL_MSG 输入消息可用。POLL_ERR I/O 错误。POLL_PRI 高优先级输入可用。POLL_HUP 设备已断开。
j. SIGSYS 的 si_code 可取:SYS_SECCOMP 由 seccomp(2) 过滤规则触发。

ucontext: 指向 ucontext_t 结构的指针。该结构包含内核保存在用户栈上的信号上下文####信息;详见 sigreturn(2)。关于 ucontext_t 的更多说明见 getcontext(3)。实际上很多处理函数不会使用第三个参数。


3. 实战注意点

(1) 信号处理函数里尽量只做“异步信号安全”操作。例如设置全局标志、调用 write;避免 printf、malloc、pthread_mutex_lock 等。
(2) 全局标志建议用 volatile sig_atomic_t;
(3) 多线程程序要配合 pthread_sigmask 统一信号接收线程;
(4) 不能捕获或忽略 SIGKILL、SIGSTOP;


二、signation实验

1. 使用 sa_handler() 回调

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>static volatile sig_atomic_t g_stop = 0;void on_sigint(int signo) {(void)signo;g_stop = 1;  //只做轻量、异步信号安全操作
}int main(void) {struct sigaction sa;memset(&sa, 0, sizeof(sa));sa.sa_handler = on_sigint;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART;  //被信号中断的部分系统调用自动重启if (sigaction(SIGINT, &sa, NULL) == -1) {perror("sigaction");return 1;}printf("Press Ctrl+C to stop...\n");while (!g_stop) {/* 让当前线程休眠,直到信号到来唤醒 */pause();}printf("Graceful exit.\n");return 0;
}


2. 使用 sa_sigaction() 回调

#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>void on_sigusr1(int signo, siginfo_t *info, void *ucontext) {ucontext_t *ctx = (ucontext_t *)ucontext;
#if defined(__x86_64__)printf("recv signal=%d from pid=%d uid=%d, si_signo=%d, si_code=%d, RSP=0x%llx, RBP = 0x%llx\n",signo, info->si_pid, info->si_uid, info->si_signo, info->si_code,/* 只有代码中define了 _GNU_SOURCE 或 gcc 加了 -D_GNU_SOURCE 才能有下面两个宏 */(unsigned long long)ctx->uc_mcontext.gregs[REG_RSP],(unsigned long long)ctx->uc_mcontext.gregs[REG_RBP]);
#elif defined(__aarch64__)printf("recv signal=%d from pid=%d uid=%d, PC=0x%llx, SP = 0x%llx\n",signo, info->si_pid, info->si_uid,(unsigned long long)ctx->uc_mcontext.pc,(unsigned long long)ctx->uc_mcontext.sp);
#endif
}int main(void) {struct sigaction sa;memset(&sa, 0, sizeof(sa));sa.sa_sigaction = on_sigusr1;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_SIGINFO;if (sigaction(SIGUSR1, &sa, NULL) == -1) {perror("sigaction");return 1;}/* 休眠直到被信号唤醒,这里设置为被信号唤醒后再次进入休眠 */while (1) pause();
}/*~$ ps -e | grep qq
41010 pts/12   00:00:00 qq
$ kill -10 41010$ ./qq
recv signal=10 from pid=59699 uid=1028, si_signo=10, si_code=0, RSP=0x7ffc4cfcc798, RBP = 0x7ffc4cfcc840*/

 

 

参考:
man sigaction
chatGPT