最全Linux驱动开发八股文(十一)

你好,我是拉依达。

这是我的Linux驱动开发八股文详细解析系列

本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。

全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料

现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询

———————————————————————————————————————————————————

12.2 中断的上下部

为保证系统实时性, 中断服务程序必须足够简短,如果都在中断服务程序中完成, 则会严重降低中断的实时性, 所以, linux 系统提出了一个概念: 把中断服务程序分为两部分: 上半部-下半部 。主要目的就是实现中断处理函数的快进快出

中断服务程序分为上半部(top half)和下半部(bottom half)上半部负责读中断源,并在清中断后登记中断下半部,而耗时的工作在下半部处理。

上半部只能通过中断处理程序实现下半部的实现目前有 3 种实现方式, 分别为: 软中断、 tasklet 、工作队列(work queues)

alt

(1)软中断

Linux 内核使用结构体 softirq_action 表示软中断

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

在 kernel/softirq.c 文件中一共定义了 10 个软中断

static struct softirq_action softirq_vec[NR_SOFTIRQS];

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
};

一共有 10 个软中断,数组 softirq_vec 有 10 个元素。 softirq_action 结构体中的 action 成员变量就是软中断的服务函数

数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。

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

    void open_softirq(int nr, void (*action)(struct softirq_action *))
    //nr:要开启的软中断,也就是上面的10个软中断
    //action:软中断对应的处理函数
    
  • 注册好软中断以后需要通过 raise_softirq 函数触发

    void raise_softirq(unsigned int nr)
    //nr:要触发的软中断 
    

(2)tasklet

tasklet是通过软中断实现的, 软中断用轮询的方式处理, 假如是最后一种中断, 则必须循环完所有的中断类型, 才能最终执行对应的处理函数。

为了提高中断处理数量,改进处理效率, 产生了 tasklet 机制。 tasklet 采用无差别的队列机制, 有中断时才执行, 免去了循环查表之苦。

tasklet 机制的优点

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

调用 tasklet 以后, tasklet 绑定的函数并不会立马执行, 而是有中断以后, 经过一个很短的不确定时间在来执行。

alt

Linux 内核使用 tasklet_struct 结构体来表示 tasklet

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 的参数 */
};
  • next: 链表中的下一个 tasklet, 方便管理和设置 tasklet
  • state: tasklet 的状态
  • count: 表示 tasklet 是否出在激活状态, 如果是 0, 就处在激活状态, 如果非 0, 就处在非激活状态
  • void (*func)(unsigned long): 结构体中的 func 成员是 tasklet 的绑定函数, data 是它唯一的参数。func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数
  • date: 函数执行的传递给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 定义在include/linux/interrupt.h 文件中

DECLARE_TASKLET(name, func, data)
  • name:要定义的 tasklet 名字, 就是一个 tasklet_struct 类型的变量
  • func:tasklet 的处理函数
  • data:传递给 func 函数的参数

上半部中断处理函数调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行

void tasklet_schedule(struct tasklet_struct *t)
  • t:要调度的 tasklet,DECLARE_TASKLET 宏里面的 name, tasklet_struct 类型的变量

杀死 tasklet 使用 tasklet_kill 函数,这个函数会等待 tasklet 执行完毕, 然后再将它移除。 该函数可能会引起休眠, 所以要禁止在中断上下文中使用。

tasklet_kill(struct tasklet_struct *t)
  • t:要删除的 tasklet

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);
    ......
}
  • ①: 定义一个 tasklet 结构体
  • ②: 动态初始化 tasklet
  • ③: 编写 tasklet 的执行函数
  • ④: 在中断上文调用 tasklet
  • ⑤: 卸载模块的时候删除 tasklet

(3)工作队列(workqueue)

工作队列(workqueue) 是实现中断下文的机制之一, 是一种将工作推后执行的形式

工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度

工作队列tasklet 机制有什么不同呢? tasklet 也是实现中断下文的机制, 最主要的区别是 tasklet不能休眠, 而工作队列是可以休眠的。 所以, tasklet 可以用来处理比较耗时间的事情, 而工作队列可以处理非常复杂并且更耗时间的事情。因此如果要推后的工作可以睡眠就可以选择工作队列,否则的话就只能选择软中断或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 都有一个工作队列工作者线程处理自己工作队列中的所有工作。在驱动开发中,只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用去管。

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;
};

初始化工作:INIT_WORK

#define INIT_WORK(_work, _func)
  • _work :要初始化的工作(work_struct)

  • _func :工作对应的处理函数

工作的创建和初始化:DECLARE_WORK

#define DECLARE_WORK(n, f)
  • n :定义的工作(work_struct)

  • f: 工作对应的处理函数

工作的调度函数: schedule_work

bool schedule_work(struct work_struct *work)
  • work: 要调度的工作。
  • 返回值: 0 成功,其他值 失败

工作队列模块

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

12.3 设备树中的中断节点

如果一个设备需要用到中断功能,需要在设备树中配置好中断属性信息, 因为设备树是用来描述硬件信息的, 然后 Linux 内核通过设备树配置的中断属性来配置中断功能。

例:imx6ull中断控制器节点

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

①#interrupt-cells:此中断控制器下设备的 cells 大小,一般会使用 interrupts 属性描述中断信息, #interrupt-cells 描述了 interrupts 属性的 cells 大小, 一条信息有几个cells。 每个 cells 都是 32 位整型值, 对于 ARM 处理的 GIC 来说, 一共有 3 个 cells。

  • 第一个 cells: 中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断
  • 第二个 cells: 中断号, SPI中断号的范围为 0987, PPI中断号的范围为 015
  • 第三个 cells: 标志, bit[3:0]表示中断触发类型, 为1表示上升沿触发, 为2表示下降沿触发, 为4表示高电平触发, 为8表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码

②interrupt-controller 节点为空, 表示当前节点是中断控制器。

③interrupts :描述中断源信息, 对于 gpio5 来说一共有两条信息: 中断类型是 SPI, 触发电平是 IRQ_TYPE_LEVEL_HIGH, 中断源 一个是74, 一个是 75

④interrupt-parent,指定父中断,也就是中断控制器。

#嵌入式##校招##八股文##Linux##linux驱动#
拉依达的Linux驱动八股文 文章被收录于专栏

你好,我是拉依达。 这是我的Linux驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务