Contents

Linux内核:动手实现一个CPU频率调节器

本文代码地址:https://github.com/Shibo-Zhu/linux-try/tree/master/kernel_cpufreq_ctl

在Linux系统中,CPU频率调节(cpufreq)是一个至关重要的子系统。它在性能和功耗之间取得平衡,允许系统根据当前负载动态调整CPU的时钟频率。理解其工作原理并能通过代码与之交互,是Linux内核开发和系统性能优化的关键技能。

本文将首先介绍cpufreq的基本概念,然后深入探讨其核心内核API,最后通过编写一个内核模块和一个用户态命令行工具,带你一步步实现一个自定义的CPU频率控制器。


1. CPU工作模式 (Governors)

Linux内核的cpufreq子系统通过不同的 调节策略(Governors) 来管理CPU频率。Governor决定了何时以及如何提高或降低CPU频率。常见的Governors有:

  • performance: 性能优先。将CPU频率锁定在支持的最高频率上,以获得最大处理能力。
  • powersave: 节能优先。将CPU频率锁定在支持的最低频率上,以最大限度地节省电能。
  • userspace: 用户空间模式。允许任何用户态程序通过sysfs接口手动设置特定的CPU频率。我们今天的实践就类似于实现一个userspace模式的控制器。
  • ondemand: 按需模式。周期性地检查CPU负载,当负载超过某个阈值时,迅速将频率提升至最高;空闲时则降低频率。这是一种反应迅速的动态调节策略。
  • conservative: 保守模式。与ondemand类似,但频率的提升和降低是渐进式的,而不是一步到位,这样可以避免因负载突增突降导致的频率剧烈波动。
  • schedutil: 基于调度器的调节策略。利用内核调度器中的CPU负载信息来更精细、更高效地进行频率决策,是现代内核中推荐的默认策略。

你可以通过以下命令查看当前CPU支持和正在使用的governor:

# 查看支持的governors
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors

# 查看当前使用的governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

改变当前系统cpu的工作模式:

# 切换到userspace模式
echo userspace | sudo tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

2. 内核核心接口:cpufreq_driver_target()

要在内核代码中直接设置CPU频率,cpufreq子系统提供了一个核心函数 cpufreq_driver_target()。这个函数是驱动程序与cpufreq核心交互的关键。

它的原型如下:

int cpufreq_driver_target(struct cpufreq_policy *policy,
                          unsigned int target_freq,
                          unsigned int relation);

参数详解

  • struct cpufreq_policy *policy: 指向一个CPU的频率调节策略对象的指针。每个受cpufreq管理的CPU(或CPU集群)都有一个对应的policy对象。我们可以通过 cpufreq_cpu_get(cpu_id) 来获取指定CPU的policy
  • unsigned int target_freq: 你期望设置的目标频率,单位是 KHz
  • unsigned int relation: 定义了target_freq与实际可设置频率之间的关系。常用的值有:
    • CPUFREQ_RELATION_L: (Lowest) 设置为小于等于target_freq的最大可用频率。
    • CPUFREQ_RELATION_H: (Highest) 设置为大于等于target_freq的最小可用频率。

在使用 cpufreq_cpu_get() 获取 policy 后,必须在操作完成时调用 cpufreq_cpu_put() 来释放它,这是一种典型的引用计数管理模式。


3. 动手实践:自定义CPU频率控制器

接下来,我们将通过一个内核模块和一个用户态程序,实现从命令行设置任意CPU核心频率的功能。

工作原理

  1. 内核模块 (cpufreq_ctl.ko):
    • 创建一个字符设备 /dev/cpufreq_ctl
    • 提供一个 ioctl 接口,用于接收来自用户空间的请求。
    • 当接收到请求时,解析出目标CPU ID和频率,并调用 cpufreq_driver_target() 函数来完成设置。
  2. 用户态程序 (cpufreq_ctl_user):
    • 一个简单的命令行工具。
    • 接收用户输入的CPU ID和目标频率作为参数。
    • 打开 /dev/cpufreq_ctl 设备文件,并通过 ioctl 系统调用将参数发送给内核模块。

内核模块代码 (cpufreq_ctl.c)

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/cpufreq.h>

#define DEVICE_NAME "cpufreq_ctl"
#define CLASS_NAME  "cpufreq"

// 定义ioctl命令和数据结构
#define IOCTL_SET_FREQ _IOW('q', 1, struct cpufreq_ioctl_data)

struct cpufreq_ioctl_data {
    unsigned int cpu;
    unsigned int freq;
};

static struct class *cpufreq_class;
static struct cdev cpufreq_cdev;
static dev_t devt;

// ioctl处理函数
static long cpufreq_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct cpufreq_ioctl_data data;
    struct cpufreq_policy *policy;
    int ret = 0;

    if (cmd != IOCTL_SET_FREQ)
        return -EINVAL;

    // 从用户空间拷贝数据
    if (copy_from_user(&data, (void __user *)arg, sizeof(data)))
        return -EFAULT;

    pr_info("cpufreq_ctl: set CPU%u -> %u kHz\n", data.cpu, data.freq);

    // 获取指定CPU的policy对象
    policy = cpufreq_cpu_get(data.cpu);
    if (!policy)
        return -EINVAL;

    // 调用核心API设置频率
    ret = cpufreq_driver_target(policy, data.freq, CPUFREQ_RELATION_L);
    
    // 释放policy对象
    cpufreq_cpu_put(policy);

    if (ret)
        pr_err("cpufreq_ctl: failed to set freq, ret=%d\n", ret);
    else
        pr_info("cpufreq_ctl: success\n");

    return ret;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = cpufreq_ioctl,
};

// 模块初始化
static int __init cpufreq_ctl_init(void)
{
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&devt, 0, 1, DEVICE_NAME);
    if (ret) return ret;

    // 2. 初始化并添加cdev
    cdev_init(&cpufreq_cdev, &fops);
    ret = cdev_add(&cpufreq_cdev, devt, 1);
    if (ret) goto err_unregister;

    // 3. 创建设备类
    cpufreq_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(cpufreq_class)) {
        ret = PTR_ERR(cpufreq_class);
        goto err_cdev;
    }

    // 4. 创建设备文件 /dev/cpufreq_ctl
    device_create(cpufreq_class, NULL, devt, NULL, DEVICE_NAME);
    pr_info("cpufreq_ctl: module loaded, /dev/%s ready\n", DEVICE_NAME);
    return 0;

err_cdev:
    cdev_del(&cpufreq_cdev);
err_unregister:
    unregister_chrdev_region(devt, 1);
    return ret;
}

// 模块卸载
static void __exit cpufreq_ctl_exit(void)
{
    device_destroy(cpufreq_class, devt);
    class_destroy(cpufreq_class);
    cdev_del(&cpufreq_cdev);
    unregister_chrdev_region(devt, 1);
    pr_info("cpufreq_ctl: module unloaded\n");
}

module_init(cpufreq_ctl_init);
module_exit(cpufreq_ctl_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_VERSION("1.0");
MODULE_DESCRIPTION("Simple CPU frequency control via ioctl");

用户态程序 (cpufreq_ctl_user.c)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define DEVICE_PATH "/dev/cpufreq_ctl"

// ioctl命令和数据结构必须与内核模块中的定义完全一致
#define IOCTL_SET_FREQ _IOW('q', 1, struct cpufreq_ioctl_data)

struct cpufreq_ioctl_data {
    unsigned int cpu;
    unsigned int freq;
};

int main(int argc, char *argv[])
{
    int fd;
    struct cpufreq_ioctl_data data;

    if (argc != 3) {
        fprintf(stderr, "Usage: sudo %s <cpu_id> <freq_khz>\n", argv[0]);
        return 1;
    }

    data.cpu = atoi(argv[1]);
    data.freq = atoi(argv[2]);

    // 打开设备文件
    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 发送ioctl请求
    if (ioctl(fd, IOCTL_SET_FREQ, &data) < 0) {
        perror("ioctl");
        close(fd);
        return 1;
    }

    printf("Request to set CPU%u frequency to %u kHz sent successfully.\n", data.cpu, data.freq);

    close(fd);
    return 0;
}

编译与运行

1. 编译内核模块

创建一个Makefile文件:

obj-m := cpufreq_ctl.o

# KDIR需要指向你的Linux内核源码树路径
# 通常是 /lib/modules/$(shell uname -r)/build
KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

执行编译:

make

成功后会生成cpufreq_ctl.ko文件。

2. 加载模块与检查设备

加载内核模块:

sudo insmod cpufreq_ctl.ko

查看内核日志,确认加载成功:

dmesg | tail

你应该能看到 cpufreq_ctl: module loaded... 的信息。

检查设备文件是否已创建:

ls -l /dev/cpufreq_ctl

3. 编译并运行用户态程序

编译:

gcc cpufreq_ctl_user.c -o cpufreq_ctl_user

重要提示: 要想手动设置频率,必须先将对应CPU的governor切换到userspace模式。

echo userspace | sudo tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

运行程序,设置CPU 0的频率为1.1GHz (1100000 KHz):

sudo ./cpufreq_ctl_user 0 1100000

再次查看内核日志 dmesg,可以看到模块打印的成功信息。

4. 验证结果

通过sysfs查看CPU 0当前的实际频率:

cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq

如果输出接近1100000,说明我们的频率控制器工作正常!

卸载模块:

sudo rmmod cpufreq_ctl