文件读写,从请求到IO启动
	在Linux中,块设备的读写是一个比较复杂的过程。如果再加上VFS的话,层次就更多了。实际上VFS和块设备驱动联系的不是非常密切。在VFS中,我们会看到当发生读写请求的时候,会调用ll_rw_block函数或者submit_bh函数,其中ll_rw_block是对submit_bh的封装。这个函数,实际上就是从VFS到实际设备读写的必经之路。关于这一点,有很多用systemtap来观测io的脚本就是在submit_bh函数安装一个stub(有关io检测的相关的文章,可以参看淘宝的大牛诸霸的博客)。
	在这里,我们不再谈论VFS的东西,而是从submit_bh开始看起,然后到数据被读出,进程又继续执行的流程。
	submit_bh的功能正如其函数的名字那样–“提交buffer head”。那么,具体提交给谁呢?由谁来提交呢?其实块设备的读写似乎是一个C/S架构的服务器。客户端是各个进行io操作的进程,服务器端就是设备的请求队列。进程把请求的信息包装成一个request的数据结构,然后,挂载在服务端,即块设备的请求队列中。我前面说的那句话“似乎是一个C/S架构的”,而不是真正C/S架构的。因为,C/S架构来源于网络程序,客户端进程把数据发往正在监听的服务端,然后服务端的进程从网络缓冲区中经过网络协议的层层解压“剥皮”,拿到数据。而我们谈论这个文件数据的读写并不真正是C/S架构的,原因就是,客户端的发送和服务端的接收请求全由一个进程,即用户进程来完成的。这一点应该很好理解,因为在OS中,除了中断以及异常处理没有上下文外,其他的都有进程上下文,因此,从submit读写请求到接收读写请求,当然就是进程自己的事情了,当然,如果你要是抬杠“在内核中完全可以由一个专门的线程用来处理服务端的事情”,我也无话可说。原理上,你这个抬杠当然是行得通的。说到这里,我想起了微内核的MINIX,就此打住,继续回到他们的处理过程中。
	当用户进程提交请求,并挂载到块设备队列的过程中,还涉及一个IO调度的问题,即是,用户进程在提交一个请求时,遇到了调度算法,这个调度算法做的事情很简单,它检查这个请求和正在排队的请求能否合二为一。如果不能合二为一,那就直接挂上去。至此,一个进程所要做的工作基本上就结束了。可是,请求被相应的时机呢?什么时候,它的请求才能满足?
	我们清楚,一个硬件设备,特别是块设备,让它来读取数据然后内核再从端口里面把数据提出来,或者说,通过DMA的方式,直接从磁盘中拿到数据,这个数据读出来的过程是很漫长的,这个漫长是相对于CPU来说的,绝对时间其实是很短暂的。一般情况下,用户进程在往块设备的请求队列上挂请求的时候,发现队列为空的话,会将该队列插入到一个全局的队列中(tq_disk,从名字中,我们也能够看出来是task queue for disk的缩写)。如果队列不为空,那么说明该队列已经加入到tq_disk的全局队列中了,既然该块设备的读写请求队列不为空,那么要利用调度策略,看时候能够和正在该块设备上排队的读写请求和二为一了。这里有一个很恰当的比方:当我们去饭店吃饭的时候,如果你要点的菜如果和师傅正在炒或者准备炒的菜一样的话,炒菜师傅会把两个人点的菜一块炒,特别是学校的食堂,每到吃饭高峰期,人很多,因此,学生们一般都会问服务员,下面要做的是什么菜,如果要节省时间的话,就要师傅下面要炒的菜了。在这里,磁盘的调度原理就是这个样子,貌似很简单哇。其实,有些时候,我们可能并不需要一个请求队列,比如,将来计算机的磁盘全部是SSD了,不再用机械磁盘了,都是电读写的,那么这个IO调度说不定就要被废除了。然后,也就不再需要请求队列了。一个请求到了,然后马上就发送到驱动程序,驱动程序想设备发送命令,读取数据。而在Linux的内核中,已经考虑到这一点了,如果进程进行同步IO的话,就直接启动驱动程序进行IO读写了(请参考代码段一)。
	前面说到,将读写请求挂载在块设备的请求队列时,如果不为空的话,会看能否进行IO调度,不能调度的话,会向块设备请求队列插入一个请求。然后进程的任务就完成了。如果插入请求的时候,发现这个队列上的请求非常多,那么怎么办呢?进程就会主动的启动磁盘IO让这些请求队列赶紧执行(请参考代码片段二)。
	上面设计到的进程启动磁盘IO,都算是主动的。除了主动的时候,还有被动的情况。当进程将请求挂载在块设备请求队列的时候,它是要用其中的数据的。什么时候用呢?该用的时候就用呀,不过用的时候,会检测相关的数据是否被读出了,如果没有读出,那么进程就被阻塞,然后启动磁盘IO(请参考代码片段三)。这一点在Linux2.6的内核中稍微进行了改进,设置了一个request数量阈值,如果大于这个阈值,那么就启动磁盘IO。
	在Linux2.6的内核中,还增加了一个启动IO磁盘的时机,即,读写请求被插入到某个块设备的请求队列时,设置了一个定时器,保证在某个时间点之内,一定要启动磁盘IO。
	启动磁盘IO后,数据怎么读出就跟进程没什么关系了。进程在使用的时候,就会查看它要用的buffer缓冲区是否locked,如果否,就说明已经读好了,如果是,那么就继续启动磁盘然后等待(代码片段三)。
	以上基本上就分析完了。在2.6的内核中,在request和buffer head中又加了一个bio,不过仅仅加了个bio并不影响理解。另外,单单看块设备驱动,并不能够解决读写请求发送到块设备请求队列,然后块设备又怎样的把读写的数据读入到buffer中。当然了,在这一个南大富士通的赵磊大牛写过一个系列的文章“写一个块设备驱动”,一共120页。对我的帮助还是蛮大,当初凌晨看到2点半,然后又加上一个上午,基本上算是一口气看完了。写得不错,希望对块设备驱动有兴趣的同学,可以google一下,看看。
	注:本文还非常naive,错误难免,如果发现,请批评指正。
	附件:参考2.4.31内核
	代码片段一:
	         submit_bh->__make_request
	1000 static int __make_request(request_queue_t * q, int rw,
	1001                   struct buffer_head * bh)
	1002 {
	1003     unsigned int sector, count, sync;
	1004     int max_segments = MAX_SEGMENTS;
	1005     struct request * req, *freereq = NULL;
	1006     int rw_ahead, max_sectors, el_ret;
	1007     struct list_head *head, *insert_here;
	1008     int latency;
	1009     elevator_t *elevator = &q->elevator;
	1010     int should_wake = 0;
	1011
	1012     count = bh->b_size >> 9;
	1013     sector = bh->b_rsector;
	1014     sync = test_and_clear_bit(BH_Sync, &bh->b_state);
	1015
	.。。。。。。。。。。。。。。。。。。。。。。。。。
	1176 out:
	1177     if (freereq)
	1178         blkdev_release_request(freereq);
	1179     if (should_wake)
	1180         get_request_wait_wakeup(q, rw);
	1181     if (sync)
	1182         __generic_unplug_device(q);//进程发起启动磁盘IO
	1183     spin_unlock_irq(&io_request_lock);
	1184     return 0;
	1185 end_io:
	1186     bh->b_end_io(bh, test_bit(BH_Uptodate, &bh->b_state));
	1187     return 0;
	1188 }
	代码片段二:
	__make_request->__get_request_wait
	 643 static struct request *__get_request_wait(request_queue_t *q, int rw)
	 644 {             
	 645     register struct request *rq;
	 646     DECLARE_WAITQUEUE(wait, current);
	 647
	 648     add_wait_queue_exclusive(&q->wait_for_requests, &wait);
	 649
	 650     do {
	 651         set_current_state(TASK_UNINTERRUPTIBLE);
	 652         spin_lock_irq(&io_request_lock);
	 653         if (blk_oversized_queue(q) || q->rq.count == 0) {
	 654             __generic_unplug_device(q);//进程发起启动磁盘IO
	 655             spin_unlock_irq(&io_request_lock);
	 656             schedule();
	 657             spin_lock_irq(&io_request_lock);
	 658         }
	 659         rq = get_request(q, rw);
	 660         spin_unlock_irq(&io_request_lock);
	 661     } while (rq == NULL);
	 662     remove_wait_queue(&q->wait_for_requests, &wait);
	 663     current->state = TASK_RUNNING;
	 664
	 665     return rq;
	 666 }
	 667
	代码片段三:
	 180 /* 
	 181  * Note that the real wait_on_buffer() is an inline function that checks
	 182  * that the buffer is locked before calling this, so that unnecessary disk
	 183  * unplugging does not occur.
	 184  */
	 185 void __wait_on_buffer(struct buffer_head * bh)
	 186 {
	 187     struct task_struct *tsk = current;
	 188     DECLARE_WAITQUEUE(wait, tsk);
	 189
	 190     get_bh(bh);
	 191     add_wait_queue(&bh->b_wait, &wait);
	 192     do {
	 193         set_task_state(tsk, TASK_UNINTERRUPTIBLE);
	 194         if (!buffer_locked(bh))
	 195             break;
	 196         /*
	 197          * We must read tq_disk in TQ_ACTIVE after the
	 198          * add_wait_queue effect is visible to other cpus.
	 199          * We could unplug some line above it wouldn't matter
	 200          * but we can't do that right after add_wait_queue
	 201          * without an smp_mb() in between because spin_unlock
	 202          * has inclusive semantics.
	 203          * Doing it here is the most efficient place so we
	 204          * don't do a suprious unplug if we get a racy
	 205          * wakeup that make buffer_locked to return 0, and
	 206          * doing it here avoids an explicit smp_mb() we
	 207          * rely on the implicit one in set_task_state.
	 208          */
	 209         run_task_queue(&tq_disk);
	 210         schedule();
	 211     } while (buffer_locked(bh));
	 212     tsk->state = TASK_RUNNING;
	 213     remove_wait_queue(&bh->b_wait, &wait);
	 214     put_bh(bh);
	 215 }
	__wait_on_buffer->run_task_queue
	119 static inline void run_task_queue(task_queue *list)
	120 {   
	121     if (TQ_ACTIVE(*list))
	122         __run_task_queue(list);
	123 }
	__wait_on_buffer->run_task_queue->__run_task_queue
	334 void __run_task_queue(task_queue *list)
	335 {
	336     struct list_head head, *next;
	337     unsigned long flags;
	338
	339     spin_lock_irqsave(&tqueue_lock, flags);
	340     list_add(&head, list);
	341     list_del_init(list);
	342     spin_unlock_irqrestore(&tqueue_lock, flags);
	343
	344     next = head.next;
	345     while (next != &head) {
	346         void (*f) (void *);
	347         struct tq_struct *p;
	348         void *data;
	349
	350         p = list_entry(next, struct tq_struct, list);
	351         next = next->next;
	352         f = p->routine;
	353         data = p->data;
	354         wmb();
	355         p->sync = 0;
	356         if (f)
	357             f(data);//这里对于普通的磁盘,就是generic_unplug_device,和代码片段一以及二是一个启动IO操作,
	358         //其实这个函数还是包装了一下,最直接的是q->request_fn
	359     }
	360 }  
	参考:
	1.《Linux内核源码情景分析(下册)》,第八章,设备驱动
	2.《深入Linux内核架构》,第六章,设备驱动程序
	3.《写一个块设备驱动》,赵磊
 
评论
发表评论