談Linux Kernel巨集 do{…}while(0) 的撰寫方式

by loda

hlchou@mail2000.com.tw

Android/Linux Source Code Tags
App BizOrz 
BizOrz.COM 
BizOrz Blog

http://loda.hala01.com/oldarticles/

不同於過去文章,都是以技術的探索為主,這次的文章,無關乎技術深度,但希望凸顯出Linux Kernel實作上的巧思.筆者相信對大家會有所收穫,也因此選擇以此為主題.

在程式設計寫作時,巨集Marco是常見的寫法,相信閱讀本文的開發者,也非常熟悉才是.

也因為是基礎知識,大家都認為對巨集的使用都已經了然於心,但其實簡單的事物背後也是有它的思考.

在Trace Linux Kernel原始碼時,常會看到把巨集用 do {….} while(0)的寫法包裝起來,時間久了,也認為這是一個合理的作法,但原因呢?就真的沒有仔細的去思考過,從編譯器的角度來說,用了do{…..} while(0)的寫法,在不開啟編譯優化參數的前提下,由於多了新的判斷,應該是會比起單純的 {….}產生額外的程式碼,影響到執行效能才是(例如,多了CMP或Branch條件判斷).而且主觀上,以Linux Kernel這樣等級的Open Source計畫,應該是不會設計出一個明知會導致效能降低的寫法才是.但沒有實際去驗證過,總是存在心頭上的一個問號.

既然有了這樣的發想,也就有了本文的誕生,在這篇文章中將會透過實際的例子,比對編譯後的程式碼,來確認Linux Kernel如此撰寫的影響.更進一步的來說,會參考Linux Kernel Coding Style與Writing CPP Marcos文章中的案例,藉此說明巨集使用上考量. 希望能對閱讀本文的讀者,帶來收穫.

Linux Kernel 中的例子

接下來,我們以 Linux Kernel中使用到do {….} while(0)的Source Code作說明,藉此了解目前實作的例子.

(1) 在檔案include/linux/spinlock.h 中,有如下宣告

# define raw_spin_lock_init(lock)                               \

do {                                                            \

static struct lock_class_key __key;                     \

\

__raw_spin_lock_init((lock), #lock, &__key);            \

} while (0)

而在kernel/fork.c中,呼叫 raw_spin_lock_init的方式為

static void rt_mutex_init_task(struct task_struct *p)

{

raw_spin_lock_init(&p->pi_lock);

#ifdef CONFIG_RT_MUTEXES

plist_head_init_raw(&p->pi_waiters, &p->pi_lock);

p->pi_blocked_on = NULL;

#endif

}

(2) 在檔案include/linux/cred.h 中,有如下宣告

#define put_group_info(group_info)                      \

do {                                                    \

if (atomic_dec_and_test(&(group_info)->usage))  \

groups_free(group_info);                \

} while (0)

而在kernel/cred.c中,呼叫 put_group_info 的方式為

…..

if (cred->group_info)

put_group_info(cred->group_info);

…..

再來,讓我們用實際的案例來驗證do {….} while(0)與{…..}的寫法,並比對透過編譯器產生的結果與Linux Kernel Coding Style文件,了解Linux Kernel對巨集的設計建議

對編譯器而言,DoWhile0會得到比較好的編譯結果嗎?

在本段驗證前,其實,腦中有個念頭,就是是否GCC對這種DoWhile0寫法有比較好的編譯結果,能讓運作效率更佳,所以Linux Kernel才會選擇這樣的設計方式.也因此我們透過如下的代碼來進行驗證,並且會透過Open Source的ARM GCC 4.4與商用版本的ARM RVCT 4.0 分別帶入優化參數 0,1,2 比對產生的編譯結果.

int funcA(int IN_A,int IN_B)

{

int OUT=0;

if(IN_A)

{

OUT=(IN_A+3)*IN_B;

}

else

{

OUT=(IN_A+33)*IN_B;

}

return OUT;

}

int funcB(int IN_A,int IN_B)

{

int OUT=0;

if(IN_A)

do{

OUT=(IN_A+3)*IN_B;

}while(0);

else

do{

OUT=(IN_A+33)*IN_B;

}while(0);

return OUT;

}

int main()

{

int vRet;

vRet=funcA(0,3);

printf("0 A:%ld\n",vRet);

vRet=funcB(0,3);

printf("0 B:%ld\n",vRet);

vRet=funcA(2,3);

printf("2 A:%ld\n",vRet);

vRet=funcB(2,3);

printf("2 B:%ld\n",vRet);

return 0;

}

透過 ARM GCC 以-O0編譯後,執行結果如下

# ./main

0 A:99

0 B:99

2 A:15

2 B:15

使用arm-eabi-objdump -x -D  進行反組譯

比對 funcA與funcB的結果如下所示

funcA funcB
0:    e52db004       push     {fp}     ; (str fp, [sp, #-4]!)

4:    e28db000       add      fp, sp, #0          ; 0×0

8:    e24dd014       sub       sp, sp, #20        ; 0×14

c:    e50b0010       str         r0, [fp, #-16]

10:   e50b1014       str         r1, [fp, #-20]

14:   e3a03000       mov     r3, #0   ; 0×0

18:   e50b3008       str         r3, [fp, #-8]

1c:    e51b3010       ldr         r3, [fp, #-16]

20:   e3530000       cmp     r3, #0   ; 0×0

24:   0a000005       beq       40 <funcA+0×40>

28:   e51b3010       ldr         r3, [fp, #-16]

2c:    e2833003       add      r3, r3, #3           ; 0×3

30:   e51b2014       ldr         r2, [fp, #-20]

34:   e0030392       mul      r3, r2, r3

38:   e50b3008       str         r3, [fp, #-8]

3c:    ea000004       b           54 <funcA+0×54>

40:   e51b3010       ldr         r3, [fp, #-16]

44:   e2833021       add      r3, r3, #33        ; 0×21

48:   e51b2014       ldr         r2, [fp, #-20]

4c:    e0030392       mul      r3, r2, r3

50:   e50b3008       str         r3, [fp, #-8]

54:   e51b3008       ldr         r3, [fp, #-8]

58:   e1a00003       mov     r0, r3

5c:    e28bd000       add      sp, fp, #0          ; 0×0

60:   e8bd0800       pop      {fp}

64:   e12fff1e          bx         lr

68:   e52db004       push     {fp}     ; (str fp, [sp, #-4]!)

6c:    e28db000       add      fp, sp, #0          ; 0×0

70:   e24dd014       sub       sp, sp, #20        ; 0×14

74:   e50b0010       str         r0, [fp, #-16]

78:   e50b1014       str         r1, [fp, #-20]

7c:    e3a03000       mov     r3, #0   ; 0×0

80:   e50b3008       str         r3, [fp, #-8]

84:   e51b3010       ldr         r3, [fp, #-16]

88:   e3530000       cmp     r3, #0   ; 0×0

8c:    0a000005       beq       a8 <funcB+0×40>

90:   e51b3010       ldr         r3, [fp, #-16]

94:   e2833003       add      r3, r3, #3           ; 0×3

98:   e51b2014       ldr         r2, [fp, #-20]

9c:    e0030392       mul      r3, r2, r3

a0:   e50b3008       str         r3, [fp, #-8]

a4:   ea000004       b           bc <funcB+0×54>

a8:   e51b3010       ldr         r3, [fp, #-16]

ac:    e2833021       add      r3, r3, #33        ; 0×21

b0:   e51b2014       ldr         r2, [fp, #-20]

b4:   e0030392       mul      r3, r2, r3

b8:   e50b3008       str         r3, [fp, #-8]

bc:    e51b3008       ldr         r3, [fp, #-8]

c0:    e1a00003       mov     r0, r3

c4:    e28bd000       add      sp, fp, #0          ; 0×0

c8:    e8bd0800       pop      {fp}

cc:    e12fff1e          bx         lr

可以發現產生的指令集是一致的,再進一步用arm-eabi-gcc 搭配 -O1,O2的優化來編譯,也可以發現,優化的結果與產生的指令集,兩者都是一致的.

在GCC編譯器後,我們改用ARM RVCT 4.0編譯器對上述程式碼進行編譯動作,經過比對,只有在armcc 用-O0時,兩者有如下的差異

funcA funcB
0×00000000:    e1a02000    . ..    MOV      r2,r0

0×00000004:    e3a00000    ….    MOV      r0,#0

0×00000008:    e3520000    ..R.    CMP      r2,#0

0x0000000c:    0a000002    ….    BEQ      {pc}+0×10 ; 0x1c

0×00000010:    e2823003    .0..    ADD      r3,r2,#3

0×00000014:    e0000193    ….    MUL      r0,r3,r1

0×00000018:    ea000001    ….    B        {pc}+0xc ; 0×24

0x0000001c:    e2823021    !0..    ADD      r3,r2,#0×21

0×00000020:    e0000193    ….    MUL      r0,r3,r1

0×00000024:    e12fff1e    ../.    BX       lr

0×00000028:    e1a02000    . ..    MOV      r2,r0

0x0000002c:    e3a00000    ….    MOV      r0,#0

0×00000030:    e3520000    ..R.    CMP      r2,#0

0×00000034:    0a000003    ….    BEQ      {pc}+0×14 ; 0×48

0×00000038:    e1a00000    ….    MOV      r0,r0

0x0000003c:    e2823003    .0..    ADD      r3,r2,#3

0×00000040:    e0000193    ….    MUL      r0,r3,r1

0×00000044:    ea000003    ….    B        {pc}+0×14 ; 0×58

0×00000048:    e1a00000    ….    MOV      r0,r0

0x0000004c:    e2823021    !0..    ADD      r3,r2,#0×21

0×00000050:    e0000193    ….    MUL      r0,r3,r1

0×00000054:    e1a00000    ….    MOV      r0,r0

0×00000058:    e12fff1e    ../.    BX       lr

多了三處可以忽略的 “mov r0,r0“ 動作,但其它的編譯結果都是一致的.

總結來說,除了ARM RVCT 4.0的-O0優化參數外,使用ARM GCC或是ARM RVCT 4.0的編譯環境,對 do {….} while(0)與{…..}的寫法,只要使用到-O1或之後的優化參數,最終產生的編譯結果機械碼兩者是一致的.

所以,筆者原先的揣測看來是多想了…@_@.再來讓我們進一步從程式碼撰寫的角度來分析.

對 if/else 區塊的影響

#define Test_DoWhileZero(IN_A,IN_B) \

do {    \

if(IN_A) \

{ OUT=(IN_A+3)*IN_B;} \

else \

{ OUT=(IN_A+33)*IN_B;} \

} while (0)

#define Test_Normal(IN_A,IN_B) \

{    \

if(IN_A) \

{ OUT=(IN_A+3)*IN_B;} \

else \

{ OUT=(IN_A+33)*IN_B;} \

}

int funcA(int IN_A,int IN_B)

{

int OUT=0;

if(IN_B)

Test_DoWhileZero(IN_A,IN_B);

else

printf("Error IB_B==NULL\n");

return OUT;

}

int funcB(int IN_A,int IN_B)

{

int OUT=0;

if(IN_B)

Test_Normal(IN_A,IN_B);

else

printf("Error IB_B==NULL\n");

return OUT;

}

int main()

{

int vRet;

vRet=funcA(0,3);

printf("0 A:%ld\n",vRet);

vRet=funcB(0,3);

printf("0 B:%ld\n",vRet);

vRet=funcA(2,3);

printf("2 A:%ld\n",vRet);

vRet=funcB(2,3);

printf("2 B:%ld\n",vRet);

return 0;

}

透過arm-eabi-gcc編譯時,會導致如下的錯誤發生

In function ‘funcB’:

error: ‘else’ without a previous ‘if’

原因在於巨集的宣告,如果是 {…….},在使用巨集Test_Normal時又有加上 ; 結尾,就會導致原本的 if/else區塊變成如下情況

if(..)

{

};

else

….

導致 if 條件式在else前就已經結尾.

反之,使用do{…}while(0)寫法的巨集,對應到上述用法時,if/else區塊的展開為

if(..)

do{

}while(0);

else

…..

並不會影響到原本 if/else區塊的條件判斷正確性,又可以滿足巨集中需要多行程式碼時,的程式碼撰寫需求.

總結來說,採用DoWhile0的寫法,可以滿足之後要用inline函式取代巨集的需求,而用在if/else這種條件判斷時,巨集展該後的程式碼也能無誤運作.最最最重要的是,從實際編譯器產生的機械碼來說,並不會因為如此撰寫,導致系統運作效率的降低.

Linux Kernel Coding Style文件

我們可以參考Linux Kernel文件”Linux kernel coding style” (檔案路徑Documentation/CodingStyle),了解Linux Kernel對於巨集使用的說明. 這份文件共有18章,是Linux Kernel程式開發者值得參考的程式設計說明,跟本文有關的DoWhile0巨集寫法是在第12章 “Macros, Enums and RTL”,筆者大致說明如下

1,要避免巨集影響到執行流程.

如下所示,在巨集中的DoWhile0,存在return返回值,這會影響到使用這巨集模組的執行流程.

#define FOO(x)                                  \

do {                                    \

if (blah(x) < 0)                \

return -EBUGGERED;      \

} while(0)

2,避免在巨集宣告中,參考到特定的變數名稱

如下所示,使用FOO巨集時,參考到區域變數index

#define FOO(val) bar(index, val)

在使用巨集FOO的函式中,如果沒有宣告區域變數index,就會導致如下錯誤 “error: ‘index’ undeclared (first use in this function)”.

而若是把index宣告為全域變數,然後使用上述的FOO巨集時,就會在編譯時產生如下的錯誤  “warning: built-in function ‘index’ declared as non-function”,

在巨集的宣告時,儘量要避免額外參考到非巨集帶入的變數,可避免在後續使用上,所造成的問題.

3,巨集所帶的參數不應該當做L-Value.

如下所示,把巨集FOO帶參數直接定義為另一個目標值.

#define FOO(val) val

int func()

{

int x=10;

FOO(x)+=30;

return x;

}

這樣的巨集可以正常運作,但一旦把巨集改為inline函式時

inline int FOO(int val)

{

return val;

}

就會導致如下的錯誤 “error: invalid lvalue in assignment”,

4, 巨集定義的運算式與常數必須有括號前後封裝.以避免因為遺忘了運算優先順序的問題,所導致的錯誤.

如下例子所示,

#define CONSTANTA 2&7

#define CONSTEXPA (400+CONSTANTA)

#define CONSTANTB (2&7)

#define CONSTEXPB (400+CONSTANTB)

在巨集展開後,

CONSTEXPA =(400+2&7)=402&7=2

CONSTEXPB=(400+(2&7))=400+2=402

避免透過巨集封裝運算式時,因為括號沒有明確的配置,導致原本設計上,規劃之外的錯誤發生.更多這部份的例子,可以參考下一段的案例.

更進一步

可以參考 ”Writing C/C++ Macros”文件(路徑 http://www.ebyte.it/library/codesnippets/WritingCppMacros.html)中,有關巨集解釋的九個章節,對於理解巨集有很大的幫助,在實際的驗證上可以透過GCC -E的參數,驗證C程式碼在巨集展開後的結果.

若你覺得對巨集已經很清楚,不彷試試回答下面三個值的結果.

定義巨集為

#define SquareOf(x) x*x

變數  int vBase=7;

而以下這三個巨集執行結果,應該是多少呢?

SquareOf(vBase) , SquareOf(vBase+1) 與 SquareOf(vBase+vBase).

透過程式碼驗證

#define SquareOf(x) x*x

int main()

{

int vBase=7;

printf("vBase=%d and define SquareOf(x) = x*x \n",vBase);

printf("SquareOf(vBase)=%d \n",SquareOf(vBase));

printf("SquareOf(vBase+1)=%d \n",SquareOf(vBase+1));

printf("SquareOf(vBase+vBase)=%d \n",SquareOf(vBase+vBase));

return 1;

}

搭配gcc -E,可以看到巨集展開後的內容如下

int main()

{

int vBase=7;

printf("vBase=%d and define SquareOf(x) = x*x \n",vBase);

printf("SquareOf(vBase)=%d \n",vBase*vBase);

printf("SquareOf(vBase+1)=%d \n",vBase+1*vBase+1);

printf("SquareOf(vBase+vBase)=%d \n",vBase+vBase*vBase+vBase);

return 1;

}

SquareOf(vBase)為49,而SquareOf(vBase+1)= vBase+1*vBase+1=7+7+1=15. (不是8*8=64),SquareOf(vBase+vBase)= vBase+vBase*vBase+vBase=7+49+7=63. (不是14*14=196).

另一個可能犯錯的例子是,

定義巨集為

#define SumOf(x,y) (x)+(y)

變數  int vBase1=3, vBase2=5;

以下這兩個巨集執行結果,應該是多少呢?

SumOf(vBase1,vBase2) 與 2*SumOf(vBase1,vBase2).

透過程式碼驗證
#define SumOf(x,y) (x)+(y)
int main()

{

int vBase1=3, vBase2=5;
printf("vBase1=%d,vBase2=%d and define SumOf(x,y)=(x)+(y) \n",vBase1,vBase2);
printf("SumOf(vBase1,vBase2)=%d \n",SumOf(vBase1,vBase2));
printf("2*SumOf(vBase1,vBase2)=%d \n",2*SumOf(vBase1,vBase2));

return 1;
}

搭配gcc -E,可以看到巨集展開後的內容如下

int main()

{

int vBase1=3, vBase2=5;

printf("vBase1=%d,vBase2=%d and define SumOf(x,y)=(x)+(y) \n",vBase1,vBase2);

printf("SumOf(vBase1,vBase2)=%d \n",(vBase1)+(vBase2));

printf("2*SumOf(vBase1,vBase2)=%d \n",2*(vBase1)+(vBase2));

return 1;

}

SumOf(vBase1,vBase2)為8,而2*SumOf(vBase1,vBase2)= 2*(vBase1)+(vBase2)=6+5=11. (不是2*8=16).

要讓結果符合預期,SquareOf與 SumOf巨集需修改為如下內容.

#define SquareOf(x)      ((x)*(x))
#define SumOf(x,y)      ((x)+(y))

結語

本文從DoWhile0的驗證,到參考有關巨集介紹的文件作探討,我們可以知道像是Linux Kernel這樣受矚目的Open Source計畫,在相關的實作上,也確實有它的思考縝密度.在閱讀Linux Kernel Source Code時,包括在判斷if/else優化動作的likely/unlikely巨集,會透過GCC內建函式__builtin_expect在程式碼編譯時進行條件判斷的優化. 或更進一步藉由GCC內建函式__builtin_constant_p判斷常數,讓__branch_check__巨集可以進行Profiling的動作.而有關平台的部份,像是Memory Barrier的操作,也透過巨集封裝,讓開發者可以便利的使用,這些設計上的思維,都必須要有對編譯器或是平台深度的理解,才能夠達成的.

談Linux Kernel巨集 do{...}while(0) 的撰寫方式相关推荐

  1. Linux kernel的中断子系统之(九):tasklet

    返回目录:<ARM-Linux中断系统>. 总结: 二介绍了tasklet存在的意义. 三介绍了通过tasklet_struct来抽想一个tasklet,每个CPU维护一个tasklet链 ...

  2. Linux Kernel 5.0或在达成600万Git Objects时到来

    早两天,Linus Torvalds在Google+上表示,Linux内核当前正在从4.0向5.0大版本迈进(half-way between),同时接近600万Git的目标.之前的大版本,比如Lin ...

  3. Linux Kernel 4.20 生命周期已结束,建议迁移 5.0

    继 Linux Kernel 4.20 版本正式发布三个月, Linux Kernel 4.20.17 维护版本也于近日更新.Greg Kroah-Hartman 在邮件中写道:"这是 4. ...

  4. Linux Kernel 0.01 的编译和运行

    Linux Kernel 0.01 的编译和运行 本文操作环境均在 Linux 系统中实现. ===================================================== ...

  5. Linux Kernel 3.0新特性概览(转)

    上周五,Linus Torvalds终于发布了备受瞩目的新一代Linux操作系统内核.Linux Kernel 3.0经过了七个RC候选版才推出正式版本,上一个版本是5月19日的2.6.39,也是2. ...

  6. Linus 发文宣布Linux Kernel 5.0 正式发布

    2019年3月4日0:43 UTC,Linux之父Linus Torvalds向Linux List Kernel Mailing发文宣布发布Linux Kernel 5.0 . ​​​​ 2019年 ...

  7. linux mint 安装内核,使用Ukuu在Ubuntu/Linux Mint上安装Linux Kernel 5.0的方法

    Linux Kernel 5.0已发布,具有大量新功能和错误修复,本文介绍使用Ukuu在Ubuntu 18.04/Linux Mint系统上安装Linux Kernel 5.0的方法.默认情况下,Ub ...

  8. Linux Kernel 6.0 CXL Core pci.c 详解

    文章目录 前言 相关链接 Ref 正文 前言 CXL 是一个比较新的技术,所以我研究的内核源码是选了当前比较新的内核版本 linux 6.0.打算将内核关于 CXL 的驱动进行解析一遍,一步一步慢慢来 ...

  9. linux 网卡 巨帧,Linux Kernel e1000e驱动巨型帧处理绕过安全检查漏洞

    发布日期:2009-12-29 更新日期:2010-01-13 受影响系统: Linux kernel 2.6.32.3 描述: ----------------------------------- ...

最新文章

  1. DNN数据库核心表结构及设计思路探研
  2. Google MapReduce架构设计
  3. ubuntu10.04+hadoop0.20.2平台配置(完全分布式模式)
  4. Java依赖注入 - DI设计模式示例教程
  5. OpenCV人脸识别之二:模型训练
  6. MongoDB介绍与安装
  7. 【有容云案例系列】基于Jenkins和Kubernetes的CI工作流
  8. Atitit  项目界面h5化静态html化计划---vue.js 把ajax获取到的数据 绑定到表格控件 v2 r33.docx
  9. 什么叫大数据 大数据的概念
  10. 计算机基础考试在线搜题,计算机基础考试题库 (含答案).doc
  11. java牛顿法求方程根_牛顿迭代法 求方程根
  12. html网页上展示晶圆的坐标图,一种测试不良芯片晶元坐标分布的方法与流程
  13. 光波叠加matlab,光波的叠加教程.ppt
  14. 苹果软件更新在哪里_手机资讯:iPhone 为什么比安卓手机好用iPhone 的独到之处在哪里...
  15. python换行输入数据_python 对比两个文件内容或字符串内容时的换行符/交作业检测小程序...
  16. php - 解决百万级全站用户消息推送问题
  17. 读取远程服务器上文件内容,读取远程服务器上的文件
  18. 水岸秀墅|千年石湖独一墅
  19. Linux基本命令-grep 命令
  20. 自动驾驶决策控制及运动规划史上最详细最接地气综述

热门文章

  1. Jupyter Notebook更换默认浏览器
  2. 遍历ArrayList的过程中移除元素的方式
  3. notability整理归档_5000 字干货:iPad 笔记神器《Notability》详细教程,助你开启学霸之路...
  4. 手机银行APP开发新门户
  5. 解释用于语义面部编辑(Semantic Face Editing)的GAN的隐空间(Latent Space)
  6. OpenCVSharp 生成 HDR 图片
  7. Mybatis环境搭建(仅供参考)
  8. 一文读懂 OceanBase 数据库的SLog日志
  9. 关于网管交换机的优劣势,看完这篇就懂了
  10. 2020 blur()和onblur的使用区别