Linux 驱动开发与内核通信机制——超详细教程
Linux 驱动开发与内核通信机制——超详细教程
在 Linux 驱动开发中,经常会遇到两个让初学者头疼的概念:内核空间与用户空间、驱动与应用的交互。如果你刚开始学习驱动编程,可能会被各种 copy_from_user
、ioremap
、mknod
弄晕。本教程将带你从零开始,一步步拆解这些知识点,结合实际例子,让你能自己写出一个简单的字符设备驱动。
内核空间与用户空间
为什么要区分?
Linux 把虚拟内存划分为两部分:
-
用户空间(User Space)
- 程序员写的应用程序运行的地方。
- 可以通过系统调用(
open
、read
、write
等)间接操作内核。 - 如果应用崩溃,只会影响自己,不会影响整个系统。
-
内核空间(Kernel Space)
- 存放 Linux 内核代码和驱动程序。
- 能直接操作硬件、管理内存、调度进程。
- 如果内核代码出错,可能导致系统崩溃(kernel panic)。
这种隔离的最大好处就是保护系统稳定性。比如你写了一个有 bug 的应用,最多应用自己崩溃,不会让整个系统挂掉。
内核与用户空间的通信方式
驱动开发的关键任务之一,就是让用户空间的应用程序能够与内核交互。常见的通信方式有以下几种:
1. copy_from_user
与 copy_to_user
这是一对最常见的 API:
copy_from_user()
:从用户空间拷贝数据到内核空间。copy_to_user()
:从内核空间拷贝数据到用户空间。
它们常用在驱动的 read()
和 write()
函数中。
代码示例:
static ssize_t my_write(struct file *file, const char __user *buf,size_t count, loff_t *ppos) {char kernel_buf[100];if (count > sizeof(kernel_buf))return -EINVAL;if (copy_from_user(kernel_buf, buf, count)) {return -EFAULT;}printk("内核收到数据: %s\n", kernel_buf);return count;
}
2. /proc
文件系统
- 驱动程序可以在
/proc
下注册一个文件。 - 用户只需
cat /proc/mydev
就能读取信息。
**优点:**实现简单,适合调试。
**缺点:**功能较弱,不适合复杂的数据交换。
3. sysfs
文件系统
/sys
目录下的文件是内核导出的信息。- 用户可以通过读写
/sys
文件和驱动交互。
**适用场景:**导出驱动参数、硬件属性(如设备 ID、工作模式)。
4. mmap
内存映射
有时用户程序需要频繁访问驱动的大块数据,比如图像缓冲区。为了效率,可以用 mmap
将内核内存直接映射到用户空间。
关键点:
- 用户调用
mmap()
。 - 驱动在
file_operations
里实现mmap()
回调。 - 内核和用户共享同一块物理内存,省去了数据拷贝。
5. 信号(Signal)
- 内核可以主动给进程发送信号(如
SIGKILL
)。 - 常用于通知用户程序发生了某个事件。
6. 自定义协议 / 套接字
- 适合复杂通信,比如网络驱动。
- 驱动与应用通过 socket 通信。
字符设备驱动基础
Linux 驱动分为三类:字符设备、块设备、网络设备。初学者最常接触的就是字符设备。
字符设备的特点
- 数据流是按字节处理的。
- 典型设备:键盘、串口。
字符设备的核心结构:file_operations
驱动中最重要的结构就是 struct file_operations
,它定义了驱动能提供的功能。
struct file_operations {ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);int (*open) (struct inode *, struct file *);int (*release)(struct inode *, struct file *);
};
解释:
open
:用户调用open("/dev/mydev")
时触发。read
:用户调用read()
时触发。write
:用户调用write()
时触发。release
:用户close()
设备时触发。
注册字符设备
驱动必须告诉内核:“我有一个设备了”。有两种注册方式:
方法一:现代方式(推荐)
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
cdev_add(&my_cdev, dev, 1);
方法二:早期方式
register_chrdev(major, "mydev", &fops);
主设备号与次设备号
Linux 中每个设备都有一个设备号,由 主设备号 和 次设备号组成:
- 主设备号(major):标识驱动。
- 次设备号(minor):区分同一个驱动下的不同设备。
创建设备节点:
mknod /dev/mydev c 250 0
c
→ 字符设备;250
→ 主设备号;0
→ 次设备号。
内存映射与硬件寄存器访问
驱动经常需要访问外设的寄存器,而寄存器地址是物理地址,必须映射到虚拟地址后才能访问。
API:ioremap
void __iomem *ioremap(unsigned long phys_addr, unsigned long size);
示例:
void __iomem *reg_base;
reg_base = ioremap(0x12340000, 0x100); // 映射外设寄存器
writel(0x1, reg_base + 0x04); // 写寄存器
常见调试方法
-
printk
- 类似于
printf
,输出到dmesg
。 - 常用于调试驱动。
- 类似于
-
strace
- 跟踪用户程序调用的系统调用。
-
sysfs/proc
- 在
/sys
或/proc
下导出信息。
- 在
-
gdb + qemu
- 在虚拟机里调试内核,适合进阶学习。
进程与线程相关知识
进程的五种状态
- 新建(new)
- 就绪(ready)
- 运行(running)
- 阻塞(blocked)
- 终止(terminated)
内核线程 vs 用户线程
- 用户线程:内核不可见,切换快,但一个线程阻塞会影响整个进程。
- 内核线程:由内核调度,阻塞不会影响其他线程,但切换开销大。
僵尸进程与守护进程
僵尸进程
- 子进程退出,但父进程未调用
wait()
。 - 占用 PID,过多会导致系统无法创建新进程。
解决:
- 在父进程中调用
wait()
。 - 或者杀死父进程。
守护进程
- 运行在后台,脱离终端,常用于服务(如
sshd
)。
总结
本教程从内核与用户空间的区别讲起,详细介绍了 Linux 驱动开发的几个关键点:
- 内核与用户空间通信的多种方式;
- 字符设备驱动的基本框架与
file_operations
; - 主设备号、次设备号的作用;
- 内存映射与寄存器访问;
- 常见调试手段;
- 进程、线程、僵尸进程等基础操作系统知识。
学完本文,你已经能理解一个简单的字符驱动是如何工作的了。