在看linux源码的时候,经常能看到一类type,例如__user,__kernel,__safe,__force等,在compiler_type.h的头文件中,我找到了它们的定义。看到两个很奇怪的现象,一个是只有在__CHECKER__宏打开的情况下,他们的定义才会被实现,否则他们的定义是空的。第二个是它们的attribute的定义,并不是gcc支持的属性。那到底是哪里使用到了呢?原来linux的作者们自己开发了一套编译期代码检查的工具Sparse,可以用于在编译阶段快速发现代码中隐含的问题。lwn上有一篇关于这个工具的讨论[1]。
如compiler_type.h文件中所示,下面通过注释说明sparse在linux内核中的一些简单用法:
#ifdef __CHECKER__// noderef 代表该指针不能被解引用,也就是不能*ptr这样访问。// address_space定义了指针能指向的内存的类型,0代表kernel space,1代表user space,// 2代表设备地址空间,3代表cpu局部的内存空间# define __user__attribute__((noderef, address_space(1)))# define __kernel__attribute__((address_space(0)))// safe表示变量可以为空# define __safe__attribute__((safe))// force表示变量可以强制类型转换# define __force__attribute__((force))// nocast表示参数类型必需和实际类型一致# define __nocast__attribute__((nocast))// io设备的地址空间# define __iomem__attribute__((noderef, address_space(2)))// __attribute__((context(x, in, out))表示要保证x在调用前值是in,调用后值是out// 举个例子,在调用某个函数时,需要检查是否lock一直被持有,就可以使用__must_hold(lock),// 保证调用时锁是被持有的。// 如果要保证函数在调用前lock是未被持有的,调用结束后,lock必需被持有,// 就可以使用__acquire(lock)// 举个内核里的例子:// static struct binder_thread *binder_get_txn_from_and_acq_inner(//struct binder_transaction *t)// __acquires(&t->from->proc->inner_lock)// {// ... // 在函数的实现中,最后一定会获取到&t->from->proc->inner_lock// }# define __must_hold(x)__attribute__((context(x,1,1)))# define __acquires(x)__attribute__((context(x,0,1)))# define __releases(x)__attribute__((context(x,1,0)))// __context__(x,v) 表示对x加上v,在上下文退出时,sparse会检查x值是否为0,不为0会报错# define __acquire(x)__context__(x,1)# define __release(x)__context__(x,-1)// 如果c为true,对x加1# define __cond_lock(x,c)((c) ? ({ __acquire(x); 1; }) : 0)# define __percpu__attribute__((noderef, address_space(3)))
除了在linux内核中使用外,其实我们如果有需求,也是可以单独使用的,安装编译和用法在这个链接里[2] [3]
在大型项目中为了保证代码质量,会加入很多防御性编程代码,这样能够即时的把问题暴露出来。例如:
// 函数本身是非线程安全的,需要在外边上锁保护void dothing_unsafe(){ ASSERT(lock == 1); // 确保函数调用时,锁是成功加上的。 ... // 执行函数逻辑 ASSERT(lock == 0); // 最后返回时,需要确保lock已被释放,否则说明代码逻辑存在bug}
例子中dothing_unsafe中典型的防御性编程的代码就是通过assert调用,保证函数在调用时lock是持有状态的。但是assert本身调用会带来除函数逻辑以外的额外开销,因此会对性能造成影响。影响包括几个方面,一个是assert中的if判断有可能会对流水线并行造成不好的影响,这个影响可以通过gcc的__builtin_expect内置函数,提前告知编译器代码生成来规避。另外一个不好的影响是会增加可执行代码的大小,影响指令cache的局部性。
但其实更好的做法是,通过编译期的静态检查,将问题提前暴露出来,而不是留到运行期再发现问题。例如还是上述的例子,通过sparse的静态检查,可以这样写:
// 函数本身是非线程安全的,需要在外边上锁保护void dothing_unsafe() __attribute__((context(lock,1,0))){ ... // 执行函数逻辑}
这样就可以在编译期发现问题。
[1] https://lwn.net/Articles/87538/
[2] https://www.kernel.org/doc/html/v5.3/dev-tools/sparse.html
[3] https://www.man7.org/linux/man-pages/man1/sparse.1.html