Linux fork之后,到底是子進程先運行還是父進程先運行【轉】


轉自:https://blog.csdn.net/dog250/article/details/105756168

大約10年前,我寫過兩篇關於Linux內核CFS調度器的文章:
https://blog.csdn.net/dog250/article/details/5302865
https://blog.csdn.net/dog250/article/details/5302864

我覺得這兩篇文章是垃圾,但我又不刪,留着給自己噴吧!

不就是一個內核參數 kernel.sched_child_runs_first 嗎?在今天看來,驗證它是否起作用實在太簡單了。

首先解釋一下 為什么要子進程先運行 。

因為fork的行為造成了后續的COW(copy on write),一般而言子進程會調用exec而替換掉需要COW的地址空間,子進程先運行可以避免不必要的COW開銷。

那么對於CFS調度器而言,kernel.sched_child_runs_first是否有作用呢?我們試一下便知道,依然使用那兩篇垃圾文章中的例子:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int v = atoi(argv[1]);
    printf("%d\n", getpid());
    nice(v);
    int i = 90000;
    while (i-->0) {
        v++;
    }

    if(fork() == 0) {
        printf("sub\n");
    }
    printf("main,%d\n",v);
}


我們設置好內核參數后,看看到底哪個先打印出來:

[root@localhost test]# sysctl -w kernel.sched_child_runs_first=1
kernel.sched_child_runs_first = 1
[root@localhost test]#
[root@localhost test]# ./a.out 10
5101
main,90010
[root@localhost test]# sub
[root@localhost test]# ./a.out -10
5105
sub
main,89990
[root@localhost test]# ./a.out -10
5108
main,89990
[root@localhost test]# sub
[root@localhost test]# ./a.out -10
5112
main,89990
[root@localhost test]# sub
[root@localhost test]# ./a.out 10
5117
main,90010
[root@localhost test]# sub


不用試了,它不起作用,不管你有沒有設置START_DEBIT這個feature!它和START_DEBIT根本沒有關系,dog250在2010年寫的那些東西故弄玄虛,把簡單問題復雜化!還扯什么START_DEBIT,還扯什么統計概覽,真是無中生有,垃圾啊垃圾。

正確的排查問題的方法完全就不是這個思路!

現在,我來展示正確的做法。在實驗之前,澄清一個事實, 不要用printf來確認到底誰先運行! 因為printf太復雜了,執行它的周期太久,有可能雖然子進程先運行但卻是父進程先打印出來。

所以,我用exit:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int v = atoi(argv[1]);
    printf("%d\n", getpid());
    nice(v);

    if(fork() == 0) {
        exit(0);
    }
    exit(0);
}


我們用操作系統的方式去觀測,而不是用printf,這次,我們用stap:

#!/usr/bin/stap -g

global g_se;
global g_cfs_rq;

probe begin {
    g_cfs_rq = 0;
    g_se = 0;
}

probe kernel.function("__schedule")
{
    t_curr = task_current();
    if (task_execname(t_curr) == "a.out")
        printf("[_schedule]  current task: %s[%d]\n", task_execname(t_curr), task_pid(t_curr));
}

probe kernel.function("do_exit")
{
    t_curr = task_current();
    if (task_execname(t_curr) == "a.out")
        printf("Exit task: %s[%d]\n", task_execname(t_curr), task_pid(t_curr));
}

probe kernel.function("pick_next_task_fair")
{
    g_cfs_rq = &$rq->cfs;
}

function container_of_entity:long(se:long)
{
    offset = &@cast(0, "struct task_struct")->se;
    return se - offset;
}

probe kernel.function("pick_next_task_fair").return
{
    if($return != 0) {
        se = &$return->se;
        t_se = container_of_entity(se);
        t_curr = task_current();
        if (task_execname(t_se) == "a.out" || task_execname(t_curr) == "a.out") {
            printf("[pick_next_task_fair] Return task: %s[%d]  From current: %s[%d]\n", task_execname(t_se), task_pid(t_se), task_execname(t_curr), task_pid(t_curr));
        }
    }
}


probe kernel.function("wake_up_new_task")
{
    g_se = &$p->se;
    g_cfs_rq = @cast(g_se, "struct sched_entity")->cfs_rq;
}

probe kernel.function("wake_up_new_task").return
{
    t_se = container_of_entity(g_se);
    tname = task_execname(t_se);
    vruntime = @cast(g_se, "struct sched_entity")->vruntime;
    if (tname == "a.out") {
        curr = @cast(g_cfs_rq, "struct cfs_rq")->curr;
        t_curr = container_of_entity(curr);
        curr_vruntime = @cast(curr, "struct sched_entity")->vruntime;
        printf("[wake_up_new_task] current:[%s][%d]  curr:%d  new:%d del:%d\n",
                task_execname(t_curr), task_pid(t_curr), curr_vruntime, vruntime,
                curr_vruntime - vruntime);
    }
    g_se = 0;
    g_cfs_rq = 0;
}

probe kernel.function("place_entity")
{
    t_initial = $initial;
    if (t_initial == 1) {
        g_cfs_rq = $cfs_rq;
        g_se = $se;
    }
}
probe kernel.function("place_entity").return
{
    if (g_se) {
        t_se = container_of_entity(g_se);
        tname = task_execname(t_se);
        vruntime = @cast(g_se, "struct sched_entity")->vruntime;
        if (tname == "a.out") {
            curr = @cast(g_cfs_rq, "struct cfs_rq")->curr;
            t_curr = container_of_entity(curr);
            curr_vruntime = @cast(curr, "struct sched_entity")->vruntime;
            printf("[place_entity] name:[%s][%d]  curr:%d  new:%d   delta:%d\n",
                task_execname(t_curr), task_pid(t_curr), curr_vruntime, vruntime,
                curr_vruntime - vruntime);
        }
        g_se = 0;
        g_cfs_rq = 0;
    }
}


執行它,然后運行多次a.out,到底發生了什么,你就徹底知道了,下面是一個結果:

[root@localhost test]# ./a.out 10
5653
main,90010
[root@localhost test]# sub

# 另一個終端上打印的stap信息
[_schedule]  current task: a.out[5653]
[pick_next_task_fair] Return task: a.out[5653]  From current: a.out[5653]
# 父進程fork子進程,並設置了它的初始vruntime。
# 后續的child runs first檢查會resched current
[place_entity] name:[a.out][5653]  curr:74161009564  new:74192039854   delta:-31030290
# 注意,這里在fork中發生了切換,why??因為在fork中spin_unlock的時候會check resched!
# 這就發生了task_fork_fair最后釋放rq lock時!
[_schedule]  current task: a.out[5653]
[pick_next_task_fair] Return task: rcu_sched[10]  From current: a.out[5653]
[pick_next_task_fair] Return task: a.out[5653]  From current: sshd[1392]
# 父進程返回運行,wakeup子進程,然而vruntime的delta卻不足一個granularity!
# 不足一個granularity,子進程無法搶占父進程!
# 換句話說,之前由於child runs first進行的resched已經失效!
[wake_up_new_task] current:[a.out][5653]  curr:74192443179  new:74192132619 del:310560
# 依然是父進程先運行。
Exit task: a.out[5653]
[_schedule]  current task: a.out[5653]
[pick_next_task_fair] Return task: rcu_sched[10]  From current: a.out[5653]
[pick_next_task_fair] Return task: a.out[5654]  From current: sshd[1392]
# 子進程被調度運行
Exit task: a.out[5654]


很尷尬的事發生了,為了child runs first而執行resched_task,需要lock住rq,之所以要resched_task可能是交換父子的vruntime之后,希望子進程繼續運行下去,替換父進程。

然而unlock rq時的check preempt卻白白消耗了這次resched的機會!為什么說白白消耗呢?因為調用sched_fork的時候,子進程尚未准備好,也就是說,它尚不足以被wakeup!

只要在rq unlock時check preempt時候,父進程被其它進程搶占(在父進程優先級低時更容易發生!!),那么子進程大概率不會runs first了,因為后面還要check granularity!

如果我們用更小的nice值運行a.out,那么子進程還是有機會runs first的,因為在rq unlock的時候,父進程不容易被其它進程搶占進而消費掉resched的機會,留到后面wakeup child的時候,還可以使用,此時子進程已經擁有了執行的條件,進而搶占掉父進程!

或者說,即便這次搶占父進程失敗,那么它的vruntime已經低於父進程,它在紅黑樹中的位置是比父進程更加leftmost的,終究還是要比父進程runs first。

好了,情景分析完畢,該解題了!如何讓kernel.sched_child_runs_first如其意之所表達,真正做到child runs first呢?

2010年dog250寫的那個patch是錯的,沒有意義。真正的解法是:

    在wakeup child的時候再check sched_child_runs_first,進而resched。

我們來驗證一下,由於我懶得為這個重新編譯一遍內核,所以我采用stap guru hook的方式來玩玩。

首先我們廢除原始的sched_child_runs_first判斷,這很容易,關掉這個開關即可:

[root@localhost test]# sysctl -w kernel.sched_child_runs_first=0
kernel.sched_child_runs_first = 0

    1
    2

然后,我們以guru模式運行下面的stap腳本:

#!/usr/bin/stap -g

global g_p;

probe begin {
    g_p = 0;
}

%{
static void *(*_resched_task)(struct task_struct *p);
%}

function resched(tsk:long, tskp:long)
%{
    struct task_struct *task = NULL, *parent = NULL;
    struct sched_entity *pse = NULL, *cse = NULL;

    task = (struct task_struct *)STAP_ARG_tsk;
    parent = (struct task_struct *)STAP_ARG_tskp;
    cse = &task->se;
    pse = &parent->se;

    if (_resched_task == NULL)
        _resched_task = (void *)kallsyms_lookup_name("resched_task");
    if (_resched_task && pse->vruntime < cse->vruntime) {
        swap(pse->vruntime, cse->vruntime);
        STAP_PRINTF("---[%lu]------[%lu]-------\n", pse->vruntime, cse->vruntime);
        _resched_task(current);
    }

%}

probe kernel.function("check_preempt_wakeup")
{
    g_p = $p;
}
// 這里的trick在於,由於我們的父子a.out都是純CPU型的,只在創建時被wakeup一次,所以hook該點。
probe kernel.function("check_preempt_wakeup").return
{
    parent = @cast(g_p, "struct task_struct")->parent;
    // 這里過濾掉了除了我們的fork場景之外的所有其它的wakeup場景。
    if (task_execname(g_p) == "a.out" || task_execname(parent) == "a.out") {
        resched(g_p, parent);
    }
    g_p = 0;
}


來吧,執行之!為了觀測效果,我們可以再次同時執行之前的腳本(hook不要沖突即可):

[root@localhost test]# ./a.out 10
6988
sub
main,90010

# 以下是輸出
[_schedule]  current task: a.out[6988]
[pick_next_task_fair] Return task: kworker/0:0[5380]  From current: a.out[6988]
[pick_next_task_fair] Return task: a.out[6988]  From current: sshd[1392]
[_schedule]  current task: a.out[6988]
[pick_next_task_fair] Return task: rcu_sched[10]  From current: a.out[6988]
[pick_next_task_fair] Return task: a.out[6988]  From current: sshd[1392]
[place_entity] name:[a.out][6988]  curr:78347075684  new:78440166555   delta:-93090871
# 在place_entity和wake_up_new_task之間沒有被打斷!
# 因為把resched從place移動到了wakeup的時候。
[wake_up_new_task] current:[a.out][6988]  curr:78440166555  new:78347480815 del:92685740
[_schedule]  current task: a.out[6988]
[pick_next_task_fair] Return task: a.out[6989]  From current: a.out[6988]
# 子進程runs first,優先退出!
Exit task: a.out[6989]
[_schedule]  current task: a.out[6989]
[pick_next_task_fair] Return task: kworker/0:0[5380]  From current: a.out[6989]
[pick_next_task_fair] Return task: a.out[6988]  From current: sshd[1392]
[_schedule]  current task: a.out[6988]
[pick_next_task_fair] Return task: kworker/0:0[5380]  From current: a.out[6988]
[pick_next_task_fair] Return task: a.out[6988]  From current: sshd[1392]
# 父進程在后
Exit task: a.out[6988]
[_schedule]  current task: a.out[6988]
[pick_next_task_fair] Return task: systemd[1]  From current: a.out[6988]


多試幾次,還是這樣的結果。

現在,是時候回到printf了,雖然它可能並不准,但是肉眼觀測,讓經理信服,也只能靠它了:

[root@localhost test]# ./a.out 18
sub
main,90018
[root@localhost test]# ./a.out -18
sub
main,89982
[root@localhost test]# ./a.out -10
sub
main,89990
[root@localhost test]# ./a.out 10
sub
main,90010
[root@localhost test]# ./a.out 1
sub
main,90001
[root@localhost test]# ./a.out -1
sub
main,89999
[root@localhost test]# ./a.out 0
sub
main,90000


咋試咋舒服!

好了,現在該出patch了。這個patch才是真的有效的:

Date: Thu, 23 Apr 2020 22:42:07 +0800
Subject: [PATCH] fix child runs first

---
 kernel/sched/fair.c | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 6a33137..f7f83a3 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -4564,6 +4564,17 @@ static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_
     int scale = cfs_rq->nr_running >= sched_nr_latency;
     int next_buddy_marked = 0;

+    if ((wake_flags&WF_FORK) && sysctl_sched_child_runs_first && se &&
+        entity_before(se, pse)) {
+        /*
+         * Upon rescheduling, sched_class::put_prev_task() will place
+         * 'current' within the tree based on its new key value.
+         */
+        swap(se->vruntime, pse->vruntime);
+        resched_task(curr);
+    }
+
+
     if (unlikely(se == pse))
         return;

@@ -7086,15 +7097,6 @@ static void task_fork_fair(struct task_struct *p)
         se->vruntime = curr->vruntime;
     place_entity(cfs_rq, se, 1);

-    if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
-        /*
-         * Upon rescheduling, sched_class::put_prev_task() will place
-         * 'current' within the tree based on its new key value.
-         */
-        swap(curr->vruntime, se->vruntime);
-        resched_task(rq->curr);
-    }
-
     se->vruntime -= cfs_rq->min_vruntime;

     raw_spin_unlock_irqrestore(&rq->lock, flags);
--
1.8.3.1


對了,為了讓這個patch不是真正可以打入內核的,我特意在老舊的內核上制作了這個patch。真正手藝人的玩法永遠不是制作真正的patch,二進制hook不好嗎?哈哈!

浙江溫州皮鞋濕,下雨進水不會胖。
————————————————
版權聲明:本文為CSDN博主「dog250」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/dog250/article/details/105756168


免責聲明!

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



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