• <dd id="3gzlp"></dd>

    <li id="3gzlp"><acronym id="3gzlp"></acronym></li>

    <span id="3gzlp"></span>

    云計算時代,容器底層 cgroup 如何使用

    編輯部的故事 發布于 06/08 17:59
    閱讀 3K+
    收藏 21

    面對海量數據,你能否從容應對?>>>

    作者:姜亞華(@二如公子 ),《精通 Linux 內核——智能設備開發核心技術》的作者,一直從事與 Linux 內核和 Linux 編程相關的工作,研究內核代碼十多年,對多數模塊的細節如數家珍。曾負責華為手機 Touch、Sensor 的驅動和軟件優化(包括 Mate、榮耀等系列),以及 Intel 安卓平臺 Camera 和 Sensor 的驅動開發(包括 Baytrail、Cherrytrail、Cherrytrail CR、Sofia 等)。現負責 DMA、Interrupt、Semaphore 等模塊的優化與驗證(包括 Vega、Navi 系列和多款 APU 產品)。

    往期回顧:點擊查看

    一、cgroup 的使用

    測試環境版本與之前一致:

    Ubuntu

    (lsb_release -a)

    Distributor ID: Ubuntu

    Description:    Ubuntu 19.10

    Release:        19.10

    Linux

    (uname -a)

    Linux yahua 5.5.5 #1 SMP … x86_64 x86_64 x86_64 GNU/Linux

    前面的文章中,我們探討了容器底層 cgroup 的數據結構代碼實現,本期是 cgroup 系列的最后一篇文章,我們將繼續探討在 mount 成功后,我們如何使用 cgroup 來實現進程限制。

    在 mount 成功后,cgroup_root 已經存在了,也就是說 cgroup 層級結構已經搭建好了,接下來我們就可以使用 cgroup 了。

    1. cgroup 的 mkdir

    mkdir 比 mount 的過程稍簡單,由 cgroup_mkdir 函數實現,主要邏輯如下:

    int cgroup_mkdir(struct kernfs_node *parent_kn, const char *name, umode_t mode)
    {
    	struct cgroup *parent, *cgrp;
    
    
    	parent = cgroup_kn_lock_live(parent_kn, false);    //1
    
    
    	cgrp = cgroup_create(parent, name, mode);    //2
    
    
    	ret = cgroup_kn_set_ugid(cgrp->kn);
    
    
    	ret = css_populate_dir(&cgrp->self);    //3
    	ret = cgroup_apply_control_enable(cgrp);
    	kernfs_activate(cgrp->kn);
    
    
    	ret = 0;
    	return ret;
    }

     第 1 步,獲得父目錄對應的 cgroup。無論是 cgroup_setup_root 還是接下來要說的cgroup_create,在創建文件的時候都將 cgroup 賦值給了 kernfs_node 的 priv。所以這里其實就是返回 parent_kn->priv 字段,不過要經過參數檢查。

    第 2 步,調用 cgroup_create:創建 cgroup,調用 kernfs_create_dir 創建目錄,建立新cgroup 和父 cgroup 的父子關系。

    第 3 步,和 mount 的時候一樣,css_populate_dir 和 cgroup_apply_control_enable 會為我們創建 cftype 對應的文件,不過有兩點區別:

    首先,帶 CFTYPE_ONLY_ON_ROOT 標志的 cftype 不會出現在這里,比如cgroup.sane_behavior 和 release_agent。

    其次,mount 的時候,新 cgroup_root.cgrp 復用了原 cgroup_root.cgrp 相關的css(rebind_subsystems,第二篇),這里新建了一個 cgroup,cgroup_apply_control_enable 需要為我們創建新的 css(ss->css_alloc(parent_css))并建立 cgroup 和 ss 的多對多關系(init_and_link_css和online_css)。 

    mount 的時候,cpuset 的 css_alloc 返回的是全局的 top_cpuset.css,這里創建一個新的 cpuset 對象并初始化,如下:

    struct cgroup_subsys_state *
    cpuset_css_alloc(struct cgroup_subsys_state *parent_css)
    {
    struct cpuset *cs;
    
     if (!parent_css)    //mount的時候,返回top_cpuset.css
      return &top_cpuset.css;
    
     cs = kzalloc(sizeof(*cs), GFP_KERNEL);
    alloc_cpumasks(cs, NULL);    //#1
    
     set_bit(CS_SCHED_LOAD_BALANCE, &cs->flags);
    nodes_clear(cs->mems_allowed);
    nodes_clear(cs->effective_mems);    //#2
    fmeter_init(&cs->fmeter);
    cs->relax_domain_level = -1;
    
     return &cs->css;
    }

    注意標號 #1 和 #2,新 cs 的 cpus_allowed 和 mems_allowed 都被清零,此時讀取cpuset.cpus 和 cpuset.mems 也是沒有內容的,也就是說對 cpu 和 memory 的限制并不能從父目錄繼承,在使用前必須正確設置它們。

    2. 限制資源

    我們在第一篇的例子中通過 echo 0-2 > cpuset.cpus 和 echo 0 > cpuset.mems 限制 /cpuset0 管理的進程使用的 cpu 和 memory node,以 cpuset.cpus 為例,它的 cftype 如下:

    {
      .name = "cpus",
      .seq_show = cpuset_common_seq_show,
      .write = cpuset_write_resmask,
      .max_write_len = (100U + 6 * NR_CPUS),
      .private = FILE_CPULIST,
    },

    最終調用的是 cpuset_write_resmask,后者調用 update_cpumask。

    update_cpumask 的目的是更新我們在 mkdir 時創建的 cpuset(cpuset_css_alloc),當然了,之前已經配置過的 cpuset 重新配置也可以。我們關心以下幾點:

    1.  不能更改top_cpuset的設置,這就是第一篇的課堂作業第一題的答案。
    2. 目標cpuset的資源必須是父目錄cpuset的子集,而且是子目錄cpuset的超集(由validate_change函數實現),這是課堂作業第二題的答案。
    3. 配置的資源最終更新cpuset的cpus_allowed字段。

     可以看到,類似課堂作業中描述的類似限制,是需要 ss 自行實現的,cgroup 本身并不保證這點,嘗試開發新的 ss 的時候需要注意這點。

    3. 管理進程

    我們在例子中將進程號寫到 tasks 文件(echo $$ > tasks),以限制進程只能使用 /cpuset0 配置的 cpu 和 memory node。實際上,寫 cgroup.procs 文件也是可以的。它們的 cftype 文件定義如下:

    {
      .name = "tasks",
      .seq_start = cgroup_pidlist_start,
      .seq_next = cgroup_pidlist_next,
      .seq_stop = cgroup_pidlist_stop,
      .seq_show = cgroup_pidlist_show,
      .private = CGROUP_FILE_TASKS,
      .write = cgroup1_tasks_write,
    },
    {
      .name = "cgroup.procs",
      .seq_start = cgroup_pidlist_start,
      .seq_next = cgroup_pidlist_next,
      .seq_stop = cgroup_pidlist_stop,
      .seq_show = cgroup_pidlist_show,
      .private = CGROUP_FILE_PROCS,
      .write = cgroup1_procs_write,
    },

    兩個文件的 write 分別是 cgroup1_tasks_write 和 cgroup1_procs_write,它們都是調用__cgroup1_procs_write 實現的,區別僅在于最后一個參數 threadgroup 不同,前者為false,后者為 true。看名字就知道,為 false 的情況下,僅作用于目標進程(線程),為 true 的情況下,作用于線程組。

    這里對線程組稍作說明。線程組是屬于同一個進程的線程的集合,同一個線程組的線程,它們的 task_struct 都通過 thread_group 字段鏈接到同一個鏈表中,鏈表的頭為線程組領導進程的 task_struct 的 thread_group 字段,可以據此來遍歷線程組。

    __cgroup1_procs_write 可以分成以下 3 步:

    第1步,調用 cgroup_kn_lock_live 獲得文件所在的目錄的 cgroup,實際上就是kernfs_node->parent->priv,kernfs_node->parent 是文件所在目錄的 kernfs_node,priv 就是目標 cgroup。

    第2步,調用 cgroup_procs_write_start 根據用戶空間傳遞的進程 id 參數獲得目標進程的 task_struct,threadgroup 為 true 的情況下,獲得的是線程組領導進程的task_struct。

    第3步,調用 cgroup_attach_task 將進程 attach 到(依附于或者連接)cgroup。

    cgroup 和 ss 之間是對等的關系,使用的是 bind,稱之為綁定;進程和 cgroup 之間并不是對等的關系,使用的是 attach,稱之為依附。 

    請注意,我們舉例中僅涉及 cpuset,并不意味著某個進程只與 cpuset 有關,進程和cgroup 的關系是通過 css_set 實現的,也就是說是一組 cgroup。我們沒有更改其他cgroup 層級結構的配置,這意味著進程關聯的是它們的 cgroup_root,并不是沒有關聯。

    先不論進程被創建后,“輾轉”了幾組cgroup,進程被創建時就已經attach cgroup了。

    進程創建的過程在書里已經詳細地分析過了,這里僅討論與cgroup相關的部分。

    首先被調用的是cgroup_fork,如下:

    void cgroup_fork(struct task_struct *child)
    {
    RCU_INIT_POINTER(child->cgroups, &init_css_set);
    INIT_LIST_HEAD(&child->cg_list);
    }

    直接指向了 init_css_set,不過這有可能是暫時的。child->cg_list 是空的,說明新進程還沒有 attach 到任何 cgroup。

    其次是 cgroup_can_fork,它調用 ss->can_fork,由 ss 判斷是否可以創建新進程,如果答案是否,整個 fork 會失敗。

    最后是 cgroup_post_fork,做最后的調整,主要邏輯如下:

    void cgroup_post_fork(struct task_struct *child)
    {
    struct cgroup_subsys *ss;
    struct css_set *cset;
    
     if (likely(child->pid)) {
      WARN_ON_ONCE(!list_empty(&child->cg_list));
      cset = task_css_set(current); /* current is @child's parent */
      get_css_set(cset);
      cset->nr_tasks++;
      css_set_move_task(child, NULL, cset, false);
    }
    
     do_each_subsys_mask(ss, i, have_fork_callback) {
      ss->fork(child);
    } while_each_subsys_mask();
    }

    首先,current 是新進程 child 的父進程,先獲得父進程的 css,然后調用css_set_move_task 將新進程轉移到該 css 上。css_set_move_task 的第二個參數是原css,這里是 NULL 是因為還沒有 attach 到任何 cgroup(css_set)上。css_set_move_task 會將 child->cg_list 插入 css->tasks 鏈表上,child->cg_list 不再為空。

    也就是說,新進程在創建時會被 attach 到與父進程同一組 cgroup 上。

    其次,如果 ss 定義了 fork,調用 ss->fork,以 cpuset 為例,它會為新進程復制父進程的設置,如下:

    void cpuset_fork(struct task_struct *task)
    {
    if (task_css_is_root(task, cpuset_cgrp_id))
      return;
    
     set_cpus_allowed_ptr(task, current->cpus_ptr);
    task->mems_allowed = current->mems_allowed;
    }

    回顧下第一篇的例子,我們在 cpuset 下創建的 cpuset0 目錄,配置資源,管理進程。修改下,在 cpuset 下再創建一個 cpuset1 目錄,進程先 attach 到 /cpuset0,然后migrate 到 /cpuset1上,以此為例分析 migrate 的過程:

    love_cc@yahua:/sys/fs/cgroup/cpuset$ sudo mkdir cpuset0
    love_cc@yahua:/sys/fs/cgroup/cpuset$ sudo mkdir cpuset1
    love_cc@yahua:/sys/fs/cgroup/cpuset$ cd cpuset0/
    root@yahua:/sys/fs/cgroup/cpuset/cpuset0# echo 0-2 > cpuset.cpus
    root@yahua:/sys/fs/cgroup/cpuset/cpuset0# echo 0 > cpuset.mems
    root@yahua:/sys/fs/cgroup/cpuset/cpuset0# echo $$ > tasks
    root@yahua:/sys/fs/cgroup/cpuset/cpuset0# cat tasks
    2682
    2690
    root@yahua:/sys/fs/cgroup/cpuset/cpuset0# cd ../cpuset1/
    root@yahua:/sys/fs/cgroup/cpuset/cpuset1# echo 0-1 > cpuset.cpus
    root@yahua:/sys/fs/cgroup/cpuset/cpuset1# echo 0 > cpuset.mems
    root@yahua:/sys/fs/cgroup/cpuset/cpuset1# echo $$ > tasks
    root@yahua:/sys/fs/cgroup/cpuset/cpuset1# cat tasks
    2682
    2713
    root@yahua:/sys/fs/cgroup/cpuset/cpuset1# cat ../cpuset0/tasks
    #沒有省略內容,空的

    繼續討論之前,先理一下目前的狀況,我們在 __cgroup1_procs_write 函數的第 3 步cgroup_attach_task,之前的兩步我們已經獲得了目標 cgroup(也就是 /cpuset1)和進程的 task_struct。

    cgroup_attach_task 的目的是將進程 attach 到目標 cgroup,邏輯上至少包括進程和原group detach 和進程和目標 cgroup attach 兩部分。三個要素,src、dst 和 migrate,正好對應三個函數 cgroup_migrate_add_src、cgroup_migrate_prepare_dst 和cgroup_migrate

    首先被調用的是 cgroup_migrate_add_src,threadgroup 為 true 的情況下,對線程組的每個線程調用一次,否則調用一次即可,它的主要邏輯如下:

    void cgroup_migrate_add_src(struct css_set *src_cset,
           struct cgroup *dst_cgrp,
           struct cgroup_mgctx *mgctx)
    {
    struct cgroup *src_cgrp;
    
     src_cgrp = cset_cgroup_from_root(src_cset, dst_cgrp->root);
    
     src_cset->mg_src_cgrp = src_cgrp;
    src_cset->mg_dst_cgrp = dst_cgrp;
    get_css_set(src_cset);
    list_add_tail(&src_cset->mg_preload_node, &mgctx->preloaded_src_csets);

    第一個參數 src_cset 表示進程原來的 css_set,也就是 task_struct 的 cgroups 字段。

    首先要做的就是在目標 cgroup(dst_cgrp,也就是 /cpuset1)所屬的 cgroup_root 中找到原 cgroup(src_cgrp,也就是 /cpuset0),它跟目標 cgroup 屬于同一個cgroup_root,查找的過程就變成找到 src_cset 對應的某個 cgroup,它的 root 字段與dst_cgrp->root 相等,如下:

    list_for_each_entry(link, &cset->cgrp_links, cgrp_link) {
      struct cgroup *c = link->cgrp;
    
      if (c->root == root) {
       res = c;    //res就是我們要找的
       break;
      }
    }

    提醒下,任何一個 cgroup 層級結構中,進程只能關聯其中一個 cgroup,所以與 /cpuset1 屬于同一個 cgroup_root 的只能是 /cpuset0 。

    另外,我們分析的只是一種情況,前面說的 Ubuntu mount cpuset 的時候,進程從默認的層級結構遷移到 cpuset 上,原 cgroup 和目標 cgroup 實際上屬于不同的cgroup_root,返回的是目標 cgroup_root 的 cgrp。

    cgroup_migrate_add_src 的第三個參數 mgctx 是 cgroup_attach_task 的局部變量,函數結束前將 src_cset 插入到 mgctx->preloaded_src_csets 等待后續處理。

    cgroup_migrate_prepare_dst 遍歷 mgctx->preloaded_src_csets上的 src_cset,根據src_cset 和 src_cset->mg_dst_cgrp 查找當前已經存在的 css_set 是否有某個 css_set與期望一致,沒有則創建新的 css_set 并賦以期望值。

    期望,一致,兩方面。

    怎么描述我們的期望呢,進程只是從 /cpuset0 移到 /cpuset1 上,關聯的其他 cgroup 層級結構的 cgroup 并沒有變化,所以以原 css_set 作為模板,調整 cpuset 層級結構上的css 即可,實際的代碼也大致如此,如下:

    for_each_subsys(ss, i) {
      if (root->subsys_mask & (1UL << i)) {
       template[i] = cgroup_e_css_by_mask(cgrp, ss);
      } else {
       template[i] = old_cset->subsys[i];
      }
    }

    root 就是發生變動的層級結構的 cgroup_root,在我們的例子中就是 cpuset,至于cgroup_e_css_by_mask,這里的 e 是 effective,不考慮 cgroup v2 的情況下,也可以理解為 cgroup_css(cgrp, ss),也就是 /cpuset1 和 cpuset ss 對應的 css 。

    某個 css_set(簡稱 cset)與我們的期望一致,需要滿足以下兩點。

    首先,cset->subsys 與 template 一致,其實還是與 v2 有關。

    其次,cset 的 css(cgrp_links字段)中,屬于當前 cgroup_root 的,關聯的 cgroup 是目標值,也就是 /cpuset1;不屬于當前 cgroup_root 的,與 old_cset 關聯的 cgroup 相等。

    css_set 的 subsys 和 cgrp_links 都表示它關聯的 css,二者有什么區別?subsys 在css_set 被創建后不會改變,cgrp_links 可以動態調整。比如 cgroup_setup_root 中調用的 link_css_set,修改的只是 cgrp_links。

    如果找不到一致的 css_set,創建一個新的,按照要求的兩點給它賦值。

    接下來就是 cgroup_migrate 了,它的實現代碼較多,但邏輯都是直來直去,我們就不直接分析代碼了,主要分以下幾步:

    1. 調用 cgroup_migrate_add_task 將需要遷移的進程放入 mgctx->tset,然后調用cgroup_migrate_execute 函數,實際的 migrate 過程由它完成。
    2. 回調有變動的 ss 的 ss->can_attach 函數,判斷是否合法。
    3. 遍歷需要 migrate 的進程,調用 css_set_move_task(task, from_cset, to_cset, true),進程的 css_set 得到更新。
    4. 回調有變動的 ss 的 ss->attach,migrate 正式生效。
    5. cpuset 的 attach 由 cpuset_attach 函數實現,核心邏輯如下:
    cgroup_taskset_for_each(task, css, tset) {
      WARN_ON_ONCE(set_cpus_allowed_ptr(task, cpus_attach));
    
      cpuset_change_task_nodemask(task, &cpuset_attach_nodemask_to);
      cpuset_update_task_spread_flag(cs, task);
    }

    遍歷進程,使 cpu 和 memory node 的限制生效。

    我們分析的限制進程使用 cpu 由 set_cpus_allowed_ptr 調用 __set_cpus_allowed_ptr 實現,主要邏輯如下:

    int __set_cpus_allowed_ptr(struct task_struct *p,
          const struct cpumask *new_mask, bool check)
    {
    const struct cpumask *cpu_valid_mask = cpu_active_mask;
    unsigned int dest_cpu;
    struct rq_flags rf;
    struct rq *rq;
    
     rq = task_rq_lock(p, &rf);
    update_rq_clock(rq);
    
     if (cpumask_equal(p->cpus_ptr, new_mask))    //1
      goto out;
    
     dest_cpu = cpumask_any_and(cpu_valid_mask, new_mask);    //2
    if (dest_cpu >= nr_cpu_ids) {
      ret = -EINVAL;
      goto out;
    }
    
     do_set_cpus_allowed(p, new_mask);    //3
    
     if (cpumask_test_cpu(task_cpu(p), new_mask))    //4
      goto out;
    
     if (task_running(rq, p) || p->state == TASK_WAKING) {    //5
      struct migration_arg arg = { p, dest_cpu };
      task_rq_unlock(rq, p, &rf);
      stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg);
      return 0;
    } else if (task_on_rq_queued(p)) {
      rq = move_queued_task(rq, &rf, p, dest_cpu);
    }
    out:
    task_rq_unlock(rq, p, &rf);
    return ret;
    
    }

    滿屏都是進程調度章節的內容,在此解釋如下:

    第1步,如果沒有改變,直接退出。

    第2步,指定的資源是否合法,如果不合法,返回錯誤。

    第3步,do_set_cpus_allowed 會調用 p->sched_class->set_cpus_allowed 由具體的調度類實現,調度類一般會更新 task_struct 的 cpus_mask 字段。

    第4步,進程當前所在的 cpu 是否在限制范圍內,如果在,不需要額外處理。

    第5步,進程被限制,不能使用當前所在的 cpu,如果正在運行則停止并 migrate,如果正在等待執行,移到其他 cpu 上。

     cgroup v1 的討論差不多了,絕大部分篇幅集中討論最常用的操作,但實際上還不完整,其余操作大家可以自行繼續當前的思路閱讀。

     

    往期回顧

    容器底層 cgroup 如何實現資源分組?

    容器底層 cgroup 的代碼實現分析

    加載中
    0
    隔壁家的老王
    二如公子
    二如公子
    熟面孔啊:laughing: 等大街擺攤的同志也對內核感興趣的時候,就不用愁操作系統了:cold_sweat:
    0
    二如公子
    二如公子

    小尾巴還是要帶的~~~ 我愛貓愛碼,養貓多多,救助流浪貓也好幾年了。但一人之力終究有限,愛貓或愛碼的同學可以加我的qq一起擼貓擼碼哈(3356954398,二如公子)。

    返回頂部
    頂部
    聚看影院