在GNUC语言中,假如晓得a和b的类型,可以在宏上面定义一个变量,将a,b形参给变量,之后再比较。

linux内核调试方法总结_调用linux内核函数_内核系统调用

Linux内核采用的是GCC编译器,GCC编译器不仅支持ANSIC,还支持GNUC。在Linux内核中,许多地方都使用了GNUC语言的扩充特点,如typeof、__attribute__、__aligned、__builtin_等,那些都是GNUC语言的特点。

typeof

下边是比较两个数大小返回最大值的精典宏写法:

#define1.

假如a传入i++,b传入j++,这么这个比较大小都会出错。诸如:

#define
int x = 1, y = 2;
printf("max=%dn", max(x++, y++));
printf("x = %d, y = %dn", x, y);1.2.3.4.5.

输出结果:max=3,x=2,y=4。这是错误的结果,正常我们希望的是max(1,2),返回max=2。怎么更改这个宏呢?

在GNUC语言中,假如晓得a和b的类型调用linux内核函数,可以在宏上面定义一个变量,将a,b形参给变量,之后再比较。诸如:

#define max(a,b) ({   
    int _a = (a);    
    int _b = (b);   
    _a > _b ? _a : _b; }) 1.2.3.4.

倘若不晓得具体的数据类型,可以使用typeof类转换宏,Linux内核中的事例:

#define max(a, b) ({        
    typeof(a) _a = (a);      
    typeof(b) _b = (b);      
    (void) (&_a == &_b);   
    _a > _b ? _a : _b; })1.2.3.4.5.

typeof(a)_a=(a):定义一个a类型的变量_a,将a形参给_a

typeof(b)_b=(b):定义一个b类型的变量_b,将b形参给_b

(void)(&_a==&_b):判定两个数的类型是否相同,倘若不相同,会抛出一个警告。由于a和b的类型不一样,其表针类型也会不一样,两个不一样的表针类型进行比较操作,会抛出一个编译警告。

typeof用法举例:

//typeof的参数可以是表达式或类型
//参数是类型
typeof(int *) a,b;//等价于:int *a,*b;
//参数是表达式
int foo();

linux内核调试方法总结_调用linux内核函数_内核系统调用

typeof(foo()) var;//声明了int类型的var变量,因为表达式foo()是int类型的。由于表达式不会被执行,所以不会调用foo函数。1.2.3.4.5.6.7.8.

零长链表

零长链表,又叫柔性链表。而它的作用主要就是为了满足须要变厚度的结构体,因而有时也习惯性地称为变长链表。

用法:在一个结构体的最后,声明一个宽度为0的链表,就可以促使这个结构体是可变长的。

对于编译器来说,此时宽度为0的链表并不占用空间,由于链表名本身不占空间,它只是一个偏斜量,链表名这个符号本身代表了一个不可更改的地址常量

结构体中定义零长链表:


struct pcpu_chunk {
    struct list_head  list;
    unsigned long    populated[];  /* 变长数组 */
};1.2.3.4.5.

数据结构最后一个元素被定义为零厚度链表,不占结构体空间。这样,我们可以按照对象大小动态地分配结构的大小。

struct line {
    int length;
    char contents[0];
};
struct line *thisline = malloc(sizeof(struct line) + this_length);
thisline->length = this_length;1.2.3.4.5.6.7.

如上例所示,structline数据结构定义了一个intlength变量和一个变长链表contents[0],这个structline数据结构的大小只包含int类型的大小,不包含contents的大小,也就是**sizeof(structline)=sizeof(int)**。

创建结构体对象时,可依据实际的须要指定这个可变长链表的厚度,并分配相应的空间,如上述实例代码分配了this_length字节的显存,但是可以通过contents[index]来访问第index个地址的数据。

case范围

GNUC语言支持指定一个case的范围作为一个标签,如:

case low ...high:
case 'A' ...'Z':1.2.

这儿low到high表示一个区间范围,在ASCII字符代码中也特别有用。下边是Linux内核中的代码反例。


    
static int local_atoi(const char *name){
    int val = 0;
    for (;; name++) {
        switch (*name) {
            case '0' ...'9':
                val = 10*val+(*name-'0');
                break;
            default:
                return val;
        }
    }
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.

内核系统调用_调用linux内核函数_linux内核调试方法总结

另外,还可以用整形数来表示范围,并且这儿须要注意在“…”两边有空格,否则编译会出错。


static int at91sam9261_udc_init(struct at91_udc *udc){
    for (i = 0; i ep[i];
        switch (i) {
            case 0:
                ep->maxpacket = 8;
                break;
            case 1 ... 3:
                ep->maxpacket = 64;
                break;
            case 4 ... 5:
                ep->maxpacket = 256;
                break;
        }
    }
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.

标号元素

GNUC语言可以通过指定索引或结构体成员名来初始化,何必根据原先的固定次序进行初始化。

结构体成员的初始化在Linux内核中常常使用,如在设备驱动中初始化file_operations数据结构:


static const struct file_operations zero_fops = {
    .llseek      = zero_lseek,
    .read        = new_sync_read,
    .write       = write_zero,
    .read_iter     = read_iter_zero,
    .aio_write     = aio_write_zero,
    .mmap        = mmap_zero,
};1.2.3.4.5.6.7.8.9.

调用linux内核函数_内核系统调用_linux内核调试方法总结

如上述代码中的zero_fops的成员llseek初始化为zero_lseek函数,read成员初始化为new_sync_read函数,依次类推。当file_operations数据结构的定义发生变化时,这些初始化方式仍然能保证已知元素的正确性,对于未初始化成员的值为0或则NULL。

可变参数宏

在GNUC语言中,宏可以接受可变数量的参数,主要用在输出函数里。诸如:


#define pr_debug(fmt, ...) 
dynamic_pr_debug(fmt, ##__VA_ARGS__)1.2.3.

“…”代表一个可以变化的参数表,“__VA_ARGS__”是编译器保留数组,预处理时把参数传递给宏。当宏的调用展开时,实际参数就传递给dynamic_pr_debug函数了。

函数属性

GNUC语言容许申明函数属性(FunctionAttribute)、变量属性(VariableAttribute)和类型属性(TypeAttribute),便于编译器进行特定方面的优化和更仔细的代码检测。特殊属性句型格式为:

__attribute__ ((attribute-list))1.

attribute-list的定义有好多,如noreturn​、format​以及const​等。据悉,还可以定义一些和处理器体系结构相关的函数属性,如ARM体系结构中可以定义interrupt​、isr等属性。

下边是Linux内核中使用format属性的一个反例。


int libcfs_debug_msg(struct libcfs_debug_msg_data *msgdata,const char *format1, ...)__attribute__ ((format (printf, 2, 3)));1.2.

libcfs_debug_msg()函数里申明了一个format函数属性,它会告诉编译器根据printf的参数表的格式规则对该函数参数进行检测。数字2表示第二个参数为低格字符串,数字3表示参数“…”里的第一个参数在函数参数总量中排在第几个。

noreturn属性告诉编译器,该函数从不返回值,这可以去除一些何必要的警告信息。比如以下函数,函数不会返回:

void __attribute__((noreturn)) die(void);1.

const属性会让编译器只调用该函数一次,之后再调用时只须要返回第一次结果即可,进而提升效率。

static inline u32 __attribute_const__ read_cpuid_cachetype(void){
    return read_cpuid(CTR_EL0);
}1.2.3.

Linux还有一些其他的函数属性,被定义在compiler-gcc.h文件中。

#define
#define
#define
#define
#define
#define
#define
#define1.2.3.4.5.6.7.8.

变量属性和类型属性

调用linux内核函数_内核系统调用_linux内核调试方法总结

变量属性可以对变量或结构体成员进行属性设置。类型属性常见的属性有alignment​、packed​和sections等。

alignment属性规定变量或则结构体成员的最小对齐格式,以字节为单位。

struct qib_user_info {
    __u32 spu_userversion;
    __u64 spu_base_info;
} __aligned(8);1.2.3.4.

在这个事例中,编译器以8字节对齐的方法来分配qib_user_info这个数据结构。

packed属性可以使变量或则结构体成员使用最小的对齐方法,对变量是以字节对齐,对域是以位对齐。

struct test{
 char a;
    int x[2] __attribute__ ((packed));
};1.2.3.4.

x成员使用了packed属性,它会储存在变量a旁边,所以这个结构体一共占用9字节。

内建函数

内建函数以“_builtin_”作为函数名前缀。下边介绍Linux内核常用的一些内建函数。

__builtin_constant_p(x):判定x是否在编译时就可以被确定为常量。若果x为常量,该函数返回1,否则返回0。

__builtin_expect(exp,c):

#define __swab16(x)        
(__builtin_constant_p((__u16)(x)) ?  
___constant_swab16(x) :      
__fswab16(x))__builtin_expect(exp, c)1.2.3.4.

__builtin_expect(exp,c):这儿的意思是exp==c的几率很大,拿来引导GCC编译器进行条件分支预测。开发人员晓得最可能执行那个分支,并将最有可能执行的分支告诉编译器,让编译器优化指令序列,使指令尽可能地次序执行,进而提升CPU预取指令的正确率。

Linux内核中常常看到likely()​和unlikely()​函数,本质也是__builtin_expect():

#define LIKELY(x) __builtin_expect(!!(x), 1) //x很可能为真
#define UNLIKELY(x) __builtin_expect(!!(x), 0) //x很可能为假1.2.

__builtin_prefetch(constvoid*addr,intrw,intlocality):主动进行数据预取,在使用地址addr的值之前就把其值加载到cache中,降低读取的延后,进而提升性能。

该函数可以接受3个参数:


#define
#define1.2.3.

下边是使用prefetch()函数进行优化的一个事例。


linux内核调试方法总结_调用linux内核函数_内核系统调用

void __init __free_pages_bootmem(struct page *page, unsigned int order){ unsigned int nr_pages = 1 << order; struct page *p = page; unsigned int loop; prefetchw(p); for (loop = 0; loop < (nr_pages - 1); loop++, p++) { prefetchw(p + 1); __ClearPageReserved(p); set_page_count(p, 0); } … }1.2.3.4.5.6.7.8.9.10.11.12.13.

在处理structpage数据之前linux操作系统版本,通过prefetchw()预取到cache中,进而提高性能。

asmlinkage

在标准C语言中,函数的实参在实际传入参数时会涉及参数储存问题。

对于x86构架,函数参数和局部变量被一起分配到函数的局部堆栈里。x86中对asmlinkage的定义:


#define1.2.

attribute((regparm(0))):告诉编译器该函数不须要通过任何寄存器来传递参数,只通过堆栈来传递。

对于ARM​来说,函数参数的传递有一套ATPCS标准adobe air linux,即通过寄存器来传递。ARM中的R0~R4寄存器储存传入参数,当参数超过5个时,多余的参数被储存在局部堆栈中。所以,ARM平台没有定义asmlinkage。


#define
#define1.2.3.

UL

在Linux内核代码中,我们常常会听到一些数字的定义使用了UL后缀修饰。

数字常量会被隐型定义为int类型调用linux内核函数,两个int类型相乘的结果可能会发生溢出。

因而使用UL强制把int​类型数据转换为unsignedlong​类型,这是为了保证运算过程不会由于int的位数不同而造成溢出。

1:表示有符号整型数字1

UL:表示无符号长整型数字1

Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复