1. Linux Signal 及其处理机制
    1. 1. 流程
    2. 2. 信号处理
      1. 2.1. 信号处理方式
      2. 2.2. 自定义信号处理方式
      3. 2.3. 写一个高级的Signal Handler
    3. 3. ucontext
    4. 4. ref

Linux Signal 及其处理机制

本文距最近一次更新已过去 620 天,部分内容可能已经过时,请结合当前资料核验。

信号(Signal)是一种软中断。信号机制是进程间通信的一种方式,采用异步通信方式。

流程

  1. 应用程序注册信号,信号事件发生后,内核将信号置为pending状态
  2. 在中断返回或者系统调用返回时,查看pending的信号,内核在应用程序的栈上构建一个信号处理栈帧
  3. 然后通过中断返回或者系统调用返回到用户态,执行信号处理函数
  4. 执行信号处理函数之后,再次通过sigreturn系统调用返回到内核
  5. 在内核中再次返回到应用程序被中断打断的地方或者系统调用返回的地方接着运行

faea12e3fcfe5db324a9dc40bb7ed272.png

信号处理

在进程task_struct结构体中有一个未决信号的成员变量 struct sigpending pending。每个信号在进程中注册都会把信号值加入到进程的未决信号集。

内核处理进程收到的signal是在当前进程的上下文,故进程必须是Running状态。当进程唤醒或者调度后获取CPU,则会从内核态转到用户态时检测是否有signal等待处理,处理完,进程会把相应的未决信号从链表中去掉。

信号处理方式

  1. 默认:接收到信号后按默认的行为处理该信号。其中,部分信号无法修改其默认的处理行为,例如SIGKILL等。
  2. 自定义:自定义信号处理函数(Handler)执行特定操作。
  3. 忽略:相当于信号没有发出。部分信号的默认操作就是忽略,例如SIGCHLD等。

自定义信号处理方式

信号处理程序会在接收到信号的线程中执行,而不是在子线程中。也就是说,信号处理程序是在同一线程的上下文中运行的。

signal and sigaction

这两个都是修改信号处理方式的系统调用。

  • void( *signal(int sig,void(*handler)(int))(int)
    • 只能设置信号及其处理Handler,每个Kernel的实现方式可能不同,因此不建议使用。
    • handler赋值为常数SIG_IGN表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号。
  • int sigaction(int sig,const struct sigaction *act,struct sigaction *oldact)
    • act: 指向 sigaction 结构体的指针,该结构体定义了信号的处理方式。如果不需要改变信号处理方式,可以传递 NULL
    • oldact: 指向 sigaction 结构体的指针,用于存储信号的旧处理方式。如果不需要获取旧的处理方式,可以传递 NULL

struct sigaction

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
  • 不要同时赋值给 sa_handlersa_sigaction

  • sa_handler 指定与 signum 相关联的动作

    • 赋值为常数SIG_IGN表示忽略信号
    • 赋值为常数SIG_DFL表示执行系统默认动作
    • 赋值为一个函数指针表示用自定义函数捕捉信号
  • sa_sigaction只有在sa_flags指定了SA_SIGINFO时才能使用,需要的函数原型如下

    1
    void handler(int sig, siginfo_t *info, void *ucontext) { ... }
    • sig 引发处理程序的信号编号
    • info 指向 siginfo_t 的指针,它是一个结构体,包含有关信号的更多信息
    • ucontext 这是一个指向ucontext_t结构体的指针,转换为void *。这个字段指向的结构包含由内核在用户空间栈上保存的信号上下文信息
  • sa_mask字段表示在信号处理程序执行期间应该被屏蔽的任何信号。

  • sa_flags字段确定了几个不同的值,但其中重要的是是否可以获取扩展信息( SA_SIGINFO ),以及是否自动重启被信号中断的系统调用( SA_RESTART )。否则,中断的系统调用将失败,因此自动重启显然是更好的方法。

struct siginfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
typedef struct siginfo {
union {
__SIGINFO;
int _si_pad[SI_MAX_SIZE/sizeof(int)];
};
} __ARCH_SI_ATTRIBUTES siginfo_t;

#ifndef __ARCH_HAS_SWAPPED_SIGINFO
#define __SIGINFO \
struct { \
int si_signo; \
int si_errno; \
int si_code; \
union __sifields _sifields; \
}
#else
#define __SIGINFO \
struct { \
int si_signo; \
int si_code; \
int si_errno; \
union __sifields _sifields; \
}
#endif /* __ARCH_HAS_SWAPPED_SIGINFO */

union __sifields {
/* kill() */
struct {
__kernel_pid_t _pid; /* sender's pid */
__kernel_uid32_t _uid; /* sender's uid */
} _kill;

/* POSIX.1b timers */
struct {
__kernel_timer_t _tid; /* timer id */
int _overrun; /* overrun count */
sigval_t _sigval; /* same as below */
int _sys_private; /* not to be passed to user */
} _timer;

/* POSIX.1b signals */
struct {
__kernel_pid_t _pid; /* sender's pid */
__kernel_uid32_t _uid; /* sender's uid */
sigval_t _sigval;
} _rt;

/* SIGCHLD */
struct {
__kernel_pid_t _pid; /* which child */
__kernel_uid32_t _uid; /* sender's uid */
int _status; /* exit code */
__ARCH_SI_CLOCK_T _utime;
__ARCH_SI_CLOCK_T _stime;
} _sigchld;

/* SIGILL, SIGFPE, SIGSEGV, SIGBUS, SIGTRAP, SIGEMT */
struct {
void *_addr; /* faulting insn/memory ref. */
#ifdef __ARCH_SI_TRAPNO
int _trapno; /* TRAP # which caused the signal */
#endif
#ifdef __ia64__
int _imm; /* immediate value for "break" */
unsigned int _flags; /* see ia64 si_flags */
unsigned long _isr; /* isr */
#endif

#define __ADDR_BND_PKEY_PAD (__alignof__(void *) < sizeof(short) ? \
sizeof(short) : __alignof__(void *))
union {
/*
* used when si_code=BUS_MCEERR_AR or
* used when si_code=BUS_MCEERR_AO
*/
short _addr_lsb; /* LSB of the reported address */
/* used when si_code=SEGV_BNDERR */
struct {
char _dummy_bnd[__ADDR_BND_PKEY_PAD];
void *_lower;
void *_upper;
} _addr_bnd;
/* used when si_code=SEGV_PKUERR */
struct {
char _dummy_pkey[__ADDR_BND_PKEY_PAD];
__u32 _pkey;
} _addr_pkey;
};
} _sigfault;

/* SIGPOLL */
struct {
__ARCH_SI_BAND_T _band; /* POLL_IN, POLL_OUT, POLL_MSG */
int _fd;
} _sigpoll;

/* SIGSYS */
struct {
void *_call_addr; /* calling user insn */
int _syscall; /* triggering system call number */
unsigned int _arch; /* AUDIT_ARCH_* of syscall */
} _sigsys;
};

存储signal发出信号时的信息。

[!NOTE]

这里可以考虑加入timestamp字段以支持记录信号发出时的时间戳。具体记录时间戳的函数位置暂时未知。

以解决ASYNC时顺序倒挂的问题。同时兼用ASYMM性能好的优势。

写一个高级的Signal Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

static void handle_prof_signal(int sig_no, siginfo_t* info, void *context)
{
printf("Done\n");
exit(1);
}

void main()
{
struct sigaction sig_action;
memset(&sig_action, 0, sizeof(sig_action));
sig_action.sa_sigaction = handle_prof_signal;

// 重启被中断的系统调用,获取SIGINFO
sig_action.sa_flags = SA_RESTART | SA_SIGINFO;

sigemptyset(&sig_action.sa_mask);
sigaction(SIGPROF, &sig_action, 0);

struct itimerval timeout={0};
timeout.it_value.tv_sec=1;
setitimer(ITIMER_PROF, &timeout, 0);

do { } while(1);
}

Async-safe函数

异步安全函数是指即使在不同的执行上下文中被调用,也能安全地操作共享资源的函数。许多系统调用在信号处理程序中是不安全的。

使函数在异步环境下安全运行的一种方法是,在函数执行期间阻止信号的到达,并在函数离开关键代码段后才重新启用信号。这可以通过使用sigprocmask()函数来实现。

防止循环调用Signal Handler

主要看ucontext上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <sys/ucontext.h>

static void handle_prof_signal(int sig_no, siginfo_t* info, void *vcontext)
{
char output[100];
ucontext_t *context = (ucontext_t*)vcontext;
unsigned long pc = context->uc_mcontext.gregs[REG_PC];

snprintf(output,100,"Illegal instruction at %lx value %lx\n",pc,*(int*)pc);
write(1,output,strlen(output)+1);

// 修改上下文中的PC和nPC,使得其跳过当前指令继续执行
context->uc_mcontext.gregs[REG_PC] = context->uc_mcontext.gregs[REG_nPC];
context->uc_mcontext.gregs[REG_nPC] = context->uc_mcontext.gregs[REG_nPC]+4;
}


void main()
{
struct sigaction sig_action;
memset(&sig_action, 0, sizeof(sig_action));
sig_action.sa_sigaction = handle_prof_signal;
sig_action.sa_flags = SA_RESTART | SA_SIGINFO;
sigemptyset(&sig_action.sa_mask);
sigaction(SIGILL, &sig_action, 0);

timeout.it_value.tv_sec=1;
setitimer(ITIMER_PROF, &timeout, 0);
asm(".word 0x00000000");
asm(".word 0x00000000");
asm(".word 0x00000000");
asm(".word 0x00000000");
}
  • ucontext_t 结构体用于获取当前执行上下文,包括程序计数器(PC)和其它寄存器。
  • pc 是当前程序计数器的值,指向出错的指令。

不可重入问题

如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。

当捕捉到信号时,不论进程的主控制流程当前执行到哪儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。引入了信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等),就有可能出现冲突。

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

  • 调用了mallocfree,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

ucontext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct ucontext {
unsigned long uc_flags;
struct ucontext *uc_link;
stack_t uc_stack;
sigset_t uc_sigmask;
/* glibc uses a 1024-bit sigset_t */
__u8 __unused[1024 / 8 - sizeof(sigset_t)];
/* last for future expansion */
struct sigcontext uc_mcontext;
};

/*
* Signal context structure - contains all info to do with the state
* before the signal handler was invoked.
*/
struct sigcontext {
__u64 fault_address;
/* AArch64 registers */
__u64 regs[31];
__u64 sp;
__u64 pc;
__u64 pstate;
/* 4K reserved for FP/SIMD state and future expansion */
__u8 __reserved[4096] __attribute__((__aligned__(16)));
};

这里给出访问ARM64的PC和opcode及其基址的代码:

1
2
3
4
5
6
7
8
9
10
struct ucontext *uc = (struct ucontext *)context;
uint64_t pc = uc->uc_mcontext.pc; // 获取程序计数器的值
uint32_t opcode;

// 读取内存中的指令(假设可以安全访问该地址)
opcode = *(uint32_t *)pc; // 从 pc 地址读取 4 字节的指令

// 输出指令和地址
printf("Received signal %d\n", sig);
printf("PC: 0x%llx, Opcode: 0x%x\n", pc, opcode);

由于ARM64的指令定长,可以使用以下方式进行指令的解码:

未完待续

ref

4. 捕捉信号

ucontext的简单介绍 - ink19 - 博客园

linux ucontext族函数的原理及使用_ucontext排查短促-CSDN博客

Linux信号(signal)机制 - Gityuan博客 | 袁辉辉的技术博客

signal(7) - Linux manual page

Linux kernel signal原理_linux kernel 如何 判断 收到的是什么 signal-CSDN博客

The Linux kernel: Signals

sigaction(2) - Linux manual page

Signals - HackMD

How to Write Advanced Signal Handlers