【linux学习笔记】linux块驱动设备基本框架探索及尝试

在此首先对比一下之前经常打交道的字符型驱动设备。二者的区别主要可以概括为支不支持随机访问,字符驱动设备如键盘,执行读命令只能获取一个按照顺序的字符流,如“linux”,对这个字符流随机的访问是没有意义的;但是块驱动设备最大的不同是,它可以支持用户程序随机的访问,比如访问nandflash的某个扇区等操作。

写一个字符型驱动涉笔的大致套路就是,首先确定该字符型驱动设备的设备号,随后,在这个设备号下,建立一个file_operation结构体,逐个实现结构体中的系统调用函数,如read,write,open,ioctl等。最后一步是将这个结构体注册,可以理解为将这个结构体指针存放在该 数组[设备号] 的位置。讲究一点的话,可以将这些基本的内容融入一些框架之中,如输入输出子系统、device-bus-driver、framebuffer等。
在获取数据方面,常用的有:
1)查询方式,在用户空间中不断调用read函数查询是否有数据获取。
2)中断方式,数据准备好后内核中产生中断,异步IO的典型。
3)多路复用IO,如poll机制,epoll、select、poll函数,当数据没有准备好的时候,内核中该进程处于休眠状态,当数据准备好了,函数返回,通知用户读取数据。
4)信号量的方式,在没有数据的时候,用户空间该线程处于挂起状态,等待该信号量;数据准备完成,内核发出信号量同步两个进程,用户再去读取数据。

块驱动程序编写有什么不同?

块驱动程序操作慢在哪里?慢在负责两个扇区跳转的机械装置上。换句话说,读取一个扇区里面的内容很快,但是读取完之后去读另外一个扇区就很慢。所以,为了节约时间,常用的方法是合并和排序。因此,块驱动设备需要将读写命令插入队列,在统筹安排读写。因此,字符型驱动设备那一套就不行了。

块驱动设备框架

在这里插入图片描述文件系统的作用是将文件的读写转化成扇区的读写,这其中还涉及到虚拟文件系统VFS,以将用户空间的命令转化为各种文件系统的命令,这里不是重点。
此处的重点是研究ll_rw_block函数的调用关系,这个函数实现了从请求插入队列到执行队列中请求的一系列操作。

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

	for (i = 0; i < nr; i++) {
		struct buffer_head *bh = bhs[i];

		if (rw == SWRITE)
			lock_buffer(bh);
		else if (test_set_buffer_locked(bh))
			continue;

		if (rw == WRITE || rw == SWRITE) {
			if (test_clear_buffer_dirty(bh)) {
				bh->b_end_io = end_buffer_write_sync;
				get_bh(bh);
				submit_bh(WRITE, bh);
				continue;
			}
		} else {
			if (!buffer_uptodate(bh)) {
				bh->b_end_io = end_buffer_read_sync;
				get_bh(bh);
				submit_bh(rw, bh);
				continue;
			}
		}
		unlock_buffer(bh);
	}
}

这里首先说明个概念,就是缓冲区头buffer_head,它描述了内存中一块区域。这块区域相当于磁盘到内存的映射,可能包含一个或若干个块。从bh中,你可以知道这个内存与哪个块设备有关。

struct buffer_head {
	unsigned long b_state;		/* buffer state bitmap (see above) */
	struct buffer_head *b_this_page;/* circular list of page's buffers */
	struct page *b_page;		/* the page this bh is mapped to */

	sector_t b_blocknr;		/* start block number */
	size_t b_size;			/* size of mapping */
	char *b_data;			/* pointer to data within the page */

	struct block_device *b_bdev;
	bh_end_io_t *b_end_io;		/* I/O completion */
 	void *b_private;		/* reserved for b_end_io */
	struct list_head b_assoc_buffers; /* associated with another mapping */
	struct address_space *b_assoc_map;	/* mapping this buffer is
						   associated with */
	atomic_t b_count;		/* users using this buffer_head */
};

在2.6内核之前,这个结构体承担着相当重要的作用,在2.6之后的内核中,其管理块设备请求的作用被struct bio结构体所替代。原因是,这个bio结构体可以管理一个块设备的全部缓冲区,相比较前者bio更加轻量化。但是二者又是不可完全替代的,前者管理一个缓冲区的全部信息,而后者则管理一个块设备的io操作。
通过submit_bh(WRITE, bh);完成缓冲区头bh到bio的转换。将bio赋值完成之后,调用submit_bio(rw, bio);实现“submit a bio to the block device layer for I/O”。
这里有一个问题,在bio结构体中,明明可以一次性的管理多个缓冲区/缓冲区头,为什么bio结构体在实际编程的时候只是填充了一个缓冲区头?
答案在submit_bio(rw, bio);中,调用generic_make_request(bio)函数:
“We only want one ->make_request_fn to be active at a time,
else stack usage with stacked devices could be a problem.”
多于一个bio有可能会引起栈的问题。在这个函数中用于向队列中添加请求__generic_make_request(bio);
在这个函数中执行:1.q = bdev_get_queue(bio->bi_bdev);获取队列。
2.ret = q->make_request_fn(q, bio);向队列中添加请求。
q->make_request_fn(q, bio);中发生了什么?

el_ret = elv_merge(q, &req, bio);
/*获取linus电梯参数:向前合并?向后合并?*/
switch (el_ret) {
	...
}
init_request_from_bio(req, bio);
/*初始化命令:这个命令应该是请求队列中的成员*/
...
add_request(q, req);
/*将命令添加到请求队列*/

调用关系:

for (i = 0; i < nr; i++) {
            struct buffer_head *bh = bhs[i];
            submit_bh(rw, bh);
                struct bio *bio; // 使用bh来构造bio (block input/output)
                submit_bio(rw, bio);
                    // 通用的构造请求: 使用bio来构造请求(request)
                    generic_make_request(bio);
                        __generic_make_request(bio);
                            request_queue_t *q = bdev_get_queue(bio->bi_bdev); // 找到队列  
                            
                            // 调用队列的"构造请求函数"
                            ret = q->make_request_fn(q, bio);
                                    // 默认的函数是__make_request
                                    __make_request
                                        // 先尝试合并
                                        elv_merge(q, &req, bio);
                                        
                                        // 如果合并不成,使用bio构造请求
                                        init_request_from_bio(req, bio);
                                        
                                        // 把请求放入队列
                                        add_request(q, req);
                                        
                                        // 执行队列
                                        __generic_unplug_device(q);
                                                // 调用队列的"处理函数"
                                                q->request_fn(q);

如何写一个块驱动程序:

怎么写块设备驱动程序呢?
1. 分配gendisk: alloc_disk
2. 设置
2.1 分配/设置队列: request_queue_t  // 它提供读写能力
    blk_init_queue
2.2 设置gendisk其他信息             // 它提供属性: 比如容量
3. 注册: add_disk

参考:
drivers\block\xd.c
drivers\block\z2ram.c
写一个虚拟块驱动设备

块驱动设备结构体gendisk :

struct gendisk {
	int major;			/* major number of driver */
	int first_minor;
	int minors;                     /* maximum number of minors, =1 for
                                         * disks that can't be partitioned. */
	char disk_name[32];		/* name of major driver */
	struct hd_struct **part;	/* [indexed by minor] */
	int part_uevent_suppress;
	struct block_device_operations *fops;
	struct request_queue *queue;
	void *private_data;
	sector_t capacity;

	int flags;
	struct device *driverfs_dev;
	struct kobject kobj;
	struct kobject *holder_dir;
	struct kobject *slave_dir;

	struct timer_rand_state *random;
	int policy;

	atomic_t sync_io;		/* RAID */
	unsigned long stamp;
	int in_flight;
#ifdef	CONFIG_SMP
	struct disk_stats *dkstats;
#else
	struct disk_stats dkstats;
#endif
	struct work_struct async_notify;
};

首先是初始化,其实可以理解为对块驱动设备结构体ramblock_disk 的填充和注册:

static int ramblock_init(void)
{
	/* 1. 分配一个gendisk结构体 */
	ramblock_disk = alloc_disk(16); /* 次设备号个数: 分区个数+1 */

	/* 2. 设置 */
	/* 2.1 分配/设置队列: 提供读写能力 */
	ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock);
	ramblock_disk->queue = ramblock_queue;
	
	/* 2.2 设置其他属性: 比如容量 */
	major = register_blkdev(0, "ramblock");  /* cat /proc/devices */	
	ramblock_disk->major       = major;
	ramblock_disk->first_minor = 0;
	sprintf(ramblock_disk->disk_name, "ramblock");
	ramblock_disk->fops        = &ramblock_fops;
	set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);

	/* 3. 硬件相关操作 */
	ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);

	/* 4. 注册 */
	add_disk(ramblock_disk);

	return 0;
}
  • 内核中默认一个扇区是512k;
  • kzalloc()申请内存并把内存设置为0;

实现do_ramblock_request函数,当有IO请求下达不急于返回数据,先执行下面的函数。

static void do_ramblock_request(request_queue_t * q)
{
	static int r_cnt = 0;
	static int w_cnt = 0;
	struct request *req;
	
	//printk("do_ramblock_request %d\n", ++cnt);

	while ((req = elv_next_request(q)) != NULL) {
		/* 数据传输三要素: 源,目的,长度 */
		/* 源/目的: */
		unsigned long offset = req->sector * 512;

		/* 目的/源: */
		// req->buffer

		/* 长度: */		
		unsigned long len = req->current_nr_sectors * 512;

		if (rq_data_dir(req) == READ)
		{
			printk("do_ramblock_request read %d\n", ++r_cnt);
			memcpy(req->buffer, ramblock_buf+offset, len);
		}
		else
		{
			printk("do_ramblock_request write %d\n", ++w_cnt);
			memcpy(ramblock_buf+offset, req->buffer, len);
		}		
		
		end_request(req, 1);
	}
}

由于内核中已经实现了linus电梯法等IO调度算法,该函数进负责执行请求队列中的命令。由于在这里采用的是RAM模拟磁盘,所以并不涉及硬件操作,仅仅是判断读写,随后将一段内存复制到另一段内存中。
最后还要实现退出函数,主要是将之前申请的资源全部归还/注销。

static void ramblock_exit(void)
{
	unregister_blkdev(major, "ramblock");
	del_gendisk(ramblock_disk);
	put_disk(ramblock_disk);
	blk_cleanup_queue(ramblock_queue);
	kfree(ramblock_buf);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值