【Linux】Linux信号产生,接受与处理机制

理解Linux信号产生,接受与处理机制

信号是Linux操作系统中一种用于进程间通信和异步事件处理的机制。在本文中,我们将结合Linux的源码,深入分析信号的产生、发送、接收和处理的底层原理。

文章目录

  • 理解Linux信号产生,接受与处理机制
    • 什么是信号
    • 信号处理相关的数据结构
    • 信号的产生与发送
    • 信号的接收与处理
      • 信号的触发点
      • 信号处理函数的执行


什么是信号

信号本质上是在软件层次上对中断机制的一种模拟,其主要有以下几种来源:

  • 程序错误:除零,非法内存访问等。
  • 外部信号:终端 Ctrl-C 产生 SGINT 信号,定时器到期产生SIGALRM等。
  • 显式请求:kill函数允许进程发送任何信号给其他进程或进程组。

目前 Linux 支持64种信号。信号分为非实时信号(不可靠信号)和实时信号(可靠信号)两种类型,对应于 Linux 的信号值为 1-31 和 34-64。

img
发送信号的系统调用通常包括:

  • kill(pid, sig):向进程ID为pid的进程发送信号sig
  • raise(sig):向自己发送信号sig
  • alarm(seconds):在指定时间后给自己发送SIGALRM信号。
  • sigqueue(pid, sig, value):向进程ID为pid的进程发送信号sig,并传递附加值value

接收信号的进程可以通过注册信号处理函数来处理收到的信号,比如使用signal()或者sigaction()系统调用。

在include/signal.h头文,gnalO函数原型声明如下:

void (*signal(int signr,void (*handler)(int)))(int);|

这个 signal()函数有两个参数。一个指定需要捕获的信号 signr;另外一个是新的信号处理函数指针(新的信号处理句柄)void (*handler)(int)。新的信号处理句柄是一个无返回值且具有一个整型参数的函数指针,该整型参数用于当指定信号发生时内核将其传递给处理句柄。

signal( )函数会给信号值是 signr 的信号安装一个新的信号处理函数句柄handler,该信号句柄可以是用户指定的一个信号处理函数,也可以是内核提供的特定的函数指针SIG_IGN或SIG_DFL。当指定的信号到来时,如果相关的信号处理句柄被设置成SIG_IGN,那么该信号就会被忽略掉。如果信号句柄是SIG_DFL,那么就会执行该信号的默认操作。否则,如果信号句柄被设置成用户的一个信号处理函数,那么内核首先会把该信号句柄被复位成其默认句柄,或者会执行与实现相关的信号阻塞操作,然后会调用执行指定的信号处理函数。signal()函数会返回原信号处理句柄,这个返回的句柄也是一个无返回值且具有一个整型参数的函数指针。并且在新句柄被调用执行过一次后,信号处理句柄又会被恢复成默认处理句柄值SIG_DFL。

在这里插入图片描述

信号是异步的,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不知道信号到底什么时候到达。一般来说,我们只需要在进程中设置信号相应的处理函数,当有信号到达的时候,由系统异步触发相应的处理函数即可。如下代码:

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sigcb(int signo) {switch (signo) {case SIGHUP:printf("Get a signal -- SIGHUP\n");break;case SIGINT:printf("Get a signal -- SIGINT\n");break;case SIGQUIT:printf("Get a signal -- SIGQUIT\n");break;}return;
}
int main() {signal(SIGHUP, sigcb);signal(SIGINT, sigcb);signal(SIGQUIT, sigcb);for (;;) {sleep(1);}
}

运行程序后,当我们按下 Ctrl+C 后,屏幕上将会打印 Get a signal – SIGINT。当然我们可以使用 kill -s SIGINT pid命令来发送一个信号给进程,屏幕同样打印出 Get a signal – SIGINT 的信息。

信号处理相关的数据结构

在进程管理结构 task_struct 中有几个与信号处理相关的字段,如下:

struct task_struct {...int sigpending;...struct signal_struct *sig;sigset_t blocked;struct sigpending pending;...
}
  • int sigpending: 表示该进程是否有待处理的信号。值为 1 表示有待处理的信号,值为 0 表示没有。
  • struct signal_struct \*sig: 指向信号处理相关信息的指针。
  • sigset_t blocked: 表示被屏蔽的信号集,每个位代表一个被屏蔽的信号。
  • struct sigpending pending: 存储接收到但尚未处理的信号队列。

其实struct signal_struct 是个比较复杂的结构,其 action 成员是个 struct k_sigaction 结构的数组,数组中的每个成员代表着相应信号的处理信息,而 struct k_sigaction 结构其实是 struct sigaction 的简单封装。

#define _NSIG 64
struct signal_struct {atomic_t		count;struct k_sigaction	action[_NSIG];spinlock_t		siglock;
};
typedef void (*__sighandler_t)(int);
struct sigaction {__sighandler_t sa_handler;//信号的处理方法unsigned long sa_flags;//信号选项标志void (*sa_restorer)(void);//信号恢复函数指针(系统内部使用)sigset_t sa_mask;//信号的屏蔽码,可以阻塞指定的信号集
};
struct k_sigaction {struct sigaction sa;
};

当信号产生时,内核会将其添加到目标进程的 pending 队列中,表示有待处理的信号。进程在执行过程中会定期检查 pending 队列,如果有待处理信号,则根据信号处理函数的定义执行相应的处理操作。信号处理函数的定义和管理通过 k_sigaction 结构和 struct sigaction 结构实现,其中包括处理函数指针和处理标志等信息。

信号的产生与发送

首先,信号的产生是随机,所以进程是不会专门用一个类似 wait( ) 函数去等待信号的发生,因为在进程运行的整个周期(开始->结束),信号可能根本不会发生,那我们进程是如何接受到信号的呢?

信号的发送,是一个系统调用,这意味着,进程需要从用户态切换到内核态,系统调用sys_kill ( )函数,这个函数的作用是,通过判断信号的种类,找遍历整个进程数组,找到符合条件的进程发送信号,而发送信号的函数用的就是 send_sig( )

sys_kill(int pid, int sig)
{struct siginfo info;info.si_signo = sig;info.si_errno = 0;info.si_code = SI_USER;info.si_pid = current->pid;info.si_uid = current->uid;return kill_something_info(sig, &info, pid);
}

sys_kill()系统调用遍历进程表,找到目标进程后调用send_sig()函数:

static int send_signal(int sig, struct siginfo *info, struct sigpending *signals)
{struct sigqueue *q = NULL;// 检查是否达到最大信号队列数if (atomic_read(&nr_queued_signals) < max_queued_signals) {q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);}if (q) {atomic_inc(&nr_queued_signals);q->next = NULL;// 添加到信号队列尾部*signals->tail = q;signals->tail = &q->next;// 根据不同情况设置信号信息switch ((unsigned long)info) {case 0:q->info.si_signo = sig;q->info.si_errno = 0;q->info.si_code = SI_USER;q->info.si_pid = current->pid;q->info.si_uid = current->uid;break;case 1:q->info.si_signo = sig;q->info.si_errno = 0;q->info.si_code = SI_KERNEL;q->info.si_pid = 0;q->info.si_uid = 0;break;default:copy_siginfo(&q->info, info);break;}} else if (sig >= SIGRTMIN && info && (unsigned long)info != 1 && info->si_code != SI_USER) {return -EAGAIN;}// 将信号添加到信号集中sigaddset(&signals->signal, sig);return 0;
}

它首先检查是否达到最大信号队列数,然后分配一个新的 sigqueue 结构并将其添加到 signals 队列中。根据信号的来源和类型,它会设置相应的信号信息,并将信号添加到信号集中。如果无法分配新的 sigqueue 结构,且信号类型为实时信号并且不是用户态产生的,函数将返回错误码 -EAGAIN

简单来说就是,send_signal函数会找到符合条件的进程,并将信号记录在其描述结构体中

信号的接收与处理

信号的触发点

上面介绍了怎么发生一个信号给指定的进程,但是什么时候会触发信号相应的处理函数呢?为了尽快让信号得到处理,Linux把信号处理过程放置在进程从内核态返回到用户态前,也就是在 ret_from_sys_call 中检查进程的 sigpending 成员是否等于1,如果是则会调用 do_signal() 函数进行处理。

ret_from_sys_call 文件中,中断返回时会调用do_signal()

在这里插入图片描述

do_signal()函数是内核系统调用(int 0x80)中断处理程序中对信号的预处理程序。在进程每次调用系统调用或者发生时钟等中断时,若进程已收到信号,则该函数就会把信号的处理句柄(即对应的信号处理函数)插入到用户程序堆栈中。这样,在当前系统调用结束返回后就会立刻执行信号句柄程序,然后再继续执行用户的程序

在这里插入图片描述

do_signal()函数实现如下:

int do_signal(struct pt_regs *regs, sigset_t *oldset)
{siginfo_t info;struct k_sigaction *ka;// 如果不在用户态,直接返回if ((regs->xcs & 3) != 3)return 1;if (!oldset)oldset = &current->blocked;for (;;) {unsigned long signr;// 获取一个待处理的信号spin_lock_irq(&current->sigmask_lock);signr = dequeue_signal(&current->blocked, &info);spin_unlock_irq(&current->sigmask_lock);if (!signr)break;ka = &current->sig->action[signr-1];// 如果信号被忽略,继续处理下一个信号if (ka->sa.sa_handler == SIG_IGN) {if (signr != SIGCHLD)continue;while (sys_wait4(-1, NULL, WNOHANG, NULL) > 0)/* nothing */;continue;}// 如果信号采用默认处理方法,进行相应处理if (ka->sa.sa_handler == SIG_DFL) {int exit_code = signr;// init 进程特殊处理if (current->pid == 1)continue;switch (signr) {case SIGCONT: case SIGCHLD: case SIGWINCH:continue;case SIGTSTP: case SIGTTIN: case SIGTTOU:if (is_orphaned_pgrp(current->pgrp))continue;current->state = TASK_STOPPED;current->exit_code = signr;if (!(current->p_pptr->sig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDSTOP))notify_parent(current, SIGCHLD);schedule();continue;case SIGQUIT: case SIGILL: case SIGTRAP:case SIGABRT: case SIGFPE: case SIGSEGV:case SIGBUS: case SIGSYS: case SIGXCPU: case SIGXFSZ:if (do_coredump(signr, regs))exit_code |= 0x80;default:sigaddset(&current->pending.signal, signr);recalc_sigpending(current);current->flags |= PF_SIGNALED;do_exit(exit_code);}}// 调用自定义的信号处理函数handle_signal(signr, ka, &info, oldset, regs);return 1;}return 0;
}

do_signal()函数会检查进程的signal成员变量,如果发现有未处理的信号,会根据信号处理函数进行处理。

static void handle_signal(unsigned long sig, struct k_sigaction *ka,siginfo_t *info, sigset_t *oldset, struct pt_regs *regs)
{if (ka->sa.sa_flags & SA_SIGINFO)setup_rt_frame(sig, ka, info, oldset, regs);elsesetup_frame(sig, ka, oldset, regs);if (ka->sa.sa_flags & SA_ONESHOT)ka->sa.sa_handler = SIG_DFL;if (!(ka->sa.sa_flags & SA_NODEFER)) {spin_lock_irq(&current->sigmask_lock);sigorsets(&current->blocked, &current->blocked, &ka->sa.sa_mask);sigaddset(&current->blocked, sig);recalc_sigpending(current);spin_unlock_irq(&current->sigmask_lock);}}

由于信号处理程序是由用户提供的,所以信号处理程序的代码是在用户态的。而从系统调用返回到用户态前还是属于内核态,CPU是禁止内核态执行用户态代码的,那么怎么办?

在这里插入图片描述

首先返回到用户态执行信号处理程序,然后执行完信号处理程序后再返回到内核态,再在内核态完成收尾工作。

信号处理函数的执行

为了达到这个目的,Linux经历了一个十分崎岖的过程。我们知道,从内核态返回到用户态时,CPU要从内核栈中找到返回到用户态的地址(就是调用系统调用的下一条代码指令地址),Linux为了先让信号处理程序执行,所以就需要把这个返回地址修改为信号处理程序的入口,这样当从系统调用返回到用户态时,就可以执行信号处理程序了。

所以,handle_signal() 调用了 setup_frame() 函数来构建这个过程的运行环境(其实就是修改内核栈和用户栈相应的数据来完成)。
以下是setup_frame()函数的关键代码:

static void setup_frame(int sig, struct k_sigaction *ka,sigset_t *set, struct pt_regs *regs)
{regs->eip = (unsigned long) ka->sa.sa_handler;// regs是内核栈中保存的寄存器集合// ...
}该函数会将信号处理程序的地址设置到内核栈中,并在用户栈中构建一个帧,以便信号处理程序执行完毕后调用`sigreturn()`系统调用返回内核态。

现在可以在内核态返回到用户态时自动执行信号处理程序了,但是当信号处理程序执行完怎么返回到内核态呢?Linux的做法就是在用户态栈空间构建一个 Frame(帧),构建这个帧的目的就是为了执行完信号处理程序后返回到内核态,并恢复原来内核栈的内容。返回到内核态的方式是调用一个名为 sigreturn() 系统调用,然后再 sigreturn() 中恢复原来内核栈的内容。

在这里插入图片描述

怎样能在执行完信号处理程序后调用 sigreturn() 系统调用呢?其实跟前面修改内核栈 eip 的值一样,这里修改的是用户栈 eip 的值,修改后跳转到一个执行下面代码的地方(用户栈的某一处):

popl %eax 
movl $__NR_sigreturn,%eax 
int $0x80

最后,当信号处理函数执行完毕后,会调用sigreturn()系统调用来恢复信号处理前的状态。sigreturn()函数会从用户栈中读取原来的内核栈数据,恢复之后继续执行信号处理前的代码。以下是sigreturn()函数的关键代码:

asmlinkage int sys_sigreturn(unsigned long __unused)
{struct pt_regs *regs = (struct pt_regs *) &__unused;struct sigframe *frame = (struct sigframe *)(regs->esp - 8);sigset_t set;int eax;// 省略部分代码if (restore_sigcontext(regs, &frame->sc, &eax))goto badframe;return eax;badframe:force_sig(SIGSEGV, current);return 0;
}

在信号处理函数执行时,内核会修改用户栈,使得返回用户态时首先执行信号处理函数。这是通过在用户栈上创建一个新的栈帧实现的,该栈帧包含信号处理函数的地址。当信号处理函数执行完毕后,恢复原来的栈帧,继续执行原来的程序。

参考文献:

  • 《Linux内核完全注释》
  • Linux内核信号处理机制介绍
  • 深入理解Linux内核信号处理机制原理

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

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

相关文章

Android 几个简单的自定义对话框介绍

Android 几个简单的自定义对话框介绍 文章目录 一、前言二、对话框相关内容1、效果2、对话框显示的调用代码&#xff08;1&#xff09;原生对话框代码&#xff1a;&#xff08;2&#xff09;自定义对话框代码&#xff1a; 3、对话框SweetAlertDialog 主要实现代码&#xff1a;4…

【Linux】-Elasticsearch安装部署[16]

目录 简介 安装 1、添加yum仓库 2、安装es 3、配置es 4、启动es 5、关闭防火墙 6、测试 简介 全文搜索属于最常见的要求&#xff0c;开源的Elasticsearch&#xff08;以下简称es&#xff09;是目前全文搜索引擎的首选。它可以快速的储存、搜索和分析海量数据。维基百科…

以人为本的人工智能:李飞飞谈AI

随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;关于AI的讨论越来越多&#xff0c;特别是围绕其可能带来的威胁。有人担心高效的AI会夺走我们的工作&#xff0c;甚至不可控的AI最终会统治人类。对此&#xff0c;斯坦福大学计算机科学系教授李飞飞提出了不同…

Paddle 稀疏计算 使用指南

Paddle 稀疏计算 使用指南 1. 稀疏格式介绍 1.1 稀疏格式介绍 稀疏矩阵是一种特殊的矩阵&#xff0c;其中绝大多数元素为0。与密集矩阵相比&#xff0c;稀疏矩阵可以节省大量存储空间&#xff0c;并提高计算效率。 例如&#xff0c;一个5x5的矩阵中只有3个非零元素: impor…

springboot中使用spring-cloud-starter-openfeign遇到的问题及解决参考

声明&#xff1a;本文使用的spring boot 版本是2.7.12 在springboot中使用spring-cloud-starter-openfeign遇到的一些问题&#xff1a; Caused by: java.lang.ClassNotFoundException: org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata java.…

微软文字转语音小工具(Text to speech)网页版

在线文字转语音工具&#xff1a;在线文本转语音 (text-to-speech.cn) 随着科技的迅猛发展&#xff0c;人工智能技术日益成熟&#xff0c;AI配音作为其中的一项重要应用&#xff0c;正在以惊人的速度改变着我们的生活。所谓AI配音&#xff0c;指的是利用人工智能技术模拟人类声音…

使用字节豆包大模型在 Dify 上实现最简单的 Agent 应用(四):AI 信息检索

这篇文章&#xff0c;我们继续聊聊&#xff0c;如何折腾 AI 应用&#xff0c;把不 AI 的东西&#xff0c;“AI 起来”。在不折腾复杂的系统和环境的前提下&#xff0c;快速完成轻量的 Agent 应用。 写在前面 在上一篇文章《使用 Dify、Meilisearch、零一万物模型实现最简单的…

PDF Reader Pro for Mac 直装激活版:专业PDF阅读编辑软件

在数字化时代&#xff0c;PDF文件已成为我们日常工作和学习中不可或缺的一部分。然而&#xff0c;如何高效、便捷地阅读、编辑和管理这些PDF文件&#xff0c;却一直是许多人面临的难题。现在&#xff0c;有了PDF Reader Pro for Mac&#xff0c;这些难题将迎刃而解。 PDF Reade…

GPIO模拟IIC通信测量环境光

目录 iic.h iic.c ap3216c.h ap3216.c main.c 实验效果 iic.h #ifndef __IIC_H__ #define __IIC_H__#include "stm32mp1xx_gpio.h" #include "stm32mp1xx_rcc.h" //SDA 数据线为PF15 //SCL 时钟线为PF14//配置PF15为输出模式 #define SET_SDA_OUT d…

列举几个淘宝商品详情API接口测试示例

API名&#xff1a;item_get 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xff09;[item_search,item_get,item_search_shop等]cacheString否[yes…

网络模型-Qinq配置与应用

Qinq配置与应用 通过配置Qinq来实现利用公网提供的VLAN100使企业1互通&#xff0c;利用公网提供的VLAN200使企业2互通不同企业之间互相隔离。并通过在连接其它厂商设备的接口上配置修改0in0外层VLAN Tag的TPID值&#xff0c;来实现与其它厂商设备的互通。 一、创建VLAN #在Swi…

等风来不如追风去 火星皮卡与罗乐的逐梦之旅

都说&#xff0c;男人至死皆少年。少年有梦&#xff0c;不应止于心动。于是&#xff0c;家在毕节的罗乐在节前果断为自己购入了一辆全尺寸火星皮卡当作自己的新年礼物。从此火星皮卡便与罗乐相伴义无反顾地踏上这场热辣滚烫的逐梦之旅 “全尺寸火星满足了我对Dream Car的所有幻…

DVWA代码审计--文件上传

NO.1 Low 首先来看下代码 <?php if( isset( $_POST[ Upload ] ) ) { // Where are we going to be writing to? $target_path DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; $target_path . basename( $_FILES[ uploaded ][ name ] ); // Can we move the f…

netcat一键开始瑞士军刀模式(KALI工具系列五)

目录 1、KALI LINUX简介 2、netcat工具简介 3、在KALI中使用netcat 3.1 目标主机IP&#xff08;win&#xff09; 3.2 KALI的IP 4、命令示例 4.1 测试某IP的端口是否打开 4.2 TCP扫描 4.3 UDP扫描 4.4 端口刺探 4.5 直接扫描 5、即时通信 5.1 单击对话互联 5.2 传…

知识表示概述

文章目录 知识表示研究现状技术发展趋势 知识表示 知识是人类在认识和改造客观世界的过程中总结出的客观事实、概念、定理和公理的集合。知识具有不同的分类方式&#xff0c;例如按照知识的作用范围可分为常识性知识与领域性知识。知识表示是将现实世界中存在的知识转换成计算机…

巨某量引擎后台登录实战笔记 | Playwright自动化框架

前言 本文章中所有内容仅供学习交流&#xff0c;抓包内容、敏感网址、数据接口均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff0c;若有侵权&#xff0c;请联系我立即删除&#xff01; 入正题看看滑块是怎么个事…

网络的基础理解

文章目录 网络的基础认识 网络协议协议分层OSI七层模型TCP/IP 五层/四层 模型 网络的基础认识 先来看下面几个问题 什么是网络&#xff1f; 网络就是有许多台设备包括计算机单不仅限于计算机&#xff0c;这些设备通过相互通信所组成起来系统&#xff0c;我们称之为网络所以如…

Gartner发布中国数据安全安全与风险管理领导者指南:将孤立的数据安全产品集成到数据安全平台中,实施一致的数据安全策略

在中国开展业务或与中国相关的组织面临着越来越多的数据安全风险和法规。安全和风险管理领导者必须采用风险优先的数据安全计划和投资&#xff0c;以响应监管要求&#xff0c;以增强数据驱动的数字创新能力。 主要发现 跨组织职能的分散的数据安全举措和不协调的利益相关者责任…

服务器c盘爆满了,这几种方法可以帮助C盘“瘦身”

我们在使用服务器的时候基本不会在C盘安装软件&#xff0c;那么用久了发现C盘满了&#xff0c;提示空间不足&#xff1f;那么这是怎么回事&#xff0c;为什么空间会占用这么快呢&#xff1f; 原因一&#xff1a; C盘满了&#xff0c;很可能是因为电脑里的垃圾文件过多。操作系…

薪资不公、晋升无望?动笔写一份申诉材料吧!

薪资不公、晋升无望&#xff1f;动笔写一份申诉材料吧&#xff01; 引言&#xff1a;每个努力工作的人都值得公平对待 在职场上&#xff0c;我们付出了汗水和智慧&#xff0c;期待着相应的回报——合理的工资和公正的晋升机会。然而&#xff0c;现实并不总是如此美好。当你感觉…