内核使用pdflush线程刷新脏页到磁盘,pdflush线程是内核在初始化的时候创建的,不过,pdflush在内核中的实现并不是一直是这样的,它经历了几次变化,我们来回顾一下:
1.第一次,linux-2.6.32之前的版本实现:
以linux-2.6.30版本的内核为例,pdflush线程的初始化在pdflush.c文件中,完整路径是linux-stable/mm/pdflush.c。我们截取它的实现:

可见,在2.6.32之前的版本中,pdflush线程是在内核启动阶段,执行init构造函数的时候调用的,并且线程数目由nr_pdflush_threads变量控制,它的函数体如下:

当需要回写脏页时,唤醒pdflush线程的操作在这里执行,唤醒过程中,丢一个处理任务background_writeout进去。

处理任务将会挂接到一个任务链表中,择机由__pdflush 执行。

__pdflush在执行回写任务:

2.第二个版本,linux-2.6.32之后的版本实现:
以v2.6.34为例,它的初始化过程变成了:




2.第三个版本,linux-4.9.99的版本实现:
加入调试打印信息:

[ 30.756556] set_worker_desc line 4586, desc = flush-8:16.
[ 30.756558] wb_workfn line 2073, comm kworker/u8:2.
[ 30.756562] CPU: 2 PID: 137 Comm: kworker/u8:2 Not tainted 5.4.129+ #28
[ 30.756563] Hardware name: Dell Inc. Vostro 3268/0TJYKK, BIOS 1.11.1 12/11/2018
[ 30.756571] Workqueue: writeback wb_workfn (flush-8:16)
[ 30.756573] Call Trace:
[ 30.756581] dump_stack+0x6d/0x8b
[ 30.756585] wb_workfn+0x90/0x4e0
[ 30.756589] ? __switch_to_asm+0x40/0x70
[ 30.756591] ? __switch_to_asm+0x34/0x70
[ 30.756593] ? __switch_to_asm+0x40/0x70
[ 30.756595] ? __switch_to_asm+0x34/0x70
[ 30.756597] ? __switch_to_asm+0x40/0x70
[ 30.756599] ? __switch_to_asm+0x34/0x70
[ 30.756601] ? __switch_to_asm+0x40/0x70
[ 30.756603] ? __switch_to_asm+0x34/0x70
[ 30.756605] ? __switch_to_asm+0x40/0x70
[ 30.756607] ? __switch_to_asm+0x34/0x70
[ 30.756611] ? __switch_to+0x85/0x490
[ 30.756613] ? __switch_to_asm+0x40/0x70
[ 30.756615] ? __switch_to_asm+0x34/0x70
[ 30.756620] process_one_work+0x20f/0x400
[ 30.756624] ? inode_wait_for_writeback+0x40/0x40
[ 30.756628] ? process_one_work+0x20f/0x400
[ 30.756631] worker_thread+0x34/0x410
[ 30.756635] kthread+0x121/0x140
[ 30.756638] ? process_one_work+0x400/0x400
[ 30.756641] ? kthread_park+0x90/0x90
[ 30.756643] ret_from_fork+0x35/0x40

可以看到,最新的内核里面,pdflush的功能被工作队列取代,不再有独立的pdflush线程了。
会刷的关键数据结构

和flush线程相关的线程还有一个kswapd,它在当前的Linux中都有存在:
比如 Tina:
root@(none):/# ps
PID USER VSZ STAT COMMAND
1 root 912 S /sbin/init
2 root 0 SW [kthreadd]
3 root 0 SW [kworker/0:0]
4 root 0 SW< [kworker/0:0H]
5 root 0 SW [kworker/u2:0]
6 root 0 SW [ksoftirqd/0]
7 root 0 SW [rcu_preempt]
8 root 0 SW [rcu_sched]
9 root 0 SW [rcu_bh]
10 root 0 SW< [lru-add-drain]
11 root 0 SW [kdevtmpfs]
12 root 0 SW [kworker/u2:1]
256 root 0 SW [oom_reaper]
257 root 0 SW< [writeback]
259 root 0 SW< [crypto]
260 root 0 SW< [bioset]
262 root 0 SW< [kblockd]
298 root 0 SW [kworker/0:1]
302 root 0 SW [irq/329-axp2101]
349 root 0 SW [sys_user]
358 root 0 SW< [cfg80211]
364 root 0 SW< [watchdogd]
456 root 0 SW [spi0]
465 root 0 SW [kswapd0]
523 root 0 SW< [SquashFS read w]
543 root 0 SW [vsync proc 0]
568 root 0 SW< [bioset]
573 root 0 SW< [bioset]
578 root 0 SW< [bioset]
583 root 0 SW< [bioset]
588 root 0 SW< [bioset]
593 root 0 SW< [bioset]
598 root 0 SW< [bioset]
603 root 0 SW< [bioset]
643 root 0 SW [kworker/u2:2]
644 root 0 SW [kworker/u2:3]
645 root 0 SW [kworker/u2:4]
649 root 0 SW [cfinteractive]
655 root 0 SW [kworker/0:2]
656 root 0 SW [kworker/0:3]
657 root 0 SW [kworker/0:4]
661 root 0 SW [irq/304-sunxi-m]
664 root 0 SW [irq/165-sdc0 cd]
705 root 0 SW< [bioset]
706 root 0 SW [mmcqd/0]
710 root 0 SW< [kworker/0:1H]
734 root 0 SWN [jffs2_gcd_mtd4]
763 root 0 SWN [jffs2_gcd_mtd7]
872 root 1048 S adbd
875 root 0 SW [file-storage]
882 root 676 S /sbin/swupdate-progress -w
889 root 916 S -/bin/sh
907 root 912 R ps
root@(none):/#
以及ubuntu:
czl@czl-VirtualBox:~$ ps -ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:02 /sbin/init splash
2 ? S 0:00 [kthreadd]
3 ? I< 0:00 [rcu_gp]
4 ? I< 0:00 [rcu_par_gp]
6 ? I< 0:00 [kworker/0:0H-kb]
7 ? I 0:00 [kworker/0:1-eve]
9 ? I< 0:00 [mm_percpu_wq]
10 ? S 0:00 [ksoftirqd/0]
11 ? I 0:00 [rcu_sched]
12 ? S 0:00 [migration/0]
13 ? S 0:00 [idle_inject/0]
14 ? S 0:00 [cpuhp/0]
15 ? S 0:00 [kdevtmpfs]
16 ? I< 0:00 [netns]
17 ? S 0:00 [rcu_tasks_kthre]
18 ? S 0:00 [kauditd]
19 ? S 0:00 [khungtaskd]
20 ? S 0:00 [oom_reaper]
21 ? I< 0:00 [writeback]
22 ? S 0:00 [kcompactd0]
23 ? SN 0:00 [ksmd]
24 ? SN 0:00 [khugepaged]
70 ? I< 0:00 [kintegrityd]
71 ? I< 0:00 [kblockd]
72 ? I< 0:00 [blkcg_punt_bio]
73 ? I< 0:00 [tpm_dev_wq]
74 ? I< 0:00 [ata_sff]
75 ? I< 0:00 [md]
76 ? I< 0:00 [edac-poller]
77 ? I< 0:00 [devfreq_wq]
78 ? S 0:00 [watchdogd]
79 ? I 0:00 [kworker/u2:1-ev]
81 ? S 0:02 [kswapd0]
应用场景
以向磁盘文件按写内容为例,我们调用write这个函数,往这个句柄里面写数据的时候,那么在内核空间里面它首先会通过打开的句柄fd,在内核里面找到file的这样一个数据结构,在file数据结构里面它找到它的page cache,如果没有找到page cache,他就会新创建一个page cache,你要把这些page cache加到内核管理page cache这种基数树里面
它会根据buffer的大小,去分配buffer_head,通常是4个buffer_head,指向一个page。
通过文件系统的get_block(每个文件系统有自己的封装实现,比如ext2_get_block/fat_get_block),这个api为page cache去查找在磁盘中对应的这个block的号。通常文件系统会管理这个数据它要存放在磁盘哪个block的号里面,接下来将数据copy到page cache中,这里直接调用copy_from_user拷贝就可以了。

接下来,把buffer_head和page cache都要设置成dirty,即这个page cache它是脏的页面,而且要把这个文件所对应的inode也要添加到系统里面的一个脏的队列里面。这一步完成后,从用户空间的角度看,这个write函数已经返回了,但是我们要注意到,这个步骤完成之后,其实数据并没有真正写入到eMMC里面,而是写入到系统的缓存或者页缓存,叫做page cache里面。
第二步,linux内核里面有一个叫做flush的内核线程,这个内核线程的主要工作是定期把一些脏的page cache写回到磁盘里面,这是flush内核线程的主要工作。
这个flush线程会从inode的dirty队列里面去处理一个inode,即脏的inode,它会调用文件系统的write pages这种接口去处理这个page cache。
唤醒flush线程的地方:

用流程图表示如下:

for non-direct read, we can find it also operate through PAGE CACHE,generic_file_read_iter

所以,实际上写操作并不等数据落盘就会返回,真正的数据落盘要等到PDFLUSH回刷的时候。
page cache的建立路径是:
page_cache_alloc()->__page_cache_alloc() 最终调用到__add_to_page_cache_locked


理论分析请参考这篇文章:Linux中的Page Cache [二] - 知乎

本文介绍了Linux内核中pdflush线程的三次版本变化,从早期的初始化方式,到2.6.32之后的版本实现,再到4.9.99版本的更新。pdflush线程负责将脏页刷新到磁盘,其功能在后续内核中逐渐被工作队列取代。同时,文章提到了与pdflush相关的kswapd线程及其作用,以及数据写入过程涉及的page cache机制。

2491





