在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

Linux是如何對容器下的進程進行CPU限制的,底層是如何工作的?

dyquk4xk2p3d ? 來源:開發內功修煉 ? 2023-11-29 14:31 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

現在很多公司的服務都是跑在容器下,我來問幾個容器 CPU 相關的問題,看大家對天天在用的技術是否熟悉。

容器中的核是真的邏輯核嗎?

Linux 是如何對容器下的進程進行 CPU 限制的,底層是如何工作的?

容器中的 throttle 是什么意思?

為什么關注容器 CPU 性能的時候,除了關注使用率,還要關注 throttle 的次數和時間?

和真正使用物理機不同,Linux 容器中所謂的核并不是真正的 CPU 核。所以在理解容器 CPU 性能的時候,必然要有一些特殊的地方需要考慮。

各家公司的容器云上,底層不管使用的是 docker 引擎,還是 containerd 引擎,都是依賴 Linux 的 cgroup 的 cpu 子系統來工作的,所以今天我們就來深入地學習一下 cgroup cpu 子系統 。理解了這個,你將會對容器進程的 CPU 性能有更深入的把握。

一、cgroup 的 cpu 子系統

在 Linux 下, cgroup 提供了對 CPU、內存等資源實現精細化控制的能力。它的全稱是 control groups。允許對某一個進程,或者一組進程所用到的資源進行控制。現在流行的 Docker 就是在這個底層機制上成長起來的。

在你的機器執行執行下面的命令可以查看當前 cgroup 都支持對哪些資源進行控制。

$lssubsys-a
cpuset
cpu,cpuacct
...

其中 cpu 和 cpuset 都是對 CPU 資源進行控制的子系統。cpu 是通過執行時間來控制進程對 cpu 的使用,cpuset 是通過分配邏輯核的方式來分配 cpu。其它可控制的資源還包括 memory(內存)、net_cls(網絡帶寬)等等。

cgroup 提供了一個原生接口并通過 cgroupfs 提供控制。類似于 procfs 和 sysfs,是一種虛擬文件系統。默認情況下 cgroupfs 掛載在 /sys/fs/cgroup 目錄下,我們可以通過修改 /sys/fs/cgroup 下的文件和文件內容來控制進程對資源的使用。

比如,想實現讓某個進程只使用兩個核,我們可以通過 cgroupfs 接口這樣來實現,如下:

#cd/sys/fs/cgroup/cpu,cpuacct
#mkdirtest
#cdtest
#echo100000>cpu.cfs_period_us//100ms
#echo100000>cpu.cfs_quota_us//200ms
#echo{$pid}>cgroup.procs

其中 cfs_period_us 用來配置時間周期長度,cfs_quota_us 用來配置當前 cgroup 在設置的周期長度內所能使用的 CPU 時間。這兩個文件配合起來就可以設置 CPU 的使用上限。

上面的配置就是設置改 cgroup 下的進程每 100 ms 內只能使用 200 ms 的 CPU 周期,也就是說限制使用最多兩個“核”。

要注意的是這種方式只限制的是 CPU 使用時間,具體調度的時候是可能會調度到任意 CPU 上執行的。如果想限制進程使用的 CPU 核,可以使用 cpuset 子系統。

docker 默認情況下使用的就是 cgroupfs 接口,可以通過如下的命令來確認。

#dockerinfo|grepcgroup
CgroupDriver:cgroupfs

二、內核中進程和 cgroup 的關系

在上一節中,我們在 /sys/fs/cgroup/cpu,cpuacct 創建了一個目錄 test,這其實是創建了一個 cgroup 對象。當我們把某個進程的 pid 添加到 cgroup 后,又是建立了進程結構體和 cgroup 之間的關系。

所以要想理解清 cgroup 的工作過程,就得先來了解一下 cgroup 和 task_struct 結構體之間的關系。

2.1 cgroup 內核對象

一個 cgroup 對象中可以指定對 cpu、cpuset、memory 等一種或多種資源的限制。我們先來找到 cgroup 的定義。

//file:include/linux/cgroup-defs.h
structcgroup{
...
structcgroup_subsys_state__rcu*subsys[CGROUP_SUBSYS_COUNT];
...
}

每個 cgroup 都有一個 cgroup_subsys_state 類型的數組 subsys,其中的每一個元素代表的是一種資源控制,如 cpu、cpuset、memory 等等。

76ae7904-8e7f-11ee-939d-92fbcf53809c.png

這里要注意的是,其實 cgroup_subsys_state 并不是真實的資源控制統計信息結構,對于 CPU 子系統真正的資源控制結構是 task_group。它是 cgroup_subsys_state 結構的擴展,類似父類和子類的概念。

76cb2f72-8e7f-11ee-939d-92fbcf53809c.png

當 task_group 需要被當成 cgroup_subsys_state 類型使用的時候,只需要強制類型轉換就可以。

對于內存子系統控制統計信息結構是 mem_cgroup,其它子系統也類似。

76e616d4-8e7f-11ee-939d-92fbcf53809c.png

之所以要這么設計,目的是各個 cgroup 子系統都統一對外暴露 cgroup_subsys_state,其余部分不對外暴露,在自己的子系統內部維護和使用。

2.2 進程和 cgroup 子系統

一個 Linux 進程既可以對它的 cpu 使用進行限制,也可以對它的內存進行限制。所以,一個進程 task_struct 是可以和多種子系統有關聯關系的。

和 cgroup 和多個子系統關聯定義類似,task_struct 中也定義了一個 cgroup_subsys_state 類型的數組 subsys,來表達這種一對多的關系。

771f190c-8e7f-11ee-939d-92fbcf53809c.png

我們來簡單看下源碼的定義。

//file:include/linux/sched.h
structtask_struct{
...
structcss_set__rcu*cgroups;
...
}
//file:include/linux/cgroup-defs.h
structcss_set{
...
structcgroup_subsys_state*subsys[CGROUP_SUBSYS_COUNT];
}

其中subsys是一個指針數組,存儲一組指向 cgroup_subsys_state 的指針。一個 cgroup_subsys_state 就是進程與一個特定的子系統相關的信息。

通過這個指針,進程就可以獲得相關聯的 cgroups 控制信息了。能查到限制該進程對資源使用的 task_group、cpuset、mem_group 等子系統對象。

2.3 內核對象關系圖匯總

我們把上面的內核對象關系圖匯總起來看一下。

7743f7d6-8e7f-11ee-939d-92fbcf53809c.png

可以看到無論是進程、還是 cgroup 對象,最后都能找到和其關聯的具體的 cpu、內存等資源控制自系統的對象。

2.4 cpu 子系統

因為今天我們重點是介紹進程的 cpu 限制,所以我們把 cpu 子系統相關的對象 task_group 專門拿出來理解理解。

//file:kernel/sched/sched.h
structtask_group{
structcgroup_subsys_statecss;
...

//task_group樹結構
structtask_group*parent;
structlist_headsiblings;
structlist_headchildren;

//task_group持有的N個調度實體(N=CPU核數)
structsched_entity**se;

//task_group自己的N個公平調度隊列(N=CPU核數)
structcfs_rq**cfs_rq;

//公平調度帶寬限制
structcfs_bandwidthcfs_bandwidth;
...
}

第一個 cgroup_subsys_state css 成員我們在前面說過了,這相當于它的“父類”。再來看 parent、siblings、children 等幾個對象。這些成員是樹相關的數據結構。在整個系統中有一個 root_task_group。

//file:kernel/sched/core.c
structtask_grouproot_task_group;

所有的 task_group 都是以 root_task_group 為根節點組成了一棵樹。

接下來的 se 和 cfs_rq 是完全公平調度的兩個對象。它們兩都是數組,元素個數等于當前系統的 CPU 核數。每個 task_group 都會在上一級 task_group(比如 root_task_group)的 N 個調度隊列中有一個調度實體。

cfs_rq 是 task_group 自己所持有的完全公平調度隊列。是的,你沒看錯。每一個 task_group 內部都有自己的一組調度隊列,其數量和 CPU 的核數一致。

假如當前系統有兩個邏輯核,那么一個 task_group 樹和 cfs_rq 的簡單示意圖大概是下面這個樣子。

775b0a20-8e7f-11ee-939d-92fbcf53809c.png

Linux 中的進程調度是一個層級的結構。對于容器來講,宿主機中進行進程調度的時候,先調度到的實際上不是容器中的具體某個進程,而是一個 task_group。然后接下來再進入容器 task_group 的調度隊列 cfs_rq 中進行調度,才能最終確定具體的進程 pid。

還有就是 cpu 帶寬限制 cfs_bandwidth, cpu 分配的管控相關的字段都是在 cfs_bandwidth 中定義維護的。

cgroup 相關的內核對象我們就先介紹到這里,接下來我們看一下 cpu 子系統到底是如何實現的。

三、CPU 子系統的實現

在第一節中我們展示通過 cgroupfs 對 cpu 子系統使用,使用過程大概可以分成三步:

第一步:通過創建目錄來創建 cgroup

第二步:在目錄中設置 cpu 的限制情況

第三步:將進程添加到 cgroup 中進行資源管控

那本小節我們就從上面三步展開,看看在每一步中,內核都具體做了哪些事情。限于篇幅所限,我們只講 cpu 子系統,對于其他的子系統也是類似的分析過程。

3.1 創建 cgroup 對象

內核定義了對 cgroupfs 操作的具體處理函數。在 /sys/fs/cgroup/ 下的目錄創建操作都將由下面 cgroup_kf_syscall_ops 定義的方法來執行。

//file:kernel/cgroup/cgroup.c
staticstructkernfs_syscall_opscgroup_kf_syscall_ops={
.mkdir=cgroup_mkdir,
.rmdir=cgroup_rmdir,
...
};

創建目錄執行整個過程鏈條如下

vfs_mkdir
->kernfs_iop_mkdir
->cgroup_mkdir
->cgroup_apply_control_enable
->css_create
->cpu_cgroup_css_alloc

其中關鍵的創建過程有:

cgroup_mkdir:在這里創建了 cgroup 內核對象

css_create:創建每一個子系統資源管理對象,對于 cpu 子系統會創建 task_group

cgroup 內核對象是在 cgroup_mkdir 中創建的。除了 cgroup 內核對象,這里還創建了文件系統重要展示的目錄。

//file:kernel/cgroup/cgroup.c
intcgroup_mkdir(structkernfs_node*parent_kn,constchar*name,umode_tmode)
{
...
//查找父cgroup
parent=cgroup_kn_lock_live(parent_kn,false);

//創建cgroup對象出來
cgrp=cgroup_create(parent);

//創建文件系統節點
kn=kernfs_create_dir(parent->kn,name,mode,cgrp);
cgrp->kn=kn;
...
}

在 cgroup 中,是有層次的概念的,這個層次結構和 cgroupfs 中的目錄層次結構一樣。所以在創建 cgroup 對象之前的第一步就是先找到其父 cgroup, 然后創建自己,并創建文件系統中的目錄以及文件。

在 cgroup_apply_control_enable 中,執行子系統對象的創建。

//file:kernel/cgroup/cgroup.c
staticintcgroup_apply_control_enable(structcgroup*cgrp)
{
...
cgroup_for_each_live_descendant_pre(dsct,d_css,cgrp){
for_each_subsys(ss,ssid){
structcgroup_subsys_state*css=cgroup_css(dsct,ss);
css=css_create(dsct,ss);
...
}
}
return0;
}

通過 for_each_subsys 遍歷每一種 cgroup 子系統,并調用其 css_alloc 來創建相應的對象。

//file:kernel/cgroup/cgroup.c
staticstructcgroup_subsys_state*css_create(structcgroup*cgrp,
structcgroup_subsys*ss)
{
css=ss->css_alloc(parent_css);
...
}

上面的 css_alloc 是一個函數指針,對于 cpu 子系統來說,它指向的是 cpu_cgroup_css_alloc。這個對應關系在 kernel/sched/core.c 文件仲可以找到

//file:kernel/sched/core.c
structcgroup_subsyscpu_cgrp_subsys={
.css_alloc=cpu_cgroup_css_alloc,
.css_online=cpu_cgroup_css_online,
...
};

通過 cpu_cgroup_css_alloc => sched_create_group 調用后,創建出了 cpu 子系統的內核對象 task_group。

//file:kernel/sched/core.c
structtask_group*sched_create_group(structtask_group*parent)
{
structtask_group*tg;
tg=kmem_cache_alloc(task_group_cache,GFP_KERNEL|__GFP_ZERO);
...
}

3.2 設置 CPU 子系統限制

第一節中,我們通過對 cpu 子系統目錄下的 cfs_period_us 和 cfs_quota_us 值的修改,來完成了 cgroup 中限制的設置。我們這個小節再看看看這個設置過程。

當用戶讀寫這兩個文件的時候,內核中也定義了對應的處理函數。

//file:kernel/sched/core.c
staticstructcftypecpu_legacy_files[]={
...
{
.name="cfs_quota_us",
.read_s64=cpu_cfs_quota_read_s64,
.write_s64=cpu_cfs_quota_write_s64,
},
{
.name="cfs_period_us",
.read_u64=cpu_cfs_period_read_u64,
.write_u64=cpu_cfs_period_write_u64,
},
...
}

寫處理函數 cpu_cfs_quota_write_s64、cpu_cfs_period_write_u64 最終又都是調用 tg_set_cfs_bandwidth 來完成設置的。

//file:kernel/sched/core.c
staticinttg_set_cfs_bandwidth(structtask_group*tg,u64period,u64quota)
{
//定位cfs_bandwidth對象
structcfs_bandwidth*cfs_b=&tg->cfs_bandwidth;
...

//對cfs_bandwidth進行設置
cfs_b->period=ns_to_ktime(period);
cfs_b->quota=quota;
...
}

在 task_group 中,其帶寬管理控制都是由 cfs_bandwidth 來完成的,所以一開始就需要先獲取 cfs_bandwidth 對象。接著將用戶設置的值都設置到 cfs_bandwidth 類型的對象 cfs_b 上。

3.3 寫 proc 進 group

cgroup 創建好了,cpu 限制規則也制定好了,下一步就是將進程添加到這個限制中。在 cgroupfs 下的操作方式就是修改 cgroup.procs 文件。

內核定義了修改 cgroup.procs 文件的處理函數為 cgroup_procs_write。

//file:kernel/cgroup/cgroup.c
staticstructcftypecgroup_base_files[]={
...
{
.name="cgroup.procs",
...
.write=cgroup_procs_write,
},
}

在 cgroup_procs_write 的處理中,主要做了這么幾件事情。

第一、邏根據用戶輸入的 pid 來查找 task_struct 內核對象。

第二、從舊的調度組中退出,加入到新的調度組 task_group 中

第三、修改進程其 cgroup 相關的指針,讓其指向上面創建好的 task_group。

我們來看下加入新調度組的過程,內核的調用鏈條如下。

cgroup_procs_write
->cgroup_attach_task
->cgroup_migrate
->cgroup_migrate_execute

在 cgroup_migrate_execute 中遍歷各個子系統,完成每一個子系統的遷移。

staticintcgroup_migrate_execute(structcgroup_mgctx*mgctx)
{
do_each_subsys_mask(ss,ssid,mgctx->ss_mask){
if(ss->attach){
tset->ssid=ssid;
ss->attach(tset);
}
}while_each_subsys_mask();
...
}

對于 cpu 子系統來講,attach 對應的處理方法是 cpu_cgroup_attach。這也是在 kernel/sched/core.c 下的 cpu_cgrp_subsys 中定義的。

cpu_cgroup_attach 調用 sched_move_task 來完成將進程加入到新調度組的過程。

//file:kernel/sched/core.c
voidsched_move_task(structtask_struct*tsk)
{
//找到task所在的runqueue
rq=task_rq_lock(tsk,&rf);

//從runqueue中出來
queued=task_on_rq_queued(tsk);
if(queued)
dequeue_task(rq,tsk,queue_flags);

//修改task的group
//將進程先從舊tg的cfs_rq中移除且更新cfs_rq的負載;再將進程添加入新tg的cfs_rq并更新新cfs_rq的負載
sched_change_group(tsk,TASK_MOVE_GROUP);

//此時進程的調度組已經更新,重新將進程加回runqueue
if(queued)
enqueue_task(rq,tsk,queue_flags);
...
}

這個函數做了三件事。

第一、先調用 dequeue_task 從原歸屬的 queue 中退出來,

第二、修改進程的 task_group

第三、重新將進程添加到新 task_group 的 runqueue 中。

//file:kernel/sched/core.c
staticvoidsched_change_group(structtask_struct*tsk,inttype)
{
structtask_group*tg;

//查找task_group
tg=container_of(task_css_check(tsk,cpu_cgrp_id,true),
structtask_group,css);
tg=autogroup_task_group(tsk,tg);

//修改task_struct所對應的task_group
tsk->sched_task_group=tg;
...
}

進程 task_struct 的 sched_task_group 是表示其歸屬的 task_group, 這里設置到新歸屬上。

四、進程 CPU 帶寬控制過程

在前面的操作完畢之后,我們只是將進程添加到了 cgroup 中進行管理而已。相當于只是初始化,而真正的限制是貫穿在 Linux 運行是的進程調度過程中的。

所添加的進程將會受到 cpu 子系統 task_group 下的 cfs_bandwidth 中記錄的 period 和 quota 的限制。

在你的新進程是如何被內核調度執行到的?一文中我們介紹過完全公平調度器在選擇進程時的核心方法 pick_next_task_fair。

這個方法的整個執行過程一個自頂向下搜索可執行的 task_struct 的過程。整個系統中有一個 root_task_group。

//file:kernel/sched/core.c
structtask_grouproot_task_group;

775b0a20-8e7f-11ee-939d-92fbcf53809c.png

CFS 中調度隊列是一顆紅黑樹, 紅黑樹的節點是 struct sched_entity, sched_entity 中既可以指向 struct task_struct 也可以指向 struct cfs_rq(可理解為 task_group)

調度 pick_next_task_fair()函數中的 prev 是本次調度時在執行的上一個進程。該函數通過 do {} while 循環,自頂向下搜索到下一步可執行進程。

//file:kernel/sched/fair.c
staticstructtask_struct*
pick_next_task_fair(structrq*rq,structtask_struct*prev,structrq_flags*rf)
{
structcfs_rq*cfs_rq=&rq->cfs;
...

//選擇下一個調度的進程
do{
...
se=pick_next_entity(cfs_rq,curr);
cfs_rq=group_cfs_rq(se);
}while(cfs_rq)
p=task_of(se);

//如果選出的進程和上一個進程不同
if(prev!=p){
structsched_entity*pse=&prev->se;
...

//對要放棄CPU的進程執行一些處理
put_prev_entity(cfs_rq,pse);
}

}

如果新進程和上一次運行的進程不是同一個,則要調用 put_prev_entity 做兩件和 CPU 的帶寬控制有關的事情。

//file:kernel/sched/fair.c
staticvoidput_prev_entity(structcfs_rq*cfs_rq,structsched_entity*prev)
{
//4.1運行隊列帶寬的更新與申請
if(prev->on_rq)
update_curr(cfs_rq);

//4.2判斷是否需要將容器掛起
check_cfs_rq_runtime(cfs_rq);

//更新負載數據
update_load_avg(cfs_rq,prev,0);
...
}

在上述代碼中,和 CPU 帶寬控制相關的操作有兩個。

運行隊列帶寬的更新與申請

判斷是否需要進行帶寬限制

接下來我們分兩個小節詳細展開看看這兩個操作具體都做了哪些事情。

4.1 運行隊列帶寬的更新與申請

在這個小節中我們專門來看看 cfs_rq 隊列中 runtime_remaining 的更新與申請

在實現上帶寬控制是在 task_group 下屬的 cfs_rq 隊列中進行的。cfs_rq 對帶寬時間的操作歸總起來就是更新與申請。申請到的時間保存在字段 runtime_remaining 字段中,每當有時間支出需要更新的時候也是從這個字段值從去除。

其實除了上述場景外,系統在很多情況下都會調用 update_curr,包括任務在入隊、出隊時,調度中斷函數也會周期性地調用該方法,以確保任務的各種時間信息隨時都是最新的狀態。在這里會更新 cfs_rq 隊列中的 runtime_remaining 時間。如果 runtime_remaining 不足,會觸發時間申請。

//file:kernel/sched/fair.c
staticvoidupdate_curr(structcfs_rq*cfs_rq)
{
//計算一下運行了多久
u64now=rq_clock_task(rq_of(cfs_rq));
u64delta_exec;
delta_exec=now-curr->exec_start;
...

//更新帶寬限制
account_cfs_rq_runtime(cfs_rq,delta_exec);
}

在 update_curr 先計算當前執行了多少時間。然后在 cfs_rq 的 runtime_remaining 減去該時間值,具體減的過程是在 account_cfs_rq_runtime 中處理的。

//file:kernel/sched/fair.c
staticvoid__account_cfs_rq_runtime(structcfs_rq*cfs_rq,u64delta_exec)
{
cfs_rq->runtime_remaining-=delta_exec;

//如果還有剩余時間,則函數返回
if(likely(cfs_rq->runtime_remaining>0))
return;
...
//調用assign_cfs_rq_runtime申請時間余額
if(!assign_cfs_rq_runtime(cfs_rq)&&likely(cfs_rq->curr))
resched_curr(rq_of(cfs_rq));
}

更新帶寬時間的邏輯比較簡單,先從 cfs->runtime_remaining 減去本次執行的物理時間。如果減去之后仍然大于 0 ,那么本次更新就算是結束了。

如果相減后發現是負數,表示當前 cfs_rq 的時間余額已經耗盡,則會立即嘗試從任務組中申請。具體的申請函數是 assign_cfs_rq_runtime。如果申請沒能成功,調用 resched_curr 標記 cfs_rq->curr 的 TIF_NEED_RESCHED 位,以便隨后將其調度出去。

我們展開看下申請過程 assign_cfs_rq_runtime 。

//file:kernel/sched/fair.c
staticintassign_cfs_rq_runtime(structcfs_rq*cfs_rq)
{
//獲取當前task_group的cfs_bandwidth
structtask_group*tg=cfs_rq->tg;
structcfs_bandwidth*cfs_b=tg_cfs_bandwidth(tg);

//申請時間數量為保持下次有sysctl_sched_cfs_bandwidth_slice這么多
min_amount=sched_cfs_bandwidth_slice()-cfs_rq->runtime_remaining;

//如果沒有限制,則要多少給多少
if(cfs_b->quota==RUNTIME_INF)
amount=min_amount;
else{
//保證定時器是打開的,保證周期性地為任務組重置帶寬時間
start_cfs_bandwidth(cfs_b);

//如果本周期內還有時間,則可以分配
if(cfs_b->runtime>0){
//確保不要透支
amount=min(cfs_b->runtime,min_amount);
cfs_b->runtime-=amount;
cfs_b->idle=0;
}
}

cfs_rq->runtime_remaining+=amount;
returncfs_rq->runtime_remaining>0;
}

首先,獲取當前 task_group 的 cfs_bandwidth,因為整個任務組的帶寬數據都是封裝在這里的。接著調用 sched_cfs_bandwidth_slice 來獲取后面要留有多長時間,這個函數訪問的 sysctl 下的 sched_cfs_bandwidth_slice 參數。

//file:kernel/sched/fair.c
staticinlineu64sched_cfs_bandwidth_slice(void)
{
return(u64)sysctl_sched_cfs_bandwidth_slice*NSEC_PER_USEC;
}

這個參數在我的機器上是 5000 us(也就是說每次申請 5 ms)。

$sysctl-a|grepsched_cfs_bandwidth_slice
kernel.sched_cfs_bandwidth_slice_us=5000

在計算要申請的時間的時候,還需要考慮現在有多少時間。如果 cfs_rq->runtime_remaining 為正的話,那可以少申請一點,如果已經變為負數的話,需要在 sched_cfs_bandwidth_slice 基礎之上再多申請一些。

所以,最終要申請的時間值 min_amount = sched_cfs_bandwidth_slice() - cfs_rq->runtime_remaining

計算出 min_amount 后,直接在向自己所屬的 task_group 下的 cfs_bandwidth 把時間申請出來。整個 task_group 下可用的時間是保存在 cfs_b->runtime 中的。

這里你可能會問了,那 task_group 下的 cfs_b->runtime 的時間又是哪兒給分配的呢?我們將在 5.1 節來討論這個過程。

4.2 帶寬限制

check_cfs_rq_runtime 這個函數檢測 task group 的帶寬是否已經耗盡, 如果是則調用 throttle_cfs_rq 對進程進行限流。

//file:kernel/sched/fair.c
staticboolcheck_cfs_rq_runtime(structcfs_rq*cfs_rq)
{
//判斷是不是時間余額已用盡
if(likely(!cfs_rq->runtime_enabled||cfs_rq->runtime_remaining>0))
returnfalse;
...

throttle_cfs_rq(cfs_rq);
returntrue;
}

我們再來看看 throttle_cfs_rq 的執行過程。

//file:kernel/sched/fair.c
staticvoidthrottle_cfs_rq(structcfs_rq*cfs_rq)
{
//1.查找到所屬的task_group下的se
se=cfs_rq->tg->se[cpu_of(rq_of(cfs_rq))];
...

//2.遍歷每一個可調度實體,并從隸屬的 cfs_rq 上面刪除。
for_each_sched_entity(se){
structcfs_rq*qcfs_rq=cfs_rq_of(se);

if(dequeue)
dequeue_entity(qcfs_rq,se,DEQUEUE_SLEEP);
...
}

//3.設置一些 throttled 信息。
cfs_rq->throttled=1;
cfs_rq->throttled_clock=rq_clock(rq);

//4.確保unthrottle的高精度定時器處于被激活的狀態
start_cfs_bandwidth(cfs_b);
...
}

在 throttle_cfs_rq 中,找到其所屬的 task_group 下的調度實體 se 數組,遍歷每一個元素,并從其隸屬的 cfs_rq 的紅黑樹上刪除。這樣下次再調度的時候,就不會再調度到這些進程了。

那么 start_cfs_bandwidth 是干啥的呢?這正好是下一節的引子。

五、進程的可運行時間的分配

在第四小節我們看到,task_group 下的進程的運行時間都是從它的 cfs_b->runtime 中申請的。這個時間是在定時器中分配的。負責給 task_group 分配運行時間的定時器包括兩個,一個是 period_timer,另一個是 slack_timer。

structcfs_bandwidth{
ktime_tperiod;
u64    quota;
...
structhrtimerperiod_timer;
structhrtimerslack_timer;
...
}

peroid_timer 是周期性給 task_group 添加時間,缺點是 timer 周期比較長,通常是100ms。而 slack_timer 用于有 cfs_rq 處于 throttle 狀態且全局時間池有時間供分配但是 period_timer 有還有比較長時間(通常大于7ms)才超時的場景。這個時候我們就可以激活比較短的slack_timer(5ms超時)進行throttle,這樣的設計可以提升系統的實時性。

這兩個 timer 在 cgroup 下的 cfs_bandwidth 初始化的時候,都設置好了到期回調函數,分別是 sched_cfs_period_timer 和 sched_cfs_slack_timer。

//file:kernel/sched/fair.c
voidinit_cfs_bandwidth(structcfs_bandwidth*cfs_b)
{
cfs_b->runtime=0;
cfs_b->quota=RUNTIME_INF;
cfs_b->period=ns_to_ktime(default_cfs_period());

//初始化period_timer并設置回調函數
hrtimer_init(&cfs_b->period_timer,CLOCK_MONOTONIC,HRTIMER_MODE_ABS_PINNED);
cfs_b->period_timer.function=sched_cfs_period_timer;

//初始化slack_timer并設置回調函數
hrtimer_init(&cfs_b->slack_timer,CLOCK_MONOTONIC,HRTIMER_MODE_REL);
cfs_b->slack_timer.function=sched_cfs_slack_timer;
...
}

在上一節最后提到的 start_cfs_bandwidth 就是在打開 period_timer 定時器。

//file:kernel/sched/fair.c
voidstart_cfs_bandwidth(structcfs_bandwidth*cfs_b)
{
...
hrtimer_forward_now(&cfs_b->period_timer,cfs_b->period);
hrtimer_start_expires(&cfs_b->period_timer,HRTIMER_MODE_ABS_PINNED);
}

在 hrtimer_forward_now 調用時傳入的第二個參數表示是觸發的延遲時間。這個就是在 cgroup 是設置的 period,一般為 100 ms。

我們來分別看看這兩個 timer 是如何給 task_group 定期發工資(分配時間)的。

5.1 period_timer

在 period_timer 的回調函數 sched_cfs_period_timer 中,周期性地為任務組分配帶寬時間,并且解掛當前任務組中所有掛起的隊列。

分配帶寬時間是在 __refill_cfs_bandwidth_runtime 中執行的,它的調用堆棧如下。

sched_cfs_period_timer
->do_sched_cfs_period_timer
->__refill_cfs_bandwidth_runtime
//file:kernel/sched/fair.c
void__refill_cfs_bandwidth_runtime(structcfs_bandwidth*cfs_b)
{
if(cfs_b->quota!=RUNTIME_INF)
cfs_b->runtime=cfs_b->quota;
}

可見,這里直接給 cfs_b->runtime 增加了 cfs_b->quota 這么多的時間。其中 cfs_b->quota 你就可以認為是在 cgroupfs 目錄下,我們配置的那個值。在第一節中,我們配置的是 500 ms。

#echo500000>cpu.cfs_period_us//500ms

5.2 slack_timer

設想一下,假如說某個進程申請了 5 ms 的執行時間,但是當進程剛一啟動執行便執行了同步阻塞的邏輯,這時候所申請的時間根本都沒有用完。在這種情況下,申請但沒用完的時間大部分是要返還給 task_group 中的全局時間池的。

在內核中的調用鏈如下

dequeue_task_fair
–>dequeue_entity
–>return_cfs_rq_runtime
–>__return_cfs_rq_runtime

具體的返還是在 __return_cfs_rq_runtime 中處理的。

//file:kernel/sched/fair.c
staticvoid__return_cfs_rq_runtime(structcfs_rq*cfs_rq)
{
//給自己留一點
s64slack_runtime=cfs_rq->runtime_remaining-min_cfs_rq_runtime;
if(slack_runtime<=?0)
??return;

?//返還到全局時間池中
?if?(cfs_b->quota!=RUNTIME_INF){
cfs_b->runtime+=slack_runtime;

//如果時間又足夠多了,并且還有進程被限制的話
//則調用start_cfs_slack_bandwidth來開啟slack_timer
if(cfs_b->runtime>sched_cfs_bandwidth_slice()&&
!list_empty(&cfs_b->throttled_cfs_rq))
start_cfs_slack_bandwidth(cfs_b);
}
...
}

這個函數做了這么幾件事情。

min_cfs_rq_runtime 的值是 1 ms,我們選擇至少保留 1ms 時間給自己

剩下的時間 slack_runtime 歸還給當前的 cfs_b->runtime

如果時間又足夠多了,并且還有進程被限制的話,開啟slack_timer,嘗試接觸進程 CPU 限制

在 start_cfs_slack_bandwidth 中啟動了 slack_timer。

//file:kernel/sched/fair.c
staticvoidstart_cfs_slack_bandwidth(structcfs_bandwidth*cfs_b)
{
...

//啟動slack_timer
cfs_b->slack_started=true;
hrtimer_start(&cfs_b->slack_timer,
ns_to_ktime(cfs_bandwidth_slack_period),
HRTIMER_MODE_REL);
...
}

可見 slack_timer 的延遲回調時間是 cfs_bandwidth_slack_period,它的值是 5 ms。這就比 period_timer 要實時多了。

slack_timer 的回調函數 sched_cfs_slack_timer 我們就不展開看了,它主要就是操作對進程解除 CPU 限制

六、總結

今天我們介紹了 Linux cgroup 的 cpu 子系統給容器中的進程分配 cpu 時間的原理。

和真正使用物理機不同,Linux 容器中所謂的核并不是真正的 CPU 核,而是轉化成了執行時間的概念。在容器進程調度的時候給其滿足一定的 CPU 執行時間,而不是真正的分配邏輯核。

cgroup 提供了的原生接口是通過 cgroupfs 提供控制各個子系統的設置的。默認是在 /sys/fs/cgroup/ 目錄下,內核這個文件系統的處理是定義了特殊的處理,和普通的文件完全不一樣的。

內核處理 cpu 帶寬控制的核心對象就是下面這個 cfs_bandwidth。

//file:kernel/sched/sched.h
structcfs_bandwidth{
//帶寬控制配置
ktime_tperiod;
u64quota;

//當前task_group的全局可執行時間
u64runtime;
...

//定時分配
structhrtimerperiod_timer;
structhrtimerslack_timer;
}

用戶在創建 cgroup cpu 子系統控制過程主要分成三步:

第一步:通過創建目錄來創建 cgroup 對象。在 /sys/fs/cgroup/cpu,cpuacct 創建一個目錄 test,實際上內核是創建了 cgroup、task_group 等內核對象。

第二步:在目錄中設置 cpu 的限制情況。在 task_group 下有個核心的 cfs_bandwidth 對象,用戶所設置的 cfs_quota_us 和 cfs_period_us 的值最后都存到它下面了。

第三步:將進程添加到 cgroup 中進行資源管控。當在 cgroup 的 cgroup.proc 下添加進程 pid 時,實際上是將該進程加入到了這個新的 task_group 調度組了。將使用 task_group 的 runqueue,以及它的時間配額

當創建完成后,內核的 period_timer 會根據 task_group->cfs_bandwidth 下用戶設置的 period 定時給可執行時間 runtime 上加上 quota 這么多的時間(相當于按月發工資),以供 task_group 下的進程執行(消費)的時候使用。

structcfs_rq{
...
intruntime_enabled;
s64runtime_remaining;
}

在完全公平器調度的時候,每次 pick_next_task_fair 時會做兩件事情

第一件:將從 cpu 上拿下來的進程所在的運行隊列進行執行時間的更新與申請。會將 cfs_rq 的 runtime_remaining 減去已經執行了的時間。如果減為了負數,則從 cfs_rq 所在的 task_group 下的 cfs_bandwidth 去申請一些。

第二件:判斷 cfs_rq 上是否申請到了可執行時間,如果沒有申請到,需要將這個隊列上的所有進程都從完全公平調度器的紅黑樹上取下。這樣再次調度的時候,這些進程就不會被調度了。

當 period_timer 再次給 task_group 分配時間的時候,或者是自己有申請時間沒用完回收后觸發 slack_timer 的時候,被限制調度的進程會被解除調度限制,重新正常參與運行。

這里要注意的是,一般 period_timer 分配時間的周期都是 100 ms 左右。假如說你的進程前 50 ms 就把 cpu 給用光了,那你收到的請求可能在后面的 50 ms 都沒有辦法處理,對請求處理耗時會有影響。這也是為啥在關注 CPU 性能的時候要關注對容器 throttle 次數和時間的原因了。






審核編輯:劉清

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Linux
    +關注

    關注

    87

    文章

    11506

    瀏覽量

    213464
  • 調度器
    +關注

    關注

    0

    文章

    98

    瀏覽量

    5487

原文標題:內核是如何給容器中的進程分配CPU資源的?

文章出處:【微信號:良許Linux,微信公眾號:良許Linux】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關推薦
    熱點推薦

    CPU底層工作原理

    前段時間,我連續寫了十來篇CPU底層系列技術故事文章,有不少讀者私信我讓我寫一CPU的寄存器。
    發表于 07-25 10:20 ?1866次閱讀

    Linux開發_Linux進程編程

    介紹Linux進程概念、進程信號捕獲、進程管理相關的命令的使用等知識點。
    的頭像 發表于 09-17 15:38 ?1653次閱讀
    <b class='flag-5'>Linux</b>開發_<b class='flag-5'>Linux</b><b class='flag-5'>下</b><b class='flag-5'>進程</b>編程

    Linux進程怎么綁定CPU

    昨天在群里有朋友問:把進程綁定到某個 CPU 上運行是怎么實現的。
    發表于 10-26 10:26 ?2055次閱讀

    Linux系統進程的幾種狀態介紹

    文章對 Linux 系統進程的幾種狀態進行介紹,并對系統出現大量僵尸進程和不可中斷進程的場景
    發表于 11-24 16:15 ?1.4w次閱讀
    <b class='flag-5'>Linux</b>系統<b class='flag-5'>下</b><b class='flag-5'>進程</b>的幾種狀態介紹

    Linux進程的睡眠和喚醒

    Linux中,僅等待CPU時間的進程稱為就緒進程,它們被放置在一個運行隊列中,一個就緒進程的狀 態標志位為 TASK_RUNNING。一旦
    發表于 06-07 12:26 ?615次閱讀

    Linux進程結構

    `#嵌入式培訓#華清遠見嵌入式linux學習資料《Linux進程結構》,進程不但包括程序的指令和數據,而且包括程序計數器和處理器的所有寄
    發表于 08-05 11:05

    Linux進程結構

    (TASK_KILLABLE):Linux內核 2.6.25 引入了一種新的進程狀態,名為 TASK_KILLABLE。該狀態的運行機制類似于 TASK_UNINTERRUPTIBLE,只不過處在該狀態
    發表于 05-27 09:24

    linux操作系統進程通信設計

    linux進程通信手段基本上是從Unix平臺上的進程通信手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T的貝
    發表于 11-24 10:53 ?821次閱讀

    linux操作系統進程通信

    的側重點有所不同。前者對Unix早期的進程間通信手段進行了系統的改進和擴充,形成了system V IPC,通信進程局限在單個計算機內;后者則跳過了該限制,形成了基于套接口(socke
    發表于 10-31 11:15 ?0次下載

    詳解如何監控和保護Linux進程安全

    。 經典的信息保密性安全模型Bell-LaPadula模型指出,進程是整個計算機系統的一個主體,它需要通過一定的安全等級來對客體發生作用。進程在一定條件可以對諸如文件、數據庫等客體進行
    發表于 11-06 11:20 ?0次下載

    Linux CPU的性能應該如何優化

    Linux系統中,由于成本的限制,往往會存在資源上的不足,例如 CPU、內存、網絡、IO 性能。本文,就對 Linux 進程
    的頭像 發表于 01-18 08:52 ?3703次閱讀

    基于linux eBPF的進程off-cpu的方法

    的swap等。如下圖所示,紅色部分屬于on-cpu部分,藍色部分屬于off-cpu。 一般我們用的perf命令等都是采樣on-cpu的指令進行CPU
    的頭像 發表于 09-25 15:41 ?3390次閱讀
    基于<b class='flag-5'>linux</b> eBPF的<b class='flag-5'>進程</b>off-<b class='flag-5'>cpu</b>的方法

    Linux技術中Cgroup的原理和實踐

    一、什么是Cgroup,使用場景? 容器本質上是進程,既然是進程就會消耗掉系統資源,比如:CPU、內存、磁盤、網絡帶寬等,如果不加以限制
    的頭像 發表于 10-15 14:04 ?5132次閱讀
    <b class='flag-5'>Linux</b>技術中Cgroup的原理和實踐

    如何將進程CPU 進行綁定

    Linux 系統提供了一個名為 sched_setaffinity 的系統調用,此系統調用可以設置進程CPU 親和性。我們來看看 sched_setaffinity 系統調用的原型。
    發表于 10-26 10:29 ?653次閱讀

    如何限制容器可以使用的CPU資源

    默認情況容器可以使用的主機 CPU 資源是不受限制的。和內存資源的使用一樣,如果不對容器可以使用的 C
    的頭像 發表于 10-24 17:04 ?663次閱讀
    如何<b class='flag-5'>限制</b><b class='flag-5'>容器</b>可以使用的<b class='flag-5'>CPU</b>資源
    主站蜘蛛池模板: 久久久一本波多野结衣 | 能直接看黄的网站 | 亚洲福利视频一区 | 天天爽天天狼久久久综合 | 免费看毛片网 | 狠狠色狠色综合曰曰 | 性色成人网| 成人永久免费视频 | ts视频在线观看 | 欧美伊久线香蕉线新在线 | 女人被狂躁视频免费网站 | 成人国产精品一级毛片了 | 人人爱人人澡 | 亚洲午夜久久久久久噜噜噜 | 欧美色久 | 九九re| 精品videosex性欧美 | 稀缺资源呦视频在线网站 | 婷婷五月小说 | 国产精品欧美一区二区三区 | 日韩一级在线 | 天天射天天干天天舔 | 午夜国产精品视频 | 伊人久久99| 9966国产精品视频 | 开心激情五月婷婷 | 五月婷婷丁香六月 | 亚洲成a人伦理 | 人人爱天天做夜夜爽毛片 | 日韩三级| 天堂在线最新资源 | 奇米久久久 | 夜夜夜夜夜夜夜猛噜噜噜噜噜噜 | 玖玖在线精品 | 88av影院| 三级三级三级网站网址 | 九色亚洲 | 羞羞答答xxdd影院欧美 | 婷婷毛片 | 午夜视频在线观看一区二区 | 亚1州区2区3区4区产品乱码 |