天天看點

開源軟體源碼閱讀小技巧

開源軟體已經廣泛的被網際網路公司所應用,不僅僅是因為其能給企業節省一大筆成本,而且最重要的是擁有更多的自主可控性,能從源頭上對軟體品質進行把控。另一方面,由于開源軟體背後往往沒有大型的商業公司,是以文檔相對來說不是非常完善(或者說文檔和代碼不一定互相對應),是以,作為一名合格程式員,尤其是基礎軟體開發的程式員,閱讀開源軟體源碼的能力是必備的素質。

MySQL作為world most popular的開源資料庫,被廣大程式員所使用,其簡單、高效、易用等優點被大家贊不絕口,作為一款已經有20多年的開源資料庫,不少開源狂熱分子對其源碼進行了詳細的剖析,然後面對MySQL上百萬行的代碼,初學者往往無從下手。古語說的好,工欲善其事必先利其器,本文分享分享一些Linux下閱讀修改源碼常用工具的小技巧,筆者認為這些小技巧對MySQL源碼(其實對其他開源項目也一樣)分析以及後續的修改有莫大的幫助。

另外說明一下,這篇文章需要你對這些常見的工具有所了解,如果之前對vim/git/gdb/Ctags/Cscope/Taglist/gcc等沒有什麼了解,建議先上網找找基礎教程。

衆所周知,MySQL資料庫采用插件式存儲引擎模式,即MySQL分Server層和plugin層,Server層主要做SQL文法的解析、優化、緩存,連接配接建立、認證以及Binlog複制等通用的功能,而plugin層才是真正負責資料的存儲,讀取,奔潰恢複等操作。Server層定義一些接口,plugin層隻要實作這些接口,那麼這個引擎就能在MySQL中使用,是以才有了這麼多的引擎,例如InnoDB,TokuDB,MyRock等,但這個同時也代表着,引擎層的代碼和Server層的代碼風格會完全不一樣,例如在Server層中,代碼縮進是2個空格而在InnoDB層中,代碼縮進是8個空格,當需要經常同時修改不同層的代碼時,容易造成格式混亂,進而影響閱讀。

Vim作為一款Linux下常用的文本檢視編輯工具,在源碼的閱讀中必屬主力。針對這個問題,常用的解決辦法是,在家目錄下,寫兩個不同的vimrc檔案,一個對應Server層的風格,一個對應InnoDB層的風格,還需要編寫一個簡單的切換腳本,當需要修改Server層的代碼時,切換到Server層的風格,反之亦然。但是當需要同時修改Server和InnoDB多處代碼時候,會比較繁瑣,同時,在檔案中切換,往往使用的是Ctags和Cscope,直接從Server層切換到InnoDB層的代碼了,根本沒有給你切換的機會(可以直接在Vim中執行source指令,但是依然麻煩),如果Vim能根據不同的檔案加載不同的格式那就友善多了。

在Vim的配置檔案中有個内置的指令autocmd,後面可以跟一些事件E,再後面可以跟一些檔案名F,最後放一些指令C,表示,當這些檔案F觸發這些事件E後,執行這些指令C。在另外一方面,MySQL的Server層代碼和InnoDB層代碼放在不同的目錄下,雖然有很多,但是可以用通配符比對。結合autocmd這個指令以及MySQL源碼分布的規律,可以寫出下面的vimrc配置檔案:

第一部分,這裡重點介紹一下BufRead和BufNewfile這兩個事件,前者表示當開始編輯新緩存區,讀入檔案後。說的通俗易懂點就是,當你打開一個已經存在的檔案後且這個檔案内容都已經被加載完畢後,這個事件被觸發。後者,表示開始編輯不存在的檔案,簡單的說,就是打開一個新的檔案。

第二部分,其中,au是autocmd的縮寫,/home/yuhui.wyh/polardb是筆者MySQL的根目錄,sql、include目錄下面放了大部分Server層的代碼,client目錄下是用戶端的代碼(比如mysqlbinlog, mysql等)也沿用了Server層的風格,同時團隊在testcase中也規定用Server層的代碼風格,是以也把它放在一塊。另外一方面,InnoDB層的代碼就相對比較統一,都在storage/innobase下面。

第三部分,就是source指令,這個指令表示加載并執行後面這個檔案裡面的配置。vimrc_server和vimrc_innodb分别表示Server層和InnoDB層的不同格式,需要自己編寫。

綜上所述,我們可以分析出這個vimrc配置檔案所表達出的意思,這裡以最後一行為例,其他幾行類似。最後一行的意思就是,當打開/home/yuhui.wyh/polardb/storage/innodb/這個目錄下的所有檔案或者在此目錄下建立一個新檔案的時候,執行~/.vimrc_innodb這個配置檔案。

至此,完美解決上述問題。

同時由于這個方式是以緩存區為粒度的,是以下述幾種使用方式都有效:

1. 目前檔案A屬于Server層,使用Ctags跳轉到InnoDB層檔案B,則檔案B使用InnoDB風格,編輯或者閱讀後,如果使用Ctrl+T傳回(或者其他方式)A,則A依然使用Server層風格,不會被影響。

2. 多視窗支援,由于緩存區獨立加載,即使同時打開多個終端中的多個vim,也不會互相影響。

3. 如果先打開Server層的檔案A,然後使用:e指令打開另外一個InnoDB層的檔案B,然後使用:bn互相切換,格式依然不會亂掉,A永遠使用Server層風格,B永遠使用InnoDB風格。

4. 如果使用vim -O方式同時打開多個InnoDB和Server層檔案,然後使用Ctrl+w在其之間切換,依然沒有什麼問題。

BufRead事件的威力就是如此牛X。

BTW,上面這圖隻是我的配置檔案的一部分,完整的檔案如下:

" ic file color

au BufRead,BufNewFile /home/yuhui.wyh/polardb/polardb_src/storage/innobase/include/*.ic setfiletype c

倒數第二行的意思是,當遇到.ic結尾的檔案時,把這個檔案當作是C語言的檔案來解析,這樣文法就會高亮啦~

這裡還有一點要說明的是,如果同時多個事件被觸發,則按照配置檔案中出現的順序依次執行,是以如上圖所示,vimrc_normal放的是我自己常用的風格,畢竟不能被MySQL完全同化麼~。而最後vimrc_base裡面放的是三種模式(normal,server,innodb)共有的配置,代碼複用麼,嘿嘿

Ctags和Cscope是很有名的Linux指令行下閱讀代碼的神器,有Linux下的sourceinsight的美稱,網上已經有很多介紹,不熟悉的可以先去網上找找。這裡分享一下筆者常用的配置,不同的配置可能導緻搜尋結果的不同。

由于源碼經常變動,是以我寫了一個alias友善重建tag資料庫。csfile其實就是生成源碼檔案清單(并不是MySQL源碼目錄下的所有檔案都是源碼檔案),這裡要注意把.ic和.i為字尾名的檔案也加進去,這種檔案也是MySQL源碼檔案,其他的字尾名基本都是比較正常的。生成了源碼檔案清單後就可以用從scope和ctags生成對應的标簽了。這裡介紹一下我使用的參數:

cscope:

-b 建立tag資料庫檔案,預設檔案名為cscope.out

-q 建立反向索引加速檢索,會産生cscope.in.out和cscope.po.out兩個檔案

-R 在目錄下遞歸搜尋

-i 從指定檔案中擷取源碼檔案路徑,有了這個參數,不用上面這個參數也可以

ctags:

--extra=+q 在tag中增加類的資訊,這樣當一個tag有多處定義的時候,搜尋時可以幫助辨認

--field=+aimSn 主要也是在tag中增加一些資訊(類的通路權限,繼承關系,函數原型等),搜尋時可以根據這些額外資訊把最有可能的定義排在前面

--c-kinds=+l 增加局部變量定義的索引,MySQL有一些函數很大,不友善查找,把這個開起來就友善多了

--total 産生tag檔案後,輸出一些統計資訊,例如,掃描了多少個源檔案,多少行源代碼以及産生了多少個tags

--sort=foldcase 對産生tags資料庫使用大小寫不敏感的排序,便于後續檢索

-L cscope.files 從檔案中擷取源代碼檔案的路徑

這裡隻是簡單的提一下,詳細可以看幫助文檔。

接下來分享一下筆者常用的使用方法:

為了友善跳轉,筆者在vimrc檔案中加入了如下定義:

set cscopetagorder=1

這樣,當我搜尋一個标簽(Ctrl+])的時候,先從ctags産生的标簽庫中搜尋,然後再從cscope中搜尋。

同時在vimrc中加入上圖的定義,友善使用cscope的功能:

Ctrl+\+g:尋找定義處,筆者一般很少用了,一般用Ctrl+]代替。

Ctrl+\+s: 當你想檢視一下這個标簽以C語言标準Symbol在哪些地方出現過時,可以用這個,也就是說,搜尋出的結果都是标準C語言Symbol的。

Ctrl+\+t: 這個是搜尋出所有出現這個tag的位置,不管是不是C語言Symbol。

這裡介紹一個上面兩個指令的差別,一般來說,Ctrl+\+s這個指令搜尋出的一般都在源代碼中且是全詞比對的,而Ctrl+\+t這個指令可能搜尋出注釋中的tag,也有可能是半個詞比對,但是Ctrl+\+t這個指令有實時性,即當你修改過檔案後,如果不重建整個tags資料庫,用Ctrl+\+s搜尋不到最新的标簽,而用Ctrl+\+t就可以,當然Ctrl+\+t這個速度也會慢一點。換句話說,Ctrl+\+t是Ctrl+\+s的超集,如果你用Ctrl+\+s搜尋不到,然後用Ctrl+\+t可能就能找到了,這種情況在MySQL源碼中還比較常見,因為其用了很多宏定義來簡化代碼,這些宏定義有些不能被ctags正确的解析成C語言Symbol,是以隻能用Ctrl+\+t才能搜尋到,一個常見的例子就是InnoDB層線程函數基本都用類似DECLARE_THREAD()的形式來定義,隻能用Ctrl+\+t來找,才能找到這個函數正确的定義處。

Ctrl+\+c:查找目前的标簽在哪些地方被引用過。筆者經常用這個功能,因為常常需要看目前這個函數在哪些地方被調用過。如下圖,可以一眼看出recv_parse_log_recs這個函數被三個函數調用過(分别用《《和》》包括起來)。

開源軟體源碼閱讀小技巧

在這例子中,如果你檢視了編号為1調用的地方,不用傳回,可以直接按下:tn(:tN代表反向)這個指令,然後會自動跳到編号為2調用的地方,這樣可以快速的在調用處檢視。這個小技巧在cscope其他指令中也支援。

Ctrl+\+d:查找這個函數中引用了哪些函數,用的相對較少一點。

Ctrl+\+f: 打開指定檔案名的檔案,需要在索引中。這個指令也還是經常用的,例如,你目前在sql_parse.cc的Server層代碼中,需要檢視一下ha_innodb.cc這個InnoDB層的檔案,你可以直接輸入:cs f f ha_innodb.cc,這樣檔案可以直接打開,而不需要你用:e或者其他指令輸入完整的檔案路徑,提高了不少效率。當然,你把光标停在一個include語句的頭檔案上,也是可以直接打開的。

Ctrl+\+e:使用了這個,你可以在tag中指定通配符,這樣就支援模糊查詢了。

此外,當你沒打開任何一個檔案的時候,突然想檢視一個tag(例如rds_update_malloc_size)的定義,你可以直接在指令行輸入vim -t rds_update_malloc_size,注意要在tags資料庫所在的目錄,然後就會直接打開rds_update_malloc_size定義的檔案并跳轉到定義處。這裡要求tag不能拼錯一點,也就是不支援模糊查詢,如果你想要模糊查詢的話,直接打開一個空的vim,然後輸入:tag rds_update,然後按Tab鍵,就可以自動補全,如果補全的不是你想要的,接着按Tab直到找到你想要的。

最後,介紹一下TagList的小工具。這個工具就是把一個檔案中的所有定義給抽取出來,顯示在一個分屏中,友善你檢視。類似下圖:

開源軟體源碼閱讀小技巧

它統計了變量,結構體,宏定義以及函數,打開後你可以得到這個檔案的概覽,有些時候,你想檢視一個函數,但是這個函數的名字又想不起來,你可以打開這個,然後在函數清單裡面找,比你在檔案中用]]指令一個個找快的多。常用指令:

回車:當你停留在某個标簽上,直接回車,即可跳轉到這個标簽的定義上,同時光标也會停留在定義所在的視窗上,如果你想接着檢視TagList視窗,需要重新切換。

p: 同回車作用差不多,不同的就是,跳轉後光标依然停留在TagList視窗,你可以接着檢視其他标簽,這個比較實用,一般現在TagList視窗中查找,找到後在敲回車,切換過去,同時可以把TagList視窗關掉。

x: 如果你嫌TagList視窗太小,就可以用這放大視窗

+,-,*,=:這些都是折疊或者展開某一類或者全部的标簽

s:排序有兩種,一種是按照出現順序,一種是按照首字母排序,可以用這個指令切換

此外,你可以在vimrc中配置TagList相關配置,例如:

其中,Tlist_Exit_OnlyWindow表示當隻剩下TagList這個視窗時,退出vim。Tlist_Use_Right_Window表示TagList視窗顯示在vim右邊。當你打開多個檔案的時候,如果不設定Tlist_Show_One_File為1,就會把所有檔案裡面的定義都輸出在TagList視窗中。Tlist_Auto_Open則表示TagList視窗是否預設打開。Tlist_Sort_Type表示預設按照首字元出現順序排序。

總之,在閱讀源碼的過程中,要善于使用各種工具便于我們快速找到我們想要的東西,如果還有什麼使用技巧值得分享,可以留言告訴筆者哈

有時候,當你在源碼中遊走的時候,會被搞的暈頭轉向,不知道自己在哪裡了,這個時候你可以使用Ctrl+G來檢視自己在哪個檔案中,但是你還想知道自己在哪個函數中呢?這個vim貌似沒有提供預設的快捷鍵,那麼我們就自己造個輪子吧:

這個showFuncName的函數跟快捷鍵f綁定起來了,你隻需把這個函數放在vimrc中,然後在源碼中按下f,就可以檢視目前在哪個函數中,但是有些時候會有問題,可能沒有找到正确的函數頭,這個時候,就隻能用最原始的[[和]]指令來找函數頭了,然後使用Ctrl+O的方式傳回之前停留的地方。

MySQL Server層的代碼對單行的注釋有點小要求:如果這行有代碼也有注釋,必須從第48列開始寫注釋。這個時候如果你用手調整到48列,會很麻煩,依然可以寫一個函數,然後綁定一個快捷鍵(Shift+Tab):

介于MySQL Server層和InnoDB層的格式很容易搞錯,你需要經常檢視格式是否正确,這個時候你可能需要把所有隐藏的不可見的字元給顯示出來,指令你給是set list,同樣,如果你頻繁使用,還不如加個快捷鍵綁定:

map l :set list! <CR>

這樣你隻要按下l就可以在是否顯示不可見字元中切換。

我們在寫代碼中,一般不希望有多餘的空格,尤其在一行代碼的結束後,後面不應該有多餘的空格,但是空格又是不可見的字元,很難察覺到,除了用上述set list檢視外,可以用一下的指令,這個指令會查找多餘的空格,然後用紅色高亮出來,時刻提醒你。

此外,這邊總結了一些常用好用的vim指令,在閱讀源碼中很有用。

set number: 顯示代碼行數

set ignorecase: 忽略大小寫,這個在使用/搜尋中很有用

set hlsearch: 搜尋結果高亮

set incsearch:當你在搜尋時,每輸入一個字母就開始搜尋一次,這樣當你要搜尋一個很複雜的東西時候,隻需要輸入部分,就可以找到了。例如,你要搜InsertShiftTabWrapper這個函數,如果這個參數不打開,需要等你輸入完所有,然後按回車才開始搜尋,而打開這個參數,則每輸入一個字母,就搜尋一次,你可能隻需要輸入Insert這個單詞,vim可能就已經跳轉到InsertShiftTabWrapper這個函數了。

set showmatch: 當你輸入後半個括号時候,打開這個開關,前半個括号會閃一下,提示你目前輸入的括号是跟他比對的。

set paste: 可以進入複制模式,複制入的東西不會被重排。

批量注釋連續多行: 光标移到第一列,切換到列選擇模式Ctrl+v,然後選擇中所有需要注釋的行,然後按一下Shift+i,接着輸入//,最後按兩下Esc鍵即可。

*: 光标停留在一個tag上,然後按下這個,就可以在檔案中找到所有這個tag,并且高亮出來,可以用n檢視下一個,用N檢視上一個。

%:停留在括号上,可以用來檢視另外半個括号,一般用來檢視括号比對。

Ctrl+F,Ctrl+B:整頁滾動

gd:檢視局部變量定義

gD:檢視全局變量定義,隻能檢視這個檔案中的

[[: 跳轉到上面一個定義

]]: 跳轉到下面一個定義

用gdb記得加上-g以及關掉-O的優化,不然單步調試中,無法跟源代碼對應,看不清楚。

gdb啟動參數中加上-q可以把煩人的版本資訊給去除掉。

gdb可以使用—args啟動,然後程式的參數就可以直接寫在後面,不需要進入gdb後再指定。

可以在家目錄下建立.gdbinit檔案,把常用配置寫進去,如下圖:

set print elements 0: 如果你要列印一個數組,set print elements 5,表示最多隻列印5個元素,set print elements 0表示列印所有元素

set print array-indexes on: 列印數組的時候,同時把索引也列印出來

set print pretty on: 打開的時候,顯示結構體會比較漂亮,按照多行縮進的格式顯示,關閉的時候,隻是在一行中列印整個結構

set print object on: 打開的時候,如果使用type指令檢視變量類型,會考慮虛函數的影響,即列印真正的類型,否則隻列印編譯時候确定的父類型

set history save on: 打開曆史指令記錄功能,這樣當你再次進入gdb的時候,你可以使用方向鍵檢視之前使用過的指令了

使用-tui參數啟動gdb,或者啟動gdb後按Ctrl+x+a,可以進入gdb的圖形化調試界面,上半部分為源代碼視窗,下半部分為指令行界面,再按一下這個組合鍵就能傳回傳統的字元界面:

開源軟體源碼閱讀小技巧

源碼界面,執行到的代碼行會高亮出來,斷點行前面會有個B+>辨別。預設的焦點在代碼視窗,即方向鍵控制的是代碼的移動,可以使用focus cmd将焦點切換到指令行視窗,方向鍵即可控制檢視之前執行過的指令,否則需要使用Ctrl+p或者Ctrl+n。其他指令跟指令行gdb類似。

另外,我們常常會碰到MySQL hang住的情況,雖然這個時候你用kill指令殺掉,然後重新開機,能解決燃眉之急,不過為了找到hang的原因,最好的辦法是保留住記憶體現場,友善後面排查。一種方法是使用kill -11的方法,讓核心産生一個coredump,但是如果當時MySQL記憶體使用的比較多,需要産生一個很大的檔案,這對磁盤寫入造成很大的沖擊。另外一種方式是使用pstack産生一個所有線程的函數調用堆棧關系,類似gdb中的bt指令,如下圖:

這裡僅僅截取了兩個線程的函數堆棧資訊。通過這個可以看出,程式在i_s_innodb_log_reader_fill_table這個函數處奔潰了,然後你需要去那個函數裡面看到底發生了什麼。後面這種方法由于隻需要産生一個很小的文本檔案,線上出問題了經常使用這種方式。但是這裡還是有點小不爽,奔潰的位置既然能定位到函數級别,那麼能不能直接定位到源碼中的行級别,這樣即使這個函數很大,後期診斷起來也友善多了。解決方法很簡單,隻需要改一下pstack的源碼:

把這行中的$readnever去掉就行了。readnever這個參數的作用如下:

說白了就是啟動效率,但是個人感覺得不償失,既然程式已經發生問題了,提供更加詳細的診斷資訊才是王道。去掉這個參數後,以後看到的pstack結果就是類似下圖了:

可以看到函數在哪個檔案中的哪一行了。在MySQL發生死鎖時,用這招進行診斷很有效。當然,記住,在編譯MySQL的時候一定要帶上-g,不然還是沒有這些調試資訊的。

在平時的代碼開發中,需要加新的feature,或者fix bug以及optimize等操作時,一般都會從master上拉一個分支出來,然後自己在上面随便折騰,這也就導緻在同一分支上,會有多次commit,最後在把這些commit都送出到主幹,會導緻主幹上比較亂,這時候git reset指令就有用了:

git reset --soft HEAD^^: 把最近的兩次送出的變動合并,結果以送出到暫存區的形式存在,即git add之後的檔案狀态,這個時候,你隻需要再git commit一下,就能把多次送出合并。

git reset --mixed HEAD^^: 跟上面的類似,隻不過檔案回退到未加入暫存區之前的狀态,也就是說,你還需要執行一把git add,然後才能執行git commit。

git reset --hard HEAD^^: 這個操作直接把最近兩次的送出都給删除掉,代碼沒有了,慎用。

合并自己的commit後,也不能直接就送出,最好把master上的變更給同步過來,因為在你開發分支的時候master上可能有新的送出。這個時候git rebase指令就上場了。筆者常用的方法是,首先checkout出master,然後git pull一把,然後切換回之前的分支并執行git rebase master,這樣就會把master上的變動給同步過來,master上的變動在前,你自己的變動在後,如果兩者有沖突,git rebase會停下來,你自己把沖突的檔案給處理好後,然後git add,再執行git rebase --continue。最後再把分支送出,發起code review過程,如果通過的話,就可以直接merge到master,不會有沖突。使用git rebase還有一個好處是,能保證master上的送出是一條線,不像使用git merge送出的,會導緻master上有很多分支,當然也有一個不好的地方,那就是會導緻送出的時間發生變動,送出的時間不會保證是遞增的順序。

此外,還有一些指令也挺好用的:

git blame: 當你發現源碼中的Bug的時候,想找出這是誰的鍋,然後這條指令就排上用場了。當然其實更有用的一種用法是通過它來找到這個新的feature的issue:比如說,代碼中多了一個變量var_path,你想知道這個變量是幹啥的,除了看注釋和源碼,你可以通過git blame找到送出的commit id,然後在git log的commit message中找到Issue id資訊以及簡介,找到Issue id後就可以在gitlab等代碼倉庫中,找到Issue的詳細資訊,比如為何建立,什麼時候建立以及解決的辦法等。

這段代碼是用來定義InnoDB這個引擎的接口資訊的,友善Server層的代碼調用。第一次看,你可能根本不知道這是個啥玩意,即使你用Ctags等工具跳轉,也不一定看的清楚,尤其針對源碼的初學者,這個時候你可以打開預編譯檔案看一下:

這下就很清楚了,這段代碼幹了兩件事:定義兩個int變量和定義一個結構體。同時還把結構體裡面兩個常量給列印了出來,看過去清晰多了。同樣道理,你還可以在#ifdef分不清走哪條路徑的時候用這招,很好用的。

暫時先總結這麼多,後續會持續更新,如果你有什麼好用的小技巧,歡迎在下面留言。

About me

    大學畢業于西安東大男子技術專修學校(好評1,好評2)

    碩士浪迹于帝都中關村,出沒在融科計算機教育訓練學校(好評1,好評2),整天捉摸着黃色圖檔、血腥暴力圖檔、反動圖檔的監控,說白了就是為某牆服務,呵呵

    家住浙江甯波,是以在阿裡巴巴工作(16年7月入職),阿裡雲事業部,雲資料庫ApsaraDB源碼組,主攻MySQL核心開發

    喜愛計算機,熱愛程式設計,尤其是資料庫領域,熟悉MySQL,資料庫基本理論,資料庫中間件等

    對高性能伺服器開發、高性能代碼優化也略有涉獵

    此外,偏愛攝影,目前維護lofter照片分享網站,立志成為碼農界最好的攝影師~

    有什麼事的話,可以在這裡留言或者通路我的新浪微網誌哦~