【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);
    else
        setup_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内核信号处理机制原理

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

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

相关文章

一、QGroundControl地面站使用介绍

文章目录 环境功能介绍飞行视图规划视图飞机设置分析工具程序设置 连接飞机飞机设置分析工具飞行视图规划任务 总结参考 环境 QGroundControl V4.2.0PX4-Autopilot V1.3.0devGazebo 模拟无人机 功能介绍 飞行视图规划视图飞机设置分析工具程序设置 飞行视图 软件打开后为飞…

跟着鲁sir学CV_Opencv(10)卡尔曼滤波

简介 卡尔曼滤波器由鲁道夫卡尔曼&#xff08;Rudolf E. Klmn&#xff09;在1960年提出&#xff0c;广泛应用于导航系统、信号处理、机器人定位、金融等多个领域。 主要分为两阶段&#xff1a;预测与更新 贝叶斯滤波器 贝叶斯框架下&#xff1a;预测&#xff08;先验&#x…

楼道堆积物视觉识别监控系统

楼道堆积物视觉识别监控系统采用了AI神经网络和深度学习算法&#xff0c;楼道堆积物视觉识别监控系统通过摄像头实时监测楼道的情况&#xff0c;通过图像处理、物体识别和目标跟踪算法&#xff0c;系统能够精确地识别楼道通道是否被堆积物阻塞。楼道堆积物视觉识别监控系统检测…

K210的MicroPython扩展例程——自动驾驶例程(视觉循迹)

前言 该例程实现的功能是循迹功能&#xff0c;可为想拿K210做视觉循迹开发作为参考 例程使用前需要搭建好MicroPython的开发环境 K210开发板MicroPython开发环境搭建 一、将K210连接好后打开CanMV IDE&#xff0c;连接成功后&#xff0c;三角形变成绿色 二、然后要把小车驱动…

接口测试怎么测?为什么要做接口测试?

一、前言 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间及内部各个子系统之间的交互点。测试的重点是检查数据的交换、传递和控制管理过程&#xff0c;以及系统间的逻辑依赖关系等。 简单地说&#xff0c;接口测试就是通过URL向服务器或者…

leetCode-hot100-数组专题之区间问题

数组专题之区间问题 知识点&#xff1a;解决思路&#xff1a;例题56.合并区间57.插入区间253.会议室 Ⅱ485.无重叠区间 数组区间问题是算法中常见的一类问题&#xff0c;它们通常涉及对数组中的区间进行排序、合并、插入或删除操作。无论是合并区间、插入区间还是删除重复空间&…

azure gpt 技术教程教学 | 在Azure OpenAI 上部署GPT-4o

Azure OpenAI GPT-4o是OpenAI推出的最新旗舰级人工智能模型。GPT-4o模型设计为能够实时对音频、视觉和文本进行推理&#xff0c;这是迈向更自然人机交互的重要一步。该模型的一大特点是能够处理多种类型的数据输入和输出&#xff0c;包括文本、音频和图像&#xff0c;实现了跨模…

适用于Windows 电脑的最佳视频恢复软件和方法

毫无疑问&#xff0c;丢失您的基本数据总是有压力的&#xff0c;尤其是当这些是您为捕捉最美好回忆而收集的重要视频文件时。要恢复丢失或损坏的视频文件&#xff0c;您可以借助视频恢复工具。但是&#xff0c;在选择最佳视频恢复工具时&#xff0c;您必须考虑多个扫描选项&…

Windows10安装python3.8.2

1、下载与安装 下载地址&#xff1a;https://www.python.org/downloads/release/python-382/ 滑动到页面底部 下载好的安装包安装到合适的位置&#xff08;默认安装到C盘&#xff09; 安装的时候&#xff0c;见到Add Python 3.8 to PATH 记得勾选. 2、检测安装是否成功 cm…

Facebook隐私保护:数据安全的前沿挑战

在数字化时代&#xff0c;随着社交媒体的普及和应用&#xff0c;个人数据的隐私保护问题日益受到关注。作为全球最大的社交平台之一&#xff0c;Facebook承载了数十亿用户的社交活动和信息交流&#xff0c;但与此同时&#xff0c;也面临着来自内外部的数据安全挑战。本文将深入…

Flask Response 对象

文章目录 创建 Response 对象设置响应内容设置响应状态码设置响应头完整的示例拓展设置响应的 cookie重定向响应发送文件作为响应 总结 Flask 是一个 Python Web 框架&#xff0c;用于快速开发 Web 应用程序。在 Flask 中&#xff0c;我们使用 Response 对象来构建 HTTP 响应。…

汽车IVI中控开发入门及进阶(十九):监控视频图像分割处理

图像分割是一种计算机视觉技术,可将数字图像分成离散的像素组(图像分段),为对象检测和相关任务提供信息。通过将图像的复杂视觉数据解析为特定形状的片段,图像分割可以实现更快、更先进的图像处理。 图像分割技术的范围很广,从简单直观的启发式分析到最前沿的深度学习实…

【论文笔记】advPattern

【论文题目】 advPattern: Physical-World Attacks on Deep Person Re-Identification via Adversarially Transformable Patterns Abstract 本文首次尝试对深度reID实施鲁棒的物理世界攻击。提出了一种新颖的攻击算法&#xff0c;称为advPattern&#xff0c;用于在衣服上生成…

CR80清洁卡都能用在什么地方?

CR80清洁卡&#xff08;也被称为ISO 7810 ID-1清洁卡&#xff09;的规格确实使其在各种需要读取磁条或接触式智能卡的设备中都有广泛的用途。这些设备包括但不限于&#xff1a; ATM自动终端机&#xff1a;当ATM机的磁条读卡器出现故障或读卡不灵敏时&#xff0c;可以使用CR80清…

H800基础能力测试

H800基础能力测试 参考链接A100、A800、H100、H800差异H100详细规格H100 TensorCore FP16 理论算力计算公式锁频安装依赖pytorch FP16算力测试cublas FP16算力测试运行cuda-samples 本文记录了H800基础测试步骤及测试结果 参考链接 NVIDIA H100 Tensor Core GPU Architecture…

CVPR2022医疗图像-GBCNet网络:胆囊癌(GBC)超声(USG)图像检测模型

Surpassing the Human Accuracy:Detecting Gallbladder Cancer from USG Images with Curriculum Learning&#xff1a;超越人类的准确性:基于课程学习的USG图像检测胆囊癌 目录 一、背景与意义 二、介绍 三、网络框架 3.1 区域选择网络 3.2 MS-SoP分类器 3.3 多尺度块 …

linux创建离线yum源给局域网机器使用

适用场景&#xff1a;在封闭的内网环境中&#xff0c;无法使用互联网进行安装各种rpm包的时候&#xff0c;离线yum源可以解决大部分问题&#xff0c;配置号后可直接使用yum进行安装包 1.准备好镜像源ISO&#xff1a; 例如以下示例&#xff0c;具体可参考自己的系统进行下载&a…

vscode插件-07Java

文章目录 Extension Pack for JavaSpring Initializr Java SupportCodeSwingJdk下载JDK安装jdkWindows安装jdkLinux安装jdk&#xff08;以Ubuntu为例&#xff09; jdk环境变量在VScode中配置Windows系统中配置Linux系统中配置&#xff08;以Ubuntu为例&#xff09; Extension P…

文心一言 VS 讯飞星火 VS chatgpt (265)-- 算法导论20.1 4题

四、假设不使用一棵叠加的度为 u \sqrt{u} u ​ 的树&#xff0c;而是使用一棵叠加的度为 u 1 k u^{\frac{1}{k}} uk1​的树&#xff0c;这里 k 是大于 1 的常数&#xff0c;则这样的一棵树的高度是多少&#xff1f;又每个操作将需要多长时间&#xff1f;如果要写代码&#xf…

DiskCatalogMaker for Mac:专业的文件搜索与整理助手

DiskCatalogMaker for Mac&#xff0c;这款专业的文件搜索与整理助手&#xff0c;为Mac用户带来了全新的文件管理体验。它不仅能快速扫描和读取各种存储设备中的文件&#xff0c;还能创建详细的磁盘目录数据库&#xff0c;使用户能够轻松查找和管理所需文件。 软件的搜索功能强…