Linux 內核中常用的 C 語言技巧

Linux 內核採用的是 GCC 編譯器,GCC 編譯器除了支持 ANSI C,還支持 GNU C。在 Linux 內核中,許多地方都使用了 GNU C 語言的擴展特性,如 typeof、attribute、__aligned、__builtin_等,這些都是 GNU C 語言的特性。

typeof

下面是比較兩個數大小返回最大值的經典宏寫法:

#define max(a,b) ((a) > (b) ? (a) : (b))

如果 a 傳入 i++,b 傳入 j++,那麼這個比較大小就會出錯。例如:

#define max(a,b) ((a)>(b)?(a):(b))

int x = 1, y = 2;
printf("max=%d\n", max(x++, y++));
printf("x = %d, y = %d\n", x, y);

輸出結果:max=3,x=2,y=4。這是錯誤的結果,正常我們希望的是 max(1,2),返回 max=2。如何修改這個宏呢?

在 GNU C 語言中,如果知道 a 和 b 的類型,可以在宏裏面定義一個變量,將 a, b 賦值給變量,然後再比較。例如:

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

如果不知道具體的數據類型,可以使用 typeof 類轉換宏,Linux 內核中的例子:

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

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();
typeof(foo()) var;//聲明瞭int類型的var變量,因爲表達式foo()是int類型的。由於表達式不會被執行,所以不會調用foo函數。

零長數組

零長數組,又叫柔性數組。而它的作用主要就是爲了滿足需要變長度的結構體,因此有時也習慣性地稱爲變長數組

用法:在一個結構體的最後, 申明一個長度爲 0 的數組, 就可以使得這個結構體是可變長的

對於編譯器來說, 此時長度爲 0 的數組並不佔用空間, 因爲數組名本身不佔空間, 它只是一個偏移量, 數組名這個符號本身代表了一個不可修改的地址常量

結構體中定義零長數組:

<mm/percpu.c>
struct pcpu_chunk {
    struct list_head  list;
    unsigned long    populated[];  /* 變長數組 */
};

數據結構最後一個元素被定義爲零長度數組,不佔結構體空間。這樣,我們可以根據對象大小動態地分配結構的大小。

struct line {
    int length;
    char contents[0];
};

struct line *thisline = malloc(sizeof(struct line) + this_length);
thisline->length = this_length;

如上例所示,struct line數據結構定義了一個 int length 變量和一個變長數組 contents[0],這個 struct line 數據結構的大小只包含 int 類型的大小,不包含 contents 的大小,也就是 sizeof (struct line) = sizeof (int)

創建結構體對象時,可根據實際的需要指定這個可變長數組的長度,並分配相應的空間,如上述實例代碼分配了 this_length 字節的內存,並且可以通過 contents[index] 來訪問第 index 個地址的數據。

case 範圍

GNU C 語言支持指定一個 case 的範圍作爲一個標籤,如:

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

這裏 low 到 high 表示一個區間範圍,在 ASCII 字符代碼中也非常有用。下面是 Linux 內核中的代碼例子。

<arch/x86/platform/uv/tlb_uv.c>
    
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;
        }
    }
}

另外,還可以用整形數來表示範圍,但是這裏需要注意在 “...” 兩邊有空格,否則編譯會出錯。

<drivers/usb/gadget/udc/at91_udc.c>

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

標號元素

GNU C 語言可以通過指定索引或結構體成員名來初始化,不必按照原來的固定順序進行初始化。

結構體成員的初始化在 Linux 內核中經常使用,如在設備驅動中初始化 file_operations 數據結構:

<drivers/char/mem.c>
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,
};

如上述代碼中的 zero_fops 的成員 llseek 初始化爲 zero_lseek 函數,read 成員初始化爲 new_sync_read 函數,依次類推。當 file_operations 數據結構的定義發生變化時,這種初始化方法依然能保證已知元素的正確性,對於未初始化成員的值爲 0 或者 NULL

可變參數宏

在 GNU C 語言中,宏可以接受可變數目的參數,主要用在輸出函數里。例如:

<include/linux/printk.h>
#define pr_debug(fmt, ...) \
dynamic_pr_debug(fmt, ##__VA_ARGS__)

“...” 代表一個可以變化的參數表,“VA_ARGS” 是編譯器保留字段,預處理時把參數傳遞給宏。當宏的調用展開時,實際參數就傳遞給 dynamic_pr_debug 函數了。

函數屬性

GNU C 語言允許聲明函數屬性(Function Attribute)變量屬性(Variable Attribute)類型屬性(Type Attribute),以便編譯器進行特定方面的優化和更仔細的代碼檢查。特殊屬性語法格式爲:

__attribute__ ((attribute-list))

attribute-list的定義有很多,如noreturnformat以及const等。此外,還可以定義一些和處理器體系結構相關的函數屬性,如 ARM 體系結構中可以定義interruptisr等屬性。

下面是 Linux 內核中使用format屬性的一個例子。

<drivers/staging/lustru/include/linux/libcfs/>
int libcfs_debug_msg(struct libcfs_debug_msg_data *msgdata,const char *format1, ...)__attribute__ ((format (printf, 2, 3)));

libcfs_debug_msg() 函數里聲明瞭一個format函數屬性,它會告訴編譯器按照 printf 的參數表的格式規則對該函數參數進行檢查數字 2 表示第二個參數爲格式化字符串,數字 3 表示參數 “...” 裏的第一個參數在函數參數總數中排在第幾個

noreturn屬性告訴編譯器,該函數從不返回值,這可以消除一些不必要的警告信息。例如以下函數,函數不會返回:

void __attribute__((noreturn)) die(void);

const 屬性會讓編譯器只調用該函數一次,以後再調用時只需要返回第一次結果即可,從而提高效率。

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

Linux 還有一些其他的函數屬性,被定義在 compiler-gcc.h 文件中。

#define __pure           __attribute__((pure))
#define __aligned(x)        __attribute__((aligned(x)))
#define __printf(a, b)      __attribute__((format(printf, a, b)))
#define __scanf(a, b)       __attribute__((format(scanf, a, b)))
#define noinline          __attribute__((noinline))
#define __attribute_const__   __attribute__((__const__))
#define __maybe_unused      __attribute__((unused))
#define __always_unused      __attribute__((unused))

變量屬性和類型屬性

變量屬性可以對變量或結構體成員進行屬性設置。類型屬性常見的屬性有alignmentpackedsections等。

alignment屬性規定變量或者結構體成員的最小對齊格式,以字節爲單位。

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

在這個例子中,編譯器以 8 字節對齊的方式來分配 qib_user_info 這個數據結構。

packed屬性可以使變量或者結構體成員使用最小的對齊方式,對變量是以字節對齊,對域是以位對齊

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

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)

__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很可能爲假

__builtin_prefetch(const void *addr, int rw, int locality)主動進行數據預取,在使用地址 addr 的值之前就把其值加載到 cache 中,減少讀取的延遲,從而提高性能

該函數可以接受 3 個參數:

<include/linux/prefetch.h>
#define prefetch(x) __builtin_prefetch(x)
#define prefetchw(x) __builtin_prefetch(x,1)

下面是使用 prefetch() 函數進行優化的一個例子。

<mm/page_alloc.c>
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);
    }
    …
}

在處理 struct page 數據之前,通過 prefetchw() 預取到 cache 中,從而提升性能

asmlinkage

在標準 C 語言中,函數的形參在實際傳入參數時會涉及參數存放問題。

對於x86架構,函數參數局部變量被一起分配到函數的局部堆棧裏。x86 中對 asmlinkage 的定義:

<arch/x86/include/asm/linkage.h>
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

attribute((regparm(0))):告訴編譯器該函數不需要通過任何寄存器來傳遞參數,只通過堆棧來傳遞

對於ARM來說,函數參數的傳遞有一套ATPCS標準,即通過寄存器來傳遞。ARM 中的 R0~R4 寄存器存放傳入參數,當參數超過 5 個時,多餘的參數被存放在局部堆棧中。所以,ARM 平臺沒有定義 asmlinkage

<include/linux/linkage.h>
#define asmlinkage CPP_ASMLINKAGE
#define asmlinkage CPP_ASMLINKAGE

UL

在 Linux 內核代碼中,我們經常會看到一些數字的定義使用了 UL 後綴修飾。

數字常量會被隱形定義爲 int 類型,兩個 int 類型相加的結果可能會發生溢出。

因此使用 UL 強制把int類型數據轉換爲unsigned long類型,這是爲了保證運算過程不會因爲int的位數不同而導致溢出。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xDkoksyfljNdVSx1H4kBTQ