linux进程管理之mm_struct,【转】Linux进程管理之SMP负载平衡(续二)
繼續來分析balance_tasks()函數,結合代碼中的注釋,理解這段代碼應該很容易,在這里主要分析它的兩個重要的子函數,即can_migrate_task()和pull_task().
先來看can_migrate_task().該函數用來判斷當前進程是否能夠遷移到目標cpu上,代碼如下:
static
int can_migrate_task(struct task_struct *p, struct rq *rq, int this_cpu,
struct sched_domain *sd, enum cpu_idle_type idle,
int *all_pinned)
{
/*
* We do not migrate tasks that are:
* 1) running (obviously), or
* 2) cannot be migrated to this CPU due to cpus_allowed, or
* 3) are cache-hot on their current CPU.
*/
/*如果進程不能在this_cpu上運行,不能遷移*/
if (!cpumask_test_cpu(this_cpu, &p->cpus_allowed)) {
schedstat_inc(p, se.nr_failed_migrations_affine);
return 0;
}
*all_pinned = 0;
/*如果進程正在運行,不能遷移*/
if (task_running(rq, p)) {
schedstat_inc(p, se.nr_failed_migrations_running);
return 0;
}
/*
* Aggressive migration if:
* 1) task is cache cold, or
* 2) too many balance attempts have failed.
*/
/*進程的cache是冷的,或者調度域load balance失敗次數太多了.
*可以遷移
*/
if (!task_hot(p, rq->clock, sd) ||
sd->nr_balance_failed > sd->cache_nice_tries) {
#ifdef CONFIG_SCHEDSTATS
if (task_hot(p, rq->clock, sd)) {
schedstat_inc(sd, lb_hot_gained[idle]);
schedstat_inc(p, se.nr_forced_migrations);
}
#endif
return 1;
}
/*如果進程的cache是熱的,不能遷移*/
if (task_hot(p, rq->clock, sd)) {
schedstat_inc(p, se.nr_failed_migrations_hot);
return 0;
}
return 1;
}
特別注意一下,如果是進程不能在目標CPU上運行,將不會更新*all_pinned的值.在該函數中,代碼中對task_hot()調用了兩次,顯然是值得優化的.
對task的Cache是否為hot是在task_hot()中判斷的,代碼如下:
static int
task_hot(struct task_struct *p, u64 now, struct sched_domain *sd)
{
s64 delta;
/*
* Buddy candidates are cache hot:
*/
/*如果進程是cfs_rq的next或者last指向,說明這是一個優先調度的進程
*Cache是熱的
*/
if (sched_feat(CACHE_HOT_BUDDY) &&
(&p->se == cfs_rq_of(&p->se)->next ||
&p->se == cfs_rq_of(&p->se)->last))
return 1;
/*不為CFS調度類,Cache是冷的*/
if (p->sched_class != &fair_sched_class)
return 0;
/*如果sysctl_sched_migration_cost為-1,進程Cache恒為
*熱,sysctl_sched_migration_cost為0,進程
*Cache恒為冷
*/
if (sysctl_sched_migration_cost == -1)
return 1;
if (sysctl_sched_migration_cost == 0)
return 0;
delta = now - p->se.exec_start;
/*如果進程開始執行的時間到當前時間的間隔小于sysctl_sched_migration_cost
*說明Cache是熱的*/
return delta < (s64)sysctl_sched_migration_cost;
}
就不對這個過程做詳細分析了,注釋中已經說的很清楚了.
pull_task()用來完在進程的遷移動作,代碼如下:
static void pull_task(struct rq *src_rq, struct task_struct *p,
struct rq *this_rq, int this_cpu)
{
/*從舊CPU上出列*/
deactivate_task(src_rq, p, 0);
/*更新進程的cpu指向*/
set_task_cpu(p, this_cpu);
/*在目標CPU上入列*/
activate_task(this_rq, p, 0);
/*
* Note that idle threads have a prio of MAX_PRIO, for this test
* to be always true for them.
*/
/*檢查目標CPU上是否需要搶占*/
check_preempt_curr(this_rq, p, 0);
}
由于更新了目標CPU上的進程,所以要檢查一下目標CPU上是否需要搶占.
3.2:cpu空閑時的load balance
在cpu空閑時,也會主動進行load balance的操作.如下代碼片段如示:
asmlinkage void __sched schedule(void)
{
......
......
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
......
......
}
在schedule()中,如果運行隊列為空,會調用idle_balance().
關于idle_balance()的操作,在這里就不再重復講述了,實際上,在之前的分析中,對CPU_NEWLY_IDLE類型的load balance關鍵地方都有指出.
對于CPU_NEWLY_IDLE與其它類型的load balace的差別主要有以下幾點:
1:CPU_NEWLY_IDLE只要發現CPU空閑就會調用,而無調整時間間隔,并且在CPU_NEWLY_IDLE的load
balance處理中,會將下次在tick中斷中進行load
balance的時間戳設為一個較小值,以便在tick中斷中較快速的發現這個不平衡狀態.
2: CPU_NEWLY_IDLE類型的load balance操作中移動較小量的進程,只需保證CPU上有進程運行即可.
3: CPU_NEWLY_IDLE是將其它CPU上的任務”拉”到本地CPU上.
四: migration線程
在load_balance()中,我們還看到,如果失敗次數大于sd->cache_nice_tries+2時,就會喚醒CPU的migration線程,我們來看一下該線程的運行.
先來看以下代碼:
static int __init migration_init(void)
{
void *cpu = (void *)(long)smp_processor_id();
int err;
/* Start one for the boot CPU: */
err = migration_call(&migration_notifier, CPU_UP_PREPARE, cpu);
BUG_ON(err == NOTIFY_BAD);
migration_call(&migration_notifier, CPU_ONLINE, cpu);
register_cpu_notifier(&migration_notifier);
return err;
}
early_initcall(migration_init);
在系統初始化時,migration_init()得到調用,并在該函數中注冊了一個cpu notifier鏈,因此就可以捕捉hotplug cpu信息,在該鏈的處理函數中,如以下代碼片段:
*/
static int __cpuinit
migration_call(struct notifier_block *nfb, unsigned long action, void *hcpu)
{
......
......
switch (action) {
case CPU_UP_PREPARE:
case CPU_UP_PREPARE_FROZEN:
p = kthread_create(migration_thread, hcpu, "migration/%d", cpu);
if (IS_ERR(p))
return NOTIFY_BAD;
kthread_bind(p, cpu);
/* Must be high prio: stop_machine expects to yield to it. */
rq = task_rq_lock(p, &flags);
__setscheduler(rq, p, SCHED_FIFO, MAX_RT_PRIO-1);
task_rq_unlock(rq, &flags);
cpu_rq(cpu)->migration_thread = p;
break;
......
......
}
從此可以看到,每個cpu UP時,都會為其創建并綁定一個migration線程,并將其設置為了SCHED_FIFO的實時進程,具有較高的優先級.
該線程的處理函數為migration_thread().代碼如下:
static int migration_thread(void *data)
{
int cpu = (long)data;
struct rq *rq;
rq = cpu_rq(cpu);
BUG_ON(rq->migration_thread != current);
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
struct migration_req *req;
struct list_head *head;
spin_lock_irq(&rq->lock);
/*如果該cpu已經離線了,跳轉到wait_to_die,等待退出*/
if (cpu_is_offline(cpu)) {
spin_unlock_irq(&rq->lock);
goto wait_to_die;
}
/*如果active_balance為1,表示該cpu上有load balance失敗的情況*/
if (rq->active_balance) {
active_load_balance(rq, cpu);
rq->active_balance = 0;
}
head = &rq->migration_queue;
/*如果rg->migration為空,睡眠,直至喚醒*/
if (list_empty(head)) {
spin_unlock_irq(&rq->lock);
schedule();
set_current_state(TASK_INTERRUPTIBLE);
continue;
}
/*從migration_queue中取得隊像,然后遷移進程
*一般在execve或者是在設置進程的所屬cpu的時候
*會有這個操作*/
req = list_entry(head->next, struct migration_req, list);
list_del_init(head->next);
spin_unlock(&rq->lock);
__migrate_task(req->task, cpu, req->dest_cpu);
local_irq_enable();
/*處理完了,喚醒進在等待的進程*/
complete(&req->done);
}
__set_current_state(TASK_RUNNING);
return 0;
wait_to_die:
/* Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
schedule();
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
}
4.1:active_load_balance()
先來看這個函數的第一個操作,即active_load_balance().該函數是處理load balance失敗的情況(在load_balance()中),代碼如下:
static void active_load_balance(struct rq *busiest_rq, int busiest_cpu)
{
int target_cpu = busiest_rq->push_cpu;
struct sched_domain *sd;
struct rq *target_rq;
/* Is there any task to move? */
/*如果繁忙隊列中只有一個可運行進程了,不用進行load balance了*/
if (busiest_rq->nr_running <= 1)
return;
target_rq = cpu_rq(target_cpu);
/*
* This condition is "impossible", if it occurs
* we need to fix it. Originally reported by
* Bjorn Helgaas on a 128-cpu setup.
*/
/*不可能出現繁忙隊列就是本地隊列的情況,因為在load balance時,找到的
*最繁忙調度組和最繁忙隊列都不是本地的*/
BUG_ON(busiest_rq == target_rq);
/* move a task from busiest_rq to target_rq */
double_lock_balance(busiest_rq, target_rq);
update_rq_clock(busiest_rq);
update_rq_clock(target_rq);
/* Search for an sd spanning us and the target CPU. */
/*找到目的cpu所在的域.在SMP中,只有一個基本調度哉*/
for_each_domain(target_cpu, sd) {
if ((sd->flags & SD_LOAD_BALANCE) &&
cpumask_test_cpu(busiest_cpu, sched_domain_span(sd)))
break;
}
/* 如果找到了要負載平衡的調度域*/
if (likely(sd)) {
schedstat_inc(sd, alb_count);
/*從繁忙隊列上遷移一個進程到目的cpu上*/
if (move_one_task(target_rq, target_cpu, busiest_rq,
sd, CPU_IDLE))
schedstat_inc(sd, alb_pushed);
else
schedstat_inc(sd, alb_failed);
}
double_unlock_balance(busiest_rq, target_rq);
}
從此可以看到,當load balance失敗的時候,只會從繁忙隊列中移動一個進程到目標cpu上.來看一下具體的遷移過程,即move_one_task(),該函數是以CPU_IDLE參數進行調用的.代碼如下:
static int move_one_task(struct rq *this_rq, int this_cpu, struct rq *busiest,
struct sched_domain *sd, enum cpu_idle_type idle)
{
const struct sched_class *class;
for (class = sched_class_highest; class; class = class->next)
if (class->move_one_task(this_rq, this_cpu, busiest, sd, idle))
return 1;
return 0;
}
從此即可以看出,直接調用調度類的move_one_task().在CFS中,該函數為move_one_task_fair().代碼如下:
static int
move_one_task_fair(struct rq *this_rq, int this_cpu, struct rq *busiest,
struct sched_domain *sd, enum cpu_idle_type idle)
{
struct cfs_rq *busy_cfs_rq;
struct rq_iterator cfs_rq_iterator;
cfs_rq_iterator.start = load_balance_start_fair;
cfs_rq_iterator.next = load_balance_next_fair;
for_each_leaf_cfs_rq(busiest, busy_cfs_rq) {
/*
* pass busy_cfs_rq argument into
* load_balance_[start|next]_fair iterators
*/
cfs_rq_iterator.arg = busy_cfs_rq;
if (iter_move_one_task(this_rq, this_cpu, busiest, sd, idle,
&cfs_rq_iterator))
return 1;
}
return 0;
}
在分析CFS組調度的時候,曾經分析過,CPU上的進程組都是掛在該cpu運行隊列的leaf_cfs_rq_list隊列上的,因此只需要遍歷該鏈表就可以遍歷該CPU上的進程組.
在后面用的迭代器是在之前已經分析過了的,這里不再贅述,流程轉入到iter_move_one_task():
static int
iter_move_one_task(struct rq *this_rq, int this_cpu, struct rq *busiest,
struct sched_domain *sd, enum cpu_idle_type idle,
struct rq_iterator *iterator)
{
struct task_struct *p = iterator->start(iterator->arg);
int pinned = 0;
while (p) {
if (can_migrate_task(p, busiest, this_cpu, sd, idle, &pinned)) {
pull_task(busiest, p, this_rq, this_cpu);
/*
* Right now, this is only the second place pull_task()
* is called, so we can safely collect pull_task()
* stats here rather than inside pull_task().
*/
schedstat_inc(sd, lb_gained[idle]);
return 1;
}
p = iterator->next(iterator->arg);
}
return 0;
}
只要該進程是可以與目標CPU關聯的,那么就調用pull_task()與之關聯,并且馬上返回.該函數中涉及到的子函數在前面都已經分析過了,這里就不做詳細分析了.
4.2: rq->migration_queue
接下來分析一下掛在rg->migration_queue中的對象的處理,首先我們得要知道是在什么情況下將對象掛到該鏈表上的.搜索kernel的代碼可發現,是在migrate_task()函數中,代碼如下:
static int
migrate_task(struct task_struct *p, int dest_cpu, struct migration_req *req)
{
struct rq *rq = task_rq(p);
/*
* If the task is not on a runqueue (and not running), then
* it is sufficient to simply update the task's cpu field.
*/
/*如果進程不處于運行狀態,不需要遷移到目標cpu的運行隊列中
*只需要將其關聯到目標cpu*/
if (!p->se.on_rq && !task_running(rq, p)) {
set_task_cpu(p, dest_cpu);
return 0;
}
/*初始化struct migration_req 結構,并將其鏈入進程所在cpu的migration_queue*/
init_completion(&req->done);
req->task = p;
req->dest_cpu = dest_cpu;
list_add(&req->list, &rq->migration_queue);
return 1;
}
該函數是將進程p移動到dest_cpu上.
同時,搜索kernel源代碼,發現有兩種情況下會調用migrate_task().如下示:
1:在更改進程所屬cpu時:
這種情況下,將進程遷移到新的CPU集上是理所當然的.如下代碼片段如示:
int set_cpus_allowed_ptr(struct task_struct *p, const struct cpumask *new_mask)
{
......
......
if (migrate_task(p, cpumask_any_and(cpu_online_mask, new_mask), &req)) {
/* Need help from migration thread: drop lock and wait. */
task_rq_unlock(rq, &flags);
wake_up_process(rq->migration_thread);
wait_for_completion(&req.done);
tlb_migrate_finish(p->mm);
return 0;
}
......
......
}
如示所示,new_mask表示進程p的新CPU集, cpumask_any_and(cpu_online_mask, new_mask)是指從cpu_online_mask和new_mask的交集中任選一個cpu(一般是序號最小的).
它調用migrate_task()將請求鏈入到migration_queu鏈表.然后喚醒該cpu上的migration線程,并且等待操作的完成.
2:在execev()時:
在下面的代碼片段中:
do_execve() à sched_exec():
void sched_exec(void)
{
int new_cpu, this_cpu = get_cpu();
/*找到相同調度域中負載最輕的CPU*/
new_cpu = sched_balance_self(this_cpu, SD_BALANCE_EXEC);
put_cpu();
/*如果當前CPU不是負載最輕的CPU,將進程遷移到負載最輕的CPU*/
if (new_cpu != this_cpu)
sched_migrate_task(current, new_cpu);
}
為什么要在execve()的時候調整所在的CPU呢?事實這時候調整CPU是最合適的,因為它此時占用的內存以及Cache損失是最小的.
Sched_balance_self()就是找到當前cpu所在調度域中的負載最輕的CPU.該函數跟我們之前分析的find_busiest_group()的邏輯差不多.這里不做分析了.
流程轉入到sched_migrate_task().代碼如下:
static void sched_migrate_task(struct task_struct *p, int dest_cpu)
{
struct migration_req req;
unsigned long flags;
struct rq *rq;
rq = task_rq_lock(p, &flags);
/*如果CPU不允許或者目標CPU已經離線了,退出*/
if (!cpumask_test_cpu(dest_cpu, &p->cpus_allowed)
|| unlikely(!cpu_active(dest_cpu)))
goto out;
/* force the process onto the specified CPU */
/*生成請求并且鏈入到migration_thread鏈表*/
if (migrate_task(p, dest_cpu, &req)) {
/* Need to wait for migration thread (might exit: take ref). */
struct task_struct *mt = rq->migration_thread;
get_task_struct(mt);
task_rq_unlock(rq, &flags);
wake_up_process(mt);
put_task_struct(mt);
wait_for_completion(&req.done);
return;
}
out:
task_rq_unlock(rq, &flags);
}
這個過程跟set_cpus_allowed_ptr()中的處理差不多,請自行結合代碼中的注釋進行分析.
接下來,我們來分析一下,到底migration線程怎么去處理這些請求.處理代碼如下:
migration_thread() à __migrate_task():
static int __migrate_task(struct task_struct *p, int src_cpu, int dest_cpu)
{
struct rq *rq_dest, *rq_src;
int ret = 0, on_rq;
if (unlikely(!cpu_active(dest_cpu)))
return ret;
rq_src = cpu_rq(src_cpu);
rq_dest = cpu_rq(dest_cpu);
double_rq_lock(rq_src, rq_dest);
/* Already moved. */
/*如果進程不在src_cpu上,可能已經遷移完成了.退出*/
if (task_cpu(p) != src_cpu)
goto done;
/* Affinity changed (again). */
/*如果進程不允許運行在des_cpu上,退出*/
if (!cpumask_test_cpu(dest_cpu, &p->cpus_allowed))
goto fail;
/*將進程遷移到目的cpu*/
on_rq = p->se.on_rq;
if (on_rq)
deactivate_task(rq_src, p, 0);
set_task_cpu(p, dest_cpu);
if (on_rq) {
activate_task(rq_dest, p, 0);
check_preempt_curr(rq_dest, p, 0);
}
done:
ret = 1;
fail:
double_rq_unlock(rq_src, rq_dest);
return ret;
}
這個過程很簡單,就是進程的遷移.請對照代碼自行分析,這里就不再贅述了.
五:cpuset中遺留的調度域問題
在分析cpuset子系統的時候,遇到了一個與調度域相關的接口partition_sched_domains().在本節中,來對它進行一個詳細的分析.代碼如下:
void partition_sched_domains(int ndoms_new, struct cpumask *doms_new,
struct sched_domain_attr *dattr_new)
{
int i, j, n;
int new_topology;
mutex_lock(&sched_domains_mutex);
/* always unregister in case we don't destroy any domains */
unregister_sched_domain_sysctl();
/* Let architecture update cpu core mappings. */
new_topology = arch_update_cpu_topology();
n = doms_new ? ndoms_new : 0;
/* Destroy deleted domains */
/*判斷當前系統中的調度域是否與要設置的調度域有相同的部份
*如有相同的部份,則這部份信息可以保存下來,不需要再次設置調度域*/
for (i = 0; i < ndoms_cur; i++) {
/*如果有相同的,繼續下一個*/
for (j = 0; j < n && !new_topology; j++) {
if (cpumask_equal(&doms_cur[i], &doms_new[j])
&& dattrs_equal(dattr_cur, i, dattr_new, j))
goto match1;
}
/* no match - a current sched domain not in new doms_new[] */
/*如果有不相同的,則需要對舊的調度域信息進行釋放*/
detach_destroy_domains(doms_cur + i);
match1:
;
}
/*如果doms_new == NULL,則必有ndoms_new == 1*/
/*如果doms_new == NULL,則取系統中除孤立CPU外的其它所有CPU,將其放至
*同一個調度域
*/
if (doms_new == NULL) {
ndoms_cur = 0;
doms_new = fallback_doms;
cpumask_andnot(&doms_new[0], cpu_online_mask, cpu_isolated_map);
WARN_ON_ONCE(dattr_new);
}
/* Build new domains */
/*構建立的調度域.同理,之前已經有的就不要再重建立了*/
for (i = 0; i < ndoms_new; i++) {
for (j = 0; j < ndoms_cur && !new_topology; j++) {
if (cpumask_equal(&doms_new[i], &doms_cur[j])
&& dattrs_equal(dattr_new, i, dattr_cur, j))
goto match2;
}
/* no match - add a new doms_new */
__build_sched_domains(doms_new + i,
dattr_new ? dattr_new + i : NULL);
match2:
;
}
/* Remember the new sched domains */
/*釋放資源,更新doms_cur,ndoms_cur等全局信息*/
if (doms_cur != fallback_doms)
kfree(doms_cur);
kfree(dattr_cur);?? /* kfree(NULL) is safe */
doms_cur = doms_new;
dattr_cur = dattr_new;
ndoms_cur = ndoms_new;
register_sched_domain_sysctl();
mutex_unlock(&sched_domains_mutex);
}
在這個函數中,會傳入三個參數,ndoms_new表示調度域的個數,doms_new表示每個調度域中的cpu成員,它是一個struct
mask數組,有ndoms_new項,dattr_new是每個調度域的屬性.關于調度域屬性在分析Cpuset的時候分析過了,這里就不再重復了.
在這里,有幾個全局量:
ndoms_cur:表示當前系統中的調度域個數
doms_cur:是當前各調度域中的CPU位圖
dattr_cur:是當前各調度域中的屬性
該接口的邏輯很清晰,而且里面核心的子函數__build_sched_domains()已經在前面詳細分析過了,所以這里就不再這個函數做過多的講解了.
六:小結
SMP負載平衡的過程有的地方還是很晦澀,比如shares值與h_load的調整過程.進程負載的計算過程以及對負載平衡條件的判斷也是一個理解的難點,不過,較2.6.9來說 ,邏輯還是清晰了不少.
總結
以上是生活随笔為你收集整理的linux进程管理之mm_struct,【转】Linux进程管理之SMP负载平衡(续二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HDFS文件导出本地合并为一个文件
- 下一篇: linux权限源码分析,Linux基础之