【Linux驱动】块设备驱动(一)—— 注册块设备

针对块设备驱动将分为两部分介绍,第一部分是注册块设备,即将块设备成功添加到内核;第二部分是介绍如何读写块设备,因为没有实际块设备,这里选择使用内存来模拟块设备。


目录

一、认识块设备

1、什么是块设备

2、块设备类型

二、模拟设备创建

三、注册块设备

1、申请主设备号

2、申请gendisk

3、初始化请求队列

4、初始化 gendisk

5、添加到内核

四、补充:分配内存

五、完整代码(待完善)


一、认识块设备

1、什么是块设备

块设备针对的是存储设备,如SD卡、EMMC、机械硬盘,与字符设备相比,块设备的读写是以块为单位,通过使用缓冲区来暂时保存数据,等满足一定条件再一次写入到块设备。这么做可以减少对块设备的擦除次数,降低块设备的读写频率,提高使用寿命。

2、块设备类型

Linux 内核提供了数据类型 block_device 来表示块设备,其中 gendisk 保存了块设备的属性信息及可执行的操作,如块设备号、设备容量、块设备分区等;bd_queue 缓存来自文件系统的读写请求,块设备会对这些请求逐个处理

【Linux驱动】块设备驱动(一)—— 注册块设备_第1张图片

struct block_device {
	/* ... */
	struct gendisk *	bd_disk;
	struct request_queue *  bd_queue;
    /* ... */
};

struct gendisk {
	int major;			            /* major number of driver */
	int first_minor;
	int minors;                     /* maximum number of minors, =1 for
                                         * disks that can't be partitioned. */

	char disk_name[DISK_NAME_LEN];	/* name of major driver */
    /* ... */

	const struct block_device_operations *fops;
	struct request_queue *queue;
	void *private_data;
}

注意:block_device 和 gendisk 都包含 request_queue 类型的成员, 实际上他们指向的是同一个请求队列,因此,在初始化时,初始化其中一个即可,一般初始化gendisk中的请求队列。

二、模拟设备创建

现在我们打算通过内存来模拟实现一个块设备,与上面介绍的流程相差无几,相当于将交互对象由块设备替换成了内存。

【Linux驱动】块设备驱动(一)—— 注册块设备_第2张图片

#define blkdev_NAME             "blkdev"               // 块设备名称
#define DISK_MINOR              1                      // 模拟块设备的分区数
#define DISK_SIZE               3 * 1024 * 1024        // 模拟块设备的大小(3MB),单位:字节

struct blk_dev {
    uint8_t*                diskbuf;         // 一小块内存,用于模拟块设备

    int                     major;          // 块设备主设备号
    spinlock_t              lock;           // 自旋锁
    struct gendisk*         gendisk;        // 块设备(保存了块设备相关信息)
    struct request_queue*   queue;          // 请求队列
};
static struct blk_dev blkdev;

三、注册块设备

第1~3步都是在为第4步初始化gendisk做准备。

1、申请主设备号

无论是字符设备还是块设备,都有一个设备号,下面为当前块设备申请主设备号。

相关API:

/**
 * @brief       注册块设备
 * @param major 主设备号(0表示由OS自动分配设备号,1~255表示自定义主设备号)
 * @param name  块设备名称
 * @return      成功返回主设备号,失败返回负值
 */
int register_blkdev(unsigned int major, const char *name);

/**
 * @brief       注销块设备
 * @param major 主设备号
 * @param name  块设备名称
 */
void unregister_blkdev(unsigned int major, const char *name);

实际应用:

/* 注册块设备 */
blkdev.major = register_blkdev(0, blkdev_NAME);
if (blkdev.major < 0)
{
    printk("block device register failed!\n");
    return -1;
}

2、申请gendisk

Linux内核为了方便保存块设备属性信息,提供了名为 gendisk 的数据类型。

相关API:

/**
 * @brief        申请gendisk
 * @param minors 申请的分区数(相当于在告诉内核块设备有多少个分区)
 * @return       成功返回gendisk的地址,失败返回NULL
 */
struct gendisk *alloc_disk(int minors);

/**
 * @brief       释放gendisk
 * @param gp    要删除的gendisk
 */
void del_gendisk(struct gendisk *gp);

实际应用:

blkdev.gendisk = alloc_disk(DISK_MINOR);
if (blkdev.gendisk == NULL)
{
    printk("gendisk allocate failed\n");
    unregister_blkdev(blkdev.major, blkdev_NAME);
    return -1;
}

3、初始化请求队列

请求队列是一种用于存储待处理的请求的数据结构,当用户发起I/O操作时,Linux内核可以有效管理这些请求,如优先级较高的请求优先处理,这样可以提高系统的性能和响应速度。

相关API如下

/**
 * @brief        初始化请求队列
 * @param rfn    请求处理函数
 *               函数指针:void (request_fn_proc) (struct request_queue *q)
 * @param lock   自旋锁    
 * @return       成功返回请求队列的地址,失败返回NULL
 */
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);

/**
 * @brief       删除请求队列
 * @param q     要删除的请求队列
 */ 
void blk_cleanup_queue(struct request_queue *q);
  • rfn: 当块设备收到一个IO请求时,请求处理函数会被触发来处理这个请求
  • lock:自旋锁,需要自己从外部传递,请求队列作为临界资源,自然需要考虑互斥问题

实际应用:

spin_lock_init(&blkdev.lock);
blkdev.queue = blk_init_queue(request_handle, &blkdev.lock);
if (blkdev.queue == NULL)
{
    printk("gendisk allocate failed\n");
    del_gendisk(blkdev.gendisk); 
    unregister_blkdev(blkdev.major, blkdev_NAME);
    return -1;
}

请求处理函数:

// 请求处理函数
void request_handle(struct request_queue *q)
{
}

4、初始化 gendisk

接下来将对 gendisk 结构体中的一部分成员进行初始化,有些是必须要初始化的,如主设备号、块设备操作函数、设备名、请求队列等。

每一个块设备都有大小,要设置块设备大小需要用到 set_capacity,函数声明如下:

/**
 * @brief        设置块设备大小
 * @param disk   gendisk 指针
 * @param size   块设备大小,单位: 扇区
 */
void set_capacity(struct gendisk *disk, sector_t size);

初始化内容如下:

// 块设备操作函数
static struct block_device_operations blkdev_fops = {
    .owner = THIS_MODULE,
};

blkdev.gendisk->major = blkdev.major;                    // 主设备号
blkdev.gendisk->first_minor = 0;                         // 起始次设备号
blkdev.gendisk->fops = &blkdev_fops;                     // 块设备操作函数
blkdev.gendisk->queue = blkdev.queue;                    // 块设备请求队列
strcpy(blkdev.gendisk->disk_name, blkdev_NAME);          // 块设备名称
set_capacity(blkdev.gendisk, DISK_SIZE/512);             // 告诉内核块设备大小,单位:扇区

5、添加到内核

最终将初始化好的 gendisk 添加到内核,相当于在告诉内核当前块设备的相关信息,因为内核无法主动得知块设备信息。

相关API:

/**
 * @brief        将gendisk添加到内核
 * @param disk   gendisk 指针
 */
void add_disk(struct gendisk *disk);

实际应用:

add_disk(blkdev.gendisk);

四、补充:分配内存

前面只是设置了块设备的大小为3MB,但实际上并没有分配这块内存,因为到下一步读写块设备才会需要操作内存,所以申请一块3MB的内存在当前阶段非必须。

分配内存可以使用kmalloc 或者 kzalloc,效果一样,区别在于kzalloc在分配的同时会先清空内存,对应的使用 kfree 来释放内存。

/**
 * @param size    分配的内存大小,单位: 字节
 * @param flags   内存分配的标志,用于指定内存分配的行为(如内存对齐、内存映射类型等)
 * @return        返回一个指向分配的内存块的指针,如果分配失败则返回NULL
 */
void* kzalloc(size_t size, gfp_t flags);

/**
 * @param ptr    内存块的地址
 */
void kfree(const void *ptr);
flags可选项 解析
GFP_KERNEL 表示内存分配应该在可抢占的内核上下文中进行,并且可以使用内核的页面置换机制(最常用的标志,用于普通的内核操作)
GFP_ATOMIC 表示内存分配应该在不可抢占的内核上下文中进行,且不可被页面置换,以避免死锁或其他错误。这通常用于中断处理程序或其他不能延迟等待内存分配的上下文。
GFP_DMA 表示内存分配应该返回可以通过内存DMA访问的内存块。这用于需要进行DMA传输的设备驱动程序。
GFP_DMA32 类似于GFP_DMA,但是指定返回的内存应该位于32位物理地址空间内,以满足32位DMA的限制。如果系统支持大于4GB的物理地址空间,则该标志将被忽略。
GFP_HIGHUSER 表示内存分配应该返回用户态进程可以访问的内存块。这用于在内核中为用户态进程分配内存。
GFP_NOFS 表示内存分配应该在文件系统内部的上下文中进行,且不能导致内核执行文件系统操作(如页面缓存)。这通常用于文件系统代码中,以避免死锁或其他错误。
GFP_NOWAIT 表示在无法立即获得所需的内存时,kmallocvmalloc函数应立即返回NULL,而不是等待内存可用再分配。
GFP_ZERO 表示分配的内存块应该在分配后清零。这个标志可以用于kmallockzalloc函数。

实际使用:

blkdev.diskbuf = kzalloc(DISK_SIZE, GFP_KERNEL);
if (blkdev.diskbuf == NULL)
{
    return -1;
}

五、完整代码(待完善)

将驱动代码编译成模块,加入到内核后,我们输入 fdisk -l 可以看到当前系统中已经注册的块设备,包括我们刚才注册的块设备。

【Linux驱动】块设备驱动(一)—— 注册块设备_第3张图片

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include            // 注册/注销块设备
#include         // 申请/释放gendisk
#include        // 申请/释放请求队列

#define blkdev_NAME             "blkdev"       // 块设备名称
#define DISK_MINOR              1               // 模拟块设备的分区数
#define DISK_SIZE               3 * 1024 * 1024        // 模拟块设备的大小,单位:字节

struct blk_dev {
    uint8_t*                diskbuf;         // 一小块内存,用于模拟块设备

    int                     major;          // 块设备主设备号
    spinlock_t              lock;           // 自旋锁
    struct gendisk*         gendisk;        // 块设备(保存了块设备相关信息)
    struct request_queue*   queue;          // 请求队列
};
static struct blk_dev blkdev;

static struct block_device_operations blkdev_fops = {
    .owner = THIS_MODULE,
};

void request_handle(struct request_queue *q)
{

}

static int __init blkdriver_init(void)
{
    /* 分配内存 */
    blkdev.diskbuf = kzalloc(DISK_SIZE, GFP_KERNEL);
    if (blkdev.diskbuf == NULL)
    {
        return -1;
    }

    /* 注册块设备 */
    blkdev.major = register_blkdev(0, blkdev_NAME);
    if (blkdev.major < 0)
    {
        printk("block device register failed!\n");
        return -1;
    }

    /* 申请 gendisk */
    blkdev.gendisk = alloc_disk(DISK_MINOR);
    if (blkdev.gendisk == NULL)
    {
        printk("gendisk allocate failed\n");
        unregister_blkdev(blkdev.major, blkdev_NAME);
        return -1;
    }

    /* 申请请求队列 */
    spin_lock_init(&blkdev.lock);
    blkdev.queue = blk_init_queue(request_handle, &blkdev.lock);
    if (blkdev.queue == NULL)
    {
        printk("gendisk allocate failed\n");
        del_gendisk(blkdev.gendisk); 
        unregister_blkdev(blkdev.major, blkdev_NAME);
        return -1;
    }

    /* 初始化 gendisk */
    blkdev.gendisk->major = blkdev.major;                    // 主设备号
    blkdev.gendisk->first_minor = 0;                         // 起始次设备号
    blkdev.gendisk->fops = &blkdev_fops;                     // 块设备操作函数
    blkdev.gendisk->queue = blkdev.queue;                    // 块设备请求队列
    strcpy(blkdev.gendisk->disk_name, blkdev_NAME);          // 块设备名称
    set_capacity(blkdev.gendisk, DISK_SIZE/512);             // 告诉内核块设备大小,单位:扇区
    
    /* 添加到内核 */
    add_disk(blkdev.gendisk);

    return 0;	
}
    
static void __exit blkdriver_exit(void)
{
    /* 释放内存 */
    kfree(blkdev.diskbuf);
    /* 注销块设备 */
    unregister_blkdev(blkdev.major, blkdev_NAME);
    /* 释放gendisk */
    del_gendisk(blkdev.gendisk); 
    /* 清除请求队列 */
    blk_cleanup_queue(blkdev.queue);
}

module_init(blkdriver_init);
module_exit(blkdriver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("author_name");

你可能感兴趣的:(linux,运维,服务器)