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

news2024/11/20 20:36:03

日常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内核完全注释》

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于springboot宠物领养系统

摘要 随着社会的不断发展和人们生活水平的提高&#xff0c;宠物在家庭中的地位逐渐上升&#xff0c;宠物领养成为一种流行的社会现象。为了更好地管理和促进宠物领养的过程&#xff0c;本文基于Spring Boot框架设计和实现了一套宠物领养系统。该系统以用户友好的界面为特点&…

游戏开发丨基于Tkinter的扫雷小游戏

文章目录 写在前面扫雷小游戏需求分析程序设计程序分析运行结果系列文章写在后面 写在前面 本期内容 基于tkinter的扫雷小游戏 所需环境 pythonpycharm或anaconda 下载地址 https://download.csdn.net/download/m0_68111267/88790713 扫雷小游戏 扫雷是一款广为人知的单…

【SpringSpringBoot】概述

Spring&SpringBoot专题 【注】&#xff1a; 本专题围绕框架核心概念展开&#xff0c;渐进式深入总结学习、面试、开发经验&#xff0c;集中整理便于回顾 持续补充与施工中~~~~ 1.发展史 2.基本架构 Spring框架的基本架构是一个分层架构&#xff0c;包括多个模块&#x…

漏洞原理反射型XSS漏洞

漏洞原理XSS漏洞 1 反射型XSS php基础链接 Web渗透编程语言基础-CSDN博客 正常思维 http://127.0.0.1/websec/day01/xss_reflect.php?name%E6%88%91%E6%98%AF%E8%B0%81 http://127.0.0.1/14_WEBSEC/DAY01/xss_reflect.php?name我是谁 黑客思维 http://127.0.0.1/websec…

【Python基础017】Python中如何进行异常判断(try...except...的使用)

1、异常判断 在python程序在运行的过程中可能会出现很多错误&#xff0c;比如语法、未定义变量、分母为0等错误&#xff1b;而我们通常使用try...except...语句来处理程序在运行中出现的这些异常&#xff0c;并显示出现错误的原因。此外&#xff0c;我们还可以用try...finally.…

Java多线程基础-18:线程安全的集合类与ConcurrentHashMap

Java标准库提供了很多集合类&#xff0c;但有一些集合类是线程不安全的&#xff0c;也就是说&#xff0c;在多线程环境下可能会出问题的。常用的ArrayList&#xff0c;LinkedList&#xff0c;HashMap&#xff0c;PriorityQueue等都是线程不安全的&#xff08;Vector, Stack, Ha…

AI技术大揭秘:探索人工智能的核心领域与必备技能

随着人工智能的不断进步&#xff0c;AI技术在各个领域都发挥着越来越关键的作用。想要成为AI领域的从业者&#xff0c;不仅需要对整体格局有清晰认识&#xff0c;更要掌握关键技术和必备技能。本文将深入解析AI的核心技术领域&#xff0c;以及在这个前沿领域里需要掌握的技能。…

java 基础学习1

目录 一.注释 二.关键字 三.字面量 四.变量和标识符 五.键盘录入 六.运算符 一.注释 1.单行注释&#xff1a;//注释信息 2.多行注释&#xff1a;/* 注释信息*/ 3.文档注释&#xff1a;/** 注释信息*/ 注:文档注释暂时用不上 二.关键字 关键字: 被Java赋予了特定…

任意一个函数都可以写成一个奇函数与一个偶函数之和的形式

定义 张宇30讲明确指出 f(x)f(-x)必定是偶函数 f(x)-f(-x)必定是奇函数 前提&#xff1a;定义域关于原点对称 由上述结论得到&#xff1a;任意一个函数都可以写成一个奇函数与一个偶函数之和的形式&#xff0c;即使该函数是非奇非偶的。 f(x) 1 2 \frac 12 21​[f(x) f(-x)]…

麒麟系统—— openKylin 安装 mongodb

麒麟系统—— openKylin 安装 mongodb 一、准备工作1. 确保麒麟系统 openKylin 已经安装完毕。 二、下载解压 MongoDB二、增加环境变量三、配置MongoDB创建数据目录创建日志文件运行 四、加入到服务中 MongoDB是一款高性能、开源的NoSQL数据库&#xff0c;因其灵活的数据结构、…

DCNv4:对视觉应用的动态和稀疏算子的重新思考

摘要 https://arxiv.org/pdf/2401.06197.pdf 我们介绍了可变形卷积v4&#xff08;DCNv4&#xff09;&#xff0c;这是一种高效且有效的运算符&#xff0c;专为广泛的视觉应用而设计。DCNv4解决了其前身DCNv3的局限性&#xff0c;通过两个关键改进&#xff1a;1. 去除空间聚合中…

java抽象类概述——abstract关键字用法

前言&#xff1a; 打好基础&#xff0c;daydayup! 抽象类 抽象类概述 在java中有一个关键字叫abstract&#xff0c;代表抽象的意思&#xff0c;可用abstract修饰类&#xff0c;成员方法。 抽象类修饰方法 在方法或类前加上abstract关键字 例1&#xff1a;修饰类&#xff08;在…

AI决策的解构与实践:初探可解释性技术(XAI)

随着人工智能&#xff08;AI&#xff09;技术在各个领域的广泛应用&#xff0c;解释性人工智能&#xff08;XAI&#xff09;的概念备受瞩目。作为开发者&#xff0c;我们深知AI系统的复杂性&#xff0c;以及对于用户和利益相关者来说理解AI决策过程的重要性。本文将深入探讨可解…

跟着pink老师前端入门教程-day12

二十六、HTML5CSS3的提高 1、HTML5 的新特性 HTML5 的新增特性主要是针对于以前的不足&#xff0c;增加了一些新的标签、新的表单和新的表单属性等 这些新特性都有兼容性问题&#xff0c;基本是 IE9 以上版本的浏览器才支持&#xff0c;如果不考虑兼容性问题&#xff0c;可以…

MyBatis详解(5)-- MyBatis注解

MyBatis详解&#xff08;5&#xff09; 注解映射器xml配置文件的缺陷&#xff1a;常用注解1.基本注解&#xff1a;实现简单的增删改查操作。Insert 新增Options(useGeneratedKeys true, keyProperty "主键属性") 主键回填SelectKey ( statement "自增规则&qu…

漏洞复现-EduSoho任意文件读取漏洞(附漏洞检测脚本)

免责声明 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的…

强化合作!浪潮信息携手业界伙伴筑牢算力底座

以太平金融科技服务&#xff08;上海&#xff09;有限公司&#xff08;以下简称“太平金科”&#xff09;为例&#xff0c;在算力新型基础设施建设方面&#xff0c;该公司一直不遗余力。近日&#xff0c;该公司更携手全球领先的IT基础设施供应商浪潮信息&#xff0c;优化算力基…

腾讯云幻兽帕鲁服务器创建教程,附4核16G服务器价格表

腾讯云0基础搭建帕鲁服务器4C16G14M服务器稳定无卡顿&#xff0c;先下载SteamCMD&#xff0c;并运行&#xff1b;然后下载Palserver&#xff0c;修改服务ini配置&#xff0c;启动PalServer&#xff0c;进入游戏服务器。腾讯云百科txybk.com分享腾讯云创建幻兽帕鲁服务器教程&am…

【Apollo CyberRT】源码分析之 “component” 模块

代码位置 apollo/cyber/component 功能 在自动驾驶系统中&#xff0c;模块&#xff08;如感知、定位、控制系统等&#xff09;在 Cyber ​​RT 下以 Component 的形式存在。不同 Component 之间通过 Channel 进行通信。Component 概念不仅解耦了模块&#xff0c;还为将模块拆…

Maven入门及其使用

目录 一、Maven入门 1.1 初识Maven 1.2 Maven的作用 1.2.1 依赖管理 1.2.2 统一项目结构 1.2.3 项目构建 1.3 Maven坐标 1.4 Maven仓库 1.4.1 Maven仓库概述 二、Maven的下载与安装 2.1 安装步骤 2.1.1 解压安装&#xff08;建议解压到没有中文、特殊字符的路径下。&#xff09…