LV06-09-调试与优化-01-代码优化

驱动中一些地方还可以做一些优化来提高稳定性和效率,怎么优化?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、用户空间内存块检测

但是其实这个不检查也问题不大,因为我们一般在用户空间肯定会做检查,检查完毕才会继续往下执行。

1. access_ok()函数

这个函数的作用是检查用户空间内存块是否可用,它可能的定义如下:

1
2
3
access_ok(type, addr, size);
// 或者
access_ok(addr, size);
  • 参数说明:

  • type :Type of access,VERIFY_READ or VERIFY_WRITE。请注意,VERIFY_WRITE是VERIFY_READ的超集——如果写入一个块是安全的,那么从它读取总是安全的。另外,这个参数并不是一定有,这个后面的版本问题会提到。

  • addr:要检查的块的开始的用户空间指针

  • size:要检查的块的大小

返回值:此函数检查用户空间中的内存块是否可用。如果可用,则返回真(非0值),否则返回假 (0) 。

2. 头文件包含

1
#define access_ok(type, addr, size)	(__range_ok(addr, size) == 0)
1
#define access_ok(type, addr, size) __access_ok((unsigned long)(addr),(size))

虽然实现有些区别,但是功能都是一样的,那么我们写代码的时候要怎么包含头文件?其实写代码的时候要包含的是这个:uaccess.h - include/linux/uaccess.h,在这个头文件中,并没有实现这个宏,但是它包含了对应的头文件:

image-20250213102453259

所以在驱动中使用的时候包含下边这个头文件即可:

1
#include <linux/uaccess.h> /* access_ok */

3. 版本问题

3.1 access_ok()在不同内核版本中的定义

其实这里有个坑,那就是函数的参数问题,在一些内核版本中是没有type参数的,例如:uaccess.h - arch/arm/include/asm/uaccess.h - Linux source code v5.0-rc1

1
#define access_ok(addr, size)	(__range_ok(addr, size) == 0)

uaccess.h - arch/arm/include/asm/uaccess.h - Linux source code v4.20.17中是这样的:

1
#define access_ok(type, addr, size)	(__range_ok(addr, size) == 0)

我看了一下,kernel官网的版本,到写这个笔记为止,v4.x版本中最新的是v4.20.17,还是三个参数,到了v5.x版本,最早的一个是v5.0-rc1,在5.0的版本中已经都是2个参数了。

3.2 怎么兼容不同的内核源码?

那我们写的驱动最好是要兼容不同的内核源码,那这里怎么兼容?要是可以获取到内核源码的版本就好了,内核中已经为我们提供了相应的版本号,是定义在linux源码 include/generated/uapi/linux/version.h 文件中的。但是下载linux kernel 源码中是么有这个文件???它其实是在编译过程中生成,在 Makefile 中生成的规则如下:

1
2
3
4
5
define filechk_version.h
(echo \#define LINUX_VERSION_CODE $(shell \
expr $(VERSION) \* 65536 + 0$(PATCHLEVEL) \* 256 + 0$(SUBLEVEL)); \
echo '#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))';)
endef

可以看到,这里其实为我们提供了两个宏定义:

1
2
#define LINUX_VERSION_CODE 330384
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c)))

LINUX_VERSION_CODE是根据Makefile中的版本号计算出来的当前linux内核源码的版本,KERNEL_VERSION是给我们用来计算某个版本的LINUX_VERSION_CODE的宏,比如我们可以在内核中这样使用:

1
2
3
4
5
6
7
8
9
#include <linux/version.h>

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,15,0)
printk("Running on kernel 5.15 or newer\n");
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5,10,0)
printk("Running on kernel 5.10-5.14\n");
#else
printk("Legacy kernel (<5.10)\n");
#endif

使用的时候需要包含一个头文件:linux/version.h,具体呢,我也没找到,没有深究了,知道可以这样用就行了:

1
#include <linux/version.h>

二、分支预测

1. likely/unlikely

现在的 CPU 都有 ICache 和流水线机制。 即运行当前指令时, ICache 会预读取后面的指令,从而提升效率。 但是如果条件分支的结果是跳转到了其他指令, 那预取下一条指令就浪费时间了。 这里用到的 likelyunlikely 宏, 会让编译器总是将大概率执行的代码放在靠前的位置, 从而提高驱动的效率。

这两个宏定义在 compiler.h - include/linux/compiler.h 中:

1
2
3
4
5
6
# ifndef likely
# define likely(x) (__branch_check__(x, 1, __builtin_constant_p(x)))
# endif
# ifndef unlikely
# define unlikely(x) (__branch_check__(x, 0, __builtin_constant_p(x)))
# endif

__builtin_expect()的作用是告知编译器预期表达式 exp 等于 c 的可能性更大, 编译器可以根据该因素更好的对代码进行优化, 所以 likelyunlikely 的作用就是表达性 x 为真的可能性更大( likely) 和更小(unlikely) 。

2. 使用示例

这里以添加传递地址检测内容后的代码为例, 对 copy_from_user 函数添加分支预测优化函数, 添加完成如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
case CMD_TEST3:
{
struct __CMD_TEST cmd_test3 = {0};
// 使用 access_ok (type, addr, size); 来检查用户空间内存块是否可用
// 在不同的内核版本中,函数也可能没有type参数:access_ok (addr, size);
#if (LINUX_VERSION_CODE <= KERNEL_VERSION(4, 20, 17))
if (!access_ok(VERIFY_WRITE, arg, sizeof(struct __CMD_TEST)))
{
return -1;
}
#else
if (!access_ok(arg, sizeof(struct __CMD_TEST)))
{
return -1;
}
#endif
// likely 和 unlikely 宏, 会让编译器总是将大概率执行的代码放在靠前的位置, 从而提高驱动的效率。
// likely(x) 与 unlikely(x) 的作用就是表达式 x 为真的可能性更大(likely) 和更小(unlikely) 。
// 在传递地址正确的前提下copy_from_user 函数运行失败为小概率事件, 所以这里使用 unlikely 函数进行驱动效率的优化。
if (unlikely(copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0))
{
PRT("copy_from_user error\n");
}
PRT("cmd_test3 data: .a=%d .b=%d .c=%d\n", cmd_test3.a, cmd_test3.b, cmd_test3.c);
break;
}

传递地址检测成功之后才会使用执行 copy_from_user 函数, 在传递地址正确的前提下copy_from_user 函数运行失败为小概率事件, 所以这里使用 unlikely 函数进行驱动效率的优化。

三、代码优化demo

demo可以看这里:10_driver_debug/01_optimizedcode · 苏木/imx6ull-driver-demo - 码云 - 开源中国