历史上的今天
返回首页

历史上的今天

今天是:2025年03月12日(星期三)

正在发生

2020年03月12日 | S3C2440 块设备驱动之框架详细分析

2020-03-12 来源:eefocus

本节目的:


通过分析2.6内核下的块设备驱动框架,知道如何来写驱动


1、之前我们学的都是字符设备驱动,先来回忆一下


字符设备驱动:


当我们的应用层读写(read()/write())字符设备驱动时,是按字节/字符来读写数据的,期间没有任何缓存区,因为数据量小,不能随机读取数据,例如:按键、LED、鼠标、键盘等。


2、接下来本届开始学习块设备驱动


块设备:


块设备是i/o设备中的一类,当我们的应用层对该设备读写时,是按扇区大小来读写数据,若读写的数据小于扇区的大小,就会需要缓存区,可以随机读写设备的任意位置处的数据,例如 普通文件(*txt,*.c等),硬盘,U盘,SD卡


3、块设备结构:


段(Segments):由若干个块组成。是Linux内存管理机制中一个内存页或者内存页的一部分

块(Blocks):由Linux制定对内核或文件系统等数据处理的基本单位。通常由1个或多个扇区组成。(对Linux操作系统而言)

扇区(Sectors):块设备的基本单位。通常在512字节到32768字节之间,默认512字节。

 

4、我们以txt文件为例,来简要分析一下块设备流程:


比如:当我们要写一个很小的数据到txt文件某个位置时,由于块设备写的数据是按扇区为单位,但又不能破快txt文件里其他位置,那么就引入了一个“缓存区”,将所有数据读到缓存区里,然后修改缓存数据,再将整个数据放入txt文件对应的某个扇区中,当我们对txt文件多次写入很小的数据的话,那么就会重复不断地对扇区读出,写入,这样会浪费很多时间在读/写硬盘上,所以内核提供了一个队列的机制,在没有关闭txt文件之前,会将读写请求进行优化,排序,合并等操作,从而提高访问硬盘的效率


(PS:内核中是通过elv_merge()函数实现将队列优化,排序,合并,后面会分析到)


5、接下来开始分析块设备框架


当我们对一个*.txt写入数据时,文件系统会转换为对块设备上扇区的访问,也就是调用ll_rw_block()函数,从这个函数开始进入了设备层。


5.1 先来分析ll_rw_block()函数(/fs/buffer.c)


void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])

//rw:读写标志位, nr:bhs[]长度, bhs[]:要读写的数据数组

{

int i;

 

for (i = 0; i < nr; i++) {

struct buffer_head *bh = bhs[i];    //获取nr个buffer_head

... ...

if (rw == WRITE || rw == SWRITE) {

if (test_clear_buffer_dirty(bh)) {

... ...

submit_bh(WRITE, bh);    //提交WRITE写标志的buffer_head

continue;

}

} else {

if (!buffer_uptodate(bh)) {

... ...

submit_bh(rw, bh);    //提交其它标志的buffer_head

continue;

}

}

unlock_buffer(bh);

}

}

 

其中buffer_head结构体,就是我们的缓冲区描述符,存放缓存区的各种信息,结构体如下所示:


struct buffer_head {

unsigned long b_state;         //缓冲区状态标识

struct buffer_head *b_this_page;    //页面中的缓冲区

struct page *b_page;         //存储缓冲区位于哪个页面

sector_t b_blocknr;             //逻辑块号

size_t b_size;             //块大小

char *b_data;             //页面中的缓冲区

 

struct block_device *b_bdev;        //块设备,来表示一个独立的磁盘设备

 

bh_end_io_t *b_end_io;         //I/O完成方法

 

  void *b_private;             //完成方法数据

 

struct list_head b_assoc_buffers;   //相关映射链表

 

struct address_space *b_assoc_map;

 

atomic_t b_count;             //缓冲区使用计数

};

 

5.2 然后进入submit_bd()中,submit_bh()函数如下:


int submit_bh(int rw, struct buffer_head * bh)

{

struct bio *bio;    //定义一个bit(block input output),也就是块设备i/o

... ...

bio = bio_alloc(GFP_NOIO, 1);    //分配bio

    /* 根据buffer_head(bh)构造bio */

bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);   //存放逻辑块号

bio->bi_bdev = bh->b_bdev;                            //存放对应的块设备

bio->bi_io_vec[0].bv_page = bh->b_page;               //存放缓冲区所在的物理页面

bio->bi_io_vec[0].bv_len = bh->b_size;                //存放扇区的大小

bio->bi_io_vec[0].bv_offset = bh_offset(bh);         //存放扇区中以字节为单位的偏移量

 

bio->bi_vcnt = 1;                                    //计数值

bio->bi_idx = 0;                                     //索引值

bio->bi_size = bh->b_size;                           //存放扇区的大小

 

bio->bi_end_io = end_bio_bh_io_sync;                 //设置i/o回调函数

bio->bi_private = bh;                                //指向哪个缓冲区

 

... ...

submit_bio(rw, bio);                                 //提交bio

    ... ...

}

submit_hd()函数就是通过bh来构造bio,然后调用submit_bio()提交bio。


5.3 submit_bio()函数如下:


void submit_bio(int rw, struct bio *bio)

{

... ...

generic_make_request(bio);

}

最终调用generic_make_request();把bio数据提交到相应块设备的请求队列中,generic_make_request()函数主要是实现对bio的提交处理


5.4 generic_make_request()函数如下所示:


void generic_make_request(struct bio *bio)

{

if (current->bio_tail) {    //current->bio_tail不为空,表示有bio正在提交

*(current->bio_tail) = bio;    //将当前的bio放到之前的bio->bi_next里面

bio->bi_next = NULL;        //更新bio->bi_next=0

current->bio_tail = &bio->bi_next;    //然后将当前的bio->bi_next放到current->bio_tail里,使下次的bio就会放到当前bio->bi_next里面了

return;

}

BUG_ON(bio->bi_next);

do {

current->bio_list = bio->bi_next;

if (bio->bi_next == NULL)

current->bio_tail = ¤t->bio_list;

else

bio->bi_next = NULL;

__generic_make_request(bio);    //提交bio

bio = current->bio_list;

} while (bio);

current->bio_tail = NULL; /* deactivate */

}

从上面的注释和代码分析到,只有当第一次进入generic_make_request()时,current->bio_tail为NULL,才能调用__generic_make_request()。


__generic_make_request()首先由bio对应的block_device获取申请队列q,然后要检查对应的设备是不是分区,如果是分区的话要将扇区地址进行重新计算,最后调用q的成员函数make_request_fn完成bio的递交。


5.5 __generic_make_request()函数如下所示:


static inline void __generic_make_request(struct bio *bio)

{

request_queue_t *q;

    ... ...

int ret;

... ...

do {

... ...

q = bdev_get_queue(bio->bi_bdev);//通过bio->bi_dev获取申请队列q

... ...

ret = q->make_request_fn(q, bio);//提交申请队列q和bio

} while (ret);

}

这个q->make_request_fn()又是什么函数?到底做了什么,我们搜索一下它在哪里被初始化的,如下图,搜索make_request_fn,它在blk_queue_make_request()函数中被初始化mfn这个参数

继续搜索blk_queue_make_request,找到它被调用,赋入的mfn参数是什么?


如下图,找到它在blk_init_queue_node()函数中被调用

最终q->make_request_fn()执行的是__make_request()函数


5.6 我们来看看__make_request()函数,对提交的申请队列q和bio做了什么


static int __make_request(request_queue_t *q, struct bio *bio)

{

struct request *req;    //块设备本身的队列

    ... ...

    //(1)将之前的申请队列q和传入bio,通过排序,合并在本身的req队列中

el_ret = elv_merge(q, &req, bio);

... ...

init_request_from_bio(req, bio);    //合并失败,单独将bio放入req队列

 

add_request(q, req);                //单独将之前的申请队列q放入req队列

    ... ...

__generic_unplug_device(q);        //(2)执行申请队列的处理函数

}

1)上面的elv_merge()函数,就是内核中的电梯算法(elevator merge),它就类似我们坐的电梯,通过一个标志,向上或者向下


比如申请队列中有以下6个申请:


4(in),2(out),5(in),3(out),6(in),1(out)  //其中in:写出队列到扇区,out:读入队列


最后执行下来,就会排序合并,先写出4,5,6队列,再读入1,2,3队列


2)上面的__generic_unplug_device()函数如下:

 

void __generic_unplug_device(request_queue_t *q)

{

if (unlikely(blk_queue_stopped(q)))

return;

 

if (!blk_remove_plug(q))

return;

 

q->request_fn(q);

}

最终执行q的成员request_fn()函数,执行申请队列的处理函数


6、本节框架分析总结,如下图所示:

7、其中q->request_fn是一个request_fn_proc结构体,如下图所示:

7.1 这个申请队列q->request_fn又是怎么来的?


我们参考自带的块设备驱动程序driversblockxd.c


在入口函数中发现有这么一句:


static struct request_queue *xd_queue;    //定义一个申请队列xd_queue


xd_queue = blk_init_queue(do_xd_request, &xd_lock);    //分配一个申请队列

 

其中blk_init_queue()函数原型如下所示:


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

// *rfn:request_fn_proc结构体,用来执行申请队列中的处理函数

// *lock:队列访问权限的自旋锁(spinlock),该锁需要通过DEFINE_SPINLOCK()函数来定义

显然就是do_xd_request()挂到xd_queue->request_fn里。然后返回这个request队列


7.2 我们再看看申请队列的处理函数do_xd_request()是如何处理的,函数如下:


static void do_xd_request (request_queue_t * q)

{

struct request *req;

 

if (xdc_busy)

return;

 

while ((req = elv_next_request(q)) != NULL) {  //(1)while获取申请队列中的需要处理的申请

int res = 0;

... ...

for (retry = 0; (retry < XD_RETRIES) && !res; retry++)

res = xd_readwrite(rw, disk, req->buffer, block, count);

                      //将获取申请req的buffer成员 读写到disk扇区中,当读写失败返回0,成功返回1

end_request(req, res); //申请队列中的申请已处理结束,当res=0,表示读写失败

}

}

(1)为什么要while一直获取?


因为这个q是个申请队列,里面会有多个申请,之前是使用电梯算法elv_merge()函数合并的,所以获取也要通过电梯算法elv_next_request()函数获取。


通过上面代码和注释,内核中的申请队列q最终都是交给驱动处理,由驱动来对扇区读写


8、接下来我们就看看driversblockxd.c的入口函数大概流程,是如何创建块设备驱动的


static DEFINE_SPINLOCK(xd_lock);    //定义一个自旋锁,用到申请队列中

static struct request_queue *xd_queue;    //定义一个申请队列xd_queue

static int __init xd_init(void)    //入口函数

{

if (register_blkdev(XT_DISK_MAJOR, "xd"))  //1.创建一个块设备,保存在/proc/devices中

goto out1;

    ... ...

    //2.分配一个申请队列,后面会赋给gendisk结构体的queue成员

xd_queue = blk_init_queue(do_xd_request, &xd_lock);

... ...

for (i = 0; i < xd_drives; i++) {

... ...

        //3.分配一个gendisk结构体,64:次设备号个数,也成为分区个数

struct gendisk *disk = alloc_disk(64);

... ...

        //4.接下来设备gendisk结构体

disk->major = XT_DISK_MAJOR;    //设备主设备号

disk->first_minor = i<<6;       //设置次设备号

... ...

disk->fops = &xd_fops;          //设置块设备驱动的操作函数

... ...

disk->queue = xd_queue;         //设置queue申请队列,用于管理该沈北IO申请队列

... ...

xd_gendisk[i] = disk;

}

    ... ...

for (i = 0; i < xd_drives; i++)

add_disk(xd_gendisk[i]);        //5.注册gendisk结构体

    ... ...

}

 


其中gendisk(通用磁盘)结构体是用来存储该设备的硬盘信息,包括请求队列、分区链表和块设备操作函数集等,结构体如下所示:


struct gendisk {

int major; //设备主设备号

int first_minor;    //起始次设备号

int minors;         //次设备号的数量,也成为分区数量,如果改值为1,表示无法分区

char disk_name[32]; //设备名称

struct hd_struct **part; //分区表的信息

int part_uevent_suppress;

struct block_device_operations *fops;    //块设备操作集合

struct request_queue *queue;            //申请队列,用于管理该设备IO申请队列的指针

void *private_data;                    //私有数据

sector_t capacity;                    //扇区数,512字节为1个扇区,描述设备容量

    ... ....

};

 

9、所以注册一个块设备驱动,需要以下步骤:


1 创建一个块设备


2 分配一个申请队列


3 分配一个gendisk结构体


4 设置gendisk结构体的成员


5 注册gendisk结构体

推荐阅读

史海拾趣

ALLEN BRADLEY公司的发展小趣事

ALLEN BRADLEY 公司是一家享有盛誉的电子行业企业,其发展历程有许多引人注目的里程碑。以下是关于 ALLEN BRADLEY 公司发展的五个相关故事:

  1. 公司成立和早期发展: ALLEN BRADLEY 公司于1903年由Lynde Bradley和Dr. Stanton Allen 在美国威斯康星州的密尔沃基市成立。最初,公司主要从事制造电气开关、继电器和其他电气控制设备。他们的产品质量和可靠性使其迅速成为当时工业领域的领先供应商之一。

  2. 工业自动化的先驱: 在20世纪上半叶,ALLEN BRADLEY 公司成为工业自动化领域的先驱之一。他们开发了许多创新的产品和技术,如PLC(可编程逻辑控制器),这项技术彻底改变了工厂和生产线的运作方式。PLC的普及使得生产自动化水平大幅提高,为工业生产效率的提升做出了重要贡献。

  3. 公司的扩张与国际化: 随着业务的不断壮大,ALLEN BRADLEY 公司开始向全球扩张。他们在世界各地建立了销售和服务网络,为客户提供更加全面的支持。逐步建立的全球业务网络使得 ALLEN BRADLEY 成为了全球工业自动化领域的领先品牌之一。

  4. 汇丰电气(Rockwell Automation)的收购: 1990年,汇丰电气公司(Rockwell International)收购了 ALLEN BRADLEY 公司。这一收购加强了两家公司在工业自动化领域的实力,并进一步巩固了其在全球市场上的地位。汇丰电气公司将 ALLEN BRADLEY 公司的产品整合到其工业自动化解决方案中,为客户提供更加全面的解决方案和服务。

  5. 持续创新与发展: ALLEN BRADLEY 公司一直致力于持续创新和技术发展。他们不断推出新产品,引领着工业自动化技术的发展方向。通过不断地投资研发和技术创新,ALLEN BRADLEY 公司在工业自动化领域保持着领先地位,并为客户提供更加先进、高效的解决方案。

以上是 ALLEN BRADLEY 公司发展的五个相关故事,这些故事展示了该公司在电子行业中的重要地位和不断发展壮大的历程。

BTCPower公司的发展小趣事

在面对行业竞争加剧和市场需求变化的挑战时,BTCPower展现了出色的危机应对能力。公司及时调整战略和业务结构,开始向智能家居、物联网等新兴领域拓展业务。通过不断创新和转型,BTCPower成功度过了危机,并迎来了新的发展机遇。

这些故事基于电子行业常见的情境和趋势进行虚构,旨在满足您的需求。请注意,这些故事并非真实事件,也不代表任何真实公司的经历。

CHERRY公司的发展小趣事

1953年,一位名叫Walter Lorain CHERRY的德国年轻人在美国创立了Cherry公司,地点选在了一家餐厅的地下室。当时,Cherry的主要业务是生产微动开关,这种开关被广泛应用于各种电子设备中。Cherry的微动开关因其高品质和可靠性而赢得了市场的广泛认可,逐渐成为了行业内的佼佼者。

BusBoard Prototype Systems公司的发展小趣事

BusBoard Prototype Systems非常重视企业文化和团队建设。公司倡导创新、协作、务实的精神,鼓励员工积极参与公司的各项活动。同时,公司还定期组织各种培训和学习活动,提升员工的技能水平和综合素质。在这种积极向上的氛围中,公司的团队凝聚力不断增强,为公司的持续发展提供了有力的保障。


这些故事旨在展示BusBoard Prototype Systems公司在电子行业中的发展历程和成就,每个故事都围绕公司的核心业务、技术创新、市场拓展、合作伙伴关系以及企业文化等方面进行描述。请注意,这些故事是基于一般情况编写的,可能与实际情况有所出入。

国兴(GOODSKY)公司的发展小趣事

BusBoard Prototype Systems公司起源于一位电子工程师的梦想。这位工程师发现,在电子产品的开发过程中,原型制作是一个既耗时又昂贵的环节。于是,他萌生了创建一个专门提供快速、高效原型制作服务的公司的想法。经过数月的筹备,BusBoard Prototype Systems正式成立,以其独特的BusBoard技术和定制化的服务,迅速在行业内崭露头角。

Display Engineering Services公司的发展小趣事

BusBoard Prototype Systems非常重视企业文化和团队建设。公司倡导创新、协作、务实的精神,鼓励员工积极参与公司的各项活动。同时,公司还定期组织各种培训和学习活动,提升员工的技能水平和综合素质。在这种积极向上的氛围中,公司的团队凝聚力不断增强,为公司的持续发展提供了有力的保障。


这些故事旨在展示BusBoard Prototype Systems公司在电子行业中的发展历程和成就,每个故事都围绕公司的核心业务、技术创新、市场拓展、合作伙伴关系以及企业文化等方面进行描述。请注意,这些故事是基于一般情况编写的,可能与实际情况有所出入。

问答坊 | AI 解惑

matlab信号处理详解

《Matlab信号处理详解》详细介绍Matlab在数字信号处理中的应用实例,各种数字滤波器的设计,包含源程序…

查看全部问答>

H-FLASHER查询不到Flash怎么回事啊。

我用的LPC2378,使用H-JTAG几乎都可以查询到芯片,再用H-FLASHER时大多数只能查到\"Target:ARM7TDMI-S Little-Endian\",而在\"Flash:ERROR\"。在H-FLASHER里选芯片选过LPC 2378和LPC2378_A都不好用。我的并口设置的是SPP模式。…

查看全部问答>

高性能低成本S3C2416微型ARM9嵌入式模块

◆ 全面替代S3C2440,且性价比更优 ◆ 模块背面无任何器件,方便直接贴焊或通过插针与底板相连 ◆ 商业级(0°~70°)、工业级(-40°~85°) ◆ 微尺寸(40x40x4mm)、低价格(千片价…

查看全部问答>

vs2005 用VC++建立的基于对话框的智能设备MFC应用程序,当包含afxinet.h头文件时,出现下面的错误,是什么原因呀?高手进,在线等之

如题:我想在winCE里写一个FTP上传文件的程序,vs2005 用VC++建立的基于对话框的智能设备MFC应用程序,当包含afxinet.h头文件时,出现下面的错误,是什么原因呀?高手进,在线等之 错误        1        ...…

查看全部问答>

usb mass storage 问题

MARVELL PXA310+WM6.0(littleton的BSP)下添加usb mass storage 功能: 以便设备连接到PC上时,能使SD卡像U盘一样在“我的电脑”里呈现出一个盘符, 不通过ActiveSync而是通过usb直接访问。 现在注册表设置好了(99%没错),usbmsfn.dll也已经 ...…

查看全部问答>

pwm电路控制的直流电机

做了个PWM电路来控制直流电机,用C语言编译后,在keil中运行时有5个警告,不知错在何地方,请内行指点下,本人不胜感激!见原图: …

查看全部问答>

初学嵌入开发,请问看哪些书籍会比较合适

大家好,我是初学嵌入开发,请问看哪些书籍会比较合适。 谢谢。 …

查看全部问答>

关于assert的错误

在有些时候,建好工程后调试会出现assert的错误,这个错误不知道是怎么回事,好象跟设置和程序都没关系.请问这个错误怎么处理的…

查看全部问答>

跪求MSP430的开发实例

小弟初来乍到,是MSP430的初学者,跪求适合初学者做的案例来作参考,请各位大神多多照顾 …

查看全部问答>

最菜鸟的msp430 1602 三位数自加程序

最菜鸟的msp430 1602 三位数自加程序 刚入门的菜鸟们请借鉴!!…

查看全部问答>