linux内核中断(二)

一、linux中断处理机制

1、最简单的中断机制

最简单的中断机制就是像芯片手册上讲的那样,在中断向量表中填入跳转到对应处理函数的指令,然后在处理函数中实现需要的功能。类似下图:

这种方式在原来的单片机课程中常常用到,一些简单的单片机系统也是这样用。它的好处很明显,简单,直接。

2、上下半部机制

中断处理函数所作的第一件事情是什么?答案是屏蔽中断(或者是什么都不做,因为常常是如果不清除IF位,就等于屏蔽中断了),当然只屏蔽同一种中断。之所以要屏蔽中断,是因为新的中断会再次调用中断处理函数,导致原来中断处理现场的破坏。即,破坏了 interrupt context。

随着系统的不断复杂,中断处理函数要做的事情也越来越多,多到都来不及接收新的中断了。于是发生了中断丢失,这显然不行,于是产生了新的机制:分离中断接收与中断处理过程。中断接收在屏蔽中断的情况下完成;中断处理在时能中断的情况下完成,这部分被称为中断下半部。

从上图中看,只看int0的处理。Func0为中断接收函数。中断只能简单的触发func0,而func0则能做更多的事情,它与funcA之间可以使用队列等缓存机制。当又有中断发生时,func0被触发,然后发送一个中断请求到缓存队列,然后让funcA去处理。
由于func0做的事情是很简单的,所以不会影响int0的再次接收。而且在func0返回时就会使能int0,因此funcA执行时间再长也不会影响int0的接收。

二、常用的下半部处理机制

1、软中断(Softirq)

是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,运行于软中断上下文。

Linux内核使用结构体softirq_action表示软中断,softirq_action 结构体定义在文件

include/linux/interrupt.h 中,内容如下:

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

kernel/softirq.c 文件中一共定义了 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 
};//定义在文件 include/linux/interrupt.h 中

要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,open_softirq 函数原型如下

void open_softirq(int nr, void (*action)(struct softirq_action *));
nr :要开启的软中断
action :软中断对应的处理函数

注册好软中断以后需要通过 raise_softirq 函数触发,raise_softirq 函数原型如下

void raise_softirq(unsigned int nr);

软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断, softirq_init 函数 定义在 kernel/softirq.c 文件里面,softirq_init 函数默认会打开 TASKLET_SOFTIRQ HI_SOFTIRQ

2、tasklet

基本原理

tasklet 是通过软中断实现的,所以它本身也是软中断。软中断用轮询的方式处理,假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。显然当年开发人员为了保证轮询的效率,于是限制中断个数为32个。

为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制。

  • Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。
  • 无类型数量限制,效率高,无需循环查表,支持 SMP 机制,
  • 一种特定类型的 tasklet只能运行在一个 CPU 上,不能并行,只能串行执行。 多个不同类型的 tasklet 可以并行在多个 CPU 上。
  • 软中 断是静态分配的,在内核编译好之后,就不能改变。但 tasklet 就灵活许多,可以在运行时改变(比如添加模块时)。

使用方法

如果要使用 tasklet,必须先定义一个 tasklet结构体,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:


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

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 定义在 include/linux/interrupt.h 文件中,定义如下:

DECLARE_TASKLET(name, func, data)

其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的变量,func 就是

tasklet 的处理函数,data 是传递给 func 函数的参数。

在需要调度 tasklet 的时候引用一个 tasklet_schedule()函数就能使系统在适当的时候进行调度运行:

void tasklet_schedule(struct tasklet_struct *t)

t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name

使用示例

/* 定义 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); 
    ...... 
}

/* 驱动出口函数 */ 
static int __exit xxxx_exit(void) 
{ 
    ...... 
    /* 释放中断资源 */ 
    free_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); 
    /* 释放 tasklet */ 
    tasklet_kill(&testtasklet); 
    ...... 
}

3、工作队列

基本原理

前面的机制不论如何折腾,有一点是不会变的。它们都在中断上下文中。什么意思?说明它们不可挂起。也就是说软中断不能睡眠、不能阻塞,原因是由于中断上下文处于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。

而且由于是串行执行,因此只要有一个处理时间较长,则会导致其他中断响应的延迟。为了完成这些不可能完成的任务,于是出现了工作队列。工作队列说白了就是一组内核线程,作为中断守护线程来使用。多个中断可以放在一个线程中,也可以每个中断分配一个线程。工作队列对线程作了封装,使用起来更方便。因为工作队列是线程,所以我们可以使用所有可以在线程中使用的方法。

工作队列结构原理
work_struct, workqueue_struct, cpu_workqueue_struct三者之间关系如下:

  • 内核启动时会为每一个CPU创建一个cpu_workqueue_struct结构,同时还会创建名为kworker/u:x(x0开始的整数,表示CPU编号)工作者内核线程,该线程创建之后处于sleep状态,这个线程创建好后处于睡眠状态,等待用户加入工作,来唤醒线程去调度工作结构体 work_struct 中的工作函数。

  • 工作work_struct 是通过链表连接在cpu_workqueue_struct上(delayed_works成员),后面其他work_struct连接在前一个后面,组成一个队列

  • 工作者线程被唤醒后,会去自己负责的工作队列上依次执行上面struct_work结构中的工作函数,执行完成后就会把 work_struct 从链表上删除

  • 如果想使用工作队列来延后执行一段代码,必须先创建work_struct -> cpu_workqueue_struct,然后把工作节点work_struct加入到workqueue_struct工作队列中,加入后工作者线程就会被唤醒,在适当的时机就会执行工作函数。

Linux 内核使用 work_struct 结构体表示一个工作,内容如下

workqueue.h - include/linux/workqueue.h - Linux source code (v3.3) - Bootlin

struct work_struct {
	atomic_long_t data;
	struct list_head entry;  //链表指针 把每个工作连接在一个链表上组成一个双向链表
	work_func_t func;        //处理函数
};

typedef void (*work_func_t)(void *work);

struct list_head {
    struct list_head *next, *prev;
};

这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下:

struct workqueue_struct {
    unsigned int        flags;      /* W: WQ_* flags */
    union {
        struct cpu_workqueue_struct __percpu    *pcpu;
        struct cpu_workqueue_struct     *single;
        unsigned long               v;
    } cpu_wq;               /* I: cwq's */
    struct list_head    list;       /* W: list of all workqueues */

    struct mutex        flush_mutex;    /* protects wq flushing */
    int         work_color; /* F: current work color */
    int         flush_color;    /* F: current flush color */
    atomic_t        nr_cwqs_to_flush; /* flush in progress */
    struct wq_flusher   *first_flusher; /* F: first flusher */
    struct list_head    flusher_queue;  /* F: flush waiters */
    struct list_head    flusher_overflow; /* F: flush overflow list */

    mayday_mask_t       mayday_mask;    /* cpus requesting rescue */
    struct worker       *rescuer;   /* I: rescue worker */

    int         nr_drainers;    /* W: drain in progress */
    int         saved_max_active; /* W: saved cwq max_active */
#ifdef CONFIG_LOCKDEP
    struct lockdep_map  lockdep_map;
#endif
    char            name[];     /* I: workqueue name */
};

workqueue_struct.list变量就是来组织work_struct串联起来的!

struct cpu_workqueue_struct {
    struct global_cwq   *gcwq;      /* I: the associated gcwq */
    struct workqueue_struct *wq;        /* I: the owning workqueue */
    int         work_color; /* L: current color */
    int         flush_color;    /* L: flushing color */
    int         nr_in_flight[WORK_NR_COLORS];
                        /* L: nr of in_flight works */
    int         nr_active;  /* L: nr of active works */
    int         max_active; /* L: max active works */
    struct list_head    delayed_works;  /* L: delayed works */
};

内核通过delayed_works成员把第一个 work_struct 连接起来,后面work_struct通过本身的entry成员把自己连接在链表上。

工作队列分类

内核工作队列分成共享工作队列和自定义工作队列两种:

共享工作队列:系统在启动时候自动创建一个工作队列,驱动开发者如果想使用这个队列,则不需要自己创建工作队列,只需要把自己的work添加到这个工作队列上即可。使用schedule_work这个函数可以把work_struct添加到工作队列中

自定义工作队列:由于共享工作队列是大家共同使用的,如果上面的工作函数有存在睡眠的情况,阻塞了,则会影响到后面挂接上去的工作执行时间,当你的动作需要尽快执行,不想受其它工作函数的影响,则自己创建一个工作队列,然后把自己的工作添加到这个自定义工作队列上去。
使用自定义工作队列分为两步:

创建工作队列:使用creat_workqueue(name)创建一个名为name的工作队列
把工作添加到上面创建的工作队列上:使用queue_work函数把一个工作结构work_struc添加到指定的工作队列上

共享工作队列使用示例:

使用共享工作队列很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,最后通过schedule_work函数将work_struct添加到工作队列,并启动调度。

/* 定义工作(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);
    ...... 
}

自定义工作队列使用示例:

/* 定义工作(work) */ 
struct work_struct testwork; 
//创建一个自定义工作队列指针
struct workqueue_struct *mywq;

/* work 处理函数 */ 
void testwork_func_t(struct work_struct *work)
{ /* work 具体处理内容 */ }

/* 中断处理函数 */ 
irqreturn_t test_handler(int irq, void *dev_id) 
{ 
    ...... 
    /* 调度 work */ 
    queue_work(mywq,&testwork);
    ...... 
}

/* 驱动入口函数 */ 
static int __init xxxx_init(void) 
{ 
    ...... 
    /* 创建自定义工作队列 */ 
    mywq = create_workqueue("mywq");
    /* 初始化 work */ 
    INIT_WORK(&testwork, testwork_func_t); 
    /* 注册中断处理函数 */ 
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    ...... 
}


 

三、调试手段

1、查看软中断和内核线程

  • /proc/softirqs 提供了软中断的运行情况;
  • /proc/interrupts 提供了硬中断的运行情况。

查看软中断在CPU上累计次数:

// Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,可以通过查看 /proc/softirqs 来观察软中断的运行情况。
// TIMER(定时中断)、NET_RX(网络接收)、SCHED(内核调度)、RCU(RCU 锁)
[root@k8s /proc]# cat softirqs
                    CPU0       CPU1
          HI:          5          1
       TIMER:  444492709  271957759
      NET_TX:      18937      15860
      NET_RX:   34769092  430587974
       BLOCK:   12265925          0
BLOCK_IOPOLL:          0          0
     TASKLET:        853        592
       SCHED:    4489427   66716813
     HRTIMER:          0          0
         RCU:  151213683  128619479
         

每个 CPU 都对应一个软中断内核线程,这个软中断内核线程就叫做 ksoftirqd/CPU 编号。

// 查看软中断线程运行情况
[root@k8s /proc]# ps aux | grep softirq
root         6  0.0  0.0      0     0 ?        S    Apr02   0:06 [ksoftirqd/0]
root        14  0.0  0.0      0     0 ?        S    Apr02   2:06 [ksoftirqd/1]

ref:

Linux中的中断处理

Linux 软中断 - galvinwang - 博客园

技术|理解 linux 内核的软中断

linux内核工作队列_年纪青青的博客-CSDN博客_linux 工作队列

linux工作队列 - workqueue总览_鸭蛋西红柿的博客-CSDN博客_alloc_ordered_workqueue

Linux工作队列workqueue源码分析 - 维科号

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值