Skip to content

LV005-代码优化

驱动中一些地方还可以做一些优化来提高稳定性和效率,怎么优化?

一、用户空间内存块检测

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

1. access_ok()函数

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

c
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. 头文件包含

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

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

image-20250213102453259

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

c
#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

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

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

c
#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 中生成的规则如下:

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

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

c
#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 的宏,比如我们可以在内核中这样使用:

c
#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,具体呢,我也没找到,没有深究了,知道可以这样用就行了:

c
#include <linux/version.h>

二、分支预测

1. likely/unlikely

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

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

c
# 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 函数添加分支预测优化函数, 添加完成如下所示:

c
        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