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协议支持:如
IPsec
和WireGuard
。 - 无线网络支持: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
命令加载内核模块
相比 insmod
,modprobe
是一个更为智能的工具,它不仅可以加载模块,还能自动处理模块的依赖关系,加载所需的依赖模块。
语法:
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. 卸载内核模块
如果你想卸载已加载的模块,可以使用 rmmod
或 modprobe -r
命令。
rmmod
卸载模块:
sudo rmmod
modprobe -r
卸载模块:
sudo modprobe -r
modprobe -r
还会自动处理模块的依赖关系,卸载该模块时会卸载依赖的模块。
5. 通过 /etc/modules-load.d/
配置自动加载
如果你希望在系统启动时自动加载某个模块,可以通过创建一个配置文件,在 /etc/modules-load.d/
目录下配置。
步骤:
-
创建一个新的文件(例如
my_module.conf
)并在其中指定要加载的模块名:sudo nano /etc/modules-load.d/my_module.conf
-
在文件中输入模块的名称(不需要
.ko
后缀):example
-
保存并关闭文件。
当系统启动时,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
文件系统的内容并不存储在磁盘上,而是动态生成的,它提供了一种方便的方式来获取运行时的内核和系统信息。
主要功能
- 系统信息访问:例如,
/proc/cpuinfo
提供 CPU 信息,/proc/meminfo
提供内存使用信息。 - 进程信息访问:每个运行中的进程在
/proc
目录下都有一个与其 PID 对应的子目录,例如/proc/1234
。 - 内核参数调整:某些文件可以用来调整内核参数,例如
/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
文件
- 内核模块代码
#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.");
- 编译和加载模块
make
sudo insmod my_module.ko
- 读取
/proc
文件
cat /proc/my_module
输出将是:
Hello, World!
- 卸载模块
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);
参数说明
-
name: 这是要创建的
/proc
文件的名称。它是一个字符串,表示文件的名称。 -
mode: 这是文件的权限模式,通常使用
S_IRUGO
、S_IWUSR
等宏来设置读、写权限等。umode_t
是一个表示文件模式的类型。 -
parent: 这是一个指向父目录条目的指针。如果为
NULL
,则在根目录下创建文件。 -
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 80static 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");
解释
-
定义
file_operations
:static struct file_operations fops = {.owner = THIS_MODULE,.open = dev_open,.read = dev_read,.write = dev_write,.release = dev_release, };
owner
指定该结构体所属的模块,防止模块在操作进行时被卸载。open
、read
、write
和release
分别指向相应的函数,实现设备文件的打开、读取、写入和关闭操作。
-
注册字符设备:
major_number = register_chrdev(0, DEVICE_NAME, &fops);
- 动态分配主设备号,并注册设备。
-
实现操作函数:
dev_open
: 打开设备时输出日志。dev_read
: 将内核缓冲区的数据复制到用户空间。dev_write
: 将用户空间的数据复制到内核缓冲区。dev_release
: 关闭设备时输出日志。
-
模块初始化与卸载:
mychardev_init
函数负责注册设备、创建类和设备文件。mychardev_exit
函数负责注销设备和清理资源。
使用 file_operations
的注意事项
-
线程安全:
file_operations
中的函数可能会被多个进程并发调用,因此在实现这些函数时需要注意线程安全,使用适当的同步机制(如自旋锁、互斥锁等)保护共享资源。 -
错误处理: 确保在各个操作函数中正确处理错误,返回合适的错误码,以便用户空间能够识别和处理。
-
内存管理: 在
read
和write
操作中,需正确管理内核和用户空间之间的数据传输,避免内存泄漏或非法访问。 -
权限控制: 对设备文件的访问权限需要在驱动初始化时设置合适的文件权限,确保只有授权的用户可以访问设备。
常见扩展功能
除了基本的 open
、read
、write
和 release
操作外,file_operations
还支持许多高级功能,如:
-
异步 I/O: 通过实现
poll
和fasync
函数,支持异步 I/O 操作,提高性能。 -
内存映射: 实现
mmap
函数,允许用户空间直接访问设备内存,减少数据拷贝,提高效率。 -
控制命令: 通过
unlocked_ioctl
或compat_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
文件。
-
读取proc文件
cat /proc/myprocfile
输出应为:
Hello, World from Kernel!
-
查看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);
参数说明:
-
fd (文件描述符):
- 这是一个打开的文件描述符,通常是通过
open()
函数返回的。它表示用户空间程序要操作的设备或文件。
- 这是一个打开的文件描述符,通常是通过
-
cmd (命令码):
- 这是一个整数值,用于指定操作的类型或设备的控制命令。它是一个唯一的命令码,通常由设备驱动程序定义,并且根据该命令来执行特定的操作。
- 命令码通常是通过宏如
_IO
,_IOR
,_IOW
,_IOWR
等来定义的,这些宏帮助设置命令码的格式以及数据的读写方向。
-
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_userstatic 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_1if (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()
提供了一种强大的方式,允许用户空间程序通过设备文件与内核空间进行低级交互。通过命令码和可选的参数,它可以控制设备的各种特性和操作,而不仅仅是读写数据。
相关文章:
CTF-内核pwn入门1: linux内核模块基础原理
本文由A5rZ在2025-2-18-21:00编写 1.可加载内核模块是什么? 内核可加载模块(*.ko 文件)是内核的一种扩展机制,可以在不重启系统的情况下加载和卸载代码。它们允许动态地向内核添加新的功能或支持。 以下是一些内核模块常见的功能&…...
第4章 4.1 Entity Framework Core概述
4.1.1 什么是ORM ORM (object tralstional mapping ,对象关系映射)中的“对象”指的就是C#中的对象,而“关系”是关系型数据库,“映射”指搭建数据库与C#对象之间的“桥梁”。 比如使用ORM ,可以通过创建C#对象的方式把数据插入数据库而不需…...
【C语言】自定义类型:联合体和枚举
1. 联合体 1.1 联合体类型的声明 像结构体一样,联合体也是由一个或者多个成员构成,这些成员可以是不同的类型。 但是编译器只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用同一块内存空间。所以联合体也叫:共用体。 给联合…...
企业组网IP规划与先关协议分析
目录 一、IP编址 1、IP地址组成 2、IP地址表达 3、IP 地址分类 4、IP地址类型 5、IP网络通信 6、子网掩码 7、默认子网掩码 8、IP 地址规划 9、有类IP编制缺陷 10、VLSM 11、变长子网掩码案例 12、网关 13、无类域间路由 一、IP编址 网络层位于数据链路层与传输层之间…...
数据结构之【顺序表简介】
1.顺序表的概念 顺序表 是 用一段物理地址连续的存储单元 依次 存储数据元素的线性结构 一般情况下采用数组存储 2.顺序表的结构 既然顺序表可以用来存储数据元素, 那就少不了 增删查改 的操作 此时,单一地只创建数组满足不了上述操作 创建相应的结构…...
如何调用 DeepSeek API:详细教程与示例
目录 一、准备工作 二、DeepSeek API 调用步骤 1. 选择 API 端点 2. 构建 API 请求 3. 发送请求并处理响应 三、Python 示例:调用 DeepSeek API 1. 安装依赖 2. 编写代码 3. 运行代码 四、常见问题及解决方法 1. API 调用返回 401 错误 2. API 调用返回…...
一周学会Flask3 Python Web开发-flask3模块化blueprint配置
锋哥原创的Flask3 Python Web开发 Flask3视频教程: 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 我们在项目开发的时候,多多少少会划分几个或者几十个业务模块,如果把这些模块的视图方法都写在app.py…...
vxe-table实现动态列
vxe-table实现动态列 1.动态列解释2.解决步骤2.1将后端返回的动态列表头,按照格式拼接在固定列表头上2.2将后端返回的列表数据按照键值对格式组装 1.动态列解释 正常列表是有固定的列;我的需求是,最初只知道表格的固定两列,查询数…...
day16_推荐系统和总结
文章目录 day16_推荐系统和总结一、推荐实现1、基于流行度推荐(掌握)1.1 近期热门商品推荐1.2 个人热门商品推荐 2、基于隐语义模型的协同过滤推荐(了解)2.1 ALS算法介绍2.2 推荐代码 3、基于物品的协同过滤推荐(了解&…...
Scifinder数据库专利检索实操教程
在上期的内容里,我为大家分享了查询专利的数据库。发出后有小伙伴问,怎么没有大佬Scifinder!这不,应大家的呼声,今天就来给大家好好讲讲 Scifinder专利检索!! SciFinder,由美国化学会…...
Linux下 <用户名> is not in the sudoers file
参考链接 https://blog.csdn.net/weixin_49192027/article/details/114702099 原因 当前的用户没有加入到sudo的配置文件里 解决方案 切换到root用户 su 编辑配置文件 vim /etc/sudoers 如果没有安装vim 运行命令 sudo apt-get install vim vim的使用教程 参考链接…...
Linux下基本指令(4)
Linux权限的概念 Linux下有两种用户:超级用户(root)、普通用户。 超级用户:可以再linux系统下做任何事情,不受限制 普通用户:在linux下做有限的事情。 超级用户的命令提示符是“#”,普通用户…...
【算法与数据结构】字典树(Trie)详解
目录 一,字典树的定义 二,字典树的代码实现 完整代码详细注释: 测试用例测试结果: 三,处理其他字符 四,内存优化与扩展 1. 内存优化 2. 扩展功能 五,扩展功能支持通配符匹配 六&…...
el-table树状表格,默认展开第一个节点的每一层
效果如图 <template><el-table:data"tableData"style"width: 100%":tree-props"{ children: children, hasChildren: hasChildren }":expand-row-keys"expandRowKeys"row-key"id"expand-change"handleExpan…...
RPA-实例(UiPath )
UiPath 是一个流行的机器人流程自动化(RPA)工具,用于自动化重复性任务。以下是一个简单的实例,展示如何使用 UiPath 自动化一个常见的任务:从 Excel 文件中读取数据并将其输入到网页表单中。 实例:从 Excel 读取数据并自动填写网页表单 步骤 1:准备工作 安装 UiPath S…...
【RabbitMQ业务幂等设计】RabbitMQ消息是幂等的吗?
在分布式系统中,RabbitMQ 自身不直接提供消息幂等性保障机制,但可通过业务逻辑设计和技术组合实现消息处理的幂等性。以下是 8 种核心实现方案及最佳实践: 一、消息唯一标识符 (Message Deduplication) 原理 每条消息携带全局唯一IDÿ…...
Spring Boot项目开发常见问题及解决方案(上)
启动相关问题 问题 1:项目启动时报错“找不到主类” 在使用 Spring Boot 打包成可执行 JAR 文件后启动,有时会遇到这个头疼的问题。通常是因为打包配置有误或者项目结构不符合要求。 解决方案: 首先,检查 pom.xml(Ma…...
具有整合各亚专科医学领域知识能力的AI智能体开发纲要(2025版)
整合各亚专科医学领域知识能力的AI代理的开发与研究 一、引言 1.1 研究背景 在科技飞速发展的当下,人工智能(AI)已成为推动各行业变革的关键力量,医疗领域也不例外。近年来,AI 在医疗行业的应用取得了显著进展,从医学影像诊断到疾病预测,从药物研发到个性化医疗,AI 技…...
Selenium实战案例1:论文pdf自动下载
在上一篇文章中,我们介绍了Selenium的基础用法和一些常见技巧。今天,我们将通过中国科学:信息科学网站内当前目录论文下载这一实战案例来进一步展示Selenium的web自动化流程。 目录 中国科学:信息科学当期目录论文下载 1.网页内…...
进程的介绍--进程状态/切换
1.冯 • 诺依曼体系结构 1.1 体系结构 冯•诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。数学家冯•诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成&#x…...
一文详解U盘启动Legacy/UEFI方式以及GPT/MBR关系
对于装系统的老手而说一直想研究一下装系统的原理,以及面对一些问题时的解决思路,故对以前的方法进行原理上的解释,主要想理解其底层原理。 引导模式 MBR分区可以同时支持UEFI和Legacy引导,我们可以看一下微pe制作的启动盘&#…...
【面试】Redis 常见面试题
一、介绍一下什么是 Redis,有什么特点? Redis 是一个高性能的 key-value 内存数据库。 不同于传统的 MySQL 这样的关系型数据库,Redis 主要使用内存存储数据(当然也支持持久化存储到硬盘上),并非是使用 “表” 这样…...
扩散模型中,Flow Matching的训练方式相比于 DDPM 训练方法有何优势?
在扩散模型中,Flow Matching(FM)相比DDPM(Denoising Diffusion Probabilistic Models)的训练方法具有以下核心优势: 1. 更简单的训练目标 DDPM:通过逐步预测噪声来间接优化数据分布的变分下界(ELBO),需要设计多步的噪声调度策略,训练目标依赖马尔可夫链的分解。Flow…...
Unity FBXExport导出的FBX无法在Blender打开
将FBX转换为obj: Convert 3D models online - free and secure...
【无标题】基于Unity写一个DelayInvoke方法
没想到来得这么块,程序员可能比司机先失业了。。。。。。。。 //测试过一定要这么调用??奇怪的是,不能(mono 直接引用)??///但AI还是给出了能用的代码 MonoBehaviourExtensions.DelayInvoke(this,()=> { },3); /* 方案一,使用示例(): public class ExampleUsag…...
JavaScript 语言基础之标签语句
标签语句的语法 label: statement label 表示标签名,可以是任何合法的标识符,但不能是 JavaScript 中的保留字。statement 表示被标记的语句块,可以是任何合法的 JavaScript 语句。 用法 标签语句的主要用途是在代码中进行跳转࿰…...
【网络编程】网络编程基础:TCP/UDP 协议
一、什么是网络? 网络是信息传输,接收和共享的虚拟世界,通过把网络上的信息汇聚在一起,将这些资源进行共享。 初衷:知识共享。这里不得不提到Internet 的历史-它其实是“冷战”的产物: 1957年…...
idea 部署 AJ-Report 启动的注意事项
AJ-Report 入门参考: AJ-Report 初学(入门教程) gitee 下载:https://gitee.com/anji-plus/report/releases 根据上面提供的 gitee 下载链接,点击直接下载 最上面的就是最新版本的,旧版本往下拉就可以找到,有三个下载…...
C# 生成二维码隐藏ASCII码
在 C# 中生成二维码时,如果需要隐藏或过滤掉 ASCII 码中的控制字符或不可见字符,可以在生成二维码之前对输入文本进行处理。以下是完整的实现步骤和代码示例: 1. 过滤 ASCII 码中的控制字符 ASCII 码中,0 到 31 以及 127 是控制字…...
python有没有不同精度的整型类型?
在 Python 中,不像 C、Java 等语言有明确的不同精度整型类型(如 int8、int16、int32、int64 等),Python 提供了统一的整数类型 int,它可以处理任意大小的整数,没有固定的精度限制。不过,Python …...
Python多线程编程理解面试题解析
一、多线程介绍 Python 的多线程是一种实现并发编程的方式,允许程序同时执行多个任务。然而,由于 Python 的全局解释器锁(GIL)的存在,多线程在某些场景下可能无法充分利用多核 CPU 的性能。以下是对 Python 多线程的理…...
网络协议相关知识有哪些?
前言 网络协议的基础是OSI和TCP/IP模型,这两个模型是理解协议分层的关键。 正文(仅是个人理解,如有遗漏望海涵) 网络协议是网络中设备间通信的规则和标准,涉及数据传输、路由、错误控制等多个方面。以下是网络协议相关知识的系统梳理: 一、网络协议分层模型 1、OSI七…...
【并发压测】高并发下Linux流量监控
在高并发环境下,Linux流量监控至关重要,可以帮助您确保网络性能和稳定性。以下是一些常用的Linux流量监控工具和方法: 1. **iftop**:iftop 是一款实时的网络流量监控工具,可以显示当前服务器上每个网络接口的流量使用情…...
Spring Boot项目中解决跨域问题(四种方式)
目录 一,跨域产生的原因二,什么情况下算跨域三,实际演示四,解决跨域的方法 1,CrossOrigin注解2,添加全局过滤器3,实现WebMvcConfigurer4,Nginx解决跨域5,注意 开发项目…...
革新之力:数字科技——重塑未来的超越想象之旅
在21世纪的科技浪潮中,数字科技如同一股不可阻挡的洪流,正以前所未有的速度和广度改变着我们的生活、工作乃至整个社会的结构。它不仅是技术的简单迭代,更是对人类社会认知边界的拓宽,对经济模式、社会治理、文化形态等多方面的深…...
matlab和java混合编程经验分享
最常用的就是可以查到再控制栏deploytool选择library complier打包,但是有问题就是比如果用了外部的求解器比如yalmip或者cplex的话用这个方法会找不到外部的求解器,网上找了很多,基本都大同小异。 后面分享一个亲测有效的打包方法࿰…...
rk3588/3576板端编译程序无法运行视频推理
图片推理可以,但是视频不行,运行视频推理报错:segment fault. 我遇到的问题原因是ffmpeg安装有问题,可以先在板端运行:ffmpeg -version ffmpeg version 4.2.4-1ubuntu1.0firefly6 Copyright (c) 2000-2020 the FFmpe…...
MATLAB在数据分析和绘图中的应用:从基础到实践
引言 股票数据分析是金融领域中的重要研究方向,通过对历史价格、成交量等数据的分析,可以帮助投资者更好地理解市场趋势和做出决策。MATLAB作为一种强大的科学计算工具,提供了丰富的数据处理和可视化功能,非常适合用于股票数据的…...
【CS285】高斯策略对数概率公式的学习笔记
公式介绍 在【CS285】中提到了高斯策略对数概率公式的公式如下: log π θ ( a t ∣ s t ) − 1 2 ∥ f ( s t ) − a t ∥ Σ 2 const \log \pi_{\theta}(\mathbf{a}_t | \mathbf{s}_t) -\frac{1}{2} \left\| f(\mathbf{s}_t) - \mathbf{a}_t \right\|_{\S…...
windows环境下用docker搭建php开发环境dnmp
安装WSL WSL即Linux子系统,比虚拟机占用资源少,安装的前提是系统必须是win10以上。 WSL的安装比较简单,网上有很多教程,例如:WSL简介与安装流程(Windows 下的 Linux 子系统)_wsl安装-CSDN博客&…...
区块链中的递归长度前缀(RLP)序列化详解
文章目录 1. 什么是RLP序列化?2. RLP的设计目标与优势3. RLP处理的数据类型4. RLP编码规则详解字符串的编码规则列表的编码规则 5. RLP解码原理6. RLP在以太坊中的应用场景7. 编码示例分析8. 总结 1. 什么是RLP序列化? 递归长度前缀(RLP&…...
PHP建立MySQL持久化连接(长连接)及mysql与mysqli扩展的区别
如果在 PHP 5.3 的版本以前想要创建MySQL的持久化连接(长连接),需要显式调用 pconnect 创建: $con mysql_pconnect($server[host], $server[username], $server[password]); if (!($con false)) { if (mysql_select_db($server[database], $con) fals…...
基于Python+Django+Vue的旅游景区推荐系统系统设计与实现源代码+数据库+使用说明
运行截图 功能介绍 前台功能包括:首页、详情页、订单、用户中心。后台功能包括:首页、轮播图管理、管理员、卖家管理、买家管理、景区管理、订单管理非开源功能(分类管理,地区管理,收藏管理,评论管理&a…...
架构学习第七周--Prometheus
目录 一、监控系统基础 二、Prometheus介绍 三、Prometheus单机部署 四、服务发现与告警功能 4.1,服务发现 4.2,告警功能实现 五、Prometheus与Kubernetes 5.1,Kubernetes指标 5.2,Prometheus集群部署 一、监控系统基础…...
基于Nanopi duo2的WiFi智能摄像头
1.固件包烧录 https://wiki.friendlyelec.com/wiki/index.php/NanoPi_Duo2/zh#.E8.BF.9E.E6.8E.A5WiFi 固件包链接以及烧录工具都在上面链接中 烧录过程 使用读卡器将SD卡插入到电脑,然后打开烧录工具 2.通过串口工具连接板子使其连接WiFi 对应的串口工具,就是这个HyperT…...
Hive Orc表数据导出和导入
导出到hdfs:hive执行 INSERT OVERWRITE DIRECTORY /test/hdfs_dir ROW FORMAT DELIMITED FIELDS TERMINATED BY \t STORED AS ORC SELECT * FROM hive_table; HDFS导出到本地:shell执行 hdfs dfs -get /test/hdfs_dis/file_name /linux_dir/xxx 本…...
Python爬虫实战:从零到一构建数据采集系统
文章目录 前言一、准备工作1.1 环境配置1.2 选择目标网站 二、爬虫实现步骤2.1 获取网页内容2.2 解析HTML2.3 数据保存 三、完整代码示例四、优化与扩展4.1 反爬应对策略4.2 动态页面处理4.3 数据可视化扩展 五、注意事项六、总结互动环节 前言 在大数据时代,数据采…...
Ubuntu 的RabbitMQ安装
目录 1.安装Erlang 查看erlang版本 退出命令 2. 安装 RabbitMQ 3.确认安装结果 4.安装RabbitMQ管理界面 5.启动服务并访问 1.启动服务 2.查看服务状态 3.通过IP:port 访问界面 4.添加管理员用户 a)添加用户名:admin,密码࿱…...
七星棋牌源码高阶技术指南:6端互通、200+子游戏玩法深度剖析与企业级搭建实战(完全开源)
在棋牌游戏行业高速发展的今天,如何构建一个具备高并发、强稳定性与多功能支持的棋牌游戏系统成为众多开发者和运营团队关注的焦点。七星棋牌全开源修复版源码 凭借其 六端互通、200子游戏玩法、多省区本地化支持,以及 乐豆系统、防沉迷、比赛场、AI智能…...
cuda安装
cuda WSL2急速搭建CUDA体验环境_wsl2 cuda-CSDN博客 cudnn cuDNN Archive | NVIDIA Developer pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118...