ctypes学习 + GearDVFS源码分析(/src/perf_lib)

发布时间 2023-10-17 17:08:27作者: ZCry

  最近在尝试复现GearDVFS的代码,代码结构很复杂,由于需要获取硬件信息,作者使用ctypes实现与底层的交互,任务紧迫,开始学习吧!

1. ctypes介绍

  资料的来源已经放在了后文的链接中,由于我的基础实在很薄弱,因此看了很多资料才搞懂ctypes的实现原理,如果有和我一样的菜鸟,在学习之前可以先了解一下:

1. C语言的编译原理
2. gcc的基本使用
3. Windows API 和 POSIX

  已经修成正佛的请忽略这些 Q_Q 。

1.1 为什么需要ctypes

  C语言可以对内存、文件接口等底层资源进行访问,因此当我们想获取一些底层信息来作为训练数据时就需要调用C的方法,同时当运算量较大时,可以使用C语言提升代码性能。Python调用C程序通常有三种方法:

  1. Cython:将 Python 代码转化为 C 代码。
  2. ctypes:加载底层动态链接库(.dll或.so),并使用其中的函数。
  3. SWIG:将 C/C++ 代码包装成 Python 库的工具。

  GearDVFS选择使用ctypes实现。

1.2 ctypes基本使用

  想要实现C语言和Python的交互,C代码需要使用Python的C API,而Python代码则需要使用ctypes库。资料来源[2]介绍了ctypes的原理,简单的来说,就是使用不同平台(Windows/Linux)API中提供的动态加载动态链接库的方法来达到链接的目的。ctypes就像C语言和Python语言之间的转换器一样,定义了一些数据类型,函数调用方法等作为C和Python的转换,即C <-> ctypes <-> Python,为方便阅读,我把一部分内容搬运了过来,想要详细了解的可以直接跳转到来源资料[3]和[4]中,建议先看官网的,不懂再看别人解释的。

1.2.1 加载动态链接库

  在linux下,有两种方式加载动态链接库:

    # 使用 dll 加载程序的方法
    cdll.LoadLibrary("libc.so.6")  
    # 调用创建 CDLL 实例来加载库
    libc = CDLL("libc.so.6")       
    libc                           

1.2.2 加载的 dll 访问函数

  访问函数使用库.函数名

    from ctypes import *
    libc.printf

    print(windll.kernel32.GetModuleHandleA)  

    print(windll.kernel32.MyOwnFunction)    

1.2.3 ctypes的数据类型

  ctypes和C以及Python的对应关系如下:

ctypes数据类型

  数组、指针等定义如下:

    # int类型
    num = ctypes.c_int(1992)
    # int指针
    ctypes.POINTER(ctypes.c_int)
    # int数组
    int_arr = ctypes.c_int * 5  # 相当于C语言的 int[5]
    # 可以指定数据来初始化数组
    my_arr = int_arr(1, 3, 5, 7, 9)

1.2.4 ctypes函数调用

  调用函数可以设置参数和返回类型,调用的方式和访问是一样的:

    # 调用可变参数类函数
    libc.printf.argtypes = [ctypes.c_char_p]
    # 自定义数据类型调用函数,下面这个例子就设置了printd的参数类型
    printf.argtypes = [c_char_p, c_char_p, c_int, c_double]
    printf(b"String '%s', Int %d, Double %f\n", b"Hi", 10, 2.2)
    # 默认情况下,假定函数返回 C int 类型,其他可以通过设置

2. GearDVFS源码分析(/src/perf_lib)

  GearDVFS的项目框架如下:

Alt text

  项目由三部分组成,各部分大致负责的是,perf_lib用于采集cpu利用率等硬件信息,Perf_Moniter写了一个客户端用于监测采集的数据,Perf_Trainer用于训练模型。perf_lib下有四个文件,分别是install.shPyPerf.pysys_perf.csys_perf.sosys_perf.sosys_perf.c编译生成的动态链接库,我们主要看PyPerf.pysys_perf.c

2.1 sys_perf.c

  perf_event.h头文件中包含了一些用于性能计数器的宏定义、数据结构和函数声明,以便于使用和操作性能计数器,对于每一个性能事件的监测,我们都需要配置性能计数器。头文件中的一些常见定义和声明包括:

  • struct perf_event_attr:这是一个结构体,用于描述性能事件的属性。它包含了事件类型、计数器的配置和其他相关的信息。
  • perf_event_open函数:这个函数用于创建和配置一个性能计数器事件。通过传递合适的参数,可以指定需要监测的事件类型,创建一个性能计数器的实例,并返回一个文件描述符,用于后续的操作和访问。
  • ioctl函数:这个函数用于在打开的性能计数器文件描述符上执行控制操作。通过传递不同的控制命令和参数,可以对性能计数器进行启动、停止、重置、读取计数器值等操作。
  • 相关的宏定义PERF_TYPE_HARDWARE、PERF_COUNT_HW_CPU_CYCLESPERF_COUNT_SW_CPU_CLOCK等宏定义,定义了不同类型的性能事件和事件计数器。

  sys_perf.c实现了对perf_event_attr的配置,建议配合源码一起看,我的介绍逻辑可能会有点混乱,有意见感谢提出。代码定义了两个结构体变量和PerfEvent类型数组,具体含义见注释:

    //性能指标
    typedef struct{
        //名称
        char* name;
        //缩写
        char* abbrev;
        //数据
        int val;
    } PerfEvent;

    //配置计数器值的格式
    struct read_format {
        //values的长度
        uint64_t nr;
        //可变长数组
        struct {
            uint64_t value;
            uint64_t id;
        } values[];
    };

    //性能数据列表,GearDVFS使用的性能数据与HWCPipe的性能指标相似
    //PERF_COUNT_HW_CPU_CYCLES等的具体含义后面会介绍
    const PerfEvent EVENT_LIST[] = {
        {"PERF_COUNT_HW_CPU_CYCLES", "cycles", PERF_COUNT_HW_CPU_CYCLES},
        {"PERF_COUNT_HW_INSTRUCTIONS", "instructions", PERF_COUNT_HW_INSTRUCTIONS},
        {"PERF_COUNT_HW_CACHE_REFERENCES",  "cache-ref", PERF_COUNT_HW_CACHE_REFERENCES},
        {"PERF_COUNT_HW_CACHE_MISSES", "cache-miss", PERF_COUNT_HW_CACHE_MISSES},
        {"PERF_COUNT_HW_STALLED_CYCLES_FRONTEND", "stalled-cycles-front", PERF_COUNT_HW_STALLED_CYCLES_FRONTEND},
        {"PERF_COUNT_HW_STALLED_CYCLES_BACKEND", "stalled-cycles-back", PERF_COUNT_HW_STALLED_CYCLES_BACKEND},
        {"PERF_COUNT_HW_BRANCH_MISSES", "branch-miss", PERF_COUNT_HW_BRANCH_MISSES},
    };

2.1.1 perf_event_open函数

    #include <linux/perf_event.h>
    #include <asm/unistd.h>
    #include <Python.h>

    static long
    perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                int cpu, int group_fd, unsigned long flags)
    {
    int ret;
    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                    group_fd, flags);
    return ret;
    }

  静态函数static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,int cpu, int group_fd, unsigned long flags)实现了对perf_event_open的系统调用,返回值是新的文件描述符,如果发生错误则返回错误码-1,参数为:

  • pid_t pid:进程ID,指定创建性能计数器的进程。
  • int cpu:CPU编号,指定创建性能计数器的CPU。
  • int group_fd:性能计数器分组的文件描述符,用于将多个计数器分组在一起。如果不需要分组,则传入-1。
  • unsigned long flags:标志位,添加附加功能。
  • struct perf_event_attr *hw_event:指向perf_event_attr结构体的指针,用于配置性能计数器的属性,对于每一个CPU和每一个指标,我们都需要配置一个perf_event:
    • type:指定性能计数器的类型,例如指令计数器、缓存事件计数器等。
    • size:结构体的大小,在初始化时需要设置为sizeof(struct perf_event_attr)。
    • config:指定具体的计数器配置,根据不同的type和硬件平台而有所不同。
    • sample_period:设置性能采样的周期,即每隔多少事件进行一次采样。例如,设置为100表示每100个事件进行一次采样。
    • sample_type:指定采样的类型,如指令指针、用户态/内核态标志等。
    • read_format:指定计数器值的格式。
    //perf_event_attr部分字段
    struct perf_event_attr { 
        __u32     type;         /* Type of event */
        __u32     size;         /* Size of attribute structure */ 
        __u64     config;       /* Type-specific configuration */
        union { 
            __u64 sample_period;    /* Period of sampling */
            __u64 sample_freq;      /* Frequency of sampling */ 
        };
        /*
        ......
        */
    };

    //type字段,GearDVFS的字段设置为了通用硬件事件
   enum perf_type_id { /* perf 类型 */
        PERF_TYPE_HARDWARE			= 0,    /* 通用硬件事件之一 */
        PERF_TYPE_SOFTWARE			= 1,    /* 内核提供的一种软件定义的事件 */
        PERF_TYPE_TRACEPOINT		= 2,    /* 内核跟踪点基础结构提供的跟踪点 /sys/bus/event_source/devices/tracepoint/type */
        PERF_TYPE_HW_CACHE			= 3,    /* 硬件cache */
        PERF_TYPE_RAW				= 4,    /* RAW/CPU /sys/bus/event_source/devices/cpu/type */
        PERF_TYPE_BREAKPOINT		= 5,    /* 断点 /sys/bus/event_source/devices/breakpoint/type */
        PERF_TYPE_MAX,				/* non-ABI */
    };

    //常见的硬件事件,可以看到EVENT_LIST就是从这里面选择的
    enum perf_hw_id {
        /*
        * Common hardware events, generalized by the kernel:
        */
        PERF_COUNT_HW_CPU_CYCLES		= 0,
        PERF_COUNT_HW_INSTRUCTIONS		= 1,
        PERF_COUNT_HW_CACHE_REFERENCES		= 2,
        PERF_COUNT_HW_CACHE_MISSES		= 3,
        PERF_COUNT_HW_BRANCH_INSTRUCTIONS	= 4,
        PERF_COUNT_HW_BRANCH_MISSES		= 5,
        PERF_COUNT_HW_BUS_CYCLES		= 6,
        PERF_COUNT_HW_STALLED_CYCLES_FRONTEND	= 7,
        PERF_COUNT_HW_STALLED_CYCLES_BACKEND	= 8,
        PERF_COUNT_HW_REF_CPU_CYCLES		= 9,
        PERF_COUNT_HW_MAX,			/* non-ABI */
    };

  更多内容请参考资料来源[6]。

2.1.2 EVENT_LIST字段get函数

  PyObject* get_supported_names()函数、PyObject* get_supported_events()函数、PyObject* get_supported_abbrevs()函数提供了Python获取EVENT_LIST各字段的接口,三个函数的实现方式相同,使用Python的C API来创建和操作Python对象,返回值为PyObject指针指向Python列表对象,无参数。

    PyObject*
    get_supported_names()
    {   
        //计算指标数量
        int length = sizeof(EVENT_LIST)/sizeof(EVENT_LIST[0]);
        PyObject* name_list = PyList_New(length);
        for (int i = 0; i < length; ++i) {
            PyList_SetItem(name_list,i,Py_BuildValue("s",EVENT_LIST[i].name));
        }
        return name_list;    
    }

2.1.3 sys_perf函数

  共有n_event个性能事件和n_cpu个CPU,那么对于每一个CPU都需要配置和储存n_event个事件结果,因此定义并初始化了二维指针数组 fdsidsbufsrfs,它们的大小都是n_cpu*n_event。fds存放的是每一个perf_event_open系统调用后的返回值,也就是文件描述符;rfs存放的是每一个事件的read_format的地址;bufs是4096字节的缓冲区大小,后续将放置read_formatrfs指向该缓冲区;pea为一个perf_event_attr结构体对象;ids用于存放每一个事件的ID。

    Py_ssize_t n_event = PyList_Size(events);
    Py_ssize_t n_cpu = PyList_Size(cpus);
    int N_COUNTER = n_cpu*n_event;
    PyObject* result_list = PyList_New(n_cpu);

    // initialization
    struct perf_event_attr pea;
    struct read_format **rfs = (struct read_format**)calloc(n_cpu, sizeof(struct read_format*));
    int **fds = (int **)calloc(n_cpu, sizeof(int *));
    uint64_t **ids = (uint64_t **)calloc(n_cpu, sizeof(uint64_t *));
    char **bufs = (char **)calloc(n_cpu, sizeof(char *));

    for (int i=0; i < n_cpu; i++) {
        fds[i] = (int*)calloc(N_COUNTER,sizeof(int));//这里感觉有点小问题,应该分配n_event大小的空间
        ids[i] = (uint64_t*)calloc(N_COUNTER,sizeof(uint64_t));
        bufs[i] = (char*)calloc(4096,sizeof(char));
        rfs[i] = (struct read_format*) bufs[i];
    }

  对每一个CPU的每一个事件进行初始化,将pea的内存清零,接下来设置pea的各个字段:type字段为硬件事件;sizeperf_event_attr大小;config为EVENT_LIST中的各事件;disabled = 1表示性能事件会被创建但处于禁用状态,不会立即开始计数;exlude_kernel表示不排除内核空间的代码执行;·exclude_hv排除虚拟化环境(Hypervisor)的代码执行;read_format的设置中PERF_FORMAT_GROUP是一个标志,用于指示性能计数器的事件被分组,也就是多个事件被同时计数,并且在读取计数器值时以一组数据的形式返回,PERF_FORMAT_ID是另一个标志,用于指示每个计数器事件的 ID 也被返回。

  那么我们就可以看到下面的代码,对于第一个事件而言,先不分组,返回的文件标识符作为后续事件的group_fd传入,效果就是事件以CPU的编号作为分组,ioctl系统调用将性能计数器的事件 ID 存储到ids数组中。

    // for each cpu and each hardware event
    for (int i = 0; i < n_cpu; i++) {
        int cpu_index = (int)PyLong_AsLong(PyList_GetItem(cpus,i));
        for (int j = 0; j < n_event; j++) {
            int event_index = (int)PyLong_AsLong(PyList_GetItem(events,j));
            memset(&pea, 0, sizeof(struct perf_event_attr));
            pea.type = PERF_TYPE_HARDWARE;
            pea.size = sizeof(struct perf_event_attr);
            pea.config = EVENT_LIST[event_index].val;
            pea.disabled = 1;
            pea.exclude_kernel = 0;
            pea.exclude_hv = 1;
            pea.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID;
            if (j == 0) {
                fds[i][j] = syscall(__NR_perf_event_open, &pea, -1, cpu_index, -1, 0);
                // fprintf(stderr,"%d,%d,%d\n",i,j,fds[i][j]);
            } else {
                fds[i][j] = syscall(__NR_perf_event_open, &pea, -1, cpu_index, fds[i][0], 0);
            }
            if (fds[i][j] == -1) {
                fprintf(stderr,"Error opening leader %llx\n", pea.config);
                exit(EXIT_FAILURE);
            }
            ioctl(fds[i][j], PERF_EVENT_IOC_ID, &ids[i][j]);
        }
    }

  下面的代码里面使用了几个ioctl的命令:PERF_EVENT_IOC_RESET重置性能计数器;PERF_EVENT_IOC_ENABLE启动性能计数器;PERF_EVENT_IOC_DISABLE禁用性能计数器;PERF_IOC_FLAG_GROUP以组的方式启动。根据ioctl获得的id去read_format中查找可变数组values的id即可找到对应值,保存为Python对象。

    // monitoring for each cpu group
    for (int i=0; i < n_cpu; i++) {
        ioctl(fds[i][0], PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP);
        ioctl(fds[i][0], PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP);   
    }
    usleep(micro_seconds);
    for (int i=0; i < n_cpu; i++) {
        ioctl(fds[i][0], PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP);
    }

    // read counters and pack into PyList
    for (int i=0; i < n_cpu; i++) {
        read(fds[i][0], bufs[i], 4096*sizeof(char));
        PyObject* cpu_result = PyList_New(n_event);
        for (int j=0; j < n_event; j++) {
            // search for ids          
            for (int k=0; k < rfs[i]->nr; k++) {
                if (rfs[i]->values[k].id == ids[i][j]) {
                    uint64_t val = rfs[i]->values[k].value;
                    PyList_SetItem(cpu_result,j,Py_BuildValue("l",val));
                }
            }
        }
        PyList_SetItem(result_list,i,cpu_result);
    }

3. PyPerf.py和install.sh

  剩下的就简单介绍一下,根据之前的介绍可以知道PyPerf.py中设置所有的返回类型为py_object,并对sys_perf.c中的函数进行调用,参数设置为cpus = [0,1]events = [0]即硬件事件,结果存放在result_dict中。

  这个就是gcc的指令,编译sys_perf.c生成.so文件,删除.o文件。

资料来源