一、背景:
嵌入式设备写SD卡的时候,偶尔会出现调用write卡顿,内核linux-3.4.y
二、linux内核io流程
1.应用程序调用write,陷入内核执行vfs_write函数,将数据写入页高速缓存(每个缓存页包含若干个缓冲区)。而在写入之前需要(1)检查页是否在回写,如果正在回写则挂起进程,等待回写标志清空时唤醒进程;(2)检查页buffer是否locked,如果locked则挂起进程等待唤醒
2.内核有一个常驻线程,为每个bdi创建一个线程,定时检查是否需要回写,需要则提交bio,让驱动写入sd卡
3.bio结束时执行回调,将页回写标志清除
三、相关函数分析(记录主要函数,方便跟踪源码)
1.写页高速缓存
(1)重要结构:
const struct file_operations fat_file_operations = {
.llseek = generic_file_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = generic_file_aio_read,
.aio_write = generic_file_aio_write,
.mmap = generic_file_mmap,
.release = fat_file_release,
.unlocked_ioctl = fat_generic_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = fat_generic_compat_ioctl,
#endif
.fsync = fat_file_fsync,
.splice_read = generic_file_splice_read,
};
struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
int (*writepages)(struct address_space *, struct writeback_control *);
int (*set_page_dirty)(struct page *page);
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);
int (*write_begin)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata);
int (*write_end)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct page *page, void *fsdata);
sector_t (*bmap)(struct address_space *, sector_t);
int (*invalidatepage) (struct page *, unsigned long);
int (*releasepage) (struct page *, int);
void (*freepage)(struct page *);
ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
loff_t offset, unsigned long nr_segs);
struct page* (*get_xip_page)(struct address_space *, sector_t,
int);
/* migrate the contents of a page to the specified target */
int (*migratepage) (struct page *, struct page *);
int (*launder_page) (struct page *);
int (*error_remove_page) (struct mapping *mapping, struct page *page);
int (*swap_activate)(struct file *);
int (*swap_deactivate)(struct file *);
};
(2)函数调用流程:
vfs_write-->
do_sync_write-->
f_op->aio_write(generic_file_aio_write)-->(mm/filemap.c)
__generic_file_aio_write-->
generic_file_buffered_write-->
generic_perform_write-->(重要函数)
a_ops->write_begin(block_write_begin)-->(fs/buffer.c)主要耗时在下面两个函数
grab_cache_page_write_begin-->
wait_on_page_writeback-->(申请到页之后,如果改页正在被回写,需要挂起当前进程,等待回写之后的唤醒)(主要耗时)
__block_write_begin
wait_on_buffer-->(为页分配缓冲区,如果申请的缓冲区加了锁,挂起进程,等待解锁后唤醒)(次要耗时)
static inline void wait_on_buffer(struct buffer_head *bh)
{
might_sleep();
if (buffer_locked(bh))
__wait_on_buffer(bh);
}
(3)参考资料:
https://www.cnblogs.com/children/p/3420430.html
https://www.jianshu.com/p/d33ec2707e7f
http://blog.chinaunix.net/uid-14528823-id-4289180.html
https://my.oschina.net/u/2475751/blog/535859
https://blog.csdn.net/wh8_2011/article/details/51787282
https://www.cnblogs.com/honpey/p/4931962.html
https://blog.csdn.net/ctoday/article/details/37966233
2.内核回写线程:
(1)函数分析
linux3.2之后,内核中有一个常驻内存的线程bdi_forker_thread,负责为bdi_object创建bdi_writeback_thread线程,同时检测如果bdi_writeback_thread线程长时间处于空闲状态,便会将其销毁。
bdi_writeback_thread线程在fs/fs-writeback.c中,它在一个while循环中检查是否需要回写,然后执行调度函数等待唤醒。内核每隔固定时间唤醒该线程,这个时间可以查看文件/proc/sys/vm/dirty_writeback_centisecs。
bdi_writeback_thread调用wb_do_writeback函数进行回写
wb_do_writeback处理bdi-work_list需要回写的work,同时也从两个方面检查有没有页高速缓存需要回写,一是有没有脏页存在过长的时间,而是脏页比例是否达到了设置的上限,相应的文件为/proc/sys/vm/dirty_expire_centisecs和/proc/sys/vm/dirty_background_ratio
wb_do_writeback-->
wb_writeback-->
writeback_sb_inodes-->
writeback_single_inode-->
do_writepages-->(mm/page-writeback.c)
mapping->a_ops->writepages-->
fat32注册的mapping->a_ops->writepages即为fat_writepages(fs/fat/inode.c),fat_writepages调用mpage_writepages(fs/mpage.c), mpage_writepages调用__mpage_writepage
【copy】--------------------------------------
_mpage_writepage函数是写文件的核心接口。代码大致流程如下:如果page有buffer_head,则完成磁盘映射,代码只支持所有page都被设为脏页的写,除非没有设为脏页的page放到文件的尾部,即要求page设置脏页的连续性。如果page没有buffer_head,在接口中所有page被设为脏页。如果所有的block都是连续的则直接进入bio请求流程,否则重新回到writepage的映射流程。
用page_has_buffers判断当前page是否有buffer_head(bh),如果有则用page_buffers将当前page转换为buffer_head的bh指针,之后用bh->b_this_page遍历当前page的所有bh,调用buffer_locked(bh)加锁buffer——head,即使出现一个bh没有被映射都会进入confused流程,first_unmapped记录了第一个没有映射的bh,除了要保证所有的bh都被映射,还要保证所有的bh都被置为脏页并且完成了uptodate。如果每个page的block数不为0(通过判断first_unmapped是否非0),则直接进入当前page已经被映射的流程page_is_mapped,否则进入confused流程。
如果当前page没有buffer_head(bh),需要将当前page映射到磁盘上,使用buffer_head变量map_bh封装,做buffer_head和bio之间的转换。
page_is_mapped流程中如果有bio资源并且检测到当前的页面和前面一个页面的磁盘块号不连续(代码对应bio && mpd->last_block_in_bio != blocks[0] – 1,blocks[0]表示第一个磁盘块),则用mpage_bio_submit来提交一个积累bio请求,将之前的连续block写到设备中。否则进入alloc_new流程。
alloc_new流程中,判断bio为空(表示前面刚刚提交了一个bio)则需要用mpage_alloc重新申请一个bio资源,之后用bio_add_page向bio中添加当前page,如果bio中的长度不能容纳下这次添加page的整个长度,则先将添加到bio上的数据提交bio请求mpage_bio_submit,剩下的数据重新进入到alloc_new流程做bio的申请操作。如果一次性将page中的所有数据全部添加到bio上,在page有buffer的情况下要将所有的buffer全部清除脏页位。用set_page_writeback设置该page为写回状态,给page解锁(unlock_page)。当bh的boundary被设置或者当前页面和前面一个页面的磁盘块号不连续,就先提交一个累积连续block的bio。否则说明当前page中的所有block都是连续的,并且与之前的page中block也是连续的,这种情况下不需要提交bio,只更新前面一个页面的磁盘块号mpd->last_block_in_bio为当前page的最后一个block号,之后退出进行下一个page的连续性检查,直到碰到不连续的再做bio提交。
confused流程中会提交bio操作,但是会设置映射错误。
【end】----------------------------------------
总之,__mpage_writepage函数调用mpage_end_io提交bio,驱动将脏页写入sd卡,这个过程中对页进行保护。bio完成后执行回调bio->bi_end_io = mpage_end_io,清除页的writeback标志
(2)参考资料:
https://blog.csdn.net/asmxpl/article/details/21548129
http://blog.sina.com.cn/s/blog_6f5549150102vaoz.html
http://blog.chinaunix.net/uid-7494944-id-3833328.html
https://blog.csdn.net/zhufengtianya/article/details/42145985
四、补充
1.buffer head的lock和unlock目前还没有分析
2.我们调用write函数写页高速缓存的时候,检查页的writeback标志,如果正在回写,就挂起进程等待唤醒,就write函数阻塞了;bio执行结束后调用回调清除页的writeback标志,应用程序被唤醒。
之前说过内核每隔固定时间(/proc/sys/vm/dirty_writeback_centisecs)做一次回写检查,一般当脏页比例达到/proc/sys/vm/dirty_background_ratio就进行回写。我们同时减小这个两个参数的值,发现bio消耗时间的峰值减低了
dirty_writeback_centisecs(s) dirty_background_ratio(%) bio_max_time(ms)
5 10 6000
2 5 4800