尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux字符设备驱动开发入门:从内核模块到虚拟设备实战

Linux字符设备驱动开发入门:从内核模块到虚拟设备实战 在嵌入式开发领域Linux设备驱动开发是连接硬件与操作系统的核心桥梁也是工程师深入理解Linux内核工作机制、掌握系统底层控制权的关键路径。无论是为定制硬件编写驱动还是优化现有驱动的性能与稳定性这项技能都直接决定了嵌入式产品的功能完整性与可靠性。对于希望从应用层开发转向系统底层或是在嵌入式Linux平台上进行深度定制的开发者而言驱动开发是必须跨越的一道技术门槛。然而从理解概念到动手编写一个能稳定工作的驱动程序中间存在着巨大的实践鸿沟。许多开发者卡在了环境搭建、内核模块编译、设备模型理解以及硬件交互调试等环节。本文将以一个虚拟的字符设备驱动为例带你从零开始完成一个Linux内核模块的编写、编译、加载、测试与卸载的全过程。我们将重点关注驱动开发中的核心骨架与通用流程而非特定硬件的寄存器操作确保你能掌握一套可复用于多种设备类型的开发方法论。通过本文你将能理解驱动的基本框架学会使用必要的内核API并掌握驱动调试的基本技巧为后续开发真实的硬件驱动打下坚实基础。1. 理解Linux设备驱动的核心概念与框架在动手写代码之前必须建立对Linux设备驱动模型的基本认知。驱动开发不是天马行空的编程而是在一套严格的框架约束下实现内核规定好的接口。1.1 驱动在内核中的角色为什么需要它简单来说设备驱动程序是操作系统内核的一部分它充当了硬件设备与用户空间应用程序或内核其他子系统之间的翻译官。应用程序通过标准的系统调用如open,read,write,ioctl来访问文件或设备这些调用经由虚拟文件系统VFS层最终被分派到具体设备驱动中对应的函数去执行。驱动则将这些抽象的操作翻译成对特定硬件寄存器的读写、中断的处理以及DMA的配置等具体操作。没有驱动内核就无法识别和管理硬件。驱动开发的核心任务就是按照内核提供的设备模型填充一个包含各种操作函数指针的结构体如file_operations并将这个驱动“注册”到内核中告诉内核“这里有一个设备它的各种操作由我定义的这些函数来处理”。1.2 内核模块驱动的主要存在形式驱动程序通常以内核模块的形式存在。模块是一种可以在系统运行时动态加载到内核或从内核卸载的代码块这为驱动的开发和调试带来了极大的便利。相比于将驱动直接编译进内核镜像模块化方式避免了每次修改都需要重新编译和重启整个内核的繁琐过程。一个内核模块至少需要两个函数module_init(xxx_init): 模块加载时被调用的初始化函数。在这里完成驱动的注册、资源的申请如内存、中断号。module_exit(xxx_exit): 模块卸载时被调用的清理函数。在这里进行反向操作注销驱动、释放资源。模块通过Makefile调用内核的构建系统kbuild进行编译生成一个.koKernel Object文件。1.3 设备类型字符设备、块设备和网络设备Linux将设备大致分为三类驱动开发也依此分类字符设备以字节流形式进行顺序访问的设备如键盘、鼠标、串口、虚拟设备等。驱动需要实现read,write等操作。本文示例即为此类。块设备以数据块为单位进行随机访问的设备如硬盘、U盘、SD卡等。驱动需要实现更复杂的请求队列管理。网络设备面向数据包的设备如网卡。驱动需要遵循网络子系统协议实现数据包的发送和接收。三类设备的驱动模型和API有显著差异。字符设备驱动是入门的最佳选择因为它模型相对简单却能涵盖驱动开发的大部分核心概念。1.4 关键数据结构file_operations与cdev这是字符设备驱动中最重要的两个数据结构struct file_operations: 这是一个函数指针的集合定义了设备所能支持的操作。驱动开发者的主要工作就是实现这个结构体中需要的函数例如struct file_operations fops { .owner THIS_MODULE, .open mydev_open, .release mydev_close, .read mydev_read, .write mydev_write, .unlocked_ioctl mydev_ioctl, // 注意现代内核多用 unlocked_ioctl // ... 其他操作 };struct cdev: 代表一个字符设备的内核结构体。它内部包含了一个指向file_operations的指针。我们需要分配并初始化一个cdev并将其与具体的设备号关联起来。理解这些概念后我们就可以开始搭建开发环境并动手实践了。2. 驱动开发环境准备与内核头文件驱动开发严重依赖于Linux内核源代码因为我们需要使用内核中定义的数据结构、函数和宏。环境配置是第一步也是最容易出错的一步。2.1 开发环境选择与内核版本确认对于学习和早期开发推荐在虚拟机如VirtualBox/VMware中安装一个桌面版Linux发行版例如Ubuntu LTS版本。这能提供完整的编译工具链和图形化调试便利。首先确定你当前运行的内核版本这决定了你需要安装哪个版本的内核头文件。uname -r输出可能类似于5.15.0-91-generic。请记录这个完整的版本字符串。2.2 安装必备的软件包你需要安装编译器、内核头文件以及构建工具。# 更新软件包列表 sudo apt update # 安装编译工具链、内核头文件版本必须与 uname -r 输出严格一致 sudo apt install build-essential linux-headers-$(uname -r)build-essential包含了gcc, make等基础编译工具。linux-headers-$(uname -r)则安装了与你当前运行内核版本完全匹配的头文件。这是最关键的一步版本不匹配会导致编译失败或模块无法加载。2.3 验证内核头文件路径安装成功后头文件通常位于/lib/modules/$(uname -r)/build或/usr/src/linux-headers-$(uname -r)/。这个路径将在后续的Makefile中用到。你可以通过以下命令验证ls -d /lib/modules/$(uname -r)/build如果该目录存在且内部有Makefile,include等目录说明头文件已正确安装。2.4 创建一个独立的驱动开发工作区为了避免污染系统目录建议创建一个独立的工作目录。mkdir -p ~/linux_driver/my_first_driver cd ~/linux_driver/my_first_driver后续所有的源代码和Makefile都将放在这个目录下。3. 编写第一个字符设备驱动my_chardev我们将创建一个名为my_chardev的虚拟字符设备。它不控制真实硬件而是在内核中维护一段内存缓冲区用户程序可以对其进行读写。这个例子涵盖了驱动的基本骨架。3.1 创建驱动源代码文件mydev.c在工作目录下创建mydev.c文件并开始编写代码。第一部分头文件与宏定义#include linux/module.h #include linux/fs.h // 包含 file_operations 结构体 #include linux/cdev.h // 包含 cdev 结构体及相关函数 #include linux/device.h // 用于自动创建设备文件可选但推荐 #include linux/uaccess.h // 提供 copy_to_user/copy_from_user #include linux/slab.h // 提供 kmalloc/kfree #define DEVICE_NAME mydev // 设备名称会在 /proc/devices 和 sysfs 中显示 #define CLASS_NAME myclass // 设备类名称用于 sysfs #define BUF_LEN 1024 // 设备内部缓冲区大小 MODULE_LICENSE(GPL); // 模块许可证必须声明如GPL MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple example character device driver);头文件是驱动与内核交互的接口每个都有其特定用途。uaccess.h中的函数用于在内核空间和用户空间之间安全地拷贝数据这是驱动中读写操作的关键。第二部分驱动全局变量定义static int major_num 0; // 主设备号0表示动态分配 static struct class* mydev_class NULL; static struct device* mydev_device NULL; static struct cdev my_cdev; // 设备数据缓冲区及其保护锁简单示例未使用锁 static char device_buffer[BUF_LEN]; static int buffer_offset 0; // 模拟一个简单的“写位置”major_num: 设备号由主设备号和次设备号组成。主设备号标识设备类型驱动次设备号标识具体设备实例。设为0让内核自动分配一个空闲的。mydev_class,mydev_device: 用于配合udev机制在/dev目录下自动创建设备节点文件比手动mknod更现代和可靠。my_cdev: 核心的字符设备结构体。device_buffer: 驱动内部模拟的“硬件”缓冲区。第三部分实现 file_operations 中的函数首先实现open和release对应close函数。它们通常用于初始化和清理与设备文件实例相关的资源。static int mydev_open(struct inode *inodep, struct file *filep) { printk(KERN_INFO mydev: Device has been opened.\n); // 这里可以增加打开计数检查防止重复打开等 return 0; // 返回0表示成功 } static int mydev_release(struct inode *inodep, struct file *filep) { printk(KERN_INFO mydev: Device has been closed.\n); return 0; }printk是内核中的打印函数输出到内核日志可通过dmesg命令查看。KERN_INFO是日志级别。接下来实现核心的read和write函数。这是驱动与用户程序交换数据的地方。static ssize_t mydev_read(struct file *filep, char __user *buf, size_t len, loff_t *offset) { int bytes_to_read; int bytes_not_copied; // 计算还能读取多少字节从缓冲区开头到当前偏移量 bytes_to_read buffer_offset - *offset; if (bytes_to_read 0) { return 0; // 表示 EOF (End Of File) } if (bytes_to_read len) { bytes_to_read len; // 用户请求的长度大于剩余数据长度 } // 将内核缓冲区数据拷贝到用户空间缓冲区 bytes_not_copied copy_to_user(buf, device_buffer *offset, bytes_to_read); if (bytes_not_copied ! 0) { printk(KERN_ERR mydev: Failed to copy %d bytes to user.\n, bytes_not_copied); return -EFAULT; // 返回一个错误码 } // 更新文件偏移量 *offset bytes_to_read; printk(KERN_INFO mydev: Sent %d bytes to user.\n, bytes_to_read); return bytes_to_read; // 返回实际读取的字节数 } static ssize_t mydev_write(struct file *filep, const char __user *buf, size_t len, loff_t *offset) { int bytes_to_write; int bytes_not_copied; // 检查写入是否会超出缓冲区 if (*offset BUF_LEN) { return -ENOSPC; // 设备空间不足 } bytes_to_write len; if (*offset bytes_to_write BUF_LEN) { bytes_to_write BUF_LEN - *offset; // 截断至缓冲区末尾 } // 将用户空间缓冲区数据拷贝到内核缓冲区 bytes_not_copied copy_from_user(device_buffer *offset, buf, bytes_to_write); if (bytes_not_copied ! 0) { printk(KERN_ERR mydev: Failed to copy %d bytes from user.\n, bytes_not_copied); return -EFAULT; } // 更新缓冲区的逻辑“结束”位置和文件偏移量 if (*offset bytes_to_write buffer_offset) { buffer_offset *offset bytes_to_write; } *offset bytes_to_write; printk(KERN_INFO mydev: Received %d bytes from user.\n, bytes_to_write); return bytes_to_write; // 返回实际写入的字节数 }关键点__user指针这是一个给编译器看的标记表明指针指向用户空间地址内核代码不能直接解引用必须通过copy_to_user/copy_from_user这类函数来访问。copy_to_user/copy_from_user: 执行从内核到用户、用户到内核的安全拷贝。它们会检查用户空间地址的合法性并处理可能的页面错误。返回值是未能成功拷贝的字节数因此0表示完全成功。偏移量*offset: 由VFS维护表示当前文件操作的位置。read/write需要更新它。返回值成功时返回实际传输的字节数失败时返回一个负的错误码如-EFAULT,-ENOSPC。最后组装file_operations结构体static struct file_operations fops { .owner THIS_MODULE, .open mydev_open, .release mydev_release, .read mydev_read, .write mydev_write, // .llseek ... 可以添加默认是通用实现 };第四部分模块的初始化与退出函数这是驱动生命的起点和终点。static int __init mydev_init(void) { int retval; dev_t dev_num; printk(KERN_INFO mydev: Initializing the mydev driver.\n); // 1. 动态申请一个设备号主设备号 retval alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME); if (retval 0) { printk(KERN_ERR mydev: Failed to allocate device number.\n); return retval; } major_num MAJOR(dev_num); // 从 dev_t 中提取主设备号 printk(KERN_INFO mydev: Allocated major number %d.\n, major_num); // 2. 创建设备类在 /sys/class/ 下可见 mydev_class class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(mydev_class)) { unregister_chrdev_region(MKDEV(major_num, 0), 1); printk(KERN_ERR mydev: Failed to create device class.\n); return PTR_ERR(mydev_class); } // 3. 初始化 cdev 结构并将其与 fops 关联 cdev_init(my_cdev, fops); my_cdev.owner THIS_MODULE; // 4. 将 cdev 添加到内核系统 retval cdev_add(my_cdev, dev_num, 1); if (retval 0) { class_destroy(mydev_class); unregister_chrdev_region(dev_num, 1); printk(KERN_ERR mydev: Failed to add cdev to system.\n); return retval; } // 5. 在 /dev 目录下创建设备节点文件 mydev_device device_create(mydev_class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(mydev_device)) { cdev_del(my_cdev); class_destroy(mydev_class); unregister_chrdev_region(dev_num, 1); printk(KERN_ERR mydev: Failed to create device.\n); return PTR_ERR(mydev_device); } printk(KERN_INFO mydev: Driver initialized successfully. Device node is /dev/%s\n, DEVICE_NAME); return 0; // 初始化成功 } static void __exit mydev_exit(void) { dev_t dev_num MKDEV(major_num, 0); // 根据主设备号重新构造 dev_t printk(KERN_INFO mydev: Removing the mydev driver.\n); // 清理顺序与初始化顺序相反 device_destroy(mydev_class, dev_num); cdev_del(my_cdev); class_destroy(mydev_class); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO mydev: Driver removed.\n); } module_init(mydev_init); module_exit(mydev_exit);初始化流程是驱动注册的标准流程每一步失败都需要回滚之前成功的步骤。__init和__exit宏告诉内核这些函数只在加载/卸载时使用之后可以释放其内存。3.2 编写内核模块的 Makefile在内核模块编译中Makefile必须调用内核的构建系统。在工作目录下创建Makefile注意首字母大写obj-m : mydev.o KERNEL_DIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNEL_DIR) M$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M$(PWD) clean解释obj-m : mydev.o: 告诉 kbuild 我们要构建一个名为mydev.ko的模块它由mydev.o生成。KERNEL_DIR: 指向已安装的内核头文件目录。-C $(KERNEL_DIR): 改变目录到内核源码目录使用那里的顶层Makefile。M$(PWD): 告诉内核构建系统模块源代码位于当前目录。modules: 要执行的目标是构建模块。4. 编译、加载、测试与卸载驱动代码和Makefile准备就绪后就可以进入实践环节了。4.1 编译驱动模块在mydev.c和Makefile所在的目录下执行make如果一切顺利你会看到类似以下的输出并生成mydev.ko文件make -C /lib/modules/5.15.0-91-generic/build M/home/yourname/linux_driver/my_first_driver modules make[1]: Entering directory /usr/src/linux-headers-5.15.0-91-generic CC [M] /home/.../mydev.o MODPOST /home/.../Module.symvers CC [M] /home/.../mydev.mod.o LD [M] /home/.../mydev.ko make[1]: Leaving directory /usr/src/linux-headers-5.15.0-91-generic4.2 加载驱动模块到内核使用insmod命令加载模块需要 root 权限sudo insmod mydev.ko加载后可以通过以下命令检查lsmod | grep mydev: 查看模块是否在已加载模块列表中。dmesg | tail -20: 查看内核日志的最后20行应该能看到我们驱动printk输出的初始化成功信息包括动态分配的主设备号例如major number 511。cat /proc/devices: 查看所有注册的设备应该能看到mydev及其主设备号。ls -l /dev/mydev: 检查设备节点文件是否被自动创建。其权限通常是crw-------主次设备号与日志中一致。4.3 编写用户空间测试程序创建一个简单的C程序test_mydev.c来测试驱动的读写功能#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include string.h int main() { int fd; char write_buf[] Hello from userspace!; char read_buf[1024] {0}; ssize_t bytes_written, bytes_read; // 1. 打开设备文件 fd open(/dev/mydev, O_RDWR); if (fd 0) { perror(Failed to open the device); return -1; } // 2. 向设备写入数据 bytes_written write(fd, write_buf, strlen(write_buf)); if (bytes_written 0) { perror(Failed to write to the device); close(fd); return -1; } printf(Wrote %zd bytes: %s\n, bytes_written, write_buf); // 3. 将文件偏移重置到开头lseek lseek(fd, 0, SEEK_SET); // 4. 从设备读取数据 bytes_read read(fd, read_buf, sizeof(read_buf) - 1); if (bytes_read 0) { perror(Failed to read from the device); close(fd); return -1; } read_buf[bytes_read] \0; // 确保字符串终止 printf(Read %zd bytes: %s\n, bytes_read, read_buf); // 5. 关闭设备文件 close(fd); return 0; }编译并运行测试程序gcc -o test_mydev test_mydev.c ./test_mydev如果驱动工作正常程序会输出写入和读取的内容。同时再次运行dmesg | tail -10可以看到驱动内部printk记录的open,write,read,release等调用日志。4.4 卸载驱动模块测试完成后使用rmmod命令卸载模块sudo rmmod mydev再次使用lsmod | grep mydev和ls -l /dev/mydev确认模块已卸载设备节点文件已自动删除。查看dmesg也能看到退出函数的打印信息。5. 驱动开发中的常见问题与排查路径驱动开发调试困难问题往往表现为内核崩溃Oops、模块加载失败、设备节点无法访问或功能异常。掌握系统化的排查方法至关重要。5.1 模块编译失败问题现象常见原因检查方式处理建议make报错提示找不到内核头文件或版本不匹配。1. 未安装linux-headers。2. 安装的linux-headers版本与uname -r不一致。3.Makefile中KERNEL_DIR路径错误。1. dpkg -lgrep linux-headers。br2. 对比uname -r和已安装的头文件版本。br3. 检查/lib/modules/$(uname -r)/build 链接是否存在。编译时提示函数或结构体未定义。1. 缺少必要的头文件#include。2. 使用的内核API在当前内核版本中已变更或移除。1. 检查错误信息中未定义的符号查阅内核文档或源码确定所属头文件。2. 核对所用内核版本与API的兼容性。1. 添加正确的头文件。2. 若API已变需修改代码使用新API。可参考内核源码中其他驱动的用法。5.2 模块加载失败问题现象常见原因检查方式处理建议sudo insmod失败提示Invalid module format。模块编译所用的内核版本与当前运行内核版本不匹配。使用modinfo mydev.ko查看模块的vermagic字段与uname -r对比。在正确的内核头文件环境下重新编译模块。sudo insmod失败提示Unknown symbol。模块引用了未导出的内核符号或依赖的其他模块未加载。查看dmesg输出明确是哪个符号未知。1. 如果符号是内核内部的确认其是否被EXPORT_SYMBOL导出。2. 如果符号来自其他模块先使用modprobe加载依赖模块。insmod不解决依赖modprobe会。sudo insmod失败提示Device or resource busy。要注册的设备号主设备号已被其他驱动占用。cat /proc/devices查看冲突的主设备号。在驱动初始化代码中改用alloc_chrdev_region动态分配设备号而不是写死的静态号。5.3 设备节点访问失败问题现象常见原因检查方式处理建议用户程序open(“/dev/mydev”)失败返回-1errno为ENOENT(No such file or directory)。/dev/mydev设备节点文件不存在。1.ls -l /dev/mydev。2.dmesg查看驱动初始化是否成功device_create是否报错。1. 确认驱动加载成功且device_create被调用。2. 检查DEVICE_NAME宏定义是否一致。3. 确认udev服务正常运行。open失败errno为EACCES(Permission denied)。当前用户对设备节点文件没有读写权限。ls -l /dev/mydev查看文件权限如crw-rw----。1. 使用sudo运行测试程序。2. 或修改设备节点权限sudo chmod 666 /dev/mydev不安全仅用于测试。3. 更好的方法是在驱动中或通过udev规则设置默认权限。5.4 驱动功能异常如读写错误问题现象常见原因检查方式处理建议write成功但read返回0或错误数据。1. 驱动内部的read函数逻辑错误如偏移量*offset处理不当。2. 内核缓冲区与用户缓冲区拷贝出错。1. 在驱动的read/write函数中增加printk打印传入的len,*offset和实际处理字节数。2. 检查copy_to_user/copy_from_user的返回值。1. 仔细调试偏移量逻辑确保read能正确返回之前write写入的数据。2. 确保传递给拷贝函数的用户空间指针buf有效。进行设备操作导致系统崩溃或内核报错Oops。1. 访问了非法内存地址如空指针解引用。2. 未在正确上下文中调用某些内核函数如在中断上下文调用可能睡眠的函数。3. 并发访问导致数据竞争未使用锁。分析dmesg输出的崩溃信息重点关注Call Trace调用栈它指向出错的代码行。1. 对所有指针进行有效性判断。2. 理解内核上下文进程上下文、中断上下文遵守API调用规则。3. 对共享数据如全局缓冲区device_buffer使用内核同步机制如互斥锁mutex。5.5 通用排查命令清单当驱动出现问题时按顺序使用以下命令收集信息dmesg | tail -50或journalctl -k --since “1 minute ago”: 查看最新的内核日志这是驱动printk输出和任何内核错误信息的第一现场。lsmod | grep 模块名: 确认模块是否已加载。cat /proc/devices: 确认设备号是否已注册。ls -l /dev/设备名: 确认设备节点是否存在及其权限。modinfo 模块名.ko: 查看模块信息确认版本。strace: 跟踪用户空间测试程序的系统调用看open,read,write,ioctl等调用是否返回错误及具体错误码。6. 从示例到生产驱动开发的最佳实践与扩展方向掌握了基础驱动框架后要将其用于真实项目还需要遵循一系列工程实践并了解更高级的主题。6.1 驱动开发最佳实践错误处理要彻底初始化函数 (xxx_init) 中每一步都可能失败必须为每一步都提供错误处理路径并在失败时回滚之前已成功的步骤如示例中所示。这是防止模块加载失败后留下“垃圾”资源的关键。善用printk分级日志printk有KERN_DEBUG,KERN_INFO,KERN_WARNING,KERN_ERR等级别。在驱动中合理使用发布时可关闭调试信息。不要用printk打印高频数据会影响性能。重视并发与同步我们的示例驱动没有处理并发访问。如果多个进程同时读写device_buffer会导致数据混乱。在实际驱动中必须使用mutex,spinlock,semaphore等内核同步原语保护共享资源。内存管理要谨慎内核内存有限且无虚拟内存交换。使用kmalloc/kfree分配内存时要注意大小限制和标志如GFP_KERNEL,GFP_ATOMIC。防止内存泄漏所有在init中分配的资源都必须在exit中释放。用户接口要健壮永远不要信任来自用户空间的输入。在read,write,ioctl中必须验证用户传入的参数如缓冲区长度、指针、命令码是否合法防止恶意或错误的用户程序导致内核崩溃或安全漏洞。考虑电源管理对于移动或嵌入式设备驱动可能需要实现suspend和resume回调以便在系统休眠时妥善保存设备状态唤醒时恢复。6.2 下一步学习与扩展方向这个简单的字符设备驱动只是一个起点。要开发真正的硬件驱动你需要深入学习以下方向硬件交互学习如何通过ioremap映射物理内存寄存器到内核虚拟地址并使用readl/writel等函数安全地读写寄存器。理解设备树Device Tree或ACPI以获取硬件资源内存地址、中断号信息而不是在代码中写死。中断处理这是驱动响应硬件事件的核心机制。学习如何申请中断号 (request_irq)、编写中断处理程序上半部要求快速、以及使用工作队列或任务队列处理下半部耗时操作。阻塞与非阻塞I/O、Poll机制当设备数据未就绪时驱动如何让用户进程睡眠阻塞或立即返回非阻塞。实现poll或select系统调用的支持。内核数据结构与API熟悉内核链表、哈希表、定时器、工作队列、等待队列、完成量等常用基础设施。具体设备类型驱动平台设备驱动针对集成在SoC内部的设备。PCI/PCIe驱动针对PCI总线设备。USB驱动针对USB设备需要了解USB协议栈。IIO子系统用于模拟数字转换器ADC等工业I/O设备。Input子系统用于键盘、鼠标、触摸屏等输入设备。FrameBuffer/DRM用于显示设备。网络设备驱动实现net_device结构体。调试技巧学习使用procfs,sysfs,debugfs为驱动暴露调试信息。掌握使用kgdb或JTAG进行内核调试的方法。驱动开发是一条深入Linux内核的路径需要耐心、严谨和对细节的关注。从这个小例子出发结合具体硬件的数据手册逐步实践上述高级主题你就能真正掌握为嵌入式Linux系统赋予硬件生命的能力。建议下一个实践目标是为一个简单的GPIO LED编写驱动通过设备树获取GPIO编号实现ioctl控制亮灭这将串联起硬件访问、设备树和用户接口等多个关键知识点。
返回列表