linux0.11源码看信号的处理流程

日常Linux写代码或者使用中难免会使用siganl,包括我们使用ctrl-c结束程序,使用kill命令发送信号,或者说程序core后操作系统向程序发送的信号,以及我们程序内部自定义的信号处理。

我们选择linux0.11一个原因是它比较简单,而且也可以表达出来信号处理的大致原理。

但是信号的处理流程是怎样的呢?这也比较困惑我,去源码学习一下。

首先提出三个疑问:

  1. 信号的处理函数是否在该程序的线程上运行呢?
  2. 是在内核态运行还是用户态运行?
  3. 运行的时机是怎么样呢?是会中断正在运行的程序,还是说执行完某个函数呢?或者其他形式

让我们带着这些问题来继续向下看

示例程序

下边我们展示下示例程序来帮助大家解答:

// sig.cpp

#include <iostream>
#include <csignal>
#include <thread>
#include <boost/stacktrace.hpp>

void loop() {
    std::cout << "loop this thread:" << std::this_thread::get_id() << std::endl;
    for(;;) {}
}

void doSignal(int sig) {
    std::cout << "signal func:" << sig << ", this thread:" << 
        std::this_thread::get_id() << std::endl;
    std::cout << boost::stacktrace::stacktrace() << std::endl;
}

int main() {
    signal(11, doSignal);
    loop();

    return 0;
}

代码很简单,设定11这个信号值的信号处理函数是doSignal,然后让程序处于死循环,循环中打印该线程的线程id,doSignal中也会答应线程id,及函数调用栈。(这里我们使用boost库来打印函数调用栈)

接下来我们编译运行该程序:

$ gcc sig.cpp -o sig -g -rdynamic
$ ./sig
loop this thread:1
signal func:11, this thread:1

可以看到信号的运行线程和loop的线程是同一个。
然后我们在命令行中向该程序发送11的信号:

$ kill -11 `pidof sig`

然后看下打印的函数调用栈:

 0# doSignal(int) in ./sig
 1# 0x00007FAAB4C0A090 in /lib/x86_64-linux-gnu/libc.so.6
 2# loop() in ./sig
 3# main in ./sig
 4# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 5# _start in ./sig

所以由上可知,信号处理程序是运行在主程序的用户态下的线程中,且会中断我们程序去调用信号处理函数。

运行的线程这个倒是不奇怪了,但是运行在自己线程且还会中断我们正在运行的函数,这一点确实不常见的,我们去linux0.11源码分析下,当然如果不想深入了解到这里也是能够解答上边的疑惑。

源码分析

基本数据结构

union __sigaction_u {
	void    (*__sa_handler)(int);
	void    (*__sa_sigaction)(int, struct __siginfo *,
	    void *);
};

struct  sigaction {
	union __sigaction_u __sigaction_u;  /* signal handler */
	sigset_t sa_mask;               /* signal mask to apply */
	int     sa_flags;               /* see signal options below */
};

以上的数据结构就是用来存放信号处理的,sigaction对象存在于表示进程的结构体(task_struct),然后sigaction中的__sa_handler就是信号处理函数。

捕获信号

注册信号处理函数,也即覆盖默认信号处理操作,我们这里简单起见,使用signal函数来分析,signal函数对应于sys_signal,因为调用signal函数中间会设计libc的库函数,所以sys_signal参数和signal的参数略有不同,我们可以忽略:

int sys_signal(int signum, long handler, long restorer)
{
	struct sigaction tmp;

	tmp.sa_handler = (void (*)(int)) handler;
	tmp.sa_mask = 0;
	tmp.sa_flags = SA_ONESHOT | SA_NOMASK;
	tmp.sa_restorer = (void (*)(void)) restorer;        // 保存恢复处理函数指针
    
	handler = (long) current->sigaction[signum-1].sa_handler;
	current->sigaction[signum-1] = tmp;
	return handler;
}

可以看到我们使用signum,handlerrestorer参数首先构造一个sigaction,然后赋值给当前进程(current)。其他的细节可以忽略。

发送信号

我们在命令行中发送信后使用的是kill指令,同样对应于内核的函数是sys_kill函数:

int sys_kill(int pid,int sig)
{
	struct task_struct **p = NR_TASKS + task;
	int err, retval = 0;

    // ...

    if ((err=send_sig(sig,*p,1)))
        retval = err;

    // ...
	
	return retval;
}

代码很简单,这里p代码进程数据结构对象,调用send_sig函数向该进程发送信号sig

static inline int send_sig(long sig,struct task_struct * p,int priv)
{
	if (priv || (current->euid==p->euid) || suser())
		p->signal |= (1<<(sig-1));
	else
		return -EPERM;
	return 0;
}

经过系列判断,最终给p->signal赋值,表示该进程收到了number为sig的信号,这里的p->signal是一个位图,用来标识收到的是哪个信号。

信号处理

首先要知道信号处理的时机,也就是什么时候去处理收到的信号呢,因为信号实时性较高,所以linux内核是在这两种情况下进行信号处理:

  • 程序进行系统调用执行后
  • 一些中断处理后
    这块代码是汇编,所以我们只需要简单了解原理即可:
system_call:
	# ...
	call sys_call_table(,%eax,4)  # 间接调用指定功能C函数
	# ...

ret_from_sys_call:
	# ...
	movl signal(%eax),%ebx 
	movl blocked(%eax),%ecx
	notl %ecx
	andl %ebx,%ecx   # 获得许可信号位图
	bsfl %ecx,%ecx
	je 3f            # 如果没有信号则向前跳转退出
	# ...
	pushl %ecx       # 信号值入栈作为调用do_signal的参数之一
	call do_signal   # 调用C函数信号处理程序(kernel/signal.c)
	# ...
	iret

以上是简单的系统调用的代码,首先会去执行真正的系统调用sys_call_table,然后系统调用完成后就会到ret_from_sys_call中,ret_from_sys_call会调用do_signal函数,在这之前先找到一个要处理的信号数值作为参数传递给do_signal并调用。

同样在一些中断也会调用到这里ret_from_sys_call

timer_interrupt:
	movl CS(%esp),%eax
	andl $3,%eax		# %eax is CPL (0 or 3, 0=supervisor)
	pushl %eax
	call do_timer		# 'do_timer(long CPL)' does everything from
	addl $4,%esp		# task switching to accounting ...
	jmp ret_from_sys_call

我们这里关注的是定时器中断执行完成后会执行ret_from_sys_call,这说明什么呢?定时器中断被调用时回去调用do_timer函数,进一步又会去调用schedule函数,也就是进程会被切换。那中断这里返回意味着什么呢,是说该进程被重新调度时会去做信号处理。这里可以花一秒钟思考下。

接下来就去看do_signal函数:

void do_signal(long signr,long eax, long ebx, long ecx, long edx,
	long fs, long es, long ds,
	long eip, long cs, long eflags,
	unsigned long * esp, long ss)
{
	unsigned long sa_handler;
	long old_eip=eip;
	struct sigaction* sa = current->sigaction + signr - 1;
	int longs;
	unsigned long * tmp_esp;

	sa_handler = (unsigned long) sa->sa_handler;
	if (sa_handler==1) // 忽略
		return;
	if (!sa_handler) { // 默认
		if (signr==SIGCHLD)
			return;
		else
			do_exit(1<<(signr-1));      // 不再返回到这里
	}

	*(&eip) = sa_handler;

    // 调整用户栈esp
	longs = (sa->sa_flags & SA_NOMASK)?7:8;
	*(&esp) -= longs;
	verify_area(esp,longs*4);

    // 在用户堆栈中从下道上存放sa_restorer、信号signr、屏蔽码blocked(如果SA_NOMASK
    // 置位)、eax,ecx,edx,eflags和用户程序原代码指针。
	tmp_esp=esp;
	put_fs_long((long) sa->sa_restorer,tmp_esp++);
	put_fs_long(signr,tmp_esp++);
	if (!(sa->sa_flags & SA_NOMASK))
		put_fs_long(current->blocked,tmp_esp++);
	put_fs_long(eax,tmp_esp++);
	put_fs_long(ecx,tmp_esp++);
	put_fs_long(edx,tmp_esp++);
	put_fs_long(eflags,tmp_esp++);
	put_fs_long(old_eip,tmp_esp++);

	current->blocked |= sa->sa_mask;  // 进程阻塞码(屏蔽码)添上sa_mask中的码位。
}

代码稍微有点长,我们打起精神一点点的来分析下。这段代码还是很深奥的,我们知道当执行系统调用或者中断时,会将用户态的程序暂停,这样用户态程序的寄存器就会被压栈,直到返回用户态时恢复。且用户态的寄存器时压到内核栈里,这样在内核执行完后就可以直接恢复。
首先参数很多,我们简单看下参数:

  • signr是在调用do_signal前取出来的信号值,被最后压栈
  • 调用sys_call_table后压入栈中的相应系统调用处理函数的返回值(eax)
  • 后边则是执行系统调用或者中断压栈进来的值

下边时函数内部的执行逻辑;

  1. 然后获取到指定要处理的信号的sigaction的sa_handler,判断处理是被忽略还是默认行为,如果不是就是被用户捕获则继续。
  2. *(&eip) = sa_handler;是修改内核栈中压入的ip执行的位置为sa_handler,也就是从内核态返回时直接到sa_handler函数执行,而不是之前的位置,那么之前的位置呢,我们继续看。
  3. 接下来就是将用户态栈的空间变大,put_fs_long这里则是向用户态的栈空间压入寄存器的值,供sa_handler函数使用。
  4. 最后将old_eip也压入到用户态栈中,也就是说执行完sa_handler就回继续执行系统调用之前的位置的代码。

调用siganl设定信号处理函数时,首先会调用掉系统库的响应函数,然后才会到系统调用那里,也就是说这里的sa_handler应该是系统库的函数,系统库的handler再去调用你自己设定的处理函数,所以调用sa_handler的参数和你自己的处理函数参数有些不同,因为系统库还需要做额外的工作。

以上总结来说就是,do_signal函数会把之前回到用户态要执行位置换成了sa_handler,然后sa_handler执行完之后再继续执行,这样大家就明白了为什么我的死循环明明没有调用任何函数,栈却显示从loop那里调用到doSignal函数。以下是这个流程的图示:

总结

本文我们从例子出发讲述了信号的处理整体流程,虽然内核的代码已经比较老了,但是总体的流程不变。
我们从信号发射,信号捕获,信号处理等方面分析其流程。

感谢大家,点个赞吧~~

ref

《linux内核完全注释》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/352824.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

程序员如何应对中年危机

中年危机是一个普遍存在的问题&#xff0c;不仅仅局限于程序员这个职业。不过&#xff0c;对于程序员来说&#xff0c;由于技术更新迅速&#xff0c;中年危机可能更加明显。以下是一些应对中年危机的建议&#xff1a; 持续学习新技术和工具&#xff1a;计算机技术发展迅速&…

快快销shop积分商城:全额积分抵扣营销 打造积分换购专区

快快销shop积分商城是一个创新的营销平台&#xff0c;它通过全额积分抵扣的策略&#xff0c;鼓励用户在商城内消费并积累积分。这种营销方式不仅能提升用户的购物体验&#xff0c;还能有效地促进销售。 全额积分抵扣意味着用户在商城内消费时&#xff0c;可以全额使用积分进行…

原生js是怎么创建元素的?

问: <div class"share-img"> <img src"../img/pic_share-tip.png" alt""> </div>原生js怎么创建一个这个元素? 回答: 问: 上面代码执行结果是什么样的? 回答:

攻防演练 |解决Nmap无法扫描B段资产问题

前段时间老大发来任务&#xff0c;让帮忙用nmap扫一些ip段&#xff0c;我拿过来就准备开扫… 但是发现nmap无法直接扫描同一B段不同C段下的IP段&#xff0c;例如111.111.111.0-111.111.222.255 原本我是准备写个工具联动nmap来扫描大批量IP段资产的 但是由于环境有些问题&am…

什么工具能将视频转成gif?分享一个在线制作gif网站

Gif动图看起来效果非常的炫酷&#xff0c;也很复杂。这种gif动图制作起来是不是也很麻烦呢&#xff1f;其实制作gif动画的方法非常的简单&#xff0c;不用下载软件&#xff0c;小白也能操作。只需要使用在线制作gif&#xff08;https://www.gif.cn/&#xff09;工具-GIF中文网&…

【云驻共创】零门槛Serverless课堂 应用全托管 so easy!

前言 一切要从一个风和日丽的早上说起&#xff1a; 那天&#xff0c;阳光正好&#xff0c;微风不燥。还来不及从容吃口早饭&#xff0c;我就接到了线上报警&#xff0c;赶忙打开了电脑&#xff0c;处理突发的流量高峰导致的页面报错。 重启好服务&#xff0c;饭都冷了。 我…

3dmax效果图渲染出现曝光怎么解决?

在使用3ds Max完成效果图渲染工作时&#xff0c;有时会遇到曝光过度的问题&#xff0c;这会使得渲染的图像出现光斑或者过亮&#xff0c;损害了效果的真实感和美观度。那么解决解决3dmax曝光问题呢&#xff1f;一起看看吧&#xff01; 3dmax效果图渲染出现曝光解决方法 1、相机…

使用Opencv-python库读取图像、本地视频和摄像头实时数据

使用Opencv-python库读取图像、本地视频和摄像头实时数据 Python中使用OpenCV读取图像、本地视频和摄像头数据很简单&#xff0c; 首先需要安装Python&#xff0c;然后安装Opencv-python库 pip install opencv-python然后在PyCharm或者VScode等IDE中输入对应的Python代码 一…

web前端---------盒子模型

1.内容 盒子的内容可以包含文字、图片等多种类型。 浏览器在加载网页时&#xff0c;会将元素按照内容区分为替换元素与非替换元素。 &#xff08;1&#xff09;替换元素指的是HTML中的一些形如<img>、<input>等非文本元素。 这些元素本身不包含任何内容&#x…

Spring Cloud 之Config详解

大家好&#xff0c;我是升仔 在微服务架构中&#xff0c;统一的配置管理是维护大规模分布式系统的关键。Spring Cloud Config为微服务提供集中化的外部配置支持&#xff0c;它可以与各种源代码管理系统集成&#xff0c;如Git、SVN等。本文将详细介绍如何搭建配置服务器、管理客…

司铭宇老师:汽车销售培训:汽车销售员培训:汽车销售技巧培训:汽车销售技巧和话术

汽车销售培训&#xff1a;汽车销售员培训&#xff1a;汽车销售技巧培训&#xff1a;汽车销售技巧和话术 汽车销售是一项充满挑战性的工作&#xff0c;它需要销售人员具备良好的沟通技巧、谈判技巧以及产品讲解能力。在这篇文章中&#xff0c;我们将详细探讨汽车销售中的技巧和话…

【MQ02】基础简单消息队列应用

基础简单消息队列应用 在上一课中&#xff0c;我们已经学习到了什么是消息队列&#xff0c;有哪些消息队列&#xff0c;以及我们会用到哪个消息队列。今天&#xff0c;就直接进入主题&#xff0c;学习第一种&#xff0c;最简单&#xff0c;但也是最常用&#xff0c;最好用的消息…

Nginx安装以及具体应用

文章目录 Centos7安装NginxNginx命令Nginx具体应用反向代理 location指令说明负载均衡动静分离 Nginx.conf配置详解 Centos7安装Nginx 下载地址&#xff1a;nginx: download 中间这个就是tar.gz包 Centos7安装Nginx 下载nginx-1.16.1.tar.gz上传到Centos7中的/user/local目…

漏洞攻击中怎么去做最全面覆盖的sql注入漏洞攻击?表信息是如何泄露的?预编译就一定安全?最受欢迎的十款SQL注入工具配置及使用

漏洞攻击中怎么去做最全面覆盖的sql注入漏洞攻击?表信息是如何泄露的?预编译就一定安全?最受欢迎的十款SQL注入工具配置及使用。 SQL注入是因为后台SQL语句拼接了用户的输入,而且Web应用程序对用户输入数据的合法性没有判断和过滤,前端传入后端的参数是攻击者可控的,攻击…

一位网友开始设计一个叫 VisionPro 的VR现实交互操作系统,可以将电脑屏幕的图片拖拽到现实空间摆放

在 Figma 中创建的资源和组件可以在 ShapesXR 中导入和同步&#xff0c;这样您就可以在 Mixedreality 中开始设计&#xff0c;而无需任何 3d 专业技能 Figma是一个矢量图形编辑器和原型设计工具&#xff0c;主要基于网页进行工作&#xff0c;通过macOS或Windows的桌面应用程序…

Docker命令---搜索镜像

介绍 使用docker命令搜索镜像。 命令 docker search 镜像命令:版本号示例 以搜索ElasticSearch镜像为例 docker search ElasticSearch

Redis的应用问题

目录 一、缓存穿透 问题描述 解决方案 缓存击穿 问题描述 解决方案 缓存雪崩 问题描述 解决方案 二、分布式锁 问题描述 解/决方案&#xff1a;使用redis实现分布式锁 优化之设置锁的过期时间 优化之UUID防误删 LUA脚本保证删除的原子性 LUA脚本 LUA脚本在Red…

Dockerfile入门指南:轻松创建定制化Docker镜像

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本&#xff0c;用这个脚本来构建、定制镜像&#xff0c;那么无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。 Doc…

(大众金融)SQL server面试题(3)-客户已用额度总和

今天&#xff0c;面试了一家公司&#xff0c;什么也不说先来三道面试题做做&#xff0c;第三题。 那么&#xff0c;我们就开始做题吧&#xff0c;谁叫我们是打工人呢。 题目是这样的&#xff1a; DEALER_INFO经销商授信协议号码经销商名称经销商证件号注册地址员工人数信息维…

Windows上安装Linux系统

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、WSL是什么&#xff1f;二、WSL安装步骤1.开启wsl支持2.安装wsl3.运行wsl4.环境配置 三、WSL删除引用 前言 提示&#xff1a;这里可以添加本文要记录的大概…