Linux信号机制-3

news2024/10/7 15:29:41

转自:深入理解Linux内核——signals | linkthinking

信号很早就在 unix 系统中出现了,它用于用户进程之间的交互,几十年以来,变化都不大。信号是一个发送给进程或者进程组的消息,它只是一个数字,没有参数或者其他辅助的信息。以 SIG 为前缀的宏定义表示着这些不同的信号数字。进程的信号主要有2个目的:

  • 告知进程一个特定的事件产生了
  • 触发进程去执行程序代码中处理信号的 signal handler

信号的最大特点是它是异步的,也就是在进程执行的任意时刻信号都可能产生,此时进程的状态是不可知的。发送给进程的信号如果没有被执行,就会由内核将它保存起来,直到进程被唤醒开始执行。如果某个信号被进程配置为屏蔽,那么这个信号就会一直被挂起而不会被触发,直到它被 unblock。

Linux 内核对信号传递过程中的2个阶段进行了区分:

  • signal generation: 产生信号,此时由内核在目标进程中的对应数据结构中更新信号的状态,也就是挂个名先
  • signal delivery: 信号被进程响应,内核将进程的执行流转到信号触发的 signal handler 中

信号在进程中已经生成,不过还没有被响应的情况,我们称之为 pending signal。同一个类型的信号只会存在一个被挂起的情况,所以也就是在信号没有被响应前,重复发送多次的信号,进程也最终也只会响应一次。通常情况下,一个信号会被阻塞多久时间是不可知的,所以需要考虑信号的这些特点:

  • 进程只有在处于运行状态的时候才能够响应信号
  • 发送给进程的信号可能被进程主动选择屏蔽,这种情况下进程将不会响应这个信号
  • 一个进程在执行信号处理函数时,通常会将这个信号屏蔽直到处理结束,这样保证了不会同时响应相同的信号,信号的处理函数也就不必要求是可重入的

进程对一个信号的响应存在下面3种情况:

  • 显式地忽略
  • 执行默认的流程,每个信号都有一个默认的处理方式,它们可能是:terminate、dump(终止并生成 core file)、ignore、stop、continue
  • 捕获信号,执行程序设定好的处理流程

信号被 block 和 ignore 是不一样的,blocked 是进程没有收到信号,而 ignore 是收到了信号,而没有做任何处理。类似于一个人收到了信,不过直接将它丢进了垃圾桶,那就是 ignore,而如果直接拒收那份信,就是 blocked。

SIGKILL & SIGSTOP 是不能被 ignore、capture、block 的,它们的默认流程必须被执行,那就是杀死这个进程。

按照 POSIX 要求,信号在多线程进程中的特性如下:

  • 信号应该被进程中的所有线程共享,不过每个线程有自己独有的 mask,用于配置 block 哪些信号
  • kill() & sigqueue() 库函数是对所有的线程发送信号,而不是特定的信号
  • 信号只会被其中一个线程响应,这个线程是所有没有屏蔽此信号的线程中的任意一个
  • 如果是致命信号,那么所有的线程都会被杀死

实时信号

除了 Linux 中使用的 1~31 的常规信号,POSIX 标准还定义了一个实时信号集,它们在 Linux 中的编号是 32~64。实时信号相比较于常规的信号,差别是常规的信号在同一时间同一类型的信号只允许触发一次,也就是重复发多次信号,进程只响应一次。而实时信号则是可以多个相同的信号在队列中排队,也就是重复发多少次信号,进程就会想要多少次。Linux 系统不使用实时信号,不过它通过一些特殊的系统调用来满足 POSIX 的标准。

信号相关的数据结构

下面这张图列出了信号相关的数据之间的关系:

从图中可以看到,一个进程的信号等待队列有2个,一个是所有线程组共享的,存放在 signal 的 shared_pending 队列中,另一个则是私有的,存放在进程的 pending 队列中。之所以采用2个队列,是因为有的系统调用是给一个所有的线程发信号,比如 kill() & rt_sigqueueinfo(),有的则是给指定的特定进程发送信号,比如 tkill() & tgkill()。

信号的产生

许多的内核函数能够产生信号,它们通过修改进程描述符中关于信号相关的数据实现,剩下进程如何响应这个信号,它们不管。根据不同的信号类型,内核会选择是否立即唤醒进程并执行信号处理流程。不管信号是由内核生成还是由其他进程发起,都是内核调用相关的信号生成的函数,用于产生信号的函数很多:

  • send_sig
  • send_sig_info
  • force_sig
  • force_sig_info
  • sys_tkill
  • sys_tgkill

我们追踪 send_sig 这个函数,看信号生成的实现细节,它大概的调用流程如下图所示:

从系统调用 sys_kill 看用户空间向一个进程发送信号时,信号的产生过程:

static inline void prepare_kill_siginfo(int sig, struct kernel_siginfo *info)
{
    clear_siginfo(info);
    info->si_signo = sig;
    info->si_errno = 0;
    info->si_code = SI_USER;
    info->si_pid = task_tgid_vnr(current);
    info->si_uid = from_kuid_munged(current_user_ns(), current_uid());
}

/**
 *  sys_kill - send a signal to a process
 *  @pid: the PID of the process
 *  @sig: signal to be sent
 */
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
    struct kernel_siginfo info;

    prepare_kill_siginfo(sig, &info);

    return kill_something_info(sig, &info, pid);
}

首先,这里通过 prepare_kill_siginfo 填充 kernel_siginfo 这个结构体,这里面包含的是发送这个信号的一些信息,比如发送的来源、发起进程的 pid/uid 等。kill_something_info 中根据 pid 参数的值来确定它的操作:

  • pid > 0,发送信号给对应这个 pid 的进程
  • pid = 0,发送信号给当前进程组中的所有进程
  • pid = -1,发送给当前用户拥有权限的所有进程( init 进程除外)
  • pid < -1,发送信号给当前进程组中对应 -pid 的进程

最终,都是调用 send_signal 这个函数,根据以上 pid 参数不同的值,选择 send_signal 函数不同的输入参数。主要是选择是对哪个 task 进行信号操作,以及这个 pid 所代表的 type,也就是信号的作用范围是怎样的,下面就是 pid 几种类型,信号传递的范围按照这个类型由小及大:

enum pid_type
{
    PIDTYPE_PID,
    PIDTYPE_TGID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX,
};

__send_signal 中,通过 __sigqueue_alloc 创建一个信号队列元素,然后添加到 pending 队列中,如果是 PIDTYPE_PID,就使用前面提及的私有的 pending 队列,否则就使用共享的 pending 队列。signalfd_noftify 则是通知 signalfd 文件,有信号发送过来了(signalfd 是将信号通过一个文件描述符进行接受,然后就可以在 select 这样的接口中监控它,具体信息请查看)。

sigpending 的数据结构如下:

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

第一项 list 就是信号等待队列的头,而第二项 signal 则是一个 64bit 的一个数据,它的每一个 bit 代表着对应编号的一个信号。如果对应的 bit 位被置位,那么就表示对应进程收到了这个序号的信号。__send_signal 中的 sigaddset 就是操作 signal,在对应的 bit 位上写1。

complete_signal 通过 signal_wake_up 在需要接受信号的 task 中设置标志位 TIF_SIGPENDING,完成信号的产生。如果是一个致命信号,那么这个线程所在的所有线程都需要被杀死,所以这个进程中的每一个进程都会被标记 TIF_SIGPENDING。

信号的响应

内核在每次跳转到用户空间之前,都会检查一下当前这个 task 的标志状态,如果前面所说的 TIF_SIGPENDING 标志被置位,那么就说明当前这个 task 有信号需要处理,内核就会开始进行信号的响应处理。下面是内核跳转到用户空间前的汇编代码 ret_to_user

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
work_pending:
    mov    x0, sp                // 'regs'
    bl    do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
    bl    trace_hardirqs_on        // enabled while in userspace
#endif
    ldr    x1, [tsk, #TSK_TI_FLAGS]    // re-check for single-step
    b    finish_ret_to_user
/*
 * "slow" syscall return path.
 */
ret_to_user:
    disable_daif
    gic_prio_kentry_setup tmp=x3
    ldr    x1, [tsk, #TSK_TI_FLAGS]
    and    x2, x1, #_TIF_WORK_MASK
    cbnz    x2, work_pending
finish_ret_to_user:
    enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
    bl    stackleak_erase
#endif
    kernel_exit 0

and x2, x1, #_TIF_WORK_MASK 这条指令判断当前的 task 判断标志,如果有需要处理的标志,那么就跳转到 work_pending,在这里判断如果有收到信号,那么就调用 do_signal 进行信号的响应。在 do_signal 函数定义前的注视说的比较清楚:

/*
 * Note that 'init' is a special process: it doesn't get signals it doesn't
 * want to handle. Thus you cannot kill init even with a SIGKILL even by
 * mistake.
 *
 * Note that we go through the signals twice: once to check the signals that
 * the kernel can handle, and then we build all the user-level signal handling
 * stack-frames in one go after that.
 */
static void do_signal(struct pt_regs *regs)

init 进程是不接受信号的,所以它是杀不死的。do_signal 中会扫描看哪些信号是内核中就可以处理的,这样就需要为跳转到用户空间而创建一个处理信号用的用户空间栈帧。下面是它的调用流程:

内核中对于信号的响应,主要做了2件事情,第一个是将本次响应的信号从队列中移除,另一个则是为用户空间的信号处理准备好环境。

get_signal 中就是从队列中将信号取出,将它存储到一个 ksignal 的结构数据中:

struct ksignal {
    struct k_sigaction ka;
    kernel_siginfo_t info;
    int sig;
};

这里面就包含了我们在响应信号时所需要的3个信息:信号编号、信号的处理方式、发送信号的附加信息。__dequeue_signal 从队列中移除此次信号,其中 sigdelset 是对标示信号的比特位进行清零操作(如果是实时信号,则可能不需要进行清空,因为运行多个相同的信号存在队列中),__sigqueue_free 则是对队列元素进行释放。完成了出队操作后,则通过 recalc_sigpending 来判断此进程是否还有在等待响应的信号,如果没有了,那么就清除 进程的 TIF_SIGPENDING 标志。

handle_signal 则是为用户空间准备好处理信号的环境。这个环境包含3个部分:一是保存进程被信号打断时的位置,这样以便信号处理完成后继续程序的执行;二是将进程的执行点转移到信号处理流程中,进行真正的信号响应;三是等用户空间的信号处理完毕后,它需要再次回到内核空间,让内核再把用户进程切换到信号中断前的位置处。当然许多的信号响应不需要进入用户空间进行处理,因为很多默认为 ignore 行为的信号直接在 get_signal 阶段就完成了,还有一下致命信号,直接就由内核去处理终止流程了。

没当进程从用户空间进入内核空间的时候,用户空间当前的寄存器状态就会被保存在内核空间的栈中(这个是由异常处理入口处的汇编指令完成入栈),在内核中就是通过一个名为 pt_regs 结构数据指示着用户空间在进入内核前的寄存器。在信号处理响应流程中,当进程第一个从用户空间进入内核空间时,pt_regs 就包含着进程被信号打断前的上下文。而当内核回到用户空间的时候,这个上下文就会丢失,因为每次由用户空间进入内核空间的时候,内核的栈都是清空状态的。所以被信号打断前的硬件上下文就需要保存在用户空间的堆栈中,也就是我们准备环境的第一个部分,这部分的处理流程由 setup_sigframe完成。

对于第二和第三部分的环境准备则是通过 setup_return 完成(下面是 ARM64 架构的实现):

static void setup_return(struct pt_regs *regs, struct k_sigaction *ka,
             struct rt_sigframe_user_layout *user, int usig)
{
    __sigrestore_t sigtramp;

    regs->regs[0] = usig;
    regs->sp = (unsigned long)user->sigframe;
    regs->regs[29] = (unsigned long)&user->next_frame->fp;
    regs->pc = (unsigned long)ka->sa.sa_handler;

    if (ka->sa.sa_flags & SA_RESTORER)
        sigtramp = ka->sa.sa_restorer;
    else
        sigtramp = VDSO_SYMBOL(current->mm->context.vdso, sigtramp);

    regs->regs[30] = (unsigned long)sigtramp;
}

信号中断前的硬件上下文已经通过 setup_sigframe 保存到了 user->subframe 中,所以在这个函数中,我们通过修改 pt_regs 来改变跳转到用户空间时的执行流。首先看 pc 寄存器被设置为了用户的信号处理函数,栈寄存器 sp 从 sigframe 开始。regs[0] 存储信号编号,按照 ARM64 的 ABI,它将作为信号处理函数的第一个输入参数。 regs[29] 是 ARM64 栈帧指针,regs[30] 则是 LR 寄存器,也就是函数执行完毕后的返回地址,这里指向 VDSO 中的 rt_sigreturn 系统调用(定义在 arch/arm64/kernel/vdso/sigreturn.S 文件中)。也即是,用户执行完信号处理流程就会通过 rt_sigreturn 系统调用重新回到内核空间,再由内核调用 restore_sigframe 将用户进程的执行流程恢复到被信号中断时的状态。整个信号的处理流程入下图所示:

前面我们说过 setup_sigframe 函数对用户的栈增加了一个栈帧用于保存恢复信号中断前的状态,接下来我们就看看这个栈帧的结构。这个新增栈帧的结构由 get_sigframe 函数规划,其结构通过结构体 rt_sigframe_user_layout 存储:

struct rt_sigframe_user_layout {
    struct rt_sigframe __user *sigframe;
    struct frame_record __user *next_frame;

    unsigned long size;    /* size of allocated sigframe data */
    unsigned long limit;    /* largest allowed size */

    unsigned long fpsimd_offset;
    unsigned long esr_offset;
    unsigned long sve_offset;
    unsigned long extra_offset;
    unsigned long end_offset;
};

在 ARM64 架构上,signal frame 的结构如下图所示:

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

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

相关文章

spring AOP中pointcut表达式详解

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是「奇点」&#xff0c;江湖人称 singularity。刚工作几年&#xff0c;想和大家一同进步&#x1f91d;&#x1f91d; 一位上进心十足的【Java ToB端大厂…

纹理过滤方式

纹理坐标不依赖于分辨率&#xff0c;opengl需要知道如何将纹理像素映射到纹理坐标。 纹理像素和纹理坐标的区别&#xff1a; 纹理像素是有限的。 纹理坐标的精度是无限的&#xff0c;可以是任意的浮点值。 一个像素需要一个颜色。 因此&#xff0c;所谓采样就是通过纹理坐标获取…

图像处理之图像灰度化

图像灰度化 将彩色图像转化成为灰度图像的过程成为图像的灰度化处理。彩色图像中的每个像素的颜色有R、G、B三个分量决定&#xff0c;而每个分量有255中值可取&#xff0c;这样一个像素点可以有1600多万 (255255255)的颜色的变化范用。而灰度图像是R、G、B三个分量相同的一种特…

分析 jsjiami.v6 代码混淆和加密技术:特点、优点和缺点

当涉及到 JavaScript 代码混淆和加密技术时&#xff0c;开发人员常常寻求方法来保护他们的代码免受逆向工程和未授权访问的威胁。这种技术可以增加代码的复杂性&#xff0c;使其难以理解和修改&#xff0c;同时也可以隐藏关键信息和保护数据的安全性。本文将探讨 JavaScript 代…

echarts在vue上使用模版可粘贴!!!一站式搞定以及动态数据渲染关键问题解决(附带模版)

阿丹&#xff1a; 之前的项目中涉及到echarts都是使用jsp写的&#xff0c;但是现在越来普及vue。所以使用在vue上使用echarts就成了问题。 导入相关依赖 在官网上面有说明npm安装Echarts依赖 在vue中使用Echarts 上模版!!!!!!!!!!!!使用了mounted&#xff08;&#xff09;这个…

在使用yum时报错Loaded plugins: fastestmirror

在使用yum时报错Loaded plugins: fastestmirror 在我安装更高版本的gcc编译器时&#xff0c;发现了这个问题 意思是加载插件&#xff0c;加载后面两个插件&#xff0c;查阅得知其中fastestmirror是yum的一个加速插件&#xff0c;这个提示意思是这个插件不能使用,那么就先把他禁…

4 款非常好用的AI生成图片软件

AI生成图片最近是越来越火了&#xff0c;越来越多的AI生成图片工具上线。 本文就给你推荐4款非常好用的AI生成图片工具&#xff0c;避免你碰雷。 即时灵感 「即时灵感」是通过文字描述等方式生成精致图像的AI绘图工具。输入文字&#xff0c;即可将创意变为现实&#xff01; …

centos7升级openssh修复安全漏洞

查看linux版本 cat /etc/redhat-releaseCentOS Linux release 7.9.2009 (Core) 升级 yum update openssh -y升级openssl和openssh 下载openssl https://ftp.openssl.org/source/ 下载openssh http://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/ yum update openssh…

盘点spring-boot-3-jwt-security中如何使用jwt+security

目录 SecurityApplication类中 AuthenticationService register方法 authenticate方法 saveUserToken略 revokeAllUserTokens方法 refreshToken方法 ApplicationConfig Bean&#xff1a;UserDetailsService Bean&#xff1a;AuthenticationProvider Bean&#xff1a…

浅谈智能集成电力电容器的应用意义

安科瑞 华楠 摘要&#xff1a;该文通过智能集成电力电容器研发背景的说明&#xff0c;介绍该产品基于数字信号处理器&#xff08;DSP&#xff09;和单片机相结合的双CPU技术智能无功补偿技术&#xff0c;集成复合开关的过零投切与计算机智能网络模块技术&#xff0c;实现了现代…

语音信号的采集--电脑录音

准备条件&#xff1a; 1.电脑麦克风打开。&#xff08;联想小新AIR2021 打开方式&#xff1a;点击电脑右下角“电源”&#xff0c;点击“麦克风打开”&#xff09; 2.打开电脑自带的录音机。&#xff08;电脑界面搜索框中键入“录音机”&#xff09; 3.打开音乐播放软件&…

【JavaEE】Linux基本使用

Linux基本使用 文章目录 【JavaEE】Linux基本使用1. 如何获取一个Linux的环境2. 终端软件来远程登录3. Linux基本使用3.1 命令提示符3.2 ls3.3 cd3.4 pwd3.5 touch3.6 cat3.7 man3.8 echo3.9 vim3.9.1 打开文件3.9.2 编辑文件3.9.3 保存退出3.9.4 vimtutor 3.10 mkdir3.11 rm3.…

【设计模式】23种设计模式——单例模式(原理讲解+应用场景介绍+案例介绍+Java代码实现)

单例模式(Singleton) 介绍 所谓类的单例设计模式&#xff0c;就是采取一定的方法&#xff0c;保证在整个的软件系统中&#xff0c;对某个类只能存在一个对象实例&#xff0c;并且该类只提供一个取得其对象实例的方法&#xff08;静态方法&#xff09;。比如Hibernate的Sessio…

UE5《Electric Dreams》项目PCG技术解析 之 PCGCustomNodes详解(三)SG_CopyPointsWithHierarchy

继续解析《Electric Dreams》项目中的自定义节点和子图&#xff1a;SG_CopyPointsWithHierarchy和PostCopyPoints-OffsetIndices 文章目录 前导文章标准组合拳SG_CopyPointsWithHierarchyPostCopyPoints-OffsetIndices使用情景Execute with ContextIteration Loop Body 小结 前…

看懂Congestion Map

往期文章链接: ​​​​​​ICC2:工具是如何平衡std cell利用率和congestion的? ICC2:使用report_placement检查floorplan ICC2:使用global route分析绕线 ICC2:congestion的解决办法

postman 自动化测试

postman 自动化测试 0、写在前面1、变量引用1.1、如何在请求体中引变量 0、写在前面 在有些时候看官方文档 比网上搜索效率要高&#xff0c; 比如网上搜一通还是不知道用法或者没有搜索到你想要的用法的时候。 postman官方文档 : https://learning.postman.com/docs/introduct…

从 robot 坐标系到 orb2 坐标系的变换

机器人坐标系 在机器人学和机器人导航中,通常使用右手坐标系,其定义如下: X轴:通常定义为机器人的"前"或"向前"方向。在移动机器人中,这通常是机器人行进的方向。 Y轴:在右手坐标系中,Y轴通常定义为机器人的"左"或"向左"方向。…

有趣的命令——————用shell脚本实现与电脑猜价格游戏,小朋友比较合适哟~~~

vim games.sh 输入以下内容#!/bin/bashecho "这是一个小游戏&#xff0c;猜价格&#xff08;1--1000&#xff09;" pc$(expr $RANDOM % 1000 1) cs0 while true doread -p "请输入你猜的价格&#xff1a;" intlet csif [ $int -gt $pc ]thenecho "价…

docker+jenkins 实现自动化部署

前期准备工作 工欲善其事必先利其器&#xff0c;所以前期的准备工作必须做好&#xff0c;那么我们要实现自动化部署&#xff0c;必须提前准备什么呢&#xff1f;哈哈哈&#xff0c;可能有一些小伙伴已经猜到了&#xff0c;我们需要一台自己的服务器&#xff0c;这里博主使用的…

你不会还不知道什么是企业博客吧?

企业博客是指由企业或组织创建的在线平台&#xff0c;主要是用于发布与其业务、产品、行业和相关主题相关的文章、信息和观点。通过企业博客可以实现促进品牌推广、客户培养和业务发展等&#xff0c;对于企业发展有极其重要的作用。 企业博客的目的 1.提供有关企业产品和服务的…