linux内存定期换出

kswapd进程就是专司定期将页面换出的守护神,它使用的是内核的地址空间

kswapd的初始化
static int __init kswapd_init(void)
{
    printk("Starting kswapd\n");
    swap_setup(); //根据物理内存大小设置设定全局量page_cluster,磁盘读道是个费时操作,每次读一个页面过于浪费,每次多读几个,这个量就要根据实际物理内存大小来确定
    kernel_thread(kswapd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL); //创建kswapd守护进程
    return 0;
}

int kswapd(void * unused){
    struct task_sturct *tsk = current;
    tsk->session =1;
    tsk->pgrp = 1;
    strcpy(tsk->comm, "kswapd");
    sigfillset(&tsk->blocked);
    kswapd_task = tsk;
   
    tsk->flags |= PF_MEMALLOC; //这是kswapd进程初始化,从该进程PCB可以看到他直接指向了系统PCB,这样他的赖以生存的页面表项表和页面表便是系统的
   
    for(;;){
        static int recalc = 0;
       
        if(inactive_shortage() || free_shortage()){ //首先判断内存页面是否短缺,包括2部分,总体量是否短缺和每个具体的管理区内部页面是否短缺
            int wait = 0;
            if(waitqueue_active(&kswapd_done))
                wait = 1; //发现内存短缺,执行do_try_to_free_pages开始执行这个方法,换出部分页面
            do_try_to_free_pages(GFP_KSWAPD, wait);
        }
       
        refill_inactive_scan(6, 0);
       
        if(time_after(jiffies, recalc + HZ)){
            recalc = jiffies;
            recalculate_vm_stats();
        }
    }
}
//从代码可以看出,该进程没次循环做2件事情,第一,检查内存页面是否短缺(总体量是否短缺,各个管理区内部是否短缺)发生短缺,则调用do_try_to_free_page,从物理页面找出可以换出的页面,将它们从活跃状态,转换成不活跃状态。(只是状态的转换,自然页面映射是要断开),第2部分,没有加if判断,每次都要执行,就是将脏页面写入交换设备,是他们变成不活跃干净页面,等待回收。

static int do_try_to_free_pages(unsigned int gfp_mask, int user){
    int ret = 0;
    if(free_shortage() || nr_inactive_dirty_pages > nr_free_pages() + nr_inactive_clean_pages())
        nr_inactive_clean_pages();//如果此方法是从kswapd进入的话,自然这个判断体是肯定会进入的
    ret+=page_launder(gfp_mask, user); //洗净脏页面,增加可以回收的干净页面,返回的值被清洗的页面数量,此时被清洗的页面加入到了干净页面队列中
   
    if(free_shortage() || inactive_shortage()){
    //经过上面page_launder清洗脏页面后,再次去检查页面是否存在短缺情况,若存在短缺就要动现役页面来满足增长的内存需求了
        shrink_dcache_memory(6, gfp_mask);
        shrink_icache_memory(6, gfp_mask);                         //文件在载入后还有系统都会生成一些数据结构,这些结构不会在使用完毕后不会立即释放,而是继续保持,以防再次用到,这个时候缺页面就该回收了
        ret += refill_inactive(gfp_mask, user); //见下面
    } else {
        kmem_cache_reap(gfp_mask);//回收系统管理机制所造成的不使用但没有释放的内存
        ret = 1;
    }
    return ret;
}
//一开始并不去动现役页面,既不断开页面映射,首先做的是清洗脏页面,使其变成干净的可回收页面.再清洗过脏页面后,再去检测可回收页面(空闲页面和干净页面)是否解决了页面短缺的问题,不短缺就解决问题就做任何动作了,如果还不够用就去动现役页面.真正的回收来源4个方面,我们看到判断内部执行了3个函数shrink_dcache_memory,shrink_icache_memory,refill_inactive,在文件系统中打开文件过程会分配一些数据结构用于描述这个文件,它们在文件关闭时候不立即释放,因此这个时候通过shrink_dcache_memory,shrink_icache_memory适当的加以回收,而判断另一边,当我们不缺页面时候执行kmem_cache_reap是回收slap机制造成内存"浪费"


//内存清洗工,主要完成工作:将脏页面内容写入换出设备,使其变成干净页面,并加入到干净页面队列
#define MAX_LAUNDER (4*(1 << page_cluster))
int page_launder(int gfp_mask, int sync){
    int launder_loop,//用于控制扫描脏队列次数
    maxscan,
    cleaned_pages, //用于统计被清洗的页面数量
    maxlaunder;
   
    int can_get_io_locks;
    struct list_head * page_lru;
    struct page * page;
   
    can_get_io_locks = gfp_mask & __GFP_IO;
   
    launder_loop = 0; //还没开始扫描,扫描次数为0
    maxlaunder = 0;
    cleaned_pages = 0; //还没开始洗页面,清洗的页面数为0
   
    dirty_page_rescan:            //开始对脏队列扫描
        spin_lock(&pagemap_lru_lock);
        maxscan = nr_inactive_dirty_pages; //由于在扫描过程中会将页面的位置移动,因此必须记下脏页面总量,按照个数去扫描,避免死循环
       
        while((page_lru = inactive_dirty_list.prev) != &inactive_dirty.list && maxscan-- > 0){
            page = list_entry(page_lru, struct_page, lru); //遍历脏页面队列
           
            if(!PageInactiveDirty(page)){ //检查页面是否真的是脏页面,进入判断内部,表示该页面不是脏页面,错误被放入脏页面队列内部
                printk("VM: page_launder, wrong page on list.\n");
                list_del(page_lru); //删除这个节点
                nr_inactive_dirty_pages--; //相应脏页面长度要减一
                page->zone->inactive_dirty_pages--;
                continue; //发现页面被错误放入脏队列中,进行相应的处理,包括从脏队列中移除,相应的队列长度计数减一
            }
           
            if(PageTestandClearReferenced(page) || page->age > 0 || (!page->buffers && page_count(page) > 1) || page_ramdisk(page)){//由于各种原因,不可以清洗的页面,原因见下面
                del_page_from_inactive_dirty_list(page);
                add_page_to_active_list(page);
                continue;
            }
           
            if(TryLockPage(page)){ //进入判断体内部,表示这个页面已经被进程锁住。这个时候就应该把它移到不活跃脏队列尾部,如果没进入,此时kswapd进程就会将它锁住
                list_del(page_lru);
                list_add(page_lru, &inactive_dirty_list);
                continue;
            }
           
            if(PageDirty(page)) {//页面仍然是脏的话
                int (*writepage)(struct page *) = page->mapping->a_ops->writepage; //获取这个页面写函数指针
                int result;
               
                if(!writepage)
                    goto page_active; //如果没有找到写函数,就只好将这个页面送回活跃页面队列
                if(!launder_loop){
                    list_del(page_lru);
                    list_add(page_lru, &inactive_dirty_list);
                    UnlockPage(page);
                    continue;
                }
               
                ClearPageDirty(page); //写之前,将干净标志清0。由于写文件是件费时的操作,因此内核很有可能会再次进入launder,写之前清0,可以防止内核2次或者多次清洗同一个页面。这也是为什么到这里还要在上面加一个判断页面是否干净的判断的原因.
                page_cache_get(page);
                spin_unlock(&pagemap_lru_lock);
               
                result = writepage(page); //根据address_page提供的函数将页面写出,当返回值是1时,表示页面清洗失败
                page_cache_release(page);
               
                spin_lock(&pagemap_lru_lock);
                if(result != 1)
                    continue;
                set_page_dirty(page); //程序运行到这里表示页面清洗过程中发生意外失败,此时要把页面脏标志恢复成1,并且将页面归还给活跃队列
                goto page_active;
            }
           
            if(page -> buffers){ //如果页面不再是脏的,用于文件缓冲。文件缓冲
                int wait, clearedbuf;
                int freed_page = 0;
               
                del_page_from_inactive_dirty_list(page); //先将该页面脱离不活跃脏页面队列
                page_cache_get(page);
                spin_unlock(&pagemap_lru_lock);
               
                if(launder_loop && maxlaunder == 0 && sync)
                    wait = 2;
                else if(launder_loop && maxlaunder-- > 0)
                    wait = 1;
                else
                    wait = 0;
                clearedbuf = try_to_free_buffers(page, wait); //试图释放这种页面
               
                spin_lock(&pagemap_lru_lock);
               
                if(!clearedbuf){ //释放失败则根据返回值进行相应的操作
                    add_page_to_inactive_dirty_list(page);
                }else if(!page->mapping) {
                    atomic_dec(&buffermem_pages);
                    freed_page = 1;
                    cleaned_pages++;
                } else if(page_count(page) > 2){
                    add_page_to_active_list(page);
                } else {
                    add_page_to_inactive_clean_list(page);
                    cleaned_pages++;
                }
                UnlockPage(page);
                page_cache_release(page);
                if(freed_page && !free_shortpage()) //检查释放后,页面是否满足了需求,满足则退出
                    break;
                continue;
            } else if(page->mapping && !PageDirty(page)){
                del_page_from_inactive_dirty_list(page);
                add_page_to_inactive_clean_list(page);
                UnlockPage(page);
                cleaned_pages++;
            } else {
            page_active:
                del_page_from_inactive_dirty_list(page); //顺讯执行到这里表示无法处理的页面,归还给活跃队列
                add_page_to_active_list(page);
                UnlockPage(page);
            }
        }
        spin_unlock(&pagemap_lru_lock);
        if(can_get_io_locks && !launder_loop && free_shortage()){
            launder_loop = 1; //从这里被设置成1,可以看出,最多只做2次扫描脏队列
           
            if(cleaned_pages)
                sync = 0;
            maxlaunder = MAX_LAUNDER;
            wakeup_bdflush(0);
            goto dirty_page_rescan; //根据参数,决定是否要进行第2次循环
        }
        return clean_pages; //返回被清洗的页面数量
}
//一开始的成员变量cleaned_pages用来累计被“清洗”的页面数量的,launder_loop用于控制扫描脏队列的次数,由于在清洗过程中,我们会移动队列中元素(当然这种移动式加到队尾,不然就会乱套),因此我们不能根据我们到达队尾来决定循环的结束,我们通过记录下队列长度到maxscan来控制循环的结束,进入while循环内部,第一步根据PG_inactive_dirty标志为1(一般来说不活跃队列中页面PG_inactive_dirty均为1,但有时候会出毛病)自然要把它从这个队列中删除.页面在通过这次判断后,对于正常的不活跃脏页面,通过下面几个处理 1.由于情况的改变的,这个页面本不属于这里(刚刚受到访问,被恢复页面印射的~_~怪了,恢复映射过程,应当同时将页面从不活跃中移除的,为什么这个动作会到这里做。不懂,可能的原因是:多进程运行,有进程正在试图访问这个页面而恢复了页面映射,但此时该页面应该被上锁).2页面寿命为尽(下面继续深入寿命) 3.页面不用作读/写文件的缓冲,使用计数大于1,表示有进程在使用这个页面.对于他们都是放回活跃队列.
继续运行,开始锁住页面,成功被锁住的页面,把它移到不活跃队列的尾部.再次判断页面是否为脏,是的话就获取对应的写函数,如果写函数提供不正确,只好将其送回活跃队列,再将页面PG_inactive_dirty改成0,表示干净,原因见注释,然后调用writepage清洗页面,返回清洗结果,如果清洗失败,则将页面标志位改成脏,同时归还给活跃队列。
能够进入到这里页面除了脏页面队列,还有用作文件缓冲读/写页面,对于这种首先看是否能释放(try_to_free_buffers),释放成功就归还给空闲队列,失败,则根据情况看其归还给哪个队列,具体情况见代码if(!clearedbuf){后面。
在清洗后,马上就观察是否还存在页面短缺情况if(freed_page && !free_shortpage()),如果不存在就退出。
对于任何都不属于的页面既page_active后面就归还给活跃队列

位置mm/vmscan.c
//在经过上面的回收脏页面还是没能达到补齐内存缺页的要求。根据do_try_to_free_pages执行就会执行到refill_inactive
static int refill_inactive(unsigned int gfp_mask, int user){
    int priority, count, start_count, made_progress;
   
    count = inactive_shortage() + free_shortages();
    if(user)
        count = (1 << page_cluster);
    start_count = count;
   
    kmem_cache_reap(gfp_mask);
   
    priority = 6;
    do{
        made_progress = 0;
       
        if(current->need_resched){ //如果值是1,表示这是个一个中断服务程序要求调度
            set_current_state(TASK_RUNNING);
            schedule(); //设置运行,继续让其运行,这个判断主要是防止kswapd进程长期占用CPU,具体原因以后可以看到
        }
       
        while(refill_inactive_scan(priority, 1)) { //扫描活跃队列,试图找出可以转入不活跃状态的页面
            made_progress = 1;
           
            if(--count <= 0)
                goto done;
        }
        shrink_dcache_memory(priority, gfp_mask);
        shrink_icache_memory(priority, gfp_mask);
       
        while(swap_out(priority, gfp_mask)){ //扫描映射表,从中试图找出可以转化成不活跃状态的页面
            made_progress = 1;
            if(--count <= 0)
                goto done;
        }
       
        if(!inactive_shortage() || !free_shortage())
            goto done;
           
        if(!made_progress)
            priority--;
    }while(priority >= 0);
   
    while(refill_inactive_scan(0,1)) {
        if(--count <= 0)
            goto done;
    }
   
    done:
        return (count < start_count);
}
//在之前做过清洗脏页面的努力后,发现仍然不能补齐内存缺口,此时就会去动现役页面。程序主体是一个do while循环,首先通过refill_inactive_state去扫描一个活跃队列,试图找出可以转化成不活跃的页面。其次是扫描进程的页面表,看是否有能够转到不活跃的页面

mm/vmscan.c
int refill_inactive_scan(unsigned int priority, int oneshot) {
    struct list_head * page_lru;
    struct page * page;
    int maxscan, //用户控制扫描页面的数量
        page_acitve = 0;
    int ret = 0;
   
    spin_lock(&pagemap_lru_lock);
    maxscan = nr_active_pages >> priority; //扫描的数量不是全部的活跃页面,而是根据传进来的优先级,决定扫描其中一部分
   
    while(maxscan-- > 0 && (page_lru = active_list.prev) != &active_list) { //扫描活跃页面队列
        page = list_entry(page_lru, sturct_page, lru);
       
        if(!PageActive(page)){ //首先验证确实是活跃页面,如果不是则做相应处理
            printk("VM: refill_inactive, wrong page on list.\n");
            list_del(page_lru);
            nr_active_pages--;
            continue;
        }
       
        if(PageTestandClearReferenced(page)) { //页面是否受到了访问,决定增加还是减少页面寿命,到0就表示页面已经耗尽了寿命
            age_page_up_nolock(page);
            page_active = 1;
        } else {
            age_page_down_ageonly(page);
           
            if(page->age == 0 && page_count(page) <= (page->buffers ? 2:1)) { //除了消耗完寿命外,页面换出还要看页面的使用计数,大于1表示还有用户空间的印射
                deactivate_page_nolock(page);//将页面转入不活跃队列
                page_active = 0;
            } else {
                page_active = 1;
            }
        }
       
        if(page_active || PageActive(page)) {
            list_del(page_lru); //对于不能转化成不活跃状态的页面,就将其放入活跃队列尾部
            list_add(page_lru, &active_list);
        } else {
            ret = 1; //对于成功装入不活跃队列的页面,根据oneshot决定是否还要继续扫描
            if(oneshot)
                break;
        }
    }
    spin_unlock(&pagemap_lru_lock);
    return ret;
}
//扫描活跃队列,看是否有能够转入不活跃队列的页面,和之前一样也要通过一个计数来控制扫描的次数,从maxscan的取值可以看出,不会扫描完整个队列,而是根据传入的参数priority来决定扫描的部分,同样,首先对扫描的页面确认是否为活跃页面,如果错误,则做相应的处理,其次通过PageTestandClearReferenced(page)判断该页面是否受到了访问,决定增加还是减少页面寿命,当寿命减少到0时候就可以换出这个页面,最后判断页面是否为不能处理的页面,放回活跃队列尾部,根据oneshot决定是否可以继续循环

mm/vmscan.c

#define SWAP_SHIFT 5
#define SWAP_MIN 8

static int swap_out(unsigned int priority, int gfp_mask){
    int counter; //决定循环次数
    int __ret;
    counter = (nr_threads << SWAP_SHIFT) >> priority; //根据内核中进程的个数和调用的优先级确定循环的次数,nr_threads当前系统中进程的数量
    if(counter < 1)
        counter = 1;
       
    for(;counter >= 0; counter--) { //每次循环的任务从所有进程中找出最合适的进程best,断开页面印射,进一步转换成不活跃状态,最合适的准则是"劫富济贫“和”轮流坐庄“的结合
        struct list_head *p;
        unsigned long max_cnt = 0;
        struct mm_struct * best = NULL;
        int assign = 0;
        int found_task = 0;
       
    select:
        spin_lock(&mmlist_lock);
        p = init_mm.mmlist.next;
       
        for(; p != init_mm.mmlist; p=p->next) {
            struct mm_struct * mm = list_entry(p, sturct mm_struct, mmlist);
            if(mm->rss <= 0) //mm->rss记录了是进程所占的内存页面数量
                continue;
            found_task++;
           
            if(assign == 1){//增加这层判断目的是,但我们找不到mm->swap_cnt不为0的mm时候,我们就会设置assign=1,然后再从新扫描一遍,此次就会直接把内存页面数量赋值给尚未考察页面数量,从而从新刷新一次,这样我们就会从最富有的进程开始下手,mm->swap_cnt用于保证我们所说的轮流坐庄,mm->rss则是保证劫富济贫
                mm->swap_cnt = (mm->rss >> SWAP_SHIFT);
                if(mm->swap_cnt < SWAP_MIN)
                    mm->swap_cnt=SWAP_MIN; //mm->swap_cnt记录一次轮换中尚未内存页面尚未考察的数量
            }
            if(mm->swap_cnt > max_cnt){//很明显这个是用来记录最大的mm->swap_cnt和mm的判断
                max_cnt = mm->swap_cnt;
                best = mm;
            }
        }//从循环退出来,我们就找到了最大的mm->swap_cnt的mm
       
        if(best)
            atomic_inc(&best->mm_users);
        spin_unlock(&mmlist->lock);
       
        if(!best){
            if(!assign && found_task > 0){//第一次进入,表示所有进程mm->swap_cnt都为0,第2次不会再进入了,一般不会出现第2次
                assign = 1;
                goto select;
            }
            break;
        } else {
            __ret = swap_out_mm(best, gfp_mask);//我们根据上面的原则找到合适的进程虚存管理结构后就开始对其进行”宰割“,具体动作下面
            mmput(best);
            break;
        }
    }
    return __ret;
}

//扫描进程的页面表,看是否有可以换出的页面,扫描次数取决于count,从他的值可以看出他是当前进程数量和传入的优先级决定的,从代码中看出两种优先级的判断其中"轮流坐庄“高于”劫富济贫“

mm/vmscan.c

static int swap_out_mm(stuct mm_struct * mm, int gfp_mask){
    int result = 0;
    unsigned long address;
    struct vm_area_struct * vma;
   
    spin_lock(&mm->page_table_lock);
    address = mm->swap_address;
    vma = find_vma(mm,address); //根据address找出vma,如果不太明白,请查询linux内存数据结构mm_struct和vm_area_struct
   
    if(vma){
        if(address < vma->vm_start)
            address = vma->vm_start;
       
        for(;;){
            result = swap_out_vma(mm, vma, address, gfp_mask); //调用swap_out_vma试图换出address所指向的vma中的一个页面
            if(result) //如果换出成功result的值会是1
                goto out_unlock;
            vma = vma->vm_next;失败就会去找下一个vma
            if(!vma)
                break;
            address = vma->vm_start;
        }
    }
    mm->swap_address = 0;
    mm->swap_cnt = 0;
   
out_unlock:
    spin_unlock(&mm->page_table_lock);
    return result;
}

swap_out_vma会调用关系swap_out_vma()>swap_out_pgd()>swap_out_pmd()>try_to_swap_out()
static int try_to_swap_out()(struct mm_struct * mm, struct vm_area_struct * vma, unsigned long address, pte_t * page_table, int gfp_mask){//page_table指向页面表项,不是页面表
    pte_t pte;
    swp_entry_t entry;
    struct page * page;
    int onlist;
   
    pte = *page_table;
   
    if(!pte_present(pte)) //测试该表项指向的物理页面是否在内存中
        goto out_failed;
    page = pte_page(pte);
   
    if((!VALID_PAGE(page)) || PageReserved(page)) //由于page必然在mem_map数组中,所以对于大于这个数组的长度的page是不合法的,其次,不允许交换出的页面也要考虑
        goto out_failed;
   
    if(!mm->swap_cnt)
        return 1;
    mm->swap_cnt--; //具体考察一个页面,自然要把swap_cnt减一
   
    onlist = PageActive(page); //表示当前页面是否活跃
   
    if(ptep_test_and_clear_young(page_table)){ //一个印射中的页面是否换出,取决于它最近是否受到访问,这个测试是否最近是否受到访问,并清0
        age_page_up(page);//页面表项中_PAGE_ACCESSED,当CPU印射一个物理页面,进而访问时候会把它赋值1,而执行ptep_test_and_clear_young会把它变成0
        goto out_failed;
    }
   
    if(!onlist)//onlist就是上面pageActive的返回,一个存在页面映射的页面,一般是在活跃队列,但也存在特殊情况使它存在于不活跃队列,这个就是不活跃队列的页面
        age_page_down_ageonly(page);//如果页面在不活跃队列,这次的调查要减少它的寿命
       
    if(page->age > 0)//即使可以换出了,也要给个“留职查看”的机会,这个标准就是age的值
        goto out_failed;
   
    //经过以上层层的筛选,目前的页面应该是可以换出的页面了
    if(TryLockPage(page))//操作之前,先锁住页面
        goto out_failed;
   
    pte = ptep_get_and_clear(page_table);//读页面表项内容,把表项清0。之所以再读一次是防止它内容可能被改
    flush_tlb_page(vma, address);
   
    if(PageSwapCache(page)){//断开也要分情况,若page数据结构已经在换入/换出队列中,那只需要把它页面映射断开,再根据是否被写过,决定放入脏队列中
        entry.val = page->index;
        if(pte_dirty(pte))
            set_page_dirty(page);
set_swap_pte:
        swap_duplicate(entry);//对新的表项进行一些验证,同时递增相应盘面的共享计数
        set_pte(page_table, swap_entry_to_pte(entry)); //改变原来对内存映射到对盘面的印射
drop_pte:
        UnlockPage(page);//解锁
        mm->rss--;//内存中少一个页面,自然要减1
        deactivate_page(page);//将页面设置成不活跃状态
out_failed:
        return 0;//在这里,若返回0,则swap_out_pmd()会去寻找下一个页面表
    }
}

mm/swap.c
void deactivate_page(struct page * page){
    spin_lock(&pagemap_lru_lock);
    deactivate_page_nolock(page);
    spin_unlock(&pagemap_lru_lock);
}

deactiveate_page_nolock(struct page * page){
    int maxcount = (page->buffers ? 3:2);
    page->age = 0;
    ClearPageReferenced(page);
   
    if(PageActive(page) && page_count(page) <= maxcount && !page_ramdisk(page)){ //换出前判断,是否还有页面映射,是否为内存模拟硬盘页面
        del_page_from_active_list(page);
        add_page_to_inactive_dirty_list(page);//第一次总是换到脏页面队列,然后再清洗
    }
}

你可能感兴趣的:(thread,数据结构,linux,cache,UP)