signal信号、软中断、硬中断、alarm
# 概念区分
# 硬中断
参考 《深入理解linux内核》
硬中断,包括同步中断和异步中断,Intel手册中将其称为异常和中断; 异常由cpu自身发出,所以叫同步中断, 中断由外部设备发出,所以叫异步中断。
Intel把异常和中断分为以下几类:
中断: 可屏蔽中断、不可屏蔽中断
异常
- 处理器探测异常,包括故障(fault)、 陷阱(trap)、 异常终止(abort)
- 编程异常:int或int3指令,可以用于执行系统调用或给调试程序通报特定事件 (注:这种由软件指定发出的中断不是软中断)
每个异常和中断都有一个0-255的中断号,对应设置中断处理程序
# 软中断
preempt_count字段
每个cpu都有一个preempt_count字段,其包含:
- 抢占计数器
- 软中断计数器, 表示软中断被禁用的次数,为0表示可以执行软中断。
- 硬中断计数器, 硬中断计数器表示硬中断的嵌套数
- PREEMPT_ACTIVE标志
另外每个cpu有一个__softirq_pending字段,描述挂起的软中断掩码。
open_softirq() 实现软中断的初始化
raise_softirq() 激活软中断,即把软中断标记为挂起状态。硬中断的top half处理完后,如果需要下半部,就会通过raise_softirq()设置softirq的__softirq_pending字段。软中断不一定是由硬中断激活。
do_softirq() 执行软中断。内核中有一些检查点,当发现存在活动(挂起)的软中断时,就调用该函数执行软中断。 用户进程在执行内核代码时可能调用do_softirq()执行软中断,但是软中断太多时该函数并不会一直执行,而是唤醒内核线程ksoftirqd来执行。
# signal
signal是进程通信的机制,跟软中断相似,但不是一个东西。
参考https://zhuanlan.zhihu.com/p/80371745 (opens new window)
处理函数设置
- 软中断(softirq)通过open_softirq()实现了软中断和执行函数的绑定;
- signal通过signal()函数实现信号和信号处理函数的绑定;
触发
- 软中断由raise_softirq()触发
- signal由kill或者内核触发
# 硬中断、软中断、tasklet和工作队列
https://www.cnblogs.com/wanghuaijun/p/7257417.html
背景知识:CPU在任何时刻都处于以下三种情况之一
- 运行于用户空间,执行用户进程
- 运行于内核空间,处于进程上下文
- 运行于内核空间,处于中断上下文
硬中断和软中断
- 硬中断执行时会屏蔽相同类型的硬中断,但是依然可以接受不同类型的硬中断,也就是说不同类型的硬中断可以嵌套执行。(新版的linux取消了硬中断的嵌套执行,因为有栈溢出的风险)
- 软中断执行时会检查当前cpu不在硬中断和软中断的处理中,本地cpu执行软中断时会串行执行,不会嵌套。但是相同类型的软中断在可以多个cpu上并行执行。
总结:
- 老版本的linux硬中断会打断不同类型硬中断; 新版本的linux硬中断不会打断硬中断(防止栈溢出)。硬中断会打断软中断。
- 软中断不会打断硬中断,软中断也不会打断软中断
软中断、tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的“下半部”(bottom half)演变而来。下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,只剩下了前三者。本文重点在于介绍这三者之间的关系。(函数细节将不会在本文中出现,可以参考文献,点这里)
# (1)上半部和下半部的区别
上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务。举个例子:在网络传输中,网卡接收到数据包这个事件不一定需要马上被处理,适合用下半部去实现;但是用户敲击键盘这样的事件就必须马上被响应,应该用中断实现。
两者的主要区别在于:中断不能被相同类型的中断打断,而下半部依然可以被中断打断;中断对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,可以参考下面四条:
a)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
b)如果一个任务和硬件相关,将其放在中断处理程序中执行。
c)如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
d)其他所有任务,考虑放在下半部去执行。
# (2)为什么要使用软中断?
软中断作为下半部机制的代表,是随着SMP(share memory processor)的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。它的特性包括:
a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
# (3)为什么要使用tasklet?(tasklet和软中断的区别)
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
b)多个不同类型的tasklet可以并行在多个CPU上。
c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,但是由于其特殊的实现机制(将在4.3节详细介绍),所以具有了这样不同于软中断的特性。而由于这种特性,所以降低了设备驱动程序开发者的负担,因此如果不需要软中断的并行特性,tasklet就是最好的选择。
# (4)可延迟函数(软中断及tasklet)的使用
一般而言,在可延迟函数上可以执行四种操作:初始化/激活/执行/屏蔽。屏蔽我们这里不再叙述,前三个则比较重要。下面将软中断和tasklet的三个步骤分别进行对比介绍。
(4.1)初始化
初始化是指在可延迟函数准备就绪之前所做的所有工作。一般包括两个大步骤:首先是向内核声明这个可延迟函数,以备内核在需要的时候调用;然后就是调用相应的初始化函数,用函数指针等初始化相应的描述符。
如果是软中断则在内核初始化时进行,其描述符定义如下:
struct softirq_action
{
void (*action)(struct softirq_action *);
void*data;
};
在\kernel\softirq.c文件中包括了32个描述符的数组static struct softirq_action softirq_vec[32];但实际上只有前6个已经被内核注册使用(包括tasklet使用的HI_SOFTIRQ/TASKLET_SOFTIRQ和网络协议栈使用的NET_TX_SOFTIRQ/NET_RX_SOFTIRQ,还有SCSI存储和系统计时器使用的两个),剩下的可以由内核开发者使用。需要使用函数:
void open_softirq(int nr, void (action)(struct softirq_action), void *data)
初始化数组中索引为nr的那个元素。需要的参数当然就是action函数指针以及data。例如网络子系统就通过以下两个函数初始化软中断(net_tx_action/net_rx_action是两个函数): open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);
这样初始化完成后实际上就完成了一个一一对应的关系:当内核中产生到NET_TX_SOFTIRQ软中断之后,就会调用net_tx_action这个函数。
tasklet则可以在运行时定义,例如加载模块时。定义方式有两种:
静态声明DECLARE_TASKET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)
动态声明
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
其参数分别为描述符,需要调用的函数和此函数的参数—必须是unsigned long类型。也需要用户自己写一个类似net_tx_action的函数指针func。初始化最终生成的结果就是一个实际的描述符,假设为my_tasklet(将在下面用到)。
(4.2)激活激活标记一个可延迟函数为挂起(pending)状态,表示内核可以调用这个可延迟函数(即使在中断过程中也可以激活可延迟函数,只不过函数不会被马上执行);这种情况可以类比处于TASK_RUNNING状态的进程,处在这个状态的进程只是准备好了被CPU调度,但并不一定马上就会被调度。
软中断使用raise_softirq()函数激活,接收的参数就是上面初始化时用到的数组索引nr。
tasklet使用tasklet_schedule()激活,该函数接受tasklet的描述符作为参数,例如上面生成的my_tasklet:
tasklet_schedule(& my_tasklet)
(4.3)执行
执行就是内核运行可延迟函数的过程,但是执行只发生在某些特定的时刻(叫做检查点,具体有哪些检查点?详见《深入》p.177)。
每个CPU上都有一个32位的掩码__softirq_pending,表明此CPU上有哪些挂起(已被激活)的软中断。此掩码可以用local_softirq_pending()宏获得。所有的挂起的软中断需要用do_softirq()函数的一个循环来处理。
而对于tasklet,由于软中断初始化时,就已经通过下面的语句初始化了当遇到TASKLET_SOFTIRQ/HI_SOFTIRQ这两个软中断所需要执行的函数: open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
因此,这两个软中断是要被区别对待的。tasklet_action和tasklet_hi_action内部实现就是为什么软中断和tasklet有不同的特性的原因(当然也因为二者的描述符不同,tasklet的描述符要比软中断的复杂,也就是说内核设计者自己多做了一部分限制的工作而减少了驱动程序开发者的工作)。
# (5)为什么要使用工作队列work queue?(work queue和软中断的区别)
上面我们介绍的可延迟函数运行在中断上下文中(软中断的一个检查点就是do_IRQ退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。
因此在2.6版的内核中出现了在内核态运行的工作队列(替代了2.4内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,以完成不同的工作。
# 多线程signal处理
# signal实现原理
参考《深入理解linux内核》
信号是由linux操作系统实现的,内核通过specific_send_sig_info() 向指定进程发送信号。其流程如下:
- 检查是否忽略信号;
- 检查信号是否是非实时的(sig<32), 如果小于32且目标进程的挂起信号集合中已经有该信号,则忽略; (实时信号可以排队)
- 调用send_signal()将信号放入进程的挂起信号集合;
- 如果send_signal()成功调用且信号不被阻塞,就调用signal_wake_up()通知进程有新的挂起信号; 如果目标进程是当前cpu,则内核返回用户空间时就会执行信号处理函数; 如果目标进程是不是当前cpu,则向目标进程所在的cpu发送核间中断,目标进程处理中断后返回用户空间时也会执行信号处理函数;
- 返回 1;
signal()或者sigaction()注册信号处理函数 https://www.cnblogs.com/cobbliu/p/5592659.html (opens new window)
# linux 多线程信号总结(一)
- 在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知。也就是说,信号会随机发个该进程的一个线程。
2 signal函数BSD/Linux的实现并不在信号处理函数调用时,恢复信号的处理为默认,而是在信号处理时阻塞此信号,直到信号处理函数返回。其他实现可能在调用信号处理函数时,恢复信号的处理为默认方式,因而需要在信号处理函数中重建信号处理函数为我们定义的处理函数,在这些系统中,较好的方法是使用sigaction来建立信号处理函数。
3 发送信号给进程,哪个线程会收到?APUE说,在多线程的程序中,如果不做特殊的信号阻塞处理,当发送信号给进程时,由系统选择一个线程来处理这个信号。
4 如果进程中,有的线程可以屏蔽了某个信号,而某些线程可以处理这个信号,则当我们发送这个信号给进程或者进程中不能处理这个信号的线程时,系统会将这个信号投递到进程号最小的那个可以处理这个信号的线程中去处理。
5 如果我们同时注册了信号处理函数,同时又用sigwait来等待这个信号,谁会取到信号?经过实验,Linux上sigwait的优先级高。
6 在Linux中的posix线程模型中,线程拥有独立的进程号,可以通过getpid()得到线程的进程号,而线程号保存在pthread_t的值中。而主线程的进程号就是整个进程的进程号,因此向主进程发送信号只会将信号发送到主线程中去。如果主线程设置了信号屏蔽,则信号会投递到一个可以处理的线程中去。
7 当调用SYSTEM函数去执行SHELL命令时,可以放心的阻塞SIGCHLD,因为SYSTEM会自己处理子进程终止的问题。
8 使用sleep()时,可以放心的去阻塞SIGALRM信号,目前sleep函数都不会依赖于ALRM函数的SIGALRM信号来工作。
# linux 多线程信号总结(二)
默认情况下,信号将由主进程接收处理,就算信号处理函数是由子线程注册的
每个线程均有自己的信号屏蔽字,可以使用sigprocmask函数来屏蔽某个线程对该信号的响应处理,仅留下需要处理该信号的线程来处理指定的信号。
对某个信号处理函数,以程序执行时最后一次注册的处理函数为准,即在所有的线程里,同一个信号在任何线程里对该信号的处理一定相同
可以使用pthread_kill对指定的线程发送信号
APUE的说法:每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有的线程共享的,这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为后,所有的线程都共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。
进程中的信号是送到单个线程的,如果信号与硬件故障或者计时器超时有关,该型号就被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。
sigprocmask的行为在多线程的进程中没有定义,线程必须使用pthread_sigmask
总结:一个信号可以被没屏蔽它的任何一个线程处理,但是在一个进程内只有一个多个线程共用的处理函数。......
# linux 多线程信号总结(三)
1 Linux 多线程应用中,每个线程可以通过调用pthread_sigmask() 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如SIGSEGV;另外不能被忽略处理的信号SIGKILL 和SIGSTOP 也无法被阻塞。
2 当一个线程调用pthread_create() 创建新的线程时,此线程的信号掩码会被新创建的线程继承。
3 信号安装最好采用sigaction方式,sigaction,是为替代signal 来设计的较稳定的信号处理,signal的使用比较简单,但是其不能不知道信号产生的原因,且处理信号中不能阻塞其他的信号。而signaction,则可以设置比较多的消息。尤其是在信号处理函数过程中接受信号,进行何种处理。sigaction函数用于改变进程接收到特定信号后的行为。
4 sigprocmask函数只能用于单线程程序,在多线程中使用pthread_sigmask函数。
5 信号是发给进程的特殊消息,其典型特性是具有异步性。
6 信号集代表多个信号的集合,其类型是sigset_t。
7 每个进程都有一个信号掩码(或称为信号屏蔽字),其中定义了当前进程要求阻塞的信号集。
8 所谓阻塞,指Linux内核不向进程交付在掩码中的所有信号。于是进程可以通过修改信号掩码来暂时阻塞特定信号的交付,被阻塞的信号不会影响进程的行为直到该信号被真正交付。
9 忽略信号不同于阻塞信号,忽略信号是指Linux内核已经向应用程序交付了产生的信号,只是应用程序直接丢弃了该信号而已。
10 sleep和nanosleep,如果没有在它调用之前设置信号屏蔽字的话,都可能会被信号处理函数打断。参见sleep和nanosleep的mannual。
# 示例代码
https://www.cnblogs.com/jiangzhaowei/p/4188428.html (opens new window)
sigwait函数, pthread_sigmask函数, pthread_kill函数
# sigwait
https://www.cnblogs.com/c-slmax/p/5842293.html (opens new window) sigwait是阻塞的,如果某个线程调用了该函数,则会阻塞等待信号到来。即便之前通过signal()注册了信号处理函数,信号也会优先被sigwait捕获。
# alarm用法
https://blog.csdn.net/u010155023/article/details/51984602 (opens new window)
#include <unistd.h>;
unsigned int alarm(unsigned int seconds);
2
3
alarm()函数的主要功能是设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。如果未设置信号SIGALARM的处理函数,那么alarm()默认处理终止进程。
如果在seconds秒内再次调用了alarm函数设置了新的闹钟,则后面定时器的设置将覆盖前面的设置,即之前设置的秒数被新的闹钟时间取代;当参数seconds为0时,之前设置的定时器闹钟将被取消,并将剩下的时间返回。
pause()函数会被SIGALRM信号结束