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
的定義有很多,如noreturn
、format
以及const
等。此外,還可以定義一些和處理器體系結構相關的函數屬性,如 ARM 體系結構中可以定義interrupt
、isr
等屬性。
下面是 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))
變量屬性和類型屬性
變量屬性可以對變量或結構體成員進行屬性設置。類型屬性常見的屬性有alignment
、packed
和sections
等。
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 個參數:
-
第一個參數
addr
表示要預取數據的地址; -
第二個參數
rw
表示讀寫屬性,1 表示可寫,0 表示只讀; -
第三個參數
locality
表示數據在 cache 中的時間局部性,其中 0 表示讀取完 addr 的之後不用保留在 cache 中,而 1~3 表示時間局部性逐漸增強。如下面的prefetch()
和prefetchw()
函數的實現。
<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
的位數不同而導致溢出。
-
1 :表示有符號整型數字 1
-
UL:表示無符號長整型數字 1
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xDkoksyfljNdVSx1H4kBTQ