🌟 各位看官好,我是

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux的信号产生,从多种信号产生方式反推理解之前一直未解决的疑惑。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

目录

信号产生

信号产生方式

键盘产生

kill命令产生

函数产生信号

kill系统调用

raise

abort 

软件条件

验证IO效率问题

理解闹钟

模拟OS行为

硬件异常

理解 /0

理解野指针

如何理解键盘产生信号?

总结


信号产生

对信号的概念进行一定的理解后,就可以从时间维度上讲解信号产生的话题

信号产生方式

键盘产生

  • Ctrl+C (SIGINT) 已经验证过,这⾥不再重复
  • Ctrl+\(SIGQUIT)可以发送终⽌信号并⽣成core dump⽂件,⽤于事后调试(后⾯详谈)
  • Ctrl+Z(SIGTSTP)可以发送停⽌信号,将当前前台进程挂起到后台等。

至于键盘是如何产生信号的话题需要到后面进行揭晓.

kill命令产生

我们之前演示过用Kill命令的9号信号可以杀死进程.这里让每一个信号都有自己的捕捉方式或者忽视该信号,验证每一个信号是否都会调用自定义函数.

void handler(int signo)
{
    std::cout <<"我这个进程:" << getpid() << ",抓到了一个信号: " << signo << std::endl;
    // 我没有在这个函数里终止进程哦!
}

int main
{
    for(int i = 1;i <= 31; i++)
        signal(i, handler);
        //signal(i, SIG_IGN);

    while (true)
    {
        std::cout<<"我是一个进程:"<<getpid()<<std::endl;
        sleep(1);
    }
    retrun 0;
}

一 一 验证后,会发现有几个信号比较特别, 9号 和 19号 信号不可被捕捉,不可被忽略 --> 为什么这样做呢? --> 防止有人设置病毒,并且将每个信号都设置成自定义捕捉方法,导致你无法杀掉该进程.

函数产生信号

kill系统调用

kill 命令实际上底层一定调用了 kill函数。 kill 函数可以给⼀个指定的进程发送指定的信号。

int kill(pid_t pid, int sig);

pid : 目标进程

sig : 什么信号

自定义实现kill命令: 

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Usage:" << argv[0] << "signumber pid" << std::endl;
        return 1;
    }
    //会把命令参数切到命令行参数表里,是一个一个的字符串,因此要stoi转成数字
    int signumber = std::stoi(argv[1]);
    pid_t target = std::stoi(argv[2]);

    int n = kill(target, signumber);
    if (n < 0)
    {
        perror("kill");
        return 2;
    }
    return 0;
}

  

raise

raise 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。

int raise(int sig);

void handler(int signal)
{
    std::cout << "收到了一个信号:" << signal << std::endl;
}

int main()
{
    signal(2,handler);
    while(true)
    {
        sleep(1);
        raise(2);
    }
    return 0;
}
abort 

abort 函数使当前进程接收到指定信号⽽异常终⽌。
void abort(void);

那么abort是给当前进程发送几号信号呢?给自己发送6号新号,如何证明呢?

软件条件

SIGPIPE 是⼀种由软件条件产⽣的信号,在“管道”中已经介绍过了。坏掉的管道,就是软件不满足 --> 也是OS给目标进程发送信号!

本节主要介绍 alarm 函数 和 SIGALRM 信号。

unsigned int alarm(unsigned int seconds);

  • 调用 alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终止当前进程。这个信号就是14号信号(默认动作为Term).

  

  • 函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
  • 闹钟响一次,默认终止进程

  • alarm调用,一次只运行一个进程,设置一个闹钟,以最新的为准第二次设置闹钟的新时间,会取消上一次的闹钟,返回上一次闹钟的剩余时间!
  • 闹钟为0则为取消闹钟

验证IO效率问题

如下图所示可见printf函数这类IO操作会降低效率

理解软件条件:

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

理解闹钟

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

如果每一个进程都有一个闹钟,有上百个进程就有上百个闹钟。那么这些闹钟是否需要被OS管理?如何管理?提出来一种对闹钟进行管理的方案 -->

先描述! 再组织!    

内核中定时器数据结构:定时器超时时间expires和处理⽅法function。

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

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

模拟OS行为

通过alarm设定闹钟,使得每1s都会向该进程发送14号新号,14号信号已经设置为自定义捕捉,去执行自定义函数,理解为硬件中断.在这个自定义函数中,可以做自己想做的事情,如进程调度、进程切换等等.就如同OS每固定一段时间都会去检测时间片是否到了,是否需要进行进程切换等等...

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

void FlushDisk()
{
    std::cout << "我是一个刷盘的操作" << std::endl;
}

void sched()
{
    std::cout << "我是一个进程调度" << std::endl;
}

void handler(int signo)
{
    for(auto &f : cb)
    {
        f();
    }

    (void)signo;
    std::cout << "我是一个信号捕捉函数, 我被调用了" << std::endl;
    alarm(1);
}

// 写一段代码,理解OS
int main()
{
    cb.push_back(FlushDisk);
    cb.push_back(sched);

    signal(SIGALRM, handler);
    alarm(1);
    while(true)
    {
        pause(); // 等待信号到来,否则暂停
    }
}

硬件异常

常见的异常如 /0 错误 , 野指针 --> 会导致我们的进程崩溃了.

但是我们从未想过为什么这些异常会导致进程崩溃呢?

因为软件问题,被操作系统识别,给目标进程发送了信号然后进程处理信号,默认终止了进程!

下面写两段程序,分别是 /0 错误 以及 野指针 的异常,先是验证 /0 错误 和 野指针 是向目标进程发送几号信号,再来从深层进行理解.

  

产生信号都是有操作系统产生的.那么是谁让OS产生的?用户,OS内部的软件条件. 

理解 /0

首先,我们的代码和数据会加载到物理地址空间,再通过页表和虚拟地址空间建立映射关系,CPU执行代码的时候,一定是在调度某一个进程! 假设此时CPU内的一个寄存器eax存放着a为10的值,ebx存放着0,此时会有 /0 错误,而CPU里有一个寄存器EFLAGS(控制盒状态寄存器),简单理解为⼀个位图,对应着⼀些状态标记位、溢出标记位,记录CPU单次运算时所对应的状态.此时当CPU执行 /0 这一行代码时,由EFLAGS检测到是非法操作,CPU触发硬件异常,那么OS需不需要知道呢?OS是软硬件资源的管理者,因此告诉OS"我出错了,快来帮我" --> OS靠 struct task_struct *current(进程描述结构体),它永远指向 “当前正在被 CPU 调度的进程” , 知道了是这个进程的代码在搞事情,就需要对这个进程进行某种操作,即发送信号.CPU内部的寄存器本质是:当前进程的硬件上下文.一旦该进程被默认杀掉了,进程的硬件上下文就不存在了,从而使CPU的报错就没有了!

因此本质是:OS是为了恢复CPU的正常运行

那为什么当我对8号信号执行自定义捕捉时,会被OS一直调度啊?

当CPU告诉OS"我报错了",OS会向目标进程发送信号,又因为8号信号有了自定义捕捉方法,并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以CPU中还保留上下⽂数据以及寄存器内容
,当执行完这自定义方法时,此时又会恢复硬件上下文,让CPU继续执行这行数据,又检测到是非法操作,依次循环下去.看起来也是在被一直调度,但这其实是异常的情况,占用着CPU的资源!!!

 

理解野指针
int *p = nullptr;
*p = 10;

OS是如何识别野指针问题,并终止进程的? 

野指针 --> 硬件报错 --> OS写信号 --> 进程终止!!! 

如何理解键盘产生信号?

 

那么键盘具体是如何让OS知道键盘外设有数据了?举ctrl + c 来说:

当我们键盘上按下 ctrl + c 时,会向CPU发送硬件中断,此时CPU通过里面内置的针脚识别发出硬件中断的是键盘(实际上就是高低电频) , 告诉CPU要键盘被按下了,此时OS会去识别是普通数据还是组合键(即将数据从外设读入内存),而OS内部内置了一套中断向量表(之后会扒出源码提供),执行键盘处理方法.

总结

本文详细介绍了Linux系统中的信号产生方式及其原理。主要内容包括:

  1. 键盘组合键产生信号(如Ctrl+C、Ctrl+\、Ctrl+Z);
  2. 通过kill命令和函数产生信号,重点分析了9号和19号信号的不可捕捉特性;
  3. 软件条件产生的信号(如alarm函数和SIGALRM信号);
  4. 硬件异常产生的信号(如除零错误和野指针访问)。

文章深入探讨了信号产生的底层机制,包括CPU寄存器状态、中断处理等硬件层面原理,并解释了操作系统如何管理这些信号。最后通过代码示例展示了信号处理的实际应用场景,如模拟操作系统调度等。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐