1. 程式人生 > >頁面交換守護程序kswapd

頁面交換守護程序kswapd

從原理上說,kswapd相當於一個程序,它有自己的程序控制塊task_struct結構。與其它程序一樣受核心的排程。而正因為核心將它按程序來排程,就可以讓它在系統相對空閒的時候來執行。不過,與普通程序相比,kswapd有其特殊性。首先,它沒有自己獨立的地址空間,所以在近代作業系統理論中把它稱為“執行緒”以與程序相區別。那麼,kswapd的地址空間是什麼?實際上,核心空間就是它的地址空間。在這一點上,它與中斷服務例程相似。其次,它的程式碼是靜態地連結在核心中的,因此,可以直接呼叫核心中的各種子程式和函式。

Kswapd的原始碼基本上都在mm/vmscan.c中,圖6.18給出了kswapd中與交換有關的主要函式呼叫關係。

      圖 6.18 kswapd 的實現程式碼中與交換相關的主要函式的呼叫關係  

從上面的呼叫關係可以看出, kswapd的實現相當複雜,這不僅僅涉及複雜的頁面交換技術,還涉及與磁碟相關的具體檔案操作,因此,為了理清思路,搞清主要內容,我們對一些主要函式給予描述:

1.Kswapd()

在Linux2.4.10以後的版本中對kswapd()的實現程式碼進行了模組化組織,可讀性大大加強,程式碼如下:

   int kswapd(void *unused)

 {

          struct task_struct *tsk = current;

         DECLARE_WAITQUEUE(wait, tsk);

         daemonize();        /*核心執行緒的初始化*/

         strcpy(tsk->comm, "kswapd");

         sigfillset(&tsk->blocked);  /*把程序PCB中的阻塞標誌位全部置為1*/

         /*

          * Tell the memory management that we're a "memory allocator",

         * and that if we need more memory we should get access to it

          * regardless (see "__alloc_pages()"). "kswapd" should

          * never get caught in the normal page freeing logic.

         *

         * (Kswapd normally doesn't need memory anyway, but sometimes

          * you need a small amount of memory in order to be able to

          * page out something else, and this flag essentially protects

          * us from recursively trying to free more memory as we're

          * trying to free the first piece of memory in the first place).

         */

         tsk->flags |= PF_MEMALLOC; /*這個標誌表示給kswapd要留一定的記憶體*/

         /*

          * Kswapd main loop.

          */

         for (;;) {

                __set_current_state(TASK_INTERRUPTIBLE);

                add_wait_queue(&kswapd_wait, &wait); /*把kswapd 加入等待佇列*/

                mb();   /*增加一條彙編指令*/

                 if (kswapd_can_sleep())    /*檢查排程標誌是否置位*/

                         schedule();     /*呼叫排程程式*/

             _set_current_state(TASK_RUNNING); /*讓kswapd 處於就緒狀態*/

             remove_wait_queue(&kswapd_wait, &wait); /*把kswapd 從等待佇列刪除*/

                 /*

                  * If we actually get into a low-memory situation,

                  * the processes needing more memory will wake us

                  * up on a more timely basis.

                 */

                 kswapd_balance();   /* kswapd 的核心函式,請看後面內容*/

                run_task_queue(&tq_disk);  /*執行tq_disk 佇列中的例程*/

         }

}

kswapd核心執行緒的建立如下:

static int __init kswapd_init(void)

{

         printk("Starting kswapd\n");

         swap_setup();

         kernel_thread(kswapd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);

        return 0;

}

   函式kswapd_init()是在系統初始化期間被呼叫的。它主要做兩件事,其中swap_setup()根據實體記憶體的大小設定一個全域性變數page_cluster。這是一個與磁碟裝置驅動有關的引數。由於讀磁碟時先要經過尋道,而尋道是比較費時的操作,因此,為了節省時間,每次最好多讀幾個頁面,這叫“預讀”。到底每次預讀幾個頁面,就是由這個函式根據記憶體本身的大小給出的(為2,3或4)。另外一個主要的任務就是呼叫kernel_thread()建立核心執行緒kswapd。

     從上面的介紹可以看出,kswapd成為核心的一個執行緒,其主迴圈是一個無限迴圈。迴圈一開始,把它加入等待佇列,但如果排程標誌為1,就執行排程程式,緊接著就又把它從等待佇列刪除,將其狀態變為就緒。只要排程程式再次執行,它就會得到執行,如此周而復始進行下去。

2.kswapd_balance()函式

 從該函式的名字可以看出,這是一個要求得平衡的函式,那麼,求得什麼樣的平衡呢?在本章的初始化一節中,我們介紹了實體記憶體的三個層次,即儲存節點、管理區和頁面。所謂平衡就是對頁面的釋放要均衡地在各個儲存節點、管理區中進行,程式碼如下:

static void kswapd_balance(void)

{

         int need_more_balance;

         pg_data_t * pgdat;

         do {

                 need_more_balance = 0;

                 pgdat = pgdat_list;

                do

                         need_more_balance |= kswapd_balance_pgdat(pgdat);

                while ((pgdat = pgdat->node_next));

        } while (need_more_balance);

}

 這個函式比較簡單,主要是對每個儲存節點進行掃描。然後又呼叫kswapd_balance_pgdat()對每個管理區進行掃描:

static int kswapd_balance_pgdat(pg_data_t * pgdat)

{

         int need_more_balance = 0, i;

        zone_t * zone;

         for (i = pgdat->nr_zones-1; i >= 0; i--) {

                zone = pgdat->node_zones + i;

                 if (unlikely(current->need_resched))

                         schedule();

                 if (!zone->need_balance)

                         continue;

                 if (!try_to_free_pages(zone, GFP_KSWAPD, 0)) {

                         zone->need_balance = 0;

                        __set_current_state(TASK_INTERRUPTIBLE);

                         schedule_timeout(HZ);

                        continue;

                 }

                if (check_classzone_need_balance(zone))

                         need_more_balance = 1;

                 else

                         zone->need_balance = 0;

         }

其中,最主要的函式是try_to_free_pages(),能否呼叫這個函式取決於平衡標誌need_balance是否為1,也就是說看某個管理區的空閒頁面數是否小於最高警戒線,這是由check_classzone_need_balance()函式決定的。當某個管理區的空閒頁面數小於其最高警戒線時就呼叫try_to_free_pages()。

3.try_to_free_pages()

該函式程式碼如下:

   int try_to_free_pages(zone_t *classzone, unsigned int gfp_mask, unsigned int order)

{

         int priority = DEF_PRIORITY;

         int nr_pages = SWAP_CLUSTER_MAX;

         gfp_mask = pf_gfp_mask(gfp_mask);

         do {

                nr_pages = shrink_caches(classzone, priority, gfp_mask, nr_pages);

                 if (nr_pages <= 0)

                        return 1;

        } while (--priority);

        /*

          * Hmm.. Cache shrink failed - time to kill something?

          * Mhwahahhaha! This is the part I really like. Giggle.

          */

        out_of_memory();

         return 0;

}

   其中的優先順序表示對佇列進行掃描的長度,預設的優先順序DEF_PRIORITY為6(最低優先順序)。假定佇列長度為L,優先順序6就表示要掃描的佇列長度為L/26,所以這個迴圈至少迴圈6次。nr_pages為要換出的頁面數,其最大值SWAP_CLUSTER_MAX為32。其中主要呼叫的函式為shrink_caches():

   static int shrink_caches(zone_t * classzone, int priority, unsigned int gfp_mask, int nr_pages)

{

         int chunk_size = nr_pages;

         unsigned long ratio;

         nr_pages -= kmem_cache_reap(gfp_mask);

        if (nr_pages <= 0)

                 return 0;

        nr_pages = chunk_size;

         /* try to keep the active list 2/3 of the size of the cache */

         ratio = (unsigned long) nr_pages * nr_active_pages / ((nr_inactive_pages + 1) * 2);

         refill_inactive(ratio);

        nr_pages = shrink_cache(nr_pages, classzone, gfp_mask, priority);

         if (nr_pages <= 0)

                 return 0;

         shrink_dcache_memory(priority, gfp_mask);

         shrink_icache_memory(priority, gfp_mask);

1 #ifdef CONFIG_QUOTA

         shrink_dqcache_memory(DEF_PRIORITY, gfp_mask);

#endif

        return nr_pages;

}

其中kmem_cache_reap()函式“收割(reap)”由Slab機制管理的空閒頁面。如果從Slap回收的頁面數已經達到要換出的頁面數nr_pages,就不用從其它地方進行換出。refill_inactive()函式把活躍佇列中的頁面移到非活躍佇列。shrink_cache()函式把一個“洗淨”且未加鎖的頁面移到非活躍佇列,以便該頁能被儘快釋放。

此外,除了從各個程序的使用者空間所對映的物理頁面中回收頁面外,還呼叫shrink_dcache_memory()、shrink_icache_memory()及shrink_dqcache_memory()回收核心資料結構所佔用的空間。在檔案系統一章將會看到,在開啟檔案的過程中,要分配和使用代表著目錄項的dentry資料結構,還有代表著檔案索引節點inode的資料結構。這些資料結構在檔案關閉後並不立即釋放,而是放在LRU佇列中作為後備,以防在不久將來的檔案操作中又用到。這樣經過一段時間後,就有可能積累起大量的dentry資料結構和inode資料結構,從而佔用數量可觀的物理頁面。這時,就要通過這些函式適當加以回收。

4.頁面置換

  到底哪些頁面會被作為後選頁以備換出,這是由Swap_out()和shrink_cache()一起完成的。這個過程比較複雜,這裡我們拋開原始碼,以理清思路為目標。

 shrink_cache()要做很多換出的準備工作。它關注兩個佇列:“活躍的” LRU 佇列 和 “非活躍的” FIFO 佇列,每個佇列都是struct page形成的連結串列。該函式的程式碼比較長,我們把它所做的工作概述如下:

·      把引用過的頁面從活躍佇列的隊尾移到該佇列的隊頭(實現LRU策略)。

·      把未引用過的頁面從活躍佇列的隊尾移到非活躍佇列的隊頭(為準備換出而排隊)。

·      把髒頁面安排在非活躍佇列的隊尾準備寫到磁碟。

·      從非活躍佇列的隊尾恢復乾淨頁面(寫出的頁面就成為乾淨的)