mpam linux kernel源码分析

MPAM (Memory System Resource Partitioning and Monitoring)是Armv8.4的feature,用于cache和内存带宽的监控和限制。截至现在,该feature在linux kernel的实现还在推进,最新一版参见https://git.kernel.org/pub/scm/linux/kernel/git/morse/linux.git/log/?h=mpam/snapshot/v6.5-rc1。要了解mpam的原理参见https://developer.arm.com/documentation/ddi0598/latest/

MPAM原理

mpam是和intel rdt类似的feature。mpam的目的是在进程级别对cache和内存带宽的使用做限制。要实现这个目标就要搞清楚几个问题:

  1. 谁来发出限制请求?
  2. 谁来执行?
  3. 限制谁?
  4. 怎么限制?

计算机里干活的是PE,这个请求自然由它来担任,当然也可以是device。具体执行部件是MSC(Memory-System Component)。被限制的主体自然是前面提到的进程。怎么限制是指策略,比如按百分比限制内存带宽,按way限制cache。搞清楚这几个问题,mpam实现的基本框架也就出来了:首先构建进程ID与限制策略的映射,这里对于每个需要限制的pid都对应一个PARTID;将PARTID保存在PE的某个寄存器里(MPAMn_ELx),当PE访问MSC时携带该信息,MSC收到该信息就会找到对应的策略进行限制。为了可以在用户态使用mpam,kernel复用了intel rdt的resctlfs。下面简单展示一下如何使用mpam。放一张图在这里:

MPAM的使用

目前只有最新的arm机器可能带有MPAM的硬件支持,能够支持MPAM的OS可能只有欧拉和龙蜥。本测试基于倚天+龙蜥。

1. 安装龙蜥OS;

2. 查看是否存在/sys/fs/resctrl/,如果没有可能时没kernel config没打开;

3. 如果resctrlfs里面是空的,需要先mount,

mount -t resctrl resctrl /sys/fs/resctrl

之后会看到在该目录下自动生成多个文件。

4,在resctrl下创建一个目录

# mkdir test
# ls test
cpus  cpus_list  id  mode  mon_data  mon_groups  schemata  size  tasks

这里最重要的是tasks和schemata,tasks用于存放需要做限制的进程pid,schemata用于设置限制策略。

查看schemata:

# cat schemata
MB:0= 100;1= 100
L3:0=ffff;1=ffff

  MB代表memory bandwidth,后面0,1代表numa node,100代表100%,也就是内存带宽的百分比; L3代表L3cache,0,1代表numa node,f代表bitmap,也就是每个cache有16way,全1代表占用所有way。

5. 修改schemata

# echo "MB:0= 50;1= 50" > schemata
# echo "L3:0=1;1=1" > schemata

现在我改成占用50%的内存带宽和1路cache。

6. 加入进程ID

准备工作好了,现在开始对task进行限制。

当前所有的pid默认都放在resctrl top dir下面的tasks里面,我们可以从那里面找一个进程写到本层目录的tasks里面。

比如100

# cat ../tasks | grep 100
100
1000
1001
# echo 100 > tasks
# cat tasks
100 #
cat ../tasks | grep 100 1000 1001

可见一旦我们把一个pid写到某个tasks里面,pid就从原来的tasks里面消失了。这是正确的,毕竟对一个task实施两种策略是不合理的。

 这样就完成了对task的限制操作,非常简单。

MPAM源码分析

进入到最难的部分,源码分析。首先clone代码:

git clone https://git.kernel.org/pub/scm/linux/kernel/git/morse/linux.git
git checkout mpam_6.5_rc1

Jams的patch有上百个,看patch非常耗时,参考https://neoverse-reference-design.docs.arm.com/en/main/mpam/mpam-resctrl.html可以更好的理解mpam的实现脉络。

MPAM涉及到从TFA,uefi/acpi/fdt到Linux初始化,链条比较长,我们先忽略固件部分,只看kernel层面做了什么。
首先要知道MPAM是硬件特性,它的描述在ACPI或者fdt里面,MPAM的初始化必然涉及到parse acpi/fdt。MPAM位于片上,属于platform device,涉及platform device初始化。MPAM最终是暴露resctrlfs接口给用户,又涉及到resctrl部分。
首先在kernel最开始boot阶段会对mpam寄存器做初始化:

// arch/arm64/include/asm/el2_setup.h.macro __init_el2_mpam
#ifdef CONFIG_ARM64_MPAM
        /* Memory Partioning And Monitoring: disable EL2 traps */
        mrs     x1, id_aa64pfr0_el1
        ubfx    x0, x1, #ID_AA64PFR0_EL1_MPAM_SHIFT, #4
        cbz     x0, 1f                          // skip if no MPAMmsr_s   SYS_MPAM0_EL1, xzr              // use the default partition..msr_s   SYS_MPAM2_EL2, xzr              // ..and disable lower traps        msr_s   SYS_MPAM1_EL1, xzr
        mrs_s   x0, SYS_MPAMIDR_EL1
        tbz     x0, #17, 1f                     // skip if no MPAMHCR regmsr_s   SYS_MPAMHCR_EL2, xzr            // clear TRAP_MPAMIDR_EL1 -> EL21:
#endif/* CONFIG_ARM64_MPAM */
.endm

首先探测一下mpam是不是支持,如果支持就将MPAMn_ELx初始化为0.需要注意的是这里的寄存器都带了SYS_前缀,这些都是自动生成的寄存器宏,在spec中的名字不带SYS_前缀。
mpam的主要初始化工作是在init_call阶段完成的。
在源码中搜索跟mpam相关的initcall可以得到如下结果:

arch_initcall(arm64_mpam_register_cpus)
subsys_initcall_sync(acpi_mpam_parse)
subsys_initcall(mpam_msc_driver_init)

initcall的顺序是arch_initcall -> subsys_initcall -> subsys_initcall_sync
先看看arm64_mpam_register_cpus里面做了啥。

staticint__init arm64_mpam_register_cpus(void)
{
    u64 mpamidr = read_sanitised_ftr_reg(SYS_MPAMIDR_EL1);
    u16 partid_max = FIELD_GET(MPAMIDR_PARTID_MAX, mpamidr);
    u8 pmg_max = FIELD_GET(MPAMIDR_PMG_MAX, mpamidr);
return mpam_register_requestor(partid_max, pmg_max);
}

从MPAMIDR中读取partid和pmg的最大支持数量,这个只是PE能够支持的最大数量,不是最终的最大值,还要收到msc的限制。当前获得的partid和pmg的最大值被放在了
mpam_pmg_max和mpam_partid_max两个全局变量中。
接下来看看mpam_msc_driver_init里面做了啥:

mpam_msc_driver_init
|- acpi_mpam_count_msc
|- debugfs_create_dir
|- platform_driver_register(&mpam_msc_driver)

忽略fdt部分,从acpi中获取msc的数量结果放入全局变量fw_num_msc中。为mpam创建debugfs dir方便暴露数据到用户态。最后注册一下mpam_msc_driver。

mpam_msc_driver:

static struct platform_driver mpam_msc_driver = {
    .driver = {
        .name = "mpam_msc",
        .of_match_table = of_match_ptr(mpam_of_match),
    },
    .probe = mpam_msc_drv_probe,
    .remove = mpam_msc_drv_remove,
};

 

 platform_driver_register会匹配与driver相关的device,如果成功就会调用driver中的probe函数,也即:mpam_msc_drv_probe。由于系统中可能存在多个msc,因此在在匹配device的时候会多次调用probe函数。

static int mpam_msc_drv_probe(struct platform_device *pdev)
{
    ...
    mutex_lock(&mpam_list_lock);
    do {
        msc = devm_kzalloc(&pdev->dev, sizeof(*msc), GFP_KERNEL);
        msc->id = mpam_num_msc++;
         err = get_msc_affinity(msc);
        mutex_init(&msc->lock);
        INIT_LIST_HEAD_RCU(&msc->ris);
        spin_lock_init(&msc->part_sel_lock);

        err = mpam_msc_setup_error_irq(msc);
        if (device_property_read_u32(&pdev->dev, "pcc-channel",
                         &msc->pcc_subspace_id))
            msc->iface = MPAM_IFACE_MMIO;
        else
            msc->iface = MPAM_IFACE_PCC;

        if (msc->iface == MPAM_IFACE_MMIO) {
            io = devm_platform_get_and_ioremap_resource(pdev, 0,
                                    &msc_res);
            msc->mapped_hwpage_sz = msc_res->end - msc_res->start;
            msc->mapped_hwpage = io;
        }         list_add_rcu(&msc->glbl_list, &mpam_all_msc);
        platform_set_drvdata(pdev, msc);
        msc->debugfs = debugfs_create_dir(name, mpam_debugfs);
        debugfs_create_x32("max_nrdy_usec", 0400, msc->debugfs, &msc->nrdy_usec);
    } while (0);
    mutex_unlock(&mpam_list_lock);
 
    err = acpi_mpam_parse_resources(msc, plat_data);
 if (!err && fw_num_msc == mpam_num_msc)
        mpam_register_cpuhp_callbacks(&mpam_discovery_cpu_online);

    return err;
}

这个函数比较长,删除错误检查还是有点长。里面主要的任务是:

  • 为每个msc实体创建一个msc数据结构;
  • 从device中拿到msc的MMIO地址空间(忽略PCC的情况),每个msc都会把自己相关的寄存器map到内存中,作为配置msc的接口,主要是MPAMCFG*寄存器;
  • 设置error irq;
  • 将msc链接到mpam_all_msc;
  • 从acpi表中获取信息填充到msc中;
  • 自增mpam_num_msc,当所有msc都probe时注册cpuhp回调函数。

看一下mpam_discovery_cpu_online做了什么。

static int mpam_discovery_cpu_online(unsigned int cpu)
{
    mutex_lock(&mpam_list_lock);
    list_for_each_entry(msc, &mpam_all_msc, glbl_list) {
        mutex_lock(&msc->lock);
        if (!msc->probed)
            err = mpam_msc_hw_probe(msc);
        mutex_unlock(&msc->lock);
    }
    mutex_unlock(&mpam_list_lock);

    if (new_device_probed && !err)
        schedule_work(&mpam_enable_work);

return mpam_cpu_online(cpu);
}

上面的那段循环的负责配置资源控制,逻辑在mpam spec的11.1.2.简单的说就是为每个partid做配置。

来看看mpam_msc_hw_probe都做了什么

for (ris_idx = 0; ris_idx <= msc->ris_max; ris_idx++) {
       ...
        spin_lock(&msc->part_sel_lock);
        __mpam_part_sel(ris_idx, 0, msc);
        mpam_ris_hw_probe(ris);
        spin_unlock(&msc->part_sel_lock);
    }

因为目前只有一个partid 0,所以只需为每个msc配置一次即可,但是一个msc上可以有多个ris(Resource instance selection)也就是一个msc的多个实例。所以需要循环给每个ris做配置。

看看具体做配置的mpam_ris_hw_probe都做了什么。

/* Cache Capacity Partitioning */
    if (FIELD_GET(MPAMF_IDR_HAS_CCAP_PART, ris->idr)) {
        u32 ccap_features = mpam_read_partsel_reg(msc, CCAP_IDR);

        props->cmax_wd = FIELD_GET(MPAMF_CCAP_IDR_CMAX_WD, ccap_features);
        if (props->cmax_wd)
            mpam_set_feature(mpam_feat_ccap_part, props);
    }
...

这个函数很长,取一个代表,可以看到这就是具体配置监控和限制策略的地方,给每个ris配置成默认的资源限制策略。上面的代码时cache capacity相关,还有Cache Portion,Memory bandwidth等。

回到mpam_discovery_cpu_online,继续看mpam_enable_work, mpam_cpu_online。

mpam_enable_work是一个workqueue,工作函数流程是mpam_enable->mpam_enable_once。

static void mpam_enable_once(void)
{
...
    mpam_resctrl_setup();
...
    cpuhp_remove_state(mpam_cpuhp_state);
...
    static_branch_enable(&mpam_enabled);
    mpam_register_cpuhp_callbacks(mpam_cpu_online);
}

 

mpam_enable_once做很多初始化的工作,其中最重要的是mpam_resctrl_setup,即setup resctrl。resctrl_init最终会创建mount点. 然后之前setup的cpuhp回调将会被替换成mpam_cpu_oneline。此时partid和pmgid的最大值将稳定下来不能再更改。当cpu online是会依每个partid对mscreprogram。mpam_enabled被设置成true标志mpam已经enable了。

有关resctrl的代码在fs/resctrl/下面,与mpam相关的代码在drivers/platform/mpam/mpam_resctrl.c。比如在resctrlfs下面创建dir,就会调用里面相关代码。

以上是有关mpam初始化的相关事项。要使用mpam必须让partid与进程相联系,这种联系必须反应在有关task的数据结构。比如task运行的时候需要将自己的partid写到MPAM_ELx里面,如果发生schedule,要保存自己的partid。task_struct和thread_info是有关task的数据结构,thread_info是跟架构相关的,因此更适合放partid。

struct thread_info {
...
#ifdef CONFIG_ARM64_MPAM
    u64            mpam_partid_pmg;
#endif
}

当向resctrl下的tasks文件写pid时,会产生同步异常,将更新该task的partid。rdtgroup_file_write->rdtgroup_tasks_write->resctrl_arch_set_closid_rmid。

在发生进程切换时,__switch_to会调用mpam_thread_switch,最后将partid写入MPAM0_EL1.

static inline void mpam_thread_switch(struct task_struct *tsk)
{
...
    write_sysreg_s(regval, SYS_MPAM0_EL1);
}

 

到此差不多分析完了,水平所限,很多分析不到位,暂时也只能这样了。

 

posted @ 2023-09-12 20:39  半山随笔  阅读(657)  评论(0编辑  收藏  举报