最近在移植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

printk manual

查看指定的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特性。

支持的特性:

  1. 变量声明在语句中间,从内核 4.x 起就已允许,但是在有些版本的编译器会有编译警告。
  2. // 单行注释,GCC 支持。C89标准中仅支持 /**/ 格式的注释
  3. struct 初始化时的点号语法,即: .field = value
  4. inline 关键字,但建议用 static inline
  5. for 循环中定义变量,如: for (int i = 0; i < 10; i++)

不支持或禁用的C99特性:

  1. float double 浮点运算
  2. 不建议使用 stdint.h 头文件,使用内核定义的 linux/types.h 头文件
  3. 用户空间的标准库函数

对于内核模块编码风格,有专门的指导文档 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),安全漏洞(信息泄露或攻击入口)。

(全文完)