内核定时器 | PHM's world

LOADING

歡迎來到烏托邦的世界

内核定时器

任务

写一个内核模块,在内核模块中实现
1.一个定时器,定期读取内存信息,并记录到一个日志文件
2.日志文件包含完整的时间戳

(提示:使用hrtimer一系列接口)

思路:

​ 关于定时器的解决:Linux中主要有两种解决方案:简单的timer_list和更精确的hrtimer。由于需求中没有特别指出对定时精度的要求,但使用hrtimer可以提供更高的精度和灵活性,所以我决定使用hrtimer接口。

​ 然后是获取内存信息:经过查找可知,Linux内核提供了获取系统内存信息的接口si_meminfo,它可以填充一个sysinfo结构体,这个结构体包含了系统当前的内存使用情况。

​ 文件的操作:内核空间处理文件和用户态中处理文件使用的接口不同,经过网上查找,在内核空间中写文件,需要使用vfs_write等函数,并且要小心处理文件系统的状态改变(比如,需要使用set_fs来临时改变内核的地址限制)。找到并引入以下头文件:<linux/fs.h>包含了对文件系统进行操作的函数原型和相关的结构体定义、<linux/uaccess.h>:包含了对用户空间和内核空间数据交互的函数原型和宏定义、<linux/kernel.h>:包含了内核编程中常用的一些宏定义和函数原型。

​ 基于上述的工具和接口选择,我设计出了模块的大体结构,包括初始化部分、定时器回调函数以及清理部分:

1.初始化部分 (my_init):打开or创建日志文件,初始化定时器,然后启动。

2.定时器回调函数 (timer_callback):这个函数是定时器到期时调用的,它会读取内存信息,获取当前时间,并将这些信息写入之前打开的日志文件。

3.模块清理部分 (my_exit):在卸载模块时,需要取消定时器(如果它还在运行),并且关闭打开的日志文件。

代码

kernel_timer_file.c

#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/mm.h> // 用于si_meminfo存储内存信息
#include <linux/timekeeping.h> // 用于获取内存时间 current_kernel_time64

#define TIMER_INTERVAL_NS 1e9 // 时间间隔 1秒

static struct hrtimer hr_timer;
struct file *file = NULL;
mm_segment_t old_fs;

// 定时器回调函数,周期性获取系统内存信息
static enum hrtimer_restart timer_callback(struct hrtimer *timer) {
    struct sysinfo si;
    char buf[128];
    int bytes;
    struct timespec64 ts;

    // 获取系统内存信息和当前时间
    si_meminfo(&si);
    ktime_get_real_ts64(&ts);

    /* 准备带有时间戳和内存信息的消息
    并格式化为字符串存在buf缓冲区中*/
    bytes = snprintf(buf, sizeof(buf), "[%lld.%09ld] Total RAM: %lu, Free RAM: %lu\n",
                     (long long)ts.tv_sec, ts.tv_nsec, si.totalram * si.mem_unit / 1024, si.freeram * si.mem_unit / 1024);

    // 在内核空间中写入文件 
    if (file) {
        old_fs = get_fs();
        set_fs(get_ds());
        vfs_write(file, buf, bytes, &file->f_pos);
        set_fs(old_fs);
    }

    // 重启定时器
    hrtimer_forward_now(timer, ns_to_ktime(TIMER_INTERVAL_NS));
    return HRTIMER_RESTART;
}

// 模块初始化函数
static int __init my_init(void) {
    ktime_t ktime;

    // 以写入模式打开文件,将输出的内存信息打印到当前目录下的kernel_memory_log.txt
    file = filp_open("./kernel_memory_log.txt", O_CREAT | O_WRONLY | O_APPEND, 0644);
    if (IS_ERR(file)) {
        file = NULL;
        pr_alert("无法打开文件\n");
        return -EFAULT;
    }
    
    ktime = ktime_set(0, TIMER_INTERVAL_NS);
    hrtimer_init(&hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    hr_timer.function = &timer_callback;
    hrtimer_start(&hr_timer, ktime, HRTIMER_MODE_REL);

    return 0;
}

// 模块退出函数,在模块被卸载时调用
static void __exit my_exit(void) {
    int ret;

    ret = hrtimer_cancel(&hr_timer);
    if (ret)
        pr_info("定时器仍在使用...\n");

    if (file) {
        filp_close(file, NULL);
    }

    pr_info("HR定时器模块已卸载\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("PHM");
MODULE_DESCRIPTION("在内核空间中记录内存信息到文件的HR(高精度定时器)定时器模块");

Makefile

obj-m += kernel_timer_file.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

操作步骤

将两个文件上传到服务器上。

准备

在服务器上安装必要的模块:

sudo yum groupinstall "Development Tools"
sudo yum install gcc
sudo yum install kernel-devel

我遇到了版本不匹配的问题,升级:

sudo yum update

内核开发包:

sudo yum install kernel-devel-$(uname -r)

切换到root用户:

su -

操作流程

1.编译模块

在包含kernel_timer_file.c源代码和Makefile的目录中运行make命令。生成.ko文件,即可加载的内核模块。

2.加载模块

编译完成后,使用insmod命令加载模块到内核:

sudo insmod kernel_timer_file.ko

可以使用modinfo查看模块信息,使用lsmod查看已加载的模块。

3.验证和监控

加载模块后,可以通过dmesg命令查看内核日志,验证模块是否按预期工作。

dmesg | tail #查看最后几行 

4.卸载模块

使用rmmod命令:

sudo rmmod kernel_timer_file