本节书摘来异步社区《嵌入式Linux开发实用教程》一书中的第4章,第4.3节,作者:朱兆祺 ,李强 ,袁晋蓉 ,更多章节内容可以访问云栖社区“异步社区”公众号查看
嵌入式Linux开发实用教程块设备和字符设备从字面上理解最主要的区别在于读写的基本单元不同,块设备的读写基本单元为数据块,数据的输入输出都是通过一个缓冲区来完成的。而字符设备不带有缓冲,直接与实际的设备相连而进行操作,读写的基本单元为字符。从实现的角度来看,块设备和字符设备是两种不同的机制,字符设备的read、write的API直接到字符设备层,但是块设备相对复杂,是先到文件系统层,然后再由文件系统层发起读写请求。
数据块指的是固定大小的数据,这个值的大小由内核来决定。一般而言,数据块的大小通常是4096 Bytes,但是大小并不是恒定不变的,而是可以根据体系结构和所使用的文件系统进行改变。与数据块相对应的是扇区,它是由底层硬件决定大小的一个块。内核所处理的设备扇区大小是512 Bytes,无论何时内核为用户提供一个扇区编号,该扇区的大小都是512 Bytes。但是如果要使用不同的硬件扇区大小,用户必须对内核的扇区数做相应的修改。
1.file_operations结构体和字符设备驱动中的fileoperations结构体类似,块设备驱动中也有一个block_device operations结构体,它的声明位于/include/linux目录下的fs.h文件中,它是对块操作的集合。
struct block_device_operations{ int(*open)(struct inode *, struct file*); //打开设备 int(*release)(struct inode *, struct file*); //关闭设备 //实现ioctl系统调用 int(*ioctl)(struct inode *, struct file *, unsigned, unsigned long); long(*unlocked_ioctl)(struct file *, unsigned, unsigned long); long(*compat_ioctl)(struct file *, unsigned, unsigned long); int(*direct_access)(struct block_device *, sector_t, unsigned long*); //调用该函数用以检查用户是否更换了驱动器的介质 int(*media_changed)(struct gendisk*); int(*revalidate_disk)(struct gendisk*); //当介质被更换时,调用该函数做出响应 int(*getgeo)(struct block_device *, struct hd_geometry*);//获取驱动器信息 struct module *owner; //指向拥有这个结构体模块的指针,通常被初始化为THIS_MODULE };``` 与字符驱动不同的是在这个结构体中缺少了read()和write()函数,那是因为在块设备的I/O子系统中,这些操作都是由request函数进行处理的。 request函数的原型如下:void request(request_queue_t *queue);`当内核需要驱动程序处理读取、写入以及其他对设备的操作时,便会调用request函数。
2.gendisk结构体gendisk结构体的定义位于/include/linux目录下的genhd.h文件中,如下所示。
struct gendisk { /* *这3个成员的定义依次是:主设备号、第一个次设备号,次设备号。一个驱动中至少有一个次设备号, *如果驱动器是一个可被分区,那么每一个分区都将分配一个次设号。 */ int major; int first_minor; int minors; //这个数组用以存储驱动设备的名字 char disk_name[DISK_NAME_LEN]; char *(*devnode)(struct gendisk *gd, umode_t *mode); unsigned int events; unsigned int async_events; struct disk_part_tbl __rcu *part_tbl; struct hd_struct part0; //这个结构体用以设置驱动中的各种设备操作 const struct block_device_operations *fops; //Linux内核使用这个结构体为设备管理I/O请求,具体详解见request_queue结构 struct request_queue *queue; void *private_data; int flags; struct device *driverfs_dev; struct kobject *slave_dir; struct timer_rand_state *random; atomic_t sync_io; struct disk_events *ev; #ifdef CONFIG_BLK_DEV_INTEGRITY struct blk_integrity *integrity; #endif int node_id; };``` gendisk结构体是动态分配,但是驱动程序自己不能动态分配该结构,而是通过调用alloc_disk()函数进行动态分配。struct gendisk *alloc_disk(int minors);`其中minors是该磁盘使用的次设备号。
但是分配了gendisk结构并不意味着该磁盘就对系统可用,使用之前的初始化结构体并且调用add_disk()函数。
//初始化结构体 struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock) //添加分区 void add_disk(struct gendisk *gd)``` 如果不再需要这个磁盘,则对其进行卸载。//删除分区Void del_gendisk(struct gendisk *gd)void blk_cleanup_queue(struct request_queue *q)`3.bio结构体bio结构体的定义位于/include/linux目录下的linux_blk_types.h文件中。
struct bio { //需要传输的第一个(512 bytes)扇区 sector_t bi_sector; struct bio *bi_next; struct block_device *bi_bdev; unsigned long bi_flags; unsigned long bi_rw; unsigned short bi_vcnt; unsigned short bi_idx; //BIO中所包含的物理段数目 unsigned int bi_phys_segments; //所传输的数据大小(以byte为单位) unsigned int bi_size; unsigned int bi_seg_front_size; unsigned int bi_seg_back_size; unsigned int bi_max_vecs; atomic_t bi_cnt; struct bio_vec *bi_io_vec; bio_end_io_t *bi_end_io; void *bi_private; #ifdef CONFIG_BLK_CGROUP struct io_context *bi_ioc; struct cgroup_subsys_state *bi_css; #endif #if defined(CONFIG_BLK_DEV_INTEGRITY) struct bio_integrity_payload *bi_integrity; #endif bio_destructor_t *bi_destructor; struct bio_vec bi_inline_vecs[0]; };``` bio结构体包含了驱动程序执行请求的所有信息,既描述了磁盘的位置,又描述了内存的位置,是上层内核与下层驱动的连接纽带。 bio结构体的核心在于:struct bio_vec *bi_io_vec;`而bio_vec结构体的声明为:
struct bio_vec { struct page *bv_page; /*数据段所在的页*/ unsigned short bv_len; /*数据段的长度*/ unsigned short bv_offset; /*数据段页内偏移*/ };``` 结构bio_vec代表了内存中的一个数据段,数据段用页、偏移和长度来描述。bio_vec结构体和bio结构体之间的关系如图4.4所示。 从图4.4可知,当I/O请求被转换到bio结构体之后,它将被单独的物理内存页所销毁。 <div style="text-align: center"><img src="https://yqfile.alicdn.com/eb997a99fbe2726a9e884fddd4ce3fa00dfa9840.png" width="" height=""> </div> 4.requeset结构体 request结构体代表了挂起的I/O请求,每个请求用一个结构request实例描述,存放在请求队列链表中,由电梯算法进行排序,每个请求包含一个或多个结构bio实例。requeest结构体声明位于/include/linux目录下的blkdev.h文件中。struct request { struct list_head queuelist; struct call_single_data csd;
struct request_queue *q;
unsigned int cmd_flags; enum rq_cmd_type_bits cmd_type; unsigned long atomic_flags;
int cpu;
unsigned int __data_len; sector_t __sector;
struct bio *bio; struct bio *biotail;
struct hlist_node hash; union { struct rb_node rb_node; void *completion_data; };
union { struct { struct io_cq *icq; void *priv[2]; } elv;
struct { unsigned int seq; struct list_head list; rq_end_io_fn *saved_end_io; } flush; };
struct gendisk *rq_disk; struct hd_struct *part; unsigned long start_time;
struct request_list *rl; unsigned long long start_time_ns; unsigned long long io_start_time_ns;
unsigned short nr_phys_segments;
unsigned short nr_integrity_segments;
unsigned short ioprio;
int ref_count;
void *special; char *buffer;
int tag; int errors;
unsigned char __cmd[BLK_MAX_CDB];
unsigned char *cmd; unsigned short cmd_len;
unsigned int extra_len; unsigned int sense_len; unsigned int resid_len; void *sense;
unsigned long deadline; struct list_head timeout_list; unsigned int timeout; int retries;
rq_end_io_fn *end_io; void *end_io_data;
struct request *next_rq;};`5.request_queue结构体每个块设备都有一个请求队列,每个请求队列单独执行I/O调度。请求队列是由请求结构实例链接成的双向链表,链表以及整个队列的信息用request_queue结构体描述,称为请求队列对象结构或请求队列结构。request_queue结构体声明位于/include/linux目录下的blkdev.h文件中。
struct request_queue { struct list_head queue_head; struct request *last_merge; struct elevator_queue *elevator; int nr_rqs[2]; int nr_rqs_elvpriv; struct request_list root_rl; request_fn_proc *request_fn; make_request_fn *make_request_fn; prep_rq_fn *prep_rq_fn; unprep_rq_fn *unprep_rq_fn; merge_bvec_fn *merge_bvec_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; sector_t end_sector; struct request *boundary_rq; struct delayed_work delay_work; struct backing_dev_info backing_dev_info; void *queuedata; unsigned long queue_flags; int id; gfp_t bounce_gfp; spinlock_t __queue_lock; spinlock_t *queue_lock; struct kobject kobj; unsigned long nr_requests; /* Max # of requests */ unsigned int nr_congestion_on; unsigned int nr_congestion_off; unsigned int nr_batching; unsigned int dma_drain_size; void *dma_drain_buffer; 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 list_head icq_list; #ifdef CONFIG_BLK_CGROUP DECLARE_BITMAP (blkcg_pols, BLKCG_MAX_POLS); struct blkcg_gq *root_blkg; struct list_head blkg_list; #endif struct queue_limits limits; unsigned int sg_timeout; unsigned int sg_reserved_size; int node; #ifdef CONFIG_BLK_DEV_IO_TRACE struct blk_trace *blk_trace; #endif unsigned int flush_flags; unsigned int flush_not_queueable:1; unsigned int flush_queue_delayed:1; unsigned int flush_pending_idx:1; unsigned int flush_running_idx:1; unsigned long flush_pending_since; struct list_head flush_queue[2]; struct list_head flush_data_in_flight; struct request flush_rq; struct mutex sysfs_lock; int bypass_depth; #if defined(CONFIG_BLK_DEV_BSG) bsg_job_fn *bsg_job_fn; int bsg_job_size; struct bsg_class_device bsg_dev; #endif #ifdef CONFIG_BLK_CGROUP struct list_head all_q_node; #endif #ifdef CONFIG_BLK_DEV_THROTTLING struct throtl_data *td; #endif };``` ####4.3.2 块设备驱动程序 块设备是一种抽象的设备驱动,它看不到、摸不着。由于这本书是针对嵌入式Linux初学者,因此笔者使用最简单、最通俗易懂的一个块设备驱动程序向各位读者展现如何开辟、挂载、使用一个分区。或许这个程序有很多不合理之处,但却是初学者容易接受的。/* * 头文件 */
/ 主设备号,COMPAQ_SMART2_MAJOR = 72 /
/ 设备名称 /
/ 次设备号 /
/ 使用数组储块设备数据 //* * 这个数组应该是最忌讳的,1MB的全局变量*/unsigned char zzq_blkdev_data[ZZQ_BLKDEV_SIZE];
/ 定义一个指向请求队列的结构体指针 /static struct request_queue *zzq_blkdev_queue;/ 定义一个指向独立分区(磁盘)的结构体指针 /static struct gendisk *zzq_blkdev_disk;
/* * 请求队列的操作 */static void zzq_blkdev_do_request(struct request_queue *q){ struct request *req;
/* * blk_rq_pos() : the current sector 当前扇区 * blk_rq_bytes() : bytes left in the entire request * blk_rq_cur_bytes() : bytes left in the current segment * blk_rq_err_bytes() : bytes left till the next error boundary * blk_rq_sectors() : sectors left in the entire request * blk_rq_cur_sectors() : sectors left in the current segment */
/* * 从请求队列中取出一个请求(可能是请求中的一段), * 如果不是为空的话 */ while((req = blk_fetch_request(q)) != NULL) { if(((blk_rq_pos(req) + blk_rq_sectors(req)) << 9) > ZZQ_BLKDEV_SIZE) { printk(KERN_ERR ZZQ_BLKDEV_DISKNAME "bad request :block = %llu,count = %un", (u64)blk_rq_pos(req), blk_rq_sectors(req)); /* * 结束一个队列请求,第二个参数表示请求处理结果 * 成功的话设定为1,失败的话设定为0或者错误号 */ __blk_end_request_all(req, -EIO); continue; } /** rq_data_dir()函数返回该请求的方向:读还是写 */ switch( rq_data_dir(req)) { / 如果是读 / case READ: printk(KERN_ALERT "readn"); / 把块设备的数据装入队列缓冲区 / memcpy(req->buffer, zzq_blkdev_data + (blk_rq_pos(req) << 9), blk_rq_sectors(req) << 9); / 请求结束 / __blk_end_request_all(req, 1); break; / 如果是写 / case WRITE: printk(KERN_ALERT "writen"); / 把缓冲区的数据写入块设备 / memcpy(zzq_blkdev_data + (blk_rq_pos(req) << 9), req->buffer, blk_rq_sectors(req) << 9); / 请求结束 / __blk_end_request_all(req, 1); break;
default: printk(KERN_ALERT "this should not happen"); break; } }}
/* * 块设备操作的集合 */struct block_device_operations zzq_blkdev_fops = { .owner = THIS_MODULE,};
/* * 块设备的初始化 */static int __init zzq_blkdev_init(void ){
int ret;printk(KERN_ALERT ZZQ_BLKDEV_DISKNAME "init! n");
/ 初始化请求队列 / zzq_blkdev_queue = blk_init_queue(zzq_blkdev_do_request, NULL); if(!zzq_blkdev_queue) { ret = -ENOMEM; goto err_init_queue; }
/ 为独立分区开辟一个空间 / zzq_blkdev_disk = alloc_disk(ZZQ_MIJORS); if(!zzq_blkdev_disk) { ret = -ENOMEM; goto err_alloc_disk; }
/* * 以下是初始化分区结构体成员 */ / 设备名称 / strcpy(zzq_blkdev_disk->disk_name, ZZQ_BLKDEV_DISKNAME); / 主设备号 / zzq_blkdev_disk->major = ZZQ_BLKDEV_DEVICEMAJOR; / 调用块设备操作集合 / zzq_blkdev_disk->fops = &zzq_blkdev_fops; / 初始化设备的请求队列 / zzq_blkdev_disk->queue = zzq_blkdev_queue; /* * 给分区分配空间 * * 由于块设备的大小使用扇区作为基本单元, * 扇区的默认大小是512byte,也就是向右移动9位 */ set_capacity(zzq_blkdev_disk, ZZQ_BLKDEV_SIZE >> 9); / 注册分区 / add_disk(zzq_blkdev_disk);
return 0;
err_alloc_disk: blk_cleanup_queue(zzq_blkdev_queue);
err_init_queue: return ret;}
/* * 块设备卸载 */static void zzq_blkdev_exit(void){printk(KERN_ALERT"exit zzqblkdev! n"); / 释放删除分区 add_disk() / del_gendisk(zzq_blkdev_disk); / blk_init_queue() / blk_cleanup_queue(zzq_blkdev_queue);}
module_init(zzq_blkdev_init);module_exit(zzq_blkdev_exit);MODULE_LICENSE("GPL");`这个程序实现的是开辟一个1MB的独立分区(磁盘),我们可以对这个分区进行读写和挂载等操作。
这里面有一个数组,使用了1MB的全局变量空间,这是很忌讳的。但是为了程序的通俗易懂性,笔者还是这样做了。要写好一个漂亮的块设备驱动程序,程序员必须要有深厚的C语言功底和数据结构知识。如果要去掉这个1MB的全局变量空间,这里内存的申请可以使用基树,或者也可以使用红黑树、哈希表等。
在这里基树是首选,笔者也希望各位读者能使用基树优化这个驱动程序。内核提供了一个基树库,代码在/lib/目录下的radix-tree.c文件中。基树是一种空间换时间的数据结构,通过空间的冗余减少时间上的消耗。我们使用如图4.5所示来描述基树算法。
如图4.5所示,元素空间总共为256,但元素个数不固定。那么如果用数组存储,好处是插入查找只用一次操作,但是存储空间需要256,空间换取时间,但是在嵌入式中,内存是宝贵的。如果用链表存储,存储空间节省了,但是极限情况下查找操作次数等于元素的个数,时间换取空间,但是时间同样是宝贵的。能不能有一种算法,可以兼顾时间和空间呢,有,基树。采用一棵高度为2的基树,第一级最多16个冗余结构,代表元素前四位的索引。第二级代表元素后四位的索引。那么只要两级查找就可以找到特定的元素,而且只有少量的冗余数据。图中假设只有一个元素10001000,那么只有树的第一级有元素,而且树的第二级只有1000个节点有子节点,其他节点都不必分配空间。这样既可以快速定位查找,也减少了冗余数据。基树很适合存储稀疏的数据,内核中文件的页cache就是采用的基树。
将这个块设备驱动程序zzqblkdev.c放入/drivers/block目录下,修改Makefile,将其编译成独立模块。
obj-m += zzqblkdev.o``` 编译:zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3$ make modules…… CC drivers/block/zzqblkdev.mod.o LD [M] drivers/block/zzqblkdev.ko……`将生成的zzqblkdev.ko模块放入OK6410的根文件系统中,挂载:
[YJR@zhuzhaoqi]\# insmod zzqblkdev.ko [YJR@zhuzhaoqi]\# lsmod zzqblkdev 1049808 0 - Live 0xbf000000``` 可知挂载zzqblkdev模块成功,并且分配给这个分区的大小即为1MB的空间。再看看/dev下面的设备节点。[YJR@zhuzhaoqi]# ls -l /dev/zzqdisk brw-rw---- 1 root root 72, 0 Jan 1 08:11 /dev/zzqdisk`主设备号为72,次设备号为1。
相关资源:嵌入式Linux应用程序开发标准教程(华清远见)