LV005-代码优化
驱动中一些地方还可以做一些优化来提高稳定性和效率,怎么优化?
一、用户空间内存块检测
但是其实这个不检查也问题不大,因为我们一般在用户空间肯定会做检查,检查完毕才会继续往下执行。
1. access_ok()函数
这个函数的作用是检查用户空间内存块是否可用,它 可能 的定义如下:
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. 头文件包含
- uaccess.h - arch/arm/include/asm/uaccess.h:体系结构相关的头文件位于内核源码的 arch/< architecture >/include/asm/ 目录。
#define access_ok(type, addr, size) (__range_ok(addr, size) == 0)- uaccess.h - include/asm-generic/uaccess.h:通用体系结构头文件位于 include/asm-generic/(不依赖特定 CPU)。
#define access_ok(type, addr, size) __access_ok((unsigned long)(addr),(size))虽然实现有些区别,但是功能都是一样的,那么我们写代码的时候要怎么包含头文件?其实写代码的时候要包含的是这个:uaccess.h - include/linux/uaccess.h,在这个头文件中,并没有实现这个宏,但是它包含了对应的头文件:

所以在驱动中使用的时候包含下边这个头文件即可:
#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:
#define access_ok(addr, size) (__range_ok(addr, size) == 0)在 uaccess.h - arch/arm/include/asm/uaccess.h - Linux source code v4.20.17 中是这样的:
#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 中生成的规则如下:
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可以看到,这里其实为我们提供了两个宏定义:
#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 的宏,比如我们可以在内核中这样使用:
#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,具体呢,我也没找到,没有深究了,知道可以这样用就行了:
#include <linux/version.h>二、分支预测
1. likely/unlikely
现在的 CPU 都有 ICache 和流水线机制。 即运行当前指令时, ICache 会预读取后面的指令,从而提升效率。 但是如果条件分支的结果是跳转到了其他指令, 那预取下一条指令就浪费时间了。 这里用到的 likely 和 unlikely 宏, 会让编译器总是将大概率执行的代码放在靠前的位置, 从而提高驱动的效率。
这两个宏定义在 compiler.h - include/linux/compiler.h 中:
# 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 的可能性更大, 编译器可以根据该因素更好的对代码进行优化, 所以 likely 与 unlikely 的作用就是表达性 x 为真的可能性更大( likely) 和更小(unlikely) 。
2. 使用示例
这里以添加传递地址检测内容后的代码为例, 对 copy_from_user 函数添加分支预测优化函数, 添加完成如下所示:
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