• linux下的块设备驱动(一)


    块设备的驱动比字符设备的难,这是因为块设备的驱动和内核的联系进一步增大,但是同时块设备的访问的几个基本结构和字符还是有相似之处的。

    有一句话必须记住:对于存储设备(硬盘~~带有机械的操作)而言,调整读写的顺序作用巨大,因为读写连续的扇区比分离的扇区快。

    但是同时:SD卡和U盘这类设备没有机械上的限制,所以像上面说的进行连续扇区的调整显得就没有必要了。

    先说一下对于硬盘这类设备的简单的驱动。

    在linux的内核中,使用gendisk结构来表示一个独立的磁盘设备或者分区。这个结构中包含了磁盘的主设备号,次设备号以及设备名称。

    在国嵌给的历程中,对gendisk这个结构体的填充是在simp_blkdev_init函数中完成的。在对gendisk这个结构填充之前要对其进行分配空间。具体代码如下:

    simp_blkdev_disk = alloc_disk(1);
            if (!simp_blkdev_disk) {
                    ret = -ENOMEM;
                    goto err_alloc_disk;
            }

    这里的alloc_disk函数是在内核中实现的,它后面的参数1代表的是使用次设备号的数量,这个数量是不能被修改的。

    在分配好了关于gendisk的空间以后就开始对gendisk里面的成员进行填充。具体代码如下:

    strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME); //宏定义simp_blkdev
            simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR; //主设备号
            simp_blkdev_disk->first_minor = 0; //次设备号
            simp_blkdev_disk->fops = &simp_blkdev_fops; //主要结构
            simp_blkdev_disk->queue = simp_blkdev_queue;
            set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9); //宏定义(16*1024*1024),实际上就是这个结构体。

    在填充好gendisk这个结构以后向内核中 注册这个磁盘设备。具体代码如下:

    add_disk(simp_blkdev_disk);

    在LDD中说,想内核中注册设备的必须在gendisk这个结构体已经填充好了以后,我们以前的字符设备的时候也是这么做的,不知道为什么LDD在这里强调了这个。

    当不需要一个磁盘的时候要释放gendisk,释放部分的代码在函数simp_blkdev_exit中实现的。具体的释放代码如下:

    del_gendisk(simp_blkdev_disk);

    在simp_blkdev_exit中同时还有put_disk(simp_blkdev_disk),这个是用来进行操作gendisk的引用计数。simp_blkdev_exit还实现了blk_cleanup_queue清除请求队列的这个函数。终于说到请求队列了。

    在说等待队列之前先要明确几个概念:

    ①用户希望对硬盘数据做的事情叫做请求,这个请求和IO请求是一样的,所以IO请求来自于上层
    每一个IO请求对应内核中的一个bio结构

    IO调度算法可以将连续的bio(也就是用户的对硬盘数据的相邻簇的请求)合并成一个request
    多个request就是一个请求队列,这个请求队列的作用就是驱动程序响应用户的需求的队列。

     请求队列在国嵌的程序中的simp_blkdev_queue

    下面先说一下硬盘这类带有机械的存储设备的驱动。

    这类驱动中用户的IO请求对应于硬盘上的簇可能是连续的,可能是不连续的,连续的当然好,如果要是不连续的,那么IO调度器就会对这些BIO进行排序(例如老谢说的电梯调度算法),合并成一个request,然后再接收请求,再合并成一个request,多个request之后那么我们的请求队列就形成了,然后就可以向驱动程序提交了。

    在硬盘这类的存储设备中,请求队列的初始化代码如下:

    simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);

    老谢说,在这种情况下首先调用的是内核中的make_requst函数,然后再调用自己定义的simp_blkdev_do_request。追了一下内核代码,会发现make_requst内核代码如下所示:

    static int make_request(struct request_queue *q, struct bio * bio)

    具体的就不贴了,不过可以知道这个make_request的作用就是使用IO调度器对多个bio的访问顺序进行了优化调整合并为一个request。也就是在执行完成了这个函数之后才去正式的执行内核的请求队列。

    合并后的request其实还是一个结构,这个结构用来表征IO的请求,这个结构在内核中有具体的定义。

    请求是一个结构,同时请求队列也是一个结构,这个请求队列在内核中的结构定义如下:

    struct request_queue
    {
    	/*
    	 * Together with queue_head for cacheline sharing
    	 */
    	struct list_head	queue_head;
    	struct request		*last_merge;
    	struct elevator_queue	*elevator;
    
    	/*
    	 * the queue request freelist, one for reads and one for writes
    	 */
    	struct request_list	rq;
    
    	request_fn_proc		*request_fn;
    	make_request_fn		*make_request_fn;
    	prep_rq_fn		*prep_rq_fn;
    	unplug_fn		*unplug_fn;
    	merge_bvec_fn		*merge_bvec_fn;
    	prepare_flush_fn	*prepare_flush_fn;
    	softirq_done_fn		*softirq_done_fn;
    	rq_timed_out_fn		*rq_timed_out_fn;
    	dma_drain_needed_fn	*dma_drain_needed;
    	lld_busy_fn		*lld_busy_fn;
    
    	/*
    	 * Dispatch queue sorting
    	 */
    	sector_t		end_sector;
    	struct request		*boundary_rq;
    
    	/*
    	 * Auto-unplugging state
    	 */
    	struct timer_list	unplug_timer;
    	int			unplug_thresh;	/* After this many requests */
    	unsigned long		unplug_delay;	/* After this many jiffies */
    	struct work_struct	unplug_work;
    
    	struct backing_dev_info	backing_dev_info;
    
    	/*
    	 * The queue owner gets to use this for whatever they like.
    	 * ll_rw_blk doesn't touch it.
    	 */
    	void			*queuedata;
    
    	/*
    	 * queue needs bounce pages for pages above this limit
    	 */
    	gfp_t			bounce_gfp;
    
    	/*
    	 * various queue flags, see QUEUE_* below
    	 */
    	unsigned long		queue_flags;
    
    	/*
    	 * protects queue structures from reentrancy. ->__queue_lock should
    	 * _never_ be used directly, it is queue private. always use
    	 * ->queue_lock.
    	 */
    	spinlock_t		__queue_lock;
    	spinlock_t		*queue_lock;
    
    	/*
    	 * queue kobject
    	 */
    	struct kobject kobj;
    
    	/*
    	 * queue settings
    	 */
    	unsigned long		nr_requests;	/* Max # of requests */
    	unsigned int		nr_congestion_on;
    	unsigned int		nr_congestion_off;
    	unsigned int		nr_batching;
    
    	void			*dma_drain_buffer;
    	unsigned int		dma_drain_size;
    	unsigned int		dma_pad_mask;
    	unsigned int		dma_alignment;
    
    	struct blk_queue_tag	*queue_tags;
    	struct list_head	tag_busy_list;
    
    	unsigned int		nr_sorted;
    	unsigned int		in_flight[2];
    
    	unsigned int		rq_timeout;
    	struct timer_list	timeout;
    	struct list_head	timeout_list;
    
    	struct queue_limits	limits;
    
    	/*
    	 * sg stuff
    	 */
    	unsigned int		sg_timeout;
    	unsigned int		sg_reserved_size;
    	int			node;
    #ifdef CONFIG_BLK_DEV_IO_TRACE
    	struct blk_trace	*blk_trace;
    #endif
    	/*
    	 * reserved for flush operations
    	 */
    	unsigned int		ordered, next_ordered, ordseq;
    	int			orderr, ordcolor;
    	struct request		pre_flush_rq, bar_rq, post_flush_rq;
    	struct request		*orig_bar_rq;
    
    	struct mutex		sysfs_lock;
    
    #if defined(CONFIG_BLK_DEV_BSG)
    	struct bsg_class_device bsg_dev;
    #endif
    };


    LDD说,请求队列实现了一个插入接口,这个接口允许使用多个IO调度器,大部分IO调度器批量累计IO请求,并将它们排列为递增或者递减的顺序提交给驱动。

    多个连续的bio会合并成为一个request,多个request就成为了一个请求队列,这样bio的是直接的也是最基本的请求,bio这个结构的定义如下:

    struct bio { sector_t            bi_sector;
           struct bio          *bi_next;    /* request queue link */
           struct block_device *bi_bdev;	/* target device */
           unsigned long       bi_flags;    /* status, command, etc */ unsigned long       bi_rw;       /* low bits: r/w, high: priority */
           unsigned int	bi_vcnt;     /* how may bio_vec's */
           unsigned int	bi_idx;		/* current index into bio_vec array */
           unsigned int	bi_size;     /* total size in bytes */
           unsigned short 	bi_phys_segments; /* segments after physaddr coalesce*/ unsigned short	bi_hw_segments; /* segments after DMA remapping */ unsigned int	bi_max;	     /* max bio_vecs we can hold used as index into pool */ struct bio_vec   *bi_io_vec;  /* the actual vec list */
           bio_end_io_t	*bi_end_io;  /* bi_end_io (bio) */
           atomic_t		bi_cnt;	     /* pin count: free when it hits zero */ void             *bi_private;
           bio_destructor_t *bi_destructor; /* bi_destructor (bio) */ };


    需要注意的是,在bio这个结构中最重要的是bio.vec这个结构。同时还有许多操作bio的宏,这些都是内核给实现好了的。

    请求队列的实现:

    首先使用 while ((req = elv_next_request(q)) != NULL)进行循环检测,看看到底传来的IO请求是个什么。

    然后进行读写区域的判定:

    if ((req->sector + req->current_nr_sectors) << 9
                            > SIMP_BLKDEV_BYTES) {
                            printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                    ": bad request: block=%llu, count=%u
    ",
                                    (unsigned long long)req->sector,
                                    req->current_nr_sectors);
    						//结束本次请求。
                            end_request(req, 0);
                            continue;
                    }

    在进行读写区域的判定的时候涉及到了很多linux的编程习惯。

    sector表示要访问的第一个扇区。

    current_nr_sectors表示预计访问扇区的数目。

    这里的左移九位其实就是乘以512。

    这样((req->sector + req->current_nr_sectors) << 9就计算出可以预计要访问的扇区的大小。进行了一次判断。

    如果上面的判断没有超出范围,那么就可以对请求的这一部分块设备进行操作了。

    simp_blkdev_disk = alloc_disk(1);
            if (!simp_blkdev_disk) {
                    ret = -ENOMEM;
                    goto err_alloc_disk;
            }
    
            strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
            simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
            simp_blkdev_disk->first_minor = 0;
            simp_blkdev_disk->fops = &simp_blkdev_fops;
            simp_blkdev_disk->queue = simp_blkdev_queue;
            set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
            add_disk(simp_blkdev_disk);
    
            return 0;

    关于gendisk结构的内存分配和成员的填充和硬盘类块设备是一样的。

    由于SD卡和U盘属于一类非机械类的设备,所以我们不需要那么复杂的调度算法,也就是不需要把io请求进行排序,所以我们需要自己为自己分配一个请求队列。具体代码如下:

     

    simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);

    需要注意一下的是在硬盘类块设备的驱动中这个函数的原型是blk_init_queue(simp_blkdev_do_request, NULL);

    在这种情况下其实并没有调用内核的make_request(这个函数的功能上文说过),也就是说我们接下来要绑定的simp_blkdev_make_request这个函数和make_request是同一级别的函数。这里就没有涉及到算法调度的问题了。

    然后需要进行的工作是:绑定制造请求函数和请求队列。具体代码如下:

    blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);

    把gendisk的结构成员都添加好了以后就可以执行add_disk(simp_blkdev_disk);这个函数把这块分区添加进内核了。

    整体列出制造请求部分的函数。

    //这个条件是在判断当前正在运行的内核版本。
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                    bio_endio(bio, 0, -EIO);
    #else
                    bio_endio(bio, -EIO);
    #endif
                    return 0;
            }
    
            dsk_mem = simp_blkdev_data + (bio->bi_sector << 9);
    		
    		//遍历
            bio_for_each_segment(bvec, bio, i) {
                    void *iovec_mem;
    
                    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_page) + bvec->bv_offset;
                            memcpy(dsk_mem, iovec_mem, bvec->bv_len);
                            kunmap(bvec->bv_page);
                            break;
                    default:
                            printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                    ": unknown value of bio_rw: %lu
    ",
                                    bio_rw(bio));
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                            bio_endio(bio, 0, -EIO);
    #else
                            bio_endio(bio, -EIO);
    #endif
                            return 0;
                    }
                    dsk_mem += bvec->bv_len;
            }
    
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
            bio_endio(bio, bio->bi_size, 0);
    #else
            bio_endio(bio, 0);
    #endif
    
            return 0;
    }
  • 相关阅读:
    精选文章
    Eclipse Git插件切换分支的时候不要Reset
    Spring ContentNegotiatingViewResolver
    Spring3 MVC 类型转换
    FTP DOS 命令行
    Java xml 解析
    Java 实现FTP上传和下载
    Hibernate update 和 merge 、saveOrUpdate的区别
    Spring MVC 文件下载时候 发现IE不支持
    Javasript 正则匹配任意字符
  • 原文地址:https://www.cnblogs.com/dyllove98/p/3165567.html
Copyright © 2020-2023  润新知