memory子系統
memory 子系統可以設定 cgroup 中任務使用的內存限制,並自動生成由那些任務使用的內存資源報告。memory子系統是通過linux的resource counter機制實現的。下面我們就先來看一下resource counter機制。
resource counter是內核為子系統提供的一種資源管理機制。這個機制的實現包括了用於記錄資源的數據結構和相關函數。Resource counter定義了一個res_counter的結構體來管理特定資源,定義如下:
struct res_counter {
unsigned long long usage;
unsigned long long max_usage;
unsigned long long limit;
unsigned long long soft_limit;
unsigned long long failcnt; /*
spinlock_t lock;
struct res_counter *parent;
};
Usage用於記錄當前已使用的資源,max_usage用於記錄使用過的最大資源量,limit用於設置資源的使用上限,進程組不能使用超過這個限制的資源,soft_limit用於設定一個軟上限,進程組使用的資源可以超過這個限制,failcnt用於記錄資源分配失敗的次數,管理可以根據這個記錄,調整上限值。Parent指向父節點,這個變量用於處理層次性的資源管理。
除了這個關鍵的數據結構,resource counter還定義了一系列相關的函數。下面我們來看幾個關鍵的函數。
void res_counter_init(struct res_counter *counter, struct res_counter *parent)
{
spin_lock_init(&counter->lock);
counter->limit = RESOURCE_MAX;
counter->soft_limit = RESOURCE_MAX;
counter->parent = parent;
}
這個函數用於初始化一個res_counter。
第二個關鍵的函數是int res_counter_charge(struct res_counter *counter, unsigned long val, struct res_counter **limit_fail_at)。當資源將要被分配的時候,資源就要被記錄到相應的res_counter里。這個函數作用就是記錄進程組使用的資源。在這個函數中有:
for (c = counter; c != NULL; c = c->parent) {
spin_lock(&c->lock);
ret = res_counter_charge_locked(c, val);
spin_unlock(&c->lock);
if (ret < 0) {
*limit_fail_at = c;
goto undo;
}
}
在這個循環里,從當前res_counter開始,從下往上逐層增加資源的使用量。我們來看一下res_counter_charge_locked這個函數,這個函數顧名思義就是在加鎖的情況下增加使用量。實現如下:
{
if (counter->usage + val > counter->limit) {
counter->failcnt++;
return -ENOMEM;
}
counter->usage += val;
if (counter->usage > counter->max_usage)
counter->max_usage = counter->usage;
return 0;
}
首先判斷是否已經超過使用上限,如果是的話就增加失敗次數,返回相關代碼;否則就增加使用量的值,如果這個值已經超過歷史最大值,則更新最大值。
第三個關鍵的函數是void res_counter_uncharge(struct res_counter *counter, unsigned long val)。當資源被歸還到系統的時候,要在相應的res_counter減輕相應的使用量。這個函數作用就在於在於此。實現如下:
for (c = counter; c != NULL; c = c->parent) {
spin_lock(&c->lock);
res_counter_uncharge_locked(c, val);
spin_unlock(&c->lock);
}
從當前counter開始,從下往上逐層減少使用量,其中調用了res_counter_uncharge_locked,這個函數的作用就是在加鎖的情況下減少相應的counter的使用量。
有這些數據結構和函數,只需要在內核分配資源的時候,植入相應的charge函數,釋放資源時,植入相應的uncharge函數,就能實現對資源的控制了。
介紹完resource counter,我們再來看memory子系統是利用resource counter實現對內存資源的管理的。
memory子系統定義了一個叫mem_cgroup的結構體來管理cgroup相關的內存使用信息,定義如下:
struct mem_cgroup {
struct cgroup_subsys_state css;
struct res_counter res;
struct res_counter memsw;
struct mem_cgroup_lru_info info;
spinlock_t reclaim_param_lock;
int prev_priority;
int last_scanned_child;
bool use_hierarchy;
atomic_t oom_lock;
atomic_t refcnt;
unsigned int swappiness;
int oom_kill_disable;
bool memsw_is_minimum;
struct mutex thresholds_lock;
struct mem_cgroup_thresholds thresholds;
struct mem_cgroup_thresholds memsw_thresholds;
struct list_head oom_notify;
unsigned long move_charge_at_immigrate;
struct mem_cgroup_stat_cpu *stat;
};
跟其他子系統一樣,mem_cgroup也包含了一個cgroup_subsys_state成員,便於task或cgroup獲取mem_cgroup。
mem_cgroup中包含了兩個res_counter成員,分別用於管理memory資源和memory+swap資源,如果memsw_is_minimum為true,則res.limit=memsw.limit,即當進程組使用的內存超過memory的限制時,不能通過swap來緩解。
use_hierarchy則用來標記資源控制和記錄時是否是層次性的。
oom_kill_disable則表示是否使用oom-killer。
oom_notify指向一個oom notifier event fd鏈表。
另外memory子系統還定義了一個叫page_cgroup的結構體:
struct page_cgroup {
unsigned long flags;
struct mem_cgroup *mem_cgroup;
struct page *page;
struct list_head lru; /* per cgroup LRU list */
};
此結構體可以看作是mem_map的一個擴展,每個page_cgroup都和所有的page關聯,而其中的mem_cgroup成員,則將page與特定的mem_cgroup關聯起來。
我們知道在linux系統中,page結構體是用來管理物理頁框的,一個物理頁框對應一個page結構體,而每個進程中的task_struct中都有一個mm_struct來管理進程的內存信息。每個mm_struct知道它屬於的進程,進而知道所屬的mem_cgroup,而每個page都知道它屬於的page_cgroup,進而也知道所屬的mem_cgroup,而內存使用量的計算是按cgroup為單位的,這樣以來,內存資源的管理就可以實現了。
memory子系統既然是通過resource counter實現的,那肯定會在內存分配給進程時進行charge操作的。下面我們就來看一下這些charge操作:
1.page fault發生時,有兩種情況內核需要給進程分配新的頁框。一種是進程請求調頁(demand paging),另一種是copy on write。內核在handle_pte_fault中進行處理。其中,do_linear_fault處理pte不存在且頁面線性映射了文件的情況,do_anonymous_page處理pte不存在且頁面沒有映射文件的情況,do_nonlinear_fault處理pte存在且頁面非線性映射文件的情況,do_wp_page則處理copy on write的情況。其中do_linear_fault和do_nonlinear_fault都會調用__do_fault來處理。Memory子系統則__do_fault、do_anonymous_page、do_wp_page植入mem_cgroup_newpage_charge來進行charge操作。
2.內核在handle_pte_fault中進行處理時,還有一種情況是pte存在且頁又沒有映射文件。這種情況說明頁面之前在內存中,但是后面被換出到swap空間了。內核用do_swap_page函數處理這種情況,memory子系統在do_swap_page加入了mem_cgroup_try_charge_swapin函數進行charge。mem_cgroup_try_charge_swapin是處理頁面換入時的charge的,當執行swapoff系統調用(關掉swap空間),內核也會執行頁面換入操作,因此mem_cgroup_try_charge_swapin也被植入到了相應的函數中。
3.當內核將page加入到page cache中時,也需要進行charge操作,mem_cgroup_cache_charge函數正是處理這種情況,它被植入到系統處理page cache的add_to_page_cache_locked函數中。
4.最后mem_cgroup_prepare_migration是用於處理內存遷移中的charge操作。
除了charge操作,memory子系統還需要處理相應的uncharge操作。下面我們來看一下uncharge操作:
1.mem_cgroup_uncharge_page用於當匿名頁完全unmaped的時候。但是如果該page是swap cache的話,uncharge操作延遲到mem_cgroup_uncharge_swapcache被調用時執行。
2.mem_cgroup_uncharge_cache_page用於page cache從radix-tree刪除的時候。但是如果該page是swap cache的話,uncharge操作延遲到mem_cgroup_uncharge_swapcache被調用時執行。
3.mem_cgroup_uncharge_swapcache用於swap cache從radix-tree刪除的時候。Charge的資源會被算到swap_cgroup,如果mem+swap controller被禁用了,就不需要這樣做了。
4.mem_cgroup_uncharge_swap用於swap_entry的引用數減到0的時候。這個函數主要在mem+swap controller可用的情況下使用的。
5.mem_cgroup_end_migration用於內存遷移結束時相關的uncharge操作。
Charge函數最終都是通過調用__mem_cgroup_try_charge來實現的。在__mem_cgroup_try_charge函數中,調用res_counter_charge(&mem->res, csize, &fail_res)對memory進行charge,調用res_counter_charge(&mem->memsw, csize, &fail_res)對memory+swap進行charge。
Uncharge函數最終都是通過調用__do_uncharge來實現的。在__do_uncharge中,分別調用res_counter_uncharge(&mem->res,PAGE_SIZE)和res_counter_uncharge(&mem->memsw, PAGE_SIZE)來uncharge memory和memory+swap。
跟其他子系統一樣,memory子系統也實現了一個cgroup_subsys。
struct cgroup_subsys mem_cgroup_subsys = {
.name = "memory",
.subsys_id = mem_cgroup_subsys_id,
.create = mem_cgroup_create,
.pre_destroy = mem_cgroup_pre_destroy,
.destroy = mem_cgroup_destroy,
.populate = mem_cgroup_populate,
.can_attach = mem_cgroup_can_attach,
.cancel_attach = mem_cgroup_cancel_attach,
.attach = mem_cgroup_move_task,
.early_init = 0,
.use_id = 1,
};
Memory子系統中重要的文件有
memsw.limit_in_bytes
{
.name = "memsw.limit_in_bytes",
.private = MEMFILE_PRIVATE(_MEMSWAP, RES_LIMIT),
.write_string = mem_cgroup_write,
.read_u64 = mem_cgroup_read,
},
這個文件用於設定memory+swap上限值。
Limit_in_bytes
{
.name = "limit_in_bytes",
.private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),
.write_string = mem_cgroup_write,
.read_u64 = mem_cgroup_read,
},
這個文件用於設定memory上限值。
作者曰:memory子系統的實現相當復雜,這篇文章只是簡單分析了一下實現框架,沒有去分析具體細節。要完全懂memory子系統,首先就要懂linux的memory管理,這個就不容易了,作者本人也只是略知一二,故不能更深入地去分析memory子系統的細節了。