Linux 驱动开发笔记
最近在移植PCIe卡驱动到 Linux 系统上,下面是移植过程中查阅资料的记录。
驱动程序的形式
编译好之后的的文件名为 *.ko
。
驱动程序的版本,在代码中增加版本号和驱动描述信息示例
MODULE_VERSION("1.0.0");
MODULE_DESCRIPTION("pcie device driver");
使用 modinfo
指令能够查看驱动的信息,其中会包含驱动的描述信息、版本信息。
驱动程序的存放目录为 /lib/modules/$(uname -r)/kernel/
使用下面的命令列出驱动文件的路径
find /lib/modules/$(uname -r) -name "*.ko*"
安装卸载驱动程序
安装驱动程序使用 insmod
命令;查看已经安装的驱动使用 lsmod
命令;卸载驱动程序使用 rmmod
命令。
调试驱动程序
在驱动中输出调试信息使用 printk
接口。查看驱动的调试信息使用 dmesg
命令。
printk
支持信息等级,使用下面的指令可以把所有的调试信息都输出出来。
echo 8 > /proc/sys/kernel/printk
对于 KERN_DEBUG
等级的调试信息(等同 pr_debug
接口),需要在编译时增加 DEBUG
宏,打开动态调试 CONFIG_DYNAMIC_DEBUG
开关,才能在 dmesg
中看到调试信息。
使用下面的命令开启整个驱动(module)的 debug 等级调试信息。
echo 'module usbcore +p' > /sys/kernel/debug/dynamic_debug/control
查看指定的PCI设备
使用 lspci 能够查看系统中所有的PCI设备,在开发驱动时会指定查看某个硬件id设备的详细信息,可以使用如下的命令。
lspci -d<vendor>:<device> -vvv
内核态与用户态的交互有哪些解决方案
- a. 系统调用(同步):
设备文件使用read()
/poll()
/select()
设备专用的指令ioctl
用户态查询变化sysfs
/procfs
- b. 信号signal(异步):
内核态可以向指定的用户态进程发送信号
优点:触发迅速
缺点:能够携带的信息有限,用户态在处理信号时稍复杂(考虑与既有线程的的并发问题)
- c. netlink socket:
优点:支持双向信息传递
缺点:使用复杂
- d. eventfd:
优点:轻量级,支持与poll
/select
一起使用
缺点:信息结果只能为计数器数值(每次写入是加法操作,每次读取是获取当前值并清零)
- e. sysfs 通知:
优点:基于文件系统,使用标准接口
缺点:需要轮询
- f. uevent(设备变化相关):
优点:标准的设备通知机制
缺点:主要用于设备变化
然而最终我并没有采用上面的任何一种方案,而是把 ioctl
交互过程改为同步交互模式,这样就不存在异步通信的需求了。
我尝试过的 eventfd 方案记录:
用户态程序调用 ioctl
将创建好的 eventfd 文件描述符传递到内核态的驱动程序中。驱动程序调用 eventfd_ctx_fdget
函数将用户态的文件描述符转换为内核态的 eventfd_ctx*
指针,调用 eventfd_signal
触发eventfd通知(这样用户态程序就能通过eventfd文件),在使用结束后调用 eventfd_ctx_fdput
函数释放占用的资源。
分两步处理的中断响应
与Windows系统的驱动程序处理中断的思路类似,先在中断处理函数中记录收到的中断信息(避免长时间阻塞中断),再在后续的流程中处理后续的中断信息。
在Linux系统中,创建了一个 kthread
来处理收到的中断信息,类似于用户态中的线程。通过 struct completion
实现中断处理函数唤醒中断处理线程。
用到的内核态函数接口如下:
// 创建线程
struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data, const char namefmt[], ...);
// 停止线程
int kthread_stop(struct task_struct *k);
// 在线程循环中检查是否需要停止
bool kthread_should_stop();
// 创建条件变量
void init_completion(struct completion *x);
// 重置条件变量
void reinit_completion(struct completion *x);
// 唤醒条件变量
void complete(struct completion *x);
// 等待条件变量
unsigned long wait_for_completion_timeout(
struct completion *x,
unsigned long timeout
);
// jiffies时间转毫秒(等待条件变量的时候需要提供jiffy时间)
unsigned long msecs_to_jiffies(const unsigned int msecs);
内核模块中的C语言标准
在编写 Linux 内核模块时,并不能完全使用C99标准,因为 Linux 内核并不完全遵循C99。
Linux 内核使用的是 GNU C(GCC) 的一个子集,支持部分C99特性。
支持的特性:
- 变量声明在语句中间,从内核 4.x 起就已允许,但是在有些版本的编译器会有编译警告。
-
//
单行注释,GCC 支持。C89标准中仅支持/**/
格式的注释
-
struct
初始化时的点号语法,即:.field = value
-
inline
关键字,但建议用static inline
-
for
循环中定义变量,如:for (int i = 0; i < 10; i++)
不支持或禁用的C99特性:
-
float
double
浮点运算
- 不建议使用
stdint.h
头文件,使用内核定义的linux/types.h
头文件
- 用户空间的标准库函数
对于内核模块编码风格,有专门的指导文档 Linux kernel coding style 。
Process Context 和 Interrupt Context
在内核开发中有两个重要的概念:程序上下文、中断上下文。
程序上下文(process context)针对的是用户空间程序发起的执行流程。例如:系统调用,内核线程。支持休眠、阻塞、调用会休眠的函数。
能够访问用户空间的内存(通过 copy_from_user()
函数)。
中断上下文(interrupt context)针对的是硬件中断发起的执行流程。例如:GPIO引脚的电压变化,定时器超时。在中断上下文中不能调用会休眠(sleep)的函数;需要快速执行,以免阻塞其他硬件中断。
在我开发PCIe卡的驱动时,需要通过用户空间的 write()
函数向PCIe卡写入数据,在内核空间中会由驱动程序将该数据拷贝至内核空间。在驱动程序中不能保存传入的 const char __user *buf
地址,然后在内核的中断处理线程( kthread
)中使用。
因为它是用户进程的虚拟地址,在 write()
调用时,这个地址只对当前进程的上下文有效,内核线程不在用户金册灰姑娘上下文中运行,无法安全访问用户空间内存。
用户空间指针的声明周期非常短,用户在执行完 write()
返回后可能会修改这个缓冲区的数据、释放或重用这个内存。此时内核中保存的 __user
指针就变成了悬空指针,会导致内核崩溃,非法访问(page fault),安全漏洞(信息泄露或攻击入口)。
(全文完)