4. 中断子系统实验

本章将结合前面章节的中断API,编写一个按键中断驱动,了解linux中断编程的方法,以及使能和屏蔽中断以及中断底半部tasklet、工作队列、软 中断机制和threaded_irq等概念。

4.1. 按键中断程序实验

4.1.1. 设备树插件实现

在前面提到的中断控制器内容都是厂商为我们提供好的,我们要做的内容很简单, 只需要在我们编写的设备树节点中引用已经写好的中断控制器父节点以及配置中断信息即可。

这里我们编写成设备树插件的形式(也可以使用设备树),方便使用,如下所示:

button按键中断的设备树插件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>

&{/} {
        button_interrupt: button_interrupt {
            status = "okay";
            compatible = "button_interrupt";
            button-gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_LOW>;;
            pinctrl-names = "default";
            pinctrl-0 = <&button_interrupt_pin>;
            interrupt-parent = <&gpio0>;
            interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
        };
    };

&{/pinctrl} {
    pinctrl_button {
        button_interrupt_pin: button_interrupt_pin {
            rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>;
        };
    };
};

这里主要介绍和中断相关部分的内容。 需要注意的是,我们编写的这个节点并不是个中断控制器,而是使用中断控制器,所以没有“interrupt-controller”标签。

  • 第4-6行:我们在设备树插件中用了几个宏定义,这里需要包含相应头文件,

  • 第8-9行,新增的button_interrupt节点,

  • 第12行,配置按键引脚,定义button使用的GPIO,这里是以lubuncat2为例,使用40pin中的GPIO0_B0,实际中不同板卡可能没有按键,可以替换成其他引脚,然后外接按键、或者接高低电平控制模拟按键。

  • 第14行,插件按键引脚的复用信息,即pinctrl。

  • 第15-16行,添加中断相关信息, interrupt-parent 表示父中断控制节点是gpio0, interrupts 表示中断引脚,触发方式

4.1.2. 按键中断驱动程序实现

虽然使用了设备树(设备树插件)但是驱动程序是一个简单的字符设备驱动,不会和设备树中的节点匹配。 无论是否匹配与我们“读设备树”无关,驱动源码大致分为驱动入口和出口函数实现、字符设备操作函数实现两部分内容, 结合源码介绍如下:

4.1.2.1. 驱动入口和出口函数实现

在驱动的入口函数中实现字符设备的注册, 在出口函数中注销字符设备,部分代码如下所示:

驱动入口和出口函数实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 /*
*驱动初始化函数
*/
static int __init button_driver_init(void)
{
    int error = -1;
    /*采用动态分配的方式,获取设备编号,次设备号为0,*/
    error = alloc_chrdev_region(&button_devno, 0, DEV_CNT, DEV_NAME);
    if (error < 0)
    {
            printk("fail to alloc button_devno\n");
            goto alloc_err;
    }
    /*关联字符设备结构体cdev与文件操作结构体file_operations*/
    button_chr_dev.owner = THIS_MODULE;
    cdev_init(&button_chr_dev, &button_chr_dev_fops);

    /*添加设备至cdev_map散列表中*/
    /*------------一下代码省略---------------*/
}

/*
*驱动注销函数
*/
static void __exit button_driver_exit(void)
{
    pr_info("button_driver_exit\n");
    /*删除设备*/
    device_destroy(class_button, button_devno);                //清除设备
    class_destroy(class_button);                                       //清除类
    cdev_del(&button_chr_dev);                                             //清除设备号
    unregister_chrdev_region(button_devno, DEV_CNT);   //取消注册字符设备
}

module_init(button_driver_init);
module_exit(button_driver_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("embedfire lubuncat2_RK , interrupt ");

字符设备注册于注销参考前面 字符设备章节,为方便阅读这里将它的部分代码列出来了。完整的内容请参考本小节配套代码。

4.1.2.2. .open函数实现

open函数实现button的初始化工作,代码如下:

open函数实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int button_open(struct inode *inode, struct file *filp)
{
    int error = -1;

    /*获取按键 设备树节点*/
    button_device_node = of_find_node_by_path("/button_interrupt");
    if(NULL == button_device_node)
    {
            printk("of_find_node_by_path error!");
            return -1;
    }

    /*获取按键使用的GPIO*/
    button_GPIO_number = of_get_named_gpio(button_device_node ,"button-gpios", 0);
    if(0 == button_GPIO_number)
    {
            printk("of_get_named_gpio error");
            return -1;
    }

    /*申请GPIO  , 记得释放*/
    error = gpio_request(button_GPIO_number, "button_gpio");
    if(error < 0)
    {
            printk("gpio_request error");
            gpio_free(button_GPIO_number);
            return -1;
    }

    error = gpio_direction_input(button_GPIO_number);

    /*获取中断号*/
    interrupt_number = irq_of_parse_and_map(button_device_node, 0);
    printk("\n irq_of_parse_and_map! =  %d \n",interrupt_number);

    /*申请中断, 记得释放*/
    error = request_irq(interrupt_number,button_irq_hander,IRQF_TRIGGER_RISING,"button_interrupt",NULL);
    if(error != 0)
    {
            printk("request_irq error");
            free_irq(interrupt_number, NULL);
            return -1;
    }


    return 0;
}
  • 第10行,获取button的设备树节点,我们之前说过,虽然驱动没有采用与设备树节点匹配的方式, 但这不影响我们获取设备树节点,只要节点路径正确即可获取其他设备树节点。

  • 第18行,获取使用的GPIO。详细说明可参考“GPIO子系统章节”。

  • 第26行,注册GPIO。

  • 第34行,设置GPIO为输入模式。

  • 第37行,使用函数irq_of_parse_and_map解析并映射(map)中断函数。函数原型如下:

  • 第41行,申请中断,这个函数在本章的开始已经介绍,需要注意的是,这里虽然没有使用共享中断, 但是仍然将dev参数设置为字符设备结构体指针。当然你也可以设置为NULL或其他值。

解析并映射中断函数
1
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

该函数的功能是从设备树中获取某一个中断,并且将中断ID转化为linux内核虚拟IRQ number。 IRQ number用于区别中断ID。

参数

  • dev:用于指定设备节点

  • index:指定解析、映射第几个中断, 一个设备树节点可能包含多个中断,这里指定第几个,标号从0开始。

返回值

  • 成功:解析、映射得到的内核中断号

  • 失败:返回0

4.1.2.3. 中断服务函数实现

在open函数申请中断时要指定中断服务函数,一个简答的中断服务函数如下。

中断服务函数实现
1
2
3
4
5
6
7
atomic_t   button_status = ATOMIC_INIT(0);  //定义整型原子变量,保存按键状态 ,设置初始值为0
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    /*按键状态加一*/
    atomic_inc(&button_status);
    return IRQ_HANDLED;
}

从以上代码可以看到我们定义了一个整型原子变量用于保存按键状态,中断发送后,整型原子变量自增一。 整型原子变量大于0表示有按键按下。

4.1.2.4. .read和.release函数实现

.read函数的工作是向用户空间返回按键状态值,.release函数实现退出之前的清理工作。函数实现源码如下:

.read 和.release函数实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int button_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int error = -1;
    int button_countervc = 0;

    /*读取按键状态值*/
    button_countervc = atomic_read(&button_status);

    /*结果拷贝到用户空间*/
    error = copy_to_user(buf, &button_countervc, sizeof(button_countervc));
    if(error < 0)
    {
            printk_red("copy_to_user error");
            return -1;
    }

    /*清零按键状态值*/
    atomic_set(&button_status,0);
    return 0;
}

/*字符设备操作函数集,.release函数实现*/
static int button_release(struct inode *inode, struct file *filp)
{
    /*释放申请的引脚,和中断*/
    gpio_free(button_GPIO_number);
    free_irq(interrupt_number, device_button);
    return 0;
}
  • 第1-20行,在button_read函数中我们读取按键状态值,然后使用copy_to_user拷贝到用户空间, 最后设置按键状态为0。

  • 第23-29行,button_release函数很简单,它只是释放.open函数中申请的中断和GPIO.

4.1.3. 测试应用程序实现

测试应用程序工作是读取按键状态然后打印状态,就这么简单,源码如下:

测试应用程序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int error = -20;
    int button_status = 0;

    /*打开文件*/
    int fd = open("/dev/button", O_RDWR);
    if (fd < 0)
    {
        printf("open file : /dev/button error!\n");
        return -1;
    }

    printf("wait button down... \n");

    do
    {
        /*读取按键状态*/
        error = read(fd, &button_status, sizeof(button_status));
        if (error < 0)
        {
            printf("read file error! \n");
        }
        usleep(1000 * 100); //延时100毫秒
    } while (0 == button_status);
    printf("button Down !\n");

    /*关闭文件*/
    error = close(fd);
    if (error < 0)
    {
        printf("close file error! \n");
    }
    return 0;
}

测试应用程序仅仅是测试驱动是否正常,我们只需要打开、读取状态、关闭文件即可。 需要注意的是打开之后需要关闭才能再次打开,如果连续打开两次由于第一次打开申请的GPIO和中断还没有释放打开会失败。

4.1.4. 实验准备

在板卡上的部分GPIO可能会被系统占用,引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源, 比如运行代码时出现“Device or resource busy”或者运行代码卡死等等现象,,可以注释其他的使用的设备树插件。

我们修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件, 添加我们编辑好的设备树插件。并把设备树插件文件放在和Makefile文件同级目录下。 以进行设备树插件的编译。

broken

在内核的根目录下执行如下命令即可:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig

make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

生成的.dtbo位于内核根目录下的“arch/arm64/boot/dts/rockchip/overlays”目录下。 本章设备树插件为“lubancat-button-overlay.dts”, 编译之后就会在内核源码/arch/arm64/boot/dts/rockchip/overlays目录下生成同名的lubancat-button-overlay.dtbo文件,得到.dtbo后,下一步就是将其加载到系统中。

4.1.4.1. 添加设备树插件文件

上一小节我们编译生成了 lubancat-button-overlay.dtbo ,该文件可以被动态的加载到系统,lubancat2板卡uboot加载设备树插件,详细参考 环境搭建章节。 只需完成简单的两个步骤:

  • 1、将需要加载的.dtbo文件放入板卡 /boot/dtb/overlays/ 目录下。

  • 2、将对应的设备树插件加载配置,写入uEnv.txt配置文件,系统启动过程中会自动从uEnv.txt读取要加载的设备树插件。打开位于“/boot/uEnv/”目录下的uEnv.txt文件,要将设备树插件写入uEnv.txt也,使用vim或者nano编辑器打开文件,书写格式为“dtoverlay=<设备树插件路径>”。

添加好后,我们重启开发板,使用命令ls /proc/device-tree/ 查看, 是否有button_interrupt目录,有就说明加载成功。

4.1.4.2. 编译驱动程序及测试程序

本节实验使用的Makefile如下所示:

Makefile(位于../linux_driver/button_interrupt/interrupt)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 KERNEL_DIR=../../kernel/

 ARCH=arm64
 CROSS_COMPILE=aarch64-linux-gnu-
 export  ARCH  CROSS_COMPILE

obj-m := interrupt.o
out =  test_app

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
    $(CROSS_COMPILE)gcc -o $(out) test_app.c

.PHONY:clean
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
    rm test_app

将配套的驱动代码如:interrupt放置在与内核同级目录下,并在驱动目录中输入如下命令来编译驱动模块及测试程序:

make

4.1.5. 实验现象

将编译好的驱动、应用程序、设备树插件并拷贝到开发板,这里就不再赘述这一部分内容了,前面的章节中都有详细介绍。

在加载模块之前,先查看 /boot/uEnv.txt 文件是否加载了板子上原有的与KEY相关设备树插件。 如KEY相关设备树插件开启,则添加’#’以注销掉与KEY相关的设备树插件。 并在添加按键中断的设备树插件后重启开发板。

ls /proc/device-tree
broken

加载内核模块,执行测试程序:

broken

没有按键,可以使用杜邦线依次短接40pin的GND和3.3V进行模拟,拉高或者拉低引脚电平,需要注意的是,要加载驱动后将IO设置为输入再短接,并且不能接到高于3.3v的电源上以免损坏主控。

我们也可以在执行test_app时,使用下面命令进行查看,确定驱动是否正常加载、初始化等:

cat /proc/interrupts
broken

可以查看到我们注册的中断等信息。

4.2. 中断使用进阶

linux中断我们需要知道以下两点:

  • 1、Linux不支持中断嵌套。

  • 2、中断服务函数运行时间应当尽量短,做到快进快出。

然而一些中断的产生之后需要较长的时间来处理,如由于网络传输产生的中断, 在产生网络传输中断后需要比较长的时间来处理接收或者发送数据,因为在linux中断并不能被嵌套 如果这时有其他中断产生就不能够及时的响应,为了解决这个问题,linux对中断的处理引入了“中断上半部”和 “中断下半部”的概念,在中断的上半部中只对中断做简单的处理,把需要耗时处理的部分放在中断下半部中,使得能够 对其他中断作为及时的响应,提供系统的实时性。这一概念又被称为中断分层:

  • “上半部分”是指在中断服务函数中执行的那部分代码,

  • “下半部分”是指那些原本应当在中断服务函数中执行但通过某种方式把它们放到中断服务函数外执行。

并不是所有的中断处理都需要用到“上半部分”和“下半部分”,如果像我们上面编写的按键中断程序一样并不需要用到 相对耗时的处理,对中断的处理只需放在中断“上半部分”即可。实现下半部分的机制主要有软中断、tasklet、工作队列和线程irq。

为了学习如何使用中断分层,这里模拟一个耗时操作,加上中断分层的“下半部分”。

4.2.1. 软中断和tasklet

tasklet是基于软中断实现,它们有很多相似之处,我们把它两个放到一块介绍。

4.2.1.1. 软中断(softirq)

Linux4.xx支持的软中断非常有限,只有十个(不同版本的内核可能不同) 在Linux内核中使用一个枚举变量列出所有可用的软中断,如下所示。

软中断中断种类(include/linux/interrupt.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

类比硬中断,这个枚举类型列出了软中断的中断编号,我们“注册”软中断以及触发软中断都会用到软中断的中断编号。

软中断“注册”函数如下所示:

注册软中断函数
1
2
3
4
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

参数

  • nr:用于指定要“注册”的软中断中断编号

  • action:指定软中断的中断服务函数

返回值

我们再看函数实现,这里只有一个赋值语句, 重点是softirq_vec变量,在内核源码中找到这个变量如下所示:

软中断“中断向量表”
1
static struct softirq_action softirq_vec[NR_SOFTIRQS]

这是一个长度为NR_SOFTIRQS的softirq_action类型数组,长度NR_SOFTIRQS在软中断的“中断编号”枚举类型中有定义, 长度为10。这个数组是一个全局的数组,作用等同于硬中断的中断向量表。接着来看数组的类型“struct softirq_action”如下所示。

 软中断结构体
1
2
3
4
struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

它只有一个参数,就是注册软中断函数的参数open_softirq。至此我们知道数组softirq_vec就是软中断的中断向量表, 所谓的注册软中断函数就是根据中断号将中断服务函数的地址写入softirq_vec数组的对应位置。

软中断注册之后还要调用“触发”函数触发软中断,进而执行软中断中断服务函数,函数如下所示:

中断interrupt-controller节点
1
void raise_softirq(unsigned int nr);

参数

  • nr:要触发的软中断

返回值

4.2.1.2. tasklet

tasklet是基于软中断实现,如果对效率没有特殊要求推荐是用tasklet实现中断分层。为什么这么说, 根据之前讲解软中断的中断服务函数是一个全局的数组,在多CPU系统中,所有CPU都可以访问, 所以在多CPU系统中需要用户自己考虑并发、可重入等问题,增加编程负担。 软中断资源非常有限一些软中断是为特定的外设准备的(不是说只能用于特定外设)例如“NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,” 从名字可以看出它们用于网络的TX和RX。像网络这种对效率要求较高的场合还是会使用软中断实现中断分层的。

相比软中断,tasklet使用起来更简单,最重要的一点是在多CPU系统中同一时间只有一个CPU运行tasklet, 所以并发、可重入问题就变得很容易处理(一个tasklet甚至不用去考虑)。而且使用时也比较简单,介绍如下。

tasklet_struct结构体

在驱动中使用tasklet_struct结构体表示一个tasklet,结构体定义如下所示:

触发软中断
1
2
3
4
5
6
7
8
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

参数介绍如下:

  • next:指向链表的下一个tasklet_struct,这个参数我们不需要自己去配置。

  • state:保存tasklet状态,等于0表示tasklet还没有被调度,等于TASKLET_STATE_SCHED表示tasklet被调度正准备运行。 等于TASKLET_STATE_RUN表示正在运行。

  • count:引用计数器,如果为0表示tasklet可用否则表示tasklet被禁止。

  • func:指定tasklet处理函数

  • data:指定tasklet处理函数的参数。

tasklet初始化函数

函数原型如下:

tasklet初始化函数
1
2
3
4
5
6
7
8
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}

参数

  • t:指定要初始化的tasklet_struct结构体

  • func:指定tasklet处理函数,等同于中断中的中断服务函数

  • data:指定tasklet处理函数的参数。函数实现就是根据设置的参数填充tasklet_struct结构体结构体。

返回值

触发tasklet

和软中断一样,需要一个触发函数触发tasklet,函数定义如下所示:

tasklet触发函数
1
2
3
4
5
static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
            __tasklet_schedule(t);
}

参数

  • t:tasklet_struct结构体。

返回值

4.2.1.3. tasklet实现中断分层实验

实验在按键中断程序基础上完成,按键中断原本不需要使用中断分层,这里只是以它为例简单介绍tasklet的具体使用方法。 tasklet使用非常简单,主要包括定义tasklet结构体、初始化定义的tasklet结构体、实现tasklet中断处理函数、触发tasklet中断。

下面结合源码介绍如下。注意,源码是在“按键中断程序”基础上添加tasklet相关代码,这里只列出了tasklet相关代码, 参考教程源码:linux_driver/button_interrupt/interrupt_tasklet目录下。

tasklet相关代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*--------------第一部分--------------- */
struct tasklet_struct button_tasklet;  //定义全局tasklet_struct类型结构体

/*--------------第二部分-----------------*/
void button_tasklet_hander(unsigned long data)
{
    int counter = 1;
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);
}

/*--------------第三部分-----------------*/
static int button_open(struct inode *inode, struct file *filp)
{
    /*----------------以上代码省略----------------*/
    /*初始化button_tasklet*/
    tasklet_init(&button_tasklet,button_tasklet_hander,0);

    return 0;
}

/*--------------第四部分-----------------*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    printk(KERN_ERR "button_irq_hander----------inter");
    /*按键状态加一*/
    atomic_inc(&button_status);

    tasklet_schedule(&button_tasklet);

    printk(KERN_ERR "button_irq_hander-----------exit");
    return IRQ_RETVAL(IRQ_HANDLED);
}

结合代码各部分介绍如下:

  • 第2行:定义tasklet_struct类型结构体。

  • 第5-18行:定义tasklet的“中断服务函数”可以看到我们在tasklet的中断服务函数中使用延时 和printk语句模拟一个耗时的操作。

  • 第21-28行:在原来的代码基础上调用tasklet_init函数初始化tasklet_struct类型结构体。

  • 第37行:在中断服务函数中调用tasklet_schedule函数触发tasklet中断。 在按键中断服务函数中的开始处和结束处添加打印语句,正常情况下程序会先执行按键中断的中短发服务函数, 退出中断服务函数后再执行中断的下半部分,既tasklet的“中断服务函数”。

4.2.1.4. 下载验证

设备树插件的加载方法在前面章节中已经多次提及,此处就不再赘述,如有疑问请回头查看。

将修改后的驱动程序编译、下载到开发板,使用insmod加载驱动然后运行测试应用程序如下所示。

找不到图片02|

4.2.2. 工作队列

与软中断和tasklet不同,工作队列运行在内核线程,允许被重新调度和睡眠。 如果中断的下部分能够接受被重新调度和睡眠,推荐使用工作队列。

和tasklet类似,从使用角度讲主要包括定义工作结构体、初始化工作、触发工作。

4.2.2.1. 工作结构体

“工作队列”是一个“队列”,但是对于用户来说不必关心“队列”以及队列工作的内核线程,这些内容由内核帮我们完成, 我们只需要定义一个具体的工作、初始化工作即可,在驱动中一个工作结构体代表一个工作,工作结构体如下所示:

work_struct结构体
1
2
3
4
5
6
7
8
struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

重点关心参数“work_func_t func;”该参数用于指定“工作”的处理函数。work_func_t如下所示。

工作函数
1
void (*work_func_t)(struct work_struct *work);

4.2.2.2. 工作初始化函数

内核提初始化宏定义如下所示。

工作初始化宏定义
1
#define INIT_WORK(_work, _func)

该宏共有两个参数,_work用于指定要初始化的工作结构体,_func用于指定工作的处理函数。

4.2.2.3. 启动工作函数

驱动工作函数执行后相应内核线程将会执行工作结构体指定的处理函数,驱动函数如下所示。

启动工作函数
1
2
3
4
static inline bool schedule_work(struct work_struct *work)
{
    return queue_work(system_wq, work);
}

启动工作函数只有一个工作结构体参数。

4.2.2.4. 工作队列实验

工作队列实验同样在按键中断程序基础上实现,这里只列出了工作队列相关代码, 完整内容请参考本小节配套驱动程序。(这里只修改驱动程序,其他内容保持不变),参考教程源码:linux_driver/button_interrupt/interrupt_work目录下。

工作队列相关函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*--------------第一部分-----------------*/
struct work_struct button_work;

/*--------------第二部分-----------------*/
void work_hander(struct work_struct  *work)
{
    int counter = 1;
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
}

/*--------------第三部分-----------------*/
static int button_open(struct inode *inode, struct file *filp)
{
    /*----------------以上代码省略----------------*/
    /*初始化button_work*/
    INIT_WORK(&button_work, work_hander);
    return 0;
}

/*--------------第四部分-----------------*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    /*按键状态加一*/
    atomic_inc(&button_status);
    schedule_work(&button_work);
    return IRQ_HANDLED;
}
  • 第2行:定义work_struct类型结构体。

  • 第5-18行:定义工作队列中的“中断服务函数”,使用延时和printk语句模拟一个耗时的操作。

  • 第21-27行:在原来的代码基础上调用INIT_WORK宏初始化work_struct类型结构体与中断下半部分函数。

  • 第34行:在中断服务函数中调用schedule_work函数触发中断下半部。

与tasklet实现中断分层类似,使用方法几乎一样,这里不进行详细描述。测试如下图所示:

工作队列

4.2.3. 线程IRQ

将中断线程化,中断将作为内核线程运行,可被赋予不同的实时优先级。在负载较高时,中断线程可以被挂起,以避免某些更高优先级的实时任务得不到及时响应。

线程irq通过devm_request_threaded_irq()申请,该函数的使用和request_irq()、devm_request_irq()类似,在例程中直接替换。

工作队列相关函数
1
2
3
request_threaded_irq(unsigned int irq, irq_handler_t handler,
         irq_handler_t thread_fn,
         unsigned long flags, const char *name, void *dev);
  • unsigned int irq:中断号,所申请的中断向量

  • irq_handler_t handler:中断处理函数,在驱动中一般这个参数是NULL,为NULL时使用默认的处理,这个相当于中断的上半段

  • irq_handler_t thread_fn:中断线程,中断发生时,如果handler为NULL,就直接将thread_fn扔到内核线程中去执行

  • flags:指定中断属性、中断触发方式

  • name: 中断的名字

  • dev: 传入中断处理程序的参数,可以为NULL,但在注册共享中断时,此参数不能为NULL。 该参数可作为共享中断时的中断区别参数,还可以把其传给一个结构体变量,用于保存一个设备的信息,使中断处理函数可以获得该设备的信息