深入理解linux内核的中断和信号

84 篇文章 0 订阅

参考文章:

<<linux设备驱动>>第十章节

<<linux内核之旅>>(http://www.kerneltravel.net/book/book/%E7%AC%AC%E4%B8%89%E7%AB%A0%E4%B8%AD%E6%96%AD%E6%9C%BA%E5%88%B6.pdf)

概述

中断这个话题很早以前就接触过,尤其是以前接触嵌入式的时候,这项技术是为了解决io等待的问题,硬件设备io很多时候收到数据时候检查标准是高低沿变化,当发生变化的时候我们就认为收到数据,如果没有中断那么我们要写一个while循环去不停的检查一个标志位,这毫无疑问对cpu资源来说是一个浪费,于是我们便引入了中断,cpu从中断控制器IO端口接收中断事件,然后进行运行栈切换,从而避免了忙等待。

Intel x86 通过两片中断控制器 8259A 来响应 15 个外中断源

中断资源是很宝贵的,我们把和中断控制器连接的每条线称为中断线,图中从0开始编号,共有16条中断线

中断控制器的作用是:

1.监视中断线,检查产生中断请求的信号,然后把irq转化为向量,放到io端口上等待cpu读取

2.把产生的向量放到cpu的引脚上

3.cpu确认这个信号,然后把他写进可中断编程控制器

异常就是cpu内部出现中断比如:

对应的unix信号

 中断描述符表

实地址模式中断描述符表从0开始占用1k的字节。表中的每一个表项占用四个字节,由两个字节的段地址,两个字节的偏移量组成

这些描述符可以分为3大类

任务门(Task gate)

中断门(Interrupt gate)

陷阱门(Trap gate)

系统门(System gate)

最后,在保护模式下,中断描述符表在内存的位置不再限于从地址 0 开始的地方,而是 可以放在内存的任何地方。为此,CPU 中增设了一个中断描述符表寄存器 IDTR,用来存放中 断描述符表在内存的起始地址。中断描述符表寄存器 IDTR 是一个 48 位的寄存器,其低 16 位保存中断描述符表的大小,高 32 位保存 IDT 的基址,如图 3.3 所示

中断描述符表初始化 

8259A 通过两个端口来进行数据传送,,这两个端口是 0xA0 和 0xA1。代码在

/usr/src/linux-source-5.4.0/linux-source-5.4.0/arch/x86/kernel/i8259.c 

static void init_8259A(int auto_eoi)
{
        unsigned long flags;

        i8259A_auto_eoi = auto_eoi;

        raw_spin_lock_irqsave(&i8259A_lock, flags);

        outb(0xff, PIC_MASTER_IMR);     /* mask all of 8259A-1 */

        /*
         * outb_pic - this has to work on a wide range of PC hardware.
         */
        outb_pic(0x11, PIC_MASTER_CMD); /* ICW1: select 8259A-1 init */

        /* ICW2: 8259A-1 IR0-7 mapped to ISA_IRQ_VECTOR(0) */
        outb_pic(ISA_IRQ_VECTOR(0), PIC_MASTER_IMR);

        /* 8259A-1 (the master) has a slave on IR2 */
        outb_pic(1U << PIC_CASCADE_IR, PIC_MASTER_IMR);

        if (auto_eoi)   /* master does Auto EOI */
                outb_pic(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR);
       else            /* master expects normal EOI */
                outb_pic(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR);

        outb_pic(0x11, PIC_SLAVE_CMD);  /* ICW1: select 8259A-2 init */

        /* ICW2: 8259A-2 IR0-7 mapped to ISA_IRQ_VECTOR(8) */
        outb_pic(ISA_IRQ_VECTOR(8), PIC_SLAVE_IMR);
        /* 8259A-2 is a slave on master's IR2 */
        outb_pic(PIC_CASCADE_IR, PIC_SLAVE_IMR);
        /* (slave's support for AEOI in flat mode is to be investigated) */
        outb_pic(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR);

        if (auto_eoi)
                /*
                 * In AEOI mode we just have to mask the interrupt
                 * when acking.
                 */
                i8259A_chip.irq_mask_ack = disable_8259A_irq;
        else
                i8259A_chip.irq_mask_ack = mask_and_ack_8259A;

        udelay(100);            /* wait for 8259A to initialize */

        outb(cached_master_mask, PIC_MASTER_IMR); /* restore master IRQ mask */

        raw_spin_unlock_irqrestore(&i8259A_lock, flags);
}

                                                                                

outb具体介绍见<<linux设备驱动>>,里面介绍了这个api是和硬件io交互的重要函数,也就是说通过这个函数 和中断控制器端口进行交互,从而进行初始化

当计算机运行在实模式时,IDT 被初始化并由 BIOS 使用。然而,一旦真正进入了 Linux 内核,IDT 就被移到内存的另一个区域,并进行进入实模式的初步初始化。

中断请求队列的数据结构

256个中断向量,32个分配个异常了,剩下224个作为中断向量

符,224 个 IRQ 形成一个数组 irq_desc[],其定义在/include/linux/irq.h 中:

status 描述 IRQ 中断线状态的一组标志(在 irq.h 中定义),其具体含义及应用将在 do_IRQ() 函数中介绍。

handler 指向 hw_interrupt_type 描述符,这个描述符是对中断控制器的描述 

action 指向一个单向链表的指针,这个链表就是对中断服务例程进行描述的 irqaction 结构,

depth 如果启用这条 IRQ 中断线,depth 则为 0,如果禁用这条 IRQ 中断线不止一次,则为一 个正数。每当调用一次 disable_irq( ),该函数就对这个域的值加 1;如果 depth 等于 0, 该函数就禁用这条 IRQ 中断线。相反,每当调用 enable_irq( )函数时,该函数就对这个 域的值减 1;如果 depth 变为 0,该函数就启用这条 IRQ 中断线。

hw_interrupt_type 介绍:

:
/*
* Interrupt controller descriptor. This is all we need
* to describe about the low-level hardware.
*/
struct hw_interrupt_type {
 const char * typename;
 unsigned int (*startup)(unsigned int irq);
 void (*shutdown)(unsigned int irq);
 void (*enable)(unsigned int irq);
 void (*disable)(unsigned int irq);
 void (*ack)(unsigned int irq);
 void (*end)(unsigned int irq); 
void (*set_affinity)(unsigned int irq, unsigned long mask);
};
typedef struct hw_interrupt_type hw_irq_controller;

struct hw_interrupt_type i8259A_irq_type = {
 "XT-PIC",
 startup_8259A_irq,
 shutdown_8259A_irq,
 do_8259A_IRQ,
 enable_8259A_irq,
 disable_8259A_irq
 };

中断服务例程描述符 irqaction

struct irqaction {
 void (*handler)(int, void *, struct pt_regs *);
 unsigned long flags;
 unsigned long mask;
 const char *name;
 void *dev_id;
 struct irqaction *next;
 };

handler 指向一个具体 I/O 设备的中断服务例程。这是允许多个设备共享同一中断线的关键域。 flags 用一组标志描述中断线与 I/O 设备之间的关系。

SA_INTERRUPT 中断处理程序必须以禁用中断来执行。

SA_SHIRQ 该设备允许其中断线与其他设备共享。

SA_SAMPLE_RANDOM 可以把这个设备看作是随机事件发生源;因此,内核可以用它做随机数产生器(用户可以从/dev/random 和/dev/urandom 设备文件中取得随机数而访问这种特征)。

SA_PROBE 内核在执行硬件设备探测时正在使用这条中断线。 name I/O 设备名(读取/proc/interrupts 文件,可以看到,在列出中断号时也显示设备名)。

dev_id 指定 I/O 设备的主设备号和次设备号。

next 指向 irqaction 描述符链表的下一个元素。共享同一中断线的每个硬件设备都有其对应 的中断服务例程,链表中的每个元素就是对相应设备及中断服务例程的描述。

中断请求队列的初始化

中断请求队列初始化发生在安装中断处理例程上,具体参见<<linux设备驱动程序>>

书里面说了两个十分重要的api

int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void*, struct pr_regs*), unsigned long flags, const char* dev_name,void* dev_id);

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

前面已经说过中断线只有16根,非常珍贵

在现在linux 代码中,中断控制器request_irq 函数被切分开了,直接看书中的request_irq实现

int request_irq(unsigned int irq,
 void (*handler)(int, void *, struct pt_regs *),
 unsigned long irqflags,
 const char * devname,
 void *dev_id)
 {
 int retval;
 struct irqaction * action;

 #if 1
 /*
 * Sanity-check: shared interrupts should REALLY pass in
 * a real dev-ID, otherwise we'll have trouble later trying
 * to figure out which interrupt is which (messes up the
 * interrupt freeing logic etc).
 */
 if (irqflags & SA_SHIRQ) {
 if (!dev_id)
 printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname,
(&irq)[-1]);
 }
#endif
 if (irq >= NR_IRQS)
 return -EINVAL;
 if (!handler)
 return -EINVAL;
 action = (struct irqaction *)
 kmalloc(sizeof(struct irqaction), GFP_KERNEL);
 if (!action)
 return -ENOMEM
action->handler = handler;
 action->flags = irqflags;
 action->mask = 0;
 action->name = devname; /*对 action 进行初始化*/
 action->next = NULL;
 action->dev_id = dev_id;
 retval = setup_irq(irq, action);
 if (retval)
 kfree(action);
 return retval;
} 

setup_irq

int setup_irq(unsigned int irq, struct irqaction * new)
{
 int shared = 0;
 unsigned long flags;
 struct irqaction *old, **p;
 irq_desc_t *desc = irq_desc + irq; /*获得 irq 的描述符*/

 /* 对中断请求队列的操作必须在临界区中进行 */
 spin_lock_irqsave(&desc->lock,flags); /*进入临界区*/
 p = &desc->action; /*让 p 指向 irq 描述符的 action 域,即 irqaction 链表的首部*/
 if ((old = *p) != NULL) { /*如果这个链表不为空*/
 /* Can't share interrupts unless both agree to */
 if (!(old->flags & new->flags & SA_SHIRQ)) {
 spin_unlock_irqrestore(&desc->lock,flags);
 return -EBUSY;
 }
 /* 把新的中断服务例程加入到 irq 中断请求队列*/
 do {
 p = &old->next;
 old = *p;
 } while (old);
 shared = 1;
 }
 *p = new;
 if (!shared) { /*如果 irq 不被共享 */
 desc->depth = 0; /*启用这条 irq 线*/
 desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING);
 desc->handler->startup(irq); /*即调用 startup_8259A_irq()函数*/
 }
 spin_unlock_irqrestore(&desc->lock,flags); /*退出临界区*/
 register_irq_proc(irq); /*在 proc 文件系统中显示 irq 的信息*/
 return 0;

也就是简单说request_irq主要申请了一个中断服务实例,挂载到了队列上

中断处理

当中断发生的时候根据中断紧急程度不同又分为

紧急的(Critical)

这样的操作诸如:中断到来时中断控制器做出应答,对中断控制器或设备控制器重新编程, 或者对设备和处理器同时访问的数据结构进行修改。这些操作都是紧急的,应该被很快地执行, 也就是说,紧急操作应该在一个中断处理程序内立即执行,而且是在禁用中断的状态下。

非紧急的(Noncritical)

这样的操作如修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。 这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但在启用中断的状态下。

3.非紧急可延迟的(Noncritical deferrable)

这样的操作如,把一个缓冲区的内容拷贝到一些进程的地址空间(例如,把键盘行缓冲 区的内容发送到终端处理程序的进程)。这些操作可能被延迟较长的时间间隔而不影响内核操 作:有兴趣的进程会等待需要的数据。非紧急可延迟的操作由一些被称为“下半部分”(bottom halves)的函数来执行

当中断发生的时候 如果在用户态,要进行用户态和内核态的切换切换到内核态

然后切换后,会从中断描述符表中找到中断描述符,然后逐条执行中断服务例程,见handle_IRQ_event

if (!(action->flags & SA_INTERRUPT))
 __sti(); /*关中断*/
do {
 status |= action->flags;
 action->handler(irq, action->dev_id, regs);
 action = action->next;
 } while (action);

 __cli(); /*开中断*/

 处理完后如果有软中断,则会执行软中断

if (softirq_pending(cpu))
 do_softirq(); /*处理软中断*/ 

从中断返回:

ret_from_intr() 终止中断处理程序。

ret_from_sys_call( ) 终止系统调用,即由 0x80 引起的异常。

ret_from_exception( ) 终止除了 0x80 的所有异常。

中断的后半部分处理机制

中断可能非常多,如果一口气全部执行完,可能会用很多cpu,所以内核提供了下半部分中断处理,

内核提供了三种下半部中断

1、软中断

2、tasklet

3、work_queue

软中断一般运行在中断上下文,一般要求实时性较高,执行要短,并且是并行

tasklet 和work_queue一般用在驱动中,tasklet运行在中断上下文而且是建立在软中断上,一个tasklet只会运行在一个cpu上;work_queue运行在进程上下文

具体的不多看了,参考<<linux 设备驱动>>第10章节,里面有不错的介绍,具体的使用区别都有

总结:

原理介绍完了,真做内核开发工作中,我们要记住以下几个中断要点:

1、上半部中断就是request_irq 和 free_irq 主要是这两个api,一般在kernel模块初始化和卸载的时候用,要尽量简单

2、下半部中断,软中断这个就不要用了,尽量使用tasklet和workqueue。tasklet 一般用在低延迟的任务,触发时候会关闭中断,使用的时候更不能使其阻塞睡,tasklet不能使用多喝;如果要使用多核,并且内部可能有阻塞、睡眠,那么毫无疑问,work_queue是你的首选

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值