在linux操作系统中,写操作是异步的,即写操作返回的时候数据并没有真正写到磁盘上,
而是先写到了系统cache里,随后由pdflush内核线程将系统中的脏页写到磁盘上,在下面几种情况下,
系统会唤醒pdflush回写脏页:
1 定时方式:
定时机制定时唤醒pdflush内核线程,周期为/proc/sys/vm/dirty_writeback_centisecs ,单位
是(1/100)秒,每次周期性唤醒的pdflush线程并不是回写所有的脏页,而是只回写变脏时间超过
/proc/sys/vm/dirty_expire_centisecs(单位也是1/100秒)。
注意:变脏的时间是以文件的inode节点变脏的时间为基准的,也就是说如果某个inode节点是10秒前变脏的,
pdflush就认为这个inode对应的所有脏页的变脏时间都是10秒前,即使可能部分页面真正变脏的时间不到10秒,
细节可以查看内核函数wb_kupdate()。
2 内存不足的时候:
这时并不将所有的dirty页写到磁盘,而是每次写大概1024个页面,直到空闲页面满足需求为止。
3 写操作时发现脏页超过一定比例:
当脏页占系统内存的比例超过/proc/sys/vm/dirty_background_ratio 的时候,write系统调用会唤醒pdflush回写dirty page,直到脏页比例低于/proc/sys/vm/dirty_background_ratio,但write系统调用不会被阻塞,立即返回。当脏页占系统内存的比例超过/proc/sys/vm/dirty_ratio的时候, write系
统调用会被被阻塞,主动回写dirty page,直到脏页比例低于/proc/sys/vm/dirty_ratio,这一点在2.4内核中是没有的。
4 用户调用sync系统调用:
这是系统会唤醒pdflush直到所有的脏页都已经写到磁盘为止。
Now, pdflush is replaced by flushing.
using flusher threads per backing device info (BDI), as a replacement for pdflush threads. Unlike pdflush threads, per-BDI flusher threads focus on a single disk spindle. With per-BDI flushing, when the request_queue is congested, blocking happens on request allocation, avoiding request starvation and providing better fairness. ()
线程池采用线程预创建技术,在程序初始化时,创建最小数量的线程,放入空闲队列中(即线程池)。这些线程工作任务函数为空,处于阻状态,占用较小的CPU和内存。当用户有工作任务时,从缓冲池选择一个空闲线程,把工作任务赋给线程的工作任务函数,让线程执行工作任务。当线程池执行完毕后线程不退出,继续保持在池中等待下一次的任务。当线程池空闲线程小于最小空闲线程数时,缓冲池自动创建一定数量的新线程,用于处理更多的任务。如果空闲线程数大于最大空闲线程数时,线程池自动销毁一部分线程。 基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小。线程池适合单位时间内处理作协频繁而且任务处理时间短、对实时性要求较高、必须经常面对高突发性事件的情况。 pdflush线程是从块缓存写回脏数据到设备的工作线程。在理想情况下,我们期望每个活动硬盘一个线程,但硬盘布局很难达到这一水平。代替的方法是,在各个地方阻止多于一个pdflush线程执行对一个文件系统的回写操作。pdflush线程设置current->flags=PF_FLUSHER来帮助解决这个问题。 pdflush线程池的管理方法是:MIN_PDFLUSH_THREADS和MAX_PDFLUSH_THREADS分别表示pdflush线程的最小实例数和最大实例数。如果1s没有空闲的pdflush线程实例,就创建一个新的。如果最近最少睡眠的pdflush线程实例已超过1s,关掉这个线程。结构pdflush_work用来传递需要做的工作到pdflush线程,也在pdflush线程实例间传递信息,被pdflush_lock锁保护。struct pdflush_work{ struct task_struct *who ; // the thread void (*fn)(unsigned long); //回调函数 unsigned long arg0; //回调函数参数 struct list_head list ; //当空闲时,pdflush_work结构实例在pdflush_list上 unsigned long when_i_went_to_sleep;};函数pdflush_init创建了线程池,其调用层次如下:static int __init pdflush_init(void){ int i; for(i=0;i
start_one_pdflush_thread();
return 0;
}
static void start_one_pdflush_thread(void)
{
kthread_run(pdflush,NULL,"pdflush"); //运行线程pdflush
}
static int pdflush(void *dumy)
{
struct pdflush_work my_work;
set_user_nice(current,0);
return __pdflush(&my_work);
}
当my_work->fn为空,即空闲时,my_work在链表pdflush_list上,线程池有一个空闲线程,线程pdflush一直在循环运行。当通过函数pdflush_operation设置了my_work->fn并从pdflush_list上取下my_work来执行时,链表pdflush_list为空,即线程池为空,如果大于1秒没有空闲线程时,就会创建一个新的线程。
函数pdflush_operation尝试唤醒一个pdflush线程,并让它做一些指定的工作,函数pdflush_opertaion如果得到一个线程实例,就把工作传给这个线程。
int pdflush_operation(void (*fn)(unsigned long),unsigned long arg0)
{
unsigned long flags;
int ret =0;
if(fn==NULL) BUG();
spin_lock_irqsave(&spdflush_lock,flags) ;
if(list_empty(&pdflush_list)){ //链表为空表示线程池没有空闲线程
spin_unlock_irqrestore(&pdflush_lock,flags);
ret = -1;
}else{
struct pdflush_work *pdf;
pdf=list_entry(pdflush_list.next,struct pdflush_work,list);
//得到pdflush_list.next对应容器结构pdflush_work
list_del_init(&pdf->list);
//pdf链接在pdflush_list中,因而是从pdflush_list中删除条目pdf->list,并初始化成链表pdf-list
if(list_empty(&pdflush_list)) last_empty_jifs=jiffies;
pdf->fn = fn; //赋上工作函数
pdf->arg0 = arg0;
wake_up_process(pdf->who); //唤醒进程
spin_unlock_irqrestore(&pdflush_lock,flags);
}
return ret;
}
writeback机制模型
在Linux-3.2新内核中,page cache和buffer cache的刷新机制发生了改变。放弃了原有的pdflush机制,改成了bdi_writeback机制。这种变化主要解决原有pdflush机制存在的一个问题:在多磁盘的系统中,pdflush管理了所有磁盘的page/buffer cache,从而导致一定程度的IO性能瓶颈。bdi_writeback机制为每个磁盘都创建一个线程,专门负责这个磁盘的page cache或者buffer cache的数据刷新工作,从而实现了每个磁盘的数据刷新程序在线程级的分离,这种处理可以提高IO性能。
writeback机制的基本原理可以描述如下:
在Linux内核中有一个常驻内存的线程bdi_forker_thread,该线程负责为bdi_object创建writeback线程,同时检测如果writeback线程长时间处于空闲状态,bdi_forker_thread线程便会将其进行销毁。bdi_forker_thread在系统中只有一个,其会被定时唤醒,检查全局链表bdi_list队列中是否存在dirty的数据需要刷新到磁盘。如果存在dirty数据并且对应bdi的writeback线程还没有被创建,bdi_forker_thread会为该bdi创建一个writeback的线程进行写回操作。
writeback线程被创建之后会处理等待的work。writeback线程拥有一个定时器会周期性唤醒这个线程处理相应的work。当用户(page cache/buffer cache)有需要处理的inode时,将inode挂载到writeback-> b_dirty链表中,然后唤醒writeback线程去处理相应的dirty_page。inode链表就是writeback线程需要处理的数据;work链表就是控制处理过程中的一些策略,不同的策略可以定义成不同的任务。
通过上述模型,对于块设备或者文件系统而言,实现dirty page的后台刷新主要做如下几个方面的工作:
1,将自己的bdi注册到系统的bdi链表中,通过bdi_forker_thread实现对bdi对象的管理,从而可以实现writeback线程的动态创建、销毁。每个块设备和文件系统都有自己的bdi对象。Ext3文件系统在创建的时候会生成superblock对象,系统会将底层块设备的backing_device关系到这个superblock对象上(在set_bdev_super函数中完成)。如果是块设备的话,在add_disk的时候直接从request_queue中得到bdi对象,然后对其进行初始化。注册bdi对象使用bdi_register_dev函数,对于ext3之类的文件系统不需要重新注册bdi对象,因为其本身就采用了底层块设备的bdi对象。
2,将需要刷新的inode节点挂载到bdi对象所属的writeback->b_dirty上,如果有特殊的work需要writeback线程完成,那么提交一个work即可;如果是通常的周期性刷新,writeback线程会自动创建相应的work。
3,操作writeback的唤醒定时器延迟唤醒writeback线程,或者直接唤醒线程,从而使得inode中radix tree上的dirty page刷新到磁盘。
bdi对象的注册
每个块设备在创建的时候会注册bdi对象(参见add_disk函数),这是Linux-3.2内核不同的地方。文件系统在mount的时候会创建superblock对象,并且通过底层块设备的request queue获取bdi对象(mount_bdev->sget->set_bdev_super)。所以,像ext3之类的文件系统都不需要重新注册bdi对象。当然,如果文件系统重新创建了一个bdi对象,那么还需要调用bdi_register_dev函数注册bdi对象。
小结
本文对linux-3.2中的writeback机制模型进行了阐述,后面还会对writeback机制中的关键函数进行分析说明。该机制是对老系统(Linux-2.6.23等)中pdflush机制的替代,其最重要的变化是每个块设备都分配了writeback线程,使得回写的IO流在各个磁盘之间独立,从而从机制上提高了IO的吞吐量
阅读(1032) | 评论(0) | 转发(0) |