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字段用於task或cgroup獲取cpuset結構。
cpus_allowed和mems_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_setall和nodes_setall將top_cpuset能使用的cpu和內存節點設置成所有節點。緊接着,初始化fmeter,設置top_cpuset的load 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的cpu和memory節點設置成所有online的節點,之前初始化時還不知道有哪些online節點所以只是簡單設成所有,在smp初始化后就可以將其設成所有online節點了。然后加入了兩個hook函數,cpuset_track_online_cpus和cpuset_track_online_nodes,這個兩個函數將在cpu和memory熱插拔時被調用。
cpuset_track_online_cpus函數中調用scan_for_empty_cpusets函數掃描空的cpuset,並將其下的進程移到其非空的parent下,同時更新cpuset的cpus_allowed信息。cpuset_track_online_nodes的處理類似。
那cpuset又是怎么對進程的調度起作用的呢?
這個就跟task_struct中cpu_allowed字段有關了。首先,這個cpu_allowed和進程所屬的cpuset的cpus_allowed保持一致;其次,在進程被fork出來的時候,進程繼承了父進程的cpuset和cpus_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會調用進程所屬的調度器的函數,對於普通進程,其調度器是CFS,CFS對應的函數是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可以指定進程可以運行的cpu。Cpuset對其進行了擴展,保證此調用設定的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允許的內存節點,對mbind,set_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中讀出可用的cpu或memory節點;在cpuset_write_resmask中則根據文件類型分別調用update_cpumask和update_nodemask更新cpu或memory節點信息。
作者曰:cpuset子系統的具體實現其實很還有可以分析的,此處只是拋磚引玉而已。