Linux Cgroups詳解(九)


cpuset子系統

cpuset子系統為cgroup 中的任務分配獨立 CPU(在多核系統)和內存節點。Cpuset子系統為定義了一個叫cpuset的數據結構來管理cgroup中的任務能夠使用的cpu和內存節點。Cpuset定義如下:

struct cpuset {

struct cgroup_subsys_state css;

 

unsigned long flags; /* "unsigned long" so bitops work */

cpumask_var_t cpus_allowed; /* CPUs allowed to tasks in cpuset */

nodemask_t mems_allowed; /* Memory Nodes allowed to tasks */

 

struct cpuset *parent; /* my parent */

 

struct fmeter fmeter; /* memory_pressure filter */

 

/* partition number for rebuild_sched_domains() */

int pn;

 

/* for custom sched domain */

int relax_domain_level;

 

/* used for walking a cpuset heirarchy */

struct list_head stack_list;

};

其中css字段用於taskcgroup獲取cpuset結構。

cpus_allowedmems_allowed定義了該cpuset包含的cpu和內存節點。

Parent字段用於維持cpuset的樹狀結構,stack_list則用於遍歷cpuset的層次結構。

Pn和relax_domain_level是跟Linux 調度域相關的字段,pn指定了cpuset的調度域的分區號,而relax_domain_level表示進行cpu負載均衡尋找空閑cpu的策略。

除此之外,進程的task_struct結構體里面還有一個cpumask_t cpus_allowed成員,用以存儲進程的cpus_allowed信息;一個nodemask_t mems_allowed成員,用於存儲進程的mems_allowed信息。

Cpuset子系統的實現是通過在內核代碼加入一些hook代碼。由於代碼比較散,我們逐條分析。

在內核初始化代碼(即start_kernel函數)中插入了對cpuset_init調用的代碼,這個函數用於cpuset的初始化。

下面我們來看這個函數:

int __init cpuset_init(void)

{

int err = 0;

 

if (!alloc_cpumask_var(&top_cpuset.cpus_allowed, GFP_KERNEL))

BUG();

 

cpumask_setall(top_cpuset.cpus_allowed);

nodes_setall(top_cpuset.mems_allowed);

 

fmeter_init(&top_cpuset.fmeter);

set_bit(CS_SCHED_LOAD_BALANCE, &top_cpuset.flags);

top_cpuset.relax_domain_level = -1;

 

err = register_filesystem(&cpuset_fs_type);

if (err < 0)

return err;

 

if (!alloc_cpumask_var(&cpus_attach, GFP_KERNEL))

BUG();

 

number_of_cpusets = 1;

return 0;

}

cpumask_setallnodes_setalltop_cpuset能使用的cpu和內存節點設置成所有節點。緊接着,初始化fmeter,設置top_cpusetload balance標志。最后注冊cpuset文件系統,這個是為了兼容性,因為在cgroups之前就有cpuset了,不過在具體實現時,對cpuset文件系統的操作都被重定向了cgroup文件系統。

除了這些初始化工作,cpuset子系統還在do_basic_setup函數(此函數在kernel_init中被調用)中插入了對cpuset_init_smp的調用代碼,用於smp相關的初始化工作。

下面我們看這個函數:

void __init cpuset_init_smp(void)

{

cpumask_copy(top_cpuset.cpus_allowed, cpu_active_mask);

top_cpuset.mems_allowed = node_states[N_HIGH_MEMORY];

 

hotcpu_notifier(cpuset_track_online_cpus, 0);

hotplug_memory_notifier(cpuset_track_online_nodes, 10);

 

cpuset_wq = create_singlethread_workqueue("cpuset");

BUG_ON(!cpuset_wq);

}

首先,將top_cpuset的cpumemory節點設置成所有online的節點,之前初始化時還不知道有哪些online節點所以只是簡單設成所有,在smp初始化后就可以將其設成所有online節點了。然后加入了兩個hook函數,cpuset_track_online_cpuscpuset_track_online_nodes,這個兩個函數將在cpumemory熱插拔時被調用。

cpuset_track_online_cpus函數中調用scan_for_empty_cpusets函數掃描空的cpuset,並將其下的進程移到其非空的parent下,同時更新cpusetcpus_allowed信息。cpuset_track_online_nodes的處理類似。

cpuset又是怎么對進程的調度起作用的呢?

這個就跟task_struct中cpu_allowed字段有關了。首先,這個cpu_allowed和進程所屬的cpusetcpus_allowed保持一致;其次,在進程被fork出來的時候,進程繼承了父進程的cpusetcpus_allowed字段;最后,進程被fork出來后,除非指定CLONE_STOPPED標記,都會被調用wake_up_new_task喚醒,在wake_up_new_task中有:

cpu = select_task_rq(rq, p, SD_BALANCE_FORK, 0);

set_task_cpu(p, cpu);

即為新fork出來的進程選擇運行的cpu,而select_task_rq會調用進程所屬的調度器的函數,對於普通進程,其調度器是CFSCFS對應的函數是select_task_rq_fair。在select_task_rq_fair返回選到的cpu后,select_task_rq會對結果和cpu_allowed比較:

if (unlikely(!cpumask_test_cpu(cpu, &p->cpus_allowed) ||

     !cpu_online(cpu)))

cpu = select_fallback_rq(task_cpu(p), p);

這就保證了新fork出來的進程只能在cpu_allowed中的cpu上運行。

對於被wake up的進程來說,在被調度之前,也會調用select_task_rq選擇可運行的cpu

這就保證了進程任何時候都只會在cpu_allowed中的cpu上運行。

最后說一下,如何保證task_struct中的cpus_allowd和進程所屬的cpuset中的cpus_allowed一致。首先,在cpu熱插拔時,scan_for_empty_cpusets會更新task_struct中的cpus_allowed信息,其次對cpuset下的控制文件寫入操作時也會更新task_struct中的cpus_allowed信息,最后當一個進程被attach到其他cpuset時,同樣會更新task_struct中的cpus_allowed信息。

cpuset之前,Linux內核就提供了指定進程可以運行的cpu的方法。通過調用sched_setaffinity可以指定進程可以運行的cpuCpuset對其進行了擴展,保證此調用設定的cpu仍然在cpu_allowed的范圍內。在sched_setaffinity中,插入了這樣兩行代碼:

cpuset_cpus_allowed(p, cpus_allowed);

cpumask_and(new_mask, in_mask, cpus_allowed);

其中cpuset_cpus_allowed返回進程對應的cpuset中的cpus_allowed,cpumask_and則將cpus_allowed和調用sched_setaffinity時的參數in_mask相與得出進程新的cpus_allowed。

通過以上代碼的嵌入,Linux內核實現了對進程可調度的cpu的控制。下面我們來分析一下cpuset對memory節點的控制。

Linux中內核分配物理頁框的函數有6個:alloc_pages,alloc_page,__get_free_pages,__get_free_page,get_zeroed_page,__get_dma_pages,這些函數最終都通過alloc_pages實現,而alloc_pages又通過__alloc_pages_nodemask實現,在__alloc_pages_nodemask中,調用get_page_from_freelist從zone list中分配一個page,在get_page_from_freelist中調用cpuset_zone_allowed_softwall判斷當前節點是否屬於mems_allowed。通過附加這樣一個判斷,保證進程從mems_allowed中的節點分配內存。

Linux在cpuset出現之前,也提供了mbind, set_mempolicy來限定進程可用的內存節點。Cpuset子系統對其做了擴展,擴展的方法跟擴展sched_setaffinity類似,通過導出cpuset_mems_allowed,返回進程所屬的cupset允許的內存節點,對mbindset_mempolicy的參數進行過濾。

最后讓我們來看一下,cpuset子系統最重要的兩個控制文件:

{

.name = "cpus",

.read = cpuset_common_file_read,

.write_string = cpuset_write_resmask,

.max_write_len = (100U + 6 * NR_CPUS),

.private = FILE_CPULIST,

},

 

{

.name = "mems",

.read = cpuset_common_file_read,

.write_string = cpuset_write_resmask,

.max_write_len = (100U + 6 * MAX_NUMNODES),

.private = FILE_MEMLIST,

},

通過cpus文件,我們可以指定進程可以使用的cpu節點,通過mems文件,我們可以指定進程可以使用的memory節點。

這兩個文件的讀寫都是通過cpuset_common_file_read和cpuset_write_resmask實現的,通過private屬性區分。

在cpuset_common_file_read中讀出可用的cpumemory節點;在cpuset_write_resmask中則根據文件類型分別調用update_cpumaskupdate_nodemask更新cpumemory節點信息。

作者曰:cpuset子系統的具體實現其實很還有可以分析的,此處只是拋磚引玉而已。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM