Linux驱动 GNU扩展C

1 结构体赋值

对成员赋值,例如结构体:

struct  st1 {
    int a;
    int b;
}

 一般可以用{}形式直接赋值,即:

struct st1 st1 = {1,2,3); 

但Linux的赋值风格是:

struct st1 st1 = {
    .a = 1,
    .b = 2,
}

注:此风格(即在成员变量之前加点“.”),优点在于可以不按成员变量的顺序进行赋值。如:

struct st1 st1 = {  
    .b = 2,  
    .a = 1, 
}; 

内联函数inline

在c文件中,为了解决一些频繁调用的小函数而大量消耗栈空间或者是叫栈内存的问题,特别的引入了inline修饰符,表示为内联函数。内联函数使用inline关键字定义,并且函数体和声明必须结合在一起,否则编译器将他作为普通函数对待。inline函数一般放在头文件中。

inline void function(int x); //仅仅是声明函数,没有任何效果 
inline void function(int x) //正确 
{
    return x;
}

3 typeof用法、offsetof、container_of分析

3.1 关键字typeof用于获取表达式的数据类型

@1 char *chptr;

typeof (*chptr) ch;    //等价于char ch  
typeof  (ch) *chptr1;  //等价于char *chptr1  
typeof (chptr1) array[5]; //char  *array[5],chptr1的数据类型为char * 

@2 typeof常用在linux内核中

#define  min(x,y)  ({                   \  
     typeof(x)  __min1 = (x);        \  
     typeof(y) __min2 = (y);             \  
     (void)  (& __min1 == & __min2);     \  
    __min1 < __min2  ? __min1 :min2})

通过typeof获得x和y的数据,然后定义两个临时变量,并把x和y的分别赋给两个临时变量最后进行比较。
另外,宏定义中(void)(& __min1 = &__min2)语句的作用是用来警告x和y不能属于不同的数据类型。

@3实例使用如下:

#include <stdio.h>  
#define  min(x,y)  ({         \  
     typeof(x)  __min1 = (x); \  
     typeof(y) __min2 = (y);  \  
     (void)  (& __min1 == & __min2); \  
     __min1 < __min2  ? __min1 :min2})  
int main()  
{  
    int a=4;  
    int b=6;  
     int min_data;  
     min_data=min(a,b);  
     printf(“the min=%d\n”,min_data);  
     return 0;  
}

执行结果:

the min=4

如改为:

#include <stdio.h>  
#define  min(x,y)  ({         \  
     typeof(x)  __min1 = (x); \  
     typeof(y) __min2 = (y);  \  
     (void)  (& __min1 == & __min2); \  
     __min1 < __min2  ? __min1 :min2})  
int main()  
{  
     int a=4;  
     float b=6;  
     int min_data;  
     min_data=min(a,b);  
     printf(“the min=%d\n”,min_data);  
     return 0;  
}

则会提示以下警告:

main.c: In function ‘main’:
main.c:17:9: warning: comparison of distinct pointer types lacks acast [enabled by default]

3.2 offsetof分析

offsetof含义:获取结构体中某个成员变量相对于结构体首地址的内存位置偏移,定义如下: 

#define offsetof(TYPE,MEMBER) ((size_t)& ((TYPE *)0)->MEMBER)

注意:这里的0地址中的内容是不能访问的,但0地址的地址我们还是可以访问的, 这里用到一个取址运算符 (TYPE *)0,它表示将 0 地址强制转换为TYPE类型,((TYPE *)0)-> MEMBER 也就是从0址址找到TYPE 的成员MEMBER 。

这里结合file_node结构体来解读下offsetof的原理,定义如下:

struct file_node{
  char c;
  struct list_head node;
};

将实参代入得到:

offset( struct file_node, node );

最终将变成这样:

((size_t)&((struct file_node*)0)->node);

即求p的成员node的地址,只不过p为0地址,从0地址开始算成员node的地址,也就是成员node在结构体struct file_node 中的偏移量。即offset宏就是算MEMBER在TYPE中的偏移量的。

3.3 container_of分析

container_of含义:根据结构体中某个成员变量的内存地址获取结构体的内存首地址。定义如下: 

#define container_of(ptr, type, member) ({             \
         const typeof( ((type *)0)->member ) *__mptr = (ptr);     \
         (type *)( (char *)__mptr - offsetof(type,member) );})

即根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针。比如,有一个结构体变量,其定义如下:

struct demo_struct {
    type1 member1;
    type2 member2;
    type3 member3;
    type4 member4;
};
struct demo_struct demo;

同时,在另一个地方,获得了变量demo中的某一个域成员变量的指针,比如:

type3 *memp = //从某处获得的member3成员的指针

此时,如果需要获取指向整个结构体变量的指针,则可以这样做,如下:

struct demo_struct *demop = container_of(memp, struct demo_struct, member3);

接下来我们来解析这个过程。首先,我们将container_of(memp, struct demo_struct, type3)根据宏的定义进行展开如下:

struct demo_struct *demop = ({ \
const typeof( ((struct demo_struct *)0)->member3 ) *__mptr = (memp); \
(struct demo_struct *)( (char *)__mptr - offsetof(struct demo_struct, member3) );})

@1 第2行解析

这里 typeof 是 GNU C对标准C的扩展,它的作用是根据变量获取变量的类型。因此,上述代码中的第2行的作用是

  1. 使用typeof获取结构体域变量member3的类型为 type3,
  2. 定义了一个type3指针类型的临时变量__mptr,并将实际结构体变量中的域变量的指针memp的值赋给临时变量__mptr。

经过这两步:

const typeof( ((struct demo_struct *)0)->member3 ) *__mptr = (memp);
//转换成
type3 *__mptr = (memp);

@2 第3行解析

假设结构体变量demo在实际内存中的位置如下所示:

   struct demo
 +-------------+ 0xA000
 |   member1          |
 +-------------+ 0xA004
 |   member2          |
 |                    |
 +-------------+ 0xA010
 |   member3          |
 |                    |
 +-------------+ 0xA018
 |   member4          |
 +-------------+------+

那么 在执行了上述代码的第2行之后 __mptr的值即为0xA010;再看上述代码的第3行:

(struct demo_struct *)( (char *)__mptr - offsetof(struct demo_struct, member3) );})

因为offsetof 就是取结构体中的域成员相对于地址0的偏移地址,也就是域成员变量相对于结构体变量首地址的偏移。因此,offsetof(struct demo_struct, member3)调用返回的值就是member3相对于demo变量的偏移。结合上述给出的变量地址分布图可知,offsetof(struct demo_struct, member3)将返回0x10。

@3 综合分析,此时有:

__mptr==0xA010
offsetof(struct demo_struct, member3)==0x10

因此, (char *)__mptr - ((size_t) &((struct demo_struct *)0)->member3) == 0xA010 - 0x10 == 0xA000,即结构体变量demo的首地址。由此,container_of实现了根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针的功能。


GNU C之表达式中的复合语句

4.1 复合语句说明

在标准C中,表达式指的是运算符和操作数的组合,而复合语句指的是由一个或多个被括在花括号里的语句构成的代码块。是不允许将复合语句用于表达式中的。但在GNU C中,允许用小括号括起来的复合语句出现在一个表达式中。这个表达式的类型为复合语句中以分号结尾的最后一个子语句表达式的类型,其值也为最后子表达式的值。使用实例如下:

#include <stdio.h>  
main()  
{  
    int  a = ({
               int b =4;  
               int c =3;  
               b+c;  
               b+c-2;  
              });  
    printf("a = %d\n",a);  
    return 0;  
}

输出结果为:

a = 5

说明:a的数值是复合语句中最后一个语句的值,并且它的数据类型与最后一个语句的数据类型相匹配。

4.2 复合语句在Linux内核中的应用

@1 常被用于宏的定义中,比如min的实现:

#define  min(x,y)  ({         \  
     typeof(x)  __min1 = (x); \  
     typeof(y) __min2 = (y);  \   
     __min1 < __min2  ? __min1 :min2;})

此处定义了一个安全的求最小值的宏。

@2 在标准C中,通常定义为:

#define min(x,y) ((x) < (y) ? (x) : (y))

@3  @1与@2相比,@1的写法可以避免了@2在min_t(x++,++y)中出现的副作用。即这个定义计算 x 和 y 分别两次,当参数有副作用时,将产生不正确的结果,而在GNU C中使用语句表达式只计算参数一次,避免了可能的错误。因此,语句表达式在内核中通常用于宏定义。


5 GNU C之标号元素

标准C要求数组或结构变量的初使化值必须以固定的顺序出现,在GNU C中,通过指定索引或结构域名,允许初始化值以任意顺序出现。指定数组索引的方法是在初始化值前写"[INDEX] =",要指定一个范围使用"[FIRST ... LAST] ="的形式。

5.1 数组应用1,对指定元素进行初始化

在数组的初始化列表中使用“[index]=value”这样的形式即可以实现对指定(通过index指定)的某个元素进行初始化。使用实例如下:

#include <stdio.h>  
int main(void)  
{  
    int i;  
    int arr[6] = {[3] =10,11,[0]=5,6};  
 
    for (i=0;i<6;i++)  
           printf("a[%d]=%d\n",i,arr[i]);  
 
    return 0;  
}

执行结果为:

a[0]=5
a[1]=6
a[2]=0
a[3]=10
a[4]=11
a[5]=0

若在指定初始化项目后跟有不止一个值,如[3]=10,11。则这些多余的数值将用来对后续的数组元素进行初始化,即数值11用来初始化arr[4]。对C语言数组来说,在初始化一个或多个元素后,其中未经初始化的元素将被自动地初始化为0后者NULL(针对指针变量而言)。同时 未经过任何初始化的数组,其所有元素的值都将是不确定的。

5.2 数组应用2,范围内的几个元素被初始化为同一值

GNU C还支持”[first…last]=value”的形式,即以个范围内的几个元素被初始化为同一值。使用实例如下:

#include <stdio.h>  
int main()  
{  
    int i;  
    int arr[]={ [0 ... 3] =1,[4 ... 5]=2,[6 ... 9] =3};  
    for(i=0; i<sizeof(arr)/sizeof(arr[0]);i++ )
        printf("arr[%d]:%d\n",i,arr[i]);  
    return 0;  
}

执行结果为:

arr[0]:1
arr[1]:1
arr[2]:1
arr[3]:1
arr[4]:2
arr[5]:2
arr[6]:3
arr[7]:3
arr[8]:3
arr[9]:3

6 GNU C之匿名联合或结构体

在GNU C中,可以在结构体中声明某个联合体(或结构体)而不用指出它的名字,这样之后就可以像使用结构体成员一样直接使用其中联合体(或结构体)的成员一样直接使用其中联合体(或结构体)的成员。使用实例如下:

#include <stdio.h>
struct test_struct {
    char * name;
    union {
        char gender;
        int id;
    }; 
    int num; 
};  
int main(void)  
{  
    struct test_struct test_struct={"jibo",'F',28};  
    printf("test_struct.gender=%c,test_struct.id=%d\n",test_struct.gender,test_sturct.id);
    return 0;  
}

执行结果为:

test_struct.gender=F,test_struct.id=70

注:在linux内核中常用匿名联合(或结构体)。


7 GNU C之分支声明

对于条件选择语句,gcc内建了一条指令用于优化,在该条件经常出现或很少出现的时候,编译器可以根据这条指令对条件分支选择进行优化。内核把这条指令封装成了宏,即likely()和unlikely(),例如:

if (foo){  
    /**/
}

如果想要把这个选择标记成绝少发生的分支:

if (unlikely(foo)){  
    /**/  
}

相反,如果想把一个分支标记为通常为真的选择:

if(likely(foo)) {  
    /**/  
}

说明:关于likely()和unlikely()的具体分析见Linux 内核源码中likely()和unlikely()


8 零长度的数组

8.1 存在的原因

在标准C中长度为零的数组是被禁止的,其要求数组最少长度为1个字节。但是GNU 扩展C 允许定义零长数组。那么为什么要支持零长度数组,它的好处是什么,它如何使使用,以及零长度数组主要用在什么场合呢:

  • 目的:是为了访问不定长结构体时节省空间和便利性。
  • 用法:在一个结构体的最后 ,申明一个长度为0的数组,就可以使得这个结构体是可变长的。

对于编译器来说,此时长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量 (注意:数组名永远都不会是指针!),但对于这个数组的大小,我们可以进行动态分配。使用案例如下:现在程序中要分配一个struct demo结构体,并紧邻其后分配长度为LEN个字节空间,则可以使用如下方法得到:

struct demo  
{  
    int a;  
    char b[256];  
    char follow[0];  
}; 

这样就可以用demo->follow来访问结构体demo随后的空间数据。当然也可以使用指针来达到这样的目的。如下所示:

struct demo{  
    int a;  
    char b[256];  
    char *follow;     
};  
struct demo *demo=(struct demo *)malloc(sizeof(struct demo)+LEN);

同样可以达到零长度数组的效果,但是却多分配了一个char指针。如果分配额外数据空间还好,否则就是白白浪费了空间。

在某一结构末尾如定义类似 char bytes[0] 的零长数组,表示该结构不定长,可通过数组的方式进行扩展。结构中必包含一个长度信息。结构本身类似于一个信息头。同时,此结构只能通过堆方式分配内存。其优点就是比起在结构体中声明一个指针变量、再进行动态分配的办法,这种方法效率要高。

8.2 直接访问

因为在访问数组内容时,不需要间接访问,避免了两次访存。实例使用如下:

#include <stdio.h>  
#include <stdlib.h>  
  
struct  test{  
    int count;  
    //reverse is array name;the array is no item;  
    //the array address follow test struct  
    int reverse[0];  
};  
   
int main()  
{  
    int i;  
    struct test *ptest = (struct test *)malloc(sizeof(struct test)+sizeof(int)*10);  
    for(i=0;i<10;i++){  
            ptest->reverse[i]=i+1;  
    }  
    for(i=0;i<10;i++){  
            printf("reverse[%d]=%d \n",i,ptest->reverse[i]);  
    }  
    printf("sizeof(struct test) =%d\n",sizeof(struct test));  
    int a = *(&ptest->count +1 );  
    printf("a=%d\n",a);  
    return 0;  
}

执行结果为:

reverse[0]=1
reverse[1]=2
reverse[2]=3
reverse[3]=4
reverse[4]=5
reverse[5]=6
reverse[6]=7
reverse[7]=8
reverse[8]=9
reverse[9]=10
sizeof(struct test) =4
a=1

可以看到test结构体中reverse数组并不占用空间。sizeof(struct test)占用内存空间为4。并且可以看到count变量之后就是零长度数组的内容。


9 范围标记

GCC还扩充了范围标记,通过它可以表示一段数值范围。这可以用在C程序的很多地方。最常用的莫过于switch/case 语句中。使用实例如下:

static int sd_major(int major_idx)  
{  
    switch (major_idx) {  
    case 0:  
        return SCSI_DISK0_MAJOR;  
    case 1 ... 7:  
        return SCSI_DISK1_MAJOR + major_idx - 1;  
    case 8 ... 15:  
        return SCSI_DISK8_MAJOR + major_idx - 8;  
    default:  
        BUG();  
        return 0;   /* shut up gcc */  
    }  
}

范围标记还可以用来初始化数组里面的一些连续元素。如前面第5节介绍的数组。


10 给函数、变量和数据类型指定属性

属性是程序员向编译器传送信息或命令的工具,通常用它们来命令编译器在编译程序的时候,帮我们完成某些特殊的处理。属性可以指定到不同种类的对象上,包括函数、变量和类型等。指定属性时,必须用关键字”__attribute__”然后再在后面跟上用两个圆括号括起来的属性列表,属性列表中的属性用逗号隔开。使用方法如下:

__attrbitue__((attr_1,attr_2,attr_3)) 

10.1 noreturn

function属性,noreturn 用于函数,表示该函数从不返回。这可以让编译器生成稍微优化的代码,最重要的是可以消除不必要的警告信息比如未初始化的变量。

10.2 format(ARCHETYPE,STRING-INDEX,FIRST-TO-CHECK)

function属性 ,format 用于函数,表示该函数使用printf, scanf 或strftime 风格的参数,
使用这类函数最容易犯的错误是格式串与参数不匹配,指定format 属性可以让编译器根据格式串检查参数类型.
参数说明:

        "archetype"指定是哪种风格;
        "string-index"指定传入函数的第几个参数是格式化字符串
        "first-to-check"指定从函数的第几个参数开始按上述规则进行检查

在文件include/linux/kernel.h中,实例使用如下: 

asmlinkage int printk(const char * fmt, ...)  __attribute__ ((format (printf, 1, 2)));

10.3 unused

function属性,unused属性用于函数和变量,表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。

10.4 deprecated

function属性,deprecated指出函数已经被废弃,不应该再使用。如果试图使用已经废弃的函数,就会收到警告。还可以对类型和变量应用这个属性,促使开发人员尽可能少使用它们。

10.5 section ("section-name")

function属性,__attribute__中的section属性将函数或数据放入指定名为"section_name"输入段中而不是输出段中。
注:在 linux 驱动程序设计中,模块加载函数前有一个 __init 宏,也用了attribute 的 section属性。如下所示:

#define __init __attribute__ ((__section__(".init.text"))) 

在linux内核中,所有标示为__init的函数在链接的时候都放在.init.text这个段中。此外,所有的__init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些 __init 函数,并在初始化完成后释放 init 区段。在linux 内核源代码中,与段相关的重要宏定义有:__init , __initdata, __exit, __exitdata 及类似的宏。

10.6 aligned(ALIGNMENT)

function:可以在定义变量时,添加__attribut__,来决定是否使用内存对齐,或是内存对齐到几个字节。使用实例如下:

struct i387_fxsave_struct {  
    unsigned short cwd;  
    unsigned short swd;  
    unsigned short twd;  
    unsigned short fop;  
} __attribute__ ((aligned (16)));

表示该结构类型的变量以16字节对齐。

10.7 packed

function属性,告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数对齐。

10.8 interrupt("")

在ARM 平台上 "__attribute((interrupt("IRQ")))" 表示改函数是一个interrupt handler。


11 可变参数宏

在 GNU C 中,宏可以接受可变数目的参数,就象函数一样。实例使用如下:

include/linux/kernel.h  
#define pr_debug(fmt,arg...) \  
     printk(KERN_DEBUG fmt,10rg)

说明:arg表示其余的参数,可以是零个或多个,这些参数以及参数之间的逗号构成arg的值,在宏扩展时替换arg,例如:

pr_debug("%s:%d",filename,line)

扩展为

printk("<7>" "%s:%d", filename, line)

使用"##"的原因是处理arg不匹配任何参数的情况,这时arg的值为空,GNU C预处理器在这种特殊情况下,丢弃"##"之前的逗号,这样

pr_debug("success!\n")  

扩展为

printk("<7>" "success!\n")

注意:宏定义最后没有逗号。


12 内建函数

GNU C提供了大量的内建函数,其中很多是标准C库函数的内建版本,例如memcpy,它们与对应的C库函数功能相同。还有一些其它内建函数,它们的名字通常以__builtin开始。

12.1 __builtin_return_address(LEVEL)

内建函数__builtin_return_address返回当前函数或其调用者的返回地址,参数LEVEL指定在栈上搜素框架的个数,0表示当前函数的返回地址,1表示当前函数的调用者的返回地址。例如在文件kernel/sched.c中有这样的定义:

printk(KERN_ERR "schedule_timeout: wrong timeout" "value %lx from %p\n", timeout,__builtin_return_address(0));

12.2 __builtin_constant_p(EXP)

内建函数__builtin_constant_p用于判断一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0。例如在文件include/asm-i386/bitops.h中有这样的定义:

    (__builtin_constant_p(nr) ?     \  
      constant_test_bit((nr),(addr)) :   \  
       variable_test_bit((nr),(addr)))

说明:很多计算或操作在参数为常数时有更优化的实现,在GNU C中用上面的方法可以根据参数是否为常数,只编译常数版本或非常数版本,这样既不失通用性,又能在参数是常数时编译出最优化的代码。

12.3 __builtin_expect(EXP,C)

内建函数__builtin_expect用于为编译器提供分支预测信息,其返回值是整数表达式EXP的值,C的值必须是编译时常数。在文件include/linux/compiler.h中有这样的定义:

#define       likely(x)         __builtin_expect((x),1)  
#define       unlikely(x)      __builtin_expect((x),0)

在文件kernel/sched.c中使用如下:

if(unlikely(in_interrupt())) {  
    printk("Scheduling in interrupt\n");  
    BUG();  
}

这个内建函数的语义是EXP的预期值是C,编译器可以根据这个信息适当地重排语句块的顺序,使程序在预期的情况下有更高的执行效率。上面的例子表示处于中断上下文是很少发生的。
 

发布了289 篇原创文章 · 获赞 47 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/vviccc/article/details/105175497