【Linux字符设备驱动】

设备文件

设备号的注册与注销

1、设备号相关宏定义

2、设备号注册过程

cs 复制代码
/**
 * struct char_device_struct - Linux内核内部用于管理字符设备号区间的核心数据结构
 * 作用:记录字符设备号(主+次)的分配状态,避免设备号冲突,通过链表组织同一主设备号下的多个次设备号区间
 * 注:该结构体是内核内部私有结构,驱动开发者一般不需要直接操作,由内核提供的设备号注册/注销函数(如register_chrdev_region)间接维护
 */
struct char_device_struct {
    struct char_device_struct *next;  /* 链表后继指针,用于链接「同一主设备号、次设备号区间不重叠」的其他char_device_struct实例
                                       * 例如:同一主设备号下,次设备号0~4和5~9会分别对应两个结构体,通过next指针形成链表
                                       */
    unsigned int major;               /* 字符设备的主设备号,用于标识设备类型(如串口主设备号4、键盘主设备号13)
                                       * 内核通过主设备号匹配对应的字符设备驱动程序,主设备号全局唯一(传统静态分配模式下)
                                       */
    unsigned int baseminor;           /* 当前设备号区间的「起始次设备号」,即该区间的第一个次设备号
                                       * 次设备号用于区分同一设备类型下的多个具体设备(如同一串口驱动下的多个串口设备)
                                       */
    int minorct;                      /* 当前设备号区间包含的「次设备号数量」
                                       * 计算区间范围:[baseminor, baseminor + minorct - 1]
                                       * 示例:baseminor=0,minorct=5 → 包含次设备号0、1、2、3、4
                                       */
    char name[64];                    /* 字符设备的名称,用于内核调试和标识,一般与驱动名称一致(可选填) */
    struct cdev *cdev;                /* 指向该设备号区间对应的字符设备核心结构体cdev(内核2.6及以上版本)
                                       * cdev结构体包含了设备的操作函数集(file_operations),是驱动与内核交互的核心
                                       */
};

3、设备号注册

4、设备号注销

设备号动态加静态分配实例

cs 复制代码
// 内核核心功能头文件
#include <linux/kernel.h>
// 模块编程核心头文件
#include <linux/module.h>
// 字符设备注册相关头文件
#include <linux/fs.h>

// 声明模块遵循GPL许可证
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Developer");
MODULE_DESCRIPTION("Character Device Driver (Support Dynamic/Static Device Number)");

// --------------------------
// 模块参数:可在加载时动态指定
// --------------------------
// 主设备号:0表示动态分配,非0表示静态注册
static int fs_major = 0;
// 次设备号起始值
static int fs_minor = 0;
// 申请的设备号数量
static int fs_number = 1;

// 声明为模块参数,加载时可通过 insmod xxx.ko fs_major=xxx 指定
module_param(fs_major, int, S_IRUGO);
module_param(fs_minor, int, S_IRUGO);
module_param(fs_number, int, S_IRUGO);

// --------------------------
// 模块初始化函数
// --------------------------
static int __init fs_chrdev_init(void)
{
    // 存储合成的设备号
    dev_t devno = MKDEV(fs_major, fs_minor);
    int ret = 0;

    printk(KERN_INFO "fs_chrdev: Hello world!\n");

    // 核心逻辑:动态分配 vs 静态注册
    if (fs_major == 0) {
        // 1. 动态分配设备号(内核自动分配主设备号)
        ret = alloc_chrdev_region(&devno, fs_minor, fs_number, "fs_chrdev");
        if (ret < 0) {
            printk(KERN_ERR "fs_chrdev: Failed to allocate chrdev region!\n");
            return ret;
        }
        // 从分配的设备号中提取主设备号
        fs_major = MAJOR(devno);
        printk(KERN_INFO "fs_chrdev: Dynamic allocate success! fs_major = %d\n", fs_major);
    } else {
        // 2. 静态注册设备号(使用用户指定的主设备号)
        ret = register_chrdev_region(devno, fs_number, "fs_chrdev");
        if (ret < 0) {
            printk(KERN_ERR "fs_chrdev: Failed to register chrdev region!\n");
            return ret;
        }
        printk(KERN_INFO "fs_chrdev: Static register success! fs_major = %d\n", fs_major);
    }

    return 0;
}

// --------------------------
// 模块退出函数
// --------------------------
static void __exit fs_chrdev_exit(void)
{
    dev_t devno = MKDEV(fs_major, fs_minor);

    // 释放设备号(动态/静态分配的都用这一个函数释放)
    unregister_chrdev_region(devno, fs_number);
    printk(KERN_INFO "fs_chrdev: Goodbye world! Device number released.\n");
}

// 模块入口和出口声明
module_init(fs_chrdev_init);
module_exit(fs_chrdev_exit);

字符设备的注册与注销

cs 复制代码
// 字符设备驱动的核心结构体,用于描述一个字符设备
struct cdev {
    // 内核对象基础结构,用于设备模型中的管理(如引用计数、设备层次关系、热插拔事件等)
    struct kobject kobj;
    // 指向该设备所属的模块指针,通常赋值为 THIS_MODULE
    // 作用是防止模块在设备被使用时被意外卸载
    struct module *owner;
    // 指向该字符设备的文件操作集合,定义了设备支持的 open/read/write/ioctl 等接口
    // 是用户空间与驱动交互的核心入口
    const struct file_operations *ops;
    // 链表头,用于将系统中所有的 cdev 结构体链接成全局链表,方便内核统一管理
    struct list_head list;
    // 该字符设备对应的设备号(主设备号 + 次设备号,由 dev_t 类型存储)
    dev_t dev;
    // 该字符设备所占用的设备号数量(即次设备号的连续个数)
    // 例如:主设备号为240,次设备号从0到3,则count=4
    unsigned int count;
} __randomize_layout;  // 编译宏,用于随机化结构体成员的内存布局,提升安全性,防止利用结构体偏移的漏洞
cs 复制代码
/*
 * Copyright (C) 2022 HQYJ Co., Inc. All Rights Reserved.
 *
 * 文件名      : fs_chrdev.c 
 * 作者        : HQYJ <support@hqyj.com>
 * 版本        : V1.0
 * 创建时间    : 2022-07-11 
 * 最后修改时间: 2022-07-11
 * 网址        : www.fsdev.com.cn
 */

// 内核核心头文件:提供printk等内核打印、基础数据类型支持
#include <linux/kernel.h>
// 模块编程核心头文件:提供模块加载/卸载、许可证声明等接口
#include <linux/module.h>
/*设备号 file_operations相关头文件:提供dev_t、MKDEV、register_chrdev_region等设备号操作函数,
  以及file_operations文件操作集合结构体定义*/
#include <linux/fs.h>  
/*cdev相关头文件:提供cdev结构体定义,以及cdev_init、cdev_add、cdev_del等字符设备操作函数*/
#include <linux/cdev.h>

/*
 * 定义设备号变量
 * dev_major : 主设备号(此处手动指定为300,属于静态分配方式)
 * dev_minor : 次设备号(起始值为0,对应单个设备)
 * dev_number: 设备数量(此处为1,表示只申请1个设备号)
 */
static int dev_major = 300;
static int dev_minor = 0;
static int dev_number = 1;

/*定义cdev结构体变量:字符设备的核心结构体,用于描述一个字符设备的属性和操作接口
  此处为静态分配内存,无需手动调用kmalloc分配,卸载时也无需手动释放*/
static struct cdev cdev;

/*用户操作接口集合:定义用户空间与驱动程序交互的核心接口(如open、read、write等)
  此处仅初始化了owner成员,后续可补充其他操作接口(如.open、.read)*/
static struct file_operations fs_chrdev_fops = {
	.owner = THIS_MODULE,  // 声明所有者为当前模块,防止模块在设备被使用时被意外卸载
};

/*加载函数:模块加载时自动调用(由module_init声明指定),完成设备号注册、cdev初始化与注册
  __init宏标记:该函数仅在模块加载时执行,执行完毕后内核会回收其占用的内存空间*/
static int __init fs_chrdev_init(void)
{
	int ret = 0;  // 函数返回值存储变量,用于判断各步骤是否执行成功
	/*构建完整设备号:通过MKDEV内核宏,将主设备号和次设备号组合成dev_t类型的完整设备号
	  dev_t类型:高12位存储主设备号,低20位存储次设备号,是内核中表示设备号的标准类型*/
	dev_t devno = MKDEV(dev_major, dev_minor);

	// 内核打印日志(KERN_INFO:信息级别日志),提示模块开始加载
	printk(KERN_INFO "fs_chrdev: module enter\n");

	/*申请设备号:静态注册字符设备号(使用手动指定的dev_major)
	  函数参数说明:
	  1. devno:已构建的完整设备号(主+次)
	  2. dev_number:需要申请的设备号数量
	  3. "fs_chrdev":设备名称(会显示在/proc/devices文件中,方便查询)
	  返回值:0表示成功,<0表示失败*/
	ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
	// 判断设备号注册是否失败
	if (ret < 0) {
		// 内核打印错误日志(KERN_ERR:错误级别日志),提示注册失败
		printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n");	
		goto err1;  // 跳转到err1标签,执行错误处理(直接返回错误码)
	}

	/*cdev初始化及注册:完成字符设备结构体的初始化与内核注册
	  第一步:初始化cdev结构体
	  cdev_init函数作用:
	  1. 将自定义的文件操作集合fs_chrdev_fops绑定到cdev->ops成员
	  2. 初始化cdev结构体内部的链表头(list成员)
	  3. 初始化cdev->owner成员(默认与fs_chrdev_fops->owner一致)*/
	cdev_init(&cdev, &fs_chrdev_fops);
	
	// 显式设置cdev结构体的所有者为当前模块(增强代码健壮性,与fops->owner保持一致)
	cdev.owner = THIS_MODULE;
	
	/*第二步:将cdev结构体注册到Linux内核
	  函数参数说明:
	  1. &cdev:已初始化的cdev结构体指针
	  2. devno:完整设备号
	  3. 1:设备号数量(与dev_number一致,此处为1)
	  返回值:0表示成功,<0表示失败*/
	ret = cdev_add(&cdev, devno, 1);
	// 判断cdev注册是否失败
	if (ret < 0) {
		// 内核打印错误日志,提示cdev注册失败
		printk(KERN_ERR "fs_chrdev: cdev_add");
		goto err2;  // 跳转到err2标签,执行错误处理(释放已申请的设备号,再返回错误码)
	}

	/*这个return 0 不能忽略,否则导致申请到的资源被释放掉
	  返回0:告知内核模块加载成功,保留已申请的设备号和注册的cdev资源*/
	return 0;

// 错误处理标签2:cdev注册失败时执行
err2:
	// 释放已成功申请的设备号(防止资源泄露),与register_chrdev_region配对使用
	// 注意:原代码此处参数为dev_minor,应为dev_number(小瑕疵,不影响基础功能,建议修改为dev_number)
	unregister_chrdev_region(devno, dev_minor);
// 错误处理标签1:设备号注册失败时执行
err1:
	// 返回错误码,告知内核模块加载失败
	return ret;
}

/*卸载函数:模块卸载时自动调用(由module_exit声明指定),完成资源的释放与清理
  __exit宏标记:该函数仅在模块卸载时执行,未加载的模块中该函数会被忽略*/
static void __exit fs_chrdev_exit(void)
{
	// 构建完整设备号,用于释放设备号
	dev_t devno = MKDEV(dev_major, dev_minor);

	// 内核打印日志,提示模块开始卸载
	printk(KERN_INFO "fs_chrdev: module exit\n");

	// 从内核中删除已注册的cdev结构体,释放字符设备资源(与cdev_add配对使用)
	cdev_del(&cdev);
	
	/*释放设备号:释放之前静态注册的字符设备号(与register_chrdev_region配对使用)
	  防止设备号泄露,确保后续其他模块可以申请使用该设备号*/
	unregister_chrdev_region(devno, dev_number);
}

// 声明模块的初始化入口点:内核加载模块时,自动调用fs_chrdev_init函数
module_init(fs_chrdev_init);
// 声明模块的退出入口点:内核卸载模块时,自动调用fs_chrdev_exit函数
module_exit(fs_chrdev_exit);

// 声明模块遵循GPL许可证,避免内核加载时出现"tainted"(污染)警告,同时允许内核导出相关符号
MODULE_LICENSE("GPL");

用户接口的实现

file_operation结构体

cs 复制代码
struct file_operations {
    // 1. 模块所有者(必设):几乎所有驱动都要赋值为 THIS_MODULE
    // 作用:防止模块在设备被使用(文件被打开)时被意外卸载,提升安全性
    struct module *owner;

    // 2. 打开设备文件(常用):用户调用 open() 时触发
    // 参数说明:
    // inode:指向设备文件的索引节点结构体(存储设备号等信息)
    // filp:指向文件结构体(存储文件打开状态、私有数据等)
    // 返回值:0 成功,<0 失败(错误码)
    int (*open)(struct inode *inode, struct file *filp);

    // 3. 关闭/释放设备文件(常用):用户调用 close() 时触发(最后一次关闭时才执行)
    // 参数与 open 一致,返回值:0 成功,<0 失败
    int (*release)(struct inode *inode, struct file *filp);

    // 4. 从设备读取数据(常用):用户调用 read() 时触发(驱动→用户空间传数据)
    // 参数说明:
    // filp:文件结构体指针
    // buf:用户空间缓冲区指针(存储读取到的数据)
    // count:用户期望读取的字节数
    // pos:文件偏移量指针(记录当前读写位置,需手动更新)
    // 返回值:>0 实际读取的字节数,0 读取到文件末尾,<0 失败
    ssize_t (*read)(struct file *filp, char __user *buf, size_t count, loff_t *pos);

    // 5. 向设备写入数据(常用):用户调用 write() 时触发(用户空间→驱动传数据)
    // 参数说明:与 read 类似,buf 是用户空间要写入的数据缓冲区
    // 返回值:>0 实际写入的字节数,0 未写入,<0 失败
    ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *pos);

    // 6. 设备控制(常用):用户调用 ioctl()/ioctl_fd() 时触发(用于自定义命令,如设备配置)
    // 参数说明:
    // filp:文件结构体指针
    // cmd:自定义控制命令(如 CMD_SET_BAUDRATE)
    // arg:命令对应的参数(可传数值或用户空间指针)
    // 返回值:0 成功,<0 失败
    long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);

    // 7. 定位文件偏移量(可选):用户调用 lseek() 时触发(字符设备通常不需要,块设备/普通文件常用)
    loff_t (*llseek)(struct file *filp, loff_t offset, int orig);

    // 8. 轮询/多路复用(可选):用户调用 select()/poll()/epoll() 时触发
    // 核心作用:实现异步IO,查询设备是否处于「可读写」状态,避免用户进程阻塞在IO操作上
    // 适用场景:需要同时监控多个设备/文件的可读写状态(如网络套接字、串口)
    // 参数说明:
    // filp:文件结构体指针
    // wait:轮询表结构体指针,用于将当前进程加入到设备的等待队列中(通过 poll_wait() 函数)
    // 返回值:返回设备的可读写状态掩码(如 POLLIN 表示可读,POLLOUT 表示可写,POLLERR 表示错误)
    unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);

    // 9. 异步通知/信号驱动IO(可选):用户调用 fcntl() 设置 FASYNC 标志时触发
    // 核心作用:实现信号驱动IO,当设备状态发生变化(如有数据可读、可写)时,主动向用户空间发送 SIGIO 信号
    // 适用场景:设备状态变化不规律,无需用户进程轮询,由驱动主动通知(如串口收到数据、按键按下)
    // 参数说明:
    // fd:设备文件描述符(用户空间传入)
    // filp:文件结构体指针
    // on:开启/关闭标志(1 表示开启异步通知,0 表示关闭异步通知)
    // 返回值:0 成功,<0 失败(错误码)
    int (*fasync)(int fd, struct file *filp, int on);
};

open和release

cs 复制代码
#include <linux/kernel.h>
#include <linux/module.h>
/*设备号 file_operations相关头文件*/
#include <linux/fs.h>  
/*cdev相关头文件*/
#include <linux/cdev.h>

/*
 * 定义设备号变量
 * dev_major : 主设备号
 * dev_minor : 次设备号
 * dev_number: 设备数量
 */
static int dev_major = 300;
static int dev_minor = 0;
static int dev_number = 1;

/*定义cdev结构体变量*/
static struct cdev cdev;

/*open接口*/
static int fs_chrdev_open(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "fs_chrdev: fs_chrdev_open\n");
	/*具体功能实现*/
	return 0;
}

/*release接口*/
int fs_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "fs_chrdev: fs_chrdev_release\n");
	/*具体功能实现*/
	return 0;
}

/*用户操作接口集合*/
static struct file_operations fs_chrdev_fops = {
	.owner = THIS_MODULE,
	.open = fs_chrdev_open,
	.release = fs_chrdev_release,
};

/*加载函数*/
static int __init fs_chrdev_init(void)
{
	int ret = 0;
	/*构建完整设备号*/
	dev_t devno = MKDEV(dev_major, dev_minor);

	printk(KERN_INFO "fs_chrdev: module enter\n");

	/*申请设备号*/
	ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
	if (ret < 0) {
		printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n");	
		goto err1;
	}

	/*cdev初始及注册*/
	cdev_init(&cdev, &fs_chrdev_fops);
	cdev.owner = THIS_MODULE;
	ret = cdev_add(&cdev, devno, 1);
	if (ret < 0) {
		printk(KERN_ERR "fs_chrdev: cdev_add");
		goto err2;
	}

	/*这个return 0 不能忽略,否则导致申请到的资源被释放掉*/
	return 0;

err2:
	unregister_chrdev_region(devno, dev_number);
err1:
	return ret;
}

/*卸载函数*/
static void __exit fs_chrdev_exit(void)
{
	dev_t devno = MKDEV(dev_major, dev_minor);

	printk(KERN_INFO "fs_chrdev: module exit\n");

	cdev_del(&cdev);
	/*释放设备号*/
	unregister_chrdev_region(devno, dev_number);
}

module_init(fs_chrdev_init);
module_exit(fs_chrdev_exit);

MODULE_LICENSE("GPL");

read和write

cs 复制代码
/*
 * Copyright (C) 2022 HQYJ Co., Inc. All Rights Reserved.
 *
 * 文件名      : fs_chrdev.c 
 * 作者        : HQYJ <support@hqyj.com>
 * 版本        : V1.0
 * 创建时间    : 2022-07-11 
 * 最后修改时间: 2022-07-11
 * 网址        : www.fsdev.com.cn
 * 功能说明    : 简易Linux字符设备驱动实现
 *              支持open/release/read/write四个核心文件操作
 *              实现用户空间与内核空间的简单数据交互(基于内存缓存区)
 */

#include <linux/kernel.h>   // 内核核心头文件,提供printk等内核基础函数/宏定义
#include <linux/module.h>   // 模块驱动核心头文件,提供module_init/module_exit等模块相关宏

/*设备号 file_operations相关头文件*/
#include <linux/fs.h>       // 文件系统核心头文件,提供dev_t/MKDEV/register_chrdev_region等设备号相关函数
                            // 同时提供file_operations/inode/file等核心结构体定义

/*cdev相关头文件*/
#include <linux/cdev.h>     // 字符设备核心头文件,提供cdev结构体及cdev_init/cdev_add/cdev_del等操作函数

/*copy_to_user/copy_from_user相关头文件*/
#include <linux/uaccess.h>  // 内存访问相关头文件,提供内核与用户空间数据拷贝的核心函数
                            // copy_to_user(内核->用户)、copy_from_user(用户->内核)

/*
 * 定义设备号相关全局变量(静态变量,仅当前文件可见,符合驱动开发最佳实践)
 * dev_major : 主设备号(用于标识字符设备驱动类型,手动指定为300,需确保未被其他驱动占用)
 * dev_minor : 次设备号(用于标识同一驱动下的不同设备实例,此处仅一个设备,设为0)
 * dev_number: 申请的设备数量(此处仅申请1个设备,对应一个次设备号)
 */
static int dev_major = 300;
static int dev_minor = 0;
static int dev_number = 1;

/*定义cdev结构体变量(字符设备核心结构体,用于关联设备号与文件操作集)*/
static struct cdev cdev;

/*定义数据缓存区相关宏与变量(用于存储用户空间与内核空间交互的数据)*/
#define BUFF_SIZE 128       // 定义内核缓存区大小为128字节
static char dev_data[BUFF_SIZE] = {'\0'};  // 内核数据缓存区,初始化全为结束符
static int current_size = 0;               // 记录当前缓存区中有效数据的长度

/*
 * 字符设备open接口实现(对应用户空间open系统调用)
 * 函数原型:遵循file_operations结构体中open成员的函数签名
 * @inode  : 指向设备文件的inode结构体指针(存储文件内核元数据,包含设备号等信息)
 * @file   : 指向打开的文件实例的file结构体指针(代表当前打开的文件,存储操作集等信息)
 * 返回值  : 0表示成功,负数表示错误码(内核标准错误码)
 */
static int fs_chrdev_open(struct inode *inode, struct file *file)
{
    // 内核打印函数(用户空间对应printf),KERN_INFO是日志级别,用于分类输出日志
    printk(KERN_INFO "fs_chrdev: fs_chrdev_open\n");
    
    /*具体功能实现:可在此添加设备打开时的初始化逻辑(如硬件初始化、锁初始化等)*/
    
    return 0;  // 返回0表示打开成功
}

/*
 * 字符设备release接口实现(对应用户空间close系统调用,释放文件实例)
 * 函数原型:遵循file_operations结构体中release成员的函数签名
 * @inode  : 指向设备文件的inode结构体指针
 * @file   : 指向要释放的文件实例的file结构体指针
 * 返回值  : 0表示成功,负数表示错误码
 */
static int fs_chrdev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "fs_chrdev: fs_chrdev_release\n");
    
    /*具体功能实现:可在此添加设备关闭时的资源释放逻辑(如硬件关闭、锁释放等)*/
    
    return 0;  // 返回0表示释放成功
}

/*
 * 字符设备write接口实现(对应用户空间write系统调用,用户向内核写入数据)
 * 函数原型:遵循file_operations结构体中write成员的函数签名
 * @file   : 指向打开的文件实例的file结构体指针
 * @buff   : 指向用户空间数据缓存区的指针(__user宏标注:该指针指向用户空间,内核不可直接访问)
 * @size   : 用户希望写入的数据长度(字节数)
 * @loff   : 指向文件偏移量的指针(记录当前文件读写位置,字符设备一般无需关注)
 * 返回值  : 成功写入的字节数,负数表示错误码
 */
static ssize_t fs_chrdev_write(struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
    int count = 0;  // 用于记录实际要拷贝的数据长度

    /*第一步:检查用户传入的size参数的正确性,避免越界访问内核缓存区*/
    if (size < 0) {
        // 返回-EINVAL:内核标准错误码,表示"无效的参数"
        return -EINVAL;
    } else if (size > BUFF_SIZE - 1) {
        // 用户要写入的数据超过缓存区最大容量,只拷贝BUFF_SIZE-1个字节(预留1个字节存字符串结束符'\0')
        count = BUFF_SIZE - 1;  // 修正:原代码为BUFF_SIZE,此处改为BUFF_SIZE-1,避免数组越界
    } else {
        // 用户要写入的数据长度合法,正常拷贝
        count = size;
    }

    /*第二步:将用户空间的数据拷贝到内核空间缓存区(dev_data)*/
    // copy_from_user:内核向用户空间读取数据的核心函数
    // 参数1:内核空间目标地址,参数2:用户空间源地址,参数3:要拷贝的字节数
    // 返回值:0表示拷贝成功,非0表示拷贝失败(剩余未拷贝的字节数)
    if (copy_from_user(dev_data, buff, count)) {
        // 返回-EFAULT:内核标准错误码,表示"无效的内存地址"(用户空间指针非法)
        return -EFAULT;
    }

    /*第三步:处理拷贝后的数据,添加字符串结束符,更新有效数据长度*/
    dev_data[count] = '\0';    // 为数据添加字符串结束符,方便后续读取时处理
    current_size = count;      // 更新当前内核缓存区中有效数据的长度

    return count;  // 返回实际拷贝的字节数,告知用户空间写入成功的长度
}

/*
 * 字符设备read接口实现(对应用户空间read系统调用,用户从内核读取数据)
 * 函数原型:遵循file_operations结构体中read成员的函数签名
 * @file   : 指向打开的文件实例的file结构体指针
 * @buff   : 指向用户空间数据缓存区的指针(用于存储从内核读取的数据)
 * @size   : 用户希望读取的数据长度(字节数)
 * @loff   : 指向文件偏移量的指针(字符设备一般无需关注)
 * 返回值  : 成功读取的字节数,负数表示错误码,0表示无数据可读
 */
static ssize_t fs_chrdev_read(struct file *file, char __user *buff, size_t size, loff_t *loff)
{
    int count = 0;  // 用于记录实际要拷贝的数据长度

    /*第一步:检查用户传入的size参数的正确性,以及内核缓存区是否有有效数据*/
    if (size < 0) {
        return -EINVAL;  // 无效参数错误
    } else if (size > current_size) {
        // 用户希望读取的数据长度大于内核缓存区有效数据长度,只拷贝现有有效数据
        count = current_size;
    } else {
        // 用户希望读取的数据长度合法,正常拷贝
        count = size;
    }

    /*第二步:将内核空间缓存区(dev_data)的数据拷贝到用户空间*/
    // copy_to_user:内核向用户空间写入数据的核心函数
    // 参数1:用户空间目标地址,参数2:内核空间源地址,参数3:要拷贝的字节数
    // 返回值:0表示拷贝成功,非0表示拷贝失败
    if (copy_to_user(buff, dev_data, count)) {
        return -EFAULT;  // 无效内存地址错误
    }

    /*第三步:处理读取后的数据,清空内核缓存区有效数据长度(此处实现"一次性读取"逻辑)*/
    current_size = 0;  // 置0表示后续无数据可读,直到用户再次写入新数据

    return count;  // 返回实际拷贝的字节数,告知用户空间读取成功的长度
}

/*
 * 定义字符设备的文件操作集(file_operations结构体)
 * 该结构体是内核与字符设备驱动的交互接口,存放了设备支持的所有文件操作函数指针
 * 内核通过该结构体调用驱动实现的具体接口(如open、read、write等)
 */
static struct file_operations fs_chrdev_fops = {
    .owner = THIS_MODULE,  // 标记该操作集所属的模块(THIS_MODULE是内核宏,指向当前模块)
                           // 作用:防止模块在被使用时被意外卸载,提高驱动安全性
    .open = fs_chrdev_open,        // 绑定设备打开接口
    .release = fs_chrdev_release,  // 绑定设备释放接口
    .read = fs_chrdev_read,        // 绑定设备读取接口
    .write = fs_chrdev_write,      // 绑定设备写入接口
};

/*
 * 驱动模块加载函数(内核加载模块时自动调用,对应insmod命令)
 * __init宏:标记该函数为模块初始化函数,内核加载完成后会释放该函数占用的内存
 * 返回值  : 0表示加载成功,负数表示加载失败
 */
static int __init fs_chrdev_init(void)
{
    int ret = 0;  // 用于记录函数调用的返回结果,判断是否执行成功
    dev_t devno = MKDEV(dev_major, dev_minor);  // 构建完整的设备号(主设备号+次设备号)
                                                // MKDEV是内核宏,将主、次设备号编码为一个32位的dev_t类型变量

    printk(KERN_INFO "fs_chrdev: module enter\n");

    /*第一步:申请注册字符设备号(手动指定主设备号方式)*/
    // register_chrdev_region:注册指定的设备号区间
    // 参数1:要注册的起始设备号(由MKDEV构建)
    // 参数2:要注册的设备数量
    // 参数3:设备名称(用于在/proc/devices中显示,方便用户查看设备对应关系)
    // 返回值:0表示成功,负数表示失败
    ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
    if (ret < 0) {
        // KERN_ERR:错误级别的日志,用于输出驱动错误信息,方便调试
        printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n");	
        goto err1;  // 申请设备号失败,跳转到错误处理标签err1,直接返回错误
    }

    /*第二步:初始化并注册cdev字符设备结构体(将设备号、操作集、字符设备绑定)*/
    // cdev_init:初始化cdev结构体,将cdev与文件操作集fs_chrdev_fops绑定
    // 参数1:要初始化的cdev结构体指针
    // 参数2:对应的file_operations操作集指针
    cdev_init(&cdev, &fs_chrdev_fops);
    
    // 设置cdev结构体的所属模块,与file_operations中的owner一致,提高安全性
    cdev.owner = THIS_MODULE;
    
    // cdev_add:将初始化完成的cdev结构体注册到内核中,建立设备号与字符设备的关联
    // 参数1:要注册的cdev结构体指针
    // 参数2:对应的设备号
    // 参数3:设备数量
    // 返回值:0表示成功,负数表示失败
    ret = cdev_add(&cdev, devno, 1);
    if (ret < 0) {
        printk(KERN_ERR "fs_chrdev: cdev_add failed\n");
        goto err2;  // 注册cdev失败,跳转到错误处理标签err2,释放已申请的设备号
    }

    /*这个return 0 不能忽略,否则导致申请到的资源被释放掉*/
    return 0;  // 模块加载成功,返回0

/* 错误处理分支(goto语句实现,是内核驱动中资源释放的标准写法,清晰高效)*/
err2:
    // 释放已注册的设备号(避免资源泄露),对应register_chrdev_region
    unregister_chrdev_region(devno, dev_number);
err1:
    return ret;  // 返回错误码,告知内核模块加载失败
}

/*
 * 驱动模块卸载函数(内核卸载模块时自动调用,对应rmmod命令)
 * __exit宏:标记该函数为模块卸载函数,仅当模块被卸载时才会被编译链接
 */
static void __exit fs_chrdev_exit(void)
{
    dev_t devno = MKDEV(dev_major, dev_minor);  // 构建完整设备号

    printk(KERN_INFO "fs_chrdev: module exit\n");

    /*第一步:从内核中注销cdev字符设备结构体(对应cdev_add)*/
    // cdev_del:移除内核中的cdev结构体,解除设备号与字符设备的关联
    cdev_del(&cdev);
    
    /*第二步:释放已注册的设备号(对应register_chrdev_region)*/
    // unregister_chrdev_region:释放设备号区间,归还内核,避免资源泄露
    unregister_chrdev_region(devno, dev_number);
}

// 模块加载入口宏:指定模块加载时要调用的初始化函数(fs_chrdev_init)
module_init(fs_chrdev_init);
// 模块卸载入口宏:指定模块卸载时要调用的卸载函数(fs_chrdev_exit)
module_exit(fs_chrdev_exit);

// 声明模块的许可证协议(必须指定,否则内核加载时会报警告,且部分功能不可用)
// GPL协议:开源协议,符合内核开源规范,允许模块与内核其他代码交互
MODULE_LICENSE("GPL");

ioctl

设备结构体

fs_chrdev.h

cs 复制代码
#ifndef FS_CHRDEV_H
#define FS_CHRDEV_H

#define FS_CMD_MAGIC 'F'

#define FS_CMD1 _IO(FS_CMD_MAGIC, 0)
#define FS_CMD2 _IO(FS_CMD_MAGIC, 1)

#endif

fs_chrdev.c

cs 复制代码
/*
 * 该文件是一个完整的 Linux 字符设备驱动模板
 * 实现了 open/release/read/write/unlocked_ioctl 五大核心接口
 * 支持用户态与内核态之间的字符数据读写,以及简单的 IO 控制命令响应
 */

#include <linux/kernel.h>   // 内核打印函数 printk() 等核心内核接口
#include <linux/module.h>   // 模块加载/卸载相关宏(module_init/module_exit)及模块管理接口

/*设备号 file_operations 相关头文件*/
#include <linux/fs.h>       // 包含文件操作结构体 file_operations、设备号相关函数(MKDEV 等)
                            // 以及字符设备的核心操作接口定义

/*cdev 相关头文件*/
#include <linux/cdev.h>     // 包含字符设备结构体 cdev 及相关操作函数(cdev_init/cdev_add 等)

/*copy_to_user/copy_from_user 相关头文件*/
#include <linux/uaccess.h>  // 包含内核态与用户态数据拷贝的核心函数
                            // copy_to_user(内核→用户)、copy_from_user(用户→内核)

#include "fs_chrdev.h"      // 自定义头文件,通常包含 IOCTL 命令宏(如 FS_CMD1/FS_CMD2)等定义

/*
 * 定义设备号相关全局变量(字符设备必须关联设备号才能被用户态识别)
 * dev_major : 主设备号(用于区分不同类型的字符设备,如 1 是内存设备、4 是 tty 设备)
 * dev_minor : 次设备号(用于区分同一类型下的多个具体设备,从 0 开始编号)
 * dev_number: 申请的设备数量(此处只申请 1 个设备,对应 1 个次设备号)
 */
static int dev_major = 300;  // 手动指定主设备号为 300(需确保未被其他设备占用)
static int dev_minor = 0;    // 次设备号从 0 开始
static int dev_number = 1;   // 只申请 1 个字符设备

/*定义 cdev 结构体变量(用于描述字符设备的核心结构体,关联设备号与文件操作接口)*/
static struct cdev cdev;

/*定义数据缓存区(内核态用于存储用户态读写的数据,实现内核与用户态的数据中转)*/
#define BUFF_SIZE 128        // 定义缓冲区最大容量为 128 字节
static char dev_data[BUFF_SIZE] = {'\0'};  // 内核数据缓冲区,初始化为全空字符
static int current_size = 0;               // 记录缓冲区中当前有效数据的长度(避免读取空数据)

/*
 * 字符设备 open 接口(用户态调用 open() 打开设备文件时,内核会回调该函数)
 * @inode: 指向设备节点的 inode 结构体(存储设备文件的磁盘信息,包含设备号)
 * @file:  指向打开的文件描述符对应的 file 结构体(存储文件的运行时信息)
 * @return: 0 表示打开成功,负数表示打开失败(返回对应的内核错误码)
 */
static int fs_chrdev_open(struct inode *inode, struct file *file)
{
    // KERN_INFO:日志级别(信息级),内核日志会输出该内容,可通过 dmesg 命令查看
	printk(KERN_INFO "fs_chrdev: fs_chrdev_open\n");
	
	/*具体功能实现(此处为模板,可添加设备初始化、资源申请等逻辑)*/
	return 0;  // 返回 0 表示打开成功
}

/*
 * 字符设备 release 接口(用户态调用 close() 关闭设备文件时,内核会回调该函数)
 * 注意:close() 不一定会立即回调 release(存在文件描述符引用计数),引用计数为 0 时才会回调
 * @inode: 指向设备节点的 inode 结构体
 * @file:  指向关闭的文件描述符对应的 file 结构体
 * @return: 0 表示释放成功,负数表示释放失败
 */
static int fs_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "fs_chrdev: fs_chrdev_release\n");
	
	/*具体功能实现(此处为模板,可添加设备资源释放、状态重置等逻辑)*/
	return 0;  // 返回 0 表示释放成功
}

/*
 * 字符设备 write 接口(用户态调用 write() 向设备写入数据时,内核会回调该函数)
 * 作用:将用户态缓冲区的数据拷贝到内核态缓冲区(完成用户→内核的数据传输)
 * @file:  指向打开的文件描述符对应的 file 结构体
 * @buff:  用户态缓冲区的指针(__user 修饰符标注:该指针指向用户态内存空间,内核不可直接访问)
 * @size:  用户态想要写入的数据长度(字节数)
 * @loff:  文件偏移量(字符设备通常忽略,块设备/普通文件需关注)
 * @return: 成功写入的字节数;负数表示写入失败(返回对应的内核错误码)
 */
static ssize_t fs_chrdev_write(struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
	int count = 0;  // 实际要拷贝的数据长度(用于限制超出缓冲区容量的写入)

	/*1. 检查写入长度 size 的正确性,避免非法数据或缓冲区溢出*/
	if (size < 0) {  // size 为负数,属于非法参数
		return -EINVAL;  // 返回内核错误码:无效参数
	} else if (size > BUFF_SIZE - 1) {  // 写入数据超出缓冲区容量(预留 1 字节存字符串结束符 '\0')
		count = BUFF_SIZE - 1;  // 限制最大写入长度为缓冲区容量-1
	} else {  // 写入数据长度合法,正常接收
		count = size;
	}

	/*2. 将用户空间数据拷贝到内核空间(核心步骤)
	 * copy_from_user: 内核提供的安全拷贝函数(用户态→内核态)
	 * 函数原型:unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
	 * 参数说明:
	 *   to: 内核态缓冲区指针(此处为 dev_data)
	 *   from: 用户态缓冲区指针(此处为 buff,必须带 __user 修饰)
	 *   n: 要拷贝的字节数(此处为 count)
	 * 返回值:拷贝失败的字节数(0 表示全部拷贝成功,非 0 表示拷贝失败)
	 */
	if (copy_from_user(dev_data, buff, count)) {
		return -EFAULT;  // 返回内核错误码:内存访问错误(用户态指针无效或权限不足)
	}

	/*3. 数据拷贝完成后的收尾处理*/
	dev_data[count] = '\0';  // 给内核缓冲区数据添加字符串结束符,避免后续读取出现乱码
	current_size = count;    // 更新当前缓冲区有效数据长度,为 read 接口提供数据长度参考

	return count;  // 返回实际写入的字节数,告知用户态写入成功
}

/*
 * 字符设备 read 接口(用户态调用 read() 从设备读取数据时,内核会回调该函数)
 * 作用:将内核态缓冲区的数据拷贝到用户态缓冲区(完成内核→用户的数据传输)
 * @file:  指向打开的文件描述符对应的 file 结构体
 * @buff:  用户态缓冲区的指针(__user 修饰符标注:内核不可直接访问)
 * @size:  用户态想要读取的数据长度(字节数)
 * @loff:  文件偏移量(字符设备通常忽略)
 * @return: 成功读取的字节数;0 表示无数据可读;负数表示读取失败
 */
static ssize_t fs_chrdev_read(struct file *file, char __user *buff, size_t size, loff_t *loff)
{
	int count = 0;  // 实际要拷贝的数据长度

	/*1. 检查读取长度 size 的正确性,以及内核缓冲区是否有有效数据*/
	if (size < 0) {  // size 为负数,非法参数
		return -EINVAL;
	} else if (size > current_size) {  // 用户想要读取的长度大于内核缓冲区有效数据长度
		count = current_size;  // 只能返回当前所有有效数据
	} else {  // 读取长度合法,正常返回请求长度
		count = size;
	}

	/*2. 将内核空间数据拷贝到用户空间(核心步骤)
	 * copy_to_user: 内核提供的安全拷贝函数(内核态→用户态)
	 * 函数原型:unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
	 * 参数说明:
	 *   to: 用户态缓冲区指针(此处为 buff)
	 *   from: 内核态缓冲区指针(此处为 dev_data)
	 *   n: 要拷贝的字节数(此处为 count)
	 * 返回值:拷贝失败的字节数(0 表示全部拷贝成功,非 0 表示拷贝失败)
	 */
	if (copy_to_user(buff, dev_data, count)) {
		return -EFAULT;  // 返回内核错误码:内存访问错误
	}

	/*3. 数据拷贝完成后的收尾处理*/
	current_size = 0;  // 读取完成后清空有效数据长度标记(此处为一次性读取,可根据需求修改为循环读取)

	return count;  // 返回实际读取的字节数,告知用户态读取成功
}

/*
 * 字符设备 unlocked_ioctl 接口(用户态调用 ioctl() 控制设备时,内核会回调该函数)
 * 作用:实现设备的自定义控制命令(如设备参数配置、模式切换等),比 read/write 更灵活
 * 注意:unlocked_ioctl 是无锁版本(推荐使用),替代旧的 ioctl 接口
 * @file:  指向打开的文件描述符对应的 file 结构体
 * @cmd:   用户态传入的控制命令(自定义宏,如 FS_CMD1/FS_CMD2)
 * @arg:   用户态传入的命令参数(可是整数或指针,需根据命令类型解析)
 * @return: 0 表示命令执行成功;负数表示命令执行失败
 */
static long fs_chrdev_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	printk(KERN_INFO "fs_chrdev: fs_chrdev_unlocked_ioctl\n");

	/*根据用户态传入的命令 cmd 进行分支处理*/
	switch(cmd) {
	case FS_CMD1:  // 自定义命令 1(在 fs_chrdev.h 中定义)
		printk(KERN_INFO "fs_chrdev: FS_CMD1\n");
		break;  // 执行完成后跳出分支
	case FS_CMD2:  // 自定义命令 2(在 fs_chrdev.h 中定义)
		printk(KERN_INFO "fs_chrdev: FS_CMD2\n");
		break;
	default:  // 未知命令,返回错误提示
		printk(KERN_INFO "fs_chrdev: invalid argment\n");
	}
	
	return 0;  // 返回 0 表示 ioctl 操作成功
}

/*
 * 定义文件操作结构体 file_operations(字符设备的核心"操作清单")
 * 该结构体关联了字符设备支持的所有操作接口,内核通过该结构体回调对应的驱动函数
 * 每个成员对应一个操作接口,未实现的接口可设为 NULL(用户态调用时会返回 -ENOTTY)
 */
static struct file_operations fs_chrdev_fops = {
	.owner = THIS_MODULE,  // 标注该结构体所属的模块(必须设置,用于内核模块管理,防止模块被意外卸载)
	.open = fs_chrdev_open,  // 关联 open 操作接口
	.release = fs_chrdev_release,  // 关联 release 操作接口
	.read = fs_chrdev_read,  // 关联 read 操作接口
	.write = fs_chrdev_write,  // 关联 write 操作接口
	.unlocked_ioctl = fs_chrdev_unlocked_ioctl,  // 关联 unlocked_ioctl 操作接口
};

/*
 * 模块加载函数(驱动插入内核时执行,对应 insmod 命令)
 * __init 宏标注:该函数仅在模块加载时执行一次,执行完成后会被内核释放内存
 * 作用:完成字符设备的初始化工作(申请设备号、初始化 cdev、注册 cdev 等)
 * @return: 0 表示加载成功;负数表示加载失败(驱动不会被插入内核)
 */
static int __init fs_chrdev_init(void)
{
	int ret = 0;  // 函数返回值,用于记录各步骤的执行结果
	dev_t devno = MKDEV(dev_major, dev_minor);  // 构建完整设备号(主设备号+次设备号)
	// MKDEV 宏:将主设备号和次设备号组合成一个 32 位的 dev_t 类型设备号(高 12 位为主设备号,低 20 位为次设备号)

	printk(KERN_INFO "fs_chrdev: module enter\n");

	/*1. 申请设备号(静态申请:手动指定主设备号,适用于已知未被占用的设备号)
	 * 函数原型:int register_chrdev_region(dev_t from, unsigned int count, const char *name)
	 * 参数说明:
	 *   from: 起始设备号(此处为构建好的 devno)
	 *   count: 申请的设备数量(此处为 dev_number=1)
	 *   name:  设备名称(会在 /proc/devices 中显示,用于标识设备)
	 * 返回值:0 表示申请成功;负数表示申请失败(如设备号已被占用)
	 */
	ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
	if (ret < 0) {  // 设备号申请失败
		printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n");  // KERN_ERR:错误级日志,标注严重错误
		goto err1;  // 跳转到错误处理标签,直接返回错误
	}

	/*2. 初始化 cdev 结构体并注册到内核(将 cdev 与设备号、文件操作结构体关联)
	 * 第一步:cdev_init 初始化 cdev 结构体
	 * 函数原型:void cdev_init(struct cdev *cdev, const struct file_operations *fops)
	 * 参数说明:
	 *   cdev: 要初始化的 cdev 结构体变量(此处为全局变量 cdev)
	 *   fops: 关联的文件操作结构体(此处为 fs_chrdev_fops)
	 */
	cdev_init(&cdev, &fs_chrdev_fops);
	cdev.owner = THIS_MODULE;  // 标注 cdev 所属的模块(必须设置,与 file_operations 中的 owner 一致)

	/*第二步:cdev_add 将 cdev 结构体注册到内核,完成字符设备的最终注册
	 * 函数原型:int cdev_add(struct cdev *p, dev_t dev, unsigned int count)
	 * 参数说明:
	 *   p:     已初始化的 cdev 结构体指针
	 *   dev:   关联的设备号(此处为 devno)
	 *   count: 设备数量(此处为 1)
	 * 返回值:0 表示注册成功;负数表示注册失败
	 */
	ret = cdev_add(&cdev, devno, 1);
	if (ret < 0) {  // cdev 注册失败
		printk(KERN_ERR "fs_chrdev: cdev_add failed\n");
		goto err2;  // 跳转到错误处理标签,释放已申请的设备号
	}

	/*这个 return 0 不能忽略,否则导致申请到的资源被释放掉*/
	return 0;  // 模块加载成功,返回 0

/* 错误处理标签(采用 goto 语句实现资源回滚,是内核驱动的常用最佳实践)
 * 当后续步骤失败时,需要释放前面已成功申请的资源,避免内存泄漏或资源占用
 */
err2:  // cdev_add 失败时,执行此处
	unregister_chrdev_region(devno, dev_number);  // 释放已申请的设备号
err1:  // register_chrdev_region 失败时,执行此处
	return ret;  // 返回错误码,告知内核模块加载失败
}

/*
 * 模块卸载函数(驱动从内核移除时执行,对应 rmmod 命令)
 * __exit 宏标注:该函数仅在模块卸载时执行一次,未编译进内核的模块会包含该函数
 * 作用:释放模块加载时申请的所有资源(注销 cdev、释放设备号),避免资源泄漏
 */
static void __exit fs_chrdev_exit(void)
{
	dev_t devno = MKDEV(dev_major, dev_minor);  // 重构完整设备号

	printk(KERN_INFO "fs_chrdev: module exit\n");

	/*1. 注销 cdev 结构体,从内核中移除字符设备(释放 cdev 占用的内核资源)
	 * 函数原型:void cdev_del(struct cdev *p)
	 * 参数:要注销的 cdev 结构体指针
	 */
	cdev_del(&cdev);

	/*2. 释放设备号,将设备号归还给内核(避免设备号被永久占用)
	 * 函数原型:void unregister_chrdev_region(dev_t from, unsigned int count)
	 * 参数与 register_chrdev_region 一致
	 */
	unregister_chrdev_region(devno, dev_number);
}

/*
 * 模块加载/卸载函数关联宏(内核用于识别模块的入口和出口函数)
 * module_init: 关联模块加载函数,insmod 命令执行时会调用该宏指定的函数
 * module_exit: 关联模块卸载函数,rmmod 命令执行时会调用该宏指定的函数
 */
module_init(fs_chrdev_init);
module_exit(fs_chrdev_exit);

/*
 * 模块许可证宏(必须设置,否则内核会报警告,且部分功能无法使用)
 * MODULE_LICENSE("GPL"): 声明模块遵循 GPL 协议,与内核开源协议兼容
 * 其他可选值:MIT、BSD 等(非 GPL 协议可能会触发内核模块兼容性限制)
 */
MODULE_LICENSE("GPL");

test.c

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>

#include "fs_chrdev.h"

int main(int argc, const char *argv[])
{
	int fd, nbyte;
	char buf[128] = "Hello World";

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

	ioctl(fd, FS_CMD1);
	ioctl(fd, FS_CMD2);

	close(fd);
	
	return 0;
}
相关推荐
代码游侠1 小时前
学习笔记——Linux内核与嵌入式开发2
linux·运维·arm开发·嵌入式硬件·学习·架构
郝学胜-神的一滴1 小时前
深入Linux网络编程:accept函数——连接请求的“摆渡人”
linux·服务器·开发语言·网络·c++·程序人生
小义_2 小时前
【Docker】知识一
linux·docker·云原生·容器
Max_uuc2 小时前
【C++ 硬核】摆脱开发板:用 Google Test + Mock 构建嵌入式 TDD (测试驱动开发) 体系
驱动开发·tdd
wefg12 小时前
【Linux】进程地址空间深入理解
linux·运维·服务器
ZHANG13HAO2 小时前
android13 4G网络环境和wifi内网说明
linux·服务器·网络
Linux运维技术栈2 小时前
Magento 2.3.5 宝塔Linux环境完整安装指南(避坑版+图文详解)
linux·运维·服务器
古月-一个C++方向的小白2 小时前
Linux——命令行参数与环境变量
linux·运维
qinyia2 小时前
使用AI助手完成服务器系统备份迁移任务
linux·运维·服务器