【Linux学习笔记】认识信号和信号的产生

发布于:2025-07-14 ⋅ 阅读:(14) ⋅ 点赞:(0)

【Linux学习笔记】认识信号和信号的产生

在这里插入图片描述

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

🔥专栏Linux学习笔记


前言

哈喽,各位小伙伴大家好!上期我们讲了消息队列和信号量 今天我们讲的是认识信号和信号的产生。话不多说,我们进入正题!向大厂冲锋!
在这里插入图片描述

一. 信号快速认识

1.1 生活角度的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”。
  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道了有一个快递已经来了。本质上是你“记住了有一个快递要去取”。
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
  • 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

基本结论:

  • 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。

  • 信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。

  • 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。

  • 信号到来 | 信号保存 | 信号处理

  • 怎么进行信号处理啊?a.默认 b.忽略 c.自定义,后续都叫做信号捕捉。

1.2 技术应用角度的信号

1.2.1 ⼀个样例
// sig.cc
#include <iostream>
#include <unistd.h>

int main()
{
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}
  • 用户输入命令,在Shell下启动一个前台进程
  • 用户按下 Ctrl+C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
  • 前台进程因为收到信号,进而引起进程退出
NAME
signal - ANSI C signal handling

SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数说明:
signum:信号编号[后面解释,只需要知道是数字即可]
handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法

而且其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下,这⾥需要引入一个系统调用函数

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

void handler(int signumber)
{
    std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程:" << getpid() << std::endl;
    signal(SIGINT/*2*/, handler);
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}
$ g++ sig.cc -o sig
$ ./sig
我是进程:212569
I am a process, I am waiting signal!
^C我是:212569,我获得了一个信号:2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是:212569,我获得了一个信号:2
I am a process, I am waiting signal!
1.2.2 一个系统函数
### NAME
signal - ANSI C signal handling

参数说明:

 - signum: 信号编号 [后面解释,只需要知道是数字即可]
 - handler: 函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法
 - 参数说明:
signum:信号编号[后⾯解释,只需要知道是数字即可]
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法

而其实,Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明一下,这里需要引入一个系统调用函数

开始测试

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

void handler(int signumber)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGINT, handler);
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
// 用本机编译运行
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!

思考:

  • 这里进程为什么不退出?
  • 这个例子能说明哪些问题?信号处理,是自己处理
  • 请将生活例子和 Ctrl+C 信号处理过程相结合,解释一下信号处理过程:进程就是你,操作系统就是快递员,信号就是快递,发信号的过程就类似给你打电话

注意:

  • 要注意的是,signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!!
  • Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样 Shell 不必等待进程结束就可以接受新的命令,启动新的进程。
  • Shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  • 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
  • 关于进程间关系,我们在网络部分会专门来讲,现在就了解即可。
  • 可以渗透 &nohup

1.3 信号概念

信号是进程之间事件异步通知的⼀种方式,属于软中断。

1.3.1 查看信号

每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

1.3.2 信号处理

可选的处理动作有以下三种:

  1. 忽略此信号
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signumber)
{
    std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程:" << getpid() << std::endl;
    signal(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}
  1. 执行该信号的默认处理动作
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signumber)
{
    std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程:" << getpid() << std::endl;
    signal(SIGINT/*2*/, SIG_DFL);
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}

$ g++ sig.cc -o sig
$ ./sig
我是进程:212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
  1. 提供⼀个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种⽅式称为自定义捕捉(Catch)⼀个信号。
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */

/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);

// 其实SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型
^C // 输入ctrl+c,进程退出,就是默认动作

在这里插入图片描述

二. 产生信号

当前阶段:

在这里插入图片描述

2.1 通过终端按键产生信号

2.1.1 基本操作
  • Ctrl+C (SIGINT) 已经验证过,这里不再重复
  • Ctrl+\ (SIGQUIT) 可以发送终止信号并生成core dump文件,用于事后调试(后面详谈)
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signumber)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGQUIT, handler);
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 213056
^Z  // 注释掉13行代码
$ ./sig
我是进程: 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C //Quit
  • Ctrl+Z (SIGTSTP) 可以发送停止信号,将当前前台进程挂起到后台等。
#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int signumber)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGTSTP, handler);
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}
2.1.2 理解OS如何得知键盘有数据
2.1.3 初步理解信号起源

注意:

  • 信号其实是从纯软件角度,模拟硬件中断的行为
  • 只不过硬件中断是发给CPU,而信号是发给进程
  • 两者有相似性,但是层级不同,这点我们后面的感觉会更加明显
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

void handler(int signumber)
{
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}

int main()
{
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGTSTP, handler);
    while(true){
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}
$ ./sig
我是进程: 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z // 多按一次回车
[1]+  Stopped                 ./sig
$ ./sig
我是进程: 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+  Stopped                 ./sig
$ web: http://code/tests_jobs

在这里插入图片描述

2-2 调用系统命令向进程发信号

示例代码:

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

int main()
{
    while(true){
        sleep(1);
    }
}
$ g++ sig.cc -o sig  // step 1
$ ./sig &           // step 2
$ ps ajx | head -1 && ps ajx | grep sig // step 3
PPID   PID  PGRP  SID  TTY      TPGID STAT  UID   TIME COMMAND
211805 213784 213784 211805 pts/0  213792 S   1002  0:00 ./sig
$ kill -SIGSEGV 213784
$ ./sig
[1]+  Segmentation fault      ./sig
  • 213784 是 sig 进程的pid。之所以要再次回车才显示 Segmentation fault,是因为在 213784进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell 不希望 Segmentationfault信息和用户的输入交错在一起,所以等用户输入命令之后才显示
  • 指定发送某种信号的 kill 命令可以有多种写法,上面的命令还可以写成 kill -11 213784,11 是信号 SIGSEGV的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错误,给它发 SIGSEGV 也能产生段错误。

2.3 使用函数产生信号

2.3.1 kill

kill 命令是调用 kill 函数实现的。kill 函数可以给一个指定的进程发送指定的信号。

NAME
       kill - send signal to a process

SYNOPSIS
       #include <sys/types.h>
       #include <signal.h>

       int kill(pid_t pid, int sig);

RETURN VALUE
       On  success  (at  least  one  signal  was  sent),  zero  is  returned.  On  error,
       -1  is  returned,  and  errno  is  set  appropriately.

示例:实现自己的 kill 命令

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

// mykill -signumber pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;
        return 1;
    }
    int number = std::stoi(argv[1]+1); // 去掉-
    pid_t pid = std::stoi(argv[2]);
    kill(pid, number);
    return 0;
}
$ g++ sig.cc -o sig  // step 1
$ ./sig &            // step 2
$ ps ajx | head -1 && ps ajx | grep sig // step 3
$ kill -SIGSEGV 213784
$ ./sig
[1]+  Segmentation fault
2.3.2 raise

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

NAME
  raise - send a signal to the caller

SYNOPSIS
  #include <signal.h>
  int raise(int sig);

RETURN VALUE
  raise() returns 0 on success, and nonzero for failure.

示例:

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

void handler(int signumber)
{
    // 整个代码就只有这一处打印
    std::cout << "获取了一个信号: " << signumber << std::endl;
}

// mykill -signumber pid
int main()
{
    signal(2, handler);    // 先对2号信号进行捕捉
    // 每隔1s,自己给自己发送2号信号
    while(true)
    {
        sleep(1);
        raise(2);
    }
}
$ g++ raise.cc -o raise
$ ./raise
获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
2.3.3 abort

abort 函数使当前进程接收到信号而异常终止

NAME
  abort - cause abnormal process termination

SYNOPSIS
  #include <stdlib.h>
  void abort(void);

RETURN VALUE
  The abort() function never returns.
  // 就像exit函数一样,abort函数总是会成功的,所以没有返回值。

示例:

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

void handler(int signumber)
{
    // 整个代码就只有这一处打印
    std::cout << "获取了一个信号: " << signumber << std::endl;
}

// mykill -signumber pid
int main()
{
    signal(SIGABRT, handler);
    while(true)
    {
        sleep(1);
        abort();
    }
}
$ g++ Abort.cc -o Abort
$ ./Abort
获取了一个信号: 6  // 实验可以得知,abort给自己发送的是固定6号信号,虽然捕捉了,但是还是要退出
Aborted
// 注释掉15行代码
$ ./Abort
Aborted

在这里插入图片描述

2.4 硬件异常产生信号

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

2.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;
}
2.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;
}
[hblocalhost code_test]$ .(sig)
Segmentation fault (core dumped)
[hblocalhost code_test]$ 
[hblocalhost code_test]$ cat sig.c
#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;
}

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

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

  • 发现一直有8号信号产生被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何处理后续的流程,实际上OS会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
  • 除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信号的现象。访问非法内存其实也是如此,大家可以自行实验。

在这里插入图片描述

后言

这就是认识信号和信号的产生。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~


网站公告

今日签到

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