linux内核中断详解

    科技2022-08-17  97

    linux内核中断详解

    1、中断的硬件触发流程

    外设:如果外设有操作或者有数据可用,那么就会产生一个电信号,这个电信号发送给中断控制器。 中断控制器:中断控制器接收到外设发来的电信号以后,进行进一步的处理,判断这个中断是否使能或者禁止,判断它的优先级等,如果需要发送给CPU一个信号,那么中断控制器就会给cup发送一个电信号。 CPU:CPU接收到中断控制器发送过来的电信号以后,CPU就会无条件跳转到异常向量表的入口,后续CPU就处理对应的中断。

    2、中断处理程序编写时的注意事项

    1.中断处理函数不隶属于任何进程,所以也就不参与进程之间的调度。 2.中断处理函数要求执行的速度要快,如果不快,其他进程无法获取CPU的资源,影响系统的并发能力。 3.中断处理函数不能与用户空间进行数据的往来!如果要想让中断给用户进行数据的往来,要配合系统调用函数(file_operations) 4.在中断处理函数中,不能调用引起休眠的函数 copy_to_user, copy_from_user, 因为这些函数会进行内存的拷贝, 在拷贝的时候,有可能空闲页不够,就会引起拷贝过程进入休眠状态,等待空闲页出现!

    3、共享中断

    共享中断:在硬件上,多个外设接在一个中断线上,中断线接在中断控制器上。 每个设备都有自己的驱动程序,每个设备的驱动程序里面都会调用request_irq来注册中断 ,但是在注册时使用的中断号都是相同的。所以在注册中断时,一定要将中断标志或上IRQ_SHARED表示这个中断是共享的,同时dev必须是唯一的(如果中断不用时,释放中断时,由于中断号是相同的,通过dev来区分不同的中断)。如果没有使用这个宏,那么一个驱动使用这个中断号,其他的驱动就无法使用。 对于共享中断,如果中断信号来了以后,其对应的中断处理程序均会执行。因此如果使用共享中断,硬件必须支持能够判断中断是否是自己发出的(例如硬件中包含有中断状态寄存器,就可以在中断处理程序中通过中断状态寄存器的状态,判断中断是否是自己发出的)。

    4、中断标志

    标志描述IRQF_SHARED多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话, request_irq函数的 dev参数就是唯一区分他们的标志。IRQF_ONESHOT单次中断,中断执行一次就 结束。即保证中断在底半部执行完之后再打开中断功能,开始接受中断。IRQF_TRIGGER_NONE无触发。IRQF_TRIGGER_RISING上升沿触发。IRQF_TRIGGER_FALLING下降沿触发。IRQF_TRIGGER_HIGH高电平触发。IRQF_TRIGGER_LOW低电平触发。IRQF_NO_SUSPEND在系统suspend的时候,不用disable这个中断,如果disable,可能会导致系统不能正常的resume。IRQF_NO_THREAD有些low level的interrupt是不能线程化的(例如系统timer的中断),这个flag就是起这个作用的。

    5、顶半部与底半部

    在某些场合,中断处理函数有可能会处理相对比较耗时,比较多的事情,就会长时间的占有CPU的资源,对系统的并发能力和响应能力有很大的影响,比如网卡的数据包的读取过程。为了解决这个问题,内核将中断处理程序分为顶半部和底半部两个部分。 在顶半部里处理优先级比较高的事情,要求占用中断时间尽量的短,还要登记底半部的事情,在处理完成后,就激活底半部,由底半部处理其余任务。顶半部其实就是中断处理函数,这个过程不可中断! 底半部的处理方式主要有soft_irq, tasklet, workqueue三种,他们在使用方式和适用情况上各有不同。做相对比较耗时,不紧急的事情,这个过程可被中断。 ①、如果要处理的内容不希望被其他中断打断,那么可以放到顶半部。 ②、如果要处理的任务对时间敏感,可以放到顶半部。 ③、如果要处理的任务与硬件有关,可以放到顶半部 ④、除了上述三点以外的其他任务,优先考虑放到底半部。

    6、底半部机制

    6.1 软中断

    soft_irq用在对底半部执行时间要求比较紧急或者非常重要的场合,主要为一些subsystem用,一般driver基本上用不上。软中断的优先级低于硬件中断,高于普通的进程。

    6.2 tasklet

    tasklet是基于软中断实现,它执行的上下文是软中断。它们之间的区别在于同一个tasklet同时一刻只能在一个CPU上运行,但对于软中断,同一时刻可以在多个CPU上执行,这时候在设计软中断的处理函数时,要求其函数具有可重入性(尽量避免使用全局变量,如果使用全局变量,记得要进行互斥访问的保护)。并且软中断的实现必须静态编译,不能采用模块化。相同点是它们都是工作在中断上下文中,不能做休眠的动作 。 tasklet的结构体定义在include\linux\interrupt.h中

    // interrupt.h struct tasklet_struct { struct tasklet_struct *next; //下一个tasklet unsigned long state; //tasklet状态 atomic_t count; //计数器,记录tasklet的引用数 void (*func)(unsigned long); //tasklet执行的函数 unsigned long data; //函数func 的参数,可以存放普通的整形变量值,也可以存放指针,一般多存放指针 };

    如果要使用 tasklet,必须先定义一个 tasklet,然后使用tasklet_init初始化tasklet。

    void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);

    @t:要初始化的 tasklet @func: tasklet的处理函数。 @data 要传递给 func函数的参数 或者使用DECLARE_TASKLET一次性完成tasklet的定义和初始化。

    DECLARE_TASKLET(name, func, data)

    其中 name为要定义的 tasklet名字,这个名字就是一个 tasklet_struct类型的变量, func就是 tasklet的处理函数, data是传递给 func函数的参数。 在顶半部,也就是中断处理函数中调用 tasklet_schedule函数登记tasklet的工作,就能使 tasklet在合适的时间运行, tasklet_schedule函数原型如下:

    void tasklet_schedule(struct tasklet_struct *t)

    @t:要调度的 tasklet,也就是 DECLARE_TASKLET宏里面的 name。 tasklet的使用流程为 1、分配tasklet结构体。 struct tasklet_struct my_tasklet; 2、初始化tasklet tasklet_init(&my_tasklet, tasklet_func, data) @或者 DECLARE_TASKLET( my_tasklet, tasklet_func, data); 3、在中断顶半部登记tasklet tasklet_schedule(&my_tasklet)

    6.3 工作队列

    tasklet和work queue在普通的driver里用的相对较多,主要区别是tasklet是在中断上下文执行不能引起休眠,而work queue是在process上下文,因此可以执行可能sleep的操作。 工作结构体定义在include\linux\workqueue.h中

    struct work_struct { atomic_long_t data; //记录工作状态和指向工作者线程的指针 struct list_head entry; //工作数据链成员 work_func_t func; //工作处理函数 #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif }; struct delayed_work { struct work_struct work; //工作结构体 struct timer_list timer; //推后执行的定时器 /* target workqueue and CPU ->timer uses to queue ->work */ struct workqueue_struct *wq; int cpu; }; //处理延时执行的工作的结构体

    工作队列结构体定义在kernel\workqueue.c中

    struct workqueue_struct { struct list_head pwqs; /* WR: all pwqs of this wq */ struct list_head list; /* PR: list of all workqueues */ struct mutex mutex; /* protects this wq */ int work_color; /* WQ: current work color */ int flush_color; /* WQ: current flush color */ atomic_t nr_pwqs_to_flush; /* flush in progress */ struct wq_flusher *first_flusher; /* WQ: first flusher */ struct list_head flusher_queue; /* WQ: flush waiters */ struct list_head flusher_overflow; /* WQ: flush overflow list */ struct list_head maydays; /* MD: pwqs requesting rescue */ struct worker *rescuer; /* MD: rescue worker */ int nr_drainers; /* WQ: drain in progress */ int saved_max_active; /* WQ: saved pwq max_active */ struct workqueue_attrs *unbound_attrs; /* PW: only for unbound wqs */ struct pool_workqueue *dfl_pwq; /* PW: only for unbound wqs */ #ifdef CONFIG_SYSFS struct wq_device *wq_dev; /* I: for sysfs interface */ #endif #ifdef CONFIG_LOCKDEP char *lock_name; struct lock_class_key key; struct lockdep_map lockdep_map; #endif char name[WQ_NAME_LEN]; /* I: workqueue name */ /* * Destruction of workqueue_struct is RCU protected to allow walking * the workqueues list without grabbing wq_pool_mutex. * This is used to dump all workqueues from sysrq. */ struct rcu_head rcu; /* hot fields used during command issue, aligned to cacheline */ unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */ struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */ struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */ };

    Linux内核使用工作者线程 (worker thread)来处理工作队列中的各个工作, Linux内核使用worker结构体表示工作者线程。

    struct worker { /* on idle list while idle, on busy hash table while busy */ union { struct list_head entry; /* L: while idle */ struct hlist_node hentry; /* L: while busy */ }; struct work_struct *current_work; /* L: work being processed */ work_func_t current_func; /* L: current_work's fn */ struct pool_workqueue *current_pwq; /* L: current_work's pwq */ struct list_head scheduled; /* L: scheduled works */ /* 64 bytes boundary on 64bit, 32 on 32bit */ struct task_struct *task; /* I: worker task */ struct worker_pool *pool; /* A: the associated pool */ /* L: for rescuers */ struct list_head node; /* A: anchored at pool->workers */ /* A: runs through worker->node */ unsigned long last_active; /* L: last active timestamp */ unsigned int flags; /* X: flags */ int id; /* I: worker id */ int sleeping; /* None */ /* * Opaque string set with work_set_desc(). Printed out with task * dump for debugging - WARN, BUG, panic or sysrq. */ char desc[WORKER_DESC_LEN]; /* used only by rescuers to point to the target workqueue */ struct workqueue_struct *rescue_wq; /* I: the workqueue to rescue */ /* used by the scheduler to determine a worker's last known identity */ work_func_t last_func; };

    可以看出,每个 worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作 (work_struct)即可,关于工作队列和工作者线程我们基本不用去管。 如果要使用工作队列 ,首先需要定义一个work_struct结构体变量,然后使用 INIT_WORK宏来初始化工作。

    #define INIT_WORK(_work, _func) \ __INIT_WORK((_work), (_func), 0) #define INIT_DELAYED_WORK(_work, _func) \ __INIT_DELAYED_WORK(_work, _func, 0)

    @_work:要初始化的工作 @_func: 工作对应的处理函数 也可以使用DECLARE_WORK宏一次性完成工作的创建和初始化。

    #define DECLARE_WORK(n, f) \ struct work_struct n = __WORK_INITIALIZER(n, f) #define DECLARE_DELAYED_WORK(n, f) \ struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)

    @n:定义的工作work_struct @f:工作对应的处理函数 和 tasklet一样,工作也是需要登记才能调度运行,工作的登记函数为 schedule_work

    static inline bool schedule_work(struct work_struct *work) { return queue_work(system_wq, work); } static inline bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay) { return queue_delayed_work(system_wq, dwork, delay); }

    @work:要调度的工作 @delay:延时调度的延时时间 工作的使用流程为 1、分配work_struct结构体 struct work_struct my_work; struct delayed_work_struct my_dwork; 2、初始化工作 INIT_WORK(&my_work, my_work_func); INIT_DELAYED_WORK(&my_dwork, my_dwork_func); @或者 DECLARE_WORK(my_work, my_work_func); DECLARE_DELAYED_WORK(my_dwork, my_dwork_func) 3、在顶半部中断中登记工作 schedule_work(&my_work) schedule_delayed_work(&my_dwork, times)

    在Linux kernel中,一个外设的中断处理被分成top half和bottom half,top half进行最关键,最基本的处理,而比较耗时的操作被放到bottom half(softirq、tasklet)中延迟执行。虽然bottom half被延迟执行,但始终都是先于进程执行的。为何不让这些耗时的bottom half和普通进程公平竞争呢?因此,linux kernel借鉴了RTOS的某些特性,对那些耗时的驱动interrupt handler进行线程化处理,在内核的抢占点上,让线程(无论是内核线程还是用户空间创建的线程,还是驱动的interrupt thread)在一个舞台上竞争CPU。

    6.4 线程化irq

    7、API函数

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

    在 Linux内核中要想使用某个中断是需要申请的, request_irq函数用于申请中断, request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq函数。 request_irq函数会激活 (使能 )中断,所以不需要我们手动去使能中断。 @irq:要申请中断的中断号。 @handler:中断处理函数,当中断发生以后就会执行此中断处理函数。 @flags:中断标志,可以在文件 include/linux/interrupt.h里面查看所有的中断标志。 @name:中断名字,设置以后可以在 /proc/interrupts文件中看到对应的中断名字。 @dev 如果将 flags设置为 IRQF_SHARED的话, dev用来区分不同的中断,一般情况下将dev设置为设备结构体, dev会传递给中断处理函数 irq_handler_t的第二个参数。 @返回值: 0 中断申请成功,其他负值 中断申请失败,如果返回 -EBUSY的话表示中断已经被申请了。

    void free_irq(unsigned int irq, void *dev)

    使用中断的时候需要通过 request_irq函数申请,使用完成以后就要通过 free_irq函数释放掉相应的中断。如果中断不是共享的,那么 free_irq会删除中断处理函数并且禁止中断。 @irq: 要释放的中断。 @dev:如果中断设置为共享 (IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。

    irqreturn_t (*irq_handler_t) (int, void *)

    使用 request_irq函数申请中断的时候需要设置中断处理函数。 第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void的指针,也就是个通用指针,需要与 request_irq函数的 dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。

    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) 输入参数描述irq要注册handler的那个IRQ number。这里要注册的handler包括两个,一个是传统意义的中断handler,我们称之primary handler(类似于顶半部),另外一个是threaded interrupt handler(类似于底半部)handlerprimary handler。需要注意的是primary handler和threaded interrupt handler不能同时为空,否则会出错。如果该函数结束时,返回的是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。该参数可以设置为空,这种情况下内核会默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。thread_fnthreaded interrupt handler。如果该参数不是NULL,那么系统会创建一个kernel thread,调用的function就是thread_fnirqflags参见本章第4节中断标志,这里支持设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。对于我们无法在顶半部清楚中断的情况,IRQF_ONESHOT特别有用。devname请求中断的设备名称dev_id传递给中断处理函数handler的参数,如果是使用IRQF_SHARED时,用于区分不同的中断。 int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) { struct irq_devres *dr; int rc; dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres), GFP_KERNEL); if (!dr) return -ENOMEM; if (!devname) devname = dev_name(dev); rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname, dev_id); if (rc) { devres_free(dr); return rc; } dr->irq = irq; dr->dev_id = dev_id; devres_add(dev, dr); return 0; }

    从函数实现可以看出,该函数的实现是通过request_threaded_irq实现的。该函数与request_threaded_irq的区别在于,该函数在驱动程序分离时,能够自动释放中断线。 如果释放该函数分配的中断线需要使用devm_free_irq

    void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id) /** * disable_irq - disable an irq and wait for completion * @irq: Interrupt to disable * * Disable the selected interrupt line. Enables and Disables are * nested. * This function waits for any pending IRQ handlers for this interrupt * to complete before returning. If you use this function while * holding a resource the IRQ handler may need you will deadlock. * * This function may be called - with care - from IRQ context. */ void disable_irq(unsigned int irq) { if (!__disable_irq_nosync(irq)) synchronize_irq(irq); } EXPORT_SYMBOL(disable_irq);

    如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,在这种情况下只能调用disable_irq_nosync(n)。

    /** * disable_irq_nosync - disable an irq without waiting * @irq: Interrupt to disable * * Disable the selected interrupt line. Disables and Enables are * nested. * Unlike disable_irq(), this function does not ensure existing * instances of the IRQ handler have completed before returning. * * This function may be called from IRQ context. */ void disable_irq_nosync(unsigned int irq) { __disable_irq_nosync(irq); } /** * enable_irq - enable handling of an irq * @irq: Interrupt to enable * * Undoes the effect of one call to disable_irq(). If this * matches the last disable, processing of interrupts on this * IRQ line is re-enabled. * * This function may be called from IRQ context only when * desc->irq_data.chip->bus_lock and desc->chip->bus_sync_unlock are NULL ! */ void enable_irq(unsigned int irq) { unsigned long flags; struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, IRQ_GET_DESC_CHECK_GLOBAL); if (!desc) return; if (WARN(!desc->irq_data.chip, KERN_ERR "enable_irq before setup/request_irq: irq %u\n", irq)) goto out; __enable_irq(desc); out: irq_put_desc_busunlock(desc, flags); }

    8 request_threaded_irq分析

    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; if (irq == IRQ_NOTCONNECTED) return -ENOTCONN; /* * Sanity-check: shared interrupts must 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). * * Also IRQF_COND_SUSPEND only makes sense for shared interrupts and * it cannot be set along with IRQF_NO_SUSPEND. */ if (((irqflags & IRQF_SHARED) && !dev_id) || (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) || ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND))) return -EINVAL; desc = irq_to_desc(irq); if (!desc) return -EINVAL; if (!irq_settings_can_request(desc) || WARN_ON(irq_settings_is_per_cpu_devid(desc))) return -EINVAL; if (!handler) { if (!thread_fn) return -EINVAL; handler = irq_default_primary_handler; } action = kzalloc(sizeof(struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM; action->handler = handler; action->thread_fn = thread_fn; action->flags = irqflags; action->name = devname; action->dev_id = dev_id; retval = irq_chip_pm_get(&desc->irq_data); if (retval < 0) { kfree(action); return retval; } retval = __setup_irq(irq, desc, action); if (retval) { irq_chip_pm_put(&desc->irq_data); kfree(action->secondary); kfree(action); } #ifdef CONFIG_DEBUG_SHIRQ_FIXME if (!retval && (irqflags & IRQF_SHARED)) { /* * It's a shared IRQ -- the driver ought to be prepared for it * to happen immediately, so let's make sure.... * We disable the irq to make sure that a 'real' IRQ doesn't * run in parallel with our fake. */ unsigned long flags; disable_irq(irq); local_irq_save(flags); handler(irq, dev_id); local_irq_restore(flags); enable_irq(irq); } #endif return retval; } EXPORT_SYMBOL(request_threaded_irq);

    首先是判断传入参数的安全性

    if (((irqflags & IRQF_SHARED) && !dev_id) || (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) || ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND))) return -EINVAL;

    如果设置了IRQF_SHARED但是没有传入dev_id时,会直接返回错误,因为这样的设置在释放中断函数的时候回出错。

    desc = irq_to_desc(irq);

    通过中断号获取中断描述符。在linux kernel中,对于每一个外设的IRQ都用struct irq_desc来描述,我们称之中断描述符(struct irq_desc)。linux kernel中会有一个数据结构保存了关于所有IRQ的中断描述符信息,我们称之中断描述符DB。当发生中断后,首先获取触发中断的HW interupt ID,然后通过irq domain翻译成IRQ number,然后通过IRQ number就可以获取对应的中断描述符。调用中断描述符中的highlevel irq-events handler来进行中断处理就OK了。

    if (!irq_settings_can_request(desc) || WARN_ON(irq_settings_is_per_cpu_devid(desc))) return -EINVAL;

    并非系统中所有的IRQ number都可以request,有些中断描述符被标记为IRQ_NOREQUEST,标识该IRQ number不能被其他的驱动request。一般而言,这些IRQ number有特殊的作用,例如用于级联的那个IRQ number是不能request。irq_settings_can_request函数就是判断一个IRQ是否可以被request。

    if (!handler) { if (!thread_fn) return -EINVAL; handler = irq_default_primary_handler;

    这里是对传入的两个函数进行判断

    primary handlerthreaded handler描述NULLNULL函数出错,返回-EINVAL设定设定正常流程。中断处理被合理的分配到primary handler和threaded handler中。设定NULL中断处理都是在primary handler中完成NULL设定这种情况下,系统会帮忙设定一个default的primary handler:irq_default_primary_handler,协助唤醒threaded handler线程 action = kzalloc(sizeof(struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM; action->handler = handler; action->thread_fn = thread_fn; action->flags = irqflags; action->name = devname; action->dev_id = dev_id; retval = irq_chip_pm_get(&desc->irq_data); if (retval < 0) { kfree(action); return retval; } retval = __setup_irq(irq, desc, action);

    这部分的代码很简单,分配struct irqaction,赋值,调用__setup_irq进行实际的注册过程。

    Processed: 0.020, SQL: 9