关于邮件列表一个问题的解释

问题:在 exit_mm() 中为什么要 atomic_inc(&mm->mm_count) 呢? 并没有对应的dec。

解答:

首先要明白,对于一个可以被释放内存的进程也就是说一个拥有mm_struct的进程来说,它的task_struct中的mm字段和active_mm字段是一样的,这个在fork时,copy_mm中就决定了,接下来解释这个问题:

在exit_mm中:

atomic_inc(&mm->mm_count); //这里递增了引用计数,你说对了,没有dec但是inc了,多了一次inc,那么就要多一次dec

BUG_ON(mm != tsk->active_mm); //保证mm和tsk->active_mm一样

task_lock(tsk);

tsk->mm = NULL; //注意这里将tsk->mm设置为NULL

up_read(&mm->mmap_sem);

enter_lazy_tlb(mm, current);

clear_freeze_flag(tsk);

task_unlock(tsk); //第一次dec

然后注意在do_exit调用exit_mm之后,最终会调用schedule将这个退出进程切出去,因此最终释放task_struct将在新进程切到运行之后再进行,那么看context_switch中:

oldmm = prev->active_mm; //这里的prev就是那个退出进程,其active_mm就是上面的mm,由此可见上面的atomic_inc就是为了这里

...

if (unlikely(!prev->mm)) { //这里if为真,因为上面tsk->mm = NULL;

prev->active_mm = NULL;

rq->prev_mm = oldmm; //rq->prev_mm就是还差一个计数就释放的prev->mm,要不早就释放了,幸亏你说的那个atomic_inc;

}

最后安全切换了新进程以后,一切都可以释放了,看finish_task_switch:

static void finish_task_switch(struct rq *rq, struct task_struct *prev)

{

struct mm_struct *mm = rq->prev_mm; //这里的mm就是上面那个退出的mm

long prev_state;

rq->prev_mm = NULL;

prev_state = prev->state;

...

if (mm)

mmdrop(mm); //引用计数彻底为0,最后被释放

...

其实mm也好,task_struct也好,进程切换时可能会用到,因此都在安全切到新进程之后也就是finish_task_switch中被释放,为了不让mm在exit_mm就被释放,那么只有增加它的引用计数了,不知道这里情景分析你懂了没有。<--解答完毕

上述的解答如果大致理解的话那么就够了,但是如果真的较起真来的话,还真的有点让人发蒙,如果说linux在退出进程的时候不释放task_struct是因为schedule中要用到这个task_struct的话,那么在退出时保留其mm_struct是为什么呢?其实为题还有一大堆,比如为何linux在schedule时还要用到退出进程的task_struct呢?为何要这样呢?因为linux为了效率而没有另立一个调度器,进程切换必须由进程自己进行,也就是说切换前在调度器在切出进程的上下文运行,而切换后在换入进程的上下文运行。那么到底为何不能释放其mm_struct呢?因为当前退出进程所用的正是当前的页目录,也就是当前的页目录绝对不能释放,注意这里的页目录的768项之前的并无所谓,因为现在在内核,只会访问768项以后,这768项以后的每一项和swapper_pg_dir的768项以后一一对应,其实就是直接指向,即便如此,也不能释放pgd,毕竟mmu访问从pgd指向,因为在exit_mm中,该mm_struct的users引用计数已经成为1了,然后最后的mmput中的dec就会使其成为0,那么就要调用mmdrop了,一旦mm的引用计数递减后为0,在后者中会调用__mmdrop释放掉pgd,为了防止这一件事,但是还必须使得users顺利成为0,那么只要防止__mmdrop就可以了,于是就递增了mm的引用计数。

另外一个作用就要仔细研究一下context_switch函数:

static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)

{

struct mm_struct *mm, *oldmm;

prepare_task_switch(rq, prev, next);

trace_sched_switch(rq, prev, next);

mm = next->mm;

oldmm = prev->active_mm;

arch_enter_lazy_cpu_mode();

if (unlikely(!mm)) { //如果下一个进程的mm为空就说明下一个进程是内核线程

next->active_mm = oldmm;

atomic_inc(&oldmm->mm_count); //又递增了一个引用计数

enter_lazy_tlb(oldmm, next); //进入懒惰模式

} else

switch_mm(oldmm, mm, next); //如果下一个不是内核线程,那么就切换mm_struct,也就是切换页表

if (unlikely(!prev->mm)) { //这里就是关键,标记为*

prev->active_mm = NULL;

rq->prev_mm = oldmm;

}

...

}

上面的标记为*的if判断很重要,prev->mm什么时候为假呢?有两种情况,第一就是前一个进程是内核线程,第二就是前一个进程是用户进程但是已经退出,我们这里的情况是第二种情况,只有在这种情况下rq->prev_mm被赋值为退出进程的active_mm,而这个和退出进程的mm一样,记住此时mm的引用计数在exit_mm由于被inc了变成了1,那么安全切换了以后在finish_task_switch中判断rq->prev_mm不为空从而被释放。这个解释很合理,但是和tlb刷新的懒惰模式没有关系,而只是保证了退出进程的pgd在mm切换之前不被释放,那么第二种情况就是要说懒惰模式相关的,如果下一个进程是内核线程,那么进入:

if (unlikely(!mm)) { //如果下一个进程的mm为空就说明下一个进程是内核线程

next->active_mm = oldmm;

atomic_inc(&oldmm->mm_count); //又递增了一个引用计数

enter_lazy_tlb(oldmm, next); //进入懒惰模式

}

可以看到,不但释放不了了mm,还又一次递增了它的引用计数,此时引用计数为2,可是不要急,不但进入了上面的if,连后面的if (unlikely(!prev->mm))也会进入,那么在切换以后还是会在mmdorp中递减一次mm的引用计数,这样这个内核线程保持了引用计数为1的mm,当内核线程被切换出去时,if (unlikely(!prev->mm))为真,因为内核线程没有mm(也可以有),那么在切换以后还是会调用mmdrop,此时mm被释放,pgd被释放,一切安然!

因此可以看出,linux对引用计数的设置是多么巧妙啊,一个mm_struct不但有users计数还有count计数,如果我设计的话,这俩肯定就合二为一了,linux的设计者考虑的真是细致,users为0了count可以不为0,为何呢?users为0说明没有进程使用了,可是count不为0说明调度器还要使用,前面说过,调度器在被调度的进程的上下文运行,调度器借用页目录,那么就要保留页目录到调度器不用为止,另外,不但调度器要借用页目录,内核线程还可以借用之,这是为了提高效率,可以少一次切换开销,内核认为少一次切换开销比释放一个mm_struct带来的收益要更有意义。


 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273512


上一篇:谷歌最新开源前端框架了解一下?前端小白都能看懂的8本书


下一篇:Facebook Newswire删除法国尼斯恐袭视频