当前位置: 首页 > article >正文

CTF-内核pwn入门1: linux内核模块基础原理

本文由A5rZ在2025-2-18-21:00编写

1.可加载内核模块是什么?

内核可加载模块(*.ko 文件)是内核的一种扩展机制,可以在不重启系统的情况下加载和卸载代码。它们允许动态地向内核添加新的功能或支持。

以下是一些内核模块常见的功能:

1 驱动程序

内核模块最常见的用途是为硬件设备提供驱动程序支持。内核驱动程序可以管理设备的输入/输出操作,处理硬件中断、读取传感器数据或控制硬件设备。

例如,常见的硬件设备驱动模块有:

  • 网络接口卡(NIC)驱动:支持不同类型的网卡和网络协议。
  • 磁盘驱动:支持硬盘、SSD、光驱等设备。
  • USB驱动:支持USB设备,如鼠标、键盘、打印机等。
2 文件系统支持

内核模块也可以提供新的文件系统支持。例如,ext4 是一种文件系统,但如果你想支持其他文件系统(如 ntfs, xfs, btrfs 等),通常会加载相应的文件系统模块。

3 网络协议栈

内核模块还可以用于添加网络协议的支持。例如,TCP/IP协议栈是内核的一部分,但可以通过加载相应的模块来支持新的协议或扩展现有协议。例如:

  • VPN协议支持:如 IPsecWireGuard
  • 无线网络支持:Wi-Fi 驱动和协议栈。
4 安全性和调试功能

有时,内核模块用于提供安全性增强或调试功能。例如:

  • SELinux模块:强化内核的安全性,提供基于策略的访问控制。
  • 内核调试模块:提供内核日志记录、追踪功能,或允许内核代码被动态调试。
  • 内存保护模块:例如提供 Address Space Layout Randomization(ASLR)功能。
5 性能监控和系统管理

内核模块还可以用于性能监控、调度管理、系统资源的动态调整等。例如:

  • CPU性能计数器:动态监控 CPU 使用情况,执行性能分析。
  • 进程调度模块:调整调度策略或优先级。
6 虚拟化支持

内核模块可以用于提供虚拟化支持,例如:

  • KVM模块:实现虚拟化支持,允许创建虚拟机。
  • 虚拟网络接口:如 tun/tap 接口,支持用户空间和内核空间之间的虚拟网络通信。

总结

内核模块(*.ko 文件)可以扩展内核的功能,允许内核在运行时动态加载或卸载代码。常见用途包括硬件驱动、文件系统支持、网络协议栈、安全性增强、性能监控等。

2.怎么加载内核模块

1. 使用 insmod 命令加载内核模块

insmod 是最直接的方式来加载一个内核模块。它会将指定的 .ko 文件加载到内核中,并立即执行该模块。

语法:
insmod .ko
  • 例如,加载一个名为 example.ko 的内核模块:
    sudo insmod example.ko
    
注意:
  • insmod 只会加载指定的模块文件,不会处理依赖关系。如果该模块依赖其他模块,它们需要提前加载。
  • 只有 root 用户或具有足够权限的用户才能加载内核模块。

2. 使用 modprobe 命令加载内核模块

相比 insmodmodprobe 是一个更为智能的工具,它不仅可以加载模块,还能自动处理模块的依赖关系,加载所需的依赖模块。

语法:
modprobe 
  • 例如,加载一个名为 example 的模块:
    sudo modprobe example
    
modprobe 的优点:
  • 自动解决依赖关系:如果模块依赖于其他模块,modprobe 会自动加载这些依赖模块。
  • 模块文件存放位置modprobe 会根据 /lib/modules/$(uname -r)/ 目录中的模块配置文件来加载模块,而不需要指定 .ko 文件的具体路径。
  • 支持配置文件modprobe 使用 /etc/modprobe.d/ 目录下的配置文件(例如 blacklist.conf)来指定哪些模块不加载或加载时的特定参数。

3. 查看已加载的内核模块

使用 lsmod 命令可以查看当前系统中已加载的内核模块。lsmod 显示一个模块的列表以及其依赖关系。

lsmod

输出中通常包含以下信息:

  • Module:模块的名称。
  • Size:模块的大小。
  • Used by:其他使用该模块的模块或进程。

4. 卸载内核模块

如果你想卸载已加载的模块,可以使用 rmmodmodprobe -r 命令。

rmmod 卸载模块:
sudo rmmod 
modprobe -r 卸载模块:
sudo modprobe -r 
  • modprobe -r 还会自动处理模块的依赖关系,卸载该模块时会卸载依赖的模块。

5. 通过 /etc/modules-load.d/ 配置自动加载

如果你希望在系统启动时自动加载某个模块,可以通过创建一个配置文件,在 /etc/modules-load.d/ 目录下配置。

步骤:
  1. 创建一个新的文件(例如 my_module.conf)并在其中指定要加载的模块名:

    sudo nano /etc/modules-load.d/my_module.conf
    
  2. 在文件中输入模块的名称(不需要 .ko 后缀):

    example
    
  3. 保存并关闭文件。

当系统启动时,example 模块将会被自动加载。

6. 使用 modinfo 查看模块信息

如果你想查看内核模块的详细信息(如版本、描述、依赖关系等),可以使用 modinfo 命令。

语法:
modinfo .ko

例如:

modinfo example.ko

它会输出诸如模块的版本、作者、依赖关系、许可证、描述等信息。

总结

  • insmod:手动加载模块,但不会处理依赖。
  • modprobe:推荐的加载模块方式,支持自动处理依赖关系。
  • lsmod:查看已加载的模块。
  • rmmod / modprobe -r:卸载模块。
  • 自动加载:通过 /etc/modules-load.d/ 配置文件实现。
  • modinfo:查看模块详细信息。

如果你需要加载一个模块,并且该模块有依赖关系,使用 modprobe 会更方便,因为它能自动加载依赖的模块。

3.初始化函数init_module与析构函数module_exit

init_module 函数在 Linux 内核模块中是一个特殊的函数,它是模块加载时调用的入口点。它通常用于模块初始化,例如分配资源、注册设备驱动、创建 /proc 文件系统条目等。以下是详细解释和示例:

1. init_module 函数的作用

init_module 函数在内核模块加载时被调用,其主要职责包括:

  • 初始化模块所需的数据结构和资源。
  • 注册设备驱动或文件系统。
  • 创建内核对象或文件系统条目。
  • 设置模块的初始状态。

2. init_module 的定义

在最简单的形式下,init_module 函数可能如下所示:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init init_module(void) {
    printk(KERN_INFO "Module initialized.\n");
    return 0; // 返回0表示成功
}
  • __init:这是一个宏,告诉内核这个函数在初始化后可以被丢弃,释放内存。
  • printk:类似于用户空间的 printf,用于向内核日志输出信息。
  • return 0:返回0表示模块初始化成功,返回非零值表示初始化失败。

3. 使用 module_init

通常,内核模块不会直接定义 init_module 函数,而是使用 module_init 宏来指定初始化函数。这使得代码更清晰,且与 module_exit 宏对称。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_module_init(void) {
    printk(KERN_INFO "Module initialized.\n");
    return 0;
}

module_init(my_module_init); // 指定模块初始化函数

4. 完整示例

以下是一个完整的内核模块示例,它在加载时创建一个 /proc 文件系统条目,并在卸载时删除它:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

#define PROC_NAME "my_module"

static struct proc_dir_entry *proc_entry;

static int my_proc_show(struct seq_file *m, void *v) {
    seq_printf(m, "Hello, World!\n");
    return 0;
}

static int my_proc_open(struct inode *inode, struct file *file) {
    return single_open(file, my_proc_show, NULL);
}

static const struct file_operations my_proc_fops = {
    .owner = THIS_MODULE,
    .open = my_proc_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = single_release,
};

static int __init my_module_init(void) {
    proc_entry = proc_create(PROC_NAME, 0, NULL, &my_proc_fops);
    if (!proc_entry) {
        return -ENOMEM; // 内存分配失败
    }
    printk(KERN_INFO "/proc/%s created\n", PROC_NAME);
    return 0;
}

static void __exit my_module_exit(void) {
    proc_remove(proc_entry);
    printk(KERN_INFO "/proc/%s removed\n", PROC_NAME);
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of a Linux kernel module.");

解释

  • 头文件

    • #include :内核模块的基本定义。
    • #include :内核信息输出功能。
    • #include :内核初始化和退出功能。
    • #include :与 /proc 文件系统交互。
    • #include :用于简化 /proc 文件系统的读写操作。
    • #define PROC_NAME "my_module":定义 /proc 文件系统条目的名字。
  • 文件操作结构

    • my_proc_show:在 /proc 文件被读取时调用,输出 “Hello, World!”。
    • my_proc_open:在 /proc 文件被打开时调用。
    • my_proc_fops:定义文件操作,包括打开、读取、查找和释放操作。
  • 初始化函数

    • my_module_init:在模块加载时调用,创建 /proc 文件系统条目,输出日志信息。
  • 退出函数

    • my_module_exit:在模块卸载时调用,删除 /proc 文件系统条目,输出日志信息。
  • 模块宏

    • module_init(my_module_init):指定模块的初始化函数。
    • module_exit(my_module_exit):指定模块的退出函数。
    • MODULE_LICENSE 等:模块的元数据,如许可证、作者、描述等。

总结

init_module 函数(或者通过 module_init 宏指定的初始化函数)是内核模块加载时的入口点,用于初始化模块的各项功能。初始化函数可以执行多种操作,如资源分配、设备注册、创建 /proc 条目等。当模块被卸载时,对应的退出函数(通过 module_exit 宏指定)会被调用,以清理资源。

4.proc文件

/proc 文件系统是 Linux 内核提供的一种虚拟文件系统,用于访问内核信息。它在系统启动时由内核自动挂载,并且通常挂载在 /proc 目录下。/proc 文件系统的内容并不存储在磁盘上,而是动态生成的,它提供了一种方便的方式来获取运行时的内核和系统信息。

主要功能

  1. 系统信息访问:例如,/proc/cpuinfo 提供 CPU 信息,/proc/meminfo 提供内存使用信息。
  2. 进程信息访问:每个运行中的进程在 /proc 目录下都有一个与其 PID 对应的子目录,例如 /proc/1234
  3. 内核参数调整:某些文件可以用来调整内核参数,例如 /proc/sys 目录下的文件。

常见的 /proc 文件和目录

  • /proc/cpuinfo:显示 CPU 相关信息。
  • /proc/meminfo:显示内存使用情况。
  • /proc/uptime:显示系统启动后的运行时间。
  • /proc/version:显示内核版本信息。
  • /proc/[pid]:每个运行中的进程都有一个与其 PID 对应的目录,包含该进程的各种信息,如环境变量、当前工作目录、内存映射等。

使用 /proc 文件系统的示例

1. 查看 CPU 信息
cat /proc/cpuinfo
2. 查看内存使用情况
cat /proc/meminfo
3. 查看系统运行时间
cat /proc/uptime

在内核模块中使用 /proc 文件系统

内核模块可以创建自己的 /proc 文件,以便用户空间程序与内核模块交互。以下是一个简单的示例,展示如何在内核模块中创建一个 /proc 文件。

示例:创建一个 /proc 文件
  1. 内核模块代码
#define PROC_NAME "my_module"

static struct proc_dir_entry *proc_entry;

static int my_proc_show(struct seq_file *m, void *v) {
    seq_printf(m, "Hello, World!\n");
    return 0;
}

static int my_proc_open(struct inode *inode, struct file *file) {
    return single_open(file, my_proc_show, NULL);
}

static const struct file_operations my_proc_fops = {
    .owner = THIS_MODULE,
    .open = my_proc_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = single_release,
};

static int __init my_module_init(void) {
    proc_entry = proc_create(PROC_NAME, 0, NULL, &my_proc_fops);
    if (!proc_entry) {
        return -ENOMEM;
    }
    printk(KERN_INFO "/proc/%s created\n", PROC_NAME);
    return 0;
}

static void __exit my_module_exit(void) {
    proc_remove(proc_entry);
    printk(KERN_INFO "/proc/%s removed\n", PROC_NAME);
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of a /proc file.");
  1. 编译和加载模块
make
sudo insmod my_module.ko
  1. 读取 /proc 文件
cat /proc/my_module

输出将是:

Hello, World!
  1. 卸载模块
sudo rmmod my_module

解释

    • #define PROC_NAME "my_module":定义 /proc 文件系统条目的名字。
  • 文件操作结构

    • my_proc_show:在 /proc 文件被读取时调用,输出 “Hello, World!”。
    • my_proc_open:在 /proc 文件被打开时调用。
    • my_proc_fops:定义文件操作,包括打开、读取、查找和释放操作。
  • 初始化函数

    • my_module_init:在模块加载时调用,创建 /proc 文件系统条目,输出日志信息。
  • 退出函数

    • my_module_exit:在模块卸载时调用,删除 /proc 文件系统条目,输出日志信息。
  • 模块宏

    • module_init(my_module_init):指定模块的初始化函数。
    • module_exit(my_module_exit):指定模块的退出函数。
    • MODULE_LICENSE 等:模块的元数据,如许可证、作者、描述等。

总结

/proc 文件系统是 Linux 内核提供的一个强大的工具,用于访问和管理系统信息。它不仅提供了一种查看和修改内核参数的机制,还允许内核模块创建自定义的 /proc 文件,以便用户空间程序与内核模块交互。

5.proc_create()

proc_create 是 Linux 内核中的一个函数,用于创建一个新的 /proc 文件系统条目。这个函数常用于内核模块中,以便在 /proc 文件系统下创建一个新的文件,使得用户空间程序可以通过这个文件与内核模块进行交互。

函数原型

struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct file_operations *fops);

参数说明

  1. name: 这是要创建的 /proc 文件的名称。它是一个字符串,表示文件的名称。

  2. mode: 这是文件的权限模式,通常使用 S_IRUGOS_IWUSR 等宏来设置读、写权限等。umode_t 是一个表示文件模式的类型。

  3. parent: 这是一个指向父目录条目的指针。如果为 NULL,则在根目录下创建文件。

  4. fops: 这是一个指向 file_operations 结构的指针,包含了对这个文件的操作函数的定义,例如打开、读取、写入等操作。

返回值

  • 如果创建成功,函数返回一个指向新创建的 proc_dir_entry 结构的指针。
  • 如果失败,返回 NULL

使用示例

下面是一个简单的示例,展示了如何使用 proc_create 函数:

#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/module.h>

static int my_proc_show(struct seq_file *m, void *v) {
    seq_printf(m, "Hello from proc!\n");
    return 0;
}

static int my_proc_open(struct inode *inode, struct file *file) {
    return single_open(file, my_proc_show, NULL);
}

static const struct file_operations my_proc_fops = {
    .owner   = THIS_MODULE,
    .open    = my_proc_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = single_release,
};

static int __init my_module_init(void) {
    proc_create("myprocfile", 0, NULL, &my_proc_fops);
    return 0;
}

static void __exit my_module_exit(void) {
    remove_proc_entry("myprocfile", NULL);
}

module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");

解释示例

  • 在这个示例中,我们定义了一个名为 myprocfile/proc 文件。
  • my_proc_show 函数负责处理读取操作,向用户空间输出 “Hello from proc!”。
  • my_proc_open 函数用于打开这个文件。
  • 在模块初始化时,调用 proc_create 创建文件,并在退出时调用 remove_proc_entry 删除文件。

6.file_operations结构体

file_operations 结构体是 Linux 内核中用于定义文件操作函数的一组函数指针集合。它在字符设备驱动程序、块设备驱动程序以及其他文件系统实现中扮演着关键角色。通过实现和注册 file_operations 结构体中的函数,驱动程序能够响应用户空间对设备文件的各种操作,如打开、读取、写入和关闭等。

file_operations 结构体简介

file_operations 结构体定义在 `` 头文件中,其基本定义如下:

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write) (struct pipe_inode_info *, struct file *,
                             loff_t *, size_t, unsigned int);
    ssize_t (*splice_read) (struct file *, loff_t *,
                            struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease) (struct file *, long, struct file_lock **);
    long (*move_mmap) (struct file *, struct vm_area_struct *);
    ssize_t (*dedupe_file_range) (struct file *, loff_t, loff_t,
                                  struct file *, loff_t, loff_t, unsigned);
    void (*show_fdinfo) (struct seq_file *m, struct file *f);
    unsigned (*atomic_open) (struct inode *, struct file *, unsigned);
};

虽然结构体中包含许多成员,但通常驱动程序只需要实现其中的一部分,根据具体需求进行选择。

常用的 file_operations 成员

以下是一些常用的 file_operations 成员及其功能说明:

  • owner: 指向该 file_operations 结构体所属的模块。通常设为 THIS_MODULE

  • open: 当用户空间调用 open() 系统调用打开设备文件时,此函数被调用。用于初始化设备状态、分配资源等。

  • read: 用户空间调用 read() 系统调用时,此函数被调用。用于从设备读取数据到用户空间。

  • write: 用户空间调用 write() 系统调用时,此函数被调用。用于将用户空间的数据写入设备。

  • release: 用户空间调用 close() 系统调用关闭设备文件时,此函数被调用。用于释放设备资源。

  • ioctl (unlocked_ioctl 和 compat_ioctl): 处理设备的控制请求,用户空间通过 ioctl() 系统调用向设备发送控制命令。

  • mmap: 当用户空间调用 mmap() 系统调用映射设备内存到用户空间时,此函数被调用。

  • llseek: 处理文件偏移量的调整,如用户调用 lseek()

  • poll: 实现设备的异步 I/O 多路复用,如 select()poll()epoll() 等系统调用。

示例:字符设备驱动中的 file_operations

下面是一个简单的字符设备驱动示例,展示如何定义和使用 file_operations 结构体。

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mychardev"
#define BUF_LEN 80

static int major_number;
static char message[BUF_LEN] = {0};
static short message_len = 0;
static struct class*  mychardev_class  = NULL;
static struct device* mychardev_device = NULL;

// 函数声明
static int     dev_open(struct inode *, struct file *);
static int     dev_release(struct inode *, struct file *);
static ssize_t dev_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t dev_write(struct file *, const char __user *, size_t, loff_t *);

// 定义 file_operations 结构体
static struct file_operations fops =
{
    .owner = THIS_MODULE,
    .open = dev_open,
    .read = dev_read,
    .write = dev_write,
    .release = dev_release,
};

// 模块初始化函数
static int __init mychardev_init(void){
    printk(KERN_INFO "MyCharDev: Initializing the MyCharDev\n");

    // 动态分配一个主设备号
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0){
        printk(KERN_ALERT "MyCharDev failed to register a major number\n");
        return major_number;
    }
    printk(KERN_INFO "MyCharDev: registered correctly with major number %d\n", major_number);

    // 创建设备类
    mychardev_class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(mychardev_class)){
        unregister_chrdev(major_number, DEVICE_NAME);
        printk(KERN_ALERT "Failed to register device class\n");
        return PTR_ERR(mychardev_class);
    }
    printk(KERN_INFO "MyCharDev: device class registered correctly\n");

    // 创建设备
    mychardev_device = device_create(mychardev_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
    if (IS_ERR(mychardev_device)){
        class_destroy(mychardev_class);
        unregister_chrdev(major_number, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create the device\n");
        return PTR_ERR(mychardev_device);
    }
    printk(KERN_INFO "MyCharDev: device class created correctly\n");
    return 0;
}

// 模块卸载函数
static void __exit mychardev_exit(void){
    device_destroy(mychardev_class, MKDEV(major_number, 0));
    class_unregister(mychardev_class);
    class_destroy(mychardev_class);
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "MyCharDev: Goodbye from the LKM!\n");
}

// 打开设备文件
static int dev_open(struct inode *inodep, struct file *filep){
    printk(KERN_INFO "MyCharDev: Device has been opened\n");
    return 0;
}

// 读取设备文件
static ssize_t dev_read(struct file *filep, char __user *buffer, size_t len, loff_t *offset){
    int error_count = 0;
    // 将内核空间的数据复制到用户空间
    error_count = copy_to_user(buffer, message, message_len);

    if (error_count == 0){
        printk(KERN_INFO "MyCharDev: Sent %d characters to the user\n", message_len);
        return (message_len=0); // 清空缓冲区并返回读取的字节数
    }
    else{
        printk(KERN_INFO "MyCharDev: Failed to send %d characters to the user\n", error_count);
        return -EFAULT;              // 返回错误
    }
}

// 写入设备文件
static ssize_t dev_write(struct file *filep, const char __user *buffer, size_t len, loff_t *offset){
    // 将用户空间的数据复制到内核空间
    if (len > BUF_LEN){
        message_len = BUF_LEN;
    }
    else{
        message_len = len;
    }
    if (copy_from_user(message, buffer, message_len) != 0){
        return -EFAULT;
    }
    printk(KERN_INFO "MyCharDev: Received %zu characters from the user\n", len);
    return len;
}

// 关闭设备文件
static int dev_release(struct inode *inodep, struct file *filep){
    printk(KERN_INFO "MyCharDev: Device successfully closed\n");
    return 0;
}

module_init(mychardev_init);
module_exit(mychardev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_VERSION("0.1");

解释

  1. 定义 file_operations:

    static struct file_operations fops =
    {
        .owner = THIS_MODULE,
        .open = dev_open,
        .read = dev_read,
        .write = dev_write,
        .release = dev_release,
    };
    
    • owner 指定该结构体所属的模块,防止模块在操作进行时被卸载。
    • openreadwriterelease 分别指向相应的函数,实现设备文件的打开、读取、写入和关闭操作。
  2. 注册字符设备:

    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    
    • 动态分配主设备号,并注册设备。
  3. 实现操作函数:

    • dev_open: 打开设备时输出日志。
    • dev_read: 将内核缓冲区的数据复制到用户空间。
    • dev_write: 将用户空间的数据复制到内核缓冲区。
    • dev_release: 关闭设备时输出日志。
  4. 模块初始化与卸载:

    • mychardev_init 函数负责注册设备、创建类和设备文件。
    • mychardev_exit 函数负责注销设备和清理资源。

使用 file_operations 的注意事项

  1. 线程安全: file_operations 中的函数可能会被多个进程并发调用,因此在实现这些函数时需要注意线程安全,使用适当的同步机制(如自旋锁、互斥锁等)保护共享资源。

  2. 错误处理: 确保在各个操作函数中正确处理错误,返回合适的错误码,以便用户空间能够识别和处理。

  3. 内存管理: 在 readwrite 操作中,需正确管理内核和用户空间之间的数据传输,避免内存泄漏或非法访问。

  4. 权限控制: 对设备文件的访问权限需要在驱动初始化时设置合适的文件权限,确保只有授权的用户可以访问设备。

常见扩展功能

除了基本的 openreadwriterelease 操作外,file_operations 还支持许多高级功能,如:

  • 异步 I/O: 通过实现 pollfasync 函数,支持异步 I/O 操作,提高性能。

  • 内存映射: 实现 mmap 函数,允许用户空间直接访问设备内存,减少数据拷贝,提高效率。

  • 控制命令: 通过 unlocked_ioctlcompat_ioctl 实现自定义的控制命令,扩展设备的功能。

  • 文件锁定: 实现 lock 函数,支持文件级别的锁定,避免并发访问导致的数据不一致。

总结

file_operations 结构体是 Linux 设备驱动开发中至关重要的一部分,通过定义和实现这个结构体中的函数,开发者可以控制设备文件的各种操作行为。理解和正确使用 file_operations 是编写高效、稳定的 Linux 驱动程序的基础。

什么是一切皆文件?

在 Linux 内核中是,“一切皆文件”(Everything is a File)的设计理念,/proc 是一个虚拟文件系统(pseudo-filesystem),它并不位于实际的存储设备上,而是在内存中动态生成。通过 /proc,用户空间的程序可以方便地访问和交互内核内部的数据结构和信息。例如,/proc/cpuinfo 提供处理器信息,/proc/meminfo 提供内存使用情况等。

示例:创建一个proc文件的内核模块

我们编写一个内核模块,将一个proc文件注册到文件系统中。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/kernel.h>

#define PROC_NAME "myprocfile"
#define MSG "Hello, World from Kernel!\n"

static ssize_t proc_read(struct file *file, char __user *ubuf,
                         size_t count, loff_t *ppos)
{
    int len = strlen(MSG);

    if (*ppos > 0 || count < len)
        return 0;

    if (copy_to_user(ubuf, MSG, len))
        return -EFAULT;

    *ppos = len;
    return len;
}

static const struct proc_ops proc_file_ops = {
    .proc_read = proc_read,
};

static int __init myproc_init(void)
{
    proc_create(PROC_NAME, 0, NULL, &proc_file_ops);
    printk(KERN_INFO "/proc/%s created\n", PROC_NAME);
    return 0;
}

static void __exit myproc_exit(void)
{
    remove_proc_entry(PROC_NAME, NULL);
    printk(KERN_INFO "/proc/%s removed\n", PROC_NAME);
}

module_init(myproc_init);
module_exit(myproc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A Simple Proc File Module");

测试模块:

现在,模块已经创建了一个/proc/myprocfile文件。

  1. 读取proc文件

    cat /proc/myprocfile
    

    输出应为:

    Hello, World from Kernel!
    
  2. 查看proc文件信息

    你也可以使用ls命令查看该文件的信息:

    ls -l /proc/myprocfile
    

在内核中,每一个文件(包括 /proc 下的文件)都有一个关联的 file_operations 结构体。这种结构体定义了一组函数指针,这些函数负责处理对文件的各种操作,如打开、读取、写入、关闭等。对于 /proc 文件系统中的文件,当对这些文件进行读写操作时,实际上是调用了内核中定义的驱动函数。这些驱动函数可以访问和修改内核的数据结构,执行特定的任务,而不仅仅是进行简单的文件 I/O 操作。

解释:

  • /proc文件系统:这是一个内核提供的虚拟文件系统,用于暴露内核和进程的信息。文件内容并不存储在磁盘上,而是动态生成的。

  • proc_create函数:用于在/proc文件系统中创建一个新的文件。当用户访问该文件时,会触发我们定义的操作函数。

  • proc_read函数:当用户读取/proc/myprocfile文件时,该函数被调用,将内核中的数据复制到用户空间。

  • copy_to_user函数:用于将数据从内核空间复制到用户空间,确保安全地传递数据。

通过这个模块,我们展示了如何通过文件系统与内核交互,这正是“一切皆文件”的体现。

8.ioctl()实现

ioctl() 是一个系统调用,其一般形式如下:

long ioctl(int fd, unsigned int cmd, unsigned long arg);

参数说明:

  1. fd (文件描述符)

    • 这是一个打开的文件描述符,通常是通过 open() 函数返回的。它表示用户空间程序要操作的设备或文件。
  2. cmd (命令码)

    • 这是一个整数值,用于指定操作的类型或设备的控制命令。它是一个唯一的命令码,通常由设备驱动程序定义,并且根据该命令来执行特定的操作。
    • 命令码通常是通过宏如 _IO, _IOR, _IOW, _IOWR 等来定义的,这些宏帮助设置命令码的格式以及数据的读写方向。
  3. arg (参数)

    • 这是一个长整型值,通常是一个指针,指向传递给设备驱动的额外数据。根据命令码的不同,arg 可以用来传递控制命令的参数,或者用于返回值。
    • 例如,如果命令需要传递数据,arg 可能是一个指向用户空间数据的指针;如果命令返回数据,arg 可能指向用于接收数据的缓冲区。

返回值:

  • 如果成功,ioctl() 通常返回 0 或正数,具体取决于设备的实现。
  • 如果失败,返回 -1 并设置 errno,表示错误的类型。例如,EBADF(文件描述符无效)、EINVAL(无效命令)、EFAULT(错误的用户内存地址)等。

示例

例如,定义和使用 ioctl 的常见方式:

1. 设备驱动中定义 ioctl 命令
#include <linux/ioctl.h>

#define IOCTL_MAGIC 'k'

#define IOCTL_CMD_1 _IO(IOCTL_MAGIC, 1)        // 无数据操作
#define IOCTL_CMD_2 _IOR(IOCTL_MAGIC, 2, int)  // 从内核读取一个整数
#define IOCTL_CMD_3 _IOW(IOCTL_MAGIC, 3, int)  // 向内核写入一个整数
2. 设备驱动中实现 ioctl 操作函数
#include <linux/fs.h>
#include <linux/uaccess.h>  // for copy_to_user, copy_from_user

static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    int value;

    switch (cmd) {
        case IOCTL_CMD_1:
            printk(KERN_INFO "Received IOCTL_CMD_1\n");
            break;
        case IOCTL_CMD_2:
            value = 100;  // 读取到的值
            if (copy_to_user((int __user *)arg, &value, sizeof(value)))
                return -EFAULT;  // 如果拷贝到用户空间失败,返回错误
            break;
        case IOCTL_CMD_3:
            if (copy_from_user(&value, (int __user *)arg, sizeof(value)))
                return -EFAULT;  // 如果从用户空间拷贝失败,返回错误
            printk(KERN_INFO "Received value: %d\n", value);
            break;
        default:
            return -ENOTTY;  // 无效的命令
    }

    return 0;
}
3. 用户空间程序调用 ioctl
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>

#define IOCTL_CMD_1  _IO('k', 1)        // 定义命令
#define IOCTL_CMD_2  _IOR('k', 2, int)
#define IOCTL_CMD_3  _IOW('k', 3, int)

int main() {
    int fd;
    int value = 42;

    fd = open("/dev/my_device", O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return -1;
    }

    // 调用 IOCTL_CMD_1
    if (ioctl(fd, IOCTL_CMD_1) < 0) {
        perror("ioctl IOCTL_CMD_1 failed");
        close(fd);
        return -1;
    }

    // 调用 IOCTL_CMD_2 读取值
    if (ioctl(fd, IOCTL_CMD_2, &value) < 0) {
        perror("ioctl IOCTL_CMD_2 failed");
        close(fd);
        return -1;
    }
    printf("Value read from device: %d\n", value);

    // 调用 IOCTL_CMD_3 写入值
    value = 99;
    if (ioctl(fd, IOCTL_CMD_3, &value) < 0) {
        perror("ioctl IOCTL_CMD_3 failed");
        close(fd);
        return -1;
    }

    close(fd);
    return 0;
}

总结

ioctl() 提供了一种强大的方式,允许用户空间程序通过设备文件与内核空间进行低级交互。通过命令码和可选的参数,它可以控制设备的各种特性和操作,而不仅仅是读写数据。


http://www.kler.cn/a/552117.html

相关文章:

  • 常见安全威胁
  • 私有化搭建、本地知识库、可联网查询、具备RAG能力的私人DeepSeek
  • 常用标准库之-std::iota
  • Kafka 在大数据生态系统中的应用:实时数据流的中枢神经
  • stm32rtc实时时钟详解文章
  • ROS-相机话题-获取图像-颜色目标识别与定位-目标跟随-人脸检测
  • 上线了一个微软工具(免费),我独自开发,本篇有源码
  • DeepSeek驱动下的数据仓库范式转移:技术解耦、认知重构与治理演进
  • 北京青蓝智慧科技:LCCI ESG的从业方向
  • 【 深林寻径:从DeepSeek-MoE架构看智能生命的觉醒之路】
  • 单元测试方法的使用
  • 【论文笔记】Transformer^2: 自适应大型语言模型
  • 在Ubuntu24.04上安装Stable-Diffusion1.10.1版本
  • 力扣-二叉树-617 合并二叉树
  • 图数据库Neo4j面试内容整理-查询语言 Cypher
  • Kubernetes 容器自动伸缩失败:解决方案及阿里云如何帮助
  • js第八题
  • C++中的线程同步方式
  • Vue 中 MVVM、MVC 和 MVP 模式的区别
  • Docker 在微服务架构中的应用(二)