天天看點

OOM-KILLer的演進與新的啟發式政策

linux在2.6.36核心中修正了oom-killer的行為,oom-killer在引入之初就曾引發過争論,這個東西到底應不應該存在,記憶體不夠用了的時候,到底應不應該由作業系統核心替我們做一些事,比如選出一個吃記憶體的大戶,然後幹掉它,這種行為甚是魯莽,按照機制和政策分離的原則,核心其實應該将這件事報告給使用者,讓使用者空間程序判斷應該怎麼做,然而此時已經沒有記憶體,機器可能已經無法操作,起碼已經是死寂之神态,核心又如何通知使用者,是以索性就oom-killer了。

     早期的oom-killer在選擇應該被殺掉的程序的時候的政策非常之簡單,就是以該程序之虛拟位址空間的大小為基準,然後以運作時間以及fork情況加上nice值等細小因素微擾之,最終取出一個得分最高者,殺之!這種政策顯然無法服衆啊,作業系統實際需要的是實體記憶體頁面,此時已經沒有,是以作業系統需要的是其它程序的實體記憶體頁面被釋放,而早期政策使用虛拟位址空間的大小為基準實則不合适,畢竟要知道虛拟位址空間隻是組織保護模式作業系統之所用,程序實際使用的是實體記憶體,實體記憶體頁面映射進虛拟記憶體之後,方可展現保護模式的作業系統之多作業并發之态。以上提及隻是老的政策不足之處之一,另還有,當時需要的僅僅是一些記憶體頁面,實則不必将記憶體占用最大者殺掉,而且不管此記憶體占用最大者是什麼,不問青紅皂白一律處死,即使它映射了大量的實體記憶體,如果它同時又做了極其重要之事,也是合理的,比如對于KDE桌面主程序,它必然占據大量頁面,然而它也确實重要,相反,整機記憶體告罄的主謀可能是一些占據記憶體很小然而很多的程序,隻需大量fork,然後做很少的事即可,核心如何識别這種情形?是以對于oom-killer需要該進之處确實有三:第一點就是将實體記憶體頁面的使用作為基準而不是虛拟位址空間的大小;第二則是導出使用者政策的控制權;第三是核心要有一個簡單然而合理的預設政策。以上三點在2.6.36核心中完全實作。

unsigned int oom_badness(struct task_struct *p, struct mem_cgroup *mem,

              const nodemask_t *nodemask, unsigned long totalpages)

{

    int points;

    ...//0

    //第一要點的展現:完全按照實體記憶體的占用情況計算分數:rss表示實體頁面的占用,totalpages表示目前節點的所有頁面數量

    points = (get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS)) * 1000 /

            totalpages;

    ...

    //第三要點的展現之一:特權程序最好不要被殺掉

    if (has_capability_noaudit(p, CAP_SYS_ADMIN))

        points -= 30;

    //第二要點的展現:可配置的使用者建議的政策,oom_score_adj可以通過/proc/pid/oom_adj檔案寫入,如果你寫入-1000,那就表示這個程序是不能被oom-killer殺死的,這種情況将在注釋0處傳回

    points += p->signal->oom_score_adj;

    ...//傳回一個1到1000之間的分數

}

第三要點的展現之二是什麼呢?在out_of_memory函數中有下面的邏輯:

retry:

    //下面的select的核心就是上面的oom_badness函數

    p = select_bad_process(&points, totalpages, NULL, mpol_mask);

    //如果找不到一個可殺的程序,直接panic即可,記憶體已經告罄!!

    if (!p) {

        dump_header(NULL, gfp_mask, order, NULL, mpol_mask);

        read_unlock(&tasklist_lock);

        panic("Out of memory and no killable processes.../n");

    }

    //oom_kill_process展現了要點之三的第二部分

    if (oom_kill_process(p, gfp_mask, order, points, totalpages, NULL,

                nodemask, "Out of memory"))

        goto retry;

    killed = 1;

在oom_kill_process中,oom-killer邏輯進行最後的抉擇:

static int oom_kill_process(...)

    struct task_struct *victim = p;

    struct task_struct *child;

    struct task_struct *t = p;

    unsigned int victim_points = 0;

    //不是已經找到一個程序p了嗎,為何還要觸摸其子程序呢? 

    do {

        list_for_each_entry(child, &t->children, sibling) {

            unsigned int child_points;

            child_points = oom_badness(child, mem, nodemask, totalpages);

            if (child_points > victim_points) {

                victim = child;

                victim_points = child_points;

            }

        }

    } while_each_thread(p, t);

    return oom_kill_task(victim, mem);

是的,問題是為何還要觸摸其子程序呢?這就是第三要點的第二部分:殺子不殺父的原則。

有兩個理由這麼做,第一個理由是如果殺掉了parent,那麼由于linux程序按照樹型結構組織,那麼一旦父程序退出,将會有很多工作要做,比如收養它的子程序之類的事,本來一個很小的騰出記憶體空間的事,最後招緻這麼多完全額外的事情,這是不合适的,第二個理由就是可能這個父程序是一個關鍵服務,比如一個網絡daemon程序,或者桌面管理器程式等等,殺掉的話會嚴重影響使用者空間的,而子程序往往都是一些工作者程序(這已經成了unix/linux的程式設計模式了),是以殺掉子程序能騰出一些記憶體夠這次使用就可以了,如果不夠的話那麼再殺一個子程序即可,可見這裡使用了一點懶惰的思想。

     另外,新舊(2.6.36核心之前和之後)兩個版本的oom-killer對待候選程序的子程序采取了幾乎相反的政策,我覺得就版本實作的稍顯複雜但是更有意思,舊版本的核心在badness函數中處理子程序相關的邏輯(基于2.6.35核心):

list_for_each_entry(child, &p->children, sibling) {

    task_lock(child);

    if (child->mm != mm && child->mm)

        points += child->mm->total_vm/2 + 1;

    task_unlock(child);

這裡面有兩個要點,這兩個要點之間有一個很有意思的權衡,第一個要點是如果一個程序多一個子程序,那麼它的bad分數就會增加,這明顯是不照顧一直在fork的程序,也就是如果一個程序瘋狂的fork,那麼它的bad分數就會很高,子程序越多,它的bad分數就越高,第二個要點則是每次隻加入子程序total_vm的一半,這是為了防止一個吃大量記憶體的子程序逃出oom-killer的眼線,因為如果父程序被加的分數過高的話,子程序的分數無論怎樣也不會超過父程序因而永遠不會被選中,特别的,如果每次都将子程序total_vm完全加到父程序的分數裡面,子程序的分數再怎麼也很難(!!)超過父程序了,是以這裡就選擇了子程序total_vm的%50加入父程序的bad分數,我倒是覺得這個百分比可調整會比較好,在/proc/pid/中導出一個檔案,可調節子程序加百分之多少分數到父程序,這樣更合理些,而且使用者也可以根據程序的性質或者功能自己調解這個百分比進而影響oom-killer的行為。不管怎麼說,舊版本的那個%50是寫死的,不可調的,這樣就會有一個問題,如果系統中有一個prefork模式的apache服務,有大量的連接配接有大量的工作程序,那麼在oom的時候,該apache主程序就有很大的可能被幹掉,這是很不合理的,是以新版本的oom-killer反其道而行之,在oom_badness中去掉了這一段代碼,孩子太多不是父親的錯(?),在新的oom-killer設計中,不再試圖殺死父程序。如果一個程序有子程序,那麼偏向于殺死子程序往往會對整個系統的影響比較小。新核心oom-killer的設計預設了這樣一個不總是事實的事,那就是一般而言,父程序都是總體控制者,它們一般都是理性的,合法的,善意的,發生oom的時候,總是不應該殺掉父程序。為了避免老核心在oom時幹掉apache這種事,你可以将/proc/pid/oom_adj的值設定的足夠低,或者直接設定成-1000,這樣它就不會被幹掉了。

附:android的killer

android是一個手機(平闆電腦)作業系統平台,它運作于記憶體基本上都很有限的手持裝置上,并且這類裝置的程序往往都具有螢幕獨占性,是以不像pc或者伺服器那樣,你可以随時調出任務管理器,在手持裝置上,調出任務管理器意味着你必須退出或者隐藏目前的任務,這就是說,一般而言,手持裝置上同時運作的任務不會太多,最重要的是,手持裝置上一般都是直接面向特定的應用,持有者往往隻是拿來直接娛樂而幾乎不會去搞什麼任務管理器之類的東西,另外如果真的到了oom的時候,使用者體驗就會非常差,是以決不能讓手持裝置oom,是以需要有一個系統的任務管理器一直在運作,時刻關注着記憶體情況,由于應用的螢幕獨占性,記憶體低于臨界值的時候,要選擇殺掉不在前端的一個或幾個程序,有了這種支援,程式本身和使用者就不用操作記憶體的問題了,是以我們就會明白為何android的程式幾乎都沒有“退出”機制了,換句話說,android的程序不是自己退出的,而是被殺的。

     接下來,android的那個一直在運作的任務管理程式會如何選擇殺哪些程序呢?政策和linux核心中的oom一樣嗎?肯定不同啦!android任務系統将所有的程序分成了下面幾類:

foreground process:一些頂層容器之類的屬于這種程序,比如主界面,這種程序一般不會被殺,除非oom,然而一般任務系統是不會到oom的,因為那時幾乎使用者都要把手機摔掉了。

visible process:目前的程序。這類程序關系到使用者體驗,一般也不會被殺掉。

service process:系統服務。這種程序默默耕耘而不露面,一般也不會被殺掉。

background process:已經到後面的程序,就是被前端的程序遮蓋住的程序,一般而言這類程序最容易被殺,如果系統記憶體不吃緊,那麼在使用者點選别的程式或者點選主界面而将目前程式遮住之後,目前程式并不自己調用exit,而是繼續保留,直到任務管理系統認為需要殺死它時才殺死它,這樣既提高了效率(如果使用者再次使用該程式的話不必重新初始化了),另外對于開發來講也很省事,不用監控退出事件了,一切交給任務管理系統,隻要一個桌面上的程式被點選,任務管理系統首先在全局的lru連結清單中找這個程式,找到的話則将它置于前端,找不到則啟動它并将它加入到lru,當确定要殺程序的時候,從lru中的表頭開始殺即可。

     将任務管理機制抽出來變成全局的機制,這完全是為了增強使用者體驗,使用者僅為娛樂而用機器,不必再為資源管理等事自己動手,并且全局的任務管理機制也能在全局上明了目前的記憶體情況,不待oom則早已開始作為killer來動作了,使用者始終感到記憶體是夠用的,并且由于隐藏而不退出,退到後面的程式在沒有被殺死之前再次運作之中又起到了cache的作用,增加了性能。程序的生命周期也不再僅僅受程式本身控制,而是受到整體記憶體情況的牽制和全局任務管理系統的直接控制。

     然而對于習慣于pc程式設計的我來講,這種全局的程序控制機制還是不甚習慣,總感覺跛腳!

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271141

繼續閱讀