对linux-2.6.29内核Freezing filesystems和Tree RCU的新理解

今天是2009年3月24日,2.6.29 内核终于放出来了,盼星星盼月亮就盼这一天呢,等了两个多月,今天终于可以尝试一把了,在以前的Changelog上略微知道了一些新特性,可那只是大致 理论,没有看到最终代码之前永远都不会明白linux内核的巧妙。好几天没有写日志了,今天就写一些对linux新内核新特新的新理解,首先还是看看文件 系统的冻结吧。
是的,linux以前就有文件系统的冻结,但是那里的冻结仅仅意味着锁住文件系统,实现上就是一个简单的信号量完成的,在所谓的freeze的时 候,down信号量,然后thaw的时候up信号量,仅此而已,那时用的信号量是block_device的bd_mount_sem,其实就是阻止了重 新mount这个文件系统,这样当然可以实现保护,但是真的执行起来情况却十分混乱,因为一旦有个执行绪thaw了文件系统,也就是说up了 bd_mount_sem,那么任何在down状态等待的执行绪都有可能从down返回,这完全取决于先后顺序,因为信号量本身有一个等待列表,up的语 义就是从这个列表的最开始取出一个,把信号量给它就可以了。以前的那个freeze实现是很脆弱的,根本没有考虑到争抢bd_mount_sem信号量实 体们的复杂性,比如说,一个进程freeze了一个文件系统,本意是想做快照,另一个进程不知道文件系统已经freeze了,该进程也想freeze这个 文件系统,它的意图是做镜像备份,可是文件系统已经被 freeze了,于是第二个进程就要在bd_mount_sem信号量上睡眠等待,等第一个进程完成再开始运行,这没有什么问题,因为第一个进程 freeze这个文件系统之后根本就不能再改动它了,两个进程对文件系统都是只读的,对于读,没有必要锁,如果能并行运行这两个进程性能一定很高,可是老 的实现并没有这么实现,这是第一个问题;第二个问题就是如果在第一个进程down掉bd_mount_sem以后,第二个进程开始进入down掉 bd_mount_sem之前对该文件系统有一个mount请求的话,那么第一个进程up掉bd_mount_sem之后,信号量将被这个mount进程 夺取(因为等待在信号量上的进程是被顺序唤醒的),于是第二个进程将再次等待,更为严重的是,在第二个进程终于可以down掉bd_mount_sem 时,该文件系统已经被mount到两个挂载点了...新的内核强化了文件系统的freeze机制,它通过增加了一个引用计数来规范了信号量 bd_mount_sem的行为,代码如下:
struct super_block *freeze_bdev(struct block_device *bdev)
{
struct super_block *sb;
int error = 0;
mutex_lock(&bdev->bd_fsfreeze_mutex);
if (bdev->bd_fsfreeze_count > 0) { //如果该文件系统已经freeze了,就不用做后面的复杂操作了,这样增加了响应粒度。
bdev->bd_fsfreeze_count++; //无论如何,引用计数递增
sb = get_super(bdev); //增加sb的计数
mutex_unlock(&bdev->bd_fsfreeze_mutex);
return sb;
}
bdev->bd_fsfreeze_count++; //无论如何,引用计数递增
...
}
bd_fsfreeze_count 这个引用计数的作用就是只有freeze操作递增它,而只有thaw操作递减它,原本就是一个信号量,现在有了一个引用计数来约束程序行为,这样程序的行 为的不确定性就降低了,以往,只要thaw操作就意味着要up信号量了,而只要up信号量,任意争夺者就有希望插入进来就搅局,现在thaw不一定up信 号量了,thaw操作递减那个引用计数,只有在它为0的时候才up信号量,也就是说freeze的行为有了自己的保护机制,就是那个简单的引用计数,该引 用计数保护着文件一直被freeze,相当于在信号量保护机制的上层又有了一层新的更高层的更具有个性的引用计数保护机制,抽象层次更高了,linux新 内核为用户提供了一个ioctl接口来freeze或者thaw文件系统,用户可以很方便的进行操作。
另外新内核将很多void返回类型的函数改为了int,这样就可以返回一些错误码了,这是很显然的,因为系统越设计越复杂,越复杂越容易有问题,越有问题 就越应该能及时捕捉到问题以提高系统健壮性。void类型没有包含任何信息,而int类型却包含了4个字节的信息,因此信息量增大,这个增加的信息量就是 为了容纳错误的,因为越来越可能发生的错误必须有个容器来容纳。
下面我觉得真的有必要再记录一下我对Tree RCU的理解,在拥有大量cpu的机器上,那么多cpu争抢一个锁的局面是非常令人讨厌的,没有任何次序性可言,十分恶心,这就好比在郑州挤公交车一样令 人烦,随着机器上的cpu越来越多,多核心中竞争和锁的问题日益浮现,没有规矩不成方圆,游戏参与者越多,就越应该细化规则,规则在某种意义上就是秩序, 两三个人挤公交车的局面没有什么,也没有必要定规矩,比如人口稀少的国家可能根本就没有必要知道什么是排队(虽然人家即使人少也排队),其实参与者很少的 情况下,竞争状态根本就是很少见的一件事,如果为稀有事件定规矩,那会造成资源浪费的。现在情况不同了,cpu越来越多,规矩必须定了,其实从早先几个内 核版本规矩就开始定了,比如2.6.25内核加入了Ticket spinlock,另外新的信号量也用顺序队列实现等等,鉴于此,rcu锁也不能落后,新的rcu锁采取的是分级策略,实际上rcu中根本就不存在显式的 锁,锁仅仅是一个隐式的概念,就是说为了判断是否所有的cpu都经过了一个quiescent state,那么就必须维持一个全局的结构变量,这个变量每个时刻只能由一个cpu进行操作,于是它就需要一个锁进行保护,这就是这个隐式的锁,rcu在 别的地方执行它本职工作时并没于锁。
分级的rcu锁保证了每个时期都只有固定数量的cpu在其rcu_node内部争抢锁,以前的系统在一个cpu完成一个cpu_quite之前必须得到这 个全局的结构的锁,现在不用了,不再使用全局的结构了,全局的结构成了分层分级的结构组合了,从最低层的组开始竞争,几个组同时,每个组内有若干的cpu 竞争锁,获胜的cpu上升到上一个级别继续,直到最后有一个cpu获得最后的胜利,它才可以得到最后的锁从而获得修改顶层数据的权力。
static void cpu_quiet_msk(unsigned long mask, struct rcu_state *rsp, struct rcu_node *rnp, unsigned long flags)
{
for (;;) { //这个循环就是遍历分级rcu模型,然后一步一步地获得锁
if (!(rnp->qsmask & mask)) {
spin_unlock_irqrestore(&rnp->lock, flags);
return;
}
rnp->qsmask &= ~mask; //将自己从掩码中清除,表示自己已经过了一个quiescent
if (rnp->qsmask != 0) { //如果这个级别还有别的cpu没有经过quiescent,那么返回,我们已经清楚了自己的了,最后的指示一个grace period完结的任务交给了最后一个完成quiescent的cpu。这里可以看出,在清除rnp->qsmask相应位的时候,当前cpu只是 占有了当前rcu_node的一个锁,每个rcu_node中有有限的cpu数量,典型的,可能是2,3,64。
spin_unlock_irqrestore(&rnp->lock, flags);
return;
}
mask = rnp->grpmask;
if (rnp->parent == NULL) { //已经到最顶层了,说明全部cpu都已经通过了quiescent,注意,此时当前cpu已经夺得了顶层node的锁了
break;
}
spin_unlock_irqrestore(&rnp->lock, flags);
rnp = rnp->parent;
spin_lock_irqsave(&rnp->lock, flags);
}
rsp->completed = rsp->gpnum;
rcu_process_gp_end(rsp, rsp->rda[smp_processor_id()]);
rcu_start_gp(rsp, flags);
}
这里面有两个技巧,第一就是锁的争抢限制在了局部,这个局部就是树形分层rcu的每个层次的每个节点,其本质就是将cpu的掩码放在了每个局部而不是全局的结构里面,掩码放到了局部,从局部信息怎么会得知全局情况呢,这就是第二个技巧,就是将一个grace
period 完结的设置交给最后一个cpu,完成quiescent的cpu一个个的在局部清楚了自己的掩码位,只要调用了这个函数,只要一个cpu可以顺利到达一个 级,那么可以肯定该级别的下面的级别的该cpu的掩码位已经被清除了,如果有机会往上,那么上面的级别该cpu的掩码位也被清除,于是向不向上走和掩码位 的清除没有关系,于是该node中只要有cpu的掩码位还在,就不要往上走了,因为没有意义了,以后再有机会往上走不会妨碍该cpu清除上面 node的掩码位的,而且分级rcu机制要求最后一个cpu设置grace period完成信息,如果得知清除了自己后掩码位仍不为但是仍往上走的话也根本完不成任务,因为该cpu不是最后一个,另外还会豪无意义争抢上层的锁。
最后说一下新的信号量的实现,用到了list_head而抛弃了原来的睡眠队列,原来是用体系相关的代码实现信号量的,在2.6.26后就改成了c实现, 在汇编实现的代码中,在__up操作时会唤醒一个睡眠队列,如果将队列进程全部唤醒,那么就是谁优先级高谁沾光了,那样的实现会使低优先级的请求信号量的 进程过度饥饿,如果加上一个deadline的话又会使实现变得复杂,幸好睡眠队列中有一个选项是WQ_FLAG_EXCLUSIVE,就是说独占方式加 入,这样的话唤醒该睡眠队列时就只会唤醒队列的第一个进程,虽然很顺序,但是还是显得繁杂,2.6.26以后的内核将信号量数据结构彻底更改,既然独占式 加入队列就意味着每次只能唤醒一个进程,那好像就不是队列了,于是就抛弃了结构体中的睡眠队列,而加入了一个链表,代表等待的进程,按照每次只唤醒一个的 语义,在__up操作时从链表头取出一个就可以了,在down的时候直接加入链表,这样很简单。也许2.6.26之前的信号量结构中的sleepers字 段的设置和__down中的if (!atomic_add_negative(sleepers - 1, &sem->count))操作可能不得不使你叫绝,可是抱歉,内核是效率和公平优先的,不是三脚猫表演花拳绣腿的地方。

你可能感兴趣的:(System)