【Linux】进程信号

1. 信号快速认识

1.1 简单认识信号

信号是内核发给进程的 "简单通知",用来让进程做某件事

总结:

  • 如何能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性
  • 信号产生之后,如何处理也知道
  • 如果信号没有产生,怎么处理信号吗? 知道。所以信号的处理方法,在信号产生之前,已经准备好了
  • 处理信号,立即处理吗?可能正在做优先级更高的事情,不会立即处理?什么时候处理?合适的时候。
  • 信号到来 | 信号保存 | 信号处理
  • 怎么进行信号处理啊?a.默认 b.忽略 c.自定义,后续都叫做信号捕捉

1.2 技术应用角度信号

一个系统函数signal

平常结束进程使用的 Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,证明一下,这⾥需要引⼊一个系统调用函数

cpp 复制代码
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⽅法
cpp 复制代码
#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!
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!
I am a process, I am waiting signal!

为什么这里进程不退出?

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

1.3 信号概念

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

1.3.1 查看信号

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

1.3.2 信号处理

  1. 忽略 此信号

    cpp 复制代码
    #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);
        }
    }
    
    $ g++ sig.cc -o sig
    $ ./sig
    我是进程: 212681
    I am a process, I am waiting signal!
    I am a process, I am waiting signal!
    ^C^C^C^C^C^CI am a process, I am waiting signal! // 输⼊ctrl+c毫⽆反应 
    I am a process, I am waiting signal!
  2. 执行该信号的默认 处理动作

    cpp 复制代码
    #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!
    ^C // 输⼊ctrl+c,进程退出,就是默认动作 
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自定义捕捉一个信号

2. 产生信号

2.1 终端按键

2.1.1 理解OS如何得知键盘有数据

2.1.2 初步理解信号起源

  • 信号其实是从纯软件角度,模拟硬件中断的行为
  • 只不过硬件中断是发给CPU,信号是发给进程

2.2 调用系统命令

当进程开始死循环,在后台执行死循环程序,然后⽤kill命令给它发SIGSEGV信号,就可以结束进程

bash 复制代码
$ kill -SIGSEGV 213784
$ // 多按⼀次回⻋
[1]+ Segmentation fault ./sig
  • 213784 是 sig 进程的pid
  • 指定发送某种信号的 kill 命令可以有多种写法,上面的命令还可以写成 kill -11 213784 ,11 是信号 SIGSEGV 的编号

2.3 函数产生

1)kill

cpp 复制代码
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

2)raise

给当前进程发送指定的信号(自己给自己发信号)

cpp 复制代码
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.

示例

cpp 复制代码
#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

3)abort

使当前进程收到信号而异常终止

cpp 复制代码
NAME
         abort - cause abnormal process termination
SYNOPSIS
         #include <stdlib.h>
         void abort(void);
RETURN VALUE
         The abort() function never returns.
 // 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值
cpp 复制代码
#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号信号
//虽然捕捉了,但是还是要退出

abort () 的执行流程:

  • 调用 abort () → 向自身发送 SIGABRT 信号
  • 如果信号被捕捉,先执行信号处理函数 handler
  • 处理完后,abort () 会恢复 SIGABRT 的默认行为,再次发送 SIGABRT,导致进程终止

2.4 软件条件

  • 在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制
  • 这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等

alarm闹钟函数

  • alarm 函数 可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程
  • alarm返回值是0或者是以前设定的闹钟时间还余下的秒数,返回值是0或者是以前设定的闹钟时间还余下的秒数

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

  • 系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟技术
  • 现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:
cpp 复制代码
struct timer_list {
 struct list_head entry;
 unsigned long expires;
 void (*function)(unsigned long);
 unsigned long data;
 struct tvec_t_base_s *base;
};
  • 为了简单理解,可以把它在组织成为**"堆结构"**

2.5 硬件异常

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号

  • 例如除0操作,CPU运算单元会发生异常,内核会把异常解释为信号SIGFPE信号发送给进程
  • 还比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程

注:实际上 OS 会检查应⽤程序的异常情况,其实在CPU中有一些控制和状态寄存器。除零异常后,并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信号的现象

Core Dump

什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump

⼀个进程允许产生多大的 core 文件取决于进程的 Resource Limit (这个信息保存在PCB中)。默认是不允许产生core文件的,因为 core文件中可能包含用户密码等敏感信息,不安全

在开发调试阶段可以用 ulimit 命令改变这个限制,允许产生core文件。首先用 ulimit 命令改变 Shell 进程的 Resource Limit ,如允许 core文件最大为 1024K: $ ulimit -c 1024

3. 保存信号

3.1 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞 (Block)某个信号
  • 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,忽略是在递达之后可选的一种处理动作

3.2 在内核中的表示

每个信号都有两个标志位分别表示阻塞( block**)** 和未决( pending,1 = 该信号**已产生、未处理)**还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志

上图的例子中:

  • SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻 塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数 sighandler

3.3 sigset_t

每个信号只有⼀个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,这个类型 可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态

阻塞信号集也叫做当前进程的信号屏蔽字,这里的"屏蔽"应该理解为阻塞而不是忽略

3.4 信号集操作函数

cpp 复制代码
#include <signal.h> 
int sigemptyset(sigset_t *set); 
int sigfillset(sigset_t *set); 
int sigaddset(sigset_t *set, int signo); 
int sigdelset(sigset_t *set, int signo); 
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号
  • 注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

3.4.1 sigprocmask

读取或更改进程的信号屏蔽字(阻塞信号集)

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改 进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字 备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了 how参数的可选值

3.4.2 sigpending

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);

读取当前进程的未决信号集,通过set参数传出
调⽤成功则返回0,出错则返回-1

4. 捕捉信号

4.1 捕捉的流程

如果信号的处理动作用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

信号处理的时间?进程从内核态返回用户态的时候,进行信号检查

捕捉过程中如果需要使用自定义函数,OS要做身份切换

4.2 穿插话题 - 操作系统是怎么运行的

4.2.1 硬件中断

  • 中断向量表就是操作系统的一部分,启动就加载到内存中了
  • 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
  • 由外部设备触发的,中断系统运行流程,叫做硬件中断

4.2.2 时钟中断

  • 进程可以在操作系统的指挥下被调度被执行,那么操作系统被谁推动执行?
  • 外部设备可以触发硬件中断,但是需要用户或者设备自己触发,有没有可以自己定期触发的设备?

操作系统就在硬件的推动下,自动调度了

4.2.3 死循环

  • 操作系统的本质:就是一个死循环
  • 操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可
cs 复制代码
void main(void) /* 这⾥确实是void,并没错。 */ 
{ /* 在startup 程序(head.s)中就是这样假设的。 */ 
 ...
 for (;;)
 pause();
}
  • 操作系统就在硬件时钟的推动下,自动调度了
  • 所以,就产生了时间片,CPU主频概念,主频可以作为OS调度执行速度的参考之一

4.2.4 软中断

  • 外部硬件中断,需要硬件设备触发
  • 软件是否也能触发和硬件时钟中断类似的 "中断 / 异常逻辑"?可以
  • CPU 为操作系统实现系统调用,也设计了专用汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑

图上流程为: 用户态程序把系统调用号 n 存入eax,执行int 0x80触发软中断;CPU查中断向量表 IDT ,跳转到system_call总入口;system_call用 n 作为下标查系统调用表 sys_call_table,执行对应内核函数;最后把结果返回给用户态

  • 用户层怎么把系统调用号给操作系统?寄存器(如EAX)
  • 操作系统怎么把返回值给用户?寄存器或者用户传入的缓冲区地址
  • 系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执 行系统调用的处理方法,这个方法会根据系统调用号,自动查表,执行对应的方法
  • 系统调用号的本质:数组下标

为什么调用的系统调用,没有见过什么 int 0x80 或者 syscall 呢?都是直接调用上层的函数?

  • 因为Linux的gnu C标准库,把几乎所有的系统调用全部封装了

  • #define SYS_ify(syscall_name) _NR##syscall_name :是一个宏定义,用于将系统调用的名称转换为对应的系统调用号。如: SYS_ify(open) 会被展开为__NR_open
  • 系统调用号,不是 glibc 提供的,是内核提供的,内核提供系统调用入口函数 man 2 syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头文件或者开发入口,让上层语言的设计者使用系统调用号,完成系统调用过程

4.2.5 缺页中断?内存碎片处理?除零野指针错误?

cpp 复制代码
void trap_init(void)
{
 int i;
 set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。 
 set_trap_gate(1,&debug);
 set_trap_gate(2,&nmi);
 set_system_gate(3,&int3); /* int3-5 can be called from all */
 set_system_gate(4,&overflow);
 set_system_gate(5,&bounds);
 set_trap_gate(6,&invalid_op);
 set_trap_gate(7,&device_not_available);
 set_trap_gate(8,&double_fault);
 set_trap_gate(9,&coprocessor_segment_overrun);
 set_trap_gate(10,&invalid_TSS);
 set_trap_gate(11,&segment_not_present);
 set_trap_gate(12,&stack_segment);
 set_trap_gate(13,&general_protection);
 set_trap_gate(14,&page_fault);
 set_trap_gate(15,&reserved);
 set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
⻔。 
 for (i=17;i<48;i++)
 set_trap_gate(i,&reserved);
 set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。 
 outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。 
 outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。 
 set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。 
}
  • 缺页中断、内存碎片、除零、野指针等异常,都会转为 CPU 内部软中断,由中断处理例程统一处理:有的负责申请内存、填充页表、建立映射,有的处理内存碎片,有的向进程发送信号并终止进程

所以:

  • 操作系统就是躺在中断处理例程上的代码块!
  • CPU内部的软中断,如int 0x80或者syscall,叫做陷阱 ,陷阱是程序主动、有意发起的 "进入内核" 请求
  • CPU内部的软中断,如除零/野指针等,叫做异常 ,异常是CPU 执行出错时触发的同步中断,由内核异常处理程序统一处理

4.3 如何理解内核态和用户态

4.4 sigaction(?)

cpp 复制代码
#include <signal.h> 
int sigaction(int signo, const struct sigaction *act, struct sigaction 
*oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调⽤成功则返回0,出错则返回-1。 signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统 默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是⼀个回调函数,不是被main函数调用,而是被系统所调用

5. 可重入函数

是指在执行过程中可以被安全中断,并在中断返回前再次被调用(重入) ,仍能保证执行结果正确、数据不混乱的函数。它是并发安全、无锁、纯函数

如果一个函数符合以下条件之⼀则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构
相关推荐
Elastic 中国社区官方博客9 分钟前
使用 EDOT Browser 和 Kibana 进行 OpenTelemetry 浏览器端埋点
大数据·服务器·数据库·elasticsearch·搜索引擎·单元测试·可用性测试
智能运维指南23 分钟前
2026 年企业IT运维监控系统选型指南:全栈可观测平台对比与落地建议
运维
sdm07042729 分钟前
进程间通信
linux·运维·服务器
蚰蜒螟33 分钟前
Linux内核启动(init)与程序执行(execve)深度解析:从kernel_init到load_elf_binary
linux·运维·服务器
thethefighter40 分钟前
信创综合档案管理系统单机版部署与使用
linux·银河麒麟·档案管理系统·单机版·nhdeep·信创版·综合档案管理系统
hhb_6181 小时前
Go高性能并发编程实战与底层原理剖析
运维·网络·golang
道清茗1 小时前
【RH294知识点汇总】第 6 章 《 管理复杂的 Play 和 Playbook 》常见问题
linux·服务器·网络
哼?~1 小时前
序列化与反序列化
linux·网络
带娃的IT创业者2 小时前
Claude Code Routines 深度解析:重新定义 AI 辅助编程的工作流自动化
运维·人工智能·自动化·ai编程·工作流·anthropic·claude code
broadview_java2 小时前
搬瓦工修改SSH端口
运维·网络·ssh