玩透Linux信号机制

系统 Linux
syscall进入内核,把该干的事情干完,就执行sysret返回用户态。返回到哪里?rcx中内存地址的位置,这个位置就是信号处理函数。执行完信号处理函数,pop出栈底元素,跳过去,这个位置就是CPU执行syscall进入内核后面的哪一行代码的内存地址,从而实现接着执行。

1.信号处理函数

如果我们想捕捉进程的这两个信号:SIGCHLD、SIGCONT,用到函数sigaction

图片图片

Linux内核提供了多少种信号呢?shell终端运行shell -l即可看到,64种。我们代码中捕捉的两个信号,分别是17号、18号信号,这两个数字记一下,等下查看内核数据能看到

图片图片

先在内核中找到我们注册的信号处理函数,再说底层实现原理。这个你得自己写内核驱动程序,市面上没有任何工具可以让你看

图片图片

找到了。再确定一下我们通过函数sigaction注册的信号处理函数地址是不是这两个,一毛一样

图片图片

接下来说说Linux内核是如何存储我们注册的信号处理函数

struct task_struct {
    struct sighand_struct		*sighand;
    ……
}

struct sighand_struct {
	spinlock_t		siglock;
	refcount_t		count;
	wait_queue_head_t	signalfd_wqh;
	struct k_sigaction	action[_NSIG];
};

struct k_sigaction {
	struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
	__sigrestore_t ka_restorer;
#endif
};

struct sigaction {
#ifndef __ARCH_HAS_IRIX_SIGACTION
	__sighandler_t	sa_handler;
	unsigned long	sa_flags;
#else
	unsigned int	sa_flags;
	__sighandler_t	sa_handler;
#endif
#ifdef __ARCH_HAS_SA_RESTORER
	__sigrestore_t sa_restorer;
#endif
	sigset_t	sa_mask;	/* mask last for extensibility */
};

Linux内核中,每个进程对应一个task_struct实例,里面有个属性sighand就是用来存储你使用函数sigaction注册的信号处理函数,具体存储在sighand_struct的action数组中,数组的索引就是信号的编号:1-64,数组的值是k_sigaction实例,真正存放信号处理函数的地方是sigaction.sa_handler

所以如果你想查看Linux内核中,某个进程注册的所有信号处理函数,代码这样写即可

图片图片

至此,我们写代码注册的信号处理函数,在内核中如何存储的,就彻底搞明白了。那内核是何时、怎么调用这个函数的呢?接着走……

2.kill-18

比如我们通过kill -18向进程18226发送信号,中间发生了什么?我就不贴源码了,直接单步调试内核,贴调用栈吧

图片图片

这里看到的只是内核态的调用栈,用户态的,kill命令底层调用的就是glibc库中的kill函数,而kill函数则是通过syscall+kill的内核调用号,进入内核,调用相关函数

图片图片

关于用户态切内核态,CPU提供了四个门、两个快速调用,以前的实现方式是0x80中断门,现在都是走syscall快速调用。如果你非科班,或者没学过操作系统,应该没听过这个,或者对这个没概念。建议非科班出身的小伙伴,一定要把操作系统补一下

那这个信号在内核中是如何存储的呢?核心逻辑在send_signal中,我就不贴代码了,直接说它做了什么吧

struct task_struct {
    struct signal_struct		*signal;
    sigset_t			blocked;
    ……
}

struct signal_struct {
    /* shared signal handling: */
	struct sigpending	shared_pending;
    ……
}

struct sigpending {
	struct list_head list;
	sigset_t signal;
};

struct sigqueue {
	struct list_head list;
	int flags;
	kernel_siginfo_t info;
	struct user_struct *user;
};

进入内核的时候,信号会被包装成kernel_siginfo

图片图片

真正与进程关联起来的步骤是,将kernel_siginfo再包装成sigqueue,然后将sigqueue实例挂到sigpending中,进程结构体task_struct中有个属性shared_pending就是链表头,有点抽象,看图

图片图片

顺便说一下,Linux内核中的64种信号分成两个阵营:可靠信号与不可靠信号,可靠信号又叫实时信号,不可靠信号又叫非实时信号

图片图片

实时与非实时,表达的不是立刻去做的意思,是指信号不会丢失。这名字起的真让人容易产生误解。Linux内核中的很多函数名也是,比如get_signal,它里面做了很多重要的事情,我看代码的时候以为就是去信息相关信息

嗯,差不多就这些。至此,信号在内核中是如何存储的就清晰了。那进程何时处理信号,Linux内核是如何设计的呢?

3.信号处理

如果这块由你来设计,你会怎么做?不知道?好吧……

Linux内核是如何设计的呢?它的设计是在进程由内核态返回用户态的路径上实现的。为什么要这么做呢?

因为要兼容运行用户态注册的信号处理函数,这个节点是最优选择。反正都要进入用户态执行,在这之前,顺便把信号处理函数执行了。这里要怎么实现呢?改线程栈结构,你如果学了我讲的汇编,你就知道要怎么改了。

同样,不贴代码了,直接单步调试Linux内核看吧!

图片图片

函数do_signal就是信号处理的核心函数

接下来详细分析代码层面实现,如果你不会汇编,你可能就看不懂了

4.执行信号处理函数

什么时候CPU会由用户态进入内核态呢?发生中断、异常,还有比较常见的:系统调用。比如write函数

图片图片

当CPU执行syscall指令,CPU就进入Linux内核中了,这时候用户态的栈比如是这样(我就画关键信息了哦)

图片图片

如果有信号需要处理的时候,这时候用户又设置了信号处理函数,那内核在回用户态前会把栈改成这样

图片图片

会把rcx设置为信号处理函数的地址,syscall进入内核,配套的返回指令是sysret,会返回到rcx中内存地址的位置执行代码。所有的函数的最后一条指令都是ret,会pop出栈顶元素,并跳转到那个内存地址开始执行代码。

syscall进入内核,把该干的事情干完,就执行sysret返回用户态。返回到哪里?rcx中内存地址的位置,这个位置就是信号处理函数。执行完信号处理函数,pop出栈底元素,跳过去,这个位置就是CPU执行syscall进入内核后面的哪一行代码的内存地址,从而实现接着执行。这样就完成了进入用户态,顺便执行信号处理函数的动作。怎么样,是不是特别有智慧!

责任编辑:武晓燕 来源: 硬核子牙
相关推荐

2011-07-05 18:40:19

QT 信号 机制

2011-07-05 18:32:52

QT 信号 机制

2021-12-10 00:01:53

Vsync信号机制

2022-11-03 07:35:47

OS内核异步

2017-01-16 15:05:17

Linux信号机制分析

2017-01-16 14:48:42

Linux信号机制分析

2017-09-14 09:40:32

PythonUbuntu信号机制

2011-06-09 09:45:35

Linux QT 信号

2022-10-08 08:34:34

JVM加载机制代码

2024-07-25 11:53:53

2010-09-09 14:15:08

无线网络信号

2011-06-23 14:40:13

Qt 信号

2009-11-23 19:47:57

ibmdwLinux

2010-12-27 10:13:05

PROC文件系统

2024-05-08 13:41:31

OpenAIA/B测试

2011-06-13 10:21:25

QT 信号 槽机制

2016-08-16 08:26:19

Linuxsignalsigaction

2020-11-05 09:59:24

Linux内核信号量

2020-10-18 07:13:44

Linux系统编程信号捕捉

2020-11-16 07:15:41

Linux权限管理
点赞
收藏

51CTO技术栈公众号