标签 Linux内核设计与实现 下的文章
上一章简单的讲了一下中断的上半部(中断处理程序),这一章就讲讲中断的下半部以及下半部的几种实现机制,最后简单的写了几个测试的例子来测试软中断、tasklet和工作队列。有需要测试程序的小伙伴,可以留言,我到时直接发到你的邮箱,这里就不放出来了。1、下半部简述中断下半部的任务是执行与中断处理密切相关但中断处理程序本身并不执行的工作。对于中断中的任务应该在上半部还是下半部执行,并没有严格的规则来规定,但是还是有一些提示可供借鉴:1)、如果一个任务对时间非常敏感,将其放在中断处理程序中执行;2)、如果一个任务和硬件相关,将其放在中断处理程序中执行;3)、如果一个任务要保证不被其他中断(特别是共享同一中断线的中断)打断,将其放在中断处理程序中执行;4)、其他所有任务可以考虑放置在下半部执行。和上半部只能通过中断处理程序实现不同,下半部可以通过多种机制实现。随着Linux内核的发展,产生了一些新的机制,也淘汰了一些旧的机制。目前,下半部主要有三种机制:软中断,tasklet,工作队列。2、中断下半部机制 — 软中断软中断是一组静态定义的下半部接口,在Linux-2.6.34内核中有10个,可以在所有处理器上同时执行(即使两个类型相同也可以)。软中断必须在编译期间就进行静态注册。软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和SCSI)直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。软中断的代码在:kernel/softirq.c使用软中断的流程图如下:1)、分配索引软中断目前有10种类型,软中断类型定义在include/linux/interrupts.h中,可以通过修改该文件中的枚举,来实现增加或者减少软中断,下面我增加一个自己定义的软中断AilsonJack_SOFTIRQ:enum{ HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ AilsonJack_SOFTIRQ,/*自己定义的一个新的软中断类型*/ NR_SOFTIRQS};接着在kernel/softirq.c文件中的softirq_to_name数组中给新增的软中断取个名字:char *softirq_to_name[NR_SOFTIRQS] = { “HI”, “TIMER”, “NET_TX”, “NET_RX”, “BLOCK”, “BLOCK_IOPOLL”, “TASKLET”, “SCHED”, “HRTIMER”, “RCU”, “AilsonJack”};/*增加一种新的softirq —> AilsonJack*/索引号小的软中断在索引号大的软中断之前执行。2)、注册处理程序注册处理程序使用的是open_softirq()函数,它的定义在kernel/softirq.c文件中:/* * 将软中断类型和软中断处理函数加入到软中断序列中 * @nr – 软中断类型 * @ void (*action)(struct softirq_action *) – 软中断处理的函数指针*/void open_softirq(int nr, void (*action)(struct softirq_action *)){ /* softirq_vec是个struct softirq_action类型的数组 */ softirq_vec[nr].action = action;}struct softirq_action 的定义在 include/linux/interrupt.h 文件中:/* * 这个结构体的成员是个函数指针,函数的名称是action * 函数指针的返回值是void型 * 函数指针的参数是 struct softirq_action 的地址,其实就是指向 softirq_vec 中的某一项 * 如果 open_softirq 是这样调用的: open_softirq(NET_TX_SOFTIRQ, my_tx_action); * 那么 my_tx_action 的参数就是 softirq_vec[NET_TX_SOFTIRQ]的地址*/struct softirq_action{ void (*action)(struct softirq_action *);};一个软中断不会抢占另一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。不过,其他的软中断(甚至是相同类型的软中断)可以在其他处理器上同时执行。软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。这意味着你不能在软中断中使用信号量或者其他什么阻塞式函数。3)、触发软中断通过在枚举类型列表中添加新项以及调用open_sofirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下一次调用do_softirq()函数时投入运行。触发软中断的函数raise_softirq()定义在kernel/softirq.c文件中:/* * 触发某个类型的软中断 * @nr – 被触发的软中断类型 * 从函数中可以看出,在处理软中断前后有保存和恢复本地中断的操作*/void raise_softirq(unsigned int nr){ unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags);}raise_softirq(NET_TX_SOFTIRQ),这样会触发NET_TX_SOFTIRQ软中断。在中断处理程序中触发软中断是最常见的形式。4)、执行软中断在触发了软中断之后,系统会在合适的时刻让软中断运行,该函数不需要自己调用。执行软中断do_softirq()定义在kernel/softirq.c文件中:asmlinkage void do_softirq(void){ __u32 pending; unsigned long flags; /* 判断是否在中断处理中,如果正在中断处理,就直接返回 */ if (in_interrupt()) return; /* 保存当前寄存器的值 */ local_irq_save(flags); /* 取得当前已注册软中断的位图 */ pending = local_softirq_pending(); /* 循环处理所有已注册的软中断 */ if (pending) __do_softirq(); /* 恢复寄存器的值到中断处理前 */ local_irq_restore(flags);}3、中断下半部机制 — tasklettasklet是用软中断实现的一种下半部机制。但是它的接口更简单,对锁保护的要求也较低。对于tasklet和软中断如何选择,通常应该选用tasklet,而对于那些执行频率很高和连续性要求很高的情况下才选用软中断。tasklet有两类软中断类型:HI_SOFTIRQ,TASKLET_SOFTIRQ。这两者之间的唯一区别就是,HI_SOFTIRQ类型的软中断会先于TASKLET_SOFTIRQ类型的软中断执行。tasklet由tasklet结构体表示。每个结构体单独代表一个tasklet,它定义在linux/interrupt.h文件中:struct tasklet_struct{ struct tasklet_struct *next;/*链表中的下一个tasklet*/ unsigned long state;/*tasklet的状态*/ atomic_t count;/*引用计数器*/ void (*func)(unsigned long);/*tasklet处理函数*/ unsigned long data;/*tasklet处理函数的参数*/};tasklet状态值:0:表示tasklet没有被调度;TASKLET_STATE_SCHED:表示tasklet已经被调度,正准备运行;TASKLET_STATE_RUN:表示tasklet正在运行。引用计数器count:非0:tasklet被禁止,不允许执行;0:tasklet被激活,可以执行。使用tasklet的流程图如下:1)、创建tasklet结构体/* 静态声明一个tasklet */#define DECLARE_TASKLET(name, func, data) \struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }#define DECLARE_TASKLET_DISABLED(name, func, data) \struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }/* 动态声明一个tasklet 传递一个tasklet_struct指针给初始化函数 */extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);2)、编写处理程序参照tasklet处理的原型,编写自己的处理函数:void tasklet_handler(unsigned long date)因为tasklet是靠软中断实现的,所以taskelt也不能睡眠,即不能在tasklet中使用信号量或者其他什么阻塞式的函数。两个相同的tasklet决不会同时执行。3)、调度tasklet通过调用tasklet_sched()函数并传递给它相应的task_struct的指针,该tasklet就会被调度以便执行:tasklet_sched(&my_tasklet);和软中断一样,通常也是在中断处理程序中来调度tasklet,之后,系统会在适合的时候调度你的tasklet处理程序来对中断的下半部进行处理。4、中断下半部机制 — 工作队列工作队列(work queue)是另一种将工作推后执行的形式,它和我们前面讨论的所有其它形式都不同。工作队列的执行会在进程上下文中进行,这样工作队列就允许重新调度甚至是睡眠。通常在工作队列和软中断/tasklet中做出选择非常容易。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择软中断或者tasklet。缺省的工作者线程名称是 events/n (n对应处理器号)。每个处理器对应一个线程。可以使用top | grep events来查看机器上的events线程。工作队列主要用到下面3个结构体,弄懂了这3个结构体的关系,也就知道工作队列的处理流程了。/* 在 include/linux/workqueue.h 文件中定义 */struct work_struct { atomic_long_t data; /* 这个并不是处理函数的参数,而是表示此work是否pending等状态的flag */ #define WORK_STRUCT_PENDING 0 /* T if work item pending execution */ #define WORK_STRUCT_FLAG_MASK (3UL) #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK) struct list_head entry; /* 中断下半部处理函数的链表 */ work_func_t func; /* 处理中断下半部工作的函数 */#ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map;#endif};/* * 在 kernel/workqueue.c文件中定义 * 每个工作者线程对应一个 cpu_workqueue_struct ,其中包含要处理的工作的链表 * (即 work_struct 的链表,当此链表不空时,唤醒工作者线程来进行处理)*//* * The per-CPU workqueue (if single thread, we always use the first * possible cpu).*/struct cpu_workqueue_struct { spinlock_t lock; /* 锁保护这种结构 */ struct list_head worklist; /* 工作队列头节点 */ wait_queue_head_t more_work; struct work_struct *current_work; struct workqueue_struct *wq; /* 关联工作队列结构 */ struct task_struct *thread; /* 关联线程 */} ____cacheline_aligned;/* * 也是在 kernel/workqueue.c 文件中定义的 * 每个 workqueue_struct 表示一种工作者类型,系统默认的就是 events 工作者类型 * 每个工作者类型一般对应n个工作者线程,n就是处理器的个数*//* * The externally visible workqueue abstraction is an array of * per-CPU workqueues:*/struct workqueue_struct { struct cpu_workqueue_struct *cpu_wq; /* 工作者线程 */ struct list_head list; const char *name; int singlethread; int freezeable; /* Freeze threads during suspend */ int rt;#ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map;#endif};注意,这里有三个概念,工作(work_struct),工作者线程(cpu_workqueue_struct),工作者类型(workqueue_struct)。这里举个例子,假设计算机有4个处理器,除了默认的events工作者类型外,我自己还新加入falcon工作者类型,对于这种情况,来说说结构体的数目。因为此时计算机上有两种工作者类型:events和falcon,那么相应的也就有2个工作者类型(workqueue_struct)结构体分别与之对应。因为计算机有4个处理器,每个处理器对应一个工作者线程,那么events工作者类型有4个工作者线程(cpu_workqueue_struct)结构体,同样的falcon工作者类型也有4个工作者线程(cpu_workqueue_struct)结构体,计算机总共有8个工作者线程(cpu_workqueue_struct)结构体。对于工作(work_struct)结构体,它就是工作者线程需要处理的任务。使用工作队列的流程图如下:1)、创建推后工作创建推后执行的工作,这里有静态和动态两种方法:/* * 静态创建一个work_struct * @n – work_struct结构体,不用事先定义 * @f – 下半部处理函数*/#define DECLARE_WORK(n, f) \ struct work_struct n = __WORK_INITIALIZER(n, f)/* * 动态创建一个 work_struct * @_work – 已经定义好的一个 work_struct * @_func – 下半部处理函数*/#ifdef CONFIG_LOCKDEP#define INIT_WORK(_work, _func) \ do { \ static struct lock_class_key __key; \ \ (_work)->data = (atomic_long_t) WORK_DATA_INIT(); \ lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0); \ INIT_LIST_HEAD(&(_work)->entry); \ PREPARE_WORK((_work), (_func)); \ } while (0)#else#define INIT_WORK(_work, _func) \ do { \ (_work)->data = (atomic_long_t) WORK_DATA_INIT(); \ INIT_LIST_HEAD(&(_work)->entry); \ PREPARE_WORK((_work), (_func)); \ } while (0)#endif工作队列处理函数的原型:typedef void (*work_func_t)(struct work_struct *work);2)、刷新现有的工作这个步骤不是必须的,可以直接进行创建推后工作和对工作进行调度这两步。刷新现有工作的意思就是在追加新的工作之前,保证队列中的已有工作已经执行完了。/* 刷新系统默认的队列,即 events 队列 */void flush_scheduled_work(void);/* * 刷新用户自定义的队列 * @wq – 用户自定义的队列*/void flush_workqueue(struct workqueue_struct *wq);3)、对工作进行调度/* * 调度第一步中新定义的工作,在系统默认的工作者线程中执行此工作 * @work – 第一步中定义的工作*/schedule_work(struct work_struct *work);/* * 调度第一步中新定义的工作,在系统默认的工作者线程中执行此工作 * @work – 第一步中定义的工作 * @delay – 延迟的时钟节拍*/int schedule_delayed_work(struct delayed_work *work, unsigned long delay);/* * 调度第一步中新定义的工作,在用户自定义的工作者线程中执行此工作 * @wq – 用户自定义的工作队列类型 * @work – 第一步中定义的工作*/int queue_work(struct workqueue_struct *wq, struct work_struct *work);/* * 调度第一步中新定义的工作,在用户自定义的工作者线程中执行此工作 * @wq – 用户自定义的工作队列类型 * @work – 第一步中定义的工作 * @delay – 延迟的时钟节拍*/int queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay);5、中断下半部小结下面对实现中断下半部工作的3种机制进行总结,便于在实际使用中决定使用哪种机制:下半部机制上下文复杂度执行性能顺序执行保障软中断中断高(需要自己确保软中断的执行顺序及锁机制)好(全部自己实现,便于调优)没有tasklet中断中(提供了简单的接口来使用软中断)中同类型不能同时执行工作队列进程低(在进程上下文中运行,与写用户程序差不多)差没有(和进程上下文一样被调度)6、软中断、tasklet、工作队列的测试这些测试程序在文章开始的地方,提供了一个下载链接,大家下载即可(重要的是那个Makefile文件,下面不会列出Makefile里面的内容)。1)、软中断在上面介绍软中断的类型时,自己添加了一个新的软中断类型,下面说说具体修改的内容吧:在include/interrupt.h文件中的枚举列表中添加新的软中断类型:在kernel/softirq.c文件中,给新添加的软中断类型命名:导出raise_softirq()和open_softirq()函数,不然在编译模块时,会发出类似下面的警告:WARNING: "open_softirq" undefinedWARNING: "raise_softirq" undefined修改好之后,就可以编译内核(make,make modules_install,make install),然后从编译好的内核启动(具体详细的编译内核的方法,可以在我的博客中搜索)。内核之所以没有导出open_softirq和raise_softirq函数,可能还是因为提倡我们尽量用tasklet来实现中断的下半部工作由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。执行下面的步骤,对软中断进行测试:编译模块:make加载模块:insmod softirq.ko卸载模块:rmmod softirq查看信息:dmesg | tail可以看见打印信息。2)、tasklet由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。执行下面的步骤,对tasklet进行测试:编译模块:make加载模块:insmod tasklet.ko卸载模块:rmmod tasklet查看信息:dmesg | tail可以看见打印信息。3)、工作队列由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。执行下面的步骤,对工作队列进行测试:编译模块:make加载模块:insmod workqueue.ko卸载模块:rmmod workqueue查看信息:dmesg | tail可以看见打印信息。
1、中断概述如何让处理器和外部设备协同工作,并且不会降低机器的整体性能呢?这里有两种方法:轮询是其中之一的解决办法,但是这种方法可能会让内核做不少的无用功。中断是另一种解决办法,即硬件在需要的时候向内核发出信号,此时内核会暂停当前正在处理的任务,去处理硬件发出的请求,待处理完成之后,接着执行之前的任务。中断分为同步中断(异常)和异步中断(一般由硬件产生)。硬件设备生成中断的时候并不考虑和处理器的时钟同步—换句话说就是中断随时可以产生。因此内核随时可能因为新到来的中断而被打断。不同的设备对应的中断不同,而每个中断都通过唯一的数字标志。这些中断值通常被称为中断请求(IRQ)线。每个IRQ线都会关联一个数值量。例如,在经典的PC机上,IRQ0是时钟中断,而IRQ1是键盘中断。2、中断处理程序在响应一个特定中断的时候,内核会执行一个函数,这个函数就叫做中断处理程序(interrupt handler)或者中断服务例程(interrupt service routine,ISR)。一个设备的中断处理程序是它设备驱动程序的一部分—设备驱动程序是用于对设备进行管理的内核代码。中断随时可能发生,因此中断处理程序也就随时可能执行。所以中断处理程序必须尽可能快的被执行完,同时中断打断了系统其它任务的执行,如果中断处理程序运行的时间过长,那么系统的整体性能也就会下降。下面来看一个例子,假设某个中断需要处理的工作量比较大,比如网卡的中断处理程序,它在接收到中断之后,除了对硬件做出应答,还需要把硬件的网络数据包拷贝到内存,然后对其进行处理再交给合适的协议栈或应用程序。显然,这种工作量不会太小。为了解决中断处理程序既想运行得快,又想完成的工作量多的需求,我们一般中断处理分成两个部分或两半。中断处理程序是上半部(top half)—接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或者复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会被推迟到下半部(bottom half)去,此后,在合适的时机,下半部会被开中断执行。这里只关心中断的上半部也就是中断的处理程序,而对于中断的下半部将在第8章中进行说明。3、中断处理相关函数1)、注册中断处理程序中断处理程序是管理硬件的驱动程序的一部分。驱动程序可以通过request_irq()函数注册一个中断处理程序(它被声明在文件<linux/interrupt.h>),并且激活给定的中断线,以处理中断,函数的定义如下:/* * irq – 表示要分配的中断号 * handler – 实际的中断处理程序 * flags – 标志位,表示此中断的具有特性 * name – 中断设备名称的ASCII 表示,这些会被/proc/irq和/proc/interrupts文件使用 * dev – 用于共享中断线,多个中断程序共享一个中断线时(共用一个中断号),依靠dev来区别各个中断程序 * 返回值: * 执行成功:0 * 执行失败:非0*/int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char* name, void *dev)其中flags常用的标志:IRQF_DISABLED:该标志被设置后,意味着内核在处理中断处理程序本身期间,要禁止所有的其它中断。如果不设置,中断处理程序可以与除本身外的其它任何中断同时运行。IRQF_TIMER:该标志是特别为系统定时器的中断处理而准备的。IRQF_SHARED:此标志表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志;否则,在每条线上只能有一个处理程序。注意:request_irq()函数可能会睡眠,因此,不能在中断上下文或者其他不允许阻塞的代码中调用该函数。还有一点很重要,初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行。2)、释放中断处理程序卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线,此时需要调用函数:void free_irq(unsigned int irq, void *dev)如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,那么仅删除dev所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序时才会被禁用。必须在进程上下文中调用free_irq()。3)、中断处理程序的声明/* * 中断处理程序的声明 * @irq – 中断处理程序(即request_irq()中handler)关联的中断号 * @dev – 与 request_irq()中的dev一样,表示一个设备的结构体 * 返回值: * irqreturn_t – 执行成功:IRQ_HANDLED 执行失败:IRQ_NONE*/static irqreturn_t intr_handler(int, irq, void *dev)4、中断处理机制中断处理的过程主要涉及3个函数:do_IRQ() — 与体系结构有关,对所接收的中断进行应答handle_IRQ_event() — 调用中断线上所有中断处理ret_from_intr() — 恢复寄存器,将内核恢复到中断前的状态中断从硬件到内核的路由如下图:5、中断控制Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程都是与体系结构相关的,可以在<asm/system.h>和<asm/irq.h>文件中找到。常见的控制方法见下表:函数说明local_irq_disable()禁止本地中断传递local_irq_enable()激活本地中断传递local_irq_save()保存本地中断传递的当前状态,然后禁止本地中断传递local_irq_restore()恢复本地中断传递到给定的状态disable_irq()禁止给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行disable_irq_nosync()禁止给定中断线enable_irq()激活给定中断线irqs_disabled()如果本地中断传递被禁止,则返回非0;否则返回0in_interrupt()如果在中断上下文中,则返回非0;如果在进程上下文中,则返回0in_irq()如果当前正在执行中断处理程序,则返回非0;否则返回06、总结中断处理程序对处理时间的要求很高,如果一个中断要花费较长时间,那么中断处理一般分为2部分:上半部只做一些必要的工作后,立即通知硬件继续自己的工作。中断处理中耗时的部分,也就是下半部的工作,CPU会在适当的时候去完成。
- 1
本站信息
目前本站共被浏览 162786 次
目前本站已经运行 3508 天
目前本站共有 165 篇文章
目前本站共有 6 条评论信息
目前本站共有 104 个标签
目前本站共有 0 条留言信息
网站创建时间: 2015年03月01日
最近更新时间: 2023年11月26日
目前本站已经运行 3508 天
目前本站共有 165 篇文章
目前本站共有 6 条评论信息
目前本站共有 104 个标签
目前本站共有 0 条留言信息
网站创建时间: 2015年03月01日
最近更新时间: 2023年11月26日
JLink V9掉固件修复(灯不亮) 3Zephyr笔记2 - 在STM32F429上运行HelloWorld 2计算NandFlash要传入的行地址和列地址 1Linux MMC子系统 - 6.eMMC 5.1工作模式-设备识别模式 0Linux MMC子系统 - 5.eMMC 5.1工作模式-引导模式 0Linux MMC子系统 - 4.eMMC 5.1常用命令说明(2) 0
最新评论
标签云
Linux嵌入式实用技巧ARM内核学习问题集合CC++编程语言阅读笔记汇编Linux内核完全注释Windows驱动开发计算机基础ARM11ARMv7-ASTM32IDESublimeLinux内核学习eMMCMMC子系统Ubuntu操作系统OfficeVMWareAPUEgccRTOS中断漫游世界随笔感悟开发工具软件应用编程VsCodearmccarmclang编译器ZephyrSPIJLink网卡驱动安装各种芯片库函数NFSμCOS内核sambaFlashUnix命令与脚本输入法Linux内核设计与实现gitRIFFWAVJATGFTPar8161安装centos有线上网μCGUI字库工程建立右键菜单网络文件系统Firefox百度NTFS文件系统CodeBlocksCentOS数据结构算法PhotoShop51KeilQTUltraEditscanfglibc宏定义UIDGID优先级娱乐天地SourceInsight磁盘扇区总线I2CPDFBComparePythonI2SFPUMakefileSWDCPUARP软件推荐FileZilla