搜尋
首頁系統教程LinuxLinux 基礎:cgroup 原理與實作

Linux 基礎:cgroup 原理與實作

Feb 10, 2024 am 08:15 AM
linuxlinux教程linux系統linux指令shell腳本linux入門linux學習

本文將透過研究原始程式碼(此處使用Linux 2.6.25版本)來詳細解析CGroup的實作原理。在深入原始碼之前,我們先來了解幾個關鍵的資料結構,因為CGroup是透過這些資料結構來管理進程組對各種資源的使用。

cgroup結構體

#前面已經提到,cgroup用來控制進程組對各種資源的使用。在核心中,cgroup是透過cgroup結構體來進行描述的,讓我們來看看它的定義:

struct cgroup {
    unsigned long flags;        /* "unsigned long" so bitops work */
    atomic_t count;
    struct list_head sibling;   /* my parent's children */
    struct list_head children;  /* my children */
    struct cgroup *parent;      /* my parent */
    struct dentry *dentry;      /* cgroup fs entry */
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
    struct cgroupfs_root *root;
    struct cgroup *top_cgroup;
    struct list_head css_sets;
    struct list_head release_list;
};

下面我們來介紹一下 cgroup 結構體各個欄位的用途:

  1. flags: 用來標識目前 cgroup 的狀態。
  2. count: 引用計數器,表示有多少個行程在使用這個 cgroup
  3. sibling、children、parent: 由於cgroup 是透過層級 來管理的,這三個欄位就把同一個層級 的所有cgroup 連接成一棵樹。 parent 指向目前cgroup 的父節點,sibling 連接所有兄弟節點,而children 連接著目前cgroup 的所有子節點。
  4. dentry: 由於cgroup 是透過虛擬檔案系統 來進行管理的,在介紹cgroup 使用時說過,可以把cgroup 當成是層級 中的一個目錄,所以dentry 欄位就是用來描述這個目錄的。
  5. subsys: 前面說過,子系統 能夠附加到層級,而附加到層級子系統都有其限制進程組使用資源的演算法和統計資料。所以 subsys 欄位就是提供給各個 子系統 存放其限制進程組使用資源的統計資料。我們可以看到 subsys 欄位是一個數組,而數組中的每一個元素都代表了一個 子系統 相關的統計資料。從實作來看,cgroup 只是把多個行程組織成控制進程組,而真正限制資源使用的是各個 子系統
  6. root: 用於保存層級 的一些數據,例如:層級 的根節點,附加到層級子系統 清單(因為一個層級 可以附加多個子系統),還有這個層級 有多少個cgroup 節點等。
  7. top_cgroup: 層級 的根節點(根cgroup)。

我們透過下面圖片來描述 層級 中各個 cgroup 組成的樹狀關係:

Linux 基础:cgroup 原理与实现cgroup-links

cgroup_subsys_state 结构体

每个 子系统 都有属于自己的资源控制统计信息结构,而且每个 cgroup 都绑定一个这样的结构,这种资源控制统计信息结构就是通过 cgroup_subsys_state 结构体实现的,其定义如下:

struct cgroup_subsys_state {
    struct cgroup *cgroup;
    atomic_t refcnt;
    unsigned long flags;
};

下面介绍一下 cgroup_subsys_state 结构各个字段的作用:

  1. cgroup: 指向了这个资源控制统计信息所属的 cgroup
  2. refcnt: 引用计数器。
  3. flags: 标志位,如果这个资源控制统计信息所属的 cgroup层级 的根节点,那么就会将这个标志位设置为 CSS_ROOT 表示属于根节点。

cgroup_subsys_state 结构的定义看不到各个 子系统 相关的资源控制统计信息,这是因为 cgroup_subsys_state 结构并不是真实的资源控制统计信息结构,比如 内存子系统 真正的资源控制统计信息结构是 mem_cgroup,那么怎样通过这个 cgroup_subsys_state 结构去找到对应的 mem_cgroup 结构呢?我们来看看 mem_cgroup 结构的定义:

struct mem_cgroup {
    struct cgroup_subsys_state css; // 注意这里
    struct res_counter res;
    struct mem_cgroup_lru_info info;
    int prev_priority;
    struct mem_cgroup_stat stat;
};

mem_cgroup 结构的定义可以发现,mem_cgroup 结构的第一个字段就是一个 cgroup_subsys_state 结构。下面的图片展示了他们之间的关系:

Linux 基础:cgroup 原理与实现cgroup-state-memory

从上图可以看出,mem_cgroup 结构包含了 cgroup_subsys_state 结构,内存子系统 对外暴露出 mem_cgroup 结构的 cgroup_subsys_state 部分(即返回 cgroup_subsys_state 结构的指针),而其余部分由 内存子系统 自己维护和使用。

由于 cgroup_subsys_state 部分在 mem_cgroup 结构的首部,所以要将 cgroup_subsys_state 结构转换成 mem_cgroup 结构,只需要通过指针类型转换即可。

cgroup 结构与 cgroup_subsys_state 结构之间的关系如下图:

Linux 基础:cgroup 原理与实现cgroup-subsys-state

css_set 结构体

由于一个进程可以同时添加到不同的 cgroup 中(前提是这些 cgroup 属于不同的 层级)进行资源控制,而这些 cgroup 附加了不同的资源控制 子系统。所以需要使用一个结构把这些 子系统 的资源控制统计信息收集起来,方便进程通过 子系统ID 快速查找到对应的 子系统 资源控制统计信息,而 css_set 结构体就是用来做这件事情。css_set 结构体定义如下:

struct css_set {
    struct kref ref;
    struct list_head list;
    struct list_head tasks;
    struct list_head cg_links;
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
};

下面介绍一下 css_set 结构体各个字段的作用:

  1. ref: 引用计数器,用于计算有多少个进程在使用此 css_set
  2. list: 用于连接所有 css_set
  3. tasks: 由于可能存在多个进程同时受到相同的 cgroup 控制,所以用此字段把所有使用此 css_set 的进程连接起来。
  4. subsys: 用于收集各种 子系统 的统计信息结构。

进程描述符 task_struct 有两个字段与此相关,如下:

struct task_struct {
    ...
    struct css_set *cgroups;
    struct list_head cg_list;
    ...
}

可以看出,task_struct 结构的 cgroups 字段就是指向 css_set 结构的指针,而 cg_list 字段用于连接所有使用此 css_set 结构的进程列表。

task_struct 结构与 css_set 结构的关系如下图:

Linux 基础:cgroup 原理与实现

cgroup-task-cssset

cgroup_subsys 结构

CGroup 通过 cgroup_subsys 结构操作各个 子系统,每个 子系统 都要实现一个这样的结构,其定义如下:

struct cgroup_subsys {
    struct cgroup_subsys_state *(*create)(struct cgroup_subsys *ss,
                          struct cgroup *cgrp);
    void (*pre_destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
    void (*destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
    int (*can_attach)(struct cgroup_subsys *ss,
              struct cgroup *cgrp, struct task_struct *tsk);
    void (*attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
            struct cgroup *old_cgrp, struct task_struct *tsk);
    void (*fork)(struct cgroup_subsys *ss, struct task_struct *task);
    void (*exit)(struct cgroup_subsys *ss, struct task_struct *task);
    int (*populate)(struct cgroup_subsys *ss,
            struct cgroup *cgrp);
    void (*post_clone)(struct cgroup_subsys *ss, struct cgroup *cgrp);
    void (*bind)(struct cgroup_subsys *ss, struct cgroup *root);

    int subsys_id;
    int active;
    int disabled;
    int early_init;
    const char *name;
    struct cgroupfs_root *root;
    struct list_head sibling;
    void *private;
};

cgroup_subsys 结构包含了很多函数指针,通过这些函数指针,CGroup 可以对 子系统 进行一些操作。比如向 CGrouptasks 文件添加要控制的进程PID时,就会调用 cgroup_subsys 结构的 attach() 函数。当在 层级 中创建新目录时,就会调用 create() 函数创建一个 子系统 的资源控制统计信息对象 cgroup_subsys_state,并且调用 populate() 函数创建 子系统 相关的资源控制信息文件。

除了函数指针外,cgroup_subsys 结构还包含了很多字段,下面说明一下各个字段的作用:

  1. subsys_id: 表示了子系统的ID。
  2. active: 表示子系统是否被激活。
  3. disabled: 子系统是否被禁止。
  4. name: 子系统名称。
  5. root: 被附加到的层级挂载点。
  6. sibling: 用于连接被附加到同一个层级的所有子系统。
  7. private: 私有数据。

内存子系统 定义了一个名为 mem_cgroup_subsyscgroup_subsys 结构,如下:

struct cgroup_subsys mem_cgroup_subsys = {
    .name = "memory",
    .subsys_id = mem_cgroup_subsys_id,
    .create = mem_cgroup_create,
    .pre_destroy = mem_cgroup_pre_destroy,
    .destroy = mem_cgroup_destroy,
    .populate = mem_cgroup_populate,
    .attach = mem_cgroup_move_task,
    .early_init = 0,
};

另外 Linux 内核还定义了一个 cgroup_subsys 结构的数组 subsys,用于保存所有 子系统cgroup_subsys 结构,如下:

static struct cgroup_subsys *subsys[] = {
    cpuset_subsys,
    debug_subsys,
    ns_subsys,
    cpu_cgroup_subsys,
    cpuacct_subsys,
    mem_cgroup_subsys
};

CGroup 的挂载

前面介绍了 CGroup 相关的几个结构体,接下来我们分析一下 CGroup 的实现。

要使用 CGroup 功能首先必须先进行挂载操作,比如使用下面命令挂载一个 CGroup

$ mount -t cgroup -o memory memory /sys/fs/cgroup/memory

在上面的命令中,-t 参数指定了要挂载的文件系统类型为 cgroup,而 -o 参数表示要附加到此 层级 的子系统,上面表示附加了 内存子系统,当然可以附加多个 子系统。而紧随 -o 参数后的 memory 指定了此 CGroup 的名字,最后一个参数表示要挂载的目录路径。

挂载过程最终会调用内核函数 cgroup_get_sb() 完成,由于 cgroup_get_sb() 函数比较长,所以我们只分析重要部分:

static int cgroup_get_sb(struct file_system_type *fs_type,
     int flags, const char *unused_dev_name,
     void *data, struct vfsmount *mnt)
{
    ...
    struct cgroupfs_root *root;
    ...
    root = kzalloc(sizeof(*root), GFP_KERNEL);
    ...
    ret = rebind_subsystems(root, root->subsys_bits);
    ...

    struct cgroup *cgrp = &root->top_cgroup;

    cgroup_populate_dir(cgrp);
    ...
}

cgroup_get_sb() 函数会调用 kzalloc() 函数创建一个 cgroupfs_root 结构。cgroupfs_root 结构主要用于描述这个挂载点的信息,其定义如下:

struct cgroupfs_root {
    struct super_block *sb;
    unsigned long subsys_bits;
    unsigned long actual_subsys_bits;
    struct list_head subsys_list;
    struct cgroup top_cgroup;
    int number_of_cgroups;
    struct list_head root_list;
    unsigned long flags;
    char release_agent_path[PATH_MAX];
};

下面介绍一下 cgroupfs_root 结构的各个字段含义:

  1. sb: 掛載的檔案系統超級區塊。
  2. subsys_bits/actual_subsys_bits: 附加到此層級的子系統標誌。
  3. subsys_list: 附加到此層級的子系統(cgroup_subsys)清單。
  4. top_cgroup: 此層級的根cgroup。
  5. number_of_cgroups: 層級中有多少個cgroup。
  6. root_list: 連線系統中所有的cgroupfs_root。
  7. flags: 標誌位。

其中最重要的是subsys_listtop_cgroup 字段,subsys_list 表示了附加到此層級 的所有子系統,而top_cgroup 表示此層級 的根cgroup

接着调用 rebind_subsystems() 函数把挂载时指定要附加的 子系统 添加到 cgroupfs_root 结构的 subsys_list 链表中,并且为根 cgroupsubsys 字段设置各个 子系统 的资源控制统计信息对象,最后调用 cgroup_populate_dir() 函数向挂载目录创建 cgroup 的管理文件(如 tasks 文件)和各个 子系统 的管理文件(如 memory.limit_in_bytes 文件)。

CGroup 添加要进行资源控制的进程

通过向 CGrouptasks 文件写入要进行资源控制的进程PID,即可以对进程进行资源控制。例如下面命令:

$ echo 123012 > /sys/fs/cgroup/memory/test/tasks

tasks 文件写入进程PID是通过 attach_task_by_pid() 函数实现的,代码如下:

static int attach_task_by_pid(struct cgroup *cgrp, char *pidbuf)
{
    pid_t pid;
    struct task_struct *tsk;
    int ret;

    if (sscanf(pidbuf, "%d", &pid) != 1) // 读取进程pid
        return -EIO;

    if (pid) { // 如果有指定进程pid
        ...
        tsk = find_task_by_vpid(pid); // 通过pid查找对应进程的进程描述符
        if (!tsk || tsk->flags & PF_EXITING) {
            rcu_read_unlock();
            return -ESRCH;
        }
        ...
    } else {
        tsk = current; // 如果没有指定进程pid, 就使用当前进程
        ...
    }

    ret = cgroup_attach_task(cgrp, tsk); // 调用 cgroup_attach_task() 把进程添加到cgroup中
    ...
    return ret;
}

attach_task_by_pid() 函数首先会判断是否指定了进程pid,如果指定了就通过进程pid查找到进程描述符,如果没指定就使用当前进程,然后通过调用 cgroup_attach_task() 函数把进程添加到 cgroup 中。

我们接着看看 cgroup_attach_task() 函数的实现:

int cgroup_attach_task(struct cgroup *cgrp, struct task_struct *tsk)
{
    int retval = 0;
    struct cgroup_subsys *ss;
    struct cgroup *oldcgrp;
    struct css_set *cg = tsk->cgroups;
    struct css_set *newcg;
    struct cgroupfs_root *root = cgrp->root;

    ...
    newcg = find_css_set(cg, cgrp); // 根据新的cgroup查找css_set对象
    ...
    rcu_assign_pointer(tsk->cgroups, newcg); // 把进程的cgroups字段设置为新的css_set对象
    ...
    // 把进程添加到css_set对象的tasks列表中
    write_lock(&css_set_lock);
    if (!list_empty(&tsk->cg_list)) {
        list_del(&tsk->cg_list);
        list_add(&tsk->cg_list, &newcg->tasks);
    }
    write_unlock(&css_set_lock);

    // 调用各个子系统的attach函数
    for_each_subsys(root, ss) {
        if (ss->attach)
            ss->attach(ss, cgrp, oldcgrp, tsk);
    }
    ...
    return 0;
}

cgroup_attach_task() 函数首先会调用 find_css_set() 函数查找或者创建一个 css_set 对象。前面说过 css_set 对象用于收集不同 cgroup 上附加的 子系统 资源统计信息对象。

因为一个进程能够被加入到不同的 cgroup 进行资源控制,所以 find_css_set() 函数就是收集进程所在的所有 cgroup 上附加的 子系统 资源统计信息对象,并返回一个 css_set 对象。接着把进程描述符的 cgroups 字段设置为这个 css_set 对象,并且把进程添加到这个 css_set 对象的 tasks 链表中。

最后,cgroup_attach_task() 函数会调用附加在 层级 上的所有 子系统attach() 函数对新增进程进行一些其他的操作(这些操作由各自 子系统 去实现)。

限制 CGroup 的资源使用

本文主要是使用 内存子系统 作为例子,所以这里分析内存限制的原理。

可以向 cgroupmemory.limit_in_bytes 文件写入要限制使用的内存大小(单位为字节),如下面命令限制了这个 cgroup 只能使用 1MB 的内存:

$ echo 1048576 > /sys/fs/cgroup/memory/test/memory.limit_in_bytes

memory.limit_in_bytes 写入数据主要通过 mem_cgroup_write() 函数实现的,其实现如下:

static ssize_t mem_cgroup_write(struct cgroup *cont, struct cftype *cft,
                struct file *file, const char __user *userbuf,
                size_t nbytes, loff_t *ppos)
{
    return res_counter_write(&mem_cgroup_from_cont(cont)->res,
                cft->private, userbuf, nbytes, ppos,
                mem_cgroup_write_strategy);
}

其主要工作就是把 内存子系统 的资源控制对象 mem_cgroupres.limit 字段设置为指定的数值。

限制进程使用资源

当设置好 cgroup 的资源使用限制信息,并且把进程添加到这个 cgrouptasks 列表后,进程的资源使用就会受到这个 cgroup 的限制。这里使用 内存子系统 作为例子,来分析一下内核是怎么通过 cgroup 来限制进程对资源的使用的。

当进程要使用内存时,会调用 do_anonymous_page() 来申请一些内存页,而 do_anonymous_page() 函数会调用 mem_cgroup_charge() 函数来检测进程是否超过了 cgroup 设置的资源限制。而 mem_cgroup_charge() 最终会调用 mem_cgroup_charge_common() 函数进行检测,mem_cgroup_charge_common() 函数实现如下:

static int mem_cgroup_charge_common(struct page *page, struct mm_struct *mm,
                gfp_t gfp_mask, enum charge_type ctype)
{
    struct mem_cgroup *mem;
    ...
    mem = rcu_dereference(mm->mem_cgroup); // 获取进程对应的内存限制对象
    ...
    while (res_counter_charge(&mem->res, PAGE_SIZE)) { // 判断进程使用内存是否超出限制
        if (!(gfp_mask & __GFP_WAIT))
            goto out;

        if (try_to_free_mem_cgroup_pages(mem, gfp_mask)) // 如果超出限制, 就释放一些不用的内存
            continue;

        if (res_counter_check_under_limit(&mem->res))
            continue;

        if (!nr_retries--) {
            mem_cgroup_out_of_memory(mem, gfp_mask); // 如果尝试过5次后还是超出限制, 那么发出oom信号
            goto out;
        }
        ...
    }
    ...
}

mem_cgroup_charge_common() 函数会对进程内存使用情况进行检测,如果进程已经超过了 cgroup 设置的限制,那么就会尝试进行释放一些不用的内存,如果还是超过限制,那么就会发出 OOM (out of memory) 的信号。

以上是Linux 基礎:cgroup 原理與實作的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:良许Linux教程网。如有侵權,請聯絡admin@php.cn刪除
Linux的主要目的是什麼?Linux的主要目的是什麼?Apr 16, 2025 am 12:19 AM

Linux的主要用途包括:1.服務器操作系統,2.嵌入式系統,3.桌面操作系統,4.開發和測試環境。 Linux在這些領域表現出色,提供了穩定性、安全性和高效的開發工具。

互聯網在Linux上運行嗎?互聯網在Linux上運行嗎?Apr 14, 2025 am 12:03 AM

互聯網運行不依賴單一操作系統,但Linux在其中扮演重要角色。 Linux廣泛應用於服務器和網絡設備,因其穩定性、安全性和可擴展性受歡迎。

Linux操作是什麼?Linux操作是什麼?Apr 13, 2025 am 12:20 AM

Linux操作系統的核心是其命令行界面,通過命令行可以執行各種操作。 1.文件和目錄操作使用ls、cd、mkdir、rm等命令管理文件和目錄。 2.用戶和權限管理通過useradd、passwd、chmod等命令確保系統安全和資源分配。 3.進程管理使用ps、kill等命令監控和控制系統進程。 4.網絡操作包括ping、ifconfig、ssh等命令配置和管理網絡連接。 5.系統監控和維護通過top、df、du等命令了解系統運行狀態和資源使用情況。

使用Linux別名提高自定義命令快捷方式的生產率使用Linux別名提高自定義命令快捷方式的生產率Apr 12, 2025 am 11:43 AM

介紹 Linux是一個強大的操作系統,由於其靈活性和效率,開發人員,系統管理員和電源用戶都喜歡。但是,經常使用長而復雜的命令可能是乏味的

Linux實際上有什麼好處?Linux實際上有什麼好處?Apr 12, 2025 am 12:20 AM

Linux適用於服務器、開發環境和嵌入式系統。 1.作為服務器操作系統,Linux穩定高效,常用於部署高並發應用。 2.作為開發環境,Linux提供高效的命令行工具和包管理系統,提升開發效率。 3.在嵌入式系統中,Linux輕量且可定制,適合資源有限的環境。

在Linux上掌握道德黑客的基本工具和框架在Linux上掌握道德黑客的基本工具和框架Apr 11, 2025 am 09:11 AM

簡介:通過基於Linux的道德黑客攻擊數字邊界 在我們越來越相互聯繫的世界中,網絡安全至關重要。 道德黑客入侵和滲透測試對於主動識別和減輕脆弱性至關重要

如何學習Linux基礎知識?如何學習Linux基礎知識?Apr 10, 2025 am 09:32 AM

Linux基礎學習從零開始的方法包括:1.了解文件系統和命令行界面,2.掌握基本命令如ls、cd、mkdir,3.學習文件操作,如創建和編輯文件,4.探索高級用法如管道和grep命令,5.掌握調試技巧和性能優化,6.通過實踐和探索不斷提陞技能。

Linux最有用的是什麼?Linux最有用的是什麼?Apr 09, 2025 am 12:02 AM

Linux在服務器、嵌入式系統和桌面環境中的應用廣泛。 1)在服務器領域,Linux因其穩定性和安全性成為託管網站、數據庫和應用的理想選擇。 2)在嵌入式系統中,Linux因其高度定制性和高效性而受歡迎。 3)在桌面環境中,Linux提供了多種桌面環境,滿足不同用戶需求。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。