一、linux驱动开发-6.1-中断

本文深入讲解了Linux内核中的中断处理机制,包括中断API的使用、上半部和下半部的概念及其实现方式,并提供了一个具体的按键中断处理示例。

一、中断简介

       内核提供了完善的中断框架,只需要申请中断,然后注册中断处理函数即可。

1.1、中断API

1.1.1、中断号

        每个中断都有一个中断号,通过中断号即可区分不同的中断。

1.1.2、request_irq

       用于申请中断,可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码中使用,次函数会激活(使能)中断,因此不需要手动去使能中断 。

/*
@Decription:申请中断
@irq:要申请的中断号
@handler:中断处理函数
@flags:中断标志
@name:中断名字
@dev:一般设置为设备结构体,dev会传递给中断处理函数irq_handler的第二个参数

@rerun:0,申请成功
      < 0,申请失败
*/
int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev)

 1.1.3、free_irq

       使用完后通过free_irq释放    

/*
@Description:释放中断
@irq:要释放的中断
@dev:
*/
void free_irq(unsigned int irq,
              void *dev)

1.1.4、中断处理函数

       使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:

irqreturn_t (*irq_handler_t) (int, void *)

        第一个参数是要中断处理函数要相应的中断号。

        第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构

        中断处理函数的返回值为 irqreturn_t 类型,定义如下:

enum irqreturn {
    IRQ_NONE = (0 << 0),
    IRQ_HANDLED = (1 << 0),
    IRQ_WAKE_THREAD = (1 << 1),
};

typedef enum irqreturn irqreturn_t;

一般返回形式:

return IRQ_RETVAL(IRQ_HANDLED)

 1.1.5、中断使能与禁止函数

void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

void disable_irq_nosync(unsigned int irq)

        disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。

        disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。

       有时候需要挂壁当前处理器的整个中断系统:

local_irq_enable()
local_irq_disable()

       考虑到多任务:

local_irq_save(flags) 
local_irq_restore(flags)

1.2、上半部和下半部

       Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快
出。

       上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
       下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出

       上半部就是中断处理函数

       下半部,内核提供了多种下半部机制

1.2.1、软中断

1.2.2、tasklet

1.2.3、工作队列

1.3、设备树中的中断信息节点

1.4、获取中断号

       在设备树里添加中断信息后,就可以从interrupts属性中提取对应的中断号了。

/*
Description:获取中断号
@dev:设备节点
@index:索引号

@return:中断号
*/
unsigned int irq_of_parse_and_map(struct device_node *dev,
                                  int index)

       如果使用gpio的话,可以使用gpio_to_irq来获取gpio对应的中断号

/*
@Description:获取中断号
@gpio:要获取的gpio编号

@return:gpio对应的中断号
*/
int gpio_to_irq(unsigned int gpio)

二、程序编写

       采用按键触发中断的方式,用定时器实现消抖。

2.1、修改设备树

        在key节点下添加中断相关属性

2.2、驱动程序

        ①、在中断服务函数中启动10ms定时器用于消抖

        ②、在定时器服务函数中读取按键,并用原子变量进行保护

#include <linux/types.h>

#include <linux/module.h>

#include <linux/ide.h>

#include <linux/kernel.h>

#include <linux/init.h>

#include <linux/errno.h>

#include <linux/fs.h>

#include <linux/slab.h>

#include <linux/uaccess.h>

#include <linux/io.h>

#include <linux/cdev.h>

#include <linux/device.h>

#include <linux/of.h>

#include <linux/of_address.h>

#include <linux/of_gpio.h>

#include <linux/gpio.h>

#include <linux/of_irq.h>

#include <linux/irq.h>

#include <linux/semaphore.h>

#include <linux/timer.h>

#include <linux/io.h>



#define IMX6UIRQ_CNT     	1     

#define IMX6UIRQ_NAME    	"imx6uirq"

#define KEY0VALUE			0X01

#define INVAKEY				0xFF 

#define KEY_NUM				1



struct irq_keydesc{

	int gpio;				//gpio编号

	unsigned int irqnum;	//中断号

	irqreturn_t (*irq_handler)(int, void*);		//中断处理函数

};



//设备结构体

struct imx6uirq_dev{

	dev_t devid;

	struct cdev cdev;

	struct class *class;

	struct device *device;

	int major;

	int minor;

	struct device_node *nd;		//设备节点

	atomic_t keyvalue;			//有效的按键值

	atomic_t releasekey;		//标记是否完成一次完整的按键

	struct timer_list timer;	//定时器

	struct irq_keydesc irq_keydesc[KEY_NUM];	//按键描述数组

};



struct imx6uirq_dev imx6uirq;  



static int imx6uirq_open(struct inode *inode, struct file *file)

{

	file->private_data = &imx6uirq;

	return 0;

}



ssize_t imx6uirq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)

{

	unsigned char keyvalue = 0;

	unsigned char releasekey = 0;

	struct imx6uirq_dev *dev = file->private_data;



	keyvalue = atomic_read(&dev->keyvalue);

	releasekey = atomic_read(&dev->releasekey);

	

	if (releasekey)

	{

		copy_to_user(buf, &keyvalue, sizeof(keyvalue));

		atomic_set(&dev->releasekey, 0);	//清除按下标志位

	}

	else

	{

		return -EINVAL;

	}

	



	return 0;

}



static const struct file_operations imx6uirq_fops = {

	.owner = THIS_MODULE,

	.open  = imx6uirq_open,

	.read  = imx6uirq_read,

};



//定时器回调函数

static void timer_function(unsigned long arg)

{

	int value;

	struct irq_keydesc *keydesc;

	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

	keydesc = &dev->irq_keydesc[0];



	value = gpio_get_value(keydesc->gpio);

	if (value == 0)

		atomic_set(&dev->keyvalue, value);

	else {

		atomic_set(&dev->keyvalue, value);

		atomic_set(&dev->releasekey, 1);	//标记按键松开

	}

}



static irqreturn_t key0_irq_handler(int irq, void *dev_id)

{

	int value;

	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;



	dev->timer.data = (volatile long)dev_id;

	//启动定时器

	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));



	return IRQ_HANDLED;

}



static int key_io_init(void)

{

	int ret;

	//通过节点名字查找节点

	imx6uirq.nd = of_find_node_by_name(NULL, "gpiokey");

	if (imx6uirq.nd == NULL)

	{

		printk("find %s fail\r\n", "gpiokey");

		return -EINVAL;

	}



	//通过节点获取GPIO编号

	imx6uirq.irq_keydesc[0].gpio = of_get_named_gpio(imx6uirq.nd, "key-gpio", 0);

	if (imx6uirq.irq_keydesc[0].gpio < 0)

	{

		printk("get %s fail\r\n", "key-gpio");

		return -EINVAL;

	}

	//初始化key-gpio

	ret = gpio_request(imx6uirq.irq_keydesc[0].gpio, "gpio_irq");

	if (ret != 0)

	{

		printk("request %s fail\r\n", "gpio_irq");

		return -EINVAL;

	}

	ret = gpio_direction_input(imx6uirq.irq_keydesc[0].gpio);

	if (ret < 0)

	{

		printk("set key status fail\r\n");

		return -EINVAL;

	}



	//通过节点获取中断号

	imx6uirq.irq_keydesc[0].irqnum = irq_of_parse_and_map(imx6uirq.nd, 0);

#if 0

	//通过GPIO编号获取中断号

	imx6uirq.irq_keydesc[0].irqnum = gpio_to_irq(imx6uirq.irq_keydesc[0].gpio);

#endif

	//设置中断处理函数

	imx6uirq.irq_keydesc[0].irq_handler = key0_irq_handler;

		

	//通过中断号申请中断

	ret = request_irq(imx6uirq.irq_keydesc[0].irqnum,	

				imx6uirq.irq_keydesc[0].irq_handler,

				IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,

				IMX6UIRQ_NAME,

				&imx6uirq);	

	if (ret < 0)

	{

		printk("request irq fail\r\n");

		return -EINVAL;

	}



	//初始化定时器

	init_timer(&imx6uirq.timer);

	imx6uirq.timer.function = timer_function;

}



static int __init imx6uirq_init(void)

{

	int ret = 0;

	int val = 0;	



	//分配设备号

	if ( imx6uirq.major) {

		 imx6uirq.devid = MKDEV( imx6uirq.major, 0);

		register_chrdev_region( imx6uirq.devid,  IMX6UIRQ_CNT,  IMX6UIRQ_NAME);

	} else {

		alloc_chrdev_region(&imx6uirq.devid, 0,  IMX6UIRQ_CNT,  IMX6UIRQ_NAME);

		 imx6uirq.major = MAJOR(imx6uirq.devid);

		 imx6uirq.minor = MINOR(imx6uirq.devid);

	}



	//初始化cdev

	imx6uirq.cdev.owner = THIS_MODULE;

	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);

	//添加cdev

	cdev_add(&imx6uirq.cdev, imx6uirq.devid,  IMX6UIRQ_CNT);

	//创建类

	imx6uirq.class = class_create(THIS_MODULE,  IMX6UIRQ_NAME);

	//创建设备

	imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL,  IMX6UIRQ_NAME);



	//按键gpio初始化

	key_io_init();

	

	return 0;

}



static void __exit imx6uirq_exit(void)

{

	//删除定时器

	del_timer_sync(&imx6uirq.timer);

	//释放中断

	free_irq(imx6uirq.irq_keydesc[0].irqnum, &imx6uirq);



	//删除设备

	cdev_del(&imx6uirq.cdev);

	//注销设备号

	unregister_chrdev_region(imx6uirq.devid,  IMX6UIRQ_CNT);

	//删除设备的类

	device_destroy(imx6uirq.class,  imx6uirq.devid);

	//删除类

	class_destroy(imx6uirq.class);

		

	printk(" dev exit\n");

}



module_init( imx6uirq_init);

module_exit( imx6uirq_exit);



MODULE_LICENSE("GPL");

MODULE_AUTHOR("ZK");



2.3、应用程序

        通过不断读取获取按键值

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define CLOSE_CMD		1
#define OPEN_CMD		2
#define SETPERIOD_CMD	3

int main(int argc, char *argv[])
{
    int fd, ret;
    char *filename;
    unsigned char keyvalue;

    if (argc != 2)
    {
        printf("Usage:\n");
        printf("\n");
        return -1;
    }
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0)
    {
        printf("open file %s fail\n", filename);
    }

    while (1)
    {
        ret = read(fd, &keyvalue, sizeof(keyvalue));
        if (ret < 0)
        {

        }
        else 
        {
            if (keyvalue)
                printf("key0 value is %d\r\n", keyvalue);
        }
    }

    close(fd);
    return ret;
}

### ✅ 回答问题:Linux 内核 6.1 中的 `block` 子系统架构与核心机制详解 --- **简要答案**: 在 Linux 内核 **6.1** 版本中,`block`(块设备)子系统是 I/O 栈的核心组件,负责管理所有面向块的存储设备(如硬盘、SSD、虚拟磁盘等)。它通过统的接口抽象物理和逻辑设备,支持多队列 I/O 调度(`blk-mq`)、请求排队、分区解析、I/O 合并与延迟控制,并为上层文件系统(ext4, XFS 等)提供可靠的读写服务。 > 🧩 主要位于源码路径:`drivers/block/`, `block/`, `include/linux/blk*.h` --- ## 🏗️ 、整体架构图(Linux 6.1) ``` +---------------------+ | 文件系统层 | ← ext4, XFS, Btrfs → 发出 bio +---------------------+ ↓ submit_bio() +---------------------+ | Generic Block Layer | ← 处理 BIO(I/O 请求向量) | | | - bio_alloc() | | - merge_bios() | | - partitioning | +---------------------+ ↓ 提交到请求队列 +---------------------+ | blk-mq 框架 | ← 多队列调度(主要入口) | | | - blk_mq_make_request| | - soft queue (hctx) | | - hardware context | +---------------------+ ↓ 派发给驱动 +---------------------+ | 块设备驱动程序 | ← NVMe, virtio-blk, SATA(AHCI), MMC | (Device Driver) | 构造硬件命令并发送 +---------------------+ ↓ +---------------------+ | 物理存储设备 | ← SSD, HDD, eMMC, SD Card +---------------------+ ``` --- ## 🔍 二、关键数据结构(Kernel 6.1) ### 1. `struct bio` —— I/O 请求的基本单位 代表个“分散-聚集”式的 I/O 请求: ```c struct bio { sector_t bi_iter.bi_sector; // 起始扇区(LBA) unsigned int bi_iter.bi_size; // 字节数 struct bvec_iter bi_iter; // 当前操作位置 struct bio_vec *bi_io_vec; // 指向 bvec 数组 unsigned short bi_vcnt; // 实际使用的 bvec 数量 unsigned short bi_max_vecs; // 最大支持的段数 struct block_device *bi_disk; // 目标设备 struct gendisk *bi_disk; bio_end_io_t *bi_end_io; // 完成回调函数 void *bi_private; // 私有数据(如 fs info) }; ``` > ⚠️ 注意:从内核 5.x 开始,`bio` 不再包含嵌入式 `bvec` 数组,而是动态分配。 #### 示例:分配并初始化个 `bio` ```c struct bio *bio = bio_alloc(GFP_NOIO, 4); // 支持最多 4 个段 bio_set_dev(bio, bdev); bio->bi_iter.bi_sector = 1024; // LBA=1024 bio_add_page(bio, page, 4096, 0); // 添加页数据 bio->bi_end_io = my_completion_fn; // 设置完成处理函数 submit_bio(REQ_OP_READ, bio); // 提交读请求 ``` --- ### 2. `struct request_queue` 和 `blk-mq` 在 Linux 6.1 中,旧的单队列模型已被完全弃用,全面采用 **`blk-mq`(Block Multi-Queue)** 架构。 #### 核心结构: ```c struct request_queue { struct blk_mq_ops *mq_ops; // 驱动实现的操作集 struct blk_mq_ctx __percpu *queue_ctx; // 每 CPU 上下文(soft queue) struct blk_mq_hw_ctx **queue_hw_ctx; // 硬件上下文数组 unsigned int nr_hw_queues; // 硬件队列数量 unsigned int queue_depth; // 每个硬件队列深度 struct elevator_queue *elevator; // I/O 调度器(可选) ... }; ``` #### 工作流程1. 进程在某个 CPU 上提交 `bio` 2. 映射到该 CPU 对应的 `blk_mq_ctx` 3. 选择个 `blk_mq_hw_ctx`(可能绑定特定 CPU 或硬件通道) 4. 将请求加入 `hw_ctx` 的队列 5. 触发驱动的 `.queue_rq()` 方法执行派发 --- ### 3. `struct request`(已逐步弱化) 在 `blk-mq` 模型中,`request` 不再是必须存在的对象。许多现代驱动(如 NVMe)直接使用 `struct scsi_cmnd` 或自定义命令结构。 但在些传统驱动或需要重试/排序时仍会创建: ```c struct request { struct request_queue *q; unsigned int cmd_flags; // REQ_OP_READ / REQ_OP_WRITE struct bio *bio, *biotail; // 链接的 bio 列表 struct list_head queuelist; // 在调度队列中的链表节点 }; ``` --- ### 4. `struct gendisk` —— 通用磁盘描述符 表示个完整的块设备及其分区信息: ```c struct gendisk { int major; // 主设备号 int first_minor; int minors; // 次设备号范围(1: 只有整盘;>1: 支持分区) char disk_name[DISK_NAME_LEN]; // 名称如 "sda" struct disk_part_tbl __rcu *part_tbl; // 分区表(索引从 0 开始) const struct block_device_operations *fops; // open/release/ioctl 等 struct device part0; // 整个设备对应的 device int flags; }; ``` > 分区设备 `/dev/sda1` 实际对应 `part_tbl->part[1]`,其 `bdev` 会被创建。 --- ## ⚙️ 三、I/O 请求生命周期(以读为例) ```text 1. VFS → ext4_readpage() 2. create BIO for page (sector + len) 3. submit_bio(REQ_OP_READ, bio) 4. generic_make_request() ├→ 查找目标 gendisk 和分区 ├→ 更新统计信息 (part_stat_inc()) └→ 调用 q->make_request_fn() → 默认为 blk_mq_make_request() 5. blk_mq_make_request() ├→ 获取当前 CPU 的 soft queue (ctx) ├→ 分配 hctx(可能基于 CPU 绑定) ├→ 加入 hctx->rq_list └→ 标记需要处理 (__blk_mq_sched_insert_request) 6. 触发运行(软中断或 tasklet) 7. 调用驱动 .queue_rq() 函数(如 nvme_queue_rq) 8. 驱动将命令写入寄存器或共享内存(如 NVMe doorbell) 9. 设备完成操作后触发中断 10. 中断处理函数调用 bio_endio(bio) → 执行 bi_end_io 回调 11. 页面被标记为 unlocked → wake_up_page() ``` --- ## 💡 四、编程示例:在模块中创建虚拟块设备(vbd) 以下是个可在 Linux 6.1 上运行的简单虚拟块设备模块: ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/blkdev.h> #include <linux/genhd.h> #include <linux/bio.h> #define VBD_SIZE (16 * 1024 * 1024) // 16MB #define VBD_SECTOR_SIZE 512 #define VBD_NUM_SECTORS (VBD_SIZE / VBD_SECTOR_SIZE) static struct request_queue *vbd_queue; static struct gendisk *vbd_disk; static u8 *vbd_data; // 模拟存储空间 // 请求处理函数(仅用于调试,实际应使用 blk-mq) static blk_status_t vbd_submit_bio(struct bio *bio) { struct bio_vec bvec; struct bvec_iter iter; sector_t sector = bio->bi_iter.bi_sector; if (sector + (bio_bytes(bio) >> 9) > VBD_NUM_SECTORS) { bio_io_error(bio); return BLK_STS_IOERR; } bio_for_each_segment(bvec, bio, iter) { void *daddr = vbd_data + (sector << 9); void *src = kmap_atomic(bvec.bv_page) + bvec.bv_offset; if (bio_data_dir(bio) == READ) memcpy(src, daddr, bvec.bv_len); else memcpy(daddr, src, bvec.bv_len); kunmap_atomic(src); sector += bvec.bv_len >> 9; } bio_endio(bio); // 完成请求 return BLK_STS_OK; } static int __init vbd_init(void) { int ret; // 分配模拟内存 vbd_data = kzalloc(VBD_SIZE, GFP_KERNEL); if (!vbd_data) return -ENOMEM; // 分配请求队列(使用 blk-mq 默认行为) vbd_queue = blk_alloc_queue(NUMA_NO_NODE); if (!vbd_queue) { ret = -ENOMEM; goto out_free; } blk_queue_make_request(vbd_queue, vbd_submit_bio); blk_queue_logical_block_size(vbd_queue, VBD_SECTOR_SIZE); // 分配磁盘结构 vbd_disk = alloc_disk(16); // 支持 16 个分区 if (!vbd_disk) { ret = -ENOMEM; goto out_cleanup_queue; } vbd_disk->major = 0; // 动态分配主设备号 vbd_disk->first_minor = 0; vbd_disk->minors = 16; strcpy(vbd_disk->disk_name, "vbd"); vbd_disk->fops = &vbd_fops; vbd_disk->queue = vbd_queue; set_capacity(vbd_disk, VBD_NUM_SECTORS); add_disk(vbd_disk); printk(KERN_INFO "Virtual block device %s (%d MB) registered\n", vbd_disk->disk_name, VBD_SIZE >> 20); return 0; out_cleanup_queue: blk_cleanup_queue(vbd_queue); out_free: kfree(vbd_data); return ret; } static void __exit vbd_exit(void) { del_gendisk(vbd_disk); put_disk(vbd_disk); blk_cleanup_queue(vbd_queue); kfree(vbd_data); printk(KERN_INFO "vbd module removed\n"); } // 必须定义 file_operations(即使为空) static const struct block_device_operations vbd_fops = { .owner = THIS_MODULE, .report_zones = NULL, }; module_init(vbd_init); module_exit(vbd_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Simple Virtual Block Device for Linux 6.1"); ``` 📌 编译 Makefile: ```makefile obj-m += vbd.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean install: sudo insmod vbd.ko remove: sudo rmmod vbd ``` 📌 加载后查看设备: ```bash dmesg | tail lsblk | grep vbd ``` 输出示例: ``` vbd 259:0 0 16M 0 disk ``` --- ## 🛠️ 五、常用调试工具与接口 | 工具 | 用途 | |------|------| | `cat /proc/partitions` | 查看当前所有分区 | | `lsblk` | 列出块设备树状结构 | | `iostat -x 1` | 实时监控 I/O 性能 | | `blktrace` | 跟踪块设备请求路径 | | `echo "dump" > /sys/block/sda/trace/act_mask` | 启用跟踪(需配置) | --- ## 📈 六、Linux 6.1 新特性(相比早期版本) | 特性 | 描述 | |------|------| | 更完善的 `bio` 生命周期管理 | 使用 refcount 控制释放 | | 支持 `BIO_FLAG_NOWAIT` | 非阻塞 I/O 提交(配合 io_uring 使用) | | blk-crypto inline support | 为 UFS/EMMC 提供原生加密卸载 | | blk-mq polling mode 改进 | 更低延迟轮询机制 | | REQ_OP_ZONE_APPEND 支持 | 用于 zoned block devices(如 SMR 硬盘) | --- ### 💎 总结 | 组件 | 作用 | |------|------| | `bio` | I/O 请求载体,支持 scatter-gather | | `request_queue` + `blk-mq` | 多队列调度框架,提升并发性能 | | `gendisk` | 块设备抽象,包含分区信息 | | `block_device_operations` | 用户空间操作钩子(open/ioctl) | | `AF_ALG` / `dm-crypt` | 上层应用利用 block+crypto 实现透明加密 | --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值