Linux 驱动的内核适配 - 方法

原生与野生

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-devel-
内核头文件安装位置 /usr/src/linux-headers- /usr/src/kernels/

Ubuntu 和 RedHat/CentOS 的内核头文件安装路径有一些小的差异,不过相同点是都可以通过 "/lib/modules/build/" 的软链接指向,所以这个 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 的 ".."(分别代表 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 的:

Linux 驱动的内核适配 - 方法_第1张图片

直接进行内核版本的比对不能很好地应对这种 backport 的场景(往往需要使用较多的 if/else,造成逻辑复杂),一种更灵活的方式是「探测性编译」。

比如自内核 5.0 之后,"access_ok" 这个用于校验用户空间传入数值的宏,从三个参数变成了两个参数,因此我们可以通过以下一段小程序来测试:

#include 

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 
	],[
		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-
(新)kernel-core/modules-
内核镜像安装位置 /boot/vmlinuz- /boot/vmlinuz-

安装新内核之后,就该切换内核版本了。这对于 RedHat/CentOS 来说比较好办,使用这篇文章里提到的 grubby 工具即可。而对于 Ubuntu 系统就稍微麻烦一些,需要三步:

  1. 通过 "grep menuentry /boot/grub/grub.cfg" 找到目标内核对应的项。
  2. 将第一步的结果填入 "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,后者是提供给用户态的程序编译用的:

Linux 驱动的内核适配 - 方法_第2张图片

原文链接:https://zhuanlan.zhihu.com/p/570218272

(免费订阅,永久学习)学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂

更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,永久学习,或点击这里加qun免费
领取,关注我持续更新哦! ! 

你可能感兴趣的:(linux,DPDK,c++,linux,dpdk,c++,虚拟机,开发语言)