《Linux4.0设备驱动开发详解》笔记--第十章:Linux设备驱动中的中断与时钟

时间:2023-01-19 17:54:12

10.1 中断与定时器

  • 分类
    • 中断来源
      • 内部中断
        • 来源于CPU的内部(软件中断的指令、溢出、除法错误等)
      • 外部中断
      • 来源于外设请求
    • 是否可屏蔽
      • 可屏蔽中断
      • 不可屏蔽中断
    • 中断的入口方式
      • 向量中断
        • CPU给每个不同的中断分配不同的中断号,中断发生时会自动跳到该中断号对应的地址执行
      • 非向量中断
        • 多个中断共享一个入口地址,进入该入口后通过软件判断中具体哪个中断
    • ARM渡河处理器里最常用的中断控制器是GIC,它支持三种类型的中断
      • SGI:软件产生的中断,可用于多核间通信,一个CPU可以通过写GIC寄存器给另一个CPU产生中断
      • PPI:某个CPU私有外设的中断,这类外设中断只能发给一个CPU
      • SPI:共享外设中断,这类外设的中断可以路由到任何一个CPU
  • ARM Linux默认情况下,中断都是由CPU0上产生的

10.2 Linux的中断处理程序架构

  • 中断分层
    • 顶半部
      • 简单地读取寄存器中中断状态,并处理中断标志后就进行“登记中断”的工作
        • “登记中断”:将底半部放在该设备的底半部执行队列中,以加快顶半部的执行速度
    • 底半部
      • 处理中断程序的所有事情,可被中断打断
  • 中断要处理的任务较少时可以所有的处理任务放到顶部处理
  • cat /proc/interrupts
    • 获得中断的统计信息,并且能统计出每个中断号发生的次数

10.3 Linux中断编程

10.3.1 申请和释放中断

1、申请irq

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
  • request_irq()函数
    • 参数
      • irq:申请的硬件中断号
      • handler:向系统登记的中断处理函数,是一个回调函数
      • irqflags:中断处理属性
        • 中断触发方式
          • IRQ_TRIGGER_RISING
          • IRQF_TRIGGER_FALLING
          • IRQF_TRIGGER_HIGH
          • IRQF_TRIGGER_LOW
        • 中断处理方式
          • IRQF_shared:多个设备共享中断
      • dev:传递给中断服务程序的私有数据,一般为设备结构体或者NULL
    • 返回值
      • 成功:返回0
      • 失败:
        • 返回-EINVAL:中断号无效或者中断处理函数为NULL
        • 返回-EBUSY:中断已经被占用且不能共享
int devm_irq_irq(struct devcice* dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id);
  • devm_request_irq()函数
    • 与request_irq()函数区别:devm_开头的API申请的是内核“managed”的资源,一般不需要在出错的地方处理和在remove()接口里面在显示的释放

10.3.2 使能和屏蔽中断

  • 屏蔽一个中断源
    • disable_irq_nosync():立即返回
    • disable_irq():等待目前的中断处理完成
      • 因为desable_irq()函数会等待指定的中断被处理完成,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,这种情况下只能调用disable_irq_nosync(n)
void disable_irq_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
  • 屏蔽本CPU上的全部中断
    • local_irq_save()函数:会将中断的状态保留在flags中
      • flags为unsigned long类型,被直接传递,而不是通过指针
    • local_irq()函数直接禁止中断而不保存状态
#define local_irq_save(flag) ...
void local_irq_disable(void);
  • 恢复本CPU上的全部中断
#define local_irq-restore(flags) ...
void local_irq_enable(void);

10.3.3 底半部机制

Linux实现底半部机制主要有tasklet、工作队列、软中断和线程化irq

  • tasklet
    • tasklet执行的上下文是软中断,执行的时机是顶半部返回的时候
    • 实际的操作是:定义tasklet及其处理函数,并将两者关联
      • DECLARE_TASKLET:实现了定义名称为my_tasklet的tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data
    • 在调度tasklet的时候引用一个tasklet_schedule()函数
void my_tasklet_func(unsigned long);//定义一个处理函数

//定义一个tasklet结构my_tasklet, 与my_tasklet_func(data)函数相关联
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);

tasklet_schedule(&my_tasklet);
  • 工作队列
    • 使用方法和tasklet相似,如下:
struct work_struct my_wq; //定义一个工作队列

void my_wq_func(unsigned long); //定义一个处理函数

INIT_WORK(&my_wq, (void (*)(void *))my_wq_func, NULL); //初始化工作队列并将其与处理函数绑定

schedule_work(&my_irq);//系统在适当的时候需要调度时使用运行
  • 软中断
    • 使用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果
    • 软中断和tasklet仍然运行与中断上下文,而工作队列则运行于进程上下文,因此软中断和tasklet的处理函数不能休眠,但工作队列是可以的。
    • softirq_action结构体表征一个软中断
      • 这个结构体中包含软中断处理函数指针和传递给函数的参数
    • open_softirq()可以注册软中断对应的处理函数
    • raise_softirq()函数可以触发一个中断
    • local_bh_disable()和local_bh_enable()是内核用于禁止和使能软中断和tasklet底半部机制的函数。
  • threaded_irq
    • 内核申请中断的新函数
      • request_threaded_irq();
      • devm_request_threaded_irq();
        • 新的函数比request_irq()函数和devm_request_irq()函数多了个thread_fn参数
        • 这两个函数申请中断的时候,内核会为相应的中断号分配一个对应的内核线程,这个线程指针对这个中断号
        • 这两个函数支持在irqflags中设置IRQF_ONESHOT标志,这样内核会自动在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新是能该中断号
      • 参数
        • handler对应的函数执行于中断上下文
        • thread_fn参数对应的函数执行于内核线程
        • 如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度线程执行thread_fn对应的函数
        • handler参数可以设置为NULL,此时内核会默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标志
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char*name, void *dev);

int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsignd long irqflags, const char *devname, void *dev_id);

static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}

10.4 中断共享

  • 共享中断的多个设备申请中断时,都应该使用IRQF_SHARED标志,申请成功的前提:
    • 该中断未被申请
    • 该中断虽然被申请了,但是之前申请的该中断的所有设备也都以IRQF_SHARED标志申请该中断
  • 尽管内核模块可以访问的全局地址都可以作为request_irq(…,void *dev_id)的最后一个参数dev_id,但是设备结构体指针显示可传入的最佳参数
  • 中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某个函数返回IRQ_HANDLED
    • 在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE

10.5 内核定时器

10.5.1 内核定时器编程

1、timer_list结构体

  • 内核定时器的数据结构
    • 定时器期满后,function()成员将被执行
      • data成员是其传入的参数
      • expires成员是定时器的到期时间(jiffies)
struct timer_list {
struct list_head entry;

unsigned long expires;
void (*function)(unsigned long);
unsigned long data;

struct tvec_base *base;
/* ... */
};
  • 定义一个名为my_timer的定时器
struct timer_list my_timer;

2、初始化定时器

  • init_timer是一个宏,用来初始化timer_list的entry的next为NULL,并给base指针赋值
void init_timer(struct timer_list *timer);
  • TIMER_INITALIZER(_function,_expires, _data)宏用于赋值定时器结构体的function、expires、data和base成员
  • DEFINE_TIMER(_name,_function,_expires,_data)宏是定义并初始化定时器成员的“快捷方式”
  • setup_timer()也可以用于初始化定时器并赋值其成员

3、增加定时器

  • 注册内核定时器,将定时器加入到内核的动态定时器链表中
void add_timer(struct timer_list *timer);

4、删除定时器

  • del_timer_sync()是del_timer的同步版,在删除一个定时器时需要等待其被处理完,因此该函数的调用不能发生在中断上下文中
int del_timer(struct timer_list *timer);

5、修改定时器

  • mod_timer()函数修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数
int mod_timer(struct timer_list *timer, unsigned long expires);

6、定时器的使用流程

-----------使用定时器的步骤--------------
struct timer_list my_timer_list;//定义一个定时器,可以把它放在你的设备结构中 struct{定义一个定时器}
init_timer(&my_timer_list);//初始化一个定时器
my_timer_list.expire=jiffies+HZ;//定时器1s后运行服务程序
my_timer_list.function=timer_function;//定时器服务函数
add_timer(&my_timer_list);//添加定时器
void timer_function(unsigned long)//写定时器服务函数
del_timer(&my_timer_list);//当定时器不再需要时删除定时器
del_timer_sync(&my_timer_list);//基本和del_timer一样,比较适合在多核处理器使用,一般推荐使用del_timer_sync

10.6 内核延时

10.6.1 短延时

  • 内核提供的纳秒、微妙和毫秒的延时
    • 忙等待,根据CPU的频率进行一定次数的循环
    • 毫秒延时对内核来说是很大的,最好不要直接用,可以用替代性函数
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

//毫秒延时的替代性函数
vooid msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);

10.6.2 长延时

  • 比较当前的jiffies和目标jiffies,直到未来的jiffies到达目标jiffies
  • time_before()与time_after()函数:将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较
    • 为了防止在比较过程中编译器对jiffies的优化,内核将其定义为volatile变量,这将保证每次都会重新读取这个变量
//延迟100个jiffies
unsigned long delay = jiffies +100;
while(time_before(jiffies, delay));
//在延迟2s
unsigned long delay = jiffies + 2*Hz;
while(time_before(jiffies, delay));

10.6.3 睡着延时

  • 睡着延时:是在等待的时间到来之前进程处于睡眠状态,CPU资源被其他的进程使用
  • schedule_timeout()可以使当前任务休眠至指定的jiffies之后再重新被调用执行
    • schedule_timeout的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程
  • 下面函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠,当超时发生时,进程将它们唤醒
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t *q, unsignd long timeout);