【Linux学习笔记】信号的深入理解之软件条件产生信号

发布于:2025-09-06 ⋅ 阅读:(12) ⋅ 点赞:(0)

【Linux学习笔记】信号的深入理解之软件条件产生信号

在这里插入图片描述

🔥个人主页大白的编程日记

🔥专栏Linux学习笔记


前言

哈喽,各位小伙伴大家好!上期我们讲了Linux基本指令及其分析(一) 今天我们讲的是Linux基本指令及其分析(二)。话不多说,我们进入正题!向大厂冲锋!
在这里插入图片描述

一 由软件条件产生信号

SIGPIPE 是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍 alarm 函数和 SIGALRM 信号。

NAME
    alarm - set an alarm clock for delivery of a signal

SYNOPSIS
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);

RETURN VALUE
    alarm() returns the number of seconds remaining until any previously
    scheduled alarm was due to be delivered, or zero if there was no previ-
    ously scheduled alarm.
  • 调用 alarm 函数可以设定一个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程。
  • 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

1.1 基本alarm验证体会IO效率问题

程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。必要的时候,对SIGALRM信号进行捕捉

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>

using func_t = std::function<void()>;

int count = 0;
std::vector<func_t> gfuncs;

// 把信号 替换 成为 硬件中断
void handler(int signo)
{
    for(auto &f : gfuncs)
    {
        f();
    }
    std::cout << "count : " << gcount << std::endl;
    int n = alarm(1); // 重设闹钟, 会返回上一次闹钟的剩余时间
    std::cout << "剩余时间 : " << n << std::endl;
}

int main()
{
    //gfuncs.push_back([](){ std::cout << "我是一个内核刷新操作" << std::endl; });
    //gfuncs.push_back([](){ std::cout << "我是一个检测进程的间片的操作, 如果时间片到了, 我会切换进程" << std::endl; });

    //gfuncs.push_back([](){ std::cout << "我是一个内存管理操作, 定期清理操作系统内部的内存碎片" << std::endl; });

    alarm(1); // 一次性的闹钟, 超时alarm会自动被取消
    signal(SIGALRM, handler);

    while (true)
    {
        pause();
        std::cout << "我醒来了..." << std::endl;
        count++;
    }
}

NAME
    pause - wait for signal

SYNOPSIS
    #include <unistd.h>

DESCRIPTION
    pause() causes the calling process (or thread) to sleep until a signal
    is delivered that either terminates the process or causes the invoca-
    tion of a signal-catching function.

RETURN VALUE
    pause() returns only when a signal was caught and the signal-catching
    function returned. In this case, pause() returns -1, and errno is set
    to EINTR.

📌 结论:
• 闹钟会响⼀次,默认终止进程
• 有IO效率低

📌 结论:
• 闹钟设置⼀次,起效⼀次
• 重复设置的方法
• 如果时间允许,可以测试⼀下 alarm(0)

1.2 如何理解软件条件

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。

1.3 如何简单快速理解系统闹钟

系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。

现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:

struct timer_list {
    struct list_head entry;
    unsigned long expires;
    void (*function)(unsigned long);
    unsigned long data;
    struct tvec_t_base_s *base;
};

我们不在这部分进行深究,为了理解它,我们可以看到:定时器超时时间expires和处理方法function。

操作系统管理定时器,采用的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为“堆结构”。

1.4 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

1.4.1 模拟除0

#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    //signal(SIGFPE, handler); // 8) SIGFPE
    sleep(1);
    int a = 10;
    a/=0;
    while(1);
    return 0;
}

1.4.2 模拟野指针

#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    //signal(SIGSEGV, handler);
    sleep(1);
    int *p = NULL;
    *p = 100;
    while(1);
    return 0;
}

通过上面的实验,我们可能发现:

发现一直有8号信号产生被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS
会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。

除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信号的现象。访问非法内存其实也如此,大家可以自行实验。
在这里插入图片描述

1.5 子进程退出core dump

在这里插入图片描述

#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>

int main()
{
    if (fork() == 0)
    {
        sleep(1);
        int a = 10;
        a /= 0;
        exit(0);
    }

    int status = 0;
    waitpid(-1, &status, 0);
    printf("exit signal: %d, core dump: %d\n", status&0x7F, (status>>7)&1);
    return 0;
}
$ man 7 signal
1.5.1 Core Dump
  • SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
  • 首先解释什么是Core
    Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
  • 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做 Post-mortem
    Debug(事后调试)。
  • 一个进程允许产生多大的 core 文件取决于进程的 Resource Limit(这个信息保存在 PCB 中)。默认是不允许产生 core
    文件的,因为 core 文件中可能包含用户密码等敏感信息,不安全。
  • 在开发调试阶段可以用 ulimit 命令改变这个限制,允许产生 core 文件。首先用 ulimit 命令改变 Shell 进程的
    Resource Limit ,如允许 core 文件最大为 1024K:
$ ulimit -c 1024
$ ulimit -c 1024
$ ulimit -a
core file size (blocks, -c) 1024
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7643
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 65535
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7643
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

在这里插入图片描述

后言

这就是信号的深入理解之软件条件产生信号。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~


网站公告

今日签到

点亮在社区的每一天
去签到