linux-中断杂谈

1.中断向量表

        我们常用的就是复位中断和 IRQ 中断。

2.GIC控制器

        GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,目前 GIC 有 4 个版本:V1~V4,V1 是最老的版本,已经被废弃了。V2~V4 目前正在大量的使用。GIC V2 是给 ARMv7-A 架构使用的,比如 Cortex-A7、Cortex-A9、Cortex-A15 等,V3 和 V4 是给 ARMv8-A/R 架构使用的,也就是 64 位芯片使用的。

 gic控制器的逻辑如下:

 gic的中断源分为三类

(1)SPI(shared peripheral interrupt)共享中断,最常见的,所有核共享的中断,比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。

(2)PPI(private peripheral interrupt) 私有中断,核自己独有的中断,这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。

(3)SGI(software generated interrupt)软件中断,有软件出发的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信

3.中断ID

ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。

ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断 ,至于
具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了.

4.gic逻辑分块

Distributor( 分发器端)

①、全局中断使能控制。
②、控制每一个中断的使能或者关闭。
③、设置每个中断的优先级。
④、设置每个中断的目标处理器列表。
⑤、设置每个外部中断的触发模式:电平触发或边沿触发。
⑥、设置每个中断属于组 0 还是组 1。

CPU Interface(CPU  接口端)

①、使能或者关闭发送到 CPU Core 的中断请求信号。

②、应答中断。
③、通知中断处理完成。
④、设置优先级掩码,通过掩

码来设置哪些中断不需要上报给 CPU Core。
⑤、定义抢占策略。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。

5.中断API

        每个中断都有一个中断号,通过中断号即可区分不同的中断。常用的中断的API有如下:

申请int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev)
释放void free_irq(unsigned int irq,void *dev)
使能void enable_irq(unsigned int irq)/local_irq_enable()
禁止void disable_irq(unsigned int irq)/local_irq_disable()

6.设备树中的中断配置

 gic: interrupt-controller@00a01000 {
 compatible = "arm,cortex-a7-gic";
 #interrupt-cells = <3>;
 interrupt-controller;
 reg = <0x00a01000 0x1000>,
 <0x00a02000 0x100>;
 };

        #interrupt-cells 和#address-cells、#size-cells 一样。表示此中断控制器下设备的 cells大小,对于设备而言,会使用 interrupts 属性描述中断信息,#interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个 cells 都是 32 位整形值,对于 ARM 处理的GIC 来说,一共有 3 个 cells,这三个 cells 的含义如下:
第一个 cells:中断类型,0 表示 SPI 中断,1 表示 PPI 中断。
第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断
号的范围为 0~15。
第三个 cells:标志,表示中断触发类型,上升沿触发,下降沿触发,高电平触发,低电平触发。

        interrupt-controller 节点为空,表示当前节点是中断控制器。
        对于 gpio 来说,gpio 节点也可以作为中断控制器。

 gpio0: gpio@020ac000 {
 compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
 reg = <0x020ac000 0x4000>;
 interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
 gpio-controller;
 #gpio-cells = <2>;
 interrupt-controller;
 #interrupt-cells = <2>;
 };

fxls8471@1e {
 compatible = "fsl,fxls8471";
 reg = <0x1e>;
 position = <0>;
 interrupt-parent = <&gpio0>;
 interrupts = <0 8>;
 };

        如果在设备树中获取中断号,可以通过 irq_of_parse_and_map函数从 interupts 属性中提取到对应的设备号,如果是gpio的话,使用gpio_to_irq 函数来获取 gpio 对应的中断号。

7.中断的上半部与下半部

        我们在使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。中断处理过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。

①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。

②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部。
④、除了上述三点以外的其他任务,优先考虑放到下半部。

Linux 内核提供了多种下半部机制,有软中断,tasklet ,工作队列

注:如果你要推后的工作可以睡眠那么就可以选择工作队列,上图biao'zh

 

8.软中断

        softirq机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个CPU负责执行它所触发的软中断,而且每个CPU都有它自己的软中断触发与控制机制。--(这里是说此次触发的中断处理的整个过程必须由这个cpu执行,但是如果再次触发的软中断,甚至相同的软中断类型不一定是同一个cpu执行,由于这点,所以对临界区需要加锁保护

Linux 内核使用结构体 softirq_action 表示软中断,结构体定义在文件 include/linux/interrupt.h 中,

struct softirq_action {
 void (*action)(struct softirq_action *); 
};

总共有10个软中断,定义如下:

static struct softirq_action softirq_vec[NR_SOFTIRQS];

enum
{
HI_SOFTIRQ=0, /* 高优先级软中断 */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* tasklet 软中断 */
SCHED_SOFTIRQ, /* 调度软中断 */
HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
RCU_SOFTIRQ, /* RCU 软中断 */
NR_SOFTIRQS
}

操作流程如下:

/*要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数*/

void open_softirq(int nr , void(*action)(struct softirq_action*));
/*
nr:要开启的软中断,在示例代码 enum 中选择一个。
action:软中断对应的处理函数。
返回值: 没有返回值。
*/

/*注册好软中断以后需要通过 raise_softirq 函数触发, raise_softirq 函数原型如下:
nr:要触发的软中断,在示例代码 enum 中选择一个。
*/
void raise_softirq(unsigned int nr) ;

/*软中断必须在编译的时候静态注册! Linux 内核使用 softirq_init 函数初始化软中断*/

void __init softirq_init(void)
 {
      int cpu;
 
      for_each_possible_cpu(cpu) {
          per_cpu(tasklet_vec, cpu).tail =
          &per_cpu(tasklet_vec, cpu).head;
          per_cpu(tasklet_hi_vec, cpu).tail =
          &per_cpu(tasklet_hi_vec, cpu).head;
      }
 
      open_softirq(TASKLET_SOFTIRQ, tasklet_action);
      open_softirq(HI_SOFTIRQ, tasklet_hi_action);
  }

注册软中断 open_softirq

触发软中断 raise_softirq

执行软中断 do_softirq

asmlinkage void do_softirq(void)
 
{
 
    __u32 pending;
 
    unsigned long flags;
 
    /* 判断是否在中断处理中,如果正在中断处理,就直接返回 */
    
    if (in_interrupt())
 
        return;
 
    /* 保存当前寄存器的值 */
 
    local_irq_save(flags);
 
    /* 取得当前已注册软中断的位图 */
 
    pending = local_softirq_pending();
 
    /* 循环处理所有已注册的软中断 */
 
    if (pending)
 
    __do_softirq();
 
    /* 恢复寄存器的值到中断处理前 */
    
    local_irq_restore(flags); 
}

        代码之中第一次就判断是否在中断处理中,如果在立刻退出函数。这说明了什么?说明了如果有其他软中断触发,执行到此处由于先前的软中断已经在处理,则其他软中断会返回。所以,软中断不能被另外一个软中断抢占!唯一可以抢占软中断的是中断处理程序,所以软中断允许响应中断。虽然不能在本处理器上抢占,但是其他的软中断甚至同类型可以再其他处理器上同时执行。由于这点,所以对临界区需要加锁保护。

        软中断留给对时间要求最严格的下半部使用。目前只有网络,内核定时器和 tasklet 建立在软中断上。 

9.tasklet

tasklet机制是一种较为特殊的软中断。

       tasklet一词的原意是“小片任务”的意思,这里是指一小段可执行的代码,且通常以函数的形式出现。软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet机制来实现的。
  从某种程度上讲,tasklet机制是Linux内核对BH机制的一种扩展。在2.4内核引入了softirq机制后,原有的BH机制正是通过tasklet机制这个桥梁来将softirq机制纳入整体框架中的。正是由于这种历史的延伸关系,使得tasklet机制与一般意义上的软中断有所不同,而呈现出以下显著的特点:

  与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,而不像一般的软中断服务函数(即softirq_action结构中的action函数指针)那样——在同一时刻可以被多个CPU并发地执行。

        tasklet 由于是基于软中断实现的,所以也允许响应中断。但不能睡眠(我认为不能睡眠原因是它们内部有 spin lock)。
描述tasklet的结构体是tasklet_struct,

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 的参数 */
};

        func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:

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_schedule 函数原型如下

void tasklet_schedule(struct tasklet_struct *t)
 /*函数参数和返回值含义如下:
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。*/

使能与禁止tasklet

static inline void tasklet_disable(struct tasklet_struct *t)
{
	tasklet_disable_nosync(t);
	tasklet_unlock_wait(t);
	smp_mb();
}
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
	atomic_inc(&t->count);
	smp_mb__after_atomic_inc();
}
static inline void tasklet_enable(struct tasklet_struct *t)
{
	smp_mb__before_atomic_dec();
	atomic_dec(&t->count);
}

关于 tasklet 的参考使用示例如下所示:

/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}

10.工作队列

        工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。        

        Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译):

struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};

        这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下(省略掉条件编译):

struct workqueue_struct {
struct list_head pwqs;
struct list_head list;
struct mutex mutex;
int work_color;
int flush_color;
atomic_t nr_pwqs_to_flush;
struct wq_flusher *first_flusher;
struct list_head flusher_queue;
struct list_head flusher_overflow;
struct list_head maydays;
struct worker *rescuer;
int nr_drainers;
int saved_max_active;
struct workqueue_attrs *unbound_attrs;
struct pool_workqueue *dfl_pwq;
char name[WQ_NAME_LEN];
struct rcu_head rcu;
unsigned int flags ____cacheline_aligned;
struct pool_workqueue __percpu *cpu_pwqs;
struct pool_workqueue __rcu *numa_pwq_tbl[];
};

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

struct worker {
union {
struct list_head entry;
struct hlist_node hentry;
};
struct work_struct *current_work;
work_func_t current_func;
struct pool_workqueue *current_pwq;
bool desc_valid;
struct list_head scheduled;
struct task_struct *task;
struct worker_pool *pool;
struct list_head node;
unsigned long last_active;
unsigned int flags;
int id;
char desc[WORKER_DESC_LEN];
struct workqueue_struct *rescue_wq;
};

每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:
        #define INIT_WORK(_work, _func) 
_work 表示要初始化的工作,_func 是工作对应的处理函数。也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
         #define DECLARE_WORK(n, f)
和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原
型如下所示:
        bool schedule_work(struct work_struct *work)
关于工作队列的参考使用示例如下所示:

/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */

void testwork_func_t(struct work_struct *work);
{
/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}

(47条消息) 中断处理“下半部”机制_Arrow的博客-CSDN博客-----好文推荐

11.各种机制的比较

下半部上下文顺序执行保障
软中断中断随意,同类型都可以在不同处理器同时执行
tasklet中断同类型不能同时执行
工作队列进程不保障,可能被调度和抢占

选择: 
- 工作队列:适用于任务推后到进程上下文完成,可以休眠 
- tasklet:任务接口简单,支持中断响应,但有同种类型不能同时执行的限制 
- 软中断:提供的执行顺序保障最少,支持中断响应,由于同类型软中断在其他处理器可同时执行,所以要采取措施保护共享数据。

以下彩蛋------------------------

中断中为何不能使用信号量?中断上下为何不能睡眠?

信号量会导致睡眠

中断发生以后,CPU跳到内核设置好的中断处理代码中去,由这部分内核代码来处理中断。这个处理过程中的上下文就是中断上下文。
     为什么可能导致睡眠的函数都不能在中断上下文中使用呢? 首先睡眠的含义是将进程置于“睡眠”状态,在这个状态的进程不能被调度执行。然后,在一定的时机,这个进程可能会被重新置为“运行”状态,从而可能被调度执行。 可见,“睡眠”与“运行”是针对进程而言的,代表进程的task_struct结构记录着进程的状态。内核中的“调度器”通过task_struct对进程进行调度。
     但是,中断上下文却不是一个进程,它并不存在task_struct,所以它是不可调度的。所以,在中断上下文就不能睡眠。
     那么,中断上下文为什么不存在对应的task_struct结构呢? 
     中断的产生是很频繁的(至少每毫秒(看配置,可能10毫秒或其他值)会产生一个时钟中断),并且中断处理过程会很快。如果为中断上下文维护一个对应的task_struct结构,那么这个结构频繁地分配、回收、并且影响调度器的管理,这样会对整个系统的吞吐量有所影响。
     但是在某些追求实时性的嵌入式linux中,中断也可能被赋予task_struct结构。这是为了避免大量中断不断的嵌套,导致一段时间内CPU总是运行在中断上下文,使得某些优先级非常高的进程得不到运行。这种做法能够提高系统的实时性,但是代价中吞吐量的降低

中断上半部,下半部理解 - 坚持,每天进步一点点 - 博客园 (cnblogs.com)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值