原生与野生
Linux 的驱动代码大致可分为两种:一种是已经进入 mainline 的,当内核 API 变化时,会被同步地修改;还有一种是 out-of-tree 的,需要用一套驱动代码去适配不同版本的内核。由于内核 API 持续变动的特性,进行内核适配就成了做驱动开发绕不过去的一个问题。
材料准备
「适配」简单说就是要编译出针对某个内核版本的 ".ko" 文件,这和普通的驱动编译没什么两样,原材料一个是内核头文件(在 make target 中由 "C" 参数指定路径),一个是驱动源码(由 "M" 参数指定路径)。
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
「内核头文件」是为编译 kernel modules 提供的一组头文件,在 RedHat/CentOS 系统中被命名为 "kernel-devel"【注-1】,因此亦可被称作「内核开发包」。
内核头文件怎么获取呢?
(1) 对于 Ubuntu 系统,以 18.04 (bionic) 版本为例,使用 "apt list linux-headers-unsigned-*-generic" 命令可列出该版本支持的标准内核(4.15, 4.18, 5.0, 5.3, 5.4)的头文件,对于这些标准内核,直接 "apt install" 安装即可。对于其他非标准内核(比如 5.2),可到这个网站下载对应的内核头文件。
(2) 对于 RedHat/CentOS 系统,标准内核(比如 4.18.0-240.el8)的头文件可从镜像网站下载。
Ubuntu | RedHat/CentOS | |
---|---|---|
内核头文件安装包名称 | linux-headers-<kernel-version> | kernel-devel-<kernel-version> |
内核头文件安装位置 | /usr/src/linux-headers-<kernel-version> | /usr/src/kernels/<kernel-version> |
Ubuntu 和 RedHat/CentOS 的内核头文件安装路径有一些小的差异,不过相同点是都可以通过 "/lib/modules/build/<kernel-version>" 的软链接指向,所以这个 symlink 成了寻找内核头文件位置的公用路径。
(3) 对于自行编译的内核,需在内核源码目录使用 "make modules_prepare",以生成编译外部 modules 所需的各种文件。
静态与动态
原材料备齐,接下来就可以按照菜谱下锅了。最直观也最简单的适配方法是通过(静态的)版本号判断,在内核头文件 "include/generated/uapi/linux/version.h" 中,有一个记录 Linux 版本号的数字(以 4.18.0 内核为例):
#define LINUX_VERSION_CODE 266752
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
因为直接使用 KERNEL_VERSION 的 "<a>.<b>.<c>"(分别代表 major, minor, macro 号)来比较大小不方便,所以转换成了一个数字 LINUX_VERSION_CODE (大家可以算一下,"4<<16 + 18<<8 + 0" 是否等于 266752)。
据此,可通过以下的判断来区别使用哪个版本的内核 API:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 18, 0)
/* use API after kernel 4.18 */
#else
/* use API before kernel 4.18 */
#endif
这种方法简捷快速,但在现实的 Linux 江湖里,大部分被使用的都是 RedHat, SUSE, Ubuntu 等厂商提供的 distribution,而这些发行版大都进行了 backporting。比如 RedHat/CentOS-8.2 用的这个 4.18.0-193.28.1 内核,它的 DRM 版本是来源于内核 5.3 的:
直接进行内核版本的比对不能很好地应对这种 backport 的场景(往往需要使用较多的 if/else,造成逻辑复杂),一种更灵活的方式是「探测性编译」。
比如自内核 5.0 之后,"access_ok" 这个用于校验用户空间传入数值的宏,从三个参数变成了两个参数,因此我们可以通过以下一段小程序来测试:
#include <linux/uaccess.h>
int main (void)
{
access_ok(0, 0);
return 0;
}
如果编译通过,说明该内核中的 "access_ok" 是两个参数,否则就是三个参数。
那如何捕获这个编译结果呢?在 Autoconf 工具里,有一个 M4 的组件,按照其定义的语法,写出类似这样的一个 "access-ok.m4" 文件:
AC_DEFUN([AC_ACCESS_OK_WITH_TWO_ARGUMENTS], [
AC_KERNEL_TRY_COMPILE([
#include <linux/uaccess.h>
],[
access_ok(0, 0);
],[
AC_DEFINE(HAVE_ACCESS_OK_WITH_TWO_ARGUMENTS, 1,
[whether access_ok(x, x) is available])
])
])
将其加入 autoconf 的配置体系里,就会自动生成一个包含探测性源码的 configure 文件,并最终得到 HAVE_ACCESS_OK_WITH_TWO_ARGUMENTS 为 1 (两个参数)或者为 0(三个参数) 的结果。
所以实际上你只要写一个 m4 文件就可以了,不用自己去写 C 代码然后编译,这些 Autoconf 都可以帮你搞定。
相较于第一种静态版本号判断的方法,这第二种动态探测的方法也并非百利而无一害。对需要判别的 API 生成配置文件再去编译,是有一定时间开销的,当要求编译的驱动版本很多时(比如针对上百个内核),完成一次的总时间就会较长。
所以需要适配的内核版本较少时,应尽量使用第二种方法,否则往往需要做出一定的妥协,混合使用以上两种方法,以兼顾高效和灵活。比如对一个 API 变动先用版本适配法,之后确实因新内核有 backport 造成难以判断(一般只有 20%~30% 的概率),再转为动态探测法。
加载验证
驱动编译成功后,需到对应的内核版本上去验证功能,这又涉及到安装「内核镜像」的过程。具体的方法同前面介绍的内核头文件的安装类似,在此不赘述。
Ubuntu | RedHat/CentOS | |
---|---|---|
内核镜像安装包名称 | linux-image-<kernel-version> | (旧)kernel-<kernel-version> (新)kernel-core/modules-<kernel-version> |
内核镜像安装位置 | /boot/vmlinuz-<kernel-version> | /boot/vmlinuz-<kernel-version> |
安装新内核之后,就该切换内核版本了。这对于 RedHat/CentOS 来说比较好办,使用这篇文章里提到的 grubby 工具即可。而对于 Ubuntu 系统就稍微麻烦一些,需要三步:
- 通过 "grep menuentry /boot/grub/grub.cfg" 找到目标内核对应的项。
- 将第一步的结果填入 "etc/default/grub" 文件,例如:
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.3.0-19-generic"
3. 执行 "update-grub" 命令。
注-1:
对于 RedHat/CentOS,需区别 kernel-devel 和 kernel-headers,后者是提供给用户态的程序编译用的:
原文链接:https://zhuanlan.zhihu.com/p/570218272
(免费订阅,永久学习)学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂
更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,永久学习,或点击这里加qun免费
领取,关注我持续更新哦! !