Linux驱动开发笔记(十五)块设备驱动


前言

  字符设备的学习我们暂时告一段落了,这部分内容我们主要进行块设备驱动的学习。

一、块设备的引入

  块设备是Linux驱动的三大驱动类型之一,其通常是按照块为单位来访问数据,比如一块为512KB。块设备也是通过/dev目录下的文件系统节点来访问,块设备和字符设备区别仅仅在于内核内部管理数据的方式,也就是内核和驱动程序的接口不同。
  块设备除了给内核提供和字符设备一样的接口外,还提供了专门面向块设备的接口,块设备的接口必须支持挂装文件系统,通过此接口,块设备能够容纳文件系统,因此应用程序一般通过文件系统来访问块设备上的内容,而不是直接和设备打交道。

1.1 块设备和字符设备的区别

  块设备主要是针对存储设备,例如SD卡、EMMC、SPI FLASH等,所以块设备其实介绍存储设备驱动,其和字符设备的区别主要为:

  1. 块设备只能按照块为单位进行数据读写访问,块是VFS(虚拟文件系统)的基本传输单位,而字符设备是以字节为单位进行数据传输的;
  2. 块设备在结构上是可以随机进行读取的,各块中均使用缓冲区来暂时安放数据。字符设备主要是数据流设备,不需要进行缓存;
  3. 块设备的驱动和具体的存储外设有关,不同的设备其I/O口算法也存在差异;
  4. 一般不需要特别注意内核中设备的关键数据结构

1.2 磁盘的基本概念

 1.扇区 —— 读写基本单位
  每个磁道上可以存储数KB的数据,但计算机通常一次不需要读取这么多的数据,从而将磁道分为若干个扇区(Sector),扇区是硬盘存储数据的物理单位。每个扇区存储的数据大小是128×2^N(N=0,1,2,3)。从DOS时代开始,每个扇区是512KB,从此业界形成了这种不成文的规定。即使计算机只需要扇区中的某个字节,也要把这个扇区中的字节全部读入,然后选择那个字节。
 2.磁道 —— 数据存储的介质,可以分化成多个扇区
  每个盘面被划分为多个狭窄的同心圆环,这样的同心圆环叫做磁道(Track),数据存储在磁道上。磁道从最外圈(0号磁道)依次向内圈增长,硬盘数据的存放就是从最外圈开始。
 3.柱面 —— 多个不同磁盘相同磁道共同组成
  每个盘面相同编号的磁道形成一个柱面(Cylinder),柱面的编号方式与磁道相同。需要注意的是,硬盘数据的读写是按照柱面进行的(而不是盘面)。即磁头读写数据时从同一柱面内的0号磁头依次开始进行操作,只有同一cylinder上的磁头全部操作完毕后才会移到next cylinder。因为选取磁头 只需要电子切换,而选取柱面需要柱面切换。电子切换速度比机械快,所以读取数据按照柱面而不是盘面进行。
在这里插入图片描述

二、块设备驱动框架

2.1 框架详解

  块设备框架由文件系统层、驱动层和硬件层组成,其中驱动层可以细分为通用块层、IO调度层、块设备驱动。其中,映射层中的文件系统会把所有数据打包成一个request,当有很多request的时候就会在通用层共同组成一个request_queue,之后调度层会根据磁头位置调整request_queue的顺序进行优化,最后在设备驱动进行处理。
在这里插入图片描述
映射层(Mapping Layer)
  映射层是文件系统和底层存储设备之间的重要桥梁。它主要用于确定文件系统的块大小(block size),然后计算所请求的数据包含多少个块。同时,映射层会调用具体文件系统的函数来访问文件的inode,从而确定所请求的数据在磁盘上的逻辑地址。
通用块层(Generic Block Layer)
  通用块层是Linux内核中的中间层,位于块设备驱动层和文件系统层之间。它负责维持I/O请求在上层文件系统与底层物理磁盘之间的关系。通用块层通过bio(Block I/O)结构体来表示和管理一个I/O请求。其主要功能包括:抽象硬件细节、管理I/O请求、缓存和缓冲区管理。
I/O调度层(I/O Scheduler Layer)
  I/O调度层是通用块层的一部分,负责决定块设备上的I/O请求的执行顺序。其目标是优化系统的性能和响应速度。常见的I/O调度算法包括:
 CFQ(完全公平队列):
根据进程的I/O请求历史和权重来调度I/O请求,试图公平地分配磁盘带宽给不同的进程。CFQ通过将I/O请求划分为不同的队列并进行轮转调度,实现了较高的公平性。
 Deadline(截止时间):
使用截止时间来调度I/O请求。Deadline调度器将I/O请求分为读取和写入两类,并尽量在截止时间之前完成这些请求。它通过设定读写请求的时间限制,保证了I/O操作的及时性和较低的延迟。
 NOOP(无操作):
简单地按照先到先服务(FIFO)的方式进行调度,不做任何优化。NOOP适用于那些具有自身优化机制的存储设备(如SSD),或者在I/O调度的开销远大于设备本身的情况下使用。
块设备驱动层(Block Device Driver Layer)
  块设备驱动层负责与具体的硬件设备进行直接的交互。它管理块设备的I/O操作,并将这些操作提交到底层硬件。其主要组成部分和功能包括:
 请求队列(Request Queue):
请求队列用于管理和存储待处理的I/O请求。对于一些响应速度较慢的磁盘设备,请求队列可以缓冲这些请求,以提高系统的整体效率。
 请求合并和排序:
在向块设备提交请求之前,内核会先对请求进行合并和排序。这一预处理操作有助于减少磁盘寻道时间和I/O操作的次数,从而提高访问效率。
 调度和派发请求:
块设备驱动层利用I/O调度程序来决定请求的排列顺序和派发时机。调度程序通过优化请求的顺序和调度策略,最大化地利用磁盘资源,提升系统性能。
 驱动程序接口:
块设备驱动程序提供一组接口,供操作系统调用。这些接口包括打开设备、关闭设备、读写数据等。驱动程序负责将这些高层操作转换为底层的硬件指令,并执行相应的I/O操作。

2.2 核心数据结构

2.2.1 block_device

  block_device 是一个重要的数据结构,用于表示一个具体的块设备对象,如硬盘或硬盘的分区。该结构体包含了管理和操作块设备所需的各种信息。

struct block_device {
	dev_t bd_dev; 	/* not a kdev_t - it's a search key */
	int bd_openers;
	struct inode *bd_inode; 	/* will die */
	struct super_block *bd_super;
	struct mutex bd_mutex; 	/* open/close mutex */
	struct list_head bd_inodes;
	void * bd_claiming;
	void * bd_holder;
	int bd_holders;
	bool bd_write_holder;
#ifdef CONFIG_SYSFS
	struct list_head bd_holder_disks;
#endif
	struct block_device *bd_contains;
	unsigned bd_block_size;
	struct hd_struct *bd_part;
	/*number of times partitions within this device have been opened.*/
	unsigned bd_part_count;
	int bd_invalidated;
	/*  bd_disk成员变量为gendisk结构体指针类型,内核使用block_device来表示
	具体块设备对象(硬盘/分区),若是硬盘的话bd_disk就指向通用磁盘结构gendisk */
	struct gendisk *bd_disk;  
	struct request_queue *bd_queue;
	struct list_head bd_list;
	unsigned long bd_private;
	/* The counter of freeze processes */
	int bd_fsfreeze_count;
	/* Mutex for freeze */
	struct mutex bd_fsfreeze_mutex;
};
  • bd_dev:设备号,dev_t 类型,唯一标识一个设备,用作搜索键。
  • bd_openers:记录打开设备的次数,反映了当前有多少用户或进程正在使用该设备。
  • bd_inode:指向与设备关联的 inode 结构体,inode 是文件系统中的一个重-要数据结构,包含文件或设备的元数据。此成员将来可能会被移除。
  • bd_super:指向设备上的文件系统的超级块 super_block 结构体,超级块包含文件系统的全局信息。
  • bd_mutex:用于同步设备的打开和关闭操作的互斥锁,确保这些操作的原子性。
  • bd_inodes:一个链表头,链表中包含了与设备相关的所有 inode 结构体。
  • bd_claiming:指向声称正在使用该设备的对象,用于协调设备的独占访问。
  • bd_holder:指向当前持有设备的对象,用于跟踪谁正在使用设备。
  • bd_holders:持有设备的对象计数,记录有多少对象当前持有该设备。
  • bd_write_holder:布尔值,表示是否有持有者正在写设备。
  • bd_holder_disks:如果启用了 SYSFS 配置选项,此链表用于管理持有设备磁盘的对象。
  • bd_contains:指向包含该设备的块设备(对于分区,它指向整个硬盘)。
  • bd_block_size:块设备的块大小,单位是字节,决定了设备的最小读写单位。
  • bd_part:指向描述块设备分区的结构体 hd_struct。
  • bd_part_count:记录该设备内的分区被打开的次数。
  • bd_invalidated:指示设备是否已失效,如果设备失效,该值为非零。
    -bd_disk:指向描述硬盘的通用磁盘结构 gendisk,内核使用 block_device 来表示具体的块设备对象(如硬盘或分区),如果是硬盘,bd_disk 就指向 gendisk 结构体。
  • bd_queue:指向块设备的请求队列 request_queue,用于管理设备的 I/O 请求。
  • bd_list:块设备的链表头,用于将块设备链接到全局设备链表中。
  • bd_private:私有数据,用于驱动程序保存特定于设备的私有信息。
  • bd_fsfreeze_count:文件系统冻结计数器,记录冻结文件系统的次数。
  • bd_fsfreeze_mutex:用于文件系统冻结的互斥锁,确保冻结和解冻操作的原子性。

2.2.2 gendisk

  gendisk是Linux内核中用于描述通用磁盘(Generic Disk)的数据结构。它包含了与磁盘设备相关的各种信息,如设备号、设备名称、操作函数集、请求队列等。在内核中,这个结构体用于管理和操作磁盘设备。下面是对该结构体各个成员变量的详细解释:

struct gendisk {
    int major;              // 主设备号
    int first_minor;        // 第一个次设备号
    int minors;             // 次设备数量
    char disk_name[32];     // 设备名称
    struct block_device_operations *fops;  // 块设备操作集
    struct request_queue *queue;  // 请求队列
    void *private_data;     // 私有数据
};
  • major:主设备号(major number),用于唯一标识一类设备。
  • first_minor:第一个次设备号(minor number)。
  • minors:次设备数量,表示在这个通用磁盘结构下管理的次设备(或分区)的数量。
  • disk_name:设备名称,是一个字符数组,存储设备的名称。名称通常用于设备的识别和管理,比如 /dev/sda。
  • fops:块设备操作集(block_device_operations),指向 struct block_device_operations 结构体。这个结构体包含了一组函数指针,定义了块设备的各种操作,如打开设备、关闭设备、读写数据等。这些函数由具体的块设备驱动程序实现。
  • queue:请求队列(request_queue),指向 struct request_queue 结构体。请求队列用于管理对块设备的I/O请求。它负责处理和调度这些请求,提高I/O操作的效率。
  • private_data:私有数据指针,用于存储驱动程序的特定数据。这个指针通常用于保存驱动程序需要的额外信息,确保在操作设备时可以访问这些信息。

2.2.3 block_device_operations

  block_device_operations 是用于描述块设备操作的一组函数指针集合。这些操作函数由具体的块设备驱动程序实现,并提供了对块设备的各种操作接口,如打开、关闭、读写、控制等。值得注意的是,这个operations结构体是给文件系统提供接口函数,而不是给应用层。

struct block_device_operations {
	int (*open) (struct block_device *, fmode_t);
	void (*release) (struct gendisk *, fmode_t);
	int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
	int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	long (*direct_access)(struct block_device *, sector_t, void **, unsigned long *pfn, long size);
	unsigned int (*check_events) (struct gendisk *disk, unsigned int clearing);
	int (*media_changed) (struct gendisk *);
	void (*unlock_native_capacity) (struct gendisk *);
	int (*revalidate_disk) (struct gendisk *);
	int (*getgeo)(struct block_device *, struct hd_geometry *);
	void (*swap_slot_free_notify) (struct block_device *, unsigned long);
	struct module *owner;
};
  • open:用于打开块设备,当用户或内核模块尝试打开设备时调用。
  • remove:用于释放块设备,当设备不再使用时调用。
  • rw_page:用于读写指定的页。
  • ioctl:用于块设备的I/O控制操作。
  • direct_access:用于直接访问块设备的特定部分。
  • check_events:用于检查块设备上的事件,例如介质更改。
  • unlock_native_capacity:用于解锁设备的原始容量。
  • revalidate_disk:用于重新验证磁盘。
  • getgeo:用于获取磁盘信息,包括磁头、柱面和扇区等信息。
  • swap_slot_free_notify:交换槽释放通知函数指针。

2.2.4 request_queue、request 和 bio

  大家如果仔细观察的话会在 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引前面提到的request_queue、request 和 bio。这三者的关系有些类似于我们spi驱动框架一节讲述的transfer_list和spi_message,request_queue下挂载着第一个request结构体,而不同request之间可以通过next_rq进行链接,且其下挂载这bio结构体,不同的bio也可以通过bi_next进行串联。
在这里插入图片描述

//表示一个块设备的请求队列
struct request_queue {
    spinlock_t queue_lock;			//队列锁,用于保护队列操作的同步
    struct list_head queue;			//请求队列链表
    struct request  *last_merge;	//指向request结构体
    request_fn_proc *request_fn;	//请求处理函数指针
    make_request_fn *make_request_fn;//用于将 bio 转换为 request 的函数指针
    struct elevator_queue *elevator;//指向电梯算法调度器的指针
    struct gendisk *disk;			//关联的磁盘设备
    unsigned int nr_requests;		//队列中的请求数
    struct blk_plug plug;			//用于请求合并的结构体
    /* 其他成员变量 */
};
//一个块设备 I/O 请求
struct request {
    sector_t __sector;		//请求开始的扇区号。
    unsigned int nr_sectors;//请求涉及的扇区数。
    int cmd_flags;			//命令标志,如读/写标志
    struct bio *bio;		//链表中的第一个 bio 结构体。
    struct bio *biotail;	//链表中的最后一个 bio 结构体。
    struct request_queue *q;//指向请求队列的指针。
    struct list_head queuelist;//用于将请求加入队列的链表指针。
    struct list_head hash;	//用于哈希表的链表指针。
    struct request *next_rq;//指向下一个request的指针。
    /* 其他成员变量 */
};
//一个bio对应一个 I/O 请求
struct bio {
    sector_t bi_sector;         /* 要传输的第一个扇区 */
    struct bio *bi_next;        /* 下一个 bio */
    struct block_device *bi_bdev; /* 关联的块设备 */
    unsigned long bi_flags;     /* 状态、命令等标志 */
    unsigned long bi_rw;        /* 低位表示读/写,高位表示优先级 */
    struct bvec_iter bi_iter;   /* 迭代器,标明数据要操作的块设备的位置 */
    unsigned short bi_vcnt;    		  /* bio_vec 数量 */
    unsigned short bi_idx;    		  /* 当前 bvl_vec 索引 */
    unsigned short bi_phys_segments;  /* 不相邻的物理段的数目 */

    /* 物理合并和 DMA remap 合并后不相邻的物理段的数目 */
    unsigned short bi_hw_segments;

    unsigned int bi_size;       /* 以字节为单位所需传输的数据大小 */

    /* 为了计算最大的硬件尺寸,我们考虑这个 bio 中第一个和最后一个
       可合并的段的尺寸 */
    unsigned int bi_hw_front_size;
    unsigned int bi_hw_back_size;
    
    unsigned int bi_max_vecs;   /* bio 能持有的最大 bvl_vec 数量 */
    struct bio_vec *bi_io_vec;  /* 实际的 vec 列表 */
    bio_end_io_t *bi_end_io;    /* I/O 操作结束后的回调函数 */
    atomic_t bi_cnt;            /* 引用计数 */
    void *bi_private;           /* 私有数据 */

    bio_destructor_t *bi_destructor; /* 析构函数 */

    /* 
     * 我们可以在 bio 末尾内联一定数量的 vecs,
     * 以避免对少量 bio_vec 进行双重分配。
     * 这个成员必须显然地保持在 bio 的最后。
     */
    struct bio_vec bi_inline_vecs[0];
};

//数据传输最小单位
struct bio_vec {		
    struct page  *bv_page; //指向用于数据传输的页面所对应的page对象
    unsigned int bv_len;   //表示当前要传输的数据大小		
    unsigned int bv_offset;//表示数据在页面内的偏移量	
};

//
struct bvec_iter {
    sector_t bi_sector;
    unsigned int bi_size;
    unsigned int bi_idx;
    unsigned int bi_bvec_done;
};

  接下来我们刨开 bio 结结构体,bi_rw 用来说明本次块 I/O 操作是还是写,bi_io_vec 指向了一个bio_vec 结构对象数组中的首元素,一个 bio 由多个 bio_vec 来组没,要完成一个块I /O操作,就要历 bio 中的每一个 bio _vec。这时我们可以使用bi_iter进行遍历,它描述了正在进行的 bio_vec。
在这里插入图片描述

三、块设备驱动的编写

3.1 块设备驱动注册

1. 注册块设备
  这里我们通常使用register_blkdev函数来获取块设备的设备号,块设备允许按块为单位的访问,常见的块设备包括硬盘、光驱、USB存储设备等。

//用于注册块设备,获得一个主编号
int register_blkdev(unsigned int major, const char *name);
  • 参数
    • major: 设备的主设备号。如果设置为 0,系统会自动分配一个主设备号。
    • name: 设备的名称。
  • 返回值
    • 成功:主设备号(正数)
    • 失败:负数

2. 申请并初始化gendisk设备
  使用 gendisk 之前要先申请, alloc_disk 函数用于申请一个 gendisk,函数原型如下:

//申请gendisk
struct gendisk *alloc_disk(int minors);
  • 参数
    • minors: 该磁盘可以管理的次设备号(分区)的数量。例如,设置为 1 表示该磁盘不支持分区,设置为 16 表示支持 15 个分区。
  • 返回值
    • 成功:指向 gendisk 结构体的指针
    • 失败:NULL
//用于初始化块设备请求队列的函数
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
  • 参数
    • rfn: 请求处理函数的指针。当队列中有新的 I/O 请求时,内核会调用这个函数处理请求。通常,它是设备驱动程序中用于实际处理 I/O 请求的函数。
    • lock: 指向用于保护请求队列的自旋锁的指针。在请求队列的操作中,这个锁用于同步对队列的访问。
  • 返回值
    • 成功:指向 request_queue 结构体的指针
    • 失败:NULL

3. 填充gendisk结构体
  将内核对块设备的读写的操作的地方 “请求队列“ 进行初始化,使用 blk_init_queue 函数来完成request_queue 的申请与初始化,函数原型如下:

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
  • 参数
    • rfn: 请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数request_fn_proc
    • lock: 指向用于保护请求队列的自旋锁的指针。在请求队列的操作中,这个锁用于同步对队列的访问。
  • 返回值
    • 成功:指向 request_queue 结构体的指针
    • 失败:NULL

  blk_fetch_request 函数来一次性完成请求的获取和开启,其函数原型如下:

struct request *blk_fetch_request(struct request_queue *q);
  • 参数
    • q: 指向请求队列 (request_queue) 结构体的指针。
  • 返回值
    • 成功:指向 request 结构体的指针,表示从请求队列中提取的请求;
    • 失败 :NULL
        获取到request后,那么就要使用request去获取bio中的数据缓冲区的数据,使用bio_data函数,其函数原型如下:
static inline void *bio_data(struct bio *bio);
  • 参数
    • bio: 指向 bio 结构体的指针。
  • 返回值
    • 成功:返回一个指针,表示 bio 结构体中的数据缓冲区的起始地址。

4. 添加gendisk
  使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中, add_disk 函数原型如下:

void add_disk(struct gendisk *disk);
  • 参数
    • disk: 指向 gendisk 结构体的指针。这个结构体表示一个块设备的磁盘,包含了磁盘的各种信息和操作函数。
//示例
static int __init mydisk_init(void) {
   /* 初始化设备数据缓冲区 */
   dev_data = vmalloc(dev_size);
   if (!dev_data) {
       printk(KERN_ERR "vmalloc failed\n");
       return -ENOMEM;
   }
   memset(dev_data, 0, dev_size);

      /* 1. 注册块设备*/
   major = register_blkdev(0, MY_DISK_NAME);
   if (major <= 0) {
       printk(KERN_ERR "register_blkdev failed\n");
       put_disk(mydisk);
       blk_cleanup_queue(myqueue);
       vfree(dev_data);
       return -EBUSY;
   }
   
   /* 2. 分配和初始化 gendisk 结构体 */
   mydisk = alloc_disk(MY_DISK_MINORS);
   if (!mydisk) {
       printk(KERN_ERR "alloc_disk failed\n");
       blk_cleanup_queue(myqueue);
       vfree(dev_data);
       return -ENOMEM;
   }
   
   // 初始化自旋锁
   spin_lock_init(&myqueue_lock);

   //初始化请求队列
   myqueue = blk_init_queue(my_request, &myqueue_lock);
   if (!myqueue) {
       printk(KERN_ERR "blk_init_queue failed\n");
       vfree(dev_data);
       return -ENOMEM;
   }  
   
   /* 3. 填充 gendisk 结构体*/
   mydisk->major = major;
   mydisk->first_minor = 0;
   mydisk->fops = &my_fops;
   mydisk->queue = myqueue;
   set_capacity(mydisk, 1024); /* 设置容量为 1024 扇区 (512KB) */

   /* 4. 添加磁盘 */
   add_disk(mydisk);
   printk(KERN_INFO "mydisk: registered with major number %d\n", major);
   	...
   	
   return 0;
}

3.2 块设备驱动注销

  这里可以理解为注册函数的逆过程,具体流程不再详述,这里仅介绍一下各部分函数及框架。

//从内核的块设备子系统中删除一个磁盘,使其不再可见。
del_gendisk(struct gendisk *disk)
  • 参数
    • disk: 指向 gendisk 结构体的指针,该结构体表示要删除的磁盘。
//释放 gendisk 结构体的引用计数,并在计数变为零时释放该结构体。
put_disk(struct gendisk *disk)
  • 参数
    • disk: 指向 gendisk 结构体的指针,该结构体表示要释放的磁盘。
//清理并释放请求队列结构体。
blk_cleanup_queue(struct request_queue *q)
  • 参数
    • q: 指向请求队列 (request_queue) 结构体的指针。
//注销块设备驱动程序并释放与其主设备号相关联的资源。
unregister_blkdev(int major, const char *name)
  • 参数
    • major: 要注销的块设备的主设备号。
    • name: 块设备的名称。
//示例
static void __exit mydisk_exit(void) {
    del_gendisk(mydisk);           // 删除磁盘
    put_disk(mydisk);              // 释放 gendisk 结构体
    blk_cleanup_queue(myqueue);    // 清理请求队列
    unregister_blkdev(major, MY_DISK_NAME); // 注销块设备驱动程序
    vfree(dev_data);               // 释放设备数据缓冲区
}

3.3 request函数的编写

  request函数是blk_init_queue的关键参数,在块设备驱动程序中,request 函数是处理 I/O 请求的核心函数。它定义了如何处理传入的块设备 I/O 请求,并将这些请求传递给底层硬件。

//遍历请求中的每个 bio 结构体。
#define rq_for_each_bio(bio, rq) \
    list_for_each_entry(bio, &rq->bio_list, bi_list)
  • 参数
    • bio: bio 结构体的指针,用于在循环中存储当前的 bio。
    • rq: 指向 request 结构体的指针。
//获取 bio 结构体中的数据缓冲区。
static inline void *bio_data(struct bio *bio);
  • 参数
    • bio: 指向 bio 结构体的指针。
  • 返回值
    • 成功:指向 bio 结构体中数据缓冲区的指针。
//判断 bio 请求是读还是写。
#define bio_data_dir(bio) \
    ((bio)->bi_rw & REQ_WRITE ? WRITE : READ)
  • 参数
    • bio: 指向 bio 结构体的指针。
  • 返回值
    • 返回 READ 或 WRITE,表示 bio 请求的方向。
      功能
//完成请求并通知内核。
void __blk_end_request_all(struct request *req, int error);
  • 参数
    • req: 指向 request 结构体的指针。
    • error: 错误码。如果请求成功,通常传入 0;如果失败,则传入负的错误码。
//示例
static void my_request(struct request_queue *q) {
    struct request *req;
    // 获取下一个请求
    while ((req = blk_fetch_request(q)) != NULL) {  
        struct bio *bio;
		// 遍历请求中的每个 bio 结构体
        rq_for_each_bio(bio, req) {  
        	// 获取 bio 结构体中的数据缓冲区
            void *buffer = bio_data(bio);
            // 获取 bio 请求的起始扇区
            sector_t sector = bio->bi_iter.bi_sector;  
            // 获取 bio 请求的扇区数
            unsigned int sectors = bio_sectors(bio); 
            // 计算数据偏移量 
            unsigned long offset = sector * KERNEL_SECTOR_SIZE; 
            // 计算数据字节数
            unsigned long nbytes = sectors * KERNEL_SECTOR_SIZE;  
			// 检查请求是否越界
            if ((offset + nbytes) > dev_size) { 
                printk(KERN_NOTICE "mydisk: bad request: sector=%llu, count=%u\n", sector, sectors);
                // 完成请求并返回错误
                __blk_end_request_all(req, -EIO);  
                continue;
            }
            
			 // 判断请求是读操作还是写操作
            if (bio_data_dir(bio) == READ) {  
            	// 读操作:将数据从设备缓冲区复制到用户缓冲区
                memcpy(buffer, dev_data + offset, nbytes);  
            } 
            else {
            	// 写操作:将数据从用户缓冲区复制到设备缓冲区
                memcpy(dev_data + offset, buffer, nbytes);  
            }
        }
		 // 完成请求并通知内核
        __blk_end_request_all(req, 0); 
    }
}

四、磁盘设备使用方法

  1. 加载完驱动后使用ls /dev/查看是否存在驱动,如果存在可以利用ls /dev/mydisk -l查询磁盘信息,如下所示: 在这里插入图片描述
  2. 此时的硬盘并不能直接使用,需要进行初始化并分区mkfs.vfat /dev/mydisk
  3. 完成上面操作之后,我们可以创建一个temp文件夹进行测试,使用mount /dev/mydisk temp进行挂载
  4. 在temp里面编写内容,使用cat test查看
  5. 返回上级目录,用umount /dev/mydisk temp取消挂载,这时再次查看temp目录,发现里面没有文件,说明此时字符设备已经卸载了
    6.
    免责声明:本内容部分参考野火科技及其他相关公开资料,若有侵权或者勘误请联系作者。
  • 24
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值