文件读写,从请求到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.《写一个块设备驱动》,赵磊

评论

此博客中的热门博文

Linux/ARM Page Table Entry 属性设置分析

提交了30次才AC ---【附】POJ 2488解题报告

笔记