Linux字符设备驱动开发入门:从file_operations到内存设备实现
1. 先搞清楚字符设备驱动到底要解决什么问题如果你刚开始接触 Linux 驱动开发看到“字符设备驱动”这个词可能会觉得抽象。简单来说它的核心任务就是让用户空间的程序能够像读写普通文件一样去操作一个硬件设备。比如你写一个echo “1” /dev/my_led就能点亮一个 LED或者cat /dev/my_sensor就能读到传感器数据背后就是字符设备驱动在起作用。所以这个教程要解决的不是一个纯理论问题而是一个非常具体的工程问题如何把一个硬件或虚拟设备的能力包装成/dev目录下的一个文件节点并定义当用户对这个文件进行open,read,write,close等操作时驱动应该执行什么代码。这整个过程就是“文件操作”与内核驱动“操作映射”的实现。对于嵌入式开发、内核模块开发或者需要与特定硬件打交道的开发者来说这是必须跨过去的一道坎。它最关键的价值在于提供了一套标准、统一的接口VFS虚拟文件系统使得应用程序无需关心底层硬件细节大大降低了开发复杂度。但它的难点也在于此你需要理解内核的这套框架并正确地将自己的驱动“挂”上去。很多人卡住的地方往往不是file_operations结构体里那几个函数指针怎么写而是不理解“为什么这么写”以及“写错了会怎么样”。下面我就以一个虚拟的“内存字符设备”为例带你从环境准备到代码调试完整走一遍。2. 动手前的环境与思路准备在开始写代码之前先明确两件事测试环境和开发思路。这能帮你避开至少一半的初级错误。2.1 你需要什么样的环境这不是一个可以在 Windows 记事本里写完就能跑的程序。你必须有一个Linux 内核开发环境。操作系统任何主流的 Linux 发行版都可以如 Ubuntu、Fedora、CentOS。我建议直接用物理机安装或者在 VMware/VirtualBox 里安装一个干净的虚拟机。不推荐在 WSLWindows Subsystem for Linux 的第一代中进行驱动开发因为 WSL1 的内核接口与标准 Linux 有差异且加载内核模块比较麻烦。WSL2 使用了真实的 Linux 内核理论上是支持的但对于初学者虚拟机的环境更纯粹问题更少。内核头文件你需要安装与你当前运行内核版本对应的内核头文件或开发包这样才能编译内核模块。# 在 Ubuntu/Debian 上 sudo apt update sudo apt install linux-headers-$(uname -r) build-essential代码编辑器Vim、VSCode配合远程开发插件连到虚拟机都可以。权限加载和卸载内核模块需要root权限所以后续操作大多需要sudo。2.2 理解核心框架file_operations这是字符设备驱动的心脏。它是一个结构体里面定义了一堆函数指针。当应用程序调用read(fd, buf, size)时VFS 最终就会找到你这个设备文件对应的file_operations结构体并调用里面你注册的.read函数指针。一个最简化的思维模型如下用户程序 write(fd, “hello”, 5) | V 系统调用层 | V VFS虚拟文件系统 | V 根据文件inode找到驱动 你的驱动my_driver_fops.write() | V 你的硬件操作代码你的工作就是实现my_driver_fops.write()这个函数并把my_driver_fops这个结构体告诉内核。3. 从零开始实现一个最简单的内存字符设备我们来实现一个叫做mychardev的设备。它不连接真实硬件只是在内核里划出一块内存比如 4KB作为“设备”。向这个设备文件写入数据就是向这块内存写从这个设备文件读取数据就是从这块内存读。3.1 第一步编写驱动源码mychardev.c#include linux/module.h #include linux/kernel.h #include linux/fs.h // 包含 file_operations 结构体定义 #include linux/cdev.h // 字符设备结构体 cdev #include linux/slab.h // kmalloc, kfree #include linux/uaccess.h // copy_to_user, copy_from_user #define DEVICE_NAME mychardev #define BUFFER_SIZE 4096 static int major_num 0; // 主设备号0表示动态分配 static struct cdev my_cdev; // 内核字符设备对象 static char *device_buffer NULL; // 我们的“设备”内存 // 当设备文件被打开时调用 static int mychardev_open(struct inode *inode, struct file *file) { printk(KERN_INFO mychardev: Device opened.\n); return 0; // 返回0表示成功 } // 当设备文件被关闭时调用 static int mychardev_release(struct inode *inode, struct file *file) { printk(KERN_INFO mychardev: Device closed.\n); return 0; } // 从设备读取数据到用户空间 static ssize_t mychardev_read(struct file *file, char __user *user_buf, size_t count, loff_t *offset) { size_t bytes_to_read; int err; // 计算还能读多少字节从偏移量*offset开始 if (*offset BUFFER_SIZE) { return 0; // 已经读到末尾了 } bytes_to_read min(count, (size_t)(BUFFER_SIZE - *offset)); // 将内核缓冲区(device_buffer *offset)的数据拷贝到用户空间(user_buf) if (bytes_to_read 0) { err copy_to_user(user_buf, device_buffer *offset, bytes_to_read); if (err) { printk(KERN_ERR mychardev: Failed to copy %zu bytes to user.\n, bytes_to_read); return -EFAULT; // 拷贝失败返回错误码 } *offset bytes_to_read; // 更新文件偏移量 printk(KERN_INFO mychardev: Read %zu bytes from offset %lld.\n, bytes_to_read, *offset - bytes_to_read); return bytes_to_read; // 返回实际读取的字节数 } return 0; } // 从用户空间写数据到设备 static ssize_t mychardev_write(struct file *file, const char __user *user_buf, size_t count, loff_t *offset) { size_t bytes_to_write; int err; // 计算还能写多少字节 if (*offset BUFFER_SIZE) { return -ENOSPC; // 设备“空间”已满 } bytes_to_write min(count, (size_t)(BUFFER_SIZE - *offset)); if (bytes_to_write 0) { // 将用户空间(user_buf)的数据拷贝到内核缓冲区(device_buffer *offset) err copy_from_user(device_buffer *offset, user_buf, bytes_to_write); if (err) { printk(KERN_ERR mychardev: Failed to copy %zu bytes from user.\n, bytes_to_write); return -EFAULT; } *offset bytes_to_write; printk(KERN_INFO mychardev: Wrote %zu bytes at offset %lld.\n, bytes_to_write, *offset - bytes_to_write); return bytes_to_write; // 返回实际写入的字节数 } return 0; } // 定义文件操作结构体建立映射关系 static struct file_operations mychardev_fops { .owner THIS_MODULE, // 防止模块在使用时被卸载 .open mychardev_open, .release mychardev_release, .read mychardev_read, .write mychardev_write, // 这里没有定义 .llseek默认是逐字节偏移对于我们的简单设备够用了 }; // 模块初始化函数insmod时调用 static int __init mychardev_init(void) { dev_t dev_num; int ret; printk(KERN_INFO mychardev: Initializing module.\n); // 1. 动态申请一个主设备号以及对应的次设备号范围这里我们只要一个设备 ret alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME); if (ret 0) { printk(KERN_ERR mychardev: Failed to allocate device number.\n); return ret; } major_num MAJOR(dev_num); // 提取出主设备号 printk(KERN_INFO mychardev: Allocated major number %d.\n, major_num); // 2. 分配“设备”内存 device_buffer kmalloc(BUFFER_SIZE, GFP_KERNEL); if (!device_buffer) { ret -ENOMEM; goto fail_buffer; } memset(device_buffer, 0, BUFFER_SIZE); // 初始化为0 // 3. 初始化 cdev 结构并将其与 file_operations 关联 cdev_init(my_cdev, mychardev_fops); my_cdev.owner THIS_MODULE; // 4. 将 cdev 添加到内核系统使其生效 ret cdev_add(my_cdev, dev_num, 1); if (ret 0) { printk(KERN_ERR mychardev: Failed to add cdev to system.\n); goto fail_cdev; } printk(KERN_INFO mychardev: Module loaded successfully. Use mknod /dev/%s c %d 0 to create device file.\n, DEVICE_NAME, major_num); return 0; fail_cdev: kfree(device_buffer); fail_buffer: unregister_chrdev_region(dev_num, 1); return ret; } // 模块清理函数rmmod时调用 static void __exit mychardev_exit(void) { dev_t dev_num MKDEV(major_num, 0); // 根据主设备号生成设备号 printk(KERN_INFO mychardev: Exiting module.\n); // 1. 从系统删除 cdev cdev_del(my_cdev); // 2. 释放设备内存 kfree(device_buffer); // 3. 释放设备号 unregister_chrdev_region(dev_num, 1); printk(KERN_INFO mychardev: Module unloaded.\n); } // 注册初始化和清理函数 module_init(mychardev_init); module_exit(mychardev_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple character device driver example.); MODULE_VERSION(0.1);3.2 第二步编写 Makefile在同一个目录下创建Makefile。注意$(shell uname -r)会自动获取你当前内核的版本确保路径正确。obj-m : mychardev.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean3.3 第三步编译、加载与测试编译模块make成功后会生成mychardev.ko文件。加载模块sudo insmod mychardev.ko使用dmesg查看内核日志应该能看到我们printk打印的“Allocated major number X”信息。记下这个主设备号X。创建设备文件 驱动加载后内核里已经有了这个设备但/dev目录下还没有对应的文件节点。需要手动创建# 假设主设备号是 250 sudo mknod /dev/mychardev c 250 0 sudo chmod 666 /dev/mychardev # 让普通用户也能读写这里c表示字符设备250是主设备号0是次设备号。测试读写# 写入数据 echo Hello from userspace! /dev/mychardev # 读取数据 cat /dev/mychardev再次查看dmesg你应该能看到类似“Wrote 22 bytes...”和“Read ... bytes...”的日志。测试偏移# 先写满一些数据 echo -n ABCDEFGHIJ /dev/mychardev # 用dd从偏移量5开始读3个字节 dd if/dev/mychardev bs1 count3 skip5 2/dev/null输出应该是“FGH”。这说明我们驱动里的*offset处理是有效的。卸载模块sudo rmmod mychardev # 记得删除设备文件可选但建议清理 sudo rm /dev/mychardev查看dmesg确认清理日志被打印。4. 核心细节与避坑指南上面的代码跑通了但里面有很多细节决定了驱动是“能用”还是“稳定好用”。下面拆开讲。4.1 用户空间与内核空间的数据交换copy_to/from_user这是驱动开发中最关键也最容易出错的地方之一。内核空间和用户空间的内存是隔离的不能直接通过指针访问。错误做法memcpy(device_buffer, user_buf, count);这会导致内核崩溃或数据错误。正确做法使用copy_from_user(dst_kernel, src_user, size)和copy_to_user(dst_user, src_kernel, size)。为什么这两个函数会检查用户空间指针的有效性并进行安全的拷贝。如果指针无效比如用户传递了一个非法地址函数会返回未能拷贝的字节数驱动可以返回-EFAULT错误给应用层而不是导致系统崩溃。避坑永远不要相信来自用户空间的任何数据。除了用copy_*_user还要检查count和offset的合法性防止越界访问我们的device_buffer。上面的代码中min(count, BUFFER_SIZE - *offset)就是做这个的。4.2 设备号管理静态 vs 动态静态分配在代码里写死#define MY_MAJOR 250。风险是可能和系统已有的设备号冲突。动态分配推荐使用alloc_chrdev_region。内核会分配一个空闲的主设备号给我们。加载模块后必须通过dmesg或/proc/devices查看实际分配到的号。cat /proc/devices | grep mychardev设备号组成一个设备号由主设备号Major和次设备号Minor组成。主设备号对应驱动次设备号对应该驱动下的不同设备实例。我们例子中只创建了一个设备所以次设备号从0开始数量为1。4.3file_operations的成员与文件指针struct file *filp我们的例子只实现了最基本的四个操作。完整的file_operations有几十个成员常见的有.llseek设置文件偏移。不实现的话默认是逐字节偏移像我们的例子对于“内存设备”够用。如果是“串口”这类设备可能就不需要seek。.unlocked_ioctl/.compat_ioctl用于实现除读写之外的各种设备控制命令如设置波特率、读取状态等。这是驱动与应用交互的另一个重要通道。.poll实现select/poll系统调用用于查询设备是否可读/可写。.mmap将设备内存映射到用户进程地址空间实现零拷贝高效访问。每个操作函数都接收一个struct file *filp参数。这个指针指向内核内部维护的“打开文件”对象。驱动可以通过filp-private_data来存储和获取一次文件打开会话中的私有数据。例如如果每个打开的设备文件需要独立的缓冲区可以在.open里分配并赋值给filp-private_data在.read/.write/.release里再取出来用。4.4 并发与同步驱动不是单线程的一个重要的认知你的驱动函数可能被多个进程同时调用。比如两个进程同时open并write同一个设备文件。问题上面的示例代码没有做任何并发保护。如果两个进程同时执行mychardev_write它们可能同时修改*offset和device_buffer导致数据错乱或偏移量计算错误。解决方案使用内核提供的同步机制如信号量semaphore、互斥锁mutex或自旋锁spinlock。对于字符设备常用struct mutex。在设备结构体中定义一个互斥锁struct mutex lock;在模块初始化时初始化它mutex_init(my_device.lock);在.open函数里上锁如果需要保护整个设备更常见的是在.read/.write/.ioctl函数内部操作共享数据如device_buffer,offset前上锁mutex_lock(my_device.lock);操作完成后解锁mutex_unlock(my_device.lock);注意锁的粒度要仔细设计。锁住整个函数最简单但会影响性能。要确保在持有锁的时候不会调用可能引起睡眠的函数如copy_from_user在某些情况下可能睡眠否则可能造成死锁。使用互斥锁mutex是允许睡眠的这在大多数字符设备场景是安全的。4.5 调试与日志printk是你的好朋友驱动运行在内核空间不能用printf。printk是内核的“打印”函数输出到内核日志缓冲区可以通过dmesg命令查看。日志级别KERN_INFO,KERN_ERR,KERN_DEBUG等。这有助于筛选信息。printk(KERN_INFO “...”)。不要滥用在频繁调用的函数如.read里打印大量日志会严重拖慢系统刷屏导致看不到关键错误。仅在关键路径、错误处理或初始化/退出时使用。动态调试更高级的调试可以使用dynamic debug或ftrace但这需要更多内核知识。初期用好printk和dmesg足以解决大部分问题。5. 进阶如何让驱动更“像”一个标准设备我们的基础版本能用但离一个“好”的驱动还有距离。以下是几个优化方向5.1 自动创建设备节点udev/mdev我们之前需要手动mknod这很不友好。现代 Linux 使用udev桌面发行版或mdev嵌入式系统来管理/dev目录。驱动只需要在初始化时在/sys/class/下创建一个类class和设备deviceudev就会根据规则自动创建/dev节点。包含头文件#include linux/device.h创建类和设备static struct class *my_class; static struct device *my_device; // 在模块初始化函数中 (mychardev_init) my_class class_create(THIS_MODULE, “mychardev_class”); if (IS_ERR(my_class)) { ... } my_device device_create(my_class, NULL, dev_num, NULL, “mychardev”); if (IS_ERR(my_device)) { ... }在模块退出函数中销毁device_destroy(my_class, dev_num); class_destroy(my_class);这样加载模块后/dev/mychardev会自动出现无需手动mknod。5.2 实现ioctl进行设备控制读写数据用read/write但像“清空缓冲区”、“获取设备状态”、“设置工作模式”这类操作更适合用ioctl。定义命令号需要保证命令号在系统中唯一。通常使用_IO,_IOR,_IOW,_IOWR宏来生成。#include linux/ioctl.h #define MYCHARDEV_IOC_MAGIC k #define MYCHARDEV_CLEAR_BUF _IO(MYCHARDEV_IOC_MAGIC, 0) #define MYCHARDEV_GET_SIZE _IOR(MYCHARDEV_IOC_MAGIC, 1, int) #define MYCHARDEV_SET_VALUE _IOW(MYCHARDEV_IOC_MAGIC, 2, int)实现.unlocked_ioctl函数static long mychardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int ret 0; switch (cmd) { case MYCHARDEV_CLEAR_BUF: memset(device_buffer, 0, BUFFER_SIZE); printk(KERN_INFO “Buffer cleared.\n”); break; case MYCHARDEV_GET_SIZE: ret copy_to_user((int __user *)arg, BUFFER_SIZE, sizeof(BUFFER_SIZE)); break; // ... 处理其他命令 default: ret -ENOTTY; // 不支持的命令 } return ret; }将.unlocked_ioctl添加到file_operations。用户空间调用应用层使用ioctl(fd, MYCHARDEV_CLEAR_BUF, 0)即可。5.3 支持poll/select异步通知如果设备数据不是随时可读比如等待传感器数据应用层可以用poll或select来等待而不是忙等待busy-loop调用read。这需要驱动实现.poll函数并在数据就绪时唤醒等待队列。6. 生产环境下的考量与排查清单如果你写的驱动要用于实际产品以下几点需要特别注意6.1 资源管理与错误处理申请的资源一定要释放kmalloc对应kfreealloc_chrdev_region对应unregister_chrdev_regioncdev_add对应cdev_delclass_create对应class_destroy。在初始化失败时要按相反顺序释放已申请的资源就像示例代码中的goto链。检查返回值内核函数调用失败很常见。每一个可能失败的内核函数调用如kmalloc,cdev_add都必须检查返回值并进行相应的错误处理和资源清理。6.2 稳定性与健壮性处理非法输入用户空间传递的count可能为0或负数offset可能非常大。驱动必须验证这些参数防止整数溢出或越界访问。处理信号中断像copy_from_user这样的函数可能被信号中断返回-ERESTARTSYS。驱动需要妥善处理通常是将错误传递回上层。内存屏障与原子操作在多核CPU上简单的操作都可能出问题。对于简单的状态标志考虑使用atomic_t类型。6.3 问题排查通用流程驱动出问题不要慌按顺序查看日志dmesg | tail -50或journalctl -k。这是第一手信息printk的输出都在这里。关注KERN_ERR级别的信息。检查模块是否加载lsmod | grep mychardev。检查设备号cat /proc/devices | grep mychardev。确认主设备号。检查设备节点ls -l /dev/mychardev。确认设备类型是c主次设备号正确权限是否足够。检查代码逻辑并发操作是否有锁保护copy_to/from_user返回值检查了吗缓冲区边界检查了吗ioctl命令号定义是否和用户空间一致使用简单测试程序不要一上来就用复杂应用测。写一个最简单的 C 程序只调用open,write,read,close看是否成功。使用内核调试工具如果问题诡异如偶发崩溃可能需要kdb,kgdb或SystemTap等高级工具这需要更深入的内核知识。驱动开发是一个对细节要求极高的领域。最好的学习方式就是动手从这个小例子开始修改它打破它再修复它。理解每一个 API 背后的原因远比记住 API 本身更重要。当你能够稳定地实现这个内存字符设备驱动时你就已经掌握了 Linux 字符设备驱动最核心的骨架后续接入真实硬件无非是将对device_buffer的读写替换成对硬件寄存器的ioread/iowrite或 DMA 操作而已。

相关新闻

业务流程图、数据流图与数据字典:系统分析与设计的核心三要素

业务流程图、数据流图与数据字典:系统分析与设计的核心三要素

1. 从“一团乱麻”到“清晰蓝图”:为什么我们需要这些图?刚入行做系统设计或者需求分析的时候,我最怕听到的一句话就是:“这个需求很简单,就是用户点一下,然后系统处理一下,最后出个结果。” 听…

2026/10/14 7:13:57 阅读更多 →
Android Root检测与绕过攻防实战:从RootBeer原理到多层次防御方案

Android Root检测与绕过攻防实战:从RootBeer原理到多层次防御方案

1. 项目概述:一场攻防视角下的Android安全博弈在Android应用安全领域,Root检测与绕过是一场持续上演的攻防博弈。作为开发者,你精心部署了各种检测手段,试图将那些运行在已获取Root权限设备上的应用拒之门外,以保护核心…

2026/10/6 6:54:18 阅读更多 →
Unity xLua性能分析工具实战:从卡顿定位到优化决策

Unity xLua性能分析工具实战:从卡顿定位到优化决策

1. 项目概述:为什么我们需要一个专门的Lua性能分析工具?在Unity项目里用xLua做热更新,这事儿现在挺普遍的。脚本逻辑用Lua写,改起来快,上线也灵活。但干过这行的都知道,爽快背后藏着坑。最让人头疼的&#…

2026/10/11 21:39:04 阅读更多 →

最新新闻

粒子群算法解柔性作业车间调度:双链编码实战与Kacem基准验证

粒子群算法解柔性作业车间调度:双链编码实战与Kacem基准验证

简介:一套基于Java的粒子群算法(PSO)实现,用于求解柔性作业车间调度(FJSP)问题,适合正在学习智能优化算法、开展生产调度相关课题或课程设计的学生与科研人员。压缩包共26个文件,主要…

2026/10/15 13:52:34 阅读更多 →
工业锅炉人孔设计:密封技术与安全运维详解

工业锅炉人孔设计:密封技术与安全运维详解

1. 锅炉人孔的核心价值解析在工业锅炉这个封闭的钢铁巨兽内部,常年维持着数百摄氏度的高温和数十个大气压的压力环境。人孔作为直径通常只有400-600mm的圆形开口,却是维系整个系统安全运行的"生命通道"。这个看似简单的结构件,实际…

2026/10/15 13:52:34 阅读更多 →
3-1 独立按键控制LED状态(SDCC + Notepad++ + STC-ISP/普中ISP)

3-1 独立按键控制LED状态(SDCC + Notepad++ + STC-ISP/普中ISP)

问题分析上节实现了按下按键LED亮,松开按键LED 灭,这节需要实现的是第一次按下按键LED会亮,第二次按下按键LED会灭,如此循环。 像要实现这个功能,可以先读取灯的值,当按键按下后,如果是高电平就…

2026/10/15 13:52:34 阅读更多 →
HyperFrames时间超分辨率全解析:从深度估计到慢动作补帧实战

HyperFrames时间超分辨率全解析:从深度估计到慢动作补帧实战

最近一段时间,“hyperframes”在视频处理圈子里出现的频率明显高了。这个词直译过来是“超帧”,但圈内讨论它的时候,基本都指向同一样东西:用深度学习做时间超分辨率(Temporal Super-Resolution)&#xff0…

2026/10/15 13:52:34 阅读更多 →
TPP技术解析:药物靶点发现的热力学蛋白质组学方法

TPP技术解析:药物靶点发现的热力学蛋白质组学方法

1. TPP技术概述:当热力学遇上蛋白质组学在药物研发领域,最令人头疼的问题莫过于"这个化合物到底作用于哪个靶点"。传统方法往往需要将蛋白质从细胞环境中分离出来进行研究,就像把鱼捞出水面观察其游动姿态——结果必然失真。TPP&am…

2026/10/15 13:52:34 阅读更多 →
R语言实战:基于KNN聚类的时间序列预测方法解析

R语言实战:基于KNN聚类的时间序列预测方法解析

大概半年前接了一个实际需求:预测某工厂循环水系统的运行负荷,为调度提供短期参考。数据拿回来倒是简单,一天24个小时一个点,连续一个月。可问题在于精度——ARIMA、指数平滑这类传统模型遇到周期性叠加随机波动、偶尔再来点异常数…

2026/10/15 13:51:29 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →