嵌入式linux 块设备驱动框架

        块设备是针对存储设备的,比如SD卡,EMMC,NAND Flash,机械硬盘等。因此块设备驱动其实就是这些存储设备的驱动,块设备和字符设备的区别有:

  • 块设备只能以块为单位进行读写访问,块是linux虚拟文件基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲
  • 块设备在结构上时可以进行随机访问的,对于这些设备的读写都是按块进行的块设备使用缓冲区来暂时存放数据,等条件成熟以后再一次性将缓冲区区的数据写入到块设备中。字符设备是顺序的数据流设备,字符设备是按字节进行访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且不需要按照固定大小进行访问。

块设备驱动框架

1,block_device结构体

        linux内核使用block_device表示块设备,block_device为结构体,定义在include/linux/fs.h文件中。

         对于block_device结构体,我们需要重点关注一下第21行的bd_disk成员变量,此成员变量为gendisk结构体指针类型,内核使用block_device来表示具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话bd_disk就指向通用磁盘结构gendisk

注册块设备

        和字符设备驱动一样,我们需要向内核注册新的块设备,申请设备号,块设备注册函数为register_blkdev,函数原型如下:

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

major:主设备号,为0的时候会自动分配设备号

name:块设备名字

返回值:0表示注册成功,负值表示注册失败,当major=0的时候,返回的就是主设备号

注销块设备

        和字符设备驱动一样,如果不使用某个设备了,就需要注销掉,函数为unregister_blkdev,函数原型如下:

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

2,gendisk结构体

        linux内核使用gendisk来描述一个磁盘设备,定义在include/linxu/genhd.h,内容如下:

         第5行,major为磁盘设备的主设备号

        第6行,first_minor为磁盘设备的主设备号

        第7行,minors为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,次设备号不同

        第21行,part_tbl为磁盘对应的分区表,为结构体disk_part_tbl类型,disk_part_tbl的核心是一个hd_struct结构体指针数组,此数组每一项都对应一个分区信息

        第24行,fops为块设备操作集,为block_device_operations结构体类型,和字符设备操作集file_operations一样,是块设备驱动的重点!!!

        第25行,queue为磁盘对应的请求队列,所以针对该磁盘设备的请求都放在此设备队列中,驱动程序需要处理此队列中的所有请求。

        编写块的设备驱动的时候需要分配并初始化一个gendisk,linux内核提供了一组gendisk操作函数,我们来看一下一些常用的API函数。

 (1)申请gendisk

        使用gendisk之前需要申请,alloc_disk函数用于申请一个gendisk,函数原型如下:

struct gendisk *alloc_disk(int minors)

 minors:次设备号数量,也就是gendisk对应的分区数量

返回值:成功:返回申请到的gendisk,失败:NULL

(2)删除gendisk

        如果要删除gendisk的话可以使用函数del_gendisk,函数原型如下:  

void de_gendisk(struct gendisk *gp)

gp:要删除的gendisk

(3)将gendisk添加进内核

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

void add_disk(struct gendisk *disk)

disk:要添加进内核的gendisk

(4)设置gendisk容量

        每一个磁盘都有容量,所以在初始化gendisk的时候还需要设置其容量,使用函数set_capacity,函数原型如下:

void set_capacity(struct gendisk *disk,sector_t size)

disk:要设置容量的gendisk

size:磁盘容量大小,注意这里是扇区数量,块设备中最小的可寻址单位是扇区,一个扇区一般是512字节,有些设备的物理扇区可能不是512字节,不管物理扇区是多少,内核和块设备驱动之间的扇区都是512字节。所以set_capacity函数设置的大小都是块设备实际容量除以512字节得到的扇区数量。

(5)调整gendisk引用计数

        内核会通过get_disk和put_disk这两个函数来调整gendisk的引用计数,根据名字就可以知道,get_disk是增加gendisk的引用计数,put_disk是减少gendisk的引用计数,这两个函数原型如下所示:

track kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)

3,block_device_operations结构体

        和字符设备的file_operations一样,块设备也有操作集,为结构体block_device_operations,此结构体定义在include/linux/blkdev.h,

        可以看出,block_device_operations结构体里面的操作集函数和字符设备的file_operations操作集基本类似,但是块设备的操作集比较少,我们可以看一下比较重要的几个成员函数:         

        第2行,open函数用于打开指定的块设备。

        第3行,release函数用于关闭(释放)指定的块设备

        第4行,rw_page函数用于读写指定的页

        第5行,ioctl函数用于块设备的I/O控制

        第6行,compat_ioctl函数和ioctl函数一样,都是用于块设备的I/O控制,区别在于在64位系统上,32位应用程序的ioctl会调用compat_ioctl函数,在32位的系统上运行的32位应用程序调用的就是ioctl函数

        第15行,getgeo甘薯用于获取磁盘信息,包括磁头,柱面和扇区等信息

        第18行,owner表示这个结构体属于哪个模块,一般直接设置为THIS_MODULE

4,块设备的I/O请求过程

        大家可以看到block_device_operations结构体中并没有找到read和write这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引出了块设备驱动中非常重要的request_queue request和bio。

(1)请求队列request_queue

        内核将块设备的读写都发送到请求队列request_queue中,request_queue中大量的request(请求结构体),而request又包含了bio,bio保存了读写相关数据,比如从块设备的哪个地址开始读取,读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。我们发现gendisk结构体里面有一个request_queue结构体指针类型成员变量queue,也就是说在编写驱动的时候,每个磁盘(gendisk)都分配一个request_queue。

初始化请求队列

        我们首先需要申请并初始化一个request_queue,然后初始化gendisk的时候将这个request_queue地址赋值给gendisk的queue成员变量,使用blk_init_queue函数来完成request_queue的申请与初始化。

request_queue *blk_init_queue(request_fn_proc *rfn,spinlock_t *lock)

rfn:请求处理函数指针,每个request_queue都要有一个请求处理函数,request_fn_proc原型如下:

void (request_fn_proc)(struct request_queue *q)

lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来,请求队列会使用这个自旋锁。

返回值:如果为NULL的话表示失败,成功的话就返回申请到的request_queue地址

删除请求队列

        当卸载块设备驱动的时候我们还需要删除掉前面申请到的request_queue,删除请求队列使用函数blk_cleanup_queue,函数原型如下:

void blk_cleanup_queue(struct request_queue *q)

q:需要删除的请求队列

分配请求队列并绑定制造请求函数(无request)

        blk_init_queue函数完成了请求队列的申请和请求处理函数的绑定,这个一般用于像机械硬盘这样的储存设备,需要I/O调度器来优化数据读写过程。但是对于EMMC,SD卡这样的非机械设备,可以进行完全随机的访问,所以就不需要复杂的I/O调度器,对于非机械设备我们可以先申请request_queue,然后将申请到的request_queue与‘’制造请求‘’函数绑定在一起。下面就是request_queue申请函数blk_alloc_queue,函数原型如下:

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

gfp_mask:内存分配掩码,具体可选择的掩码值请参考include/linux/gfp.h中的相关宏定义,一般为GPP_KERNEL。

返回值:申请到的无I/O调度的request_queue

        我们需要为blk_alloc_queue函数申请到的请求队列绑定一个“制造请求”函数。因此我们需要用到函数blk_queue_make_request,函数原型如下:

void blk_queue_make_request(struct request_queue *q,make_request_fn *mfn)

q:需要绑定的请求队列,也就是blk_alloc_queue申请到的请求队列

mfn:需要绑定的“制造”请求函数,函数原型如下:

void (make_requeset_fn)(struct request_queue *q,struct bio *bio)

        一般blk_alloc_queue和blk_queue_make_request是搭配在一起使用的,用于非机械的存储设备,无需I/O调度器,比如EMMC,SD卡等。blk_init_queue函数会给请求队列分配一个I/O调度器,用于机械存储设备,比如机械硬盘等

(2)请求request

        请求队列(request_queue)里面包含的就是一系列的请求(request),request是一个结构体,定义在include/linux/blkdev.h里面。request里面有一个名为“bio”的成员变量,类型为bio的结构体指针,真正的数据都保存在bio里面,所以我们需要从request_queue中取出一个一个的requeset然后再从每个request里面取出bio,最后根据bio的描述将数据写入到块设备,或者从块设备中读取数据

获取请求

        我们需要从request_queue中依次获取每个request,使用blk_peek_request函数来完成此操作,函数原型如下:

requeset *blk_peek_requeset(struct request_queue *q)

q:指定requeset_queue

返回值:requeset_queue中下一个要处理的请求(requeset),如果没有要处理的请求就返回NULL

开启请求

        使用blk_peek_requeset函数获取到下一个要处理的请求以后就要处理这个请求,这里要用到blk_start_request函数,函数原型如下:

void blk_start_requeset(struct request *req)

req:要开始处理的请求

一步到位处理请求

        我们也可以使用blk_fetch_requeset函数来一次性完成请求的获取和开启,blk_fetch_requeset函数原型如下:

其他和请求有关的函数

(3)bio结构

         每个requeset里面会有多个bio,bio保存着最终要读写的数据,地址等信息。上层应用对于块设备的读写都会被构造成一个或者多个bio结构,bio结构描述了要读写的起始扇区,要㝍的扇区数量,是读取还是写入,页偏移,数据长度等等信息。上层会将bio提交给I/O调度器,I/O调度器会将这些bio构造成requeset结构,而一个物理存储设备对应一个requeset_queue,requeset_queue里面顺序存放着一系列的requeset。新产生的bio可能会被合并到requeset中,也可能产生新的requeset,然后插入到requeset_queue中合适的位置,这一切都是I/O调度器来完成的,下面是requeset_queue,requeset和bio之间的关系。

        bio是一个结构体,定义在include/linux/blk_types.h中,结构体内容如下:

         重点来看一下第6行和第30行,第6行为bvec_iter结构体类型的成员变量,第30行为bio_vec结构体指针类型的成员变量。bvec_iter结构体描述了要操作的涉笔扇区等信息,内容如下:

        bio_vec就是“怕个,offset,len”组合,page指定了所在的物理页,offset表示所处页的偏移地址,len就是数据长度。bio_vec结构体内容如下: 

       我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇 区地址。bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度,“页地址”是 linux 内核里面内存管理相关的概念,这里我们不深究 linux 内存管理,我们只需要知道对于 RAM 的操作最终会转换为页相关操作。  

        bio、bvec_iter 以及 bio_vec 这三个机构体之间的关系如下图:

 遍历请求中的bio

        前面说了,请求中包含大量的bio,因此就涉及到遍历中所有bio并进行处理,遍历请求中的bio使用函数_rq_for_each_bio,这是一个宏,内容如下:

遍历bio中的所有段

        bio包含了最终要操作的数据,因此还需要遍历bio中的所有段,就要用bio_for_each_segment函数,此函数也是一个宏,内容如下:

         第一个 bvl 参数就是遍历出来的每个 bio_vec,第二个 bio 参数就是要遍历的 bio,类型为bio 结构体指针,第三个 iter 参数保存要遍历的 bio bi_iter 成员变量。

通知bio处理结束

        如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:
bvoid bio_endio(struct bio *bio,int error)

bio:要结束的bio

error:如果bio处理成功的话就填0,如果失败的话就填个负值,比如-EIO

返回值:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 嵌入式Linux设备驱动开发是指为嵌入式系统中的硬件设备开发驱动程序,使其能够在Linux操作系统上正常工作。嵌入式设备通常具有特定的功能和限制,因此其驱动开发与常规PC设备有所不同。 嵌入式Linux设备驱动开发需要掌握嵌入式系统的硬件架构、设备注册与初始化、设备访问接口、设备的中断处理和DMA传输等关键概念和技术。 首先,了解嵌入式系统的硬件架构是必要的,包括处理器架构、总线、外设等。这有助于理解设备的寄存器操作和驱动程序的编写。 其次,设备的注册与初始化是驱动开发的第一步。这包括将硬件设备与驱动程序关联起来,设定设备的基本参数并进行初始化,如配置中断、设置工作模式等。 接下来,需要实现设备访问接口。这包括读写设备寄存器、处理设备传感器数据、执行控制命令等。根据设备的特点,可以采用内存映射、IO端口访问等方法来完成设备的访问。 同时,中断处理也是嵌入式设备驱动开发的重要一环。通过中断,设备可以及时响应外部事件,提高系统的实时性。驱动程序需要实现中断处理函数,对中断事件进行处理,并及时通知系统做出相应的响应。 还有一种常用的数据传输方式是DMA。DMA传输可以大大提高数据传输效率,特别适用于高速数据传输设备。在驱动程序中,需要实现DMA传输的初始化和管理。 此外,嵌入式Linux设备驱动开发还需要关注功率管理、设备的热插拔支持、设备的调试和错误处理等方面的内容。 总的来说,嵌入式Linux设备驱动开发需要掌握嵌入式系统的硬件架构、设备访问接口、中断处理和DMA传输等关键技术。只有充分理解设备的特点和运行环境,才能开发出稳定、高效并且可靠的设备驱动程序。 ### 回答2: 嵌入式Linux设备驱动开发是指针对嵌入式系统中特定硬件设备的驱动程序编写和开发过程。设备驱动程序作为操作系统与硬件之间的桥梁,负责控制和管理硬件设备的功能。 嵌入式Linux设备驱动开发需要有一定的硬件和软件知识基础。首先,需要了解目标设备的硬件架构以及所使用的处理器类型和架构。其次,需要熟悉Linux内核的架构和编程模型,了解Linux设备模型和驱动框架。 开发嵌入式Linux设备驱动的主要步骤包括以下几个方面: 1. 编写设备驱动模块:根据设备的硬件特性和功能需求,编写相应的设备驱动模块。这涉及到底层硬件访问和控制,包括寄存器操作、中断处理、DMA等。 2. 设备的初始化和资源分配:在设备驱动模块中,在设备初始化阶段,需要分配和初始化设备所需的各种资源,如内存、中断、I/O端口等。 3. 实现驱动程序的接口:设备驱动程序需要提供一组接口,供用户空间的应用程序调用,以实现对设备的读写、控制等操作。 4. 注册和卸载设备驱动:在Linux内核启动时,通过注册设备驱动模块,将其与目标设备相关联。在不需要使用设备的时候,可以通过卸载设备驱动来释放资源。 5. 进行设备的测试和调试:编写驱动程序后,需要进行相应的测试和调试工作,以确保其正常运行。可以使用Linux提供的一些工具和调试技术,例如sysfs、devfs、strace等。 嵌入式Linux设备驱动开发需要深入了解Linux内核和硬件设备的工作原理,同时需要熟练掌握C语言和汇编语言等编程技术。开发者还需具备良好的调试和排错能力,能够解决因硬件设备和驱动之间的兼容性、稳定性等问题带来的挑战。 ### 回答3: 嵌入式Linux设备驱动开发是指在嵌入式Linux系统中开发驱动程序,使得硬件设备能够在Linux系统中正常工作。它是为了满足特定应用需求而针对特定硬件设备编写的一段软件代码。 嵌入式Linux设备驱动开发需要具备扎实的嵌入式系统和Linux内核的理论基础,熟悉设备驱动的工作原理和开发流程。开发者需要了解设备的硬件特性、寄存器的操作方法和设备的工作模式。 开发驱动的第一步是在Linux内核源码中查找相关设备的驱动框架,并创建一个新的驱动模块。驱动模块通常包含设备初始化、资源分配、中断处理和数据传输等功能。 在驱动模块中,开发者需要编写设备的初始化函数和操作函数,以初始化设备和提供设备的读写操作接口。对于不同的设备类型,开发者需要根据驱动框架中的规范和硬件特性进行相应的编码和配置。 在设备驱动的开发过程中,我们需要通过嵌入式Linux系统提供的工具链来编译和生成设备驱动的二进制代码,并将其加载到系统中。一旦驱动程序加载成功,设备就可以被系统正确地识别和使用。 嵌入式Linux设备驱动开发需要进行严格的测试和调试,以确保驱动程序的正确性和稳定性。调试过程中,我们可以利用调试工具和打印信息来排查问题并进行修复。 总之,嵌入式Linux设备驱动开发是一个复杂而有挑战性的任务,但同时也是非常重要的。通过开发设备驱动,我们可以在嵌入式Linux系统中充分发挥硬件设备的功能,实现各种特定应用需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值