一步一步学linux操作系统: 30 输入与输出系统_ 字符设备二_中断处理机制

中断处理函数 irq_handler_t

鼠标的中断

鼠标就是通过中断,将自己的位置和按键信息,传递给设备驱动程序。

\drivers\input\mouse\logibm.c


static int logibm_open(struct input_dev *dev)
{
  if (request_irq(logibm_irq, logibm_interrupt, 0, "logibm", NULL)) {
    printk(KERN_ERR "logibm.c: Can't allocate irq %d\n", logibm_irq);
    return -EBUSY;
  }
  outb(LOGIBM_ENABLE_IRQ, LOGIBM_CONTROL_PORT);
  return 0;
}


static irqreturn_t logibm_interrupt(int irq, void *dev_id)
{
  char dx, dy;
  unsigned char buttons;


  outb(LOGIBM_READ_X_LOW, LOGIBM_CONTROL_PORT);
  dx = (inb(LOGIBM_DATA_PORT) & 0xf);
  outb(LOGIBM_READ_X_HIGH, LOGIBM_CONTROL_PORT);
  dx |= (inb(LOGIBM_DATA_PORT) & 0xf) << 4;
  outb(LOGIBM_READ_Y_LOW, LOGIBM_CONTROL_PORT);
  dy = (inb(LOGIBM_DATA_PORT) & 0xf);
  outb(LOGIBM_READ_Y_HIGH, LOGIBM_CONTROL_PORT);
  buttons = inb(LOGIBM_DATA_PORT);
  dy |= (buttons & 0xf) << 4;
  buttons = ~buttons >> 5;


  input_report_rel(logibm_dev, REL_X, dx);
  input_report_rel(logibm_dev, REL_Y, dy);
  input_report_key(logibm_dev, BTN_RIGHT,  buttons & 1);
  input_report_key(logibm_dev, BTN_MIDDLE, buttons & 2);
  input_report_key(logibm_dev, BTN_LEFT,   buttons & 4);
  input_sync(logibm_dev);


  outb(LOGIBM_ENABLE_IRQ, LOGIBM_CONTROL_PORT);
  return IRQ_HANDLED

在这里插入图片描述
irq_handler_t函数


irqreturn_t (*irq_handler_t)(int irq, void * dev_id);


/**
 * enum irqreturn
 * @IRQ_NONE    interrupt was not from this device or was not handled
 * @IRQ_HANDLED    interrupt was handled by this device
 * @IRQ_WAKE_THREAD  handler requests to wake the handler thread
 */
enum irqreturn {
  IRQ_NONE    = (0 << 0),
  IRQ_HANDLED    = (1 << 0),
  IRQ_WAKE_THREAD    = (1 << 1),
};
  • irq
    中断信号

  • dev_id
    void * 的通用指针,用于区分同一个中断处理函数对于不同设备的处理

  • 三种返回值

    • IRQ_NONE:设备不是中断接收者
    • IRQ_HANDLED:处理完了的中断
    • IRQ_WAKE_THREAD:有一个进程正在等待这个中断,中断处理完了,应该唤醒它

很多中断处理程序将整个中断要做的事情分成两部分,称为上半部和下半部,或者成为关键处理部分和延迟处理部分。

在中断处理函数中,仅仅处理关键部分,完成了就将中断信号打开,使得新的中断可以进来,需要比较长时间处理的部分,也即延迟部分,往往通过工作队列等方式慢慢处理。

注册中断处理函数 request_irq

\include\linux\interrupt.h


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

在这里插入图片描述

  • 参数
    • unsigned int irq 是中断信号
    • irq_handler_t handler 是中断处理函数
    • unsigned long flags 是一些标识位
    • const char *name 是设备名称
    • void *dev 这个通用指针应该和中断处理函数的 void *dev 相对应

request_irq 调用的是 request_threaded_irq

request_threaded_irq 函数

\kernel\irq\manage.c


int request_threaded_irq(unsigned int irq, irq_handler_t handler,
       irq_handler_t thread_fn, unsigned long irqflags,
       const char *devname, void *dev_id)
{
  struct irqaction *action;
  struct irq_desc *desc;
  int retval;
......
  desc = irq_to_desc(irq);
......
  action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
  action->handler = handler;
  action->thread_fn = thread_fn;
  action->flags = irqflags;
  action->name = devname;
  action->dev_id = dev_id;
......
  retval = __setup_irq(irq, desc, action);
......
}

在这里插入图片描述
每一个中断,都有一个对中断的描述结构 struct irq_desc,其中 一个重要的成员变量是 struct irqaction,用于表示处理这个中断的动作。

irqaction 结构


struct irq_desc {
......
  struct irqaction  *action;  /* IRQ action list */
......
  struct module    *owner;
  const char    *name;
};


/**
 * struct irqaction - per interrupt action descriptor
 * @handler:  interrupt handler function
 * @name:  name of the device
 * @dev_id:  cookie to identify the device
 * @percpu_dev_id:  cookie to identify the device
 * @next:  pointer to the next irqaction for shared interrupts
 * @irq:  interrupt number
 * @flags:  flags (see IRQF_* above)
 * @thread_fn:  interrupt handler function for threaded interrupts
 * @thread:  thread pointer for threaded interrupts
 * @secondary:  pointer to secondary irqaction (force threading)
 * @thread_flags:  flags related to @thread
 * @thread_mask:  bitmask for keeping track of @thread activity
 * @dir:  pointer to the proc/irq/NN/name entry
 */
struct irqaction {
  irq_handler_t    handler;
  void      *dev_id;
  void __percpu    *percpu_dev_id;
  struct irqaction  *next;
  irq_handler_t    thread_fn;
  struct task_struct  *thread;
  struct irqaction  *secondary;
  unsigned int    irq;
  unsigned int    flags;
  unsigned long    thread_flags;
  unsigned long    thread_mask;
  const char    *name;
  struct proc_dir_entry  *dir;
};

在这里插入图片描述
\include\linux\irqdesc.h
在这里插入图片描述
\include\linux\interrupt.h

其中有有 next 指针,这是一个链表,对于这个中断的所有处理动作,都串在这个链表上。
几个主要成员:

  • 中断处理函数 handler
  • void *dev_id 为设备 id
  • irq 为中断信号
  • 如果中断处理函数在单独的线程运行,则有 thread_fn 是线程的执行函数,thread 是线程的 task_struct。

中断描述结构 struct irq_desc 的查找

在 request_threaded_irq 函数中,irq_to_desc 根据中断信号查找中断描述结构

区分情况:

  • 一般情况下,所有的 struct irq_desc 都放在一个数组里面直接按下标查找
  • 配置了 CONFIG_SPARSE_IRQ 那中断号是不连续的,不适合用数组保存,使用基数树

为什么中断信号有不连续的情况?
irq 并不是真正的、物理的中断信号,而是一个抽象的、虚拟的中断信号。

为了适配各种各样的硬件中断控制器,需要有一层中断抽象层。虚拟中断信号到中断描述结构的映射,就是抽象中断层的主要逻辑。

如果只有一个 CPU,一个中断控制器,则基本能够保证从物理中断信号到虚拟中断信号的映射是线性的。

但是如果有多个 CPU,多个中断控制器,每个中断控制器各有各的物理中断信号,就没办法保证虚拟中断信号是连续的,所以就要用到了基数树。

request_threaded_irq 函数分配了一个 struct irqaction,并且初始化它,接着调用 __setup_irq。

__setup_irq 函数

\linux-4.13.16\kernel\irq\manage.c


static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
  struct irqaction *old, **old_ptr;
  unsigned long flags, thread_mask = 0;
  int ret, nested, shared = 0;
......
  new->irq = irq;
......
  /*
   * Create a handler thread when a thread function is supplied
   * and the interrupt does not nest into another interrupt
   * thread.
   */
  if (new->thread_fn && !nested) {
    ret = setup_irq_thread(new, irq, false);
  }
......
  old_ptr = &desc->action;
  old = *old_ptr;
  if (old) {
    /* add new interrupt at end of irq queue */
    do {
      thread_mask |= old->thread_mask;
      old_ptr = &old->next;
      old = *old_ptr;
    } while (old);
  }
......
  *old_ptr = new;
......
  if (new->thread)
    wake_up_process(new->thread);
......
}


static int
setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
  struct task_struct *t;
  struct sched_param param = {
    .sched_priority = MAX_USER_RT_PRIO/2,
  };


  t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
  sched_setscheduler_nocheck(t, SCHED_FIFO, &param);
  get_task_struct(t);
  new->thread = t;
......
  return 0;

在这里插入图片描述
在这个函数里面,如果 struct irq_desc 里面已经有 struct irqaction,就将新的 struct irqaction 挂在链表的末端。
如果设定了以单独的线程运行中断处理函数,setup_irq_thread 就会创建这个内核线程,wake_up_process 会唤醒它。

request_irq就是根据中断信号 irq,找到基数树上对应的 irq_desc,然后将新的 irqaction 挂在链表上。

中断处理流程

真正中断的发生从硬件开始,有四个层次:

  • 第一个层次是外部设备给中断控制器发送物理中断信号
  • 第二个层次是中断控制器将物理中断信号转换成为中断向量 interrupt vector,发给各个 CPU。
  • 第三个层次是每个 CPU 都会有一个中断向量表,根据 interrupt vector 调用一个 IRQ 处理函数。这一层还是 CPU 硬件的要求。IRQ 处理函数还不是上面指定的 irq_handler_t
  • 第四个层次是在 IRQ 处理函数中,将 interrupt vector 转化为抽象中断层的中断信号 irq,调用中断信号 irq 对应的中断描述结构里面的 irq_handler_t。

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

中断向量定义
arch/x86/include/asm/irq_vectors.h


/*
 * Linux IRQ vector layout.
 *
 * There are 256 IDT entries (per CPU - each entry is 8 bytes) which can
 * be defined by Linux. They are used as a jump table by the CPU when a
 * given vector is triggered - by a CPU-external, CPU-internal or
 * software-triggered event.
 *
 * Linux sets the kernel code address each entry jumps to early during
 * bootup, and never changes them. This is the general layout of the
 * IDT entries:
 *
 *  Vectors   0 ...  31 : system traps and exceptions - hardcoded events
 *  Vectors  32 ... 127 : device interrupts
 *  Vector  128         : legacy int80 syscall interface
 *  Vectors 129 ... INVALIDATE_TLB_VECTOR_START-1 except 204 : device interrupts
 *  Vectors INVALIDATE_TLB_VECTOR_START ... 255 : special interrupts
 *
 * 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table.
 *
 * This file enumerates the exact layout of them:
 */
#define FIRST_EXTERNAL_VECTOR    0x20
#define IA32_SYSCALL_VECTOR    0x80
#define NR_VECTORS       256
#define FIRST_SYSTEM_VECTOR    NR_VECTORS

在这里插入图片描述
CPU 能够处理的中断总共 256 个,用宏 NR_VECTOR 或者 FIRST_SYSTEM_VECTOR 表示。

CPU 硬件要求每一个 CPU 都有一个中断向量表 idt_table,通过 load_idt 加载,里面记录着每一个中断对应的处理函数。

中断向量表定义
\linux-4.13.16\arch\x86\kernel\traps.c

gate_desc idt_table[NR_VECTORS] __page_aligned_bss;

在这里插入图片描述

中断分类

编号 0 到 31 的前 32 :

系统陷入或者系统异常,这些错误无法屏蔽,一定要处理。中断向量表中已经填好了前 32 位,外加一位 32 位系统调用。
这些中断的处理函数在 是系统初始化的时候,在start_kernel函数中调用trap_init(),在trap_init 函数调用了大量的 set_intr_gate,最终都会调用 _set_gate 将每个中断都设置了中断处理函数,放在中断向量表 idt_table 中。
arch/x86/include/asm/traps.h


/* Interrupts/Exceptions */
enum {
  X86_TRAP_DE = 0,  /*  0, Divide-by-zero */
  X86_TRAP_DB,    /*  1, Debug */
  X86_TRAP_NMI,    /*  2, Non-maskable Interrupt */
  X86_TRAP_BP,    /*  3, Breakpoint */
  X86_TRAP_OF,    /*  4, Overflow */
  X86_TRAP_BR,    /*  5, Bound Range Exceeded */
  X86_TRAP_UD,    /*  6, Invalid Opcode */
  X86_TRAP_NM,    /*  7, Device Not Available */
  X86_TRAP_DF,    /*  8, Double Fault */
  X86_TRAP_OLD_MF,  /*  9, Coprocessor Segment Overrun */
  X86_TRAP_TS,    /* 10, Invalid TSS */
  X86_TRAP_NP,    /* 11, Segment Not Present */
  X86_TRAP_SS,    /* 12, Stack Segment Fault */
  X86_TRAP_GP,    /* 13, General Protection Fault */
  X86_TRAP_PF,    /* 14, Page Fault */
  X86_TRAP_SPURIOUS,  /* 15, Spurious Interrupt */
  X86_TRAP_MF,    /* 16, x87 Floating-Point Exception */
  X86_TRAP_AC,    /* 17, Alignment Check */
  X86_TRAP_MC,    /* 18, Machine Check */
  X86_TRAP_XF,    /* 19, SIMD Floating-Point Exception */
  X86_TRAP_IRET = 32,  /* 32, IRET Exception */
};

在这里插入图片描述
其他的都是用于设备中断:

在中断向量表里面都会设置为从 irq_entries_start 开始,偏移量为 i - FIRST_EXTERNAL_VECTOR 的一项。

任何一个中断向量到达任何一个 CPU,最终都会走到 do_IRQ进行统一处理,在这里会让中断向量,通过 vector_irq 映射为 irq_desc

do_IRQ 函数

linux-4.13.16\arch\x86\kernel\irq.c


/*
 * do_IRQ handles all normal device IRQ's (the special
 * SMP cross-CPU interrupts have their own specific
 * handlers).
 */
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
  struct pt_regs *old_regs = set_irq_regs(regs);
  struct irq_desc * desc;
  /* high bit used in ret_from_ code  */
  unsigned vector = ~regs->orig_ax;
......
  desc = __this_cpu_read(vector_irq[vector]);
  if (!handle_irq(desc, regs)) {
......
  }
......
  set_irq_regs(old_regs);
  return 1;
}

在这里插入图片描述
从 AX 寄存器里面拿到了中断向量 vector。中断控制器发送给每个 CPU 的中断向量都是每个 CPU 局部的,而抽象中断处理层的虚拟中断信号 irq 以及它对应的中断描述结构 irq_desc 是全局的。也即这个 CPU 的 200 号的中断向量和另一个 CPU 的 200 号中断向量对应的虚拟中断信号 irq 和中断描述结构 irq_desc 可能不一样,这就需要一个映射关系。这个映射关系放在 Per CPU 变量 vector_irq 里面。

DECLARE_PER_CPU(vector_irq_t, vector_irq);

系统初始化的时候 调用 __assign_irq_vector,将虚拟中断信号 irq 分配到某个 CPU 上的中断向量。

__assign_irq_vector 函数
\linux-4.13.16\arch\x86\kernel\apic\vector.c


static int __assign_irq_vector(int irq, struct apic_chip_data *d,
             const struct cpumask *mask,
             struct irq_data *irqdata)
{
  static int current_vector = FIRST_EXTERNAL_VECTOR + VECTOR_OFFSET_START;
  static int current_offset = VECTOR_OFFSET_START % 16;
  int cpu, vector;
......
  while (cpu < nr_cpu_ids) {
    int new_cpu, offset;
......
    vector = current_vector;
    offset = current_offset;
next:
    vector += 16;
    if (vector >= first_system_vector) {
      offset = (offset + 1) % 16;
      vector = FIRST_EXTERNAL_VECTOR + offset;
    }


    /* If the search wrapped around, try the next cpu */
    if (unlikely(current_vector == vector))
      goto next_cpu;




    if (test_bit(vector, used_vectors))
      goto next;


......
    /* Found one! */
    current_vector = vector;
    current_offset = offset;
    /* Schedule the old vector for cleanup on all cpus */
    if (d->cfg.vector)
      cpumask_copy(d->old_domain, d->domain);
    for_each_cpu(new_cpu, vector_searchmask)
      per_cpu(vector_irq, new_cpu)[vector] = irq_to_desc(irq);
    goto update;


next_cpu:
    cpumask_or(searched_cpumask, searched_cpumask, vector_cpumask);
    cpumask_andnot(vector_cpumask, mask, searched_cpumask);
    cpu = cpumask_first_and(vector_cpumask, cpu_online_mask);
    continue;
  }
....

在这里插入图片描述

一旦找到某个向量,就将 CPU 的此向量对应的向量描述结构 irq_desc,设置为虚拟中断信号 irq 对应的向量描述结构 irq_to_desc(irq)。这样 do_IRQ 会根据中断向量vector 得到对应的 irq_desc,然后调用 handle_irq
handle_irq 会调用 generic_handle_irq_desc,里面调用 irq_desc 的 handle_irq,最终会调用 __handle_irq_event_percpu

__handle_irq_event_percpu 函数

\linux-4.13.16\kernel\irq\handle.c


irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
  irqreturn_t retval = IRQ_NONE;
  unsigned int irq = desc->irq_data.irq;
  struct irqaction *action;


  record_irq_time(desc);


  for_each_action_of_desc(desc, action) {
    irqreturn_t res;
    res = action->handler(irq, action->dev_id);
    switch (res) {
    case IRQ_WAKE_THREAD:
      __irq_wake_thread(desc, action);
    case IRQ_HANDLED:
      *flags |= action->flags;
      break;
    default:
      break;
    }
    retval |= res;
  }
  return retval;

在这里插入图片描述

__handle_irq_event_percpu 里面调用了 irq_desc 里每个 hander,这些 hander 是 action 列表中注册的,这才是设置的那个中断处理函数

如果返回值是 IRQ_HANDLED,就说明处理完毕;如果返回值是 IRQ_WAKE_THREAD 就唤醒线程。

总结

图片来自极客时间趣谈linux操作系统
图片来自极客时间趣谈linux操作系统

中断是从外部设备发起的,形成外部中断。外部中断会到达中断控制器,中断控制器会发送中断向量 Interrupt Vector 给 CPU。

对于每一个 CPU,都要求有一个 idt_table,里面存放了不同的中断向量的处理函数。中断向量表中已经填好了前 32 位,外加一位 32 位系统调用,其他的都是用于设备中断。

硬件中断的处理函数是 do_IRQ 进行统一处理,在这里会让中断向量,通过 vector_irq 映射为 irq_desc。

irq_desc 是一个用于描述用户注册的中断处理函数的结构,放在一个基数树里面,便于根据中断向量得到 irq_desc 结构

irq_desc 里面有一个成员是 irqaction,指向设备驱动程序里面注册的中断处理函数。

参考资料:

趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值