块设备驱动

出处:23.Linux-块设备驱动(详解)

通过分析块设备驱动框架,学习如何写块设备驱动

字符设备驱动:

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

块设备:

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

块设备结构:

  • 段(Segments):由若干个块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。
  • 块  (Blocks):   由Linux制定对内核或文件系统等数据处理的基本单位。通常由1个或多个扇区组成。(对Linux操作系统而言)
  • 扇区(Sectors):块设备的基本单位。通常在512字节到32768字节之间,默认512字节

以txt文件为例,来简要分析下块设备流程:

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

 

 

一、块设备驱动框架分析

应用程序对“1.txt”的读写,最终会转化成对硬件的操作,而硬件又由驱动程序操作

读写一个普通的文件,如何转换成对扇区的读写——由文件系统完成

ll_rw_block是进入扇区读写的一个通用入口,他的功能:

  1.把读写放入队列

  2.调用队列的处理函数(优化/调序/合并)

为什么是ll_rw_block,涉及到文件系统,可详见《Linux 内核源代码情景分析》,此处更关心驱动程序

 

1.1分析ll_rw_block  (low level read/write block)  ---(linux-2.6.22.6\fs\Buffer.c)-->通用的文件

 1 void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
 2 //rw:读写标志位,  nr:bhs[]长度,  bhs[]:要读写的数据数组
 3 {
 4       int i; 
 5       for (i = 0; i < nr; i++) {
 6       struct buffer_head *bh = bhs[i];             //获取nr个buffer_head
 7        ... ...
 8        if (rw == WRITE || rw == SWRITE) {
 9               if (test_clear_buffer_dirty(bh)) {
10               ... ...
11               submit_bh(WRITE, bh);                //提交WRITE写标志的buffer_head   
12          continue;
13               }}
14        else {
15               if (!buffer_uptodate(bh)) {
16               ... ...
17               submit_bh(rw, bh);               //提交其它标志的buffer_head
18               continue;
19               }}
20               unlock_buffer(bh); }
21 }

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

 1 struct buffer_head {
 2     unsigned long b_state;          //缓冲区状态标志 
 3     struct buffer_head *b_this_page;    //页面中的缓冲区 
 4     struct page *b_page;           //存储缓冲区位于哪个页面
 5     sector_t b_blocknr;           //逻辑块号
 6     size_t b_size;              //块的大小
 7     char *b_data;               //页面中的缓冲区
 8 
 9     struct block_device *b_bdev;     //块设备,来表示一个独立的磁盘设备
10 
11     bh_end_io_t *b_end_io;         //I/O完成方法
12  
13     void *b_private;             //完成方法数据
14  
15     struct list_head b_assoc_buffers;   //相关映射链表
16 
17     /* mapping this buffer is associated with */
18     struct address_space *b_assoc_map;   
19     atomic_t b_count;             //缓冲区使用计数 
20 };

 

 1.2然后进入submit_bh()中

 1 int submit_bh(int rw, struct buffer_head * bh)
 2 {
 3        struct bio *bio;                    //定义一个bio(block input output),也就是块设备i/o
 4        ... ...
 5        bio = bio_alloc(GFP_NOIO, 1);      //分配bio
 6       /*根据buffer_head(bh)构造bio */
 7        bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);      //存放逻辑块号
 8        bio->bi_bdev = bh->b_bdev;                              //存放对应的块设备
 9        bio->bi_io_vec[0].bv_page = bh->b_page;           //存放缓冲区所在的物理页面
10        bio->bi_io_vec[0].bv_len = bh->b_size;              //存放扇区的大小
11        bio->bi_io_vec[0].bv_offset = bh_offset(bh);            //存放扇区中以字节为单位的偏移量
12 
13        bio->bi_vcnt = 1;                                    //计数值
14        bio->bi_idx = 0;                                     //索引值
15        bio->bi_size = bh->b_size;                         //存放扇区的大小
16 
17        bio->bi_end_io = end_bio_bh_io_sync;             //设置i/o回调函数
18        bio->bi_private = bh;                               //指向哪个缓冲区
19        ... ...
20        submit_bio(rw, bio);                           //提交bio
21        ... ...
22 }

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

 

 1.3 submit_bio()函数如下:

void submit_bio(int rw, struct bio *bio)
{
       ... ...
       generic_make_request(bio);        
}

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

 

 1.4 generic_make_request()函数如下所示:

 1 void generic_make_request(struct bio *bio)
 2 {
 3  if (current->bio_tail) {                   // current->bio_tail不为空,表示有bio正在提交
 4               *(current->bio_tail) = bio;     //将当前的bio放到之前的bio->bi_next里面
 5               bio->bi_next = NULL;           //更新bio->bi_next=0;
 6               current->bio_tail = &bio->bi_next; //然后将当前的bio->bi_next放到current->bio_tail里,使下次的bio就会放到当前bio->bi_next里面了
 7 
 8               return;    }
 9 
10 BUG_ON(bio->bi_next);
11        do {
12               current->bio_list = bio->bi_next;
13               if (bio->bi_next == NULL)
14                      current->bio_tail = &current->bio_list;
15               else
16                      bio->bi_next = NULL;
17 
18               __generic_make_request(bio);           //调用__generic_make_request()提交bio
19               bio = current->bio_list;
20        } while (bio);
21        current->bio_tail = NULL; /* deactivate */
22 }

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

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

 

1.5 __generic_make_request()函数如下所示:

 1 static inline void __generic_make_request(struct bio *bio)
 2 {
 3 request_queue_t *q;    
 4 int ret;  
 5  ... ...
 6        do {
 7               q = bdev_get_queue(bio->bi_bdev);  //通过bio->bi_bdev获取申请队列q
 8               ... ...
 9               ret = q->make_request_fn(q, bio);             //提交申请队列q和bio
10        } while (ret);
11 }

这个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()函数 

 

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

 1 static int __make_request(request_queue_t *q, struct bio *bio)
 2 {
 3 
 4   struct request *req;          //块设备本身的队列
 5   ... ...
 6 //(1)将之前的申请队列q和传入的bio,通过排序,合并在本身的req队列中
 7   el_ret = elv_merge(q, &req, bio);
 8   ... ...
 9 
10   init_request_from_bio(req, bio);        //合并失败,单独将bio放入req队列
11   add_request(q, req);                  //单独将之前的申请队列q放入req队列
12   ... ...
13   __generic_unplug_device(q);      //(2) 执行申请队列的处理函数     
14  }

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

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

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

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

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

1 void __generic_unplug_device(request_queue_t *q)
2 {      if (unlikely(blk_queue_stopped(q)))
3               return;
4        if (!blk_remove_plug(q))
5               return;
6        q->request_fn(q);         
7 }

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

 框架分析总结

应用层:read/write——普通文件、U盘、内存

----------------------------------------------------------------------------------------

文件系统(FS)

---------------------------------------------------------------------------------------

内核层:

  ll_rw_block();             //进入内核设备层,提交buffer_head缓存区结构体

   submit_bh();           //通过提交上来的buffer_head来构造bio,然后提交bio

    submit_bio();       //把提交上来的bio提交到相应块设备的请求队列中

     generic_make_request(bio);          //对bio进行提交处理

      __generic_make_request(bio);   //获取等待队列q, 然后提交q和bio

        __make_request;                   //合并q和bio,然后执行队列

          elv_merge(q, &req, bio);                //先尝试合并

          init_request_from_bio(req, bio);   // 若合并不成用bio构造请求

          add_request(q, req);                       // 把请求放入队列

          __generic_unplug_device(q);        // 执行队列

              q->request_fn(q);                   // 调用队列的"处理函数"


小结

  • 应用程序通过文件系统最终调用了 ll_rw_block(); 来实现对块设备文件的读写,读写过程中会根据硬件的限制优化读写的顺序,先将读写放入队列再执行。‘
  • 内核中用include/linux/genhd.h 中定义的gendisk[通用磁盘(generic disk)]结构体表示一个磁盘,其中包含请求队列queue,由结构体request_queue描述。
  •  ll_rw_block(); 最终会·调用到队列中的执行函数  q->request_fn(q); 也就是说读写函数是在此处实现的,其他都由框架做好了。


二、 q->request_fn(q);
 2.1 q->request_fn是一个request_fn_proc结构体,如下图所示:

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

  参考自带的块设备驱动程序 drivers\block\xd.c

  入口函数中发现有:

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

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

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

1 request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
2 //  *rfn: request_fn_proc结构体,用来执行申请队列中的处理函数
3 //  *lock:队列访问权限的自旋锁(spinlock),该锁需要通过DEFINE_SPINLOCK()函数来定义

  显然就是将请求处理函数 do_xd_request() 挂到 xd_queue->request_fn 里,然后返回这个request_queue队列

2.2 申请队列的处理函数 do_xd_request()是如何处理的,函数如下:

 1 static void do_xd_request (request_queue_t * q)
 2 {
 3   struct request *req;            
 4 
 5   if (xdc_busy)
 6       return;
 7   /*以电梯调度算法从队列 q 中取出下一个请求*/
 8   while ((req = elv_next_request(q)) != NULL)    //(1)while获取申请队列中的需要处理的申请
 9   {
10     int res = 0;
11     ... ...
12    for (retry = 0; (retry < XD_RETRIES) && !res; retry++)         
13         res = xd_readwrite(rw, disk, req->buffer, block, count);
14                  //将获取申请req的buffer成员 读写到disk扇区中,当读写失败返回0,成功返回1
15 
16    end_request(req, res);         //申请队列中的的申请已处理结束,当res=0,表示读写失败
17     }
18 }

 

三、看看drivers\block\xd.c的入口函数大概流程,是如何创建块设备驱动的

 1 static DEFINE_SPINLOCK(xd_lock);     //定义一个自旋锁,用到申请队列中
 2 static struct request_queue *xd_queue; //定义一个申请队列xd_queue
 3 
 4 static int __init xd_init(void)          //入口函数
 5 {
 6 if (register_blkdev(XT_DISK_MAJOR, "xd"))  //1.创建一个块设备,保存在/proc/devices中
 7             goto out1;
 8 
 9 xd_queue = blk_init_queue(do_xd_request, &xd_lock);  //2.分配一个申请队列,后面会赋给gendisk结构体的queue成员
10 ... ...
11 
12 for (i = 0; i < xd_drives; i++) {                   
13   ... ...
14   struct gendisk *disk = alloc_disk(64);  //3.分配一个gendisk结构体, 64:次设备号个数,也称为分区个数
15 
16 /*    4.接下来设置gendisk结构体        */
17   disk->major = XT_DISK_MAJOR;             //设置主设备号
18   disk->first_minor = i<<6;                //设置次设备号
19   disk->fops = &xd_fops;                   //设置块设备驱动的操作函数
20   disk->queue = xd_queue;                  //设置queue申请队列,用于管理该设备IO申请队列
21   ... ...
22 
23   xd_gendisk[i] = disk;
24 }
25 
26  ... ...
27  for (i = 0; i < xd_drives; i++)
28   add_disk(xd_gendisk[i]);                                //5.注册gendisk结构体
29 }

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

 1 struct gendisk {
 2   int major;                        /*设备主设备号*/
 3   int first_minor;                  /*起始次设备号*/
 4   int minors;                       /*次设备号的数量,也称为分区数量,如果改值为1,表示无法分区*/
 5   char disk_name[32];              /*设备名称*/
 6   struct hd_struct **part;          /*分区表的信息*/
 7   int part_uevent_suppress;
 8   struct block_device_operations *fops;  /*块设备操作集合 */
 9   struct request_queue *queue;           /*申请队列,用于管理该设备IO申请队列的指针*/
10   void *private_data;                    /*私有数据*/
11   sector_t capacity;                     /*扇区数,512字节为1个扇区,描述设备容量*/
12   ....
13     };

 

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

  1. 创建一个块设备
  2. 分配一个申请队列
  3. 分配一个gendisk结构体
  4. 设置gendisk结构体的成员
  5. 注册gendisk结构体

五、自己写程序实现内存盘(内存模拟硬盘)

  参考内核自带的块设备驱动程序:

    drivers/block /xd.c  

    drivers/block /z2ram.c  

5.1、所需的结构体

  gendisk磁盘结构体,同上

 

  request申请结构体:

 1 struct request {  
 2     //用于挂在请求队列链表的节点,使用函数elv_next_request()访问它,而不能直接访问  
 3 
 4     struct list_head queuelist;   
 5     struct list_head donelist;  /*用于挂在已完成请求链表的节点*/  
 6     struct request_queue *q;   /*指向请求队列*/  
 7 
 8     unsigned int cmd_flags;    /*命令标识*/  
 9 
10     enum rq_cmd_type_bits cmd_type;  //读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写
11  
12     sector_t sector;                       //要提交的下一个扇区偏移位置(offset)
13     ... ...
14     unsigned int current_nr_sectors;   //当前需要传送的扇区数(长度) 
15     ... ...
16 
17     char *buffer;        //当前请求队列链表的申请里面的数据,用来读写扇区数据(源地址)
18     ... ...
19   };

 

5.2、 所需函数

 

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

  创建一个块设备,当major==0时,表示动态创建,创建成功会返回一个主设备号

 

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

  卸载一个块设备, 在出口函数中使用,major:主设备号, name:名称

 

struct gendisk *alloc_disk(int minors);

  分配一个gendisk结构,minors为分区数,填1表示不分区

 

void del_gendisk(struct gendisk *disk);

  释放gendisk结构,在出口函数中使用,也就是不需要这个磁盘了

 

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

  分配一个request_queue请求队列,分配成功返回一个request_queue结构体

    rfn: request_fn_proc结构体,用来执行放置在队列中的请求的处理函数

    lock:队列访问权限的自旋锁(spinlock),该锁通过DEFINE_SPINLOCK()来定义

 

void blk_cleanup_queue(request_queue_t * q);

  清除内核中的request_queue请求队列,在出口函数中使用

 

static DEFINE_SPINLOCK(spinlock_t lock);     

  定义一个自旋锁(spinlock)

 

static inline void set_capacity(struct gendisk *disk, sector_t size); 

  设置gendisk结构体扇区数(成员copacity), size等于扇区数

  该函数内容如下:

  disk->capacity = size;

 

void add_disk(struct gendisk *gd);

  向内核中注册gendisk结构体

 

void put_disk(struct gendisk *disk);

  注销内核中的gendisk结构体,在出口函数中使用

 

struct request *elv_next_request(request_queue_t *q);

  通过电梯算法获取申请队列中未完成的申请,获取成功返回一个request结构体,不成功返回NULL

  (PS: 不使用获取到的这个申请时,应使用end_request()来结束获取申请)

 

void end_request(struct request *req, int uptodate);

 结束获取申请, 当uptodate==0,表示使用该申请读写扇区失败, uptodate==1,表示成功

 

static inline void *kzalloc(size_t size, gfp_t flags);

  分配一段静态缓存,这里用来当做我们的磁盘扇区用,分配成功返回缓存地址,分配失败会返回0

 

void kfree(const void *block);

  注销一段静态缓存,与kzalloc()成对,在出口函数中使用

 

rq_data_dir(rq);

  获取request申请结构体的命令标志(cmd_flags成员),当返回READ(0)表示读扇区命令,否则为写扇区命令

 

6.步骤

6.1在入口函数中:

  • 1)使用register_blkdev()创建一个块设备
  • 2) blk_init_queue()使用分配一个申请队列,并赋申请队列处理函数
  • 3)使用alloc_disk()分配一个gendisk结构体 
  • 4)设置gendisk结构体的成员
  •   ->4.1)设置成员参数(major、first_minor、disk_name、fops)
  •   ->4.2)设置queue成员,等于之前分配的申请队列
  •   ->4.3)通过set_capacity()设置capacity成员,等于扇区数
  • 5)使用kzalloc()来获取缓存地址,用做扇区
  • 6)使用add_disk()注册gendisk结构体

6.2在申请队列的处理函数中

  • 1) while循环使用elv_next_request()获取申请队列中每个未处理的申请
  • 2)使用rq_data_dir()来获取每个申请的读写命令标志,为 0(READ)表示读, 为1(WRITE)表示写
  • 3)使用memcp()来读或者写扇区(缓存)
  • 4)使用end_request()来结束获取的每个申请

6.3在出口函数中

  • 1)使用put_disk()和del_gendisk()来注销,释放gendisk结构体
  • 2)使用kfree()释放磁盘扇区缓存
  • 3)使用blk_cleanup_queue()清除内存中的申请队列
  • 4)使用unregister_blkdev()卸载块设备

7.代码如下

  1 /*
  2  * 参考内核自带的块设备驱动程序:
  3  *  drivers/block /xd.c  
  4  *rivers/block /z2ram.c  
  5  */
  6 #include <linux/module.h>
  7 #include <linux/errno.h>
  8 #include <linux/interrupt.h>
  9 #include <linux/mm.h>
 10 #include <linux/fs.h>
 11 #include <linux/kernel.h>
 12 #include <linux/timer.h>
 13 #include <linux/genhd.h>
 14 #include <linux/hdreg.h>
 15 #include <linux/ioport.h>
 16 #include <linux/init.h>
 17 #include <linux/wait.h>
 18 #include <linux/blkdev.h>
 19 #include <linux/blkpg.h>
 20 #include <linux/delay.h>
 21 #include <linux/io.h>
 22 
 23 #include <asm/system.h>
 24 #include <asm/uaccess.h>
 25 #include <asm/dma.h>
 26 
 27 static DEFINE_SPINLOCK(ramblock_lock);  //定义一个自旋锁
 28 
 29 static struct gendisk *ramblock_disk;   //磁盘结构体
 30 static request_queue_t *ramblock_queue; //申请队列
 31 static int major;
 32 #define RAMBOCK_SIZE (1024*1024)    //设置磁盘容量为1M
 33 static unsigned char *ramblock_buf; //分配一块内存(地址)
 34 
 35 
 36 static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo)
 37 {
 38     //容量 = heads*cylinders*sectors*512
 39     geo->heads     = 2;  //两个磁头分区
 40     geo->cylinders = 32; //一个磁头有32个柱面
 41     geo->sectors   = RAMBOCK_SIZE/2/32/512; //一个柱面有多少个扇区
 42     
 43     return 0;
 44 }
 45 
 46 
 47 static struct block_device_operations ramblock_fops = {
 48     .owner    = THIS_MODULE,
 49     .getgeo = ramblock_getgeo,  //获得几何属性,保存磁盘的信息(柱头,柱面,扇区)
 50 };
 51 
 52 /* 申请队列处理函数 */
 53 static void do_ramblock_request(request_queue_t * q)
 54 {
 55     struct request *req;
 56     static int r_cnt = 0;
 57     static int w_cnt = 0;
 58 
 59     while ((req = elv_next_request(q)) != NULL) //获取每个申请
 60     {
 61     /* 数据传输三要素:源,目的,长度 */
 62     /* 源/目的 */
 63     unsigned long offset = req->sector << 9; //左移9位-->乘以512
 64 
 65     /* 目的/源 */
 66     //req->buffer
 67     
 68     /* 长度 */
 69     unsigned long len    = req->current_nr_sectors << 9;
 70 
 71     if (rq_data_dir(req) == READ)
 72     {       
 73         //printk("do_ramblock_request read %d\n", ++r_cnt);
 74         //从磁盘里的源中读长度为len的数据到buffer中
 75         memcpy(req->buffer, ramblock_buf + offset, len); 
 76     }
 77     else
 78     {   
 79         //printk("do_ramblock_request write %d\n", ++w_cnt); 
 80         //把源里的len长度的数据写到目的buffer中
 81         memcpy(ramblock_buf + offset, req->buffer, len); 
 82     }
 83     end_request(req, 1);    /* wrap up, 0 = fail, 1 = success */
 84     }
 85 }
 86 
 87 /*入口函数*/
 88 static int ramblock_init(void)
 89 {
 90     /* 1.分配一个gendisk结构体*/
 91     ramblock_disk = alloc_disk(16);  //次设备号个数:分区个数+1--->15个分区
 92     /* 2.设置*/
 93     /* 2.1分配/设置队列:提供读写*/
 94     ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock);
 95     ramblock_disk->queue = ramblock_queue; //构造好的队列放到gendisk结构体中
 96     
 97     /* 2.2 设置其他属性:比如容量*/
 98     //之前对于字符设备,注册字符设备时,还有一个fop结构体
 99     major = register_blkdev(0, "ramblock"); /* cat/proc/devices */
100     ramblock_disk->major  = major;
101     ramblock_disk->first_minor = 0; //从0开始的16个次设备都对应这个块设备
102     sprintf(ramblock_disk->disk_name, "ramblock", i+'a');
103     ramblock_disk->fops   = &ramblock_fops;
104     set_capacity(ramblock_disk, RAMBOCK_SIZE/512); //第二个参数单位是扇区(512字节):容量/512
105 
106     /* 3.硬件相关操作*/
107     ramblock_buf = kzalloc(RAMBOCK_SIZE, GFP_KERNEL);
108     /* 4.注册*/
109     add_disk(ramblock_disk);
110     
111     return 0;
112 }
113 /*出口函数*/
114 static void ramblock_exit(void)
115 {
116     unregister_blkdev(major, "ramblock");
117     del_gendisk(ramblock_disk);
118     put_disk(ramblock_disk);
119     blk_cleanup_queue(ramblock_queue);
120 
121     kfree(ramblock_buf);
122 }
123 
124 /*由于以上只是C函数,通过宏修饰使之成为入口出口函数*/
125 module_init(ramblock_init);
126 module_exit(ramblock_exit);
127 MODULE_LICENSE("GPL");

 

posted @ 2019-01-20 14:34  朱果果  阅读(449)  评论(0编辑  收藏  举报