浅学信号与中断

0 阅读15分钟

一. 信号

1.什么是信号?

信号就是个 消息 , 可以被发送到 一个进程 或 多个进程(10086给广大用户发短信,好消息好消息,特大好消息,100G流量1块钱!)。

每个信号本质就是个 ”数字“ 而已。

pid ——> process id (进程ID)

tgid ——> thread group id (线程组ID)

image.png Linux内核里面如何根据pid快速找到对应的 task_struct 呢?

答:内核使用散列表来维护pid 和 task_struct的关系

kill -9 2334 //你一定执行过这条命令 kill -9 进程号 
linux内核先找到 pid=2334的进程,并杀掉这个进程以及tgid=2334的线程 
给进程pid=2334发送 “9号” 信号:SIGKILL

2.信号的作用 & 什么时候发信号

image.png image.png

3.信号处理大体流程

(1)信号发送

发送方式:

  • kill(pid, sig) : 向pid所在的线程组,发送一个 “sig号” 信号
  • tkill(pid, sig): 向指定的pid进程(或线程),发送一个 "sig号" 信号
  • tgkill(pid, sig, tgid): 向指定的pid进程(或线程),发送一个 "sig号" 信号(检查下这个进程的tgid是否等于参数中的tgid)
Linux中发送信号的API类型方法说明
kill(pid_t pid, int sig)系统调用/C库向进程/进程组发送信号:pid>0时发送给指定进程;pid=0时发送给当前进程组;pid=-1时发送给所有有权限的进程;pid<-1时发送给进程组ID为-pid的进程组
tkill(int tid, int sig)系统调用已废弃,向指定线程ID发送信号,不检查线程组ID。推荐使用tgkill
tgkill(int tgid, int tid, int sig)系统调用向指定线程ID发送信号,并验证其所属线程组ID(tgid)是否匹配,防止线程终止后ID重用导致误发
*rt_sigqueueinfo(pid_t pid, int sig, siginfo_t uinfo)系统调用发送信号并携带额外数据(siginfo_t结构),可用于传递自定义信息或文件描述符
*rt_tgsigqueueinfo(int tgid, int tid, int sig, siginfo_t uinfo)系统调用tgkill与rt_sigqueueinfo功能的组合,向指定线程发送带数据的信号
killpg(int pgrp, int sig)C库函数向指定进程组发送信号,等价于kill(-pgrp, sig)
raise(int sig)C库函数向当前进程自身发送信号,等价于kill(getpid(), sig)
sigqueue(pid_t pid, int sig, const union sigval value)C库函数kill()的扩展版本,可附带一个整型或指针数据,需链接-rt库
pthread_kill(pthread_t thread, int sig)C库函数向同一进程内的指定pthread线程发送信号,信号0用于检测线程是否存在
abort(void)C库函数向当前进程发送SIGABRT信号并终止进程,即使捕获该信号也会终止

信号已经发送给A进程,但是A进程未处理的此信号,称为“挂起信号” (也叫待处理信号,会存储在 task_struck中)

task_struct 中的信号数据结构:

struct task_struct{
   ......
 //信号处理 相关信息
  struct signal_struct *signal;      // 进程共享信号处理结构
  struct sighand_struct *sighand;    // 信号处理函数表
  sigset_t blocked;          // 被阻塞的信号集
  sigset_t real_blocked;     // 临时阻塞信号集(用于    rt_sigtimedwait)
  sigset_t saved_sigmask;    // 保存的信号掩码(用于TASK_RUNNING状态切换)
  struct sigpending pending; // 待处理信号队列
  unsigned long sas_ss_sp;   // 信号处理备用栈指针
  size_t sas_ss_size;        // 信号处理备用栈大小
  unsigned int sas_ss_flags; // 信号处理栈标志               
   ......
}

image.png

(2)信号处理

每个信号都有默认的处理方式:

Terminate : 终止(杀死)进程—>【Term:终止进程(进程退出)】

Dump : 终止(杀死)进程, 将进程运行的上下文信息保存到文件中,方便查询进程相关信息 —>【Core:终止进程并生成core dump文件(用于调试)】

Ignore :信号被忽略—>【Ign:忽略该信号(进程不采取任何动作)】

Stop :停止进程,将进程的状态设置为 TASK_STOPPED—>【Stop:暂停进程(进程进入停止状态)】

Contineue : 如果进程的状态是 TASK_STOPPED, 那么把它设置为 TASK_RUNNING—>【Cont:进程转为就绪态或获得CPU时间片执行代码指令】

信号信号名默认处理方式用途说明
1SIGHUPTerm终端挂起或控制进程结束,通常用于重新加载配置(如:reload nginx)
2SIGINTTerm中断进程(Ctrl+C),请求优雅终止
3SIGQUITCore退出进程并生成core dump文件(Ctrl+\),用于调试
4SIGILLCore非法指令,通常因二进制文件损坏、不兼容或栈溢出导致
5SIGTRAPCore跟踪/断点陷阱,调试器专用(如gdb)
6SIGABRTCore进程主动中止(调用abort()),通常因断言失败或调用assert()
7SIGBUSCore总线错误(内存对齐错误、访问不存在的物理地址)
8SIGFPECore浮点运算异常(除零、溢出、非法操作等)
9SIGKILLTerm强制杀死进程,无法捕获或忽略(kill -9)
10SIGUSR1Term用户自定义信号1,应用程序自由定义用途
11SIGSEGVCore段错误(非法内存访问、越界、空指针),最常见崩溃原因
12SIGUSR2Term用户自定义信号2,应用程序自由定义用途
13SIGPIPETerm管道破裂(读端关闭,写端继续写入),网络编程中常见
14SIGALRMTerm定时器到期(alarm()函数触发)
15SIGTERMTerm默认终止信号,请求优雅退出(kill默认发送)
16SIGSTKFLTTerm协处理器栈错误(Linux中已废弃,几乎不使用)
17SIGCHLDIgn子进程状态改变(停止或退出),父进程用wait()回收
18SIGCONTCont继续执行已停止的进程(与STOP对应)
19SIGSTOPStop强制停止进程,无法捕获或忽略(Ctrl+Z)
20SIGTSTPStop终端停止信号(Ctrl+Z),可捕获处理
21SIGTTINStop后台进程组尝试从终端读取
22SIGTTOUStop后台进程组尝试向终端写入
23SIGURGIgn套接字上出现紧急数据(带外数据)
24SIGXCPUCoreCPU时间限制超出(ulimit -t设定)
25SIGXFSZCore文件大小限制超出(ulimit -f设定)
26SIGVTALRMTerm虚拟时钟信号(进程实际使用的CPU时间)
27SIGPROFTerm性能分析定时器(进程+内核CPU时间)
28SIGWINCHIgn窗口大小改变(终端窗口resize)
29SIGIOTerm异步I/O事件通知(fcntl()设置)
30SIGPWRTerm电源失效(UPS低电量警告)
31SIGSYSCore无效系统调用(seccomp过滤触发)
32SIGRTMINTerm实时信号最小值,用于自定义,支持排队
33-63SIGRTMIN+1~31Term实时信号,共31个,可自定义用途
64SIGRTMAXTerm实时信号最大值
(2.1)自定义信号处理—signal :

image.png 例如: 编写程序,在自己的进程中自定义信号处理

signal_test.c
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
//自己写的程序——M进程 自定义信号处理逻辑
void func(intg sig){
    printf("sig=%d  comeing!\n", sig);
}
int main(){
    //signal(SIGINI, SIG_DFL); //默认处理————按2号信号的默认处理就行
    //signal(SIGINT, SIG_IGN); //忽略信号————忽略了2号信号
    signal(SIGINI,  func); //捕获信号,然后调用自定义处理逻辑————接收2号信号,按我的方式来
    
    return 0;
}
(2.2)自定义信号处理—sigaction :

image.png

(2.3)task_struct信号处理之——保存信号处理函数

注册的信号处理程序保存在进程对应的 task_struck 中, 通过 signal、sigaction 自定义的信号处理,都保存在 *sighand 中

struct task_struct{ 
    struct sighand_struct *sighand; // 信号处理函数表 
}

image.png image.png image.png

二. 中断

操作系统中的异常分为:

  • 故障(Fault):可恢复,返回时重新执行错误指令
  • 陷阱(Trap【系统待用】):可恢复,返回时执行下一条指令
  • 中断(Interrupt):异步事件,不影响指令流
  • 终止(Abort):不可恢复的严重错误 image.png 常识:一个CPU同一时间只能执行一条代码指令,正常情况下,CPU一般是在执行 进程/线程 image.png

中断的分类

image.png

1.异步中断

(1)I/O设备中断 它是, 外设I/O__设备CPU 交互的一种方式
I/O设备发送中断时间发送中断信号CPU执行__相对应的__中断处理程序
键盘键盘点击输入字符给CPU发中断信号CPU执行_键盘中断处理程序_处理键盘输入的字符
鼠标鼠标点击给CPU发中断信号CPU执行_鼠标中断处理程序_处理鼠标的坐标和点击状态
网卡接收到 “数据包”给CPU发中断信号CPU执行_网卡中断处理程序_将数据包拷贝到内存,并解析
磁盘数据通过DMA拷贝到物理内存完毕后给CPU发中断信号唤醒正在阻塞的进程,告诉它,你要的数据来了,在内存中呢,快去用吧!
(2)时钟中断 它是, 固定的时间片如:10秒,时钟芯片CPU 发送“时钟中断信号”

image.png 总结: 上面两类中断,都是由外部设备产生,可能发生在任意时间,这类中断并不和特定的进程关联,也就是在任意进程执行时,都有可能产生这类中断———它们叫——》异步中断

如果是 I/O设备产生的中断, CPU可以屏蔽或忽略它———也称———》可屏蔽中断

image.png

2.同步中断

这类中断由CPU自身产生,并且和当前执行的进程密切关联—它们叫—>同步中断(或异常)

(1)CPU发出的中断—— “divide error 中断

image.png

(2)CPU发出的中断——“缺页中断缺页异常

缺页异常(中断),是可以恢复的异常,我们也称它为 “故障(fault)” {属于同步中断,属于异常} image.png

(3)CPU发出的中断——“80中断” (32位操作系统触发系统调用的方式)

image.png

中断处理程序在哪里?

每一种中断都有相对应的中断处理程序,它们到底在哪里呢?如何根据中断信号找到对应的处理程序呢?

1.中断描述符表

中断描述符表(Interrupt Descriptor Table, 简称 IDT ). 在Linux内核中,每种中断都有自己唯一的中断号

IDT 维护着:中断号—对应—》中断处理程序的内存首地址

IDT存放在一个名为 idt_table 的表中,有256个表项

IDT 是操作系统启动的时候,事先生成好存放在内存中

CPU中有个名为 idtr 的寄存器存放着IDT内存地址 image.png image.png

外部中断就是I/O设备发出的中断

CPU内部就是CPU发出的中断

处理器间中断(多核CPU之间,cpu与cpu之间的交互方式)

0x00-0x1F:Intel保留的异常(不可屏蔽)
中断号名称类型Linux处理函数说明
0 (0x00)Divide Error故障divide_error除零错误
1 (0x01)Debug陷阱/故障debug调试异常
2 (0x02)NMI中断nmi不可屏蔽中断
3 (0x03)Breakpoint陷阱int3断点指令
4 (0x04)Overflow陷阱overflow溢出陷阱
5 (0x05)Bound Range Exceeded故障boundsBOUND指令越界
6 (0x06)Invalid Opcode故障invalid_op无效操作码
7 (0x07)Device Not Available故障device_not_available设备不可用(无FPU)
8 (0x08)Double Fault中止double_fault双重故障
9 (0x09)Coprocessor Segment Overrun故障coprocessor_segment_overrun协处理器段溢出(已弃用)
10 (0x0A)Invalid TSS故障invalid_TSS无效任务状态段
11 (0x0B)Segment Not Present故障segment_not_present段不存在
12 (0x0C)Stack-Segment Fault故障stack_segment栈段故障
13 (0x0D)General Protection Fault故障general_protection通用保护故障
14 (0x0E)Page Fault故障page_fault页故障(缺页)
15 (0x0F)Reserved--Intel保留
16 (0x10)x87 FPU Error故障coprocessor_error浮点错误
17 (0x11)Alignment Check故障alignment_check对齐检查
18 (0x12)Machine Check中止machine_check机器检查
19 (0x13)SIMD Floating-Point Exception故障simd_coprocessor_errorSIMD浮点异常
20-31 (0x14-0x1F)Reserved--Intel保留,未来扩展

0x20-0x2F:可编程中断控制器(PIC)
中断号名称Linux处理函数说明
32 (0x20)IRQ0timer_interrupt可编程定时器(PIT)
33 (0x21)IRQ1keyboard_interrupt键盘控制器
34 (0x22)IRQ2cascade级联信号
35 (0x23)IRQ3serial_interruptCOM2串口
36 (0x24)IRQ4serial_interruptCOM1串口
37 (0x25)IRQ5-LPT2并口/声卡
38 (0x26)IRQ6floppy_interrupt软盘控制器
39 (0x27)IRQ7parallel_interruptLPT1并口
40 (0x28)IRQ8rtc_interrupt实时时钟(RTC)
41 (0x29)IRQ9-重定向IRQ2
42 (0x2A)IRQ10-保留/SCSI/网卡
43 (0x2B)IRQ11-保留/SCSI/网卡
44 (0x2C)IRQ12mouse_interruptPS/2鼠标
45 (0x2D)IRQ13fpu_interrupt协处理器/FPU
46 (0x2E)IRQ14ide_interrupt主IDE通道
47 (0x2F)IRQ15ide_interrupt从IDE通道

0x30-0x7F:用户定义中断
中断号用途
48-127 (0x30-0x7F)用户自定义中断,常用于设备驱动和系统调用

0x80-0xEF:系统调用与扩展
中断号名称Linux处理函数说明
128 (0x80)System Callsystem_callLinux传统系统调用入口(int 0x80)
129-239 (0x81-0xEF)--保留/未使用

0xF0-0xFF:高级可编程中断控制器(APIC)
中断号名称Linux处理函数说明
240 (0xF0)APIC Spuriousspurious_interruptAPIC伪中断
241-250 (0xF1-0xFA)--APIC保留
251 (0xFB)APIC Errorapic_error_interruptAPIC错误中断
252 (0xFC)APIC Timerlocal_timer_interruptAPIC定时器
253 (0xFD)APIC Performanceperformance_monitoring_interrupt性能监控
254 (0xFE)APIC Thermalthermal_interrupt热管理中断
255 (0xFF)APIC IPIsmp_call_function_interrupt核间中断(IPI)

a. Linux实现特点

  • 前32个异常由trap_init()初始化
  • 32-47的IRQ由init_IRQ()设置
  • 0x80是经典32位系统调用入口(现代x86_64使用syscall指令)

b. 实际地址:完整的中断描述符表(IDT)包含256个条目,每个条目8字节,存储在CPU的IDTR寄存器指向的内存区域。

中断处理过程1.png

2.IDT表的初始化

image.png

中断处理流程举例

(1).异步中断处理大体粗略流程

异步中断大体粗略处理流程.png

硬中断&软中断

外部I/O设备,时钟芯片,CPU芯片 等都是硬件发出的中断(它们必须要快速处理)——硬中断

软件程序代码触发的中断(它们会处理耗时的业务)——软中断

1.中断处理程序的特点

CPU在执行中断处理程序的时候,不会再去执行其他程序

中断处理程序 并不是 进程(它没有对应的task_struct噢!), 而是一个比较特殊的内核代码执行路径而已,所以,中断处理程序不会参与进程调度

中断处理程序会打断 A, B, C等进程的执行,但是中断处理程序一般是不会被打断的

中断处理程序必须非常快,否则:

  • 会影响其他进程的执行
  • 无法响应其他的中断请求

所以,中断处理程序一般都是短小精悍的,执行非常的快,一般不会做读写磁盘I/O等耗时操作。

2. 如果中断处理程序需要执行很慢的操作(业务),怎么办?

将中断处理逻辑分为 【上部分】【下部分】 两部分。

  • 上部分用来快速处理中断,主要负责处理跟硬件紧密相关或者时间敏感的事情
  • 下部分用来延迟处理上部分未完成的工作,一般以【内核线程】的方式运行

3.Linux系统内核中的软中断处理程序

软中断的处理由内核线程 ksoftirqd 来做 image.png

下图我们可以看到 1号软中断 TIMER_SOFTIRQ 粗略处理流程 image.png

序号软中断名内核里到底在干啥(一句话场景说明)内核中的软中断处理程序(函数名)
0HI_SOFTIRQ优先级最高的 tasklet,驱动想“插个队”时用;任何普通 tasklet 之前执行。tasklet_hi_action
1TIMER_SOFTIRQ处理“传统低精度定时器”超时,时钟中断底半部把到期的 timer 链表在这里统一跑一遍。run_timer_softirq
2NET_TX_SOFTIRQ网络发包下半部分:驱动把包递交给内核后,由 NET_TX 完成队列调度、实际 DMA 启动等。net_tx_action
3NET_RX_SOFTIRQ网络收包底半部:NIC 收到包触发硬中断后,把帧挂到 CPU 的 poll 队列,NET_RX 负责协议栈层层解析。net_rx_action
4BLOCK_SOFTIRQ块设备层下半部分:完成 I/O request 的回调、bio 结束处理、触发下一个 request 等。blk_done_softirq
5IRQ_POLL_SOFTIRQ“中断轮询”(NAPI) 的通用底半部;网卡关闭中断后,用轮询方式批量收包,提高 10G+ 网卡吞吐。irq_poll_softirq
6TASKLET_SOFTIRQ普通 tasklet 的统一入口;驱动调用 tasklet_schedule() 时最终在此被执行。tasklet_action
7SCHED_SOFTIRQ调度器下半部分:负载均衡、唤醒抢占、CPU 之间拉任务等,把硬中断里来不及做的调度决策延后执行。sched_softirq
8HRTIMER_SOFTIRQ高精度定时器下半部分;硬件时钟事件设备(HPET/APIC-timer)把到期的 hrtimer 在这里统一处理。hrtimer_run_softirq
9RCU_SOFTIRQRCU 宽限期处理与回调;CPU 检测到 RCU grace-period 结束后,把注册的回调函数在这里批量跑完。rcu_process_callbacks

下图我们可以看到 3号软中断 NET_RX_SOFTIRQ 网卡收包粗略处理流程 网卡中的软中断处理.png