最全Linux驱动开发八股文(十一)
你好,我是拉依达。
这是我的Linux驱动开发八股文详细解析系列。
本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。
全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。
现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。
【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询
———————————————————————————————————————————————————
12.2 中断的上下部
为保证系统实时性, 中断服务程序必须足够简短,如果都在中断服务程序中完成, 则会严重降低中断的实时性, 所以, linux 系统提出了一个概念: 把中断服务程序分为两部分: 上半部-下半部 。主要目的就是实现中断处理函数的快进快出
中断服务程序分为上半部(top half)和下半部(bottom half),上半部负责读中断源,并在清中断后登记中断下半部,而耗时的工作在下半部处理。
上半部只能通过中断处理程序实现, 下半部的实现目前有 3 种实现方式, 分别为: 软中断、 tasklet 、工作队列(work queues)
(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 绑定的函数并不会立马执行, 而是有中断以后, 经过一个很短的不确定时间在来执行。
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驱动开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章,目前已经是csdn搜索“linux驱动”综合推荐第一名,累计阅读次数4w次。 全文总字数近8w字,是目前全网最全面,最清晰的入门linux驱动学习资料。 现在我重新对内容进行整理,已专栏的形式发布在牛客上,希望可以帮助到更多学习嵌入式的同学。