linux内核设计与实现 - 中断与中断处理

第七章 中断与中断处理

小结:

  1. 中断和异常
  2. 中断处理程序,注册(request_irq())、注销、禁止(local_irq_disable())、屏蔽(disable_irq())、检查中断(irqs_disable(), in_interrupt())
  3. 中断处理机制的实现
    设备产生中断 -> 总线将电信号发给中断控制器 -> 发给处理器 -> 处理器关中断 -> 中断处理程序入口 -> 返回内核运行中断的代码(每个中断线都有唯一的位置)
  4. 下半部
    软中断、tasklet、工作队列和内核定时器
下半部 注册时间 上下文 顺序执行保障 场景 是否响应中断
软中断 编译时静态注册 中断 没有,即使相同类型也可以同时在其他处理器上执行,锁保护或单处理器 给系统中对时间要求严格以及最重要的下半部使用 允许响应中断,但自己不能休眠
tasklet 动态注册,通过软中断实现 中断 相同类型的不能同时执行,锁保护要求低 不需休眠的场景 不能睡眠,调度时会禁止本地中断
工作队列 可动态注册,通过内核线程实现 进程 没有(和进程上下文一样被调度) 需要睡眠的场景 允许重新调度甚至是睡眠,不能访问用户空间,因为没有相关内存映射

软中断:do_softirq(),内核定时器和tasklet都是建立在软中断之上
tasklet:tasklet调度时会禁止本地中断
workqueue_struct:每个CPU一个,系统有默认的event/n
ksoftirq:一组辅助处理软中断(和tasklet)的内核线程,ksoftirqd/n

选择标准:需要睡眠,选工作队列;不需要睡眠,选软中断或tasklet。

  1. 下半部的加锁
    同步问题???还是不太懂
    记住,一个下半部实体可能在任何时候运行。
    tasklet:intra_tasklet加锁
    工作队列:任何共享数据都要加锁。
    void local_bh_disable()禁止下半部

文章目录

  • 第七章 中断与中断处理
    • 7.1 中断
    • 7.2 中断处理程序
    • 7.3 上半部与下半部的对比
    • 7.4 注册中断处理程序
    • 7.5 编写中断处理程序
    • 7.6 中断上下文
    • 7.7 中断处理机制的实现
    • 7.8 /proc/interrupts
    • 7.9 中断控制
  • 第8章 下半部和推后执行的工作
    • 8.1 下半部
    • 8.2 软中断
    • 8.3 tasklet
    • 8.4 工作队列
    • 8.5 下半部机制的选择
    • 8.6 在下半部之间加锁
    • 8.7 禁止下半部

处理器和外部慢设备通过 轮询中断协同工作。

7.1 中断

中断使得硬件得以发出通知给处理器,处理器收到中断后,会马上向操作系统反馈此信号的到来,然后由操作系统处理。硬件设备生成中断的时候并不考虑与处理器的时钟同步(中断随时可以产生)。
特定的中断总是与特定的设备相关联。每个IRQ线都会被关联一个数值量。

异常:异常与中断不同,产生时必须考虑处理器时钟同步。异常也称为同步中断。

7.2 中断处理程序

内核执行中断处理函数来相应中断,一个设备的中断处理程序是设备驱动程序的一部分。
中断处理程序与内核函数的区别:中断处理程序是被内核调用来响应中断的,他们运行在中断上下文(不可阻塞)

7.3 上半部与下半部的对比

中断处理切为2个部分:
中断处理程序是上半部:接到中断立即执行,只做严格实现的工作,此时中断被禁止
下半部:被允许稍后完成的工作,此时为开中断

7.4 注册中断处理程序

  1. 设备如果使用中断,则需要注册一个中断处理程序。
    驱动程序通过request_irq()注册一个中断处理程序,并激活给定的中断线:

    int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, 
    				const char* name, void *dev)
    irq:要分配的中断号(预定或动态分配)
    handler:指向实际中断处理程序
    typedef irqreturn_t (*irq_handler_t)(int, void*);这是handler的原型,接受两个参数
    
  2. 中断处理程序标志
    第三个参数flags是标识:

    • IRQF_DISABLED - 在中断处理程序本身期间,禁止所有其他中断
    • IRQF_SAMPLE_RANDOM - 来自该设备的中断间隔时间会作为熵填充到熵池
    • IRQF_TIMER - 系统定时器的中断处理准备
    • IRQF_SHARED - 可以在多个中断处理程序之间共享中断线。

第四个参数name,为中断相关的设备的ASCII文本表示。
第五个参数dev用于共享中断线,dev提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的哪一个。实践中往往传递驱动程序的设备结构

注意:request_irq()函数可能睡眠, 原因是由于request_irq()或需要在/proc/irq文件中创建一个与中断对应的项,会调用proc_mkdir() ⇒ proc_create() ⇒ kmalloc(),而kmalloc是睡眠的。
3. 释放中断处理程序
卸载驱动时,要注销相应中断处理程序,释放中断线,调用void free_irq(unsigned int irq, void *dev)

7.5 编写中断处理程序

  1. 重入和中断处理程序
    当前中断线在处理时总是被禁止的,所以中断处理程序无须重入。同一个中断处理程序绝对不会被同时调用以处理嵌套的中断。
  2. 共享的中断处理程序
  • request_irq()的flags被设为IRQF_SHARED
  • dev参数必须唯一
  • 中断处理程序必须区分是否真产生了中断
  1. 中断处理程序实例
    例:RTC(real-time clock)驱动程序:从系统定时器独立出来,用于设置系统时钟(向某个特定寄存器或IO地址写入时间即可)、周期性的定时器或报警器(中断实现)。

7.6 中断上下文

中断上下文具有严格的时间限制,应迅速、简洁,因为中断处理程序打断了其他代码(有可能是另一个中断处理程序)。
中断处理程序栈:没有自己的栈,它共享所中断进程的内核栈。内核栈大小是2页。

7.7 中断处理机制的实现

设备产生中断 ⇒ 总线把电信号发给中断控制器 ⇒ 若为激活的,中断控制器将中断发往处理器 ⇒ 处理器关闭中断 ⇒ 跳到中断处理程序入口 ⇒ 返回内核运行中断的代码。
linux内核设计与实现 - 中断与中断处理_第1张图片
中断开始于预定义入口,类似系统调用,对于每条中断线,处理器会跳到对应的一个唯一的位置。

7.8 /proc/interrupts

procfs是一个虚拟文件系统,只存在于内核内存,在procfs中读写文件都需要调用内核函数,模拟从真实文件中读或写。/proc/interrupts文件,存放系统与中断相关的统计信息。

7.9 中断控制

linux内核提供一组接口用于操作机器上的中断状态,这些接口为我们提供了能够禁止当前处理器的中断系统。
一般来说控制中断系统的原因最终还是提供同步,以确保某个中断处理程序不会抢占当前代码,还可以禁止内核抢占。但无法防止共享数据的并发访问。
总之,锁,用于防止来自其他处理器的并发访问;禁止中断,防止来自其他中断处理程序的并发访问。

  1. 禁止和激活中断
    local_irq_disable():clear允许中断标志的汇编调用
    local_irq_enable():set。。。。。。。。。。。。
    上述函数有潜在危险,由可能中断一开始就是关闭的。
    local_irq_save(flags);
    local_irq_restore(flags);
    flags不能传递给另一个函数,因此,local_irq_save()和loca_irq_restore()的调用必须在同一个函数中进行。

以前的代码:使用全局cli():禁止系统中所有处理器上的中断
现在的代码:结合使用本地中断控制自旋锁

  1. 禁止指定中断线
    void disable_irq(unsigned int irq); 只有当前正在执行的所有处理程序完成后才能返回。
    void disable_irq_nosync(unsigned int irq);不会等待当前中断处理程序完成
    void enable_irq(unsigned int irq);
    void synchronize_irq(unsigned int irq);

可以从中断或进程上下文中调用,且不会睡眠。

PCI设备必须支持中断线共享,因此,它们根本不应该使用这些接口。

  1. 中断系统的状态
    irqs_diable()用于判断本地处理器上中断是否被禁止。
    in_interrupt():判断内核是否在中断处理程序或下半部处理程序中
    in_irq():判断是否在中断处理程序

通常情况,要检查是否在进程上下文,因为睡眠只能在进程上下文。

第8章 下半部和推后执行的工作

8.1 下半部

为了减少中断处理程序所需完成的工作量,因为它运行时,当前中断线在所有处理器上都会被屏蔽。

  1. 中断处理程序最好处理:
  • 对时间敏感任务
  • 和硬件相关任务
  • 如果一个任务要保证不被其他中断打断

下半部关键在于,他们运行时允许响应所有的中断。

  1. 下半部的环境
    实现中断处理程序的方法仅有一种:中断处理程序
    下半部:
    • 推后到某一时间执行(3种) 软中断、tasklet和工作队列(ps:与系统调用中软中断不同,系统调用中为软件中断)
    • 推后到确定时间段执行:内核定时器
      工作队列:不太灵活
      软中断:静态定义的下半部接口,编译时静态注册,如网络等对性能要求高的使用这种,即使相同类型也可以同时在其他处理器上执行,因此要么在单处理器上执行要么加锁。软中断允许响应中断,但自己不能休眠。
      tasklet:相同类型的不能同时执行,可动态注册,通过软中断实现的,因此也不能睡眠,锁保护要求低。

8.2 软中断

  1. 软中断的实现
    软中断试在编译期静态分配的,由softirq_action结构表示
struct softirq_action{
	void (*action)(struct softirq_action *);
}
static struct softirq_action softirq_vec[NR_SOFTIRQS];要么
//每个被注册的软中断占据数组一项,因此最多32个软中断

(1). 软中断处理程序
实现上面的action函数
(2). 执行软中断
通常,中断处理程序会在返回前标记它的软中断,以触发软中断。
以下会检查和执行软中断:

  • 在一个硬件中断代码返回时
  • 在ksoftirqd内核线程中
  • 显示检查和执行待处理软中断代码,如网络子系统

do_softirq() 执行软中断,会遍历每一个,并调用其处理程序。
2. 使用软中断
软中断保留给系统中对时间要求最严格以及最重要的下半部使用,目前只有网络和SCSI直接使用软中断。内核定时器和tasklet都是建立在软中断之上
(1). 分配索引
通过在中定义一个枚举类型静态声明软中断,且0的优先级最高
(2). 注册处理程序
运行时通过调用open_softirq()注册软中断处理程序
open_softirq(NET_TX_SOFTIRQ, net_tx_action);//参数分别为软中断处理号和处理函数
(3). 触发你的软中断
raise_softirq()将软中断设置为挂起,让它下次调用do_softirq()函数时投入运行。且该函数在触发一个软中断之前要先禁止中断,触发后再恢复。如果已经禁止,则使用raise_softirq_irqoff().

//中断已经被禁止
raise_softirq_irqoff(NET_TX_SOFTIRQ);

在中断处理函数中触发软中断是最常见的形式。内核执行完中断处理函数后,马上执行do_softirq(),于是软中断开始执行剩余工作。

8.3 tasklet

  1. tasklet的实现
    tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ
    (1). tasklet结构体
struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;     //tasklet状态,仅能0、TASKLET_STATE_SCHED、TASKLET_STATE_RUN
	atomic_t count;			//引用计数,不为0,则tasklet被禁止;为0才可执行
	void (*func)(unsigned long);		// tasklet处理函数
	unsigned long data;					//给tasklet处理函数的参数
};

(2). 调度tasklet
已调度的tasklet(即被触发的)存放在两个每处理器链表:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。

tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度。tasklet调度时会禁止本地中断
其处理程序tasklet_action()和tasklet_hi_action()为tasklet处理的核心。

总之,所有tasklet都通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现。但tasklet保证同一时间里只有一个给定类型的tasklet会被执行(其他不同类型也可以同时执行)。

  1. 使用tasklet
    (1). 声明你自己的tasklet
    静态创建:
    DECLARE_TASKLET(name, func, data) // func 函数,data 其参数
    DECLARE_TASKLET_DISABLED(name, func, data)
    当该tasklet被调度以后,给定func函数会被执行。disable会将tasklet引用计数设为1,使处于禁止状态。

    动态创建:
    tasklet_init(t, tasklet_handler, dev); // 通过将一个间接引用赋给动态创建的tasklet_struct

(2). 编写你自己的tasklet处理程序
void tasklet_handler(unsigned long data)
由于tasklet运行时允许响应中断,因此也必须做好预防工作。适当加锁:

  • tasklet和中断处理程序之间共享数据
  • tasklet跟其他tasklet或软中断共享数据

(3). 调度你的tasklet
tasklet_schedule(&my_tasklet);调度my_tasklet
一般一个tasklet总在调度它的处理器上执行,以更好利用处理器高速缓存。

tasklet_disable():禁止某个tasklet
tasklet_enable():激活某个tasklet
tasklet_kill():从队列中去掉一个tasklet

(4). ksoftirq
一组辅助处理软中断(和tasklet)的内核线程,当内核中出现大量软中断时,会辅助。
问题:当软中断触发频率很高,尤其是有自行重复触发时
内核解决:不会立即处理重新触发的软中断。且当大量软中断出现时,唤醒一组内核线程处理,这些线程运行在最低优先级(nice值为19),以减少跟其他重要任务抢夺资源。且重新触发的软中断也能得到执行。

所有线程名为ksoftirqd/n,n是处理器编号。只要do_softirq()函数发现已经执行过的内核线程重新触发了自己,ksoftirq就会被唤醒。

  1. BH机制
    静态定义,最多32个,严格顺序执行,不允许任何两个BH同时执行
    优点:同步简单
    缺点:不利于多处理器的可扩展性

8.4 工作队列

工作队列(等价于内核线程)是另外一种将工作推后执行的形式,将工作推后,交由内核线程去执行 - 这个下半部分总是会在进程上下文中执行,因此,工作队列允许重新调度甚至是睡眠虽然在进程上下文,但不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。

需要睡眠,选工作队列;不需要睡眠,选软中断或tasklet。

  1. 工作队列的实现
    工作队列可以让你的驱动创建一个专门的工作者线程来处理要推后的工作,不过工作队列子系统提供了缺省的工作者线程称为events/n,n为处理器编号。
    (1). 表示线程的数据结构
struct workqueue_struct {
	。。。
};

由cpu_workqueue_struct组成的数组,数组每一项对应一个处理器。每个工作者线程对应一个cpu_workqueue_struct结构体。
2. 表示工作的数据结构

struct work_struct {
	atomic_long_t data;		//参数
	struct list_head entry;
	work_func_t func;		//待执行函数
};

工作者线程执行worker_thread(),这个函数执行死循环,当有操作被插入队列时会被唤醒。每个处理器的每种类型的队列都有这样的work_struct的链表,唤醒后会执行链表上的所有工作,完成后睡眠。

  1. 工作队列实现机制的总结
    linux内核设计与实现 - 中断与中断处理_第2张图片
    最高一层是工作者线程。系统允许多种工作者线程,每种类型,**CPU上都有一个该类的工作者线程。**每个工作者线程都要cpu_workqueue_struct结构体表示。
    (4个CPU,有四个event类型的线程,4个自定义的falcon类型的线程,则会有一个对应event类型和一个对应falcon类型的workqueue_struct)

    work_struct:推后执行的工作

  2. 使用工作队列
    (1). 创建推后的工作
    静态:DECLARE_WORK(name, void (*func)(void *), void *data);
    动态:INIT_WORK(struct work_struct *work, void (*func)(void *), void *data);
    (2). 工作队列处理函数
    void work_handler(void *data);
    (3). 对工作进行调度
    将工作提交给缺省的events工作线程:schedule_work(&work); 会被马上执行
    schedule_delayed_work(&work,delay);会延迟delay个节拍后执行
    (4). 刷新操作
    目的:模块卸载前,或防止竞争,可能需要确保不再有待处理的工作
    刷新:void flush_scheduled_work(void);只是等待执行结束,不会刷新,等待时会休眠
    取消延迟工作:int cancel_delayed_work(work);
    (5). 创建新的工作队列
    struct workqueue_struct *create_workqueue(const char* name);会创建所有CPU上的工作者线程。
    其调度函数:
    int queue_work(struct workqueue_struct *wq, work);
    int queue_delayed_work( wq, work, delay);
    刷新函数:
    flush_workqueue(struct workqueue_struct *wq);

8.5 下半部机制的选择

三种选择:软中断、tasklet和工作队列。
tasklet基于软中断,工作队列则基于内核线程。

  1. 软中断
    提供的执行序列化最少,因此要小心处理共享数据。对于时间要求严格和执行频率高的应用来说,它执行得也最快。
  2. tasklet
    接口简单,且两个同类型的tasklet不能同时执行,实现会简单一些。驱动程序开发者应当尽可能选择tasklet而不是软中断。
  3. 工作队列
    如果要把任务推后到进程上下文中,则只能选择工作队列。工作队列的开销最大,因为它要牵扯到内核线程甚至是上下文切换。
下半部 上下文 顺序执行保障
软中断 中断 没有
tasklet 中断 同类型不能同时执行,即使是不同处理器
工作队列 进程 没有(和进程上下文一样被调度)

一般考虑两点:
(1). 有休眠的需要么?是,则选工作队列,否,选tasklet。
(2). 是否专注性能的提高?是则选软中断。

8.6 在下半部之间加锁

重要性:在使用下半部机制时,即使是在单处理器上,避免共享数据被同时访问也是至关重要的。
记住,一个下半部实体可能在任何时候执行

tasklet:只需要考虑intra-tasklet的同步问题。
(1). 如果进程上下文和一个下半部共享数据,在访问数据之前,需要禁止下半部的处理并得到锁的使用权。为了本地和SMP的保护并防止死锁的出现。
(2). 如果中断上下文和一个下半部共享数据,。。。。。。。。,需要禁止中断并得到锁的使用权。。。。。。。。。。。。

工作队列:任何在工作队列中被共享的数据需要锁机制。

8.7 禁止下半部

为保证共享数据的安全,一般是先得到一个锁后在禁止下半部的处理(驱动),如果是内核代码,一般仅禁止下半部就好。

void local_bh_disable():禁止本地处理器的软中断和tasklet的处理
void local_bh_enable():激活本地处理器的软中断和tasklet的处理
这两个函数是通过preempt_count(每个进程维护的一个计数器,之前是用于判断内核是否可被抢占),当计数器变为0时,下半部才能被处理。

不能禁止工作队列的执行,因为他是在进程上下文,不会涉及异步执行的问题。由于软中断和tasklet是异步发生的(即在中断处理返回的时候),所以内核代码必须禁止他们。???

你可能感兴趣的:(操作系统)