Chinaunix首页 | 论坛 | 博客
  • 博客访问: 94411
  • 博文数量: 21
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 0
  • 用 户 组: 普通用户
  • 注册时间: 2015-05-16 09:56
文章分类
文章存档

2016年(6)

2015年(15)

我的朋友

分类: LINUX

2016-02-26 09:14:12

原文地址:块设备读写流程 作者:lxhhust

块设备与字符设备的区别

1、  从字面上理解,块设备和字符设备最大的区别在于读写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个sector,而字符设备的基本单元为字节。所以Linux中块设备驱动往往为磁盘设备的驱动,但是由于磁盘设备的IO性能与CPU相比很差,因此,块设备的数据流往往会引入文件系统的Cache机制。

2、 从实现角度来看,Linux为块设备和字符设备提供了两套机制。字符设备实现的比较简单,内核例程和用户态API一一对应,用户层的Read函数直接对应了内核中的Read例程,这种映射关系由字符设备的file_operations维护。块设备接口相对于字符设备复杂,readwrite API没有直接到块设备层,而是直接到文件系统层,然后再由文件系统层发起读写请求。

打开设备过程

1.asmlinkage  long  sys_open(const char __user *filename, int flags, int mode)

流程:用户态程序通过open()打开指定的块设备,通过systemcall机制陷入内核.

参数:filename 文件名

      flags 读写标志

      mode  权限模式

2.sys_open-->do_sys_open-->do_filp_open-->nameidata_to_filp-->__dentry_open-->f->f_op->open-->blkdev_open

static int blkdev_open(struct inode * inode, struct file * filp)

流程:执行blkdev_open()函数,该函数注册到文件系统方法(file_operations)中的open上。 

参数:inode inode

      filp 内核中文件对象结构

注:

filp_open() 

调用 open_namei() 函数取出和该文件相关的 dentry 和 inode (因为前提指明了文件已经存在,所以 dentry 和 inode 能够查找到,不用创建),然后调用 dentry_open() 函数创建新的 file 对象,并用 dentry 和 inode 中的信息初始化 file 对象(文件当前的读写位置在 file 对象中保存)。注意到 dentry_open() 中有一条语句:

f->f_op = fops_get(inode->i_fop);

这个赋值语句把和具体文件系统相关的,操作文件的函数指针集合赋给了 file 对象的  f _op 变量(这个指针集合是保存在 inode 对象中的),

3.blkdev_open-->bd_acquire

                   |

               \/

           

do_open

blkdev_open函数中调用bd_acquire()函数,bd_acquire函数完成文件系统inode到块设备bdev的转换,具体的转换方法通过hash查找实现。得到具体块设备的bdev之后,调用do_open()函数完成设备打开的操作。 

4.do_open-->get_gendisk

|

\/

gendisk->fops->open(bdev->bd_inode, file)

流程:在do_open函数中会调用到块设备驱动注册的open方法,具体调用如下:gendisk->fops->open(bdev->bd_inode, file)。 

5.

读设备过程

注:不经过文件系统,直接读写块设备,如果经过文件系统,readpage过程有区别

1.asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)

流程:用户程序通过调用read(),通过systemcall机制陷入内核, 

参数:fd 文件描述符

          buf 缓冲区

  count 缓冲区大小

2.sys_read-->vfs_read-->file->f_op->read-->do_generic_file_read-->do_generic_mapping_read-->mapping->a_ops->readpage(filp, page);

注:非direct io方式 

流程:内核执行generic_file_read,如果不是direct io方式,那么直接调用do_generic_file_read->do_generic_mapping_read()函数,在do_generic_mapping_read(函数位于filemap.c)函数中,首先查找数据是否命中Cache,如果命中,那么直接将数据返回给用户态;否则通过address_space->a_ops->readpage函数发起一个真实的读请求。 

读操作在没有命中Cache的情况下通过address_space_operations方法中的readpage函数发起块设备读请求;写操作在替换Cache或者Pdflush唤醒时发起块设备请求。发起块设备请求的过程都一样,首先根据需求构建bio结构,bio结构中包含了读写地址、长度、目的设备、回调函数等信息。构造完bio之后,通过简单的submit_bio函数将请求转发给具体的块设备。从这里可以看出,块设备接口很简单,接口方法为submit_bio(更底层函数为generic_make_request),数据结构为struct bio。 

3.static int blkdev_readpage(struct file * file, struct page * page)

blkdev_readpage-->block_read_full_page-->end_buffer_async_read

|

\/

  submit_bh

流程:

readpage函数中,构造一个buffer_head,设置bh回调函数end_buffer_async_read,然后调用submit_bh发起请求。 

4.submit_bh-->submit_bio

submit_bh函数中,根据buffer_head构造bio,设置bio的回调函数end_bio_bh_io_sync,最后通过submit_biobio请求发送给指定的快设备。 

5.submit_bio-->generic_make_request()-->q->make_request_fn

   submit_bio函数通过generic_make_request转发biogeneric_make_request是一个循环,其通过每个块设备下注册的q->make_request_fn函数与块设备进行交互。如果访问的块设备是一个有queue的设备,那么会将系统的__make_request函数注册到q->make_request_fn中;否则块设备会注册一个私有的方法。在私有的方法中,由于不存在queue队列,所以不会处理具体的请求,而是通过修改bio中的方法实现bio的转发,在私有make_request方法中,往往会返回1,告诉generic_make_request继续转发比bioGeneric_make_request的执行上下文可能有两种,一种是用户上下文,另一种为pdflush所在的内核线程上下文 

6.__make_request

通过generic_make_request的不断转发,最后请求一定会到一个存在queue队列的块设备上,假设最终的那个块设备是某个scsi disk/dev/sda)。generic_make_request将请求转发给sda时,调用__make_request,该函数是Linux提供的块设备请求处理函数。在该函数中实现了极其重要的操作,通常所说的IO Schedule就在该函数中实现。在该函数中试图将转发过来的bio merge到一个已经存在的request中,如果可以合并,那么将新的bio请求挂载到一个已经存在request中。如果不能合并,那么分配一个新的request,然后将bio添加到其中。这一切搞定之后,说明通过generic_make_request转发的bio已经抵达了内核的一个站点——request,找到了一个临时归宿。此时,还没有真正启动物理设备的操作。在__make_request退出之前,会判断一个bio中的sync标记,如果该标记有效,说明请求的bio是一个是实时性很强的操作,不能在内核中停留,因此调用了__generic_unplug_device函数,该函数将触发下一阶段的操作;如果该标记无效的话,那么该请求就需要在queue队列中停留一段时间,等到queue队列触发闹钟响了之后,再触发下一阶段的操作。__make_request函数返回0,告诉generic_make_request无需再转发bio了,bio转发结束。

7. request_queue

 到目前为止,文件系统(pdflush或者address_space_operations)发下来的bio已经mergerequest queue中,如果为sync bio,那么直接调用__generic_unplug_device,否则需要在unplug timer的软中断上下文中执行q->unplug_fn。后继request的处理方法应该和具体的物理设备相关,但是在标准的块设备上如何体现不同物理设备的差异性呢?这种差异性就体现在queue队列的方法上,不同的物理设备,queue队列的方法是不一样的。举例中的sda是一个scsi设备,在scsi middle levelscsi_request_fn函数注册到了queue队列的request_fn方法上。在q->unplug_fn(具体方法为:generic_unplug_device)函数中会调用request队列的具体处理函数q->request_fnOk,到这一步实际上已经将块设备层与scsi总线驱动层联系在了一起,他们的接口方法为request_fn(具体函数为scsi_request_fn)。 

struct request_queue

{

......

unplug_fn *unplug_fn;

.......

};

8.scsi_request_fn

明白了第(9)点之后,接下来的过程实际上和具体的scsi总线操作相关了。在scsi_request_fn函数中会扫描request队列,通过elv_next_request函数从队列中获取一个request。在elv_next_request函数中通过scsi总线层注册的q->prep_rq_fnscsi层注册为scsi_prep_fn)函数将具体的request转换成scsi驱动所能认识的scsi command。获取一个request之后,scsi_request_fn函数直接调用scsi_dispatch_cmd函数将scsi command发送给一个具体的scsi host。到这一步,有一个问题:scsi command具体转发给那个scsi host呢?秘密就在于q->queuedata中,在为sda设备分配queue队列时,已经指定了sda块设备与底层的scsi设备(scsi device)之间的关系,他们的关系是通过request queue维护的。 

9.scsi_dispatch_cmd

 在scsi_dispatch_cmd函数中,通过scsi host的接口方法queuecommandscsi command发送给scsi host。通常scsi hostqueuecommand方法会将接收到的scsi command挂到自己维护的队列中,然后再启动DMA过程将scsi command中的数据发送给具体的磁盘。DMA完毕之后,DMA控制器中断CPU,告诉CPU DMA过程结束,并且在中断上下文中设置DMA结束的中断下半部。DMA中断服务程序返回之后触发软中断,执行SCSI中断下半部。 

10.完成,回调

SCSi中断下半部中,调用scsi command结束的回调函数,这个函数往往为scsi_done,在scsi_done函数调用blk_complete_request函数结束请求request,每个请求维护了一个bio链,所以在结束请求过程中回调每个请求中的bio回调函数,结束具体的bioBio又有文件系统的buffer head生成,所以在结束bio时,回调buffer_head的回调处理函数bio->bi_end_io(注册为end_bio_bh_io_sync)。自此,由中断引发的一系列回调过程结束,总结一下回调过程如下:scsi_done->end_request->end_bio->end_bufferhead。 

11.结束

每个块设备都拥有一个操作接口:struct block_device_operations,该接口定义了opencloseioctl等函数接口,但没有,也没有必要定义readwrite函数接口。 

写设备过程

1.asmlinkage ssize_t sys_write(unsigned int fd, const char __user * buf, size_t count)

流程:用户程序通过调用write(),通过systemcall机制陷入内核, 

参数:fd 文件描述符

          buf 缓冲区

  count 缓冲区大小

2.sys_write-->vfs_write-->blkdev_file_write

3.blkdev_file_write-->generic_file_write_nolock-->generic_file_aio_write_nolock-->__generic_file_aio_write_nolock-->generic_file_buffered_write

流程:如果不是direct io操作方式,那么执行buffered write操作过程,直接调用generic_file_buffered_write函数。Buffered write操作方法会将数据直接写入Cache,并进行Cache的替换操作,在替换操作过程中需要对实际的块设备进行操作。address_space->a_ops提供了块设备操作的方法。当数据被写入到Cache之后,write函数就可以返回了,后继异步写入的任务绝大部分交给了pdflush daemon(有一部分在替换的时候做了)

总结:数据流操作到这一步,我们已经很清楚用户的数据是如何到内核了。与用户最接近的方法是file_operations,每种设备类型都定义了这一方法(由于Linux将所有设备都看成是文件,所以为每类设备都定义了文件操作方法,例如,字符设备的操作方法为def_chr_fops,块设备为def_blk_fops,网络设备为bad_sock_fops)。每种设备类型底层操作方法是不一样的,但是通过file_operations方法将设备类型的差异化屏蔽了,这就是Linux能够将所有设备都理解为文件的缘由。到这里,又提出一个问题:既然这样,那设备的差异化又该如何体现呢?在文件系统层定义了文件系统访问设备的方法,该方法就是address_space_operations,文件系统通过该方法可以访问具体的设备。对于字符设备而言,没有实现address_space_operations方法,也没有必要,因为字符设备的接口与文件系统的接口是一样的,在字符设备open操作的过程中,将inode所指向的file_operations替换成cdev所指向的file_operations就可以了。这样用户层读写字符设备可以直接调用cdevfile_operations方法了。 

const struct address_space_operations def_blk_aops = {

.readpage = blkdev_readpage,

.writepage = blkdev_writepage,

.sync_page = block_sync_page,

.prepare_write = blkdev_prepare_write,

.commit_write = blkdev_commit_write,

.writepages = generic_writepages,

.direct_IO = blkdev_direct_IO,

};

const struct file_operations def_blk_fops = {

.open = blkdev_open,

.release = blkdev_close,

.llseek = block_llseek,

.read = generic_file_read,

.write = blkdev_file_write,

   .aio_read = generic_file_aio_read,

   .aio_write = blkdev_file_aio_write, 

.mmap = generic_file_mmap,

.fsync = block_fsync,

.unlocked_ioctl = block_ioctl,

#ifdef CONFIG_COMPAT

.compat_ioctl = compat_blkdev_ioctl,

#endif

.readv = generic_file_readv,

.writev = generic_file_write_nolock,

.sendfile = generic_file_sendfile,

.splice_read = generic_file_splice_read,

.splice_write = generic_file_splice_write,

};

const struct file_operations def_chr_fops = {

.open = chrdev_open,

};

初始化一个块设备的过程

int setup_device(block_dev_t *dev, int minor)

{

       int hardsect_size = HARDSECT_SIZE;

       int chunk_size;

       sector_t dev_size;

      

       /* 分配一个请求队列 */

       dev->queue = blk_alloc_queue(GFP_KERNEL);

       if (dev->queue == NULL) {

              printk(ERROR, "blk_alloc_queue failure!\n");

              return -ENOMEM;

       }

 

       chunk_size = dev->chunk_size >> 9;    //sectors

 

       /* block_make_request注册到q->make_request上 */

       blk_queue_make_request(dev->queue, block_make_request);

      

       blk_queue_max_sectors(dev->queue, chunk_size);

       blk_queue_hardsect_size(dev->queue, hardsect_size);

       blk_queue_merge_bvec(dev->queue, block_mergeable_bvec);

 

       dev->queue->queuedata = dev;

       /* block_unplug注册到q->unplug_fn上 */

       dev->queue->unplug_fn = block_unplug;

      

       /* 分配一个gendisk */

       dev->gd = alloc_disk(1);

       if (!dev->gd) {

              prink(ERROR, "alloc_disk failure!\n");

              blk_cleanup_queue(dev->queue);

              return -ENOMEM;

       }

      

       dev->gd->major = block_major;               /* 设备的major号 */

       dev->gd->first_minor = minor;            /* 设备的minor号 */

       dev->gd->fops = &block_ops;                 /* 块设备的操作接口,opencloseioctl */

       dev->gd->queue = dev->queue;           /* 块设备的请求队列 */

       dev->gd->private_data = dev;                    

       snprintf(dev->gd->disk_name, 32, dev->block_name);

 

       dev_size = (sector_t) dev->dev_size >> 9;  

       set_capacity(dev->gd, dev_size);         /* 设置块设备的容量 */

 

       add_disk(dev->gd);                                   /* 添加块设备 */

 

       return 0;

}

注册/释放一个块设备

通过register_blkdev函数将块设备注册到Linux系统。示例代码如下:

 

static int blockdev_init(void)

{

       …

 

block_major = register_blkdev(block_major, "blockd");

       if (block_major <= 0) {

              printk(ERROR, "blockd: cannot get major %d\n", block_major);

              return -EFAULT;

       }

      

}

 

通过unregister_blkdev函数清除一个块设备。示例代码如下:

static int blockdev_cleanup(void)

{

       …

 

unregister_blkdev(block_major, "blockd");

 

}

make_request函数

make_request函数是块设备中最重要的接口函数,每个块设备都需要提供make_request函数。如果块设备为有请求队列的实际设备,那么用blk_init_queuemake_request函数被注册为__make_request,该函数由Linux系统提供;反之,需要用户提供私有函数,并用blk_queue_make_request注册。__make_request函数功能在前文已述。

 

在用户提供的私有make_request函数中往往对bio进行过滤处理,这样的驱动在Linux中有mdraid0raid1raid5),过滤处理完毕之后,私有make_request函数返回1,告诉generic_make_request函数进行bio转发。

struct request_queue

struct request_queue

{

/*

 * Together with queue_head for cacheline sharing

 */

struct list_head queue_head;

struct request *last_merge;

elevator_t *elevator;

/*

 * the queue request freelist, one for reads and one for writes

 */

struct request_list rq;

request_fn_proc *request_fn;

merge_request_fn *back_merge_fn;

merge_request_fn *front_merge_fn;

merge_requests_fn *merge_requests_fn;

make_request_fn *make_request_fn;

prep_rq_fn *prep_rq_fn;

unplug_fn *unplug_fn;

merge_bvec_fn *merge_bvec_fn;

activity_fn *activity_fn;

issue_flush_fn *issue_flush_fn;

prepare_flush_fn *prepare_flush_fn;

softirq_done_fn *softirq_done_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;

void *activity_data;

/*

 * queue needs bounce pages for pages above this limit

 */

unsigned long bounce_pfn;

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;

unsigned int max_sectors;

unsigned int max_hw_sectors;

unsigned short max_phys_segments;

unsigned short max_hw_segments;

unsigned short hardsect_size;

unsigned int max_segment_size;

unsigned long seg_boundary_mask;

unsigned int dma_alignment;

struct blk_queue_tag *queue_tags;

unsigned int nr_sorted;

unsigned int in_flight;

/*

 * sg stuff

 */

unsigned int sg_timeout;

unsigned int sg_reserved_size;

int node;

struct blk_trace *blk_trace;

/*

 * 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;

unsigned int bi_size;

struct mutex sysfs_lock;

};

阅读(2367) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~