欢迎光临
我们一直在努力

Linux 信号(产生篇):从键盘到内核解析信号的产生方式

文章目录

  • 1. 信号的基本概念
    • 1.1 生活中的信号:从类比到技术
    • 1.2 信号的处理方式
    • 1.3 信号的定义与核心特征
  • 2. 信号的生命周期
    • 2.1 信号的三大阶段:产生 → 保存 → 处理
    • 2.2 为什么要保存信号?
  • 3. 信号的产生方式
    • 3.1 通过 kill 命令产生信号
      • 3.1.1 命令行发送信号
      • 3.1.2 signal 函数注册信号处理(系统调用)
    • 3.2 通过键盘产生信号
      • 3.2.1 `Ctrl+C` → SIGINT(2号信号)
      • 3.2.2 `Ctrl+\\` → SIGQUIT(3号信号)
      • 3.2.3 `Ctrl+Z` → SIGTSTP(19号信号)
    • 3.3 通过硬件异常产生信号
      • 3.3.1 除零错误 → SIGFPE(8号信号)
      • 3.3.2 野指针/段错误 → SIGSEGV(11号信号)
      • 3.3.3 硬件异常转软件异常的完整流程
    • 3.4 通过函数主动产生信号
      • 3.4.1 kill:向任意进程发信号
      • 3.4.2 raise:自己给自己发信号
      • 3.4.3 abort:异常终止进程
    • 3.5 通过软件条件产生信号
      • 3.5.1 alarm:闹钟信号
  • 4. 核心转储(Core Dump)
    • 4.1 什么是 Core Dump?
    • 4.2 如何开启 Core Dump
    • 4.3 如何使用 Core Dump 调试
    • 4.4 为什么云服务器默认关闭 Core Dump?
  • 5. 补充细节

封面


1. 信号的基本概念

1.1 生活中的信号:从类比到技术

信号 vs 信号量:有没有关系?没有关系,就像老婆饼里没有老婆一样。 罗列生活中的信号,提炼相关结论:

  • 信号产生之后,你知道怎么处理吗?知道。
  • 信号处理的方法,在信号产生之前就已经准备好了。
  • 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理,什么时候处理,合适的时候。
  • 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。 认识信号 → 识别产生 → 动作处理

1.2 信号的处理方式

信号的处理有三种:

  • 默认处理
  • 忽略
  • 自定义动作
  • 信号的处理就叫做信号捕捉。

    1.3 信号的定义与核心特征

    给信号下定义:外部或者其他人或者硬件给进程发送的一种异步的事件通知机制——告诉进程什么事情发生了。

    • 异步:多种事件,彼此不影响,同时发生。
    • 通知:告诉进程发生什么事情了。

    2. 信号的生命周期

    2.1 信号的三大阶段:产生 → 保存 → 处理

    信号产生 → 信号保存 → 信号处理 信号处理的三个过程

    2.2 为什么要保存信号?

    因为信号不会被立即处理,所以才需要保存起来。

    3. 信号的产生方式

    signal

    用来对特定信号进行捕捉的函数。 查看信号: 查看信号

    • 1-31:标准信号
    • 31-64:实时信号(32和33通常保留或被内核占用)

    使用的时候既可以使用名字,也可以使用数字。但是注意没有0号信号。

    3.1 通过 kill 命令产生信号

    3.1.1 命令行发送信号

    kill[编号] [进程编号]

    进程的默认动作是终止信号。

    3.1.2 signal 函数注册信号处理(系统调用)

    如何让进程执行指定动作:signal()

    #include <iostream>
    #include <signal.h>
    #include <unistd.h>
    #include <stdlib.h>

    // signo:是收到了几号信号,才让我执行的
    void sig_handler(int signo)
    {
    std::cout << "catch signal: " << signo << std::endl;
    exit(10);
    }

    int main()
    {
    // 调用一次就可以了,不用放到循环里
    signal(SIGINT, sig_handler);
    signal(3, sig_handler);

    while (true)
    {
    std::cout << "test sig…, pig " << getpid() << std::endl;
    sleep(1);
    }
    return 0;
    }

    结论:Ctrl+C 终止进程,本质上是通过键盘给目标进程发送二号信号,处理动作就是终止。

    3.2 通过键盘产生信号

    3.2.1 Ctrl+C → SIGINT(2号信号)

    Ctrl+C 是向目标进程发送二号信号(SIGINT),默认动作终止进程。

    3.2.2 Ctrl+\\ → SIGQUIT(3号信号)

    Ctrl+\\ 是向目标进程发送三号信号(SIGQUIT),默认动作终止进程。

    3.2.3 Ctrl+Z → SIGTSTP(19号信号)

    Ctrl+Z 是向目标进程发送十九号信号(SIGTSTP),默认暂停进程。用 kill -18 将其唤醒,默认变成后台进程,不再从键盘获取输入。 我们怎么知道信号的默认动作是什么?

    man 7 signal

    查看7号手册。 信号使用表

    键盘发送只能控制前台进程,无法控制后台进程——因为只有前台进程才能获取键盘输入! jobs 查看后台进程。 为什么 bash 进程自己不对信号做响应?因为 bash 将信号全部忽略了。但是9号信号依旧能杀掉 bash 进程。

    Makefile忽略告警:-w,显示全部告警:-Wall

    3.3 通过硬件异常产生信号

    如果我们的程序 div 0、野指针等,程序就会崩溃——为什么他会崩溃?

    3.3.1 除零错误 → SIGFPE(8号信号)

    #include <iostream>
    #include <signal.h>
    #include <unistd.h>
    #include <stdlib.h>

    void sig_handler(int signo)
    {
    std::cout << "catch signal: " << signo << std::endl;
    exit(10);
    }

    int main()
    {
    signal(SIGINT, sig_handler);
    signal(3, sig_handler);

    while (true)
    {
    std::cout << "test sig…, pig " << getpid() << std::endl;
    sleep(1);
    // 模拟除0
    int a = 10;
    a /= 0;
    // 模拟野指针
    int *p = nullptr;
    *p = 100;
    }
    return 0;
    }

    本质是因为:程序因为异常,导致收到了信号,让进程终止了。

    除0信号捕捉

    标志寄存器(EFlags):标志位表明当前计算是否可信。计算溢出时,标志位置为1,表明运算出现异常,当前CPU执行当前代码的一部分。

    3.3.2 野指针/段错误 → SIGSEGV(11号信号)

    问题1证明 为什么野指针也会触发硬件异常?操作系统识别到 MMU 报错。

    3.3.3 硬件异常转软件异常的完整流程

    当前CPU执行当前进程代码的一部分,但是OS如何知道当前进程是谁?操作系统内核中有一个全局的指针,永远指向当前进程正在运行的进程。 收到8号信号,用户捕捉到了这个信号,打印,没有让进程退出!OS会继续调度它,寄存器的内容都是当前进程的硬件上下文,依旧接着执行。这个进程一直在,CPU就会一直给你发消息,就会陷入死循环。 问题:为什么会收到信号?OS怎么知道当前进程 div 0 或者野指针? 进程是如何保存信号的?[1-31]用什么数据结构保存?用位图保存。信号被保存到进程的 task_struct 里。

    比特位的位置 = 信号编号;比特位的内容(1 / 0)表示是否收到信号。

    你如何理解向目标进程“发送”信号?本质是修改信号位图,也就是写信号。

    信号由谁来写?信号位图在 task_struct 中,修改位图本质就是修改内核数据结构,只有操作系统有资格修改。发送信号的方式有很多种,但是最终只能由 OS 向目标进程写信号。

    • div 0 本质是 CPU 出错了。
    • 野指针本质是 MMU + 页表转化不了,MMU 报错。这都属于硬件报错。OS 必须知道硬件报错了,操作系统是硬件的管理者,必须知道谁把我的硬件搞坏了——当前正在运行的进程。这个时候操作系统就会对目标进程写信号,就可以将硬件异常转变为软件异常,让你的进程直接崩溃。

    3.4 通过函数主动产生信号

    3.4.1 kill:向任意进程发信号

    kill

    实现一个 kill 命令: mykill.cc

    #include <iostream>
    #include <signal.h>

    void Usage(const std::string &cmd)
    {
    std::cout << "Usage: " << cmd << " signumber who " << std::endl;
    }

    int main(int argc, char *argv[])
    {
    if (argc != 3)
    {
    Usage(argv[0]);
    exit(1);
    }

    int signumber = std::stoi(argv[1]);
    pid_t pid = std::stoi(argv[2]);

    int n = kill(pid, signumber);
    (void)n;
    return 0;
    }

    loop.cc

    #include <iostream>
    #include <unistd.h>

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

    Makefile

    .PHONY:all
    all:loop_process mykill

    mykill:mykill.cc
    g++ -w -o $@ $^

    loop_process:loop.cc
    g++ -w -o $@ $^

    .PHONY: clean
    clean:
    rm -f mykill loop_process

    信号是谁发的?OS。谁让的?用户!

    3.4.2 raise:自己给自己发信号

    raise

    raise 说白了就是自举,也就是自己给自己发信号。

    #include <iostream>
    #include <signal.h>

    void hander(int sig)
    {
    std::cout << "进程捕捉到信号: " << sig << " pid: " << getpid() << std::endl;
    }

    int main()
    {
    signal(2, hander);

    while (true)
    {
    std::cout << "进程正在运行: " << getpid() << std::endl;
    sleep(2);
    raise(2);
    }
    return 0;
    }

    这个系统调用就相当于:

    kill(getpid(), number);

    3.4.3 abort:异常终止进程

    abort

    abort 引起进程直接终止,给调用者进程发送指定信号。 这个系统调用就相当于:

    void abort(void)
    {
    int n = kill(getpid(), SIGABRT);
    (void)n;
    }

    通常用来进行终止进程的,类似于 exit。很多项目里喜欢用它来终止进程,因为出异常的时候可以产生 Core 文件,根据 Core 文件来获取错误信息。

    abort 收到的是6号信号(SIGABRT),是可以被捕捉的:

    #include <iostream>
    #include <signal.h>

    void hander(int sig)
    {
    std::cout << "进程捕捉到信号: " << sig << " pid: " << getpid() << std::endl;
    }

    int main()
    {
    signal(6, hander);

    while (true)
    {
    std::cout << "进程正在运行: " << getpid() << std::endl;
    sleep(2);
    abort();
    }
    return 0;
    }

    abort信号直接被捕捉

    这个就比较特殊了,既会执行 hander 方法,又会 Aborted。前面的对信号的修改是"非此即彼"的,这里是“即可又可”的。

    3.5 通过软件条件产生信号

    信号产生方式总结:

  • 键盘 —— IO硬件
  • 异常 —— 硬件CPU、MMU
  • 系统命令 —— 用户
  • 系统调用 —— 开发者 OS 识别到了,进程向无效管道里面写入数据,就会将该进程杀掉。这个就是 OS 识别到了软件异常,OS 就杀掉了目标进程。
  • 发送信号的方式很多,围绕着用户、硬件、软件各种场景展开的——借助 OS 之手向目标进程写信号。

    3.5.1 alarm:闹钟信号

    给特定的进程设定闹钟: alerm

    seconds 秒之后的闹钟,OS 会给调用者发送一个 14号信号(SIGALRM),默认动作是终止。 怎么证明是收到了14号信号? 捕捉到14号信号

    为什么往显示器上打的时候只打了10000多?往函数里面写不打印就写了1亿多。 因为如果你不断 ++ 的过程中,其实你是在不断访问外设。不断访问显示器,不断 IO,所以效率就降低了。想要输出的结论就是 IO 的效率低。为什么要存在文件内核缓冲区,也就是为了增加效率。不仅仅是外设,还有网卡硬件通过网络传递进来的效率也很低。 为什么往显示器上打的时候只打了10000多? alarm 设置的闹钟是一次性的,那么怎么样让其不是一次性的呢?反复设置。 alarm 的返回值:alarm 的返回值是上一个闹钟剩余时间。 闹钟剩余时间

    所以千万不要将闹钟的设置放到 while 循环里,这个闹钟不会被触发,因为每次循环都在重新调 alarm,就会将上次的闹钟取消,所以你的闹钟永远不会被触发。闹钟参数设置为0,叫做取消闹钟。

    #include <iostream>
    #include <signal.h>

    int cnt = 1;

    void hander(int sig)
    {
    std::cout << "进程捕捉到信号: " << sig << " pid: " << getpid()
    << " cnt: " << cnt << std::endl;
    int n = alarm(2);
    (void)n;
    std::cout << "上一个闹钟的剩余时间: " << n << std::endl;
    }

    int main(int argc, char *argv[])
    {
    // signal(SIGALRM, hander);
    alarm(200);
    sleep(1);
    int n = alarm(0); // 取消闹钟
    std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << " n= " << n << std::endl;

    while (true)
    {
    std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << " n= " << n << std::endl;
    sleep(1);
    }
    return 0;
    }

    如何理解闹钟?如何理解闹钟是如何实现的? 闹钟实际上是一个计时器!操作系统里面有很多闹钟,OS 要不要管理所有的闹钟?要管理,先描述再组织! 描述结构体里肯定有:

  • 未来过期的时间?
  • 要触发几次?
  • 闹钟响了我要做什么?
  • 找到超时时间最近的,后面的就都不会超时了。所以我们可以利用最小堆!堆顶不超时,其余的都不会超时。如果堆顶超时,你就出堆,并将函数回调,给目标进程发送14号信号,堆就会终止了。但是 OS 没有用这种方式,OS 使用的是一种有顺序的顺序表,说堆是为了便于大家理解。

    4. 核心转储(Core Dump)

    4.1 什么是 Core Dump?

    core dumped

    进程退出了,核心转储。

    • Term:没有异常的情况下,就是单纯终止
    • Core:进程本身有错误,收到信号退出,可能会发生核心转储,设置进程退出 status core dumped 标志位给父进程 上面二者都是终止进程。 核心转储:为什么退出?在哪里退出?在内存中 -> 便于用户调试 -> 进程运行时异常信息,转储到磁盘中!核心转储是为了后续 debug。

    4.2 如何开启 Core Dump

    ulimit -a 云服务器默认是将核心转储功能关闭的。

    ulimit -c 10240

    开启核心转储。

    4.3 如何使用 Core Dump 调试

    这里引出一种实用的调试技巧,将你的代码开启 cgdb 开始调试,直接输入:

    core-file core.(编号)

    就可以直接定位到错误的地方。 形成 core 是方便我们进行事后调试的,所以说核心转储是为了后续 debug 的。

    4.4 为什么云服务器默认关闭 Core Dump?

    为什么云服务器默认是把核心转储功能关闭的?很少用到,使用也是因为这样的错误是很不稳定的。我们在写软件服务的时候,进程可能会因为各种问题挂掉,这个时候运维平台会将软件自动立即重启,挂一次重启一次,挂一次重启一次,core 文件会很占存储空间,所以一般是关掉的。

    我怎么知道当前的信息发生了核心转储呢? bash -> 子进程(执行我们的代码)-> Floating point exception 或 Floating point exception (core dumped) -> bash bash 进程等待子进程的退出信息,bash 进程拿到子进程的退出信息之后,从终止信号中得到 core dump 标志位。

    5. 补充细节

    细节一:信号自定义捕捉,如果你的捕捉方法不退出,进程可能会不退出了。如果我把所有信号都自定义了,那就都不退出了?(变成后台进程了)

    注意:9号信号(SIGKILL)不可被自定义,是管理员信号。

    细节二:信号处理是谁处理?有没有创建新进程?信号处理是进程自己做的。显然没有创建新的进程 细节三:

    • SIG_DFL:默认处理,执行该信号的系统默认动作
    • SIG_IGN:忽略。就是处理了,但是什么动作都不做

    9号(SIGKILL)和 19号(SIGSTOP)不仅不可被忽略,也不可被自定义捕捉。

    signal(SIGINT, sig_handler);

    发送信号的本质:就是 OS 写信号,利用系统调用。


    欢迎大家批评指正!!!

    赞(0)
    未经允许不得转载:171主机测评 » Linux 信号(产生篇):从键盘到内核解析信号的产生方式
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址