zoukankan      html  css  js  c++  java
  • 块设备驱动

    出处: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.6fsBuffer.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又是怎么来的?

      参考自带的块设备驱动程序 driverslockxd.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 }

    三、看看driverslockxd.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
    ", ++r_cnt);
     74         //从磁盘里的源中读长度为len的数据到buffer中
     75         memcpy(req->buffer, ramblock_buf + offset, len); 
     76     }
     77     else
     78     {   
     79         //printk("do_ramblock_request write %d
    ", ++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");
  • 相关阅读:
    ORM框架
    js获取浏览器和元素对象的尺寸
    js中的兼容问题
    JS页面上的流氓广告功能
    JS计算1到10的每一个数字的阶乘之和
    JS中 有一个棋盘,有64个方格,在第一个方格里面放1粒芝麻重量是0.00001kg,第二个里面放2粒,第三个里面放4,棋盘上放的所有芝麻的重量
    JS中99乘法表
    JS 中计算 1
    JS中判断一个数是否为质数
    JS水仙花数
  • 原文地址:https://www.cnblogs.com/y4247464/p/10280803.html
Copyright © 2011-2022 走看看