Linux中断和中断上半部处理

        操作系统的任务之一就是管理外设,如鼠标、键盘、硬盘等等,想要管理这些设备就必须与它们进行通信,存储器可以直接挂在总线上,外设是通过串行或者并行接口与总线连接(USB就是一个串行的接口)。但是CPU与这些外设进行通信的时候,存在一个问题,就是cpu和外设之间的信息处理速度相差很大。如果让cpu向外设发出请求,然后一直等待硬件回应,显然是非常浪费资源的。所以由处理速度相对慢的外设硬件发出处理请求,由处理速度相对快的CPU接受处理请求并处理问题,就可以让CPU尽可能的在工作;外设没有处理请求时,cpu可以处理其他事务。

1.中断

        中断机制就是变内核主动为硬件主动,由硬件发出通知给处理器。比如在敲击键盘的时候,键盘会发出一个中断给处理器,通知操作系统有按键。中断是一种特殊的电信号,硬件设备发给处理器,处理器接收到中断后,会马上向操作系统通知此中断信号的到来,然后操作系统负责处理这些新到来的数据。硬件产生中断的时候并不考虑与处理器的时钟同步,即中断可以随时产生,来打断内核。

        中断是一种电信号,硬件设备生成该信号后,直接送入中断控制器的输入引脚中(中断控制器是一种简单的电子芯片,其作用是将多路中断管线采用复用技术只通过一个和处理器相连接的管线与处理器通信)。当接收到一个中断后,中断控制器会给处理器发送一个电信号,处理器经检测到该信号,便中断自己的当前工作转而处理中断。此后,处理器通知操作系统已经产生中断,操作系统就可以对这个中断进行适当的处理了。

        不同设备对应的中断不同,每个中断都通过一个唯一的数据来标识。因此,来自于硬盘的中断和来自键盘的中断是不同的,从而操作系统能够对中断进行区分。并知道是那个硬件设备产生了那个中断。这样操作系统就可以给不同中断提供对应的中断处理程序。

        这些中断值通常被称为中断请求线IRQ。每个IRQ线都会被关联到一个数值量上,例如在PC机上,IRQ0是时钟中断,IRQ1是键盘中断,但是中断号并不是严格定义的,对于连接在PCI总线上的设备,中断是动态分配的。中断号是多少并不重要,重要的是要将特定的中断和特定的设备关联起来。

1.1 异常

        在操作系统中,提到中断就不得不提及异常。异常常常被称为同步中断,其原因是它在产生的时候必须考虑与处理器的时钟同步,而上面提过了中断的产生是不需要与处理器的时钟同步。当处理器执行到由于编程错误导致错误指令或者出现其他特殊情况的时候,必须靠内核来处理的时候,处理器就会产生一个异常。由于许多处理器处理异常的方式与处理中断的方式类似。所以,对中断的讨论大部分情况下也适合于异常。

1.2 中断处理程序

        在响应一个特定的中断的时候,内核会执行一个函数,该函数叫做中断处理程序 interrupt handle或者中断服务例程Interrupt Service Routine。产生中断的每个设备都有一个相应的中断程序(并不是指中断处理程序与特定设备关联,而是和特定中断相关联,如果一个设备可以产生多种不同的中断,那自然也需要多种不同的中断处理程序)。一个设备的中断处理程序是它设备驱动程序的一部分。

        在Linux内核中,中断处理程序必须按照特定的类型进行声明,以便内核能够以标准的方式传递处理程序的信息,在其他方面它与普通程序一样。中断处理程序是被内核调用来响应中断的,它运行于中断上下文中。

        中断随时可能发生,因此,中断处理程序也随时可能执行。所以必须保证中断处理程序能够快速执行,这样才能保证尽可能快的恢复被中断代码的执行。因此,操作系统能够迅速对中断进行服务很重要,对系统其他部分,让中断处理程序尽可能快的执行完成也很重要。

        最起码,中断处理程序要通知硬件设备中断已被接收,从而令硬件设备恢复工作,但是中断处理程序往往需要完成其他工作,例如,网络设备接收来自网络的数据包后,要立即对OLT进行应答,并且将数据包存入内存中,对其进行处理后再交给协议栈。这种工作量并不小,尤其是随着10GPON 50GPON的出现。

1.3 上半部和下半部

        我们既需要中断处理程序速度运行,又想中断处理程序完成的工作量足够多,这两个目的明显是有所矛盾的。所以,通常人们把中断处理程序切分为两半。中断处理程序是上半部,接收到一个中断,它就立即开始工作,但只做有严格时限的工作,例如,对接收的中断进行应答或者复位硬件(在所有中断被禁止的情况下完成)。能够被允许稍后完成的工作会放到下半部,此后,在合适的时候,下半部会被执行。

        继续以网卡为例,当接收到来自网络的数据包后,需要通知内核数据包到了。网卡需要立即完成这件事,从而优化网卡的吞吐量和传输周期。因此,硬件立即通知软件,而软件通过已注册的中断处理程序来做出应答。

        中断开始执行,通知硬件拷贝网络数据包到内存,然后让网卡接收更多的数据包,这些工作是重要而又紧急的。内核必须要快速的拷贝数据包到内存中,因为网卡中接收数据包的缓存大小有限,而且比系统内存要小的多,如果数据包没有及时从缓存中拷贝到内存中,那么当数据包占满缓存后,后续来的数据包就会被丢弃。

        当数据包被拷贝到内存后,中断的任务算是完成了,这是控制权被交还给中断前运行的程序,处理网络数据包的其他工作将在下半部中进行。

1.4 注册中断处理程序

        中断处理程序是管理硬件的驱动程序的组成部分,每一个设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序中就需要注册一个中断处理程序(大部分设备如此)

static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char* name, void* dev);

驱动程序可以通过request_irq函数注册一个中断处理程序,其声明在文件interrupt.h中,并且激活中断线以处理中断。接下来解析这个函数代表的意义。

第一个参数irq表示要分配的中断号,传统设备上通常是预设的,大多数其他设备是可以通过探测获取或者编程动态确定。

第二个参数是一个指针,指向实际处理这个中断的中断处理程序,只要系统一接收到中断就调用该函数。

typedef irqreturn_t (*irq_handle_t) (int, void*);

这句代码是使用typedef对函数指针起了一个别名,具体关键字typedef使用见附录。

第三个参数flags可以是0,也可以是一个或者多个标志的位掩码,定义在interrupt.h

第四个参数name是与中断相关设备的ASCII文本表示,以便于与用户通信

第五个参数dev用与共享中断线,当一个中断处理程序需要释放的时候,dev将提供唯一的标志信息cookie,以便于共享中断线的诸多中断处理程序中删除指定的哪一个。如果无需共享中断,则该值为NULL,若果是共享中断线,那么就必须传递唯一的信息。另外,内核在调用中断处理程序时,都会把这个参数传输给内核,将有用的环境信息传输给内核,这个指针必须唯一。

request_irq成功执行会返回0,如果返回非0值则表示有错误发生,这种情况下指定的中断处理程序不会被注册。最常见的是-EBUSY,表示该中断线已经被使用,或者当前用户没有指定IRQF_SHARED。

值得注意的是,request_irq是会睡眠的,因此不能在中断上下文或者其他不允许阻塞的代码中使用它。在睡眠不安全的上下文中调用该函数是一种常见的错误。造成该错误的原因部分是request_irq会引起堵塞。为什么?在注册过程中,内核需要在irq文件中创建一个与中断对应的项。函数porc_mkdir就是用来创建这个新的procs项的。它要调用proc_create对这个新项进行设置,而proc_create会调用kmalloc请求分配内存。

1.4.1 中断处理程序的标志

        第三个参数中断处理程序标志在linux/interrupt.h文件中有定义,下面对这些标志进行一些说明:

  1. IRQF_DISABLED,该标志被设置后,意味着内核在处理该中断处理程序期间,要禁止其他中断,如果不设置,中断处理程序可以与除本身外其他任何中断同时运行。多数中断不会设置这个。这种用法留给希望快速执行的轻量级的中断。是过去的中断标志SA_INTERRUPT的当前形式,在过去用于区分快速中断和慢速中断。
  2. IRQF_SAMPLE_RANDOM,此标志表示这个设备产生的中断对内核熵池有贡献。熵池是Linux系统产生真随机数给各种随机事件。最好是那种产生中断速率不可预知的硬件是较好的熵源。
  3. IRQF_TIMER,该标志是特别为系统定时器准备的。
  4. IRQF_SHARED,此标志表明可以在多个中断处理程序之间共享中断线,在同一个线上注册的每个处理程序都必须指定这个标志,否则每条线上只能有一个处理程序。
  5. IRQF_PROBE_SHARED,称为共享探测,允许和其他device共享一个中断线,但是实际上是否能够share还是需要其他条件
  6. IRQF_NOBALANCING,不想让你的中断参与到irq balancing的过程
  7. IRQF_ONESHOT,中断不会在硬件中断后重新启用,直到进程处理程序开始运行前都禁止。
  8. IRQF_NO_SUSPEND,不要在挂起期间禁用此 IRQ,可能会导致系统不能正常的。 不保证此中断会将系统从挂起状态唤醒。
  9. IRQF_FORCE_RESUME 即使设置了 IRQF_NO_SUSPEND 也强制启用它
  10. IRQF_NO_THREAD,中断不能被线程化
  11. IRQF_EARLY_RESUME,在syscore 期间提前恢复中断,而不是在设备恢复时
  12. IRQF_COND_SUSPEND,如果该中断与NO_SUSPEND的使用者共享,则在中断挂起结束后,执行此中断处理程序。 对于系统唤醒设备,用户需要在其中断处理程序中实现唤醒检测。

1.4.2 一个中断的例子

        在一个驱动程序中请求一个中断线,并通过上面的注册程序安装中断处理程序:

if( request_irq(irqn, irq_handle, IRQF_SHARED, “my_device”, my_dev))
{
    printk(KERN_ERR “my_device:cannot register IRQ %d\n”, irqn);
    return -EIO;
}

        在这个例子里,irqn是请求中断线:irq_handle是中断处理程序;设置中断标志为中断线可以共享;设备命名为 my_device;传递my_dev变量给第五个参数。如果请求失败,则这段代码将打印一个错误出来。如果返回0,则表示处理程序已经安装,当中断被响应后,处理程序就会执行。有一点很重要,初始化硬件必须要在注册中断处理程序之前,防止中断处理程序在初始化完成之前就执行。

1.5释放中断处理程序

卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。需要调用函数:

void free_irq(unsigned int irq, void *dev);

在4.1内核中,该函数调用__free_irq函数,根据*dev,搜索irq描述符中对应的action部分,找到后移除链表中的该实体,irq_pm_remove_action,调用irq_shutdown;irq_release_resources对中断线进行关闭,调用unregister_handler_proc,同步到另一个CPU,synchronize_irq(irq)。

        如果指定的中断线不是共享的,那么该函数删除处理程序的同时会禁用这条中断线。如果中断线是共享的,则仅删除dev对应的处理程序,而这条中断线在删除了最后一个处理程序后才会禁用。由此得知,唯一的dev非常重要,对于共享中断线,需要一个唯一的信息来区分上面的多个处理程序,并且只删除指定的处理程序。无论在共享或者不共享的情况,如果dev参数非空,它都必须与需要删除的处理程序相匹配,从进程上下文中执行free_irq.

1.6 进程上下文和中断上下文

在讲这两个概念之前,需要先说明一下用户空间和内核空间。对32位Linux操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。

Linux内核,独立于普通的用户程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

用户进程,通常是运行用户程序,创建在用户态下的进程,通常运行在用户空间,但是可以通过系统调用 异常 中断来陷入内核。这样的进程,调用库函数或者调用自己写的函数的时候,使用的地址均在用户空间中。LUNIX下,每个进程(用户进程)产生时,内核都会为其开辟一段虚拟内存,每一个进程的这段空间都是独立的,在32位系统中,这段空间有4G大小,0-3G为进程所用。内核将这段空间与二进制文件建立映射关系,并在程序运行时将其载入。

        内核进程,是执行操作系统程序的运行在内核态下的进程。只占有内核地址空间,通常用来完成特定的资源管理功能,如页面扫描进程。它们的”current->mm”都为空,其本身就是内核的一部分。它们从来不会进入用户态。

        可执行代码程序是进程中的重要组成部分。这些可执行代码从一个文件载入到进程的地址空间执行。一般程序在用户空间执行,这个程序执行了系统调用或者触发了异常,此时,程序的执行就陷入了内核空间(系统调用和异常相关的在内核空间)。此时称之为内核“代表进程执行”并处于进程上下文中。在内核退出的时候,程序恢复在用户空间执行。系统调用和异常处理程序是内核给出的明确的接口。进程只有通过这些接口才能陷入内核。

        进程上下文实际上是进程执行活动全过程的静态描述。已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为上文,把正在执行的指令和数据在寄存器和堆栈中的内容称为正文,把待执行的指令和数据在寄存器与堆栈中的内容称为下文。

        具体的说,进程上下文包括计算机系统中与执行该进程有关的各种寄存器(例如,通用寄存器,程序计数器PC,程序状态寄存器PS等)的值,代码在经过编译过后形成的机器指令代码集,数据集及各种堆栈值PCB结构。这里,有关寄存器和栈区的内容是重要的,例如没有程序计数器PC和程序 状态寄存器PS,CPU将无法知道下一条待执行指令的地址和控制有关操作。在这种情况下,可以通过current宏关联到当前进程,此外,进程是以进程上下文的形式链接到内核中,因此,进程上下文可以睡眠,也可以调用调度程序。

        中断上下文与进程没有什么瓜葛,与current宏也不相干(尽管会指向被中断的进程)。硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上下文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)。

        在LINUX中,当前进程上下文均保存在进程的任务task数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。

        通俗的来说,进程上下文其实就是说当前执行的任务是进程,当前跑的代码是为进程而跑,可能是进程的代码也可能是经常调用内核去为进程跑。而中断上下文是说 当前跑的代码是中断的代码,和当前进程没有关系。在进程上下文中,current这个宏指向的进程是有效的,可以挂起可以睡眠可以调度。而中断上下文中的current没什么用。上下文context: 上下文简单说来就是一个环境。中断上下文有较为严格的时间限制,因为它打断了其他的代码。中断上下文的代码应该迅速 简洁,尽量不使用循环去处理繁重的工作。中断处理程序曾经并不具有自己的栈,相反该程序共享其打断进程的内核栈。现在中断处理程序拥有了自己的栈。

所以总结上面几点,处理器总是处于三种状态中:

  1. 内核态,处于进程上下文中,内核代表进程运行于内核空间;
  2. 内核态,处于中断上下文中,内核代表硬件运行于内核空间;
  3. 用户态,进程运行于用户空间。

  

2.编写中断处理程序

static irqreturn_t intr_handle(int irq, void* dev);

        上面是一个4.1内核的中断处理程序的声明的方法。该函数返回类型与中断注册函数request_irq的第二个参数所要求的的参数类型相符。

        第一个参数为中断号irq,是这个处理程序要响应的中断的中断号,这个参数没有很大的作用,在打印日志信息时可能用到。在2.0内核以前,由于没有dev这个参数,所以需要irq才能区分,由于使用相同驱动程序,从而也使用相同中断处理程序的多个设备。

        第二个参数dev是一个通用指针,它必须与中断注册是的第五个参数dev相同。如果该值有唯一确定性,那么它就相当于一个小型的cookie(身份证明),可以用来区分共享同一个中断处理程序的多个设备。因为对于每个设备而言,设备结构都是唯一的,通常用它来做dev。

中断处理程序的返回值是一个特殊的类型irqreturn_t,该类型有三个枚举值:

IRQ_NONE:0 
IRQ_HANDLED:1 
IRQ_WAKE_THREAD:2

        当中断处理程序检测到一个中断,但是该中断对应的设备不是在注册处理函数期间指定的产生源时,返回IRQ_NONE。当中断处理程序被正确调用且确实是它所对应设备产生了中断,那么返回IRQ_HANDLED。另外也可以使用宏,如下

#define IRQ_RETVAL(x)    ((x) ? IRQ_HANDLED : IRQ_NONE)

        当给定的一个中断处理程序正在执行时,相应的中断线在所有的处理器上都会被屏蔽,以防止在同一个中断线上接受一个新的中断而产生的嵌套。通常情况下,其他中断是打开的,但当前中断线是屏蔽的。所以同一个中断处理程序不会被同时调用以处理嵌套的中断。

2.1 共享中断处理程序

共享的处理程序与非共享的处理程序在注册和运行的方式上比较相似,但差异主要有以下几处:

        1. 对于每个注册的中断处理程序而言,dev参数必须唯一,指向任一设备结构的指针就可以满足该需求。一般选择设备结构。共享的中断处理程序的该参数不能传递NULL,非共享的可以传递NULL。

        2. 中断处理程序必须能够区分他的设备是否真的产生的中断。这既需要硬件支持,也需要处理程序中有相关的处理逻辑。如果硬件不支持,那么中断处理程序就没办法确定到底是它对应的设备发出了这个中断还是共享这条中断线的其他设备发出了中断。

        所有共享中断线的驱动程序都必须满足上面的要求。只要有一个设备没有按规则进行共享,中断线就无法共享。在指定表示flags为IRQF_SHARED来调用request_irq时,只有以下两种情况可以成功:中断线当前未被注册,或者这条中断线上的所有已经注册的处理程序都指定了标志为IRQF_SHARED。

        内核接收到一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此,中断处理程序必须知道它是否应为这个中断负责,如果与它相关的设备并没有产生中断,那么处理程序应该立即退出。这需要硬件设备提供状态寄存器(或者类似机制),以便于中断处理程序进行检查。

RTC实例:

        实时时钟real-time clock是很多机器包括PC用来生成时间的设备。32768Hz的晶振经过15次分频产生1Hz来对时间进行计数。通常我们设置一个系统时钟,只需要往某个寄存器或者特殊的I/O地址写入就可以了,但是对于一个报警器或者周期性定时器通常就得靠中断实现。这种中断和闹铃差不多,中断发出后,报警器和定时器启动。4.1内核中,RTC驱动程序一般会在drivers/char/rtc.c.

RTC驱动程序装载时,rtc_init函数会被调用,对这个驱动进行初始化,初始化过程中就有中断处理程序的注册,如下:

if (request_irq(rtc_irq, rtc_interrupt, IRQF_SHARED,"rtc",(void *)&rtc_port)) 
{
    rtc_has_irq = 0;
    printk(KERN_ERR "rtc: cannot register IRQ %d\n", rtc_irq);
    return -EIO;
}

第一个参数是rtc的中断号rtc_irq,在PC上,RTC位于IRQ 8。

第二个参数是我们中断处理程序rtc_interrupt,它与其他中断处理程序共享了中断线。

第三个参数是IRQF_SHARED。

第四个参数可以看出驱动程序的名称叫做rtc。因为这个设备允许共享中断线,所以它给dev参数传递了一个面向rtc设备的实参值。

下来就是rtc处理程序,

static unsigned long rtc_irq_data;

static irqreturn_t rtc_interrupt(int irq, void *dev_id)

{
    /*
     *>-Can be an alarm interrupt, update complete interrupt,
     *>-or a periodic interrupt. We store the status in the
     *>-low byte and the number of interrupts received since
     *>-the last read in the remainder of rtc_irq_data.
     */

    spin_lock(&rtc_lock);
    rtc_irq_data += 0x100;
    rtc_irq_data &= ~0xff;
    if (is_hpet_enabled()) {

        /*
         * In this case it is HPET RTC interrupt handler
         * calling us, with the interrupt information
         * passed as arg1, instead of irq.
         */

        rtc_irq_data |= (unsigned long)irq & 0xF0;
    } else {

        rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) & 0xF0);

    }

    if (rtc_status & RTC_TIMER_ON)

        mod_timer(&rtc_irq_timer, jiffies + HZ/rtc_freq + 2*HZ/100);

    spin_unlock(&rtc_lock);

    /* Now do the rest of the actions */

    spin_lock(&rtc_task_lock);
    if (rtc_callback)
        rtc_callback->func(rtc_callback->private_data);

    spin_unlock(&rtc_task_lock);
    wake_up_interruptible(&rtc_wait);
    kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);

    return IRQ_HANDLED;

}

        只要计算机一接收到RTC中断,就执行这个程序,这里使用自旋锁是因为保证rtc_irq_data不被SMP机器上其他处理器同时访问,第二次调用避免rtc_callback出现相同情况。rtc_irq_data存放有关RTC的信息,每次中断都会更新来反映中断状态。接下来,如果设置了RTC周期性定时器,就通过函数mod_timer对其更新。最后一部分,会执行一个预先设置好的回调函数。最后函数返回IRQ_HANDLED。

3.中断机制的实现

1.设备产生中断,通过总线把电信号发送给中断控制器,如果中断线是激活的,那么中断控制器就把中断发往cpu。(在大多数体系结构中,这个工作就是通过电信号给处理器的一个特定管脚发送一个信号)除非处理器禁止了该中断,否则,CPU会立即停止它正在做的事情,关闭中断系统,然后跳入内存中预定义的位置开始执行(这个位置是由内核设置的,是中断处理程序的入口点)。

2.在内核中,中断的旅程开始于预定义入口点:对于每条中断线,处理器跳到该中断线对应的一个唯一位置(这样内核就知道接收到中断irq号)。入口点这边会保存这个irq号到栈里,并存放当前寄存器的值(被中断任务)。然后,内核调用do_IRQ函数,该函数声明如下:

unsigned int do_IRQ(struct pt_regs regs);

(pt_regs结构体成员是用来存放寄存器的值,orig_eax存放的是IRQ number)

3.因为C语言中调用函数时,要把函数参数很早的放入栈中(在调用函数返回地址之前,也在ebp寄存器存放的地址之前,此时寄存器里的相关值还没有改变,这时pt_regs结构体存放了中断号),因此,中断号可以保存下来,所以do_IRQ函数可以获取到中断号。

4.计算出中断号后,do_IRQ函数对所接收的中断应答,禁止这条中断线

5.do_IRQ需要确保这条中断线上有一个有效的处理程序,而且这个程序已经启动且没有执行。如果没有中断处理程序,执行ret_from_intr函数返回内核运行中断的代码;如果有,do_IRQ就调用handle_IRQ_event函数来运行该中断线上安装的中断处理程序。

4. /proc/interrupts

procfs是一个虚拟文件系统,它只存在于内核中,一般安装于/proc目录。在procfs系统中读写文件都要调用内核函数,这些函数模拟从真实文件中读或写。与中断相关的例子是/proc/interrupts文件,该文件存放的是系统中与中断相关的统计信息。下面是从单处理器PC中输出的信息:

                      CPU0

0:       23165465   XT-PIC  timer

1:         23132     XT-PIC  i8042

4:         45413     XT-PIC  uhci-hcd,eth0

第一列是中断线,这里没有显示没有安装中断处理程序的中断线,第二列是一个接收中断的计数器,每个处理器有一列,这里只有一列。第三列是处理这个中断的中断控制器。最后一列是中断相关的设备名字,这个名字是通过参数devname提供给函数request_irq的,如果该中断线是共享的,则这条中断线上所有设备都列出来(第4条)。

5.中断控制

Linux内核提供了一组用于操作机器上中断状态的接口,这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力。这些接口例程可以在<asm/system.h>和<asm/irq.h>中找到。

一般来说控制中断系统的原因归根结底是为了同步,通过禁止中断可以确保某个中断处理程序不会抢占当前运行代码,另外,还可以禁止内核抢占。但是,无论是禁止中断抢占还是禁止内核抢占,都没有提供保护机制来防止其他处理器的并发访问。但是,Linux支持多处理器,因此,内核代码一般都需要获取某种锁来防止来自其他处理器的并发访问。

锁提供的保护机制是防止来自其他处理器的并发访问;禁止中断的保护机制是防止来自其他中断处理程序的并发访问。

5.1禁止和激活中断

用于禁止当前处理器上的本地中断的语句:

local_irq_disable();

用于激活当前处理器上本地中断:

local_irq_disable();

这两个函数通常以单个汇编指令来实现,cli和sti,实际上这两个指令分别是对一个名为 允许中断(allow interrupt)的标志 进行clear和set的汇编调用。

如果在调用local_irq_disable之前就已经禁止了中断,那么该例程往往会带来潜在的危险。同样相应的local_irq_enable例程也会存在危险,因为它会无条件激活中断,即使一些中断在一开始是关闭(一些中断可能在disable之前就是关闭的),所以我们需要一个机制能把中断恢复到以前的状态而不是简单地禁止或激活。为什么需要这个机制?因为内核中给定的一些代码既可以在中断激活的情况下到达,也可以在中断禁止的情况下到达,这取决于具体的调用链。

例如:某一部分代码是一个大函数的组成部分,这个大函数被另外两个函数调用:其中一个函数是禁止中断的,另一个是不禁止中断的。随着内核的不断增加,要想知道达到这个函数的代码路径会变得越来越困难,因此,在禁止中断之前保存中断状态会安全一点。相反在解除上一次禁止中断时,只需要把它恢复到原来的状态。

unsigned long flags;

local_irq_save(flags);//保存当前中断状态到flags中,禁止当前处理器的中断发送

/*...............*/

local_irq_restore(flags);//将local_irq_save保存的flags状态值恢复

local_irq_save和local_irq_restore表面上是以flags为参数传递的,实际上是以宏的形式实现的。flags参数包含具体体系结构的数据,也就是包含中断系统的状态。存在体系结构体把栈信息和值相结合,因此不能把flags当初参数传递给另一个函数(特别是它必须驻留在同一个栈帧中),基于这个原因,这两个函数的使用在同一个函数中。

上面四个函数可以在中断中调用也可以在进程上下文中调用。

5.2禁止指定中断线

在前面的内容中,给出了很多禁止整个处理器上所有的中断的函数。在某些情况下,只禁止整个系统中的一条特定的中断线就可以了。这就是所谓的屏蔽掉一条中断线,为此Linux提供了一下四个接口:

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);

        前两个函数禁止中断控制器上指定的中断线,即禁止给定中断向系统中所有处理器传递。另外,函数只有在当前正在执行的所有处理程序完成后,disable_irq才能返回。因此使用者不仅要保证不在指定线上传递新的中断(如果有其他中断处理程序未处理完,该中断线又来中断,则该函数的返回就会拖延),还要确保所有已经开始的处理程序已全部退出(如果在中断处理程序中使用disable_irq屏蔽相应中断,系统将会出现死锁状态,最后死机,然后重启,不要把该函数用在中断处理程序中)。而disable_irq_nosync则不会等待中断处理程序执行完毕。synchronize_irq用于等待一个特定的PENDING状态的中断处理程序结束(中断处理包括硬中断的处理以及中断线程的处理)。如果该处理程序正在执行,那该函数必须等待该处理程序退出后才返回。

     对这写函数的调用可以嵌套,但是要记住在一条指定的中断线上,对disable_irq、disable_irq_nosync和synchronize_irq的每次调用都要相应的调用一次enable_irq,只有在完成对应的enable_irq的最后一次调用,该中断线才是真正的激活。例如,如果disable_irq调用了两次,那么直到第二次调用enable_irq后才能真正激活中断线。这三个函数可以从中断或者进程上下文中调用,而且不会睡眠。但如果从中断上下文中调用就要特别小心。

        禁止多个中断处理程序共享的中断线是不合适的,禁止中断线也就禁止了这条中断线上所有设备的中断传递,因此,用于新设备的驱动应该倾向于不使用这些接口(因为当代计算机中,几乎所有中断线都是可以共享的)。PCI设备必须支持中断线共享。所有这些函数在老式设备的驱动中更容易找到。

5.3中断系统的状态

        通常有必要了解中断系统的状态,比如,中断是禁止的还是激活的,或者当前是否处于中断上下文的执行状态中。宏irqs_disable定义在<asm/system.h>中。如果本地处理器的中断系统被禁止,则它返回非0,否则返回0.(4.1内核中没有)

在4.1内核<linux/preempt_mask.h>中提供了两个宏用来检查内核当前的状态,它们是:

in_interrupt();
in_softirq();
in_irq();

宏 in_interrupt检查(hard)irq和softirq计数器。无论是执行中断处理程序还是下半部处理程序,此函数都会返回非0。

宏in_softirq只看softirq计数器,只有当内核正在执行下半部处理程序返回非0.

宏in_irq只看(hard)irq计数器,只有当内核正在执行中断处理程序才返回非0.

通常情况下,我们可能会检查自己是否处于进程上下文中,我们要确保自己不在中断上下文中。(因为代码可能要有睡眠等只能在进程上下文中做的事情)。

6.小结

中断是一种由设备使用的硬件资源异步向处理器发送信号。实际上,中断就是由硬件来打断操作系统。大多数现代硬件都通过中断与操作系统通信。对某个硬件管理的驱动程序注册中断处理程序,是为了响应并处理来自硬件的相关中断。中断过程所做的工作包括应答并重新设置硬件,从设备拷贝数据或反之,或处理硬件请求或发送硬件请求。

内核提供的接口包括注册和注销中断程序、禁止中断、屏蔽中断线以及检查中断系统状态。由于中断打断了其他代码的运行,他们必须赶紧完成,但是通常需要做的工作是很多的。为了大量的工作与快速执行之间找到一个平衡。内核把处理中断的工作分为两半。

附录

static

        C语言函数定义默认为全局的,而声明为static的函数只在当前.c文件中可见,其他.c文件不可见;可以在别的.c文件中重新定义同名的函数。因为当前头文件interrupt.h要在许多.c文件中包含,如果不添加这个声明,会出现多重定义的报错,c文件包含头文件实则要在编译c文件中展开该头文件,第二个点是优化,当c文件中没有使用该static声明的函数的话,编译器会把该函数的汇编代码优化掉。

inline

inline是内联函数的声明,该声明的功能是直接用函数体代码来替代对函数的调用,这一过程称为函数体的内联展开。在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,引入了inline功能。

当某一主函数调用内联函数时,在编译过程中,内联函数是直接复制“镶嵌”到主函数中去的,就是将内联函数的代码直接放在主函数中调用内联函数的位置上。与一般函数不同,主函数在调用一般函数的时候,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码;而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行。内联函数一般在头文件中定义,而一般函数在头文件中声明,在cpp中定义。

对于只有几条语句的小函数来说,与函数的调用、返回有关的准备和收尾工作的代码往往比函数体本身的代码要大得多。因此,对于这类简单的、使用频繁的小函数,将之说明为内联函数可提高运行效率

inline和#define的区别

在读到上一小节的时候,许多有基础的读者可能会根据inline的功能而联想到宏定义。接下来我们就一起看看两者各自的特点。

宏:预编译时,使用定义符号代替宏

优点:提高了代码的效率和通用性

缺点:

1.没有类型检测,返回值无法类型转换;

2.宏在预处理时进行的是简单的文本替换,而不是传参,存在风险,

如#define A(X) x*x

3.造成代码膨胀

4.宏不能进行调试

5.预处理时搜索宏定义符号并替代时,无法处理到字符常量中

Inline:将内联函数的代码复制到调用该内联函数的地方

优点:

  1. inline函数是真正的函数,所以要进行一系列的数据类型检查
  2. 内联函数是在程序编译时展开,而且是进行的是参数传递
  3. 内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数。

  (1)函数体内的代码比较长,将导致内存消耗代价;

  (2)函数体内有循环,函数执行时间要比函数调用开销大

     4. 可以进行调试

缺点:

  1. 代码膨胀,占用内存变多

共同点:

  1. 占用内存变多
  2. 提高代码的通用性,提升运行效率

不同点:

  1. 内联函数的调用是传参,宏定义只是简单的文本替换
  2. 内联函数有类型检测更加的安全,宏定义没有类型检测语法判断等功能
  3. 内联函数在程序编译时嵌入,宏定义是在程序预编译时替换
  4. 内联函数在运行时可调式,宏定义不可以
  5. 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义

inline的使用

Inline的使用是有所限制的,inline只适合涵数体内代码简单的涵数使用,不能包含复杂的结构控制语句例如开关语句 循环语句,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。

因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。

inline函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。

内联函数并不是一个增强性能的灵丹妙药。只有当函数非常短小的时候它才能得到我们想要的效果,如果内联函数不能增强性能,就避免使用它。

typedef

typedef是C语言中的一个关键字,其作用有是为某一种类型定义一个别名,这种别名与宏不同,不是简单的文本替换,这种类型可以是内部类型也可以是用户自定义类型,也可以将复杂声明简化。

例如,

typedef int* PINT;

PINT p1,p2;

这种情况p1,p2均为int类型的指针;而下面这种情况则不相同

int* p1,p2;

p1为int类型指针,p2为int类型。

按照惯例,用户自定义的类型名为大写,以便于提醒。这样对已存在的数据类型起别名,可以解决平台移植时的问题,比如typedef bool BOOL和typedef int BOOL,在以前没有bool类型的版本中通常使用后者。

另外一种常用的是我们使用typedef来定义结构体的别名,例如,定义一个存储书本信息的结构体

struct books{

    char title[50];
    char author[50];
    int book_id;

};

在使用该结构体去定义变量时,声明为

struct books book1;

而使用typedef给结构体起类型别名后,这个过程可以理解为做了两件事情,一为定义结构体,二是为结构体起别名。

typedef struct books{

    char title[50];
    char author[50];
    int book_id;

} BOOK;

声明就可以改为,BOOK book1;

使用typedef还可以对数组的声明进行简化,例如

typedef struct TableEntry Table[Num];

首先,这句代码去掉typedef后,就是一个数组的声明,该数组的类型是结构体TableEntry,数组中元素的数量是Num。

而增加了typedef之后,这句话的意思就是定义了一个新的类型名,观察整个语句中,除了数组元素的类型和数量外,用户自定义部分只有Table,因此,这句话声明了 一个拥有数组元素数量为Num且数组元素类型为struct TableEntry 的用户自定义类型为Table。

另外,使用typedef还可以将复杂声明简化

理解复杂声明可用的“右左法则”:
  从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。

例如,

int (*func[5])(int *);

func右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明func的元素是指针([]运算符优先级比*高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指针,它指向的函数具有int*类型的形参,返回值类型为int。声明了一个含有5个元素的数组,每个元素是一个函数的指针。接下来用几个例子进行说明typedef如何简化复杂声明,

int *(*a[5])(int, char*); 

首先a是一个拥有5个元素的数组,其中的元素是函数指针,函数的入参是int和char指针,返回值是int指针。上面的代码可以用下面两句代码来代替:

typedef int* (*PA)(int,char*);

//PA是 入参是int、char指针,返回值是int指针的函数指针的类型名

PA a[5];

//使用定义的类型名来声明一个数组

例二

void (*b[10])(void (*)());

首先,b是一个拥有是个元素的数组,其中的元素是入参为void (*)()(入参为void,返回值为void的函数指针),返回值为void的函数指针。

先对void (*)()声明一个新类型

typedef void (*v1)();

//声明一个新类型v1,入参void,返回值void的函数指针

typedef void (*v2)(v1);

//声明一个新类型,入参为v1,返回值为void的函数指针

v2 b[10];

例三

double (* (*pa)[9])();

首先,pa是一个指针,pa指向一个九个元素的数组,数组里的元素都是void为入参,返回值会double的函数指针。

typedef double (*v1)();

//定义一个类型v1,void为入参,double为返回值的函数指针

//上面代码退化为v1 (*pa)[9];

typedef v1 (*v2)[9];

//定义一个新类型v2,数据类型为v1,元素数量为9的数组的指针

v2 pa;

上面讨论的 typedef 行为有点像 #define 宏,但是不同的是,#define宏是在预处理时进行的,而typdef是在编译是完成的,它可以完成预处理时无法完成的工作。 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样。

函数属性

GNU C的一大特色(却不被初学者所知)就是__attribute__机制。__attribute__可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。函数属性来指定某些函数属性,这些属性可以帮助编译器优化调用或更仔细地检查代码的正确性。
__attribute__书写特征是:__attribute__前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__参数。
__attribute__语法格式为:__attribute__ ((attribute-list))
其位置约束为:放于声明的尾部“;”之前。

例如:

static inline int __must_check request_irq(...);
void* my_memalign (size_t, size_t) __attribute__ ((alloc_align (1)));

上面的内核代码中出现了一个例子__must_check

#define __must_check   __attribute__((warn_unused_result))

如果具有warn_unused_result此属性的函数的调用者不使用其返回值,则该属性会导致发出警告。这对于那些不检查结果的安全隐患或者有BUG的函数来说很有用,例如 realloc.

参考文献:

1. 《Linux内核设计与实现》第三版 美Robert Love著 陈莉君 康华译;

2.  Linux 4.1.25内核

3. 众多CSDN博客 网络博客

        Linux内核设计理念精深,覆盖范围浩如烟海,知识点更是多如牛毛。学习Linux内核还是要从积累 分享做起,作为新人,希望有前辈多多指点,提出令人振聋发聩、耳目一新的评论观点。
 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值