您的位置首页>企业动态>

linux中block驱动的编写详解

导读大家好,我是极客范的本期栏目编辑小友,现在为大家讲解linux中block驱动的编写详解问题。序IIC、发光二极管、钥匙等。都属于字符设备,这

大家好,我是极客范的本期栏目编辑小友,现在为大家讲解linux中block驱动的编写详解问题。

IIC、发光二极管、钥匙等。都属于字符设备,这些设备的驱动程序是所有类型的驱动程序中最简单的。Block设备是区别于character设备的另一种类型,这两种类型的设备在linux的驱动结构上有很大的区别。一般来说,块设备驱动比字符设备驱动复杂得多,在IO操作上也表现出很大的差异。缓冲区、IO调度、请求队列等都是与块设备驱动相关的概念。

本章从驱动程序小白(指他自己)的个人实际出发,我们不想知道深奥的XXX,而是从最简单的例子开始,对block设备驱动程序的结构有一个大概的了解。在修远这是一条很长的路。司机是个大坑,够你填10年。慢慢学。

1.块状设备结构

块设备是指磁盘和光盘等硬件存储介质。块设备驱动连接块设备和用户空间,实现用户空间对磁盘的海量数据访问。如下图所示,整个子系统包括虚拟文件系统、块IO调度层、块设备驱动和特定的块设备。块设备与字符设备的不同之处在于,它以块为单位接收输入并返回输出,而字符设备则以字节为单位。Block设备支持随机访问,读写速度比字符设备快,所以驱动的性能也很重要。这就是为什么块设备的驱动结构和字符设备的驱动结构是分开写的。区块是读写的最小单位。不同的文件系统具有不同的块大小,但它必须是2的指数,并且不能超过页面大小。常用的大小有512字节、1字节、4K字节等。

虚拟文件系统(VFS):它隐藏了各种硬件的细节,为用户操作不同的硬件提供了统一的界面。它基于不同的文件系统格式,如EXT、FAT等。用户在设备上的操作都是通过VFS完成的,在VFS之上是打开、关闭、写入和读取等功能API。

映射层:该层主要用于确定文件系统的块大小,然后计算请求的数据包含多少块。同时,调用特定的文件系统函数来访问文件的信息节点,以确定磁盘上请求数据的逻辑地址。

IO调度器:这部分是linux block系统中非常关键的部分,涉及到如何最高效地接收用户请求和访问硬件磁盘中的数据。

块驱动程序:完成与块设备的特定交互。

2.驾驶员详细说明

通过编写vmem_disk驱动程序了解块驱动程序的结构。vmem_disk是一种模拟磁盘,其数据实际存储在RAM中。它通过vmalloc()分配的内存空间来模拟磁盘,并通过块设备访问该内存。现在看看它的主要结构。

2.1块_设备_操作

Block_device_operations类似于字符设备驱动程序中的file_operations结构。它是块设备上各种操作的集合,定义代码如下:

结构块_设备_操作{

int (*open) (struct block_device *,fmode _ t);

int (*release) (struct gendisk *,fmode _ t);

int(* lock _ ioctl)(struct block _ device *,fmode_t,无符号,无符号长整型);

int(* ioctl)(struct block _ device *,fmode_t,无符号,无符号long);

int(* compat _ ioctl)(struct block _ device *,fmode_t,无符号,无符号long);

int(* direct _ access)(struct block _ device *,sector_t,void **,无符号长*);

int(* media _ changed)(struct gendisk *);

int(* revalidate _ disk)(struct gendisk *);

int(* getgeo)(struct block _ device *,struct HD _ geometry *);

struct模块*所有者;

};

1)打开并释放

int (*open)(struct inode *inode,struct file * filp);

int(* release)(struct inode * inode,struct file * filp);

类似于这个字符设备驱动程序,设备在打开和关闭时都会被调用。

2)输入输出控制

int (*ioctl)(结构索引节点*索引节点,结构文件*filp uusignwd intcmd,无符号长参数)

类似于这个字符设备驱动程序中的ioctrl,它也用于系统调用。块包含大量标准请求,由linux通用块设备层处理,因此大多数ioctrl函数都很短。

/p>

3) 介质改变

int (*check_media_change) (kdev_t);int (*revalidate) (kdev_t);

像磁盘、CD-ROM等块设备是可插拔的,因此需要有个函数来检测设备是否存在。当介质发生改变,使用revalidate_disk来响应,给驱动一个机会进行必要的工作来使介质准备好。

4) 获得驱动信息

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

该函数根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry包含磁头、扇区、柱面等信息。

所以我们要填充这个结构体信息,并定义其对应函数。填充如下:

static struct block_device_operations vmem_fops={ .owner=THIS_MODULE, .getgeo=vmem_getgeo, .ioctl=vmem_ioctl, .open=vmem_open, .release=vmem_release,};

我们只定义了open、release、ioctrl、getgeo函数。为了简化这个驱动,我们把open、release、ioctrl函数的具体内容也都省略了,只是给出一个定义,没有任何有效内容。但是hd_geometry的信息需要填充,所以getgeo函数定义如下:

static int vmem_getgeo(struct block_device *bdev, struct hd_geometry *geo){ geo->cylinders=1; geo->heads=1; geo->sectors=BLK_SIZE/SECTOR_SIZE; return 0;}

定义了使用的块设备的柱面、磁头和扇区个数。

2.2 gendisk结构体

在linux内核中,用gendisk结构体来表示一个独立的磁盘设备。就像字符设备驱动中使用cdev结构体一样,它也包含主次设备号,需要分配内存,释放结构体和初始化操作。

1) 分配gendisk

分配函数为:

struct gendisk *alloc_disk(int minors);

2) 增加gendisk

这个是用于注册磁盘设备,函数为:

void add_disk(struct gendisk *gd);

3) 释放gendisk

当不再需要使用磁盘时候,需要释放这个结构体,也即释放其分配的内存。

void del_gendisk(struct gendisk *gd);

以上这些函数在快设备初始化和关闭驱动中调用。

2.3 请求处理

每个块设备驱动的核心是它的请求函数,实际的工作,至少如设备的启动,都是在这个函数里完成的。块设备驱动程序的request函数有以下原型:

void request(request_queue_t *queue);

当内核需要驱动程序处理读取、写入以及其它对设备的操作时,就会调用该函数。在其返回前,request函数不必完成所有队列中的请求。事实上,对大多数真实设备而言,它可能没有完成任何请求。

每个设备都有一个请求队列,这是因为对磁盘数据实际传入和传出发生的时间,与内核请求的时间相差很大,因此内核需要有一定灵活性,以安排在适当时刻(比如把影响相邻磁盘扇区的请求分成一组)进行传输。

我们用一个简单的request函数:

static void vmem_request(struct request_queue *q){ struct request *req; uint64_t pos=0; ssize_t size=0; struct bio_vec bvec; int rv=0; struct req_iterator iter; void *kaddr=NULL; while((req=blk_fetch_request(q)) != NULL){ spin_unlock_irq(q->queue_lock); pos=blk_rq_pos(req)*SECTOR_SIZE; size=blk_rq_bytes(req); if(pos+size>vdev->size){ printk(KERN_WARNING"beyond addr/n"); rv=-EIO; goto skip; } rq_for_each_segment(bvec, req, iter){ kaddr=kmap(bvec.bv_page); rv=vmem_transfer(vdev, pos, bvec.bv_len, kaddr+bvec.bv_offset, rq_data_dir(req)); if(rvqueue_lock); }}

Blk_fetch_request从请求队列中获取一个请求,当没有请求需要时,返回NULL。然后while中的程序开始处理这个请求。当请求队列创建的时候,request函数绑定了它,并且提供了一个自旋锁。当调用request函数时,该锁由内核控制。因此request函数是一个原子上下文中运行的。因此在获得request时,需要通过spin_unlock_irq函数来解锁。

然后通过blk_rq_pos和blk_rq_bytes来获得请求中的位置和大小。rq_for_each_segment是一个宏定义,其遍历一个请求中的所有bio。这里插入一下对bio的介绍:

从本质上讲,一个request结构是作为一个bio结构的链表实现的。Bio结构是在底层对部分块设备IO请求的描述。Bio结构体定义如下:

与bio对应的数据每次存放的内存不一定是连续的,bio_vec结构体用于描述与这个bio对应的所有内存,它并不总是在一个页面里,因此需要一个向量。IO调度算法将连续的bio合并成一个request,然后可以改善读写磁盘的性能。

遍历bio的时候,就可以定义一个transfer函数来完成bio的数据转移了。Rq_data_dir获得从request中得到数据传输方向,返回值0表示从设备读数据,非0表示写数据。Transfer中就可以通过简单的memcpy来完成数据拷贝:

static int vmem_transfer(struct vmem_device *vdev, uint64_t pos, ssize_t size, void *buffer, int write){ if(write) memcpy(vdev->buf+pos, buffer, size); else memcpy(buffer, vdev->buf+pos, size); return 0;}

如果一个请求不是文件系统请求,就将请求传递给end_request。当处理非文件系统请求时,传递0表示不能成功完成该请求。

2.4 设备初始化

在块设备初始化阶段,与字符设备类似。基本过程如下:

1) 注册块设备

vmem_major=register_blkdev(0,"VMEM");

第一个参数0表示由内核自动分配主设备号,如果成功注册就返回这个主设备号,如果注册失败就返回负值。

2) 定义设备结构体

这个设备结构体是自己定义的,一般包含gendisk、设备号、请求队列等。

struct vmem_device { struct gendisk *disk; struct request_queue *que; void *buf; spinlock_t lock; ssize_t size;};

3) vmem_dev结构体分配和buf分配

vdev=kzalloc(sizeof(struct vmem_device), GFP_KERNEL); if(!vdev){ printk(KERN_WARNING"vmem_device: unable to allocate mem/n"); goto out; } vdev->size=BLK_SIZE; vdev->buf=vmalloc(vdev->size); if(vdev->buf==NULL){ printk(KERN_WARNING"failed to vmalloc vdev->buf/n"); goto out_dev; }

Buf就是一个虚拟的磁盘。

4) 初始化请求队列

vdev->que=blk_init_queue(vmem_request, &vdev->lock);

5) 分配磁盘

disk=alloc_disk(1);

6) 填充vmem_dev结构体中的信息。

vdev->disk=disk; disk->major=vmem_major; disk->first_minor=1; disk->fops=&vmem_fops; disk->queue=vdev->que; disk->private_data=vdev; sprintf(disk->disk_name,"VMEM");

7)注册磁盘

set_capacity(disk, BLK_SIZE/SECTOR_SIZE); add_disk(disk);

3.实验

我们注册驱动,并看到在dev下面有VMEM设备,这个就是我们的虚拟磁盘设备文件。

然后将其格式化为ext2文件系统:

接下来我们就可以将其挂载并创建文件了。

总结

最后总结一下linux中block驱动的编写过程:

1) 填充request函数,这个函数在请求队列初始化中将喝队列绑定;

2) 定义vdev结构体,其中包含gendisk、request_queue等结构;

3) 定义设备初始化函数,并完成对disk的分配,注册,请求队列初始化工作;

4) 填充block_device_operations结构体;

5) 定义设备退出函数,主要是释放结构体;

编辑:hfy

引言

像IIC、LED、KEY等都属于字符设备,这些设备的驱动是所有驱动类型中最为简单的。块设备是另外一种不同于字符设备的类型,这两类设备在linux的驱动结构中有很大差异。总体来说,块设备驱动比字符设备驱动复杂的多,在IO操作上也表现出很大的不同。缓冲、IO的调度、请求队列等都是和块设备驱动相关的概念。

本章从驱动小白(指本人)的切身实际出发,先不去了解那些深奥的XXX,只从一个最简单的例子开始,对块设备驱动的结构有一个大体的了解。路漫漫其修远兮,驱动是一个大坑,够你用10年来填。慢慢学吧。

1. 块设备结构

块设备就是指磁盘、CD-ROM等硬件存储介质,块设备驱动连接了块设备和用户空间,实现用户空间对磁盘的大块数据访问。整个子系统如下图所示,包含虚拟文件系统,块IO调度层,块设备驱动以及具体的块设备。块设备不同于字符设备,它是以块为单位接收输入和返回输出,而字符设备是以字节为单位。块设备支持随机访问,而且其读写速度都快于字符设备,因此驱动的表现也至关重要。这也是为什么块设备驱动的结构和字符设备的驱动结构被分开来写。块是最小的读写单位,不同的文件系统有不同大小的块尺寸,但是它必须是2的指数,同时不能超过页大小。通常使用的大小有512字节,1K字节,4K字节等。

虚拟文件系统(VFS):隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口。其基于不同的文件系统格式,比如EXT,FAT等。用户程序对设备的操作都通过VFS来完成,在VFS上面就是诸如open、close、write和read的函数API。

映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址。

IO调度器:这部分是linux块系统中非常关键的部分,其涉及到如何接收用户请求并能最高效去访问硬件磁盘中的数据。

Block driver:完成和块设备的具体交互。

2. 驱动程序详解

通过编写一个vmem_disk驱动来了解block驱动的结构,vmem_disk是一种模拟磁盘,其数据实际上存储在RAM中。它通过vmalloc()分配出来的内存空间来模拟出一个磁盘,以块设备方式来访问这片内存。现在来看其主要结构。

2.1 block_device_operations

Block_device_operations类似于字符设备驱动中的file_operations结构,它是对块设备各种操作的集合,定义代码如下:

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 *, struct hd_geometry *); struct module *owner;};

1) 打开和释放

int (*open)(struct inode *inode ,struct file *filp);int (*release)(struct inode *inode ,struct file *filp);

这个和字符设备驱动类似,当设备被打开和关闭时将调用它们。

2) IO控制

int (*ioctl)(struct inode *inode,struct file *filp uusignwd intcmd,unsigned long arg)

这个和字符设备驱动中的ioctrl类似,也是用于系统调用。块设备包含大量的标准请求,这些标准请求由linux通用块设备层处理,因此大部分ioctrl函数相当短。

3) 介质改变

int (*check_media_change) (kdev_t);int (*revalidate) (kdev_t);

像磁盘、CD-ROM等块设备是可插拔的,因此需要有个函数来检测设备是否存在。当介质发生改变,使用revalidate_disk来响应,给驱动一个机会进行必要的工作来使介质准备好。

4) 获得驱动信息

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

该函数根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry包含磁头、扇区、柱面等信息。

所以我们要填充这个结构体信息,并定义其对应函数。填充如下:

static struct block_device_operations vmem_fops={ .owner=THIS_MODULE, .getgeo=vmem_getgeo, .ioctl=vmem_ioctl, .open=vmem_open, .release=vmem_release,};

我们只定义了open、release、ioctrl、getgeo函数。为了简化这个驱动,我们把open、release、ioctrl函数的具体内容也都省略了,只是给出一个定义,没有任何有效内容。但是hd_geometry的信息需要填充,所以getgeo函数定义如下:

static int vmem_getgeo(struct block_device *bdev, struct hd_geometry *geo){ geo->cylinders=1; geo->heads=1; geo->sectors=BLK_SIZE/SECTOR_SIZE; return 0;}

定义了使用的块设备的柱面、磁头和扇区个数。

2.2 gendisk结构体

在linux内核中,用gendisk结构体来表示一个独立的磁盘设备。就像字符设备驱动中使用cdev结构体一样,它也包含主次设备号,需要分配内存,释放结构体和初始化操作。

1) 分配gendisk

分配函数为:

struct gendisk *alloc_disk(int minors);

2) 增加gendisk

这个是用于注册磁盘设备,函数为:

void add_disk(struct gendisk *gd);

3) 释放gendisk

当不再需要使用磁盘时候,需要释放这个结构体,也即释放其分配的内存。

void del_gendisk(struct gendisk *gd);

以上这些函数在快设备初始化和关闭驱动中调用。

2.3 请求处理

每个块设备驱动的核心是它的请求函数,实际的工作,至少如设备的启动,都是在这个函数里完成的。块设备驱动程序的request函数有以下原型:

void request(request_queue_t *queue);

当内核需要驱动程序处理读取、写入以及其它对设备的操作时,就会调用该函数。在其返回前,request函数不必完成所有队列中的请求。事实上,对大多数真实设备而言,它可能没有完成任何请求。

每个设备都有一个请求队列,这是因为对磁盘数据实际传入和传出发生的时间,与内核请求的时间相差很大,因此内核需要有一定灵活性,以安排在适当时刻(比如把影响相邻磁盘扇区的请求分成一组)进行传输。

我们用一个简单的request函数:

static void vmem_request(struct request_queue *q){ struct request *req; uint64_t pos=0; ssize_t size=0; struct bio_vec bvec; int rv=0; struct req_iterator iter; void *kaddr=NULL; while((req=blk_fetch_request(q)) != NULL){ spin_unlock_irq(q->queue_lock); pos=blk_rq_pos(req)*SECTOR_SIZE; size=blk_rq_bytes(req); if(pos+size>vdev->size){ printk(KERN_WARNING"beyond addr/n"); rv=-EIO; goto skip; } rq_for_each_segment(bvec, req, iter){ kaddr=kmap(bvec.bv_page); rv=vmem_transfer(vdev, pos, bvec.bv_len, kaddr+bvec.bv_offset, rq_data_dir(req)); if(rvqueue_lock); }}

Blk_fetch_request从请求队列中获取一个请求,当没有请求需要时,返回NULL。然后while中的程序开始处理这个请求。当请求队列创建的时候,request函数绑定了它,并且提供了一个自旋锁。当调用request函数时,该锁由内核控制。因此request函数是一个原子上下文中运行的。因此在获得request时,需要通过spin_unlock_irq函数来解锁。

然后通过blk_rq_pos和blk_rq_bytes来获得请求中的位置和大小。rq_for_each_segment是一个宏定义,其遍历一个请求中的所有bio。这里插入一下对bio的介绍:

从本质上讲,一个request结构是作为一个bio结构的链表实现的。Bio结构是在底层对部分块设备IO请求的描述。Bio结构体定义如下:

与bio对应的数据每次存放的内存不一定是连续的,bio_vec结构体用于描述与这个bio对应的所有内存,它并不总是在一个页面里,因此需要一个向量。IO调度算法将连续的bio合并成一个request,然后可以改善读写磁盘的性能。

遍历bio的时候,就可以定义一个transfer函数来完成bio的数据转移了。Rq_data_dir获得从request中得到数据传输方向,返回值0表示从设备读数据,非0表示写数据。Transfer中就可以通过简单的memcpy来完成数据拷贝:

static int vmem_transfer(struct vmem_device *vdev, uint64_t pos, ssize_t size, void *buffer, int write){ if(write) memcpy(vdev->buf+pos, buffer, size); else memcpy(buffer, vdev->buf+pos, size); return 0;}

如果一个请求不是文件系统请求,就将请求传递给end_request。当处理非文件系统请求时,传递0表示不能成功完成该请求。

2.4 设备初始化

在块设备初始化阶段,与字符设备类似。基本过程如下:

1) 注册块设备

vmem_major=register_blkdev(0,"VMEM");

第一个参数0表示由内核自动分配主设备号,如果成功注册就返回这个主设备号,如果注册失败就返回负值。

2) 定义设备结构体

这个设备结构体是自己定义的,一般包含gendisk、设备号、请求队列等。

struct vmem_device { struct gendisk *disk; struct request_queue *que; void *buf; spinlock_t lock; ssize_t size;};

3) vmem_dev结构体分配和buf分配

vdev=kzalloc(sizeof(struct vmem_device), GFP_KERNEL); if(!vdev){ printk(KERN_WARNING"vmem_device: unable to allocate mem/n"); goto out; } vdev->size=BLK_SIZE; vdev->buf=vmalloc(vdev->size); if(vdev->buf==NULL){ printk(KERN_WARNING"failed to vmalloc vdev->buf/n"); goto out_dev; }

Buf就是一个虚拟的磁盘。

4) 初始化请求队列

vdev->que=blk_init_queue(vmem_request, &vdev->lock);

5) 分配磁盘

disk=alloc_disk(1);

6) 填充vmem_dev结构体中的信息。

vdev->disk=disk; disk->major=vmem_major; disk->first_minor=1; disk->fops=&vmem_fops; disk->queue=vdev->que; disk->private_data=vdev; sprintf(disk->disk_name,"VMEM");

7)注册磁盘

set_capacity(disk, BLK_SIZE/SECTOR_SIZE); add_disk(disk);

3.实验

我们注册驱动,并看到在dev下面有VMEM设备,这个就是我们的虚拟磁盘设备文件。

然后将其格式化为ext2文件系统:

接下来我们就可以将其挂载并创建文件了。

总结

最后总结一下linux中block驱动的编写过程:

1) 填充request函数,这个函数在请求队列初始化中将喝队列绑定;

2) 定义vdev结构体,其中包含gendisk、request_queue等结构;

3) 定义设备初始化函数,并完成对disk的分配,注册,请求队列初始化工作;

4) 填充block_device_operations结构体;

5) 定义设备退出函数,主要是释放结构体;

编辑:hfy

.dfma { position: relative; width: 1000px; margin: 0 auto; } .dfma a::after { position: absolute; left: 0; bottom: 0; width: 30px; line-height: 1.4; text-align: center; background-color: rgba(0, 0, 0, .5); color: #fff; font-size: 12px; content:"广告"; } .dfma img { display: block; }
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。