分享

Linux内核中常见内存分配函数

 宇宙之彬 2019-11-22
1.   原理说明

Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示。四级页表分别为:

l  页全局目录(Page Global Directory)

l  页上级目录(Page Upper Directory)

l  页中间目录(Page Middle Directory)

l  页表(Page Table)

    页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址,每一个页表项指向一个页框。Linux中采用4KB大小的页框作为标准的内存分配单元。

多级分页目录结构

1.1.   伙伴系统算法

    在实际应用中,经常需要分配一组连续的页框,而频繁地申请和释放不同大小的连续页框,必然导致在已分配页框的内存块中分散了许多小块的空闲页框。这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足。

    为了避免出现这种情况,Linux内核中引入了伙伴系统算法(buddy system)。把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连续页框,对应4MB大小的连续内存。每个页框块的第一个页框的物理地址是该块大小的整数倍。

    假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找,如果仍然没有,则返回错误。

    页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块。

1.2.   slab分配器

    slab分配器源于 Solaris 2.4 的分配算法,工作于物理内存页框分配器之上,管理特定大小对象的缓存,进行快速而高效的内存分配。

    slab分配器为每种使用的内核对象建立单独的缓冲区。Linux 内核已经采用了伙伴系统管理物理内存页框,因此 slab分配器直接工作于伙伴系统之上。每种缓冲区由多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象。根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024个页框构成。出于对齐等其它方面的要求,slab 中分配给对象的内存可能大于用户要求的对象实际大小,这会造成一定的内存浪费。

2.    常用内存分配函数

2.1.   __get_free_pages

    unsigned long __get_free_pages(gfp_tgfp_mask, unsigned int order)

    __get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返回值为第一个页框的起始地址。__get_free_pages在实现上只是封装了alloc_pages函数,从代码分析,alloc_pages函数会分配长度为1<<order的连续页框块。order参数的最大值由include/linux/Mmzone.h文件中的MAX_ORDER宏决定,在默认的2.6.18内核版本中,该宏定义为10。也就是说在理论上__get_free_pages函数一次最多能申请1<<10 * 4KB也就是4MB的连续物理内存。但是在实际应用中,很可能因为不存在这么大量的连续空闲页框而导致分配失败。在测试中,order为10时分配成功,order为11则返回错误。

2.2.   kmem_cache_alloc

    struct kmem_cache *kmem_cache_create(constchar *name, size_t size,

        size_talign, unsigned long flags,

        void(*ctor)(void*, struct kmem_cache *, unsigned long),

        void(*dtor)(void*, struct kmem_cache *, unsigned long))

    void *kmem_cache_alloc(struct kmem_cache *c,gfp_t flags)

    kmem_cache_create/ kmem_cache_alloc是基于slab分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合。首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从该高速缓存区域中获取新的内存块。 kmem_cache_alloc一次能分配的最大内存由mm/slab.c文件中的MAX_OBJ_ORDER宏定义,在默认的2.6.18内核版本中,该宏定义为5,于是一次最多能申请1<<5 * 4KB也就是128KB的连续物理内存。分析内核源码发现,kmem_cache_create函数的size参数大于128KB时会调用BUG()。测试结果验证了分析结果,用kmem_cache_create分配超过128KB的内存时使内核崩溃。

2.3.   kmalloc

    void *kmalloc(size_t size, gfp_t flags)

    kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函数来实现。kmalloc一次最多能申请的内存大小由include/linux/Kmalloc_size.h的内容来决定,在默认的2.6.18内核版本中,kmalloc一次最多能申请大小为131702B也就是128KB字节的连续物理内存。测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译不能通过。

2.4.   vmalloc

    void *vmalloc(unsigned long size)

    前面几种内存分配方式都是物理连续的,能保证较低的平均访问时间。但是在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存。图3-1表示的是vmalloc分配的内存使用的地址范围。vmalloc对一次能分配的内存大小没有明确限制。出于性能考虑,应谨慎使用vmalloc函数。在测试过程中,最大能一次分配1GB的空间。

Linux内核部分内存分布

2.5.   dma_alloc_coherent

    void *dma_alloc_coherent(struct device *dev,size_t size,

ma_addr_t*dma_handle, gfp_t gfp)

    DMA是一种硬件机制,允许外围设备和主存之间直接传输IO数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的吞吐量。DMA操作中,涉及到CPU高速缓存和对应的内存数据一致性的问题,必须保证两者的数据一致,在x86_64体系结构中,硬件已经很好的解决了这个问题,dma_alloc_coherent和__get_free_pages函数实现差别不大,前者实际是调用__alloc_pages函数来分配内存,因此一次分配内存的大小限制和后者一样。__get_free_pages分配的内存同样可以用于DMA操作。测试结果证明,dma_alloc_coherent函数一次能分配的最大内存也为4M。

2.6.   ioremap

    void * ioremap (unsigned long offset,unsigned long size)

    ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段物理地址映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,和上面的几种内存分配方式并不太一样,并不是分配一段新的物理内存。ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间。ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试。

2.7.   Boot Memory

    如果要分配大量的连续物理内存,上述的分配函数都不能满足,就只能用比较特殊的方式,在Linux内核引导阶段来预留部分内存。

2.7.1.      在内核引导时分配内存

    void* alloc_bootmem(unsigned long size)

    可以在Linux内核引导过程中绕过伙伴系统来分配大块内存。使用方法是在Linux内核引导时,调用mem_init函数之前用alloc_bootmem函数申请指定大小的内存。如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL导出,然后就可以使用这块内存了。这种内存分配方式的缺点是,申请内存的代码必须在链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统看不到这部分内存,需要用户自行管理。测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块。

2.7.2.      通过内核引导参数预留顶部内存

    在Linux内核引导时,传入参数“mem=size”保留顶部的内存区间。比如系统有256MB内存,参数“mem=248M”会预留顶部的8MB内存,进入系统后可以调用ioremap(0xF800000,0x800000)来申请这段内存。

3.    几种分配函数的比较


分配原理

最大内存

其他

__get_free_pages

直接对页框进行操作

4MB

适用于分配较大量的连续物理内存

kmem_cache_alloc

基于slab机制实现

128KB

适合需要频繁申请释放相同大小内存块时使用

kmalloc

基于kmem_cache_alloc实现

128KB

最常见的分配方式,需要小于页框大小的内存时可以使用

vmalloc

建立非连续物理内存到虚拟地址的映射


物理不连续,适合需要大内存,但是对地址连续性没有要求的场合

dma_alloc_coherent

基于__alloc_pages实现

4MB

适用于DMA操作

ioremap

实现已知物理地址到虚拟地址的映射


适用于物理地址已知的场合,如设备驱动

alloc_bootmem

在启动kernel时,预留一段内存,内核看不见


小于物理内存大小,内存管理要求较高

  注:表中提到的最大内存数据来自CentOS5.3 x86_64系统,其他系统和体系结构会有不同

Linux中断(interrupt)子系统之一:中断系统基本原理

这个中断系列文章主要针对移动设备中的Linux进行讨论,文中的例子基本都是基于ARM这一体系架构,其他架构的原理其实也差不多,区别只是其中的硬件抽象层。内核版本基于3.3。虽然内核的版本不断地提升,不过自从上一次变更到当前的通用中断子系统后,大的框架性的东西并没有太大的改变。

/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  设备、中断控制器和CPU

一个完整的设备中,与中断相关的硬件可以划分为3类,它们分别是:设备、中断控制器和CPU本身,下图展示了一个smp系统中的中断硬件的组成结构:

                                                                       图 1.1  中断系统的硬件组成

        设备  设备是发起中断的源,当设备需要请求某种服务的时候,它会发起一个硬件中断信号,通常,该信号会连接至中断控制器,由中断控制器做进一步的处理。在现代的移动设备中,发起中断的设备可以位于soc(system-on-chip)芯片的外部,也可以位于soc的内部,因为目前大多数soc都集成了大量的硬件IP,例如I2C、SPI、Display Controller等等。

        中断控制器  中断控制器负责收集所有中断源发起的中断,现有的中断控制器几乎都是可编程的,通过对中断控制器的编程,我们可以控制每个中断源的优先级、中断的电器类型,还可以打开和关闭某一个中断源,在smp系统中,甚至可以控制某个中断源发往哪一个CPU进行处理。对于ARM架构的soc,使用较多的中断控制器是VIC(Vector Interrupt Controller),进入多核时代以后,GIC(General Interrupt Controller)的应用也开始逐渐变多。

        CPU  cpu是最终响应中断的部件,它通过对可编程中断控制器的编程操作,控制和管理者系统中的每个中断,当中断控制器最终判定一个中断可以被处理时,他会根据事先的设定,通知其中一个或者是某几个cpu对该中断进行处理,虽然中断控制器可以同时通知数个cpu对某一个中断进行处理,实际上,最后只会有一个cpu相应这个中断请求,但具体是哪个cpu进行响应是可能是随机的,中断控制器在硬件上对这一特性进行了保证,不过这也依赖于操作系统对中断系统的软件实现。在smp系统中,cpu之间也通过IPI(inter processor interrupt)中断进行通信。

2.  IRQ编号

系统中每一个注册的中断源,都会分配一个唯一的编号用于识别该中断,我们称之为IRQ编号。IRQ编号贯穿在整个Linux的通用中断子系统中。在移动设备中,每个中断源的IRQ编号都会在arch相关的一些头文件中,例如arch/xxx/mach-xxx/include/irqs.h。驱动程序在请求中断服务时,它会使用IRQ编号注册该中断,中断发生时,cpu通常会从中断控制器中获取相关信息,然后计算出相应的IRQ编号,然后把该IRQ编号传递到相应的驱动程序中。

3.  在驱动程序中申请中断

Linux中断子系统向驱动程序提供了一系列的API,其中的一个用于向系统申请中断:

  1. int request_threaded_irq(unsigned int irq, irq_handler_t handler,  

  2.              irq_handler_t thread_fn, unsigned long irqflags,  

  3.              const char *devname, void *dev_id)  

其中,

  • irq是要申请的IRQ编号,

  • handler是中断处理服务函数,该函数工作在中断上下文中,如果不需要,可以传入NULL,但是不可以和thread_fn同时为NULL;

  • thread_fn是中断线程的回调函数,工作在内核进程上下文中,如果不需要,可以传入NULL,但是不可以和handler同时为NULL;

  • irqflags是该中断的一些标志,可以指定该中断的电气类型,是否共享等信息;

  • devname指定该中断的名称;

  • dev_id用于共享中断时的cookie data,通常用于区分共享中断具体由哪个设备发起;

关于该API的详细工作机理我们后面再讨论。

4.  通用中断子系统(Generic irq)的软件抽象

在通用中断子系统(generic irq)出现之前,内核使用__do_IRQ处理所有的中断,这意味着__do_IRQ中要处理各种类型的中断,这会导致软件的复杂性增加,层次不分明,而且代码的可重用性也不好。事实上,到了内核版本2.6.38,__do_IRQ这种方式已经彻底在内核的代码中消失了。通用中断子系统的原型最初出现于ARM体系中,一开始内核的开发者们把3种中断类型区分出来,他们是:

  • 电平触发中断(level type)

  • 边缘触发中断(edge type)

  • 简易的中断(simple type)

后来又针对某些需要回应eoi(end of interrupt)的中断控制器,加入了fast eoi type,针对smp加入了per cpu type。把这些不同的中断类型抽象出来后,成为了中断子系统的流控层。要使所有的体系架构都可以重用这部分的代码,中断控制器也被进一步地封装起来,形成了中断子系统中的硬件封装层。我们可以用下面的图示表示通用中断子系统的层次结构:

                                                 图 4.1  通用中断子系统的层次结构

        硬件封装层  它包含了体系架构相关的所有代码,包括中断控制器的抽象封装,arch相关的中断初始化,以及各个IRQ的相关数据结构的初始化工作,cpu的中断入口也会在arch相关的代码中实现。中断通用逻辑层通过标准的封装接口(实际上就是struct irq_chip定义的接口)访问并控制中断控制器的行为,体系相关的中断入口函数在获取IRQ编号后,通过中断通用逻辑层提供的标准函数,把中断调用传递到中断流控层中。我们看看irq_chip的部分定义:

  1. struct irq_chip {  

  2.     const char  *name;  

  3.     unsigned int    (*irq_startup)(struct irq_data *data);  

  4.     void        (*irq_shutdown)(struct irq_data *data);  

  5.     void        (*irq_enable)(struct irq_data *data);  

  6.     void        (*irq_disable)(struct irq_data *data);  


  7.     void        (*irq_ack)(struct irq_data *data);  

  8.     void        (*irq_mask)(struct irq_data *data);  

  9.     void        (*irq_mask_ack)(struct irq_data *data);  

  10.     void        (*irq_unmask)(struct irq_data *data);  

  11.     void        (*irq_eoi)(struct irq_data *data);  


  12.     int     (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);  

  13.     int     (*irq_retrigger)(struct irq_data *data);  

  14.     int     (*irq_set_type)(struct irq_data *data, unsigned int flow_type);  

  15.     int     (*irq_set_wake)(struct irq_data *data, unsigned int on);  

  16.         ......  

  17. };  

看到上面的结构定义,很明显,它实际上就是对中断控制器的接口抽象,我们只要对每个中断控制器实现以上接口(不必全部),并把它和相应的irq关联起来,上层的实现即可通过这些接口访问中断控制器。而且,同一个中断控制器的代码可以方便地被不同的平台所重用。

        中断流控层  所谓中断流控是指合理并正确地处理连续发生的中断,比如一个中断在处理中,同一个中断再次到达时如何处理,何时应该屏蔽中断,何时打开中断,何时回应中断控制器等一系列的操作。该层实现了与体系和硬件无关的中断流控处理操作,它针对不同的中断电气类型(level,edge......),实现了对应的标准中断流控处理函数,在这些处理函数中,最终会把中断控制权传递到驱动程序注册中断时传入的处理函数或者是中断线程中。目前内核提供了以下几个主要的中断流控函数的实现(只列出部分):

  • handle_simple_irq(); 

  • handle_level_irq();  电平中断流控处理程序

  • handle_edge_irq();  边沿触发中断流控处理程序

  • handle_fasteoi_irq();  需要eoi的中断处理器使用的中断流控处理程序

  • handle_percpu_irq();  该irq只有单个cpu响应时使用的流控处理程序

        中断通用逻辑层  该层实现了对中断系统几个重要数据的管理,并提供了一系列的辅助管理函数。同时,该层还实现了中断线程的实现和管理,共享中断和嵌套中断的实现和管理,另外它还提供了一些接口函数,它们将作为硬件封装层和中断流控层以及驱动程序API层之间的桥梁,例如以下API:

  • generic_handle_irq();

  • irq_to_desc();

  • irq_set_chip();

  • irq_set_chained_handler();

        驱动程序API  该部分向驱动程序提供了一系列的API,用于向系统申请/释放中断,打开/关闭中断,设置中断类型和中断唤醒系统的特性等操作。驱动程序的开发者通常只会使用到这一层提供的这些API即可完成驱动程序的开发工作,其他的细节都由另外几个软件层较好地“隐藏”起来了,驱动程序开发者无需再关注底层的实现,这看起来确实是一件美妙的事情,不过我认为,要想写出好的中断代码,还是花点时间了解一下其他几层的实现吧。其中的一些API如下:

  • enable_irq();

  • disable_irq();

  • disable_irq_nosync();

  • request_threaded_irq();

  • irq_set_affinity();

这里不再对每一层做详细的介绍,我将会在本系列的其他几篇文章中做深入的探讨。

5.  irq描述结构:struct irq_desc

整个通用中断子系统几乎都是围绕着irq_desc结构进行,系统中每一个irq都对应着一个irq_desc结构,所有的irq_desc结构的组织方式有两种:

        基于数组方式  平台相关板级代码事先根据系统中的IRQ数量,定义常量:NR_IRQS,在kernel/irq/irqdesc.c中使用该常量定义irq_desc结构数组:

  1. struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {  

  2.     [0 ... NR_IRQS-1] = {  

  3.         .handle_irq = handle_bad_irq,  

  4.         .depth      = 1,  

  5.         .lock       = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),  

  6.     }  

  7. };  


        基于基数树方式  当内核的配置项CONFIG_SPARSE_IRQ被选中时,内核使用基数树(radix tree)来管理irq_desc结构,这一方式可以动态地分配irq_desc结构,对于那些具备大量IRQ数量或者IRQ编号不连续的系统,使用该方式管理irq_desc对内存的节省有好处,而且对那些自带中断控制器管理设备自身多个中断源的外部设备,它们可以在驱动程序中动态地申请这些中断源所对应的irq_desc结构,而不必在系统的编译阶段保留irq_desc结构所需的内存。

下面我们看一看irq_desc的部分定义:

  1. struct irq_data {  

  2.     unsigned int        irq;  

  3.     unsigned long       hwirq;  

  4.     unsigned int        node;  

  5.     unsigned int        state_use_accessors;  

  6.     struct irq_chip     *chip;  

  7.     struct irq_domain   *domain;  

  8.     void            *handler_data;  

  9.     void            *chip_data;  

  10.     struct msi_desc     *msi_desc;  

  11. #ifdef CONFIG_SMP  

  12.     cpumask_var_t       affinity;  

  13. #endif  

  14. };  

  1. struct irq_desc {  

  2.     struct irq_data     irq_data;  

  3.     unsigned int __percpu   *kstat_irqs;  

  4.     irq_flow_handler_t  handle_irq;  

  5. #ifdef CONFIG_IRQ_PREFLOW_FASTEOI  

  6.     irq_preflow_handler_t   preflow_handler;  

  7. #endif  

  8.     struct irqaction    *action;    /* IRQ action list */  

  9.     unsigned int        status_use_accessors;  

  10.     unsigned int        depth;      /* nested irq disables */  

  11.     unsigned int        wake_depth; /* nested wake enables */  

  12.     unsigned int        irq_count;  /* For detecting broken IRQs */  


  13.     raw_spinlock_t      lock;  

  14.     struct cpumask      *percpu_enabled;  

  15. #ifdef CONFIG_SMP  

  16.     const struct cpumask    *affinity_hint;  

  17.     struct irq_affinity_notify *affinity_notify;  

  18. #ifdef CONFIG_GENERIC_PENDING_IRQ  

  19.     cpumask_var_t       pending_mask;  

  20. #endif  

  21. #endif  

  22.     wait_queue_head_t       wait_for_threads;  


  23.     const char      *name;  

  24. } ____cacheline_internodealigned_in_smp;  

对于irq_desc中的主要字段做一个解释:     

        irq_data  这个内嵌结构在2.6.37版本引入,之前的内核版本的做法是直接把这个结构中的字段直接放置在irq_desc结构体中,然后在调用硬件封装层的chip->xxx()回调中传入IRQ编号作为参数,但是底层的函数经常需要访问->handler_data,->chip_data,->msi_desc等字段,这需要利用irq_to_desc(irq)来获得irq_desc结构的指针,然后才能访问上述字段,者带来了性能的降低,尤其在配置为sparse irq的系统中更是如此,因为这意味着基数树的搜索操作。为了解决这一问题,内核开发者把几个低层函数需要使用的字段单独封装为一个结构,调用时的参数则改为传入该结构的指针。实现同样的目的,那为什么不直接传入irq_desc结构指针?因为这会破坏层次的封装性,我们不希望低层代码可以看到不应该看到的部分,仅此而已。

        kstat_irqs  用于irq的一些统计信息,这些统计信息可以从proc文件系统中查询。

        action  中断响应链表,当一个irq被触发时,内核会遍历该链表,调用action结构中的回调handler或者激活其中的中断线程,之所以实现为一个链表,是为了实现中断的共享,多个设备共享同一个irq,这在外围设备中是普遍存在的。

        status_use_accessors  记录该irq的状态信息,内核提供了一系列irq_settings_xxx的辅助函数访问该字段,详细请查看kernel/irq/settings.h

        depth  用于管理enable_irq()/disable_irq()这两个API的嵌套深度管理,每次enable_irq时该值减去1,每次disable_irq时该值加1,只有depth==0时才真正向硬件封装层发出关闭irq的调用,只有depth==1时才会向硬件封装层发出打开irq的调用。disable的嵌套次数可以比enable的次数多,此时depth的值大于1,随着enable的不断调用,当depth的值为1时,在向硬件封装层发出打开irq的调用后,depth减去1后,此时depth为0,此时处于一个平衡状态,我们只能调用disable_irq,如果此时enable_irq被调用,内核会报告一个irq失衡的警告,提醒驱动程序的开发人员检查自己的代码。

        lock  用于保护irq_desc结构本身的自旋锁。

        affinity_hit  用于提示用户空间,作为优化irq和cpu之间的亲缘关系的依据。

        pending_mask  用于调整irq在各个cpu之间的平衡。

        wait_for_threads  用于synchronize_irq(),等待该irq所有线程完成。

irq_data结构中的各字段:

        irq  该结构所对应的IRQ编号。

        hwirq  硬件irq编号,它不同于上面的irq;

        node  通常用于hwirq和irq之间的映射操作;

        state_use_accessors  硬件封装层需要使用的状态信息,不要直接访问该字段,内核定义了一组函数用于访问该字段:irqd_xxxx(),参见include/linux/irq.h。

        chip  指向该irq所属的中断控制器的irq_chip结构指针

        handler_data  每个irq的私有数据指针,该字段由硬件封转层使用,例如用作底层硬件的多路复用中断。

        chip_data  中断控制器的私有数据,该字段由硬件封转层使用。

        msi_desc  用于PCIe总线的MSI或MSI-X中断机制。

        affinity  记录该irq与cpu之间的亲缘关系,它其实是一个bit-mask,每一个bit代表一个cpu,置位后代表该cpu可能处理该irq。

这是通用中断子系统系列文章的第一篇,这里不会详细介绍各个软件层次的实现原理,但是有必要对整个架构做简要的介绍:

  • 系统启动阶段,取决于内核的配置,内核会通过数组或基数树分配好足够多的irq_desc结构;

  • 根据不同的体系结构,初始化中断相关的硬件,尤其是中断控制器;

  • 为每个必要irq的irq_desc结构填充默认的字段,例如irq编号,irq_chip指针,根据不同的中断类型配置流控handler;

  • 设备驱动程序在初始化阶段,利用request_threaded_irq() api申请中断服务,两个重要的参数是handler和thread_fn;

  • 当设备触发一个中断后,cpu会进入事先设定好的中断入口,它属于底层体系相关的代码,它通过中断控制器获得irq编号,在对irq_data结构中的某些字段进行处理后,会将控制权传递到中断流控层(通过irq_desc->handle_irq);

  • 中断流控处理代码在作出必要的流控处理后,通过irq_desc->action链表,取出驱动程序申请中断时注册的handler和thread_fn,根据它们的赋值情况,或者只是调用handler回调,或者启动一个线程执行thread_fn,又或者两者都执行;

  • 至此,中断最终由驱动程序进行了响应和处理。

6.  中断子系统的proc文件接口

在/proc目录下面,有两个与中断子系统相关的文件和子目录,它们是:

  • /proc/interrupts:文件

  • /proc/irq:子目录

读取interrupts会依次显示irq编号,每个cpu对该irq的处理次数,中断控制器的名字,irq的名字,以及驱动程序注册该irq时使用的名字,以下是一个例子:

/proc/irq目录下面会为每个注册的irq创建一个以irq编号为名字的子目录,每个子目录下分别有以下条目:

  • smp_affinity            irq和cpu之间的亲缘绑定关系;

  • smp_affinity_hint   只读条目,用于用户空间做irq平衡只用;

  • spurious                  可以获得该irq被处理和未被处理的次数的统计信息;

  • handler_name       驱动程序注册该irq时传入的处理程序的名字;

根据irq的不同,以上条目不一定会全部都出现,以下是某个设备的例子:

# cd /proc/irq
# ls
ls
332
248
......
......
12
11
default_smp_affinity


# ls 332
bcmsdh_sdmmc
spurious
node
affinity_hint
smp_affinity


# cat 332/smp_affinity
3

可见,以上设备是一个使用双核cpu的设备,因为smp_affinity的值是3,系统默认每个中断可以由两个cpu进行处理。

Linux中断(interrupt)子系统之二:arch相关的硬件封装层

Linux的通用中断子系统的一个设计原则就是把底层的硬件实现尽可能地隐藏起来,使得驱动程序的开发人员不用关注底层的实现,要实现这个目标,内核的开发者们必须把硬件相关的内容剥离出来,然后定义一些列标准的接口供上层访问,上层的开发人员只要知道这些接口即可完成对中断的进一步处理和控制。对底层的封装主要包括两部分:

  • 实现不同体系结构中断入口,这部分代码通常用asm实现;

  • 中断控制器进行封装和实现;

本文的内容正是要讨论硬件封装层的实现细节。我将以ARM体系进行介绍,大部分的代码位于内核代码树的arch/arm/目录内。

/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  CPU的中断入口

 我们知道,arm的异常和复位向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端向量,向量地址位于0xffff0000,Linux选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上:

ARM的异常向量表
地址异常种类
FFFF0000复位
FFFF0004未定义指令
FFFF0008软中断(swi)
FFFF000CPrefetch abort
FFFF0010Data abort
FFFF0014保留
FFFF0018IRQ
FFFF001CFIQ

中断向量表在arch/arm/kernel/entry_armv.S中定义,为了方便讨论,下面只列出部分关键的代码:

[plain] view plain copy
  1.     .globl  __stubs_start  

  2. __stubs_start:  


  3.     vector_stub irq, IRQ_MODE, 4  


  4.     .long   __irq_usr           @  0  (USR_26 / USR_32)  

  5.     .long   __irq_invalid           @  1  (FIQ_26 / FIQ_32)  

  6.     .long   __irq_invalid           @  2  (IRQ_26 / IRQ_32)  

  7.     .long   __irq_svc           @  3  (SVC_26 / SVC_32)  


  8.     vector_stub dabt, ABT_MODE, 8  


  9.     .long   __dabt_usr          @  0  (USR_26 / USR_32)  

  10.     .long   __dabt_invalid          @  1  (FIQ_26 / FIQ_32)  

  11.     .long   __dabt_invalid          @  2  (IRQ_26 / IRQ_32)  

  12.     .long   __dabt_svc          @  3  (SVC_26 / SVC_32)  


  13. vector_fiq:  

  14.     disable_fiq  

  15.     subs    pc, lr, #4  

  16.     ......  

  17.     .globl  __stubs_end  

  18. __stubs_end:  




  19.     .equ    stubs_offset, __vectors_start + 0x200 - __stubs_start  

  20.     .globl  __vectors_start  

  21. __vectors_start:  

  22.  ARM(   swi SYS_ERROR0  )  

  23.  THUMB( svc #0      )  

  24.  THUMB( nop         )  

  25.     W(b)    vector_und + stubs_offset  

  26.     W(ldr)  pc, .LCvswi + stubs_offset  

  27.     W(b)    vector_pabt + stubs_offset  

  28.     W(b)    vector_dabt + stubs_offset  

  29.     W(b)    vector_addrexcptn + stubs_offset  

  30.     W(b)    vector_irq + stubs_offset  

  31.     W(b)    vector_fiq + stubs_offset  


  32.     .globl  __vectors_end  

  33. __vectors_end:  

代码被分为两部分:

  • 第一部分是真正的向量跳转表,位于__vectors_start和__vectors_end之间;

  • 第二部分是处理跳转的部分,位于__stubs_start和__stubs_end之间;

[plain] view plain copy
  1. vector_stub irq, IRQ_MODE, 4  

以上这一句把宏展开后实际上就是定义了vector_irq,根据进入中断前的cpu模式,分别跳转到__irq_usr或__irq_svc。

[plain] view plain copy
  1. vector_stub dabt, ABT_MODE, 8  

以上这一句把宏展开后实际上就是定义了vector_dabt,根据进入中断前的cpu模式,分别跳转到__dabt_usr或__dabt_svc。
系统启动阶段,位于arch/arm/kernel/traps.c中的early_trap_init()被调用:

  1. void __init early_trap_init(void)  

  2. {  

  3.     ......  

  4.     /* 

  5.      * Copy the vectors, stubs and kuser helpers (in entry-armv.S) 

  6.      * into the vector page, mapped at 0xffff0000, and ensure these 

  7.      * are visible to the instruction stream. 

  8.      */  

  9.     memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);  

  10.     memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);  

  11.     ......  

  12. }  

以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x200处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们。

                                                                        图1.1  Linux中ARM体系的中断向量拷贝过程

对于系统的外部设备来说,通常都是使用IRQ中断,所以我们只关注__irq_usr和__irq_svc,两者的区别是进入和退出中断时是否进行用户栈和内核栈之间的切换,还有进程调度和抢占的处理等,这些细节不在这里讨论。两个函数最终都会进入irq_handler这个宏:

[plain] view plain copy
  1.     .macro  irq_handler  

  2. #ifdef CONFIG_MULTI_IRQ_HANDLER  

  3.     ldr r1, =handle_arch_irq  

  4.     mov r0, sp  

  5.     adr lr, BSYM(9997f)  

  6.     ldr pc, [r1]  

  7. #else  

  8.     arch_irq_handler_default  

  9. #endif  

  10. 9997:  

  11.     .endm  

如果选择了MULTI_IRQ_HANDLER配置项,则意味着允许平台的代码可以动态设置irq处理程序,平台代码可以修改全局变量:handle_arch_irq,从而可以修改irq的处理程序。这里我们讨论默认的实现:arch_irq_handler_default,它位于arch/arm/include/asm/entry_macro_multi.S中:

[plain] view plain copy
  1.     .macro  arch_irq_handler_default  

  2.     get_irqnr_preamble r6, lr  

  3. 1:  get_irqnr_and_base r0, r2, r6, lr  

  4.     movne   r1, sp  

  5.     @  

  6.     @ routine called with r0 = irq number, r1 = struct pt_regs *  

  7.     @  

  8.     adrne   lr, BSYM(1b)  

  9.     bne asm_do_IRQ  

  10.     ......  

get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得IRQ编号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是IRQ编号和寄存器结构指针,这个函数在arch/arm/kernel/irq.c中实现:

  1. /* 

  2.  * asm_do_IRQ is the interface to be used from assembly code. 

  3.  */  

  4. asmlinkage void __exception_irq_entry  

  5. asm_do_IRQ(unsigned int irq, struct pt_regs *regs)  

  6. {  

  7.     handle_IRQ(irq, regs);  

  8. }  

到这里,中断程序完成了从asm代码到C代码的传递,并且获得了引起中断的IRQ编号。

2.  初始化

与通用中断子系统相关的初始化由start_kernel()函数发起,调用流程如下图所视:

                                                                           图2.1  通用中断子系统的初始化

  • 首先,在setup_arch函数中,early_trap_init被调用,其中完成了第1节所说的中断向量的拷贝和重定位工作。

  • 然后,start_kernel发出early_irq_init调用,early_irq_init属于与硬件和平台无关的通用逻辑层,它完成irq_desc结构的内存申请,为它们其中某些字段填充默认值,完成后调用体系相关的arch_early_irq_init函数完成进一步的初始化工作,不过ARM体系没有实现arch_early_irq_init。

  • 接着,start_kernel发出init_IRQ调用,它会直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义。

  • machine_desc->init_irq()完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联。

3.  中断控制器的软件抽象:struct irq_chip

正如上一篇文章Linux中断(interrupt)子系统之一:中断系统基本原理所述,所有的硬件中断在到达CPU之前,都要先经过中断控制器进行汇集,合乎要求的中断请求才会通知cpu进行处理,中断控制器主要完成以下这些功能:

  • 对各个irq的优先级进行控制;

  • 向CPU发出中断请求后,提供某种机制让CPU获得实际的中断源(irq编号);

  • 控制各个irq的电气触发条件,例如边缘触发或者是电平触发;

  • 使能(enable)或者屏蔽(mask)某一个irq;

  • 提供嵌套中断请求的能力;

  • 提供清除中断请求的机制(ack);

  • 有些控制器还需要CPU在处理完irq后对控制器发出eoi指令(end of interrupt);

  • 在smp系统中,控制各个irq与cpu之间的亲缘关系(affinity);

通用中断子系统把中断控制器抽象为一个数据结构:struct irq_chip,其中定义了一系列的操作函数,大部分多对应于上面所列的某个功能:

  1. struct irq_chip {  

  2.     const char  *name;  

  3.     unsigned int    (*irq_startup)(struct irq_data *data);  

  4.     void        (*irq_shutdown)(struct irq_data *data);  

  5.     void        (*irq_enable)(struct irq_data *data);  

  6.     void        (*irq_disable)(struct irq_data *data);  


  7.     void        (*irq_ack)(struct irq_data *data);  

  8.     void        (*irq_mask)(struct irq_data *data);  

  9.     void        (*irq_mask_ack)(struct irq_data *data);  

  10.     void        (*irq_unmask)(struct irq_data *data);  

  11.     void        (*irq_eoi)(struct irq_data *data);  


  12.     int     (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);  

  13.     int     (*irq_retrigger)(struct irq_data *data);  

  14.     int     (*irq_set_type)(struct irq_data *data, unsigned int flow_type);  

  15.     int     (*irq_set_wake)(struct irq_data *data, unsigned int on);  


  16.     void        (*irq_bus_lock)(struct irq_data *data);  

  17.     void        (*irq_bus_sync_unlock)(struct irq_data *data);  


  18.     void        (*irq_cpu_online)(struct irq_data *data);  

  19.     void        (*irq_cpu_offline)(struct irq_data *data);  


  20.     void        (*irq_suspend)(struct irq_data *data);  

  21.     void        (*irq_resume)(struct irq_data *data);  

  22.     void        (*irq_pm_shutdown)(struct irq_data *data);  


  23.     void        (*irq_print_chip)(struct irq_data *data, struct seq_file *p);  


  24.     unsigned long   flags;  


  25.     /* Currently used only by UML, might disappear one day.*/  

  26. #ifdef CONFIG_IRQ_RELEASE_METHOD  

  27.     void        (*release)(unsigned int irq, void *dev_id);  

  28. #endif  

  29. };  

各个字段解释如下:

name  中断控制器的名字,会出现在 /proc/interrupts中。

irq_startup  第一次开启一个irq时使用。

irq_shutdown  与irq_starup相对应。

irq_enable  使能该irq,通常是直接调用irq_unmask()。

irq_disable  禁止该irq,通常是直接调用irq_mask,严格意义上,他俩其实代表不同的意义,disable表示中断控制器根本就不响应该irq,而mask时,中断控制器可能响应该irq,只是不通知CPU,这时,该irq处于pending状态。类似的区别也适用于enable和unmask。

irq_ack  用于CPU对该irq的回应,通常表示cpu希望要清除该irq的pending状态,准备接受下一个irq请求。

irq_mask  屏蔽该irq。

irq_unmask  取消屏蔽该irq。

irq_mask_ack  相当于irq_mask + irq_ack。

irq_eoi  有些中断控制器需要在cpu处理完该irq后发出eoi信号,该回调就是用于这个目的。

irq_set_affinity  用于设置该irq和cpu之间的亲缘关系,就是通知中断控制器,该irq发生时,那些cpu有权响应该irq。当然,中断控制器会在软件的配合下,最终只会让一个cpu处理本次请求。

irq_set_type  设置irq的电气触发条件,例如IRQ_TYPE_LEVEL_HIGH或IRQ_TYPE_EDGE_RISING。

irq_set_wake  通知电源管理子系统,该irq是否可以用作系统的唤醒源。

以上大部分的函数接口的参数都是irq_data结构指针,irq_data结构的由来在上一篇文章已经说过,这里仅贴出它的定义,各字段的意义请参考注释:

  1. /** 

  2.  * struct irq_data - per irq and irq chip data passed down to chip functions 

  3.  * @irq:        interrupt number 

  4.  * @hwirq:      hardware interrupt number, local to the interrupt domain 

  5.  * @node:       node index useful for balancing 

  6.  * @state_use_accessors: status information for irq chip functions. 

  7.  *          Use accessor functions to deal with it 

  8.  * @chip:       low level interrupt hardware access 

  9.  * @domain:     Interrupt translation domain; responsible for mapping 

  10.  *          between hwirq number and linux irq number. 

  11.  * @handler_data:   per-IRQ data for the irq_chip methods 

  12.  * @chip_data:      platform-specific per-chip private data for the chip 

  13.  *          methods, to allow shared chip implementations 

  14.  * @msi_desc:       MSI descriptor 

  15.  * @affinity:       IRQ affinity on SMP 

  16.  * 

  17.  * The fields here need to overlay the ones in irq_desc until we 

  18.  * cleaned up the direct references and switched everything over to 

  19.  * irq_data. 

  20.  */  

  21. struct irq_data {  

  22.     unsigned int        irq;  

  23.     unsigned long       hwirq;  

  24.     unsigned int        node;  

  25.     unsigned int        state_use_accessors;  

  26.     struct irq_chip     *chip;  

  27.     struct irq_domain   *domain;  

  28.     void            *handler_data;  

  29.     void            *chip_data;  

  30.     struct msi_desc     *msi_desc;  

  31. #ifdef CONFIG_SMP  

  32.     cpumask_var_t       affinity;  

  33. #endif  

  34. };  

根据设备使用的中断控制器的类型,体系架构的底层的开发只要实现上述接口中的各个回调函数,然后把它们填充到irq_chip结构的实例中,最终把该irq_chip实例注册到irq_desc.irq_data.chip字段中,这样各个irq和中断控制器就进行了关联,只要知道irq编号,即可得到对应到irq_desc结构,进而可以通过chip指针访问中断控制器。 

4.  进入流控处理层

进入C代码的第一个函数是asm_do_IRQ,在ARM体系中,这个函数只是简单地调用handle_IRQ:

  1. /* 

  2.  * asm_do_IRQ is the interface to be used from assembly code. 

  3.  */  

  4. asmlinkage void __exception_irq_entry  

  5. asm_do_IRQ(unsigned int irq, struct pt_regs *regs)  

  6. {  

  7.     handle_IRQ(irq, regs);  

  8. }  

handle_IRQ本身也不是很复杂:

  1. void handle_IRQ(unsigned int irq, struct pt_regs *regs)  

  2. {  

  3.     struct pt_regs *old_regs = set_irq_regs(regs);  


  4.     irq_enter();  


  5.     /* 

  6.      * Some hardware gives randomly wrong interrupts.  Rather 

  7.      * than crashing, do something sensible. 

  8.      */  

  9.     if (unlikely(irq >= nr_irqs)) {  

  10.         if (printk_ratelimit())  

  11.             printk(KERN_WARNING "Bad IRQ%u\n", irq);  

  12.         ack_bad_irq(irq);  

  13.     } else {  

  14.         generic_handle_irq(irq);  

  15.     }  


  16.     /* AT91 specific workaround */  

  17.     irq_finish(irq);  


  18.     irq_exit();  

  19.     set_irq_regs(old_regs);  

  20. }  

irq_enter主要是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占:

  1. #define __irq_enter()                   \  

  2.     do {                        \  

  3.         account_system_vtime(current);      \  

  4.         add_preempt_count(HARDIRQ_OFFSET);  \  

  5.         trace_hardirq_enter();          \  

  6.     } while (0)  

CPU一旦响应IRQ中断后,ARM会自动把CPSR中的I位置位,表明禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。你可能会奇怪,既然此时的irq中断都是都是被禁止的,为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理。

下一步,generic_handle_irq被调用,generic_handle_irq是通用逻辑层提供的API,通过该API,中断的控制被传递到了与体系结构无关的中断流控层:

  1. int generic_handle_irq(unsigned int irq)  

  2. {  

  3.     struct irq_desc *desc = irq_to_desc(irq);  


  4.     if (!desc)  

  5.         return -EINVAL;  

  6.     generic_handle_irq_desc(irq, desc);  

  7.     return 0;  

  8. }  

最终会进入该irq注册的流控处理回调中:

  1. static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)  

  2. {  

  3.     desc->handle_irq(irq, desc);  

  4. }  

5.  中断控制器的级联

在实际的设备中,经常存在多个中断控制器,有时多个中断控制器还会进行所谓的级联。为了方便讨论,我们把直接和CPU相连的中断控制器叫做根控制器,另外一些和跟控制器相连的叫子控制器。根据子控制器的位置,我们把它们分为两种类型:

  • 机器级别的级联  子控制器位于SOC内部,或者子控制器在SOC的外部,但是是某个板子系列的标准配置,如图5.1的左边所示;

  • 设备级别的级联  子控制器位于某个外部设备中,用于汇集该设备发出的多个中断,如图5.1的右边所示;

 

                                                                                 图5.1  中断控制器的级联类型

对于机器级别的级联,级联的初始化代码理所当然地位于板子的初始化代码中(arch/xxx/mach-xxx),因为只要是使用这个板子或SOC的设备,必然要使用这个子控制器。而对于设备级别的级联,因为该设备并不一定是系统的标配设备,所以中断控制器的级联操作应该在该设备的驱动程序中实现。机器设备的级联,因为得益于事先已经知道子控制器的硬件连接信息,内核可以方便地为子控制器保留相应的irq_desc结构和irq编号,处理起来相对简单。设备级别的级联则不一样,驱动程序必须动态地决定组合设备中各个子设备的irq编号和irq_desc结构。本章我只讨论机器级别的级联,设备级别的关联可以使用同样的原理,也可以实现为共享中断,我会在本系列接下来的文章中讨论。

要实现中断控制器的级联,要使用以下几个的关键数据结构字段和通用中断逻辑层的API:

        irq_desc.handle_irq  irq的流控处理回调函数,子控制器在把多个irq汇集起来后,输出端连接到根控制器的其中一个irq中断线输入脚,这意味着,每个子控制器的中断发生时,CPU一开始只会得到根控制器的irq编号,然后进入该irq编号对应的irq_desc.handle_irq回调,该回调我们不能使用流控层定义好的几个流控函数,而是要自己实现一个函数,该函数负责从子控制器中获得irq的中断源,并计算出对应新的irq编号,然后调用新irq所对应的irq_desc.handle_irq回调,这个回调使用流控层的标准实现。

        irq_set_chained_handler()  该API用于设置根控制器与子控制器相连的irq所对应的irq_desc.handle_irq回调函数,并且设置IRQ_NOPROBE和IRQ_NOTHREAD以及IRQ_NOREQUEST标志,这几个标志保证驱动程序不会错误地申请该irq,因为该irq已经被作为级联irq使用。

        irq_set_chip_and_handler()  该API同时设置irq_desc中的handle_irq回调和irq_chip指针。

以下例子代码位于:/arch/arm/plat-s5p/irq-eint.c:

  1. int __init s5p_init_irq_eint(void)  

  2. {  

  3.     int irq;  


  4.     for (irq = IRQ_EINT(0); irq <= IRQ_EINT(15); irq++)  

  5.         irq_set_chip(irq, &s5p_irq_vic_eint);  


  6.     for (irq = IRQ_EINT(16); irq <= IRQ_EINT(31); irq++) {  

  7.         irq_set_chip_and_handler(irq, &s5p_irq_eint, handle_level_irq);  

  8.         set_irq_flags(irq, IRQF_VALID);  

  9.     }  


  10.     irq_set_chained_handler(IRQ_EINT16_31, s5p_irq_demux_eint16_31);  

  11.     return 0;  

  12. }  

该SOC芯片的外部中断:IRQ_EINT(0)到IRQ_EINT(15),每个引脚对应一个根控制器的irq中断线,它们是正常的irq,无需级联。IRQ_EINT(16)到IRQ_EINT(31)经过子控制器汇集后,统一连接到根控制器编号为IRQ_EINT16_31这个中断线上。可以看到,子控制器对应的irq_chip是s5p_irq_eint,子控制器的irq默认设置为电平中断的流控处理函数handle_level_irq,它们通过API:irq_set_chained_handler进行设置。如果根控制器有128个中断线,IRQ_EINT0--IRQ_EINT15通常占据128内的某段连续范围,这取决于实际的物理连接。IRQ_EINT16_31因为也属于跟控制器,所以它的值也会位于128以内,但是IRQ_EINT16--IRQ_EINT31通常会在128以外的某段范围,这时,代表irq数量的常量NR_IRQS,必须考虑这种情况,定义出超过128的某个足够的数值。级联的实现主要依靠编号为IRQ_EINT16_31的流控处理程序:s5p_irq_demux_eint16_31,它的最终实现类似于以下代码:

  1. static inline void s5p_irq_demux_eint(unsigned int start)  

  2. {  

  3.     u32 status = __raw_readl(S5P_EINT_PEND(EINT_REG_NR(start)));  

  4.     u32 mask = __raw_readl(S5P_EINT_MASK(EINT_REG_NR(start)));  

  5.     unsigned int irq;  


  6.     status &= ~mask;  

  7.     status &= 0xff;  


  8.     while (status) {  

  9.         irq = fls(status) - 1;  

  10.         generic_handle_irq(irq + start);  

  11.         status &= ~(1 << irq);  

  12.     }  

  13. }  

在获得新的irq编号后,它的最关键的一句是调用了通用中断逻辑层的API:generic_handle_irq,这时它才真正地把中断控制权传递到中断流控层中来。

Linux中断(interrupt)子系统之三:中断流控处理层

1.  中断流控层简介

早期的内核版本中,几乎所有的中断都是由__do_IRQ函数进行处理,但是,因为各种中断请求的电气特性会有所不同,又或者中断控制器的特性也不同,这会导致以下这些处理也会有所不同:

  • 何时对中断控制器发出ack回应;

  • mask_irq和unmask_irq的处理;

  • 中断控制器是否需要eoi回应?

  • 何时打开cpu的本地irq中断?以便允许irq的嵌套;

  • 中断数据结构的同步和保护;

/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/
为此,通用中断子系统把几种常用的流控类型进行了抽象,并为它们实现了相应的标准函数,我们只要选择相应的函数,赋值给irq所对应的irq_desc结构的handle_irq字段中即可。这些标准的回调函数都是irq_flow_handler_t类型:

  1. typedef void (*irq_flow_handler_t)(unsigned int irq,  

  2.                         struct irq_desc *desc);  

目前的通用中断子系统实现了以下这些标准流控回调函数,这些函数都定义在:kernel/irq/chip.c中,

  • handle_simple_irq  用于简易流控处理;

  • handle_level_irq  用于电平触发中断的流控处理;

  • handle_edge_irq  用于边沿触发中断的流控处理;

  •  handle_fasteoi_irq  用于需要响应eoi的中断控制器;

  • handle_percpu_irq  用于只在单一cpu响应的中断;

  • handle_nested_irq  用于处理使用线程的嵌套中断;

驱动程序和板级代码可以通过以下几个API设置irq的流控函数:

  • irq_set_handler();

  • irq_set_chip_and_handler();

  • irq_set_chip_and_handler_name();

以下这个序列图展示了整个通用中断子系统的中断响应过程,flow_handle一栏就是中断流控层的生命周期:

                                                                                           图1.1  通用中断子系统的中断响应过程

2.  handle_simple_irq

该函数没有实现任何实质性的流控操作,在把irq_desc结构锁住后,直接调用handle_irq_event处理irq_desc中的action链表,它通常用于多路复用(类似于中断控制器级联)中的子中断,由父中断的流控回调中调用。或者用于无需进行硬件控制的中断中。以下是它的经过简化的代码:

  1. void  

  2. handle_simple_irq(unsigned int irq, struct irq_desc *desc)  

  3. {  

  4.     raw_spin_lock(&desc->lock);  

  5.     ......  

  6.     handle_irq_event(desc);  


  7. out_unlock:  

  8.     raw_spin_unlock(&desc->lock);  

  9. }  

3.  handle_level_irq

该函数用于处理电平中断的流控操作。电平中断的特点是,只要设备的中断请求引脚(中断线)保持在预设的触发电平,中断就会一直被请求,所以,为了避免同一中断被重复响应,必须在处理中断前先把mask irq,然后ack irq,以便复位设备的中断请求引脚,响应完成后再unmask irq。实际的情况稍稍复杂一点,在mask和ack之后,还要判断IRQ_INPROGRESS标志位,如果该标志已经置位,则直接退出,不再做实质性的处理,IRQ_INPROGRESS标志在handle_irq_event的开始设置,在handle_irq_event结束时清除,如果监测到IRQ_INPROGRESS被置位,表明该irq正在被另一个CPU处理中,所以直接退出,对电平中断来说是正确的处理方法。但是我觉得在ARM系统中,这种情况根本就不会发生,因为在没有进入handle_level_irq之前,中断控制器没有收到ack通知,它不会向第二个CPU再次发出中断请求,而当程序进入handle_level_irq之后,第一个动作就是mask irq,然后ack irq(通常是联合起来的:mask_ack_irq),这时候就算设备再次发出中断请求,也是在handle_irq_event结束,unmask irq之后,这时IRQ_INPROGRESS标志已经被清除。我不知道其他像X86之类的体系是否有不同的行为,有知道的朋友请告知我一下。以下是handle_level_irq经过简化之后的代码:

  1. void  

  2. handle_level_irq(unsigned int irq, struct irq_desc *desc)  

  3. {  

  4.     raw_spin_lock(&desc->lock);  

  5.     mask_ack_irq(desc);  


  6.     if (unlikely(irqd_irq_inprogress(&desc->irq_data)))  

  7.             goto out_unlock;  

  8.         ......  


  9.     if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data)))  

  10.         goto out_unlock;  


  11.     handle_irq_event(desc);  


  12.     if (!irqd_irq_disabled(&desc->irq_data) && !(desc->istate & IRQS_ONESHOT))  

  13.         unmask_irq(desc);  

  14. out_unlock:  

  15.     raw_spin_unlock(&desc->lock);  

  16. }  

虽然handle_level_irq对电平中断的流控进行了必要的处理,因为电平中断的特性:只要没有ack irq,中断线会一直有效,所以我们不会错过某次中断请求,但是驱动程序的开发人员如果对该过程理解不透彻,特别容易发生某次中断被多次处理的情况。特别是使用了中断线程(action->thread_fn)来响应中断的时候:通常mask_ack_irq只会清除中断控制器的pending状态,很多慢速设备(例如通过i2c或spi控制的设备)需要在中断线程中清除中断线的pending状态,但是未等到中断线程被调度执行的时候,handle_level_irq早就返回了,这时已经执行过unmask_irq,设备的中断线pending处于有效状态,中断控制器会再次发出中断请求,结果是设备的一次中断请求,产生了两次中断响应。要避免这种情况,最好的办法就是不要单独使用中断线程处理中断,而是要实现request_threaded_irq()的第二个参数irq_handler_t:handler,在handle回调中使用disable_irq()关闭该irq,然后在退出中断线程回调前再enable_irq()。假设action->handler没有屏蔽irq,以下这幅图展示了电平中断期间IRQ_PROGRESS标志、本地中断状态和触发其他CPU的状态:

                                          图3.1  电平触发中断状态

上图中颜色分别代表不同的状态:

状态红色绿色
IRQ_PROGRESS           TRUE       FALSE
是否允许本地cpu中断            禁止                允许  
是否允许该设备再次触发中断(可能由其它cpu响应)            禁止          允许

4.  handle_edge_irq

该函数用于处理边沿触发中断的流控操作。边沿触发中断的特点是,只有设备的中断请求引脚(中断线)的电平发生跳变时(由高变低或者有低变高),才会发出中断请求,因为跳变是一瞬间,而且不会像电平中断能保持住电平,所以处理不当就特别容易漏掉一次中断请求,为了避免这种情况,屏蔽中断的时间必须越短越好。内核的开发者们显然意识到这一点,在正是处理中断前,判断IRQ_PROGRESS标志没有被设置的情况下,只是ack irq,并没有mask irq,以便复位设备的中断请求引脚,在这之后的中断处理期间,另外的cpu可以再次响应同一个irq请求,如果IRQ_PROGRESS已经置位,表明另一个CPU正在处理该irq的上一次请求,这种情况下,他只是简单地设置IRQS_PENDING标志,然后mask_ack_irq后退出,中断请求交由原来的CPU继续处理。因为是mask_ack_irq,所以系统实际上只允许挂起一次中断。

  1. if (unlikely(irqd_irq_disabled(&desc->irq_data) ||  

  2.          irqd_irq_inprogress(&desc->irq_data) || !desc->action)) {  

  3.     if (!irq_check_poll(desc)) {  

  4.         desc->istate |= IRQS_PENDING;  

  5.         mask_ack_irq(desc);  

  6.         goto out_unlock;  

  7.     }  

  8. }  


  9. desc->irq_data.chip->irq_ack(&desc->irq_data);  

从上面的分析可以知道,处理中断期间,另一次请求可能由另一个cpu响应后挂起,所以在处理完本次请求后还要判断IRQS_PENDING标志,如果被置位,当前cpu要接着处理被另一个cpu“委托”的请求。内核在这里设置了一个循环来处理这种情况,直到IRQS_PENDING标志无效为止,而且因为另一个cpu在响应并挂起irq时,会mask irq,所以在循环中要再次unmask irq,以便另一个cpu可以再次响应并挂起irq:

  1. do {  

  2.                ......  

  3.     if (unlikely(desc->istate & IRQS_PENDING)) {  

  4.         if (!irqd_irq_disabled(&desc->irq_data) &&  

  5.             irqd_irq_masked(&desc->irq_data))  

  6.             unmask_irq(desc);  

  7.     }  


  8.     handle_irq_event(desc);  


  9. while ((desc->istate & IRQS_PENDING) &&  

  10.      !irqd_irq_disabled(&desc->irq_data));  

IRQS_PENDING标志会在handle_irq_event中清除。

                                 图4.1   边沿触发中断状态

上图中颜色分别代表不同的状态:

状态        红色        绿色
IRQ_PROGRESS        TRUE        FALSE
是否允许本地cpu中断        禁止        允许
是否允许该设备再次触发中断(可能由其它cpu响应)        禁止        允许
是否处于中断上下文    处于中断上下文    处于进程上下文

由图4.1也可以看出,在处理软件中断(softirq)期间,此时仍然处于中断上下文中,但是cpu的本地中断是处于打开状态的,这表明此时嵌套中断允许发生,不过这不要紧,因为重要的处理已经完成,被嵌套的也只是软件中断部分而已。这个也就是内核区分top和bottom两个部分的初衷吧。

5.  handle_fasteoi_irq

现代的中断控制器通常会在硬件上实现了中断流控功能,例如ARM体系中的GIC通用中断控制器。对于这种中断控制器,CPU只需要在每次处理完中断后发出一个end of interrupt(eoi),我们无需关注何时mask,何时unmask。不过虽然想着很完美,事情总有特殊的时候,所以内核还是给了我们插手的机会,它利用irq_desc结构中的preflow_handler字段,在正式处理中断前会通过preflow_handler函数调用该回调。

  1. void  

  2. handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)  

  3. {  

  4.     raw_spin_lock(&desc->lock);  


  5.     if (unlikely(irqd_irq_inprogress(&desc->irq_data)))  

  6.         if (!irq_check_poll(desc))  

  7.             goto out;  

  8.         ......  

  9.     if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {  

  10.         desc->istate |= IRQS_PENDING;  

  11.         mask_irq(desc);  

  12.         goto out;  

  13.     }  


  14.     if (desc->istate & IRQS_ONESHOT)  

  15.         mask_irq(desc);  


  16.     preflow_handler(desc);  

  17.     handle_irq_event(desc);  


  18. out_eoi:  

  19.     desc->irq_data.chip->irq_eoi(&desc->irq_data);  

  20. out_unlock:  

  21.     raw_spin_unlock(&desc->lock);  

  22.     return;  

  23.         ......  

  24. }  

此外,内核还提供了另外一个eoi版的函数:handle_edge_eoi_irq,它的处理类似于handle_edge_irq,只是无需实现mask和unmask的逻辑。

6.  handle_percpu_irq

该函数用于smp系统,当某个irq只在一个cpu上处理时,我们可以无需用自旋锁对数据进行保护,也无需处理cpu之间的中断嵌套重入,所以函数很简单:

  1. void  

  2. handle_percpu_irq(unsigned int irq, struct irq_desc *desc)  

  3. {  

  4.     struct irq_chip *chip = irq_desc_get_chip(desc);  


  5.     kstat_incr_irqs_this_cpu(irq, desc);  


  6.     if (chip->irq_ack)  

  7.         chip->irq_ack(&desc->irq_data);  


  8.     handle_irq_event_percpu(desc, desc->action);  


  9.     if (chip->irq_eoi)  

  10.         chip->irq_eoi(&desc->irq_data);  

  11. }  

7.  handle_nested_irq

该函数用于实现其中一种中断共享机制,当多个中断共享某一根中断线时,我们可以把这个中断线作为父中断,共享该中断的各个设备作为子中断,在父中断的中断线程中决定和分发响应哪个设备的请求,在得出真正发出请求的子设备后,调用handle_nested_irq来响应中断。所以,该函数是在进程上下文执行的,我们也无需扫描和执行irq_desc结构中的action链表。父中断在初始化时必须通过irq_set_nested_thread函数明确告知中断子系统:这些子中断属于线程嵌套中断类型,这样驱动程序在申请这些子中断时,内核不会为它们建立自己的中断线程,所有的子中断共享父中断的中断线程。

    1. void handle_nested_irq(unsigned int irq)  

    2. {  

    3.     ......  

    4.         might_sleep();  


    5.     raw_spin_lock_irq(&desc->lock);  

    6.         ......  

    7.     action = desc->action;  

    8.     if (unlikely(!action || irqd_irq_disabled(&desc->irq_data)))  

    9.         goto out_unlock;  


    10.     irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);  

    11.     raw_spin_unlock_irq(&desc->lock);  


    12.     action_ret = action->thread_fn(action->irq, action->dev_id);  


    13.     raw_spin_lock_irq(&desc->lock);  

    14.     irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);  


    15. out_unlock:  

    16.     raw_spin_unlock_irq(&desc->lock);  

Linux中断(interrupt)子系统之四:驱动程序接口层 & 中断通用逻辑层

在本系列文章的第一篇:Linux中断(interrupt)子系统之一:中断系统基本原理,我把通用中断子系统分为了4个层次,其中的驱动程序接口层和中断通用逻辑层的界限实际上不是很明确,因为中断通用逻辑层的很多接口,既可以被驱动程序使用,也可以被硬件封装层使用,所以我把这两部分的内容放在一起进行讨论。

本章我将会讨论这两层对外提供的标准接口和内部实现机制,几乎所有的接口都是围绕着irq_desc和irq_chip这两个结构体进行的,对这两个结构体不熟悉的读者可以现读一下前面几篇文章。

/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  irq的打开和关闭

中断子系统为我们提供了一系列用于irq的打开和关闭的函数接口,其中最基本的一对是:

  • disable_irq(unsigned int irq);

  • enable_irq(unsigned int irq);

这两个API应该配对使用,disable_irq可以被多次嵌套调用,要想重新打开irq,enable_irq必须也要被调用同样的次数,为此,irq_desc结构中的depth字段专门用于这两个API嵌套深度的管理。当某个irq首次被驱动程序申请时,默认情况下,设置depth的初始值是0,对应的irq处于打开状态。我们看看disable_irq的调用过程:

                                                                             图1.1  disable_irq的调用过程

函数的开始使用异步方式的内部函数__disable_irq_nosync(),所谓异步方式就是不理会当前该irq是否正在被处理(有handler在运行或者有中断线程尚未结束)。有些中断控制器可能挂在某个慢速的总线上,所以在进一步处理前,先通过irq_get_desc_buslock获得总线锁(最终会调用chip->irq_bus_lock),然后进入内部函数__disable_irq:

  1. void __disable_irq(struct irq_desc *desc, unsigned int irq, bool suspend)  

  2. {  

  3.     if (suspend) {  

  4.         if (!desc->action || (desc->action->flags & IRQF_NO_SUSPEND))  

  5.             return;  

  6.         desc->istate |= IRQS_SUSPENDED;  

  7.     }  


  8.     if (!desc->depth++)  

  9.         irq_disable(desc);  

  10. }  

前面几句是对suspend的处理,最后两句,只有之前的depth为0,才会通过irq_disable函数,调用中断控制器的回调chip->irq_mask,否则只是简单地把depth的值加1。irq_disable函数还会通过irq_state_set_disabled和irq_state_set_masked,设置irq_data.flag的IRQD_IRQ_DISABLED和IRQD_IRQ_MASK标志。

disable_irq的最后,调用了synchronize_irq,该函数通过IRQ_INPROGRESS标志,确保action链表中所有的handler都已经处理完毕,然后还要通过wait_event等待该irq所有的中断线程退出。正因为这样,在中断上下文中,不应该使用该API来关闭irq,同时要确保调用该API的函数不能拥有该irq处理函数或线程的资源,否则就会发生死锁!!如果一定要在这两种情况下关闭irq,中断子系统为我们提供了另外一个API,它不会做出任何等待动作:

  • disable_irq_nosync();

中断子系统打开irq的的API是:

  • enable_irq();

打开irq无需提供同步的版本,因为irq打开前,没有handler和线程在运行,我们关注一下他对depth的处理,他在内部函数__enable_irq中处理:

  1. void __enable_irq(struct irq_desc *desc, unsigned int irq, bool resume)  

  2. {  

  3.     if (resume) {  

  4.             ......  

  5.     }  


  6.     switch (desc->depth) {  

  7.     case 0:  

  8.  err_out:  

  9.         WARN(1, KERN_WARNING "Unbalanced enable for IRQ %d\n", irq);  

  10.         break;  

  11.     case 1: {  

  12.                 ......  

  13.         irq_enable(desc);  

  14.                 ......  

  15.     }  

  16.     default:  

  17.         desc->depth--;  

  18.     }  

  19. }  

当depth的值为1时,才真正地调用irq_enable(),它最终通过chip->unmask或chip->enable回调开启中断控制器中相应的中断线,如果depth不是1,只是简单地减去1。如果已经是0,驱动还要调用enable_irq,说明驱动程序处理不当,造成enable与disable不平衡,内核会打印一句警告信息:Unbalanced enable for IRQ xxx。

2.  中断子系统内部数据结构访问接口

我们知道,中断子系统内部定义了几个重要的数据结构,例如:irq_desc,irq_chip,irq_data等等,这些数据结构的各个字段控制或影响着中断子系统和各个irq的行为和实现方式。通常,驱动程序不应该直接访问这些数据结构,直接访问会破会中断子系统的封装性,为此,中断子系统为我们提供了一系列的访问接口函数,用于访问这些数据结构。

存取irq_data结构相关字段的API:

        irq_set_chip(irq, *chip) / irq_get_chip(irq)  通过irq编号,设置、获取irq_cip结构指针;

        irq_set_handler_data(irq, *data) / irq_get_handler_data(irq)  通过irq编号,设置、获取irq_desc.irq_data.handler_data字段,该字段是每个irq的私有数据,通常用于硬件封装层,例如中断控制器级联时,父irq用该字段保存子irq的起始编号。

        irq_set_chip_data(irq, *data) / irq_get_chip_data(irq)  通过irq编号,设置、获取irq_desc.irq_data.chip_data字段,该字段是每个中断控制器的私有数据,通常用于硬件封装层。

        irq_set_irq_type(irq, type)  用于设置中断的电气类型,可选的类型有:

  • IRQ_TYPE_EDGE_RISING

  • IRQ_TYPE_EDGE_FALLING

  • IRQ_TYPE_EDGE_BOTH

  • IRQ_TYPE_LEVEL_HIGH

  • IRQ_TYPE_LEVEL_LOW

        irq_get_irq_data(irq)  通过irq编号,获取irq_data结构指针;

        irq_data_get_irq_chip(irq_data *d)  通过irq_data指针,获取irq_chip字段;

        irq_data_get_irq_chip_data(irq_data *d)  通过irq_data指针,获取chip_data字段;

        irq_data_get_irq_handler_data(irq_data *d)  通过irq_data指针,获取handler_data字段;

设置中断流控处理回调API:

        irq_set_handler(irq, handle)  设置中断流控回调字段:irq_desc.handle_irq,参数handle的类型是irq_flow_handler_t。

        irq_set_chip_and_handler(irq, *chip, handle)  同时设置中断流控回调字段和irq_chip指针:irq_desc.handle_irq和irq_desc.irq_data.chip。

        irq_set_chip_and_handler_name(irq, *chip, handle, *name)  同时设置中断流控回调字段和irq_chip指针以及irq名字:irq_desc.handle_irq、irq_desc.irq_data.chip、irq_desc.name。

        irq_set_chained_handler(irq, *chip, handle)  设置中断流控回调字段:irq_desc.handle_irq,同时设置标志:IRQ_NOREQUEST、IRQ_NOPROBE、IRQ_NOTHREAD,该api通常用于中断控制器的级联,父控制器通过该api设置流控回调后,同时设置上述三个标志位,使得父控制器的中断线不允许被驱动程序申请。

3.  在驱动程序中申请中断

系统启动阶段,中断子系统完成了必要的初始化工作,为驱动程序申请中断服务做好了准备,通常,我们用一下API申请中断服务:

  1. request_threaded_irq(unsigned int irq, irq_handler_t handler,  

  2.              irq_handler_t thread_fn,  

  3.              unsigned long flags, const char *name, void *dev);  

        irq  需要申请的irq编号,对于ARM体系,irq编号通常在平台级的代码中事先定义好,有时候也可以动态申请。

        handler  中断服务回调函数,该回调运行在中断上下文中,并且cpu的本地中断处于关闭状态,所以该回调函数应该只是执行需要快速响应的操作,执行时间应该尽可能短小,耗时的工作最好留给下面的thread_fn回调处理。

        thread_fn  如果该参数不为NULL,内核会为该irq创建一个内核线程,当中断发生时,如果handler回调返回值是IRQ_WAKE_THREAD,内核将会激活中断线程,在中断线程中,该回调函数将被调用,所以,该回调函数运行在进程上下文中,允许进行阻塞操作。

        flags  控制中断行为的位标志,IRQF_XXXX,例如:IRQF_TRIGGER_RISING,IRQF_TRIGGER_LOW,IRQF_SHARED等,在include/linux/interrupt.h中定义。

        name  申请本中断服务的设备名称,同时也作为中断线程的名称,该名称可以在/proc/interrupts文件中显示。

        dev  当多个设备的中断线共享同一个irq时,它会作为handler的参数,用于区分不同的设备。

下面我们分析一下request_threaded_irq的工作流程。函数先是根据irq编号取出对应的irq_desc实例的指针,然后分配了一个irqaction结构,用参数handler,thread_fn,irqflags,devname,dev_id初始化irqaction结构的各字段,同时做了一些必要的条件判断:该irq是否禁止申请?handler和thread_fn不允许同时为NULL,最后把大部分工作委托给__setup_irq函数:

  1. desc = irq_to_desc(irq);  

  2. if (!desc)  

  3.     return -EINVAL;  


  4. if (!irq_settings_can_request(desc) ||  

  5.     WARN_ON(irq_settings_is_per_cpu_devid(desc)))  

  6.     return -EINVAL;  


  7. if (!handler) {  

  8.     if (!thread_fn)  

  9.         return -EINVAL;  

  10.     handler = irq_default_primary_handler;  

  11. }  


  12. action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);  

  13. if (!action)  

  14.     return -ENOMEM;  


  15. action->handler = handler;  

  16. action->thread_fn = thread_fn;  

  17. action->flags = irqflags;  

  18. action->name = devname;  

  19. action->dev_id = dev_id;  


  20. chip_bus_lock(desc);  

  21. retval = __setup_irq(irq, desc, action);  

  22. chip_bus_sync_unlock(desc);  

进入__setup_irq函数,如果参数flag中设置了IRQF_SAMPLE_RANDOM标志,它会调用rand_initialize_irq,以便对随机数的生成产生影响。如果申请的不是一个线程嵌套中断(关于线程嵌套中断,请参阅Linux中断(interrupt)子系统之三:中断流控处理层中的handle_nested_irq一节),而且提供了thread_fn参数,它将创建一个内核线程:

  1. if (new->thread_fn && !nested) {  

  2.     struct task_struct *t;  


  3.     t = kthread_create(irq_thread, new, "irq/%d-%s", irq,  

  4.                new->name);  

  5.     if (IS_ERR(t)) {  

  6.         ret = PTR_ERR(t);  

  7.         goto out_mput;  

  8.     }  

  9.     /* 

  10.      * We keep the reference to the task struct even if 

  11.      * the thread dies to avoid that the interrupt code 

  12.      * references an already freed task_struct. 

  13.      */  

  14.     get_task_struct(t);  

  15.     new->thread = t;  

  16. }  

如果irq_desc结构中断action链表不为空,说明这个irq已经被其它设备申请过,也就是说,这是一个共享中断,所以接下来会判断这个新申请的中断与已经申请的旧中断的以下几个标志是否一致:

  • 一定要设置了IRQF_SHARED标志

  • 电气触发方式要完全一样(IRQF_TRIGGER_XXXX)

  • IRQF_PERCPU要一致

  • IRQF_ONESHOT要一致

检查这些条件都是因为多个设备试图共享一根中断线,试想一下,如果一个设备要求上升沿中断,一个设备要求电平中断,当中断到达时,内核将不知如何选择合适的流控操作。完成检查后,函数找出action链表中最后一个irqaction实例的指针。

  1. /* add new interrupt at end of irq queue */  

  2. do {  

  3.     thread_mask |= old->thread_mask;  

  4.     old_ptr = &old->next;  

  5.     old = *old_ptr;  

  6. while (old);  

  7. shared = 1;  

如果这不是一个共享中断,或者是共享中断的第一次申请,函数将初始化irq_desc结构中断线程等待结构:wait_for_threads,disable_irq函数会使用该字段等待所有irq线程的结束。接下来设置中断控制器的电气触发类型,然后处理一些必要的IRQF_XXXX标志位。如果没有设置IRQF_NOAUTOEN标志,则调用irq_startup()打开该irq,在irq_startup()函数中irq_desc中的enable_irq/disable_irq嵌套深度字段depth设置为0,代表该irq已经打开,如果在没有任何disable_irq被调用的情况下,enable_irq将会打印一个警告信息。

  1. if (irq_settings_can_autoenable(desc))  

  2.     irq_startup(desc);  

  3. else  

  4.     /* Undo nested disables: */  

  5.     desc->depth = 1;  

接着,设置cpu和irq的亲缘关系:

  1. /* Set default affinity mask once everything is setup */  

  2. setup_affinity(irq, desc, mask);  

然后,把新的irqaction实例链接到action链表的最后:

  1. new->irq = irq;  

  2. *old_ptr = new;  

最后,唤醒中断线程,注册相关的/proc文件节点:

  1. if (new->thread)  

  2.     wake_up_process(new->thread);  


  3. register_irq_proc(irq, desc);  

  4. new->dir = NULL;  

  5. register_handler_proc(irq, new);  

至此,irq的申请宣告完毕,当中断发生时,处理的路径将会沿着:irq_desc.handle_irq,irqaction.handler,irqaction.thread_fn(irqaction.handler的返回值是IRQ_WAKE_THREAD)这个过程进行处理。下图表明了某个irq被申请后,各个数据结构之间的关系:

                                                     图3.1  irq各个数据结构之间的关系

4.  动态扩展irq编号

在ARM体系的移动设备中,irq的编号通常在平台级或板级代码中事先根据硬件的连接定义好,最大的irq数目也用NR_IRQS常量指定。几种情况下,我们希望能够动态地增加系统中irq的数量:

  • 配置了CONFIG_SPARSE_IRQ内核配置项,使用基数树动态管理irq_desc结构。

  • 针对多功能复合设备,内部具备多个中断源,但中断触发引脚只有一个,为了实现驱动程序的跨平台,不希望这些中断源的irq被硬编码在板级代码中。

中断子系统为我们提供了以下几个api,用于动态申请/扩展irq编号:

        irq_alloc_desc(node)  申请一个irq,node是对应内存节点的编号;

        irq_alloc_desc_at(at, node)  在指定位置申请一个irq,如果指定位置已经被占用,则申请失败;

        irq_alloc_desc_from(from, node)  从指定位置开始搜索,申请一个irq;

        irq_alloc_descs(irq, from, cnt, node)  申请多个连续的irq编号,从from位置开始搜索;

        irq_free_descs(irq, cnt)  释放irq资源;

以上这些申请函数(宏),会为我们申请相应的irq_desc结构并初始化为默认状态,要想这些irq能够正常工作,我们还要使用第二节提到的api,对必要的字段进行设置,例如:

  • irq_set_chip_and_handler_name

  • irq_set_handler_data

  • irq_set_chip_data

对于没有配置CONFIG_SPARSE_IRQ内核配置项的内核,irq_desc是一个数组,根本不可能做到动态扩展,但是很多驱动又确实使用到了上述api,尤其是mfd驱动,这些驱动并没有我们一定要配置CONFIG_SPARSE_IRQ选项,要想不对这些驱动做出修改,你只能妥协一下,在你的板级代码中把NR_IRQS定义得大一些,留出足够的保留数量

5.  多功能复合设备的中断处理

在移动设备系统中,存在着大量的多功能复合设备,最常见的是一个芯片中,内部集成了多个功能部件,或者是一个模块单元内部集成了功能部件,这些内部功能部件可以各自产生中断请求,但是芯片或者硬件模块对外只有一个中断请求引脚,我们可以使用多种方式处理这些设备的中断请求,以下我们逐一讨论这些方法。

5.1  单一中断模式 

       对于这种复合设备,通常设备中会提供某种方式,以便让CPU获取真正的中断来源, 方式可以是一个内部寄存器,gpio的状态等等。单一中断模式是指驱动程序只申请一个irq,然后在中断处理程序中通过读取设备的内部寄存器,获取中断源,然后根据不同的中断源做出不同的处理,以下是一个简化后的代码:

  1. static int xxx_probe(device *dev)  

  2. {  

  3.     ......  

  4.     irq = get_irq_from_dev(dev);  


  5.     ret = request_threaded_irq(irq, NULL, xxx_irq_thread,  

  6.                    IRQF_TRIGGER_RISING,  

  7.                    "xxx_dev", NULL);  

  8.     ......  

  9.     return 0;  

  10. }  


  11. static irqreturn_t xxx_irq_thread(int irq, void *data)  

  12. {  

  13.     ......  

  14.     irq_src = read_device_irq();  

  15.     switch (irq_src) {  

  16.     case IRQ_SUB_DEV0:  

  17.         ret = handle_sub_dev0_irq();  

  18.         break;  

  19.     case IRQ_SUB_DEV1:  

  20.         ret = handle_sub_dev1_irq();  

  21.         break;  

  22.         ......  

  23.     default:  

  24.         ret = IRQ_NONE;  

  25.         break;  

  26.     }  

  27.     ......  

  28.     return ret;  

  29. }  

5.2  共享中断模式

共享中断模式充分利用了通用中断子系统的特性,经过前面的讨论,我们知道,irq对应的irq_desc结构中的action字段,本质上是一个链表,这给我们实现中断共享提供了必要的基础,只要我们以相同的irq编号多次申请中断服务,那么,action链表上就会有多个irqaction实例,当中断发生时,中断子系统会遍历action链表,逐个执行irqaction实例中的handler回调,根据handler回调的返回值不同,决定是否唤醒中断线程。需要注意到是,申请多个中断时,irq编号要保持一致,flag参数最好也能保持一致,并且都要设上IRQF_SHARED标志。在使用共享中断时,最好handler和thread_fn都要提供,在各自的中断处理回调handler中,做出以下处理:

  • 判断中断是否来自本设备;

  • 如果不是来自本设备:

    • 直接返回IRQ_NONE;

  • 如果是来自本设备:

    • 关闭irq;

    • 返回IRQ_WAKE_THREAD,唤醒中断线程,thread_fn将会被执行;

5.3  中断控制器级联模式

多数多功能复合设备内部提供了基本的中断控制器功能,例如可以单独地控制某个子中断的打开和关闭,并且可以方便地获得子中断源,对于这种设备,我们可以把设备内的中断控制器实现为一个子控制器,然后使用中断控制器级联模式。这种模式下,各个子设备拥有各自独立的irq编号,中断服务通过父中断进行分发。

对于父中断,具体的实现步骤如下:

  • 首先,父中断的irq编号可以从板级代码的预定义中获得,或者通过device的platform_data字段获得;

  • 使用父中断的irq编号,利用irq_set_chained_handler函数修改父中断的流控函数;

  • 使用父中断的irq编号,利用irq_set_handler_data设置流控函数的参数,该参数要能够用于判别子控制器的中断来源;

  • 实现父中断的流控函数,其中只需获得并计算子设备的irq编号,然后调用generic_handle_irq即可;

对于子设备,具体的实现步骤如下
  • 为设备内的中断控制器实现一个irq_chip结构,实现其中必要的回调,例如irq_mask,irq_unmask,irq_ack等;

  • 循环每一个子设备,做以下动作:

    • 为每个子设备,使用irq_alloc_descs函数申请irq编号;

    • 使用irq_set_chip_data设置必要的cookie数据;

    • 使用irq_set_chip_and_handler设置子控制器的irq_chip实例和子irq的流控处理程序,通常使用标准的流控函数,例如handle_edge_irq;

  • 子设备的驱动程序使用自身申请到的irq编号,按照正常流程申请中断服务即可。

5.4  中断线程嵌套模式

该模式与中断控制器级联模式大体相似,只不过级联模式时,父中断无需通过request_threaded_irq申请中断服务,而是直接更换了父中断的流控回调,在父中断的流控回调中实现子中断的二次分发。但是这在有些情况下会给我们带来不便,因为流控回调要获取子控制器的中断源,而流控回调运行在中断上下文中,对于那些子控制器需要通过慢速总线访问的设备,在中断上下文中访问显然不太合适,这时我们可以把子中断分发放在父中断的中断线程中进行,这就是我所说的所谓中断线程嵌套模式。下面是大概的实现过程:

对于父中断,具体的实现步骤如下:

  • 首先,父中断的irq编号可以从板级代码的预定义中获得,或者通过device的platform_data字段获得;

  • 使用父中断的irq编号,利用request_threaded_irq函数申请中断服务,需要提供thread_fn参数和dev_id参数;

  • dev_id参数要能够用于判别子控制器的中断来源;

  • 实现父中断的thread_fn函数,其中只需获得并计算子设备的irq编号,然后调用handle_nested_irq即可;

对于子设备,具体的实现步骤如下
  • 为设备内的中断控制器实现一个irq_chip结构,实现其中必要的回调,例如irq_mask,irq_unmask,irq_ack等;

  • 循环每一个子设备,做以下动作:

    • 为每个子设备,使用irq_alloc_descs函数申请irq编号;

    • 使用irq_set_chip_data设置必要的cookie数据;

    • 使用irq_set_chip_and_handler设置子控制器的irq_chip实例和子irq的流控处理程序,通常使用标准的流控函数,例如handle_edge_irq;

    • 使用irq_set_nested_thread函数,把子设备irq的线程嵌套特性打开;

  • 子设备的驱动程序使用自身申请到的irq编号,按照正常流程申请中断服务即可。

应为子设备irq的线程嵌套特性被打开,使用request_threaded_irq申请子设备的中断服务时,即是是提供了handler参数,中断子系统也不会使用它,同时也不会为它创建中断线程,子设备的thread_fn回调是在父中断的中断线程中,通过handle_nested_irq调用的,也就是说,尽管子中断有自己独立的irq编号,但是它们没有独立的中断线程,只是共享了父中断的中断服务线程。

Linux中断(interrupt)子系统之五:软件中断(softIRQ)

软件中断(softIRQ)是内核提供的一种延迟执行机制,它完全由软件触发,虽然说是延迟机制,实际上,在大多数情况下,它与普通进程相比,能得到更快的响应时间。软中断也是其他一些内核机制的基础,比如tasklet,高分辨率timer等。

/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  软件中断的数据结构

1.1  struct softirq_action

        内核用softirq_action结构管理软件中断的注册和激活等操作,它的定义如下:

  1. struct softirq_action  

  2. {  

  3.     void    (*action)(struct softirq_action *);  

  4. };  

非常简单,只有一个用于回调的函数指针。软件中断的资源是有限的,内核目前只实现了10种类型的软件中断,它们是:

  1. enum  

  2. {  

  3.     HI_SOFTIRQ=0,  

  4.     TIMER_SOFTIRQ,  

  5.     NET_TX_SOFTIRQ,  

  6.     NET_RX_SOFTIRQ,  

  7.     BLOCK_SOFTIRQ,  

  8.     BLOCK_IOPOLL_SOFTIRQ,  

  9.     TASKLET_SOFTIRQ,  

  10.     SCHED_SOFTIRQ,  

  11.     HRTIMER_SOFTIRQ,  

  12.     RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */  


  13.     NR_SOFTIRQS  

  14. };  

内核的开发者们不建议我们擅自增加软件中断的数量,如果需要新的软件中断,尽可能把它们实现为基于软件中断的tasklet形式。与上面的枚举值相对应,内核定义了一个softirq_action的结构数组,每种软中断对应数组中的一项:

  1. static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;  

 

1.2  irq_cpustat_t

        多个软中断可以同时在多个cpu运行,就算是同一种软中断,也有可能同时在多个cpu上运行。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t:

  1. typedef struct {  

  2.     unsigned int __softirq_pending;  

  3. } ____cacheline_aligned irq_cpustat_t;  

  1. irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;  

__softirq_pending字段中的每一个bit,对应着某一个软中断,某个bit被置位,说明有相应的软中断等待处理。

 

1.3  软中断的守护进程ksoftirqd

        在cpu的热插拔阶段,内核为每个cpu创建了一个用于执行软件中断的守护进程ksoftirqd,同时定义了一个per_cpu变量用于保存每个守护进程的task_struct结构指针:

  1. DEFINE_PER_CPU(struct task_struct *, ksoftirqd);  

大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。

 

2.  触发软中断

        要触发一个软中断,只要调用api:raise_softirq即可,它的实现很简单,先是关闭本地cpu中断,然后调用:raise_softirq_irqoff

  1. void raise_softirq(unsigned int nr)  

  2. {  

  3.     unsigned long flags;  


  4.     local_irq_save(flags);  

  5.     raise_softirq_irqoff(nr);  

  6.     local_irq_restore(flags);  

  7. }  

再看看raise_softirq_irqoff:

  1. inline void raise_softirq_irqoff(unsigned int nr)  

  2. {  

  3.     __raise_softirq_irqoff(nr);  


  4.         ......  

  5.     if (!in_interrupt())  

  6.         wakeup_softirqd();  

  7. }  

先是通过__raise_softirq_irqoff设置cpu的软中断pending标志位(irq_stat[NR_CPUS] ),然后通过in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,如果都不成立,则唤醒软中断的守护进程,在守护进程中执行软中断的回调函数。否则什么也不做,软中断将会在中断的退出阶段被执行。

 

3.  软中断的执行

        基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。

 

3.1  在irq_exit中执行

        看看irq_exit的部分:

  1. void irq_exit(void)  

  2. {  

  3.         ......  

  4.     sub_preempt_count(IRQ_EXIT_OFFSET);  

  5.     if (!in_interrupt() && local_softirq_pending())  

  6.         invoke_softirq();  

  7.         ......  

  8. }  

如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq:

  1. asmlinkage void __do_softirq(void)  

  2. {  

  3.         ......  

  4.     pending = local_softirq_pending();  


  5.     __local_bh_disable((unsigned long)__builtin_return_address(0),  

  6.                 SOFTIRQ_OFFSET);  

  7. restart:  

  8.     /* Reset the pending bitmask before enabling irqs */  

  9.     set_softirq_pending(0);  


  10.     local_irq_enable();  


  11.     h = softirq_vec;  


  12.     do {  

  13.         if (pending & 1) {  

  14.                     ......  

  15.             trace_softirq_entry(vec_nr);  

  16.             h->action(h);  

  17.             trace_softirq_exit(vec_nr);  

  18.                         ......  

  19.         }  

  20.         h++;  

  21.         pending >>= 1;  

  22.     } while (pending);  


  23.     local_irq_disable();  


  24.     pending = local_softirq_pending();  

  25.     if (pending && --max_restart)  

  26.         goto restart;  


  27.     if (pending)  

  28.         wakeup_softirqd();  


  29.     lockdep_softirq_exit();  


  30.     __local_bh_enable(SOFTIRQ_OFFSET);  

  31. }  

  • 首先取出pending的状态;

  • 禁止软中断,主要是为了防止和软中断守护进程发生竞争;

  • 清除所有的软中断待决标志;

  • 打开本地cpu中断;

  • 循环执行待决软中断的回调函数;

  • 如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出:

    • 没有新的软中断等待执行;

    • 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次;

  • 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断;

  • 推出前恢复软中断;

 

3.2  在ksoftirqd进程中执行

        从前面几节的讨论我们可以看出,软中断也可能由ksoftirqd守护进程执行,这要发生在以下两种情况下:

  • 在irq_exit中执行软中断,但是在经过MAX_SOFTIRQ_RESTART次循环后,软中断还未处理完,这种情况虽然极少发生,但毕竟有可能;

  • 内核的其它代码主动调用raise_softirq,而这时正好不是在中断上下文中,守护进程将被唤醒;

守护进程最终也会调用__do_softirq执行软中断的回调,具体的代码位于run_ksoftirqd函数中,内核会关闭抢占的情况下执行__do_softirq,具体的过程这里不做讨论。

 

4.  tasklet

       因为内核已经定义好了10种软中断类型,并且不建议我们自行添加额外的软中断,所以对软中断的实现方式,我们主要是做一个简单的了解,对于驱动程序的开发者来说,无需实现自己的软中断。但是,对于某些情况下,我们不希望一些操作直接在中断的handler中执行,但是又希望在稍后的时间里得到快速地处理,这就需要使用tasklet机制。 tasklet是建立在软中断上的一种延迟执行机制,它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断类型。

 

4.1  tasklet_struct        

在软中断的初始化函数softirq_init的最后,内核注册了TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断:

  1. void __init softirq_init(void)  

  2. {  

  3.         ......  

  4.     open_softirq(TASKLET_SOFTIRQ, tasklet_action);  

  5.     open_softirq(HI_SOFTIRQ, tasklet_hi_action);  

  6. }  

        内核用一个tasklet_struct来表示一个tasklet,它的定义如下:

  1. struct tasklet_struct  

  2. {  

  3.     struct tasklet_struct *next;  

  4.     unsigned long state;  

  5.     atomic_t count;  

  6.     void (*func)(unsigned long);  

  7.     unsigned long data;  

  8. };  

next用于把同一个cpu的tasklet链接成一个链表,state用于表示该tasklet的当前状态,目前只是用了最低的两个bit,分别用于表示已经准备被调度执行和已经在另一个cpu上执行:

  1. enum  

  2. {  

  3.     TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */  

  4.     TASKLET_STATE_RUN   /* Tasklet is running (SMP only) */  

  5. };  

原子变量count用于tasklet对tasklet_disable和tasklet_enable的计数,count为0时表示允许tasklet执行,否则不允许执行,每次tasklet_disable时,该值加1,tasklet_enable时该值减1。func是tasklet被执行时的回调函数指针,data则用作回调函数func的参数。

 

4.2  初始化一个tasklet

有两种办法初始化一个tasklet,第一种是静态初始化,使用以下两个宏,这两个宏定义一个tasklet_struct结构,并用相应的参数对结构中的字段进行初始化:

  • DECLARE_TASKLET(name, func, data);定义名字为name的tasklet,默认为enable状态,也就是count字段等于0。

  • DECLARE_TASKLET_DISABLED(name, func, data);定义名字为name的tasklet,默认为enable状态,也就是count字段等于1。

第二个是动态初始化方法:先定义一个tasklet_struct,然后用tasklet_init函数进行初始化,该方法默认tasklet处于enable状态:
  1. struct tasklet_struct tasklet_xxx;  

  2. ......  

  3. tasklet_init(&tasklet_xxx, func, data);  

 

4.3  tasklet的使用方法

使能和禁止tasklet,使用以下函数:

  • tasklet_disable()  通过给count字段加1来禁止一个tasklet,如果tasklet正在运行中,则等待运行完毕才返回(通过TASKLET_STATE_RUN标志)。

  • tasklet_disable_nosync()  tasklet_disable的异步版本,它不会等待tasklet运行完毕。

  • tasklet_enable()  使能tasklet,只是简单地给count字段减1。

调度tasklet的执行,使用以下函数:
  • tasklet_schedule(struct tasklet_struct *t)  如果TASKLET_STATE_SCHED标志为0,则置位TASKLET_STATE_SCHED,然后把tasklet挂到该cpu等待执行的tasklet链表上,接着发出TASKLET_SOFTIRQ软件中断请求。

  • tasklet_hi_schedule(struct tasklet_struct *t)  效果同上,区别是它发出的是HI_SOFTIRQ软件中断请求。

销毁tasklet,使用以下函数:
  • tasklet_kill(struct tasklet_struct *t)  如果tasklet处于TASKLET_STATE_SCHED状态,或者tasklet正在执行,则会等待tasklet执行完毕,然后清除TASKLET_STATE_SCHED状态。

 

4.4  tasklet的内部执行机制

内核为每个cpu用定义了一个tasklet_head结构,用于管理每个cpu上的tasklet的调度和执行:

  1. struct tasklet_head  

  2. {  

  3.     struct tasklet_struct *head;  

  4.     struct tasklet_struct **tail;  

  5. };  


  6. static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);  

  7. static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);  

回到4.1节,我们知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现的,两个软中断只是有优先级的差别,所以我们只讨论TASKLET_SOFTIRQ的实现,TASKLET_SOFTIRQ的中断回调函数是tasklet_action,我们看看它的代码:

  1. static void tasklet_action(struct softirq_action *a)  

  2. {  

  3.     struct tasklet_struct *list;  


  4.     local_irq_disable();  

  5.     list = __this_cpu_read(tasklet_vec.head);  

  6.     __this_cpu_write(tasklet_vec.head, NULL);  

  7.     __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);  

  8.     local_irq_enable();  


  9.     while (list) {  

  10.         struct tasklet_struct *t = list;  


  11.         list = list->next;  


  12.         if (tasklet_trylock(t)) {  

  13.             if (!atomic_read(&t->count)) {  

  14.                 if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))  

  15.                     BUG();  

  16.                 t->func(t->data);  

  17.                 tasklet_unlock(t);  

  18.                 continue;  

  19.             }  

  20.             tasklet_unlock(t);  

  21.         }  


  22.         local_irq_disable();  

  23.         t->next = NULL;  

  24.         *__this_cpu_read(tasklet_vec.tail) = t;  

  25.         __this_cpu_write(tasklet_vec.tail, &(t->next));  

  26.         __raise_softirq_irqoff(TASKLET_SOFTIRQ);  

  27.         local_irq_enable();  

  28.     }  

  29. }  

解析如下:

  • 关闭本地中断的前提下,移出当前cpu的待处理tasklet链表到一个临时链表后,清除当前cpu的tasklet链表,之所以这样处理,是为了处理当前tasklet链表的时候,允许新的tasklet被调度进待处理链表中。

  • 遍历临时链表,用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行,而且tasklet没有被禁止:

    • 如果没有运行,也没有禁止,则清除TASKLET_STATE_SCHED状态位,执行tasklet的回调函数。

    • 如果已经在运行,或者被禁止,则把该tasklet重新添加会当前cpu的待处理tasklet链表上,然后触发TASKLET_SOFTIRQ软中断,等待下一次软中断时再次执行。

分析到这了我有个疑问,看了上面的代码,如果一个tasklet被tasklet_schedule后,在没有被执行前被tasklet_disable了,岂不是会无穷无尽地引发TASKLET_SOFTIRQ软中断?

通过以上的分析,我们需要注意的是,tasklet有以下几个特征:

  • 同一个tasklet只能同时在一个cpu上执行,但不同的tasklet可以同时在不同的cpu上执行;  

  • 一旦tasklet_schedule被调用,内核会保证tasklet一定会在某个cpu上执行一次;

  • 如果tasklet_schedule被调用时,tasklet不是出于正在执行状态,则它只会执行一次;

  • 如果tasklet_schedule被调用时,tasklet已经正在执行,则它会在稍后被调度再次被执行;

  • 两个tasklet之间如果有资源冲突,应该要用自旋锁进行同步保护;

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多