【嵌入式Linux驱动程序-基础篇】- 块设备

# 块设备 1 块设备驱动程序框架 == 1.1 块设备加载过程 –   **在块设备的模块加载函数中,需要完成一些重要的工作,这些工作涉及将在后面的内容中进行详解,本节的目的是为了给出一个整体的概念。块设备驱动加载模块中需要完成的工作如下图所示。**

  (1) 使用alloc_disk()函数分配通用磁盘gendisk结构。
  (2) 通过register_blkdev()函数注册设备,该过程是一个可选的过程,也可以不用注册设备,驱动程序一样能够工作。   (3) 根据是否需要I/O调度,将分为两种情况,一种是使用请求队列进行数据传输,一种是不适用请求队列进行数据传输。    (4) 设置通用磁盘gendisk结构的成员变量。如给gendisk的major、fops、queue等赋初值。    (5) 使用add_disk()函数激活磁盘设备。当调用该函数后,就可以立即对磁盘设备进行操作,所有该函数的调用必须在所有准备工作就绪后。   下面的函数是一个不适用I/O队列的块设备加载模块,代码中涉及的重要概念将在后面讲解。这里,只需要对加载函数有个整体的了解即可。
static int __init xxx_blkdev_init(void)
{
    int ret;
    static struct gendisk *xxx_disk; // 通用磁盘结构
    static struct request_queue *xxx_queue; // 请求队列
    xxx_disk = alloc_disk(1); // 分配通用磁盘结构
    if(!xxx_disk){
        ret = -ENOMEM;
        goto err_alloc_disk;
    }
    if(register_blkdev(xxx_MAJOR,"xxx")<0){ // 请求队列初始化
        ret = -ENOMEM; // 请求队列初始化失败
        goto err_init_queue;
    }
    strcpy(xxx_disk->disk_name, XXX_DISKNAME); // 设定设备名
    xxx_disk->major = xxx_MAJOR; // 设备主设备号
    xxx_disk->first_minor = 0; // 设置次设备号
    xxx_disk->fops = &xxx_fops;
    xxx_disk->queue = xxx_queue;
    set_capacity(xxx_disk, xxx_BYTES>>9); // 设置设备容量
    add_disk(xxx_disk);
    return 0;
    err_init_queue: // 队列初始化失败
        unregister_blkdev(xxx_MJOR, "xxx");
    err_alloc_disk: // 分配磁盘失败
        put_disk(xxx_disk);
        return ret;
}
1.2 块设备卸载过程 –   **在块设备驱动的卸载模块中完成与模块加载函数相反的工作,如下图所示。**
  (1) 使用de;_gendisk()函数删除gendisk设备,并使用put_disk()函数删除对gendisk设备的引用。   (2) 使用blk_cleanup_queue()函数请求请求队列,并释放请求队列所占的资源。   (3) 如果在模块加载函数中使用resigter_blkdev()注册设备,那么需要在模块卸载函数中使用unregisgter_bkjdev()函数块设备,并释放对块设备的引用。   块设备驱动程序卸载函数的模板如下。
static void __exit xxx_blkdev_exit(void)
{
    del_gendisk(xxx_disk); // 删除gendisk磁盘
    put_disk(xxx_disk); // 删除gendisk磁盘引用
    blk_cleanup_queue(xxx_queue); // 删除请求队列
    unregister_blkdev(xxx_major, "xxx"); // 注销块设备
}
2 通用块 == 2.1 通用块层 –   **通用块层是一个内核组件,它处理来自系统其他组件发出的块设备请求。换句话说,通用块层包含了块设备操作的一些通用函数和数据结构。下图是块设备加载函数中用到的一些重要数据结构,如通用磁盘结构gendisk、请求队列结构request_queue、请求结构request、块设备I/O操作结构bio和块设备操作结构blokc_device_operation等。这些结构将在下面详细介绍。**

2.1 alloc_disk()函数对应的gendisk结构体

  现实生活中有许多具体的物理块设备,例如磁盘、光盘等。不同的物理块设备其结构是不一样的,为了将这些块设备公用属性在内核中统一,一般称为通用磁盘。
  (1) gendisk结构体
  在Linux内核中,gendisk结构体可以表示一个磁盘,也可以表示一个分区。这个结构体的定义代码如下:

struct gendisk{
    int major; // 设备主设备号
    int first_minor; // 第一次设备号
    int minors; // 磁盘可以进行分区的最大数目,如为1,则磁盘不能分区
    char disk_name[DISK_NAME_LEN]; // 设备名称
    struct disk_part_tbl *part_tbl;
    struct hd_struct_part0;
    struct block_device_operations *fops;
    struct request_queue *queue;
    void *private_data;
    int flags;
    struct devive *driverfs_dev;
    struct kobject *slave_dir;
    ...
};

  gendisk结构体的主要参数说明如下表所示。
这里写图片描述
  Linux内核中提供了一组函数来操作gendisk结构体,这些函数如下:

  1. 分配gendisk
      gendisk结构体是一个动态的结构,其成员是随系统状态不断变化的,所以不能静态地分配该结构,并对其成员赋值。对该结构的分配,应该使用内核提供的专用函数alloc_disk(),其原型如下:
struct gendisk *alloc_disk(int minors);

  minors参数是这个磁盘使用的次设备数量,起始就是磁盘的分区数量,磁盘的分区一旦由alloc_disk()函数设定,就不能修改。alloc_disk()的例子如下:

struct gendisk *xxx_disk == alloc_disk(16); // 分配一个gendisk设备
if(xxx_disk==NULL)
    goto error_alloc_disk;

  该代码分配了一个通用磁盘xxx_disk,用参数16表示,这个磁盘可以有15个分区,其中0用来表示整块设备。
2. 设置gendisk的属性
  使用alloc_disk()函数分配一个disk后,需要对该结构的一些成员进行设置,代码如下:

strcpy(xxx_disk->disk_name, XXX_DISKNAME); //设置设备名
xxx_disk->major = xxx_MAJOR;
xxx_disk->first_minot = 0;
xxx_disk->fops = &xxx_fops;
set_capacity(xxx_disk, xxx_BYTES>>9);// 设置设备容量,为了加快速度使用了位移9位的方法

  需要注意的是set_capacity()函数,该函数用来设置磁盘的容量,但不是以字节为单位,而是以扇区为单位。为了将set_capacity()函数解释清除,这里以扇区分为两种,一种是为例设备的真实扇区,二是内核中的扇区。物理设备的真是扇区大小有512、1024、2048字节等,但不管真是扇区的大小是多少,内核中的扇区大小都被定义为512字节。set_capacity()函数是以512字节为单位的,所以set_capacity()函数的第2个参数是xxx_BYTES>>9,表示设备的字节容量除以512后得到的内核扇区数。

  3. 激活gendisk
  当使用alloc_disk()函数分配了gendisk通用磁盘,并设置了相关属性后,就可以调用add_disk()函数向系统激活这个磁盘设备了。add_disk()函数的原型如下:

void add_disk(struct gendisk *disk);

  需要特别注意的是,一旦调用add_disk()函数,那么磁盘设备就开始工作了,所有关于gendisk的初始化必须在add_disk()函数之前。

  4. 删除gendisk
  当不再需要磁盘时,应该删除gendisk结构,可使用del_gendisk()函数完成这个功能,它和alloc_disk()函数是对应的。del_gendisk()函数的原型如下:

void del_gendisk(struct gendisk *disk);

  5. 删除gendisk的引用计数
  在调用del_gendisk()函数后,需要使用put_disk()函数减少gendisk的阴影计数,因为在add_disk()函数中增加了gendisk的引用计数。put_disk()函数的原型如下:

void put_disk(struct gendisk *disk);

2.2 块设备的注册和注销

  为了使内核知道块设备的存在,需要使用块设备注册函数。在不适用块设备时,也㤇注销块设备。块设备的注册和注销如下所述。

  1. 注销块设备函数register_blkdev()
  与字符设备的register_chrdev()函数对应的是register_blkdev()函数。对于大多数块设备驱动来说,第一个工作就是向内核注册自己。但值得注意的是,在Linux2.6内核中,对register_blkdev()函数的调用完全是可选的,内核中的register_blkdev()函数的功能已经逐渐减少。在新内核中,一般只完成两件事情。
- 根据参数分配一个块设备好。
- 在/proc.devices中新增加一行数据,表示块设备的设备信息。
  块设备的注册函数register_blkdev()的原型如下:

int register_blkdev(unsigned int major, const char *name);

  register_blkdev()函数的第一个参数是设备需要申请的主设备号,如果传入的主设备号是0,那么内核将动态的分配一个主设备号给设备。第2个参数是块设备的名字,该名字将在/proc/devices文件中显示。register_blkdev()函数成功时,返回申请的设备号;函数失败时,将返回一个负的错误码。在未来的内核中,register_blkdev()函数可能会被去掉,但是目前大多数驱动程序仍然使用它,使用register_blkdev()函数的一个例子如下所示:

if(xxx_major = register_blkdev(xxx_MAJOR, "xxx") < 0) // 注册设备
{
    ret = -EBUSY;
    goto err_alloc_disk;
}

  2. 注销块设备函数unregister_blkdev()
  与register_blkdev()函数对应的是注销函数unregister_blkdev(),其函数原型如下:

int unregister_blkdev(unsigned int major, const char *name);

  unregister_blkdev()函数的第一个参数是设备需要释放的主设备号,这个主设备是由register_blkdev()函数申请的。第二个参数是设备的设备名。当函数成功时返回0,失败时返回-EINVAL。
  使用unregiser_blkdev()函数的一个例子如下:

unregister_blkdev(xxx_major, "VirtualDisk");

2.3 请求队列

  简单地讲,一个块设备的请求队列就是包含块设备I/O请求的一个队列。这个队列使用链表线性的排列。请求队列中存储未完成的块设备I/O请求,并不是所有的I/O块请求都可以顺利地加入请求队列中。请求队列中定义了自己能处理的块设备请求限制。这些限制包括:请求的最大尺寸、一个请求能够包含的队理段数、硬盘扇区大小等。
  请求队列提供了一些处理函数,使不同块设备可以使用不同的I/O调度器,甚至不是用I/O调度器。一个I/O调度器的作用,是以最大的性能来优化请求的顺序。大多数I/O调度器控制着所有的请求,根据请求执行的顺序和位置对其进行排序,使块设备能够以最快的数据将数据写入和读出。
  请求队列使用request_queue结构体来描述,在include/linux/blkdev.h中定义了该结构体和其对应的操作函数。对于请求队列的这些了解是远远不够的,在后面用到请求队列时,将其进行详细解释。

2.4 设置gendisk属性中规定block_device_operations结构体

  在块设备中有一个和字符设备中file_operations对应的结构体block_device_operationss。其也是一个对块设备操作的函数集合,定义代码如下:

struct block_device_operations{
    int (*open)(struct block_device *, fmode_t);
    int (*release)(struct gendisk *, fmode_t);
    int (*locked_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
    int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
    int (*compat_ioctl)(struct block_device*,fmode_t, unsigned, unsigned long);
    int (*direct_access)(struct block_device *, sector_t, void **, unsigned long *);
    int (*media_changed)(struct gendisk*);
    int (*revalidate_disk)(struct gendisk *);
    int (*getgeo)(struct block_device *, strcut hd_geometry *);
    struct module *owner;
}

  下面对这个结构体的主要成员进行分析。
  1. 打开和释放函数

int (*open)(struct block_device *, fmode_t);
int (*release)(struct gendisk *, fmode_t);
&emsp;&emsp;open()函数在设备打开时被调用,release()函数在设备关闭时被调用。这两个函数完成的功能与字符设备的打开和关闭函数相似。

  2. I/O控制函数

int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
&emsp;&emsp;ioctl()函数实现了Linux的ioctl()系统调用,块设备的大多数标准请求已经有内核开发者实现了,驱动开发者需要实现的请求非常少,所以一般ioctl()函数也比较简单。

  3. 介质改变函数

int (*media_changed)(struct gendisk *);

  该函数会被内核调用来检查块设备是否改变,如果改变,则返回一个非0值,否则返回0值。这个函数仅对能够移动的块设备有效,不可以移动的块设备不需要实现这个函数。假设设备A用一个端口port1的第0位表示设备是否可用,该位为1时,表示设备被移除;该位为0时,表示设备仍然连接在主机上。那么介质改变函数可以写成如下形式:

int A_mdeia_changed(struct gendisk *gd)
{
        struct A_dev *dev = gd->private_data;// 从gendisk的私有数据中得到A设备结构体
        if(dev->port1&0x01 == 1)// A设备的端口1的第0位等于1,表示设备已经移除
        {
            return 1;// 1表示设备已经移除
        }
        else
        {
            return 0;// 0表示设备可用
        }
}

  4. 使介质有效函数
  当介质改变时,系统会调用revalidate()函数。该函数对设备进行重新的设置,以使设备准备好。介质有效函数可以写成如下形式:

int xxx_revalidate(struct gendisk *gd)
{
    struct xxx_dev *dev = gd->private_data;
    ...   // 设备重新设置
    return 0;
}

  5. 获取驱动器信息的函数

int (*getgeo)(struct block_device *, struct hd_geometry *);

  该函数根据驱动器的硬件信息填充一个hd_geometry结构体,hd_geometry结构包含了磁盘的磁头、扇区、柱面等信息。
  6. 模块指针

struct moudle *owner;

  几乎在所有的驱动程序中,该成员被初始化为THIS_MODULE,表示这个结构体属于目前运行的模块。

猜你喜欢

转载自blog.csdn.net/santapasserby/article/details/81503353