赵磊 写一个块设备驱动 的阅读笔记

1、

alloc_disk(1);与del_gendisk(struct gendisk*) 对应。//其中alloc_disk这个参数是表示次设备号的数量。

add_disk(struct gendisk*)  与put_disk(struct gendisk*);对应

初始化过程:先创造gendisk 结构体。再添加这个结构体。

退出过程:先去掉结构体gendisk (del_gendisk)再删除gendisk本身(put_disk)。

总结:init里面都是disk,exit里面del开头的为gendisk  put是disk所以共:三个disk后缀,一个gendisk后缀。

2、单纯的用alloc_disk(1)生成的struct gendisk结构体指针里面内容并不健全,需要在alloc_disk之间和add_disk()之间增加设备相关属性。属性添加如下:

strcpy(simple_blkdev_disk->disk_name,SIMPLE_BLKDEV_DISKNAME);

simple_blkdev_disk->major=?  //正常是动态分配,但是第一次,固定设备号。抢别人的先。

simple_blkdev_disk->first_minor=0;

simple_blkdev_disk->fops=?

simple_blkdev_disk->queue=?

set_capacity(simple_blkdev_disk, ?);

以上是目前需要添加的完善gendisk结构体的内容:名字,次设备第一个编号,major号,fops操作函数 ,queue队列挂上。设置容量。

3、抢设备号,从代码中看,linux/include/linux/major.h中看,选COMPAQ_SMART2_xxx即可。

定义一个宏:#define SIM_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR

4、fop设置一个空的即可,只用定义.owner=THIS_MODULE;

struct block_device_operations simple_blkdev_fops={

.owner= THIS_MODULE,  //注意此处为逗号

};  //注意此处为分号,因为不是函数,是个数据定义结束了。

5、队列定义是要麻烦些

记住:io调度器把排序后的访问就是bio和bio_vec内容,通过request_queue结构传递给块设备驱动程序处理。所以需要定义一个request_queue结构体,上传给io调度器这些上层使用。所以顺序是:系统先生成bio然后传给request,然后io调度器再调度由多个request组成的request_queue,队列,所以gendisk---》的queue值是多少,是一个队列。所以填写队列指针。如果使用系统自带的队列形式,那么函数blk_init_queue会返回一个指针,用户只用指定处理方式即可,指定方式是参数使用处理函数,该函数void* xx_do_queue(*q),函数里面是处理queue中取出来的每一个request而不是直接面对bio

make_request的时候,需要人为创建queue, queue=blk_alloc_queue(GFP_KERNEL)

blk_queue_make_request(simple_blkdev_disk->queue, struct bio*bio)//虽然有个queue但是queue没用。核心用在bio参数上。

并且注意此时因为是最简单的,因此只是用了alloc_disk产生的gendisk指针,而真正的是一个结构体里面包含gendisk指针,lock锁,queue队列,data数据等。包在一个大的结构体里面了,此处是用来最原始的gendisk.  gendisk自带的queue和大结构体里面的queue有什么区别。至少gendisk中是带queue成员的。一样的因为最后复杂代码中将gendisk->queue又用大结构中的queue赋值了,所以没什么意思。只是用大的结构体显得清晰。核心还是gendisk

当使用默认request的时候,是:只用使用一个系统函数blk_init_queue() 生成了queue返回request_queue指针。  而使用make_request时候使用两个系统函数:blk_alloc_queue()生成request_queue指针;和blk_queue_make_request()是处理bio的函数,。虽然说绑定q-->make_request 和自己写的make_request函数,但是很隐晦的一个调用过程、流程是:

1)request_queue.make_request_fn指向我们设计的make_request函数
2)
把我们设计的make_request函数写出来

但是实际是struct request_queue *q=blk_alloc_queue(GFP_KERNEL);和  然后调用系统的blk_queue_make_request(*q, int xxx_make_queue);这就是绑定了。而xxx_make_queue(*q, bio*bio)此时注意这个bio!!!!!!!这个bio是系统在调用blk_queue_make_request后就知道调用xxx_make_queue了,所以什么时候调用xxx_make_queue是系统干的事情,所以传进去的bio不是我们用户能够控制的。这就是回调?我给系统说写,然后系统说你对设备清楚,那么你写个处理函数我来调用吧。


而当request的时候的系统自带__generic_make_request的函数内容作用



此处还需要加一个这都是单独队列的,那么multi-queue这种形式的怎么用?是否是nvme的架构??如果不是nvme类似的架构,那么nvme的队列架构与multi-queue的区别是什么。


使用默认的,则不用创建queue,直接使用一个函数即可:blk_init_queue(simple_blkdev_do_request,NULL);因为第二个参数是一个调度器选择目前还没用到。

两者区别是:xxx_make_request缺少了调度以及直接参数就是对bio*操作(实现函数中肯定是封装好的直接对bio的操作函数。)。而xxx_do_request是带调度,以及操作参数是queue.   :request_queue*q那么实现函数中肯定是封装好的操作queue的函数

6、建立的queue记得卸载的时候使用blk_cleanup_queue(); 不论是make request模式还是request模式。都要cleanup queue的。

7、扇区数目,如果16MB按照每个512byte那么个数定义成宏最好,#define SIMPLE_BLKDEV_BYTES (16*1024*1024)

然后再其上填写SIMPLE_BLKDEV_BYTES >>9.    

此处看似加上do_queue函数来说是解决了,但是有个重大问题,并没有显示实际设备的结构,例如设备是ssd还是512的机械硬盘?或者是假的数组?


8、后面很重要的就是怎么处理queue了。即simple_blkdev_do_queue(struct request_queue* q)函数

这个是存储的核心,首先需要自己知道自己的物理设备结构,是磁盘扇区还是ssd?还是什么那么此处用最简单的,就是一个整体数组来存储。这样最简单,将所有的数据都加在数组这个结构中。最好处理了,不用想着分扇区处理。

分两大步骤:1、解析queue         2、将queue的内容即要存到哪个扇区哪个磁道中 转化为自己设备的结构中。所以自己编写dlc设备代码的时候,看到的哪个块哪个页的分法已经是usb驱动程序处理处理过queue这个结构体的了,没有处理过的时候是标准的磁道和扇区。??

1)解析queue用到的系统函数:elv_next_request()函数,原型是:struct  request* elv_next_request(struct reqeust_queue* q);  用来取出请求中的一段或者一条请求,所以注意返回值是request结构而不是request_queue了。然后根据rq_data_dir(req)返回请求读写方向,然后把块设备数据与request中buffer地址之间看怎么交换,是你进来还是我出去。

request结构体那么就是核心了:围绕它有 rq_data_dir()宏函数和.buffer成员。剩下的还有:request.sector请求的开始磁道不是地址而是扇区个数

request.current_nr_sectors请求的磁道数。 end_request(struct request* req, int uptodata)结束一个请求,就是说将这个req从request_queue中剥离掉,第二个参数是表示处理结果,1表示处理成功,0表示处理失败。

为什么说request是核心,因为它上面包含自己想知道的用户数据的地址,.buff就是这个用户数据各种数据的存储地方,所以至少到这里清晰,想追源头可以再往上追看怎么组成bio->request->reqeust_queue这个过程就是用户数据被内核利用的过程

想追就提出问题:bio怎么生成的。这就是用户空间数据怎么被内核操控的过程


此处还要注意:有的时候想end_request中还嵌套的有其他系统函数,例如end_that_request_first这种告诉块设备层一个io请求的扇区全部传输完了。所以碰见函数不要慌,整体流程把握好就不会思路跑太远

2)如上所述,就是先当成数组为:unsigned char simple_blkdev_data[SIMPLE_BLKDEV_BYTES];

此时处理函数static void simp_blkdev_do_request(struct request_queue *q)为:

{

struct  request *req;

while((req=elv_next_request(q))!=NULL){

if(((req->sector+req->current_nr_sectors)<<9)>SIMPLE_BLKDEV_BYTES){

printk(KERN_ERR  SIMP_BLKDEV_DISKNAME ":bad request:block=%llu,count=%u\n",(unsigned long long)req->sector, req->current_nr_sectors);

end_request(req,0);

continue;//意思是可以允许中间超出容量,但是不崩溃,下次写入小点就行了。

}

}

switch(rq_data_dir(req)){

case READ:

memcpy(req->buffer, simple_blkdev_data+(req->sector<<9), req->current_nr_sectors<<9);

end_request(req,1);

break;

case WRITE:

memcpy(simple_blkdev_data+(req->sector<<9), req->buffer, req->current_nr_sectors<<9);//请注意这个地方明显解决了以前自己的疑问,对磁盘最小操作单位是512,即使读1字节那么也是读512上去,然后再剪出来那个1字节。只是目前没有追到放到req中的512怎么减出来那个1字节,估计是按照地址偏移过去的吧。

end_request(req,1);

default:

break;

}

}


上述写完是没有问题的,但是头文件成了大问题,一个一个尝试添加:

set_capacity没有,先添加linux/blkdev.h

加了也没用。。诶,程序太老了。但是思路是对的。

例如函数elv_next_request都改成 blk_fetch_request了。。


假设通过编译,那么其中add_disk函数就是即插即用的udev机制,会在/dev目录下简历设备文件,名字是gendisk.disk_name中的"simp_blkdev"当然显示是没有引号的。主从设备肯定是72,0自己设置的。

注意此时lsmod时候,看到的计数used by是0因为没有使用,当mount以后used by就变成1了。umount掉后used by就又是0 了。


第2章

io调度器由四种anticipatory  cfq  deadline noop   2.6.18之前是anticipatory后面是cfq。

选择按个io调度器需要的一个函数:int  elevator_init(struct request_queue *q, char*name); 其中name就是四个名字之一,最好还是选,如果空置,那么就是将选择权给了启动参数elevator=xxx的设置。上面blk_init_queue第二个参数好像不清楚是不是要调度器还是lock锁的,好奇怪。确定了,是锁,只是说blk_init_queue后面可以加上elevator_init(request_queue*,"noop");这句话。

这个不确定,但是使用make_request方式就可以自然省掉调度方式,因为看/sys/xxx自己设备下面没有queue了。但是bdbm下面为什么还有queue文件夹???但是确实是在/sys/devices/virtual/block/robusta/queue中看到schedule值为none.

而作者赵磊也改成功了。改完查看设备下面的/queue/scheduler是cfq. 所以思路肯定是有效的。只是代码更新后细节会变化。

第3章

此时引入make request方式,但是先说说后面需要的make_request_fn的来源。为什么用make request方式的时候,要自己编写make_request_fn。因为实际的request_queue结构体上是包含有一个make_request_fn函数指针的,而这个make_request_fn指针指向的函数是系统定制好的一个。

如果往上追,会发现,上层处理一个bio时候,例如读一个块设备的一段数据,上层通用块层调用的是generic_make_request函数,调用完就完善了bio,将bio经过调度且封装成request,再到request_queue.追完generic_make_request函数发现调用的就是request_queue结构体中的make_request_fn函数。所以如果我们把request_queue中的make_request_fn指针指定到我们自己的make_request函数。就算是躲开了通用块对io的调度封装。


但是前面并没有make_request_fn函数添加的位置,但是前面讲过了,其实就是在blk_init_queue函数调用生成request_queue队列的时候。里面自动给这个队列的make_request_fn指针赋值了系统设计好的。这个make_request_fn函数就是前面说的生成了、且使用调度器调度了bio,生成了request_queue.然后才调用驱动设计者设计的do_request处理这个队列中的request的。

总结:核心就是上层有个bio已经从用户空间要求io中生成了,然后要调度之后包装成request再到request_queue。那么调度和包装过程都是request_queue结构体中指定的系统make_request函数处理的,处理成功后,设计驱动者才有资格用do_request去操作

好了,现在更简单的是:踢开这个处理bio的make_request咸鱼。自己直接处理bio本身,那么调度可以没有,并且也不用包装了。直接用。那么此时没了blk_init_queue和do_request因为request只能处理request不能处理裸露的bio.所以我们为了绑定自己的make_request_fn,还要创造个简易的request_queue,然后将自己的make_request_fn与q绑定(blk_queue_make_request绑定),就是blk_queue_make_request函数来绑定生成的queue和自己的make_request_fn(所以绑定函数两个参数就是queue和make函数)而自己的这个make_request_fn函数参数同样是表明虽然处理的是bio但是不忘记队列也捎带上,虽然队列已经没有用了。:struct request_queue*q, struct bio* bio。所以对于request的bio操作只是被generic_make_request()函数处理的。而当自己亲自处理bio时候,要模仿这个过程,有queue和io处理make_request_fn函数处理bio。

原来的blk_init_queue是生成队列和生成queue两步骤都自己做了,用户只用处理queue即可。

现在是没有queue,就创造queue,创造完绑定,绑定完执行每个bio(只是带上虚假的队列自身当参数)

既然要单独处理bio,那么就要好好介绍bio了,bio对应设备上一段连续空间请求(是设备上的连续请求???)而一个bio使用了bio_vec这个地址数组域来指出每个请求对应的每段内存。(这个内存是用户空间还是内核空间???)而我们自己的bio处理函数叫做make_request_fn,本质就是处理各个blk_vec处内存内容的。

与bio相关的函数。类似于操作request的系统函数,此处也有bio相关系统处理函数。bio_for_each_segment(bvec,bio,i);是bvec和bio和i。bio_rw(bio);是判断bio方向的。而为了记忆一次bio内多个bio_vec中的地址,每次bio_vec[n]数组中每一个元素操作完都要有一个位置记录,直到这个bio的所有的bio_vec都处理完了,才行。所以,bio是个结构体,而bio_vec同样是个结构体。bio_vec结构体内容会少些,为:三个:

bio_vec.page*  //叫做页指针,怪不得百度出来都有page*号还不知道什么意思。

bio_vec.bv_offset   //要知道是页指针,而实际起始地址可能在页内,所以偏移量是必不可少的。因为一般page 4k而机械盘是512字节。

bio_vec.bv_len  //已经知道开始的页指针和页内偏移地址了,带上长度就可以完整操作了。

bio量其实挺多的,但是用到的好像不太多,既然说到bio那么多说一句:调度过后,一个请求里面可能包含多个bio。用到的结构体内元素是:bio.sector(要传输的第一个扇区),按道理用bio_vec*的起始页地址和页内偏移即可,起始这个bio.sector和bio.size就是为了刚开始判断是否超过容量了,核心内容是:实际bio就是往设备物理地址中哪个扇区写入连续的多少内容,如果不连续,就不是在一个bio内!!!!在一个bio内的一定是物理地址连续的,所以刚开始的bio.sector作为起始和bio.size作为长度,就是对的,而bio_vec是这些在磁盘上连续的地址是在内存中分散的,!!!!!所以写入的时候记住bio.sector是磁盘位置,往起始sector上写,然后bio_vec内容是内核中的位置。两个根本不是同一个类型。所以并不冲突。,,而真正读写之类就是用bio_for_each_segment这个函数操作,。因为虽然每个bio起始地址都可能是挖洞式的存在,但是单独的这个bio起始扇区已经确定了,并且对应的是物理扇区,所以如果size过长,那么肯定是不行的,并不是说文件系统管理用链表之类的,这地方已经是物理地址了,所以超了就是超了,要报错的。

bio_for_segment()参数是bio_bvec*结构体,bio, 然后还有个int型计数的iter?反正没有使用,第一个参数是输出,第二个参数是输入,就是用户不要自己去bio_bvec而是用这个函数取,这不算是个函数,而是个宏,什么样的宏?就是for宏,所以肯定bio中bio_vec取完了,自动结束,自己不要怕不结束。

#define bio_for_each_segment(bvl, bio, iter) \

__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)

#define __rq_for_each_bio(_bio, rq) \

if ((rq->bio)) \ for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)

看这个就是类似于for循环,自己不用设置结束符号。

bio讲完后,用do_request函数处理队列queue的,好像要整齐一些,是直接的很整齐的扇区开头位置(第几个扇区)和扇区个数。这样多整齐,直接用memcpy操作。而bio由于靠底层需要自己亲自收集碎片内核空间存储位置。而ruquest处理就是直接内核空间中的地址已经汇合成扇区区域了。不再琐碎了。而bio地址很琐碎,有很多bio_vec*类型列表。还是page*类型地址,且有页内偏移,所以自己整理。不过从代码编写可以看出,bio_vec已经是最小颗粒了,每次都能操作完,不会更碎了,已经是一个带页内偏移的整体小块了。

simple_block_make_request(struct request_queue*q, struct bio*bio)//这个函数是用户定义的,但是怎么调用什么时候用不受用户控制。

{

struct bio_vec *bvec;

int i;

void * dsk_mem;//用于记忆目前设备+每个bvec的长度,下次开始就从最新的dsk_mem开始

//使用bio判断是否超容量,如果超了,那么此处是每次执行一个bio所以超了,就不再是request_queue处理函数do_request中continue而是直接返回,这个bio不能用啊。

if((bio->bio.sector<<9)+bio->bio_size>SIMPLE_BLKDEV_BYTES){

printk();

bio_endio(bio,-EIO);

return 0;

}

dsk_mem=simp_blkdev_data+(bio->bi_sector<<9);

bio_for_each_segment(bvec,bio,i){

void *iovec_mem;//用于使用kmap来映射成可以操作的地址。

switch(bio_rw(bio)){

case READ:

case READA://这个还是提前读,好厉害,可是自己不会编写,所以不编写了。

iovec_mem=kmap(bvec->bv_page)+bvec->bv_offset;

memcpy(iovec_mem, dsk_mem, bvec->bv_len);

kunmap(bvec->bv_page);

break;

case WRITE:

iovec_mem=kmap()+bvec->bv_offset;

.....

default:

.....

bio_endio();//是表明有一个是坏的,那么就结束整个bio

return 0;

}

dsk_mem+=bvec->bv_len;


}

}


此时如果真是make request方式类型处理io,那么通常还真是在/sys对应的设备文件中找不到queue目录了。

还有一句话,按道理一个bio是一个请求,但是如果有调度器,那么可能将bio合并了,那么一个请求可能包含多个bio了。

第三章关于地址使用kmap的总结:

为什么内存地址使用page*描述的就意味着内存页面可能处于高端内存中无法直接访问????使用kmap是将其映射到非线性区域进行访问,访问完记得把映射区域还回去。





第四章

给设备增加一个分区。

注意按道理说分区就是alloc_disk(这里面几个数),但是直接注册几个块设备,与几个分区是不一样的。所以因为alloc_disk使用比较简单,那么先分析怎么注册多个块设备吧。是在字符设备驱动中介绍到的。支持两个globalmem设备驱动。


而原来的存储磁盘设备固定的最大值:盘面,磁道数,每个磁道扇区数,分别是255, 1024,63.而现代磁盘已经不再是旧的结构了,但是还是按照最大的盘面和每个磁道分区数来分的,所以16MB=16*1024*1024然后除以512这个扇区大小,那么扇区数再除以/255*63=2就是说只有两个磁道,而分区是一个按照磁道大小区分的。注意,一个分区至少一个磁道,就是说磁道是分区最小单位。


第5章

为设备添加物理接口,此时自己设定机械结构,是什么,方法是。但是设置了机械结构那么还能直接当成数组写入吗

先考虑添加,此时用到前面说的给struct block_device_operations{}结构体赋值,里面不仅有struct module*owner,还有很多,例如int (*getgeo) ()函数。

赋值方式struct block_device_operations={

.owner=THIS_MODULE,

getgeo=simp_blkdev_getgo,

};

而struct block_device_operations结构体格式是 内部使用分号semicolon:

struct block_device_operations{

int(*fun)A;

int(*fun)B;

};

        /*
         * capacity       heads        sectors        cylinders
         * 0~16M       1        1        0~32768
         * 16M~512M       1        32        1024~32768
         * 512M~16G       32        32        1024~32768
         * 16G~...       255        63        2088~...
         */

根据上述可知,对于不同的容量,其盘面和每个磁道扇区内容,最终会达到最大255,63然后剩下就看磁道了。

由于使用了hd_geometry结构,那么要添加linux/hdreg.h头文件,而自定义的int  simp_blkdev_getgo(){}函数参数为:

struct block_device* dev,和struct hd_geometry*geo其中geo是输出点,函数体内是给geo的各种成员参数赋值的。


在此说明一个问题 :为什么将盘面和磁道内的扇区数目设置为最大。因为:分区是以磁道数,磁道为边界的,那么磁道数越多越有可能有多个分区的。

还要回答:hd_geometry这个结构体中,如果当cylinders到达最大了,为65536那么最大根据255,63可知最大是50.2GB容量,

struct  hd_geometry {

        unsigned char heads;
        unsigned char sectors;
        unsigned short cylinders;
        unsigned long start;
};

远远不够表达磁盘容量的,那么怎么办,原因在于应用程序或者fdisk调用会得到hd_geometry结构体后将cylinders值扔掉,然后自己根据硬盘容量和设置的磁头数目和每个磁道扇区数目来自己计算真正的geo-->cylinder.


第6章

模块是加载在内核非映射区的????,原因是加载到线性映射区的时候,主要原因是很难在实际的物理内存中找到连续的物理区间,然后映射到连续的逻辑线性区间。就是连续物理实际内存比较难找。

而按照以前的1G内核逻辑区,那么剩下的逻辑空间也小,(此处补充了,为什么要将用户空间和内核空间区分开来成3+1GB。因为如果不分开,都是0-4GB,那么每次因为用户空间很多的内核函数调用,即系统调用,那么调用一次换一次mmu表,很浪费时间的,而如果分开,不切换用户程序的时候就不用切换mmu了。)https://blog.csdn.net/gatieme/article/details/52384791

这个链接讲内核的高端映射讲的特别好,例如8GB的实际物理内存,如何让逻辑空间中0xc0000000-0xffffffff这部分1GB的内核空间都访问到?除了前面的896MB线性映射之外,还有后面128MB的非线性,此时内核的PTE页表映射表是在一个位置存着的,所以内核可以分配物理不连续,逻辑连续的8GB空间,因为PTE表决定了映射空间,而128MB只是一段连续的页框空间可用(连逻辑地址都不是,就是一个可用页框空间)在这个页框中填入逻辑地址,然后在8GB的物理空间找到非连续的空间,然后将两者映射在一起即可,保存在PTE映射表中,当然映射使用完马上还,所以见了alloc_page()函数或者其他函数产生page*地址内容(是8GB的物理内存页面),然后用kmap()在128MB的空闲页框中产生出线性地址并且将这个page*给映射链接在一起,返回可访问的线性地址。完成了可以访问的所有步骤。

还有其他的api实现内存管理://参数order代表2的order次方的数目的页个数。

alloc_pages()

get_zero_page()//前两个函数都是返回实际物理地址,而第二个是将实际物理地址给赋值为0了。

get_free_pages(mask,order)//是返回的直接映射好的虚拟地址。直接可以代码使用


永久内核映射可以允许内核建立高端页框(目前这个是用词最准确的,高端页框,连逻辑地址都还不是的)到物理内存的永久映射。使用的是内核映射表中的页表,pkmap_page_table。还有各种散列管理,细节先放弃。

现在回到代码中,16MB的数组,是因为内核编码,直接吃掉了128MB中的16MB页框空间,这个太过分了。所以就不适用静态数组,而是使用vmalloc这种动态分配的,但是仍然占用了非线性空间,怎么使用线性空间呢?就是自己在线性空间页框中放逻辑地址,再到实际物理内存中找物理地址,

__get_free_page(GFP_KERNEL)就可以了,返回的不是实际物理页而是可用的映射逻辑地址值。所以直接当memcpy的内容使用了。



实际上,虽然bio_vec可能跨越页面边界,但它无论如何也不可能跨越2个以上的页面。

这是因为bio_vec本身对应的数据最大长度只有一个页面

此时还是page的大小设定,而到实际的物理设备上面就是512扇区的内容单位了,这个单位不一样的兼容性处理有什么方法

由于使用了一页一页分开的,因此此时不是连续的了,所以bio_vec是会跨页的,所以此时就要判断是否跨页现象出现。但是按照512来管理那么一定是好多bio_vec会跨多个512的。


再次强调数据:bio自带的是bio.sector和bio.size分别是起始扇区(同样是扇区个数值,所以要算地址需要乘以512)和长度占用的扇区个数。还有很重要的bio_vec.bv_page   bio_vec.bv_len   bio_vec.bv_offset.

假如不考虑分页,那么就是将bio_vec.bv_page物理地址处bio_vec.bv_offset的偏移处的bio_vec.bv_len长度的值写到bio.sector(这个就是逻辑地址了。)开始的地方,然后bio.sector+bio_vec.bv_len即可就是不断偏移往前走。  其中物理地址转换为  :kmap(bvec->bv_page)+ bvec->bv_offset;

这时候是一个bio做一次kmap。

而带上分页以及设备512扇区的约束,那么就要多加个每次对开始的逻辑地址bio.sector+countdown并且要比较countdown长度以后bv_len-countdown与pagesize-(bio.sector+countdown)%pagesize的大小.是用于判断每次bio是否写完,并且是在一页内部,这是写入的目的地地址处理方法。而源头的数据还是kmap处理的值加上已经写入的值。





对于最后,显示lsmod可以看到这个在非线性区的模块有多大,这两者差别也是很大了,并且让人知道加载的.ko驱动大小到底是什么东西了,驱动一定是非线性空间的大小。

看看模块的加载时分配的非线性映射区域大小:
# lsmod
Module                 Size  Used by
simp_blkdev            8212  0
...
#
如果这个Size一栏的数字没有引起读者的足够重视的话,我们拿修改前的模块来对比一下:
# lsmod
Module                 Size  Used by

simp_blkdev         16784392  0


问题,为什么用函数分配空间得出的代码大小比数组的要小?因为前者只真正保存了映射表,映射表特别小,机制是使用的时候映射到实际物理空间中,,而后者是将数组保存在内存中一直不动。。。


而关于申请内存,那么可以看到一个总结:因为bdbm就是使用这种方法来分配空间的,是高端映射先开始。其代码为:

/* allocate the memory for the SSD */
    if ((ptr_ramssd = (void*)bdbm_malloc
                (ssd_size_in_bytes * sizeof (uint8_t))) == NULL)

  kmalloc() / kzalloc():所申请的内存虚拟地址和物理地址都是连续的,一般最大 4M
                释放用 kfree()
            vmalloc() / vzalloc():申请大内存,虚拟地址一定连续,物理地址可能不连续
                释放用 vfree()
                vmalloc / vzalloc 需要自己的头文件 <linux/vmalloc.h>

                申请优先从高端内存(HIGH)分配内存,高端内存没有时再取低端内存。

而基树操作是:

voidINIT_RADIX_TREE((struct radix_tree_root *root, gfp_t mask);  用来初始化一个基树的结构,root是基树结构指针,mask是基树内部申请内存时使用的标志。

intradix_tree_insert(struct radix_tree_root *root, unsigned long index, void*item);   用来往基树中插入一个指针,index是指针的索引,item是指针,将来可以通过index从基树中快速获得这个指针的值。

  void*radix_tree_lookup(struct radix_tree_root *root, unsigned long index);    用来根据索引从基树中查找对应的指针,index是指针的索引。

其中插入怎么三个参数,意义分别是:

      INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);  

     ret =radix_tree_insert(&simp_blkdev_data, i, p);

dsk_mem= radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >>PAGE_SHIFT);dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) /PAGE_SIZE);   求商就是个数,就是下面的i,就是根据索引找到指针的。

  p =radix_tree_lookup(&simp_blkdev_data, i);



这两个分别是头初始化,然后插入初始化,插入的时候,p是实际                p = (void*)__get_free_page(GFP_KERNEL);的page*地址,就是将元素插入,元素是什么其实无关 紧要。

第7章

使用将4GB分开的方法,就使内核可以与用户进程共用同一个页表。

这样一来,内核地址空间中的3G~3G+896M便映射了0~896M(这个就是实际的物理地址)范围的物理内存。这个映射关系在启动系统时完成,并且在系统启动后不会改变其实这128M还不是全都能用,在其开头和结尾处还有一些区域拿去干别的事情了(希望读者去详细了解一下)
所以我们可以用这剩下的接近128M的区域来映射高于896M的物理内存。


我们在讨论内存管理的时候,需要区分是物理内存管理还是虚拟内存管理。

先说物理内存管理,内存页,这是物理内存中最小的管理单元。

    而内物理内存的分配采用的是伙伴管理方式,每一个物理页都有一个page*类型指针或者叫对象指着,分配物理内存的函数分配出来的可是不同位置的物理内存,例如get_free_pages是不能分配高端映射区的(物理的896以上)   gfp_mask是页面分配器的一个重要参数。

伙伴系统(buddy information)与slab这两个都是分配物理内存的,区别在于前者以页框为最小颗粒,后者更小了

上面说到物理页面4KB已经是最小的内容了,但是如果还需要更小的物理内存空间怎么办,使用slab allocator. slub allocator.  slob allocator三种分配器可以分配最小的几十字节几百字节的。第2,3个是第1个slab的替换用品。要想分配更小的,那么就要有最小物理内存几十字节这样对应的结构体。结构体为:struct kmem_cache和struct slab

kmalloc是为了实现连续的物理内存分配使用的函数。在讲到物理内存管理的时候提到,而vmalloc是虚拟内存管理(分配),当介绍虚拟管理的时候介绍的,但是其两者返回值一模一样。都是虚拟地址 。只是在不同地方介绍了。

vmalloc就是经历了通过伙伴系统分配物理内存,分配逻辑内存(用红黑树管理虚拟内存的块的分配和管理),然后两者绑定,

ioremap是将io地址(实际物理地址)映射到分配的虚拟地址中。

物理区管理的,是以zone为区域的,所以cat  /proc/buddyinfo   可以看到分为三种,DMA、 Normal、HighMem.

i386中,常见的zoneDMANormalHighMem,分别对应0~16M16~896M896M以上的物理内存。

Node0, zone   Normal   9955   1605    24      1     0      1      1     0      0     0      1

该行的意义是9955个0次方的页。1605个2页连续的,24个4个页连续的。依次往后推。

当我们申请4页连续为基本单元,那么伙伴系统中的小于4页的都不可使用,因此如果空间不够分配,系统会释放不用的页空间,都是实际物理的,或者是将更大连续的拆分开让4页这个单位使用(看到靠后面大的数目减少)。如果看到更小的 页数目在/proc/buddyinfo中还多了,那就说明系统确实释放了。

伙伴buddy系统会分裂大的,也会适当的时候合并小的成一整个大的

第8章

给模块添加参数,让参数确定模块大小。

步骤:在代码中添加了,一个变量a然后a=1024;最为默认值,因为模块参数可能并不会被使用,用户认为设计者总得有个默认值。然后使用static int num = 4000; module_param(num, int, S_IRUGO); 旧内核和新内核的函数类型与新的不同。好奇怪。

引入一个新的c函数:

if(sscanf(simp_blkdev_param_size, "%llu%c%c", &simp_blkdev_bytes,
                &unit, &tailc)!= 2) {
  &unit, &tailc)!= 2) {


第9章

数据安全问题。因为如果A用户申请了物理地址,用完释放,那么B也用,此时可能就会申请的完全同物理内存地址,那么可能内存中内容就可以读到了,那么驱动在设计时候就使用申请到的是0内容的,所以驱动也涉及到安全性问题。当然驱动就是内核的一部分了。

修改的方法很简单,我们申请内存时使用了__get_free_pages()函数,
这个函数的第一个参数是gfp_mask,原先我们传递的是GFP_KERNEL,表示用于内核中的一般情况。
现在我们只要向gfp_mask中添加__GFP_ZERO标志,以提示需要申请清0后的内存。类似于vzmalloc()函数。

多加一个内容:位操作

一个数乘以512就是向左移动9位,除以512就是向右移动9位,而求余数是将后9位全部变为1进行&操作,为:&~((1ULL<<9)-1)其中ULL是unsigned long long类型。

向上对齐到512的倍数,是加上一个512然后将后面512长度的真实值直接扔掉即可。为,+((1ULL<<9)-1)再&(~((1ULL<<9)-1))

再添加个新知识,hexdump可以查看任何文件,例如查看加载的任何驱动程序例如:  hexdump  /dev/xxx_dev  -vn512这就是显示加载的驱动。因为这样显示可以看到其是否有内容,用加上GFP_ZERO标志的是显示内容全部为0没有值的。

第10-12章,10、11都是铺垫。

使用free和cat /proc/meminfo都可以看到内存使用情况。与cat /proc/buddyinfo内容是有重复的。

更换分配的内存,因为使用get_free_pages是获取的低端内存的内容。使用完896就基本崩溃了。

linux在启动阶段为全部物理内存按页为单位建立了的对应的struct page结构,用来管理这些物理内存
也就是,每个页的物理内存,都有着11struct page结构,而这些struct page结构是位于低端内存中的

我们只要使用指向某个struct page结构的指针,就能指定物理内存中的一个页。

而对于自己分配的低端地址的物理内存页可以使用page_address()函数的到虚拟地址的。

而使用高端地址方法是:

                page = alloc_pages(GFP_KERNEL| __GFP_ZERO | __GFP_HIGHMEM,
                       SIMP_BLKDEV_DATASEGORDER);


我们可以使用kmap()kunmap()函数解决这个问题。kmap是一次只能是得到一page的虚拟地址而vmap可以得到好多页的起始虚拟地址。

使用低端地址映射,会有问题:   i386中无论如何也无法建立容量超过896M的块设备

第13章

释放驱动空间的使用方法。

首先看radix_tree_lookup()函数,它在基数中查找指定索引对应的指针,为了获得这一指针的值,基本上它需要把基树从上到下找一遍。

而对于free_diskmem()函数而言,我们仅仅是需要遍历基树中的所有节点,使用逐一查找的方法进行遍历未免代价太大了。radix_tree_gang_lookup()这个函数是可以使用的,

第14章

内存的推迟分配,postpone distribute.  

前面是alloc_disk时候就将内存全部分配了,,不好,修改方法是在读写函数中处理,当开始读的时候,或者写的时候,发现查找没有找到page那么才开始分配映射即可。

第15章

最后就是数据访问冲突问题,

访问冲突是问题,解决方法是加锁。或者设置临界区。







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值