歡迎光臨
每天分享高質量文章

宋寶華: 論一個程式員問問題的自我修養(修訂版)


問問題如打麻將


麻場如戰場,麻品如人品,檢驗一個人的最好方法就是帶他去搓麻將,一葉而知秋。同樣地,透過程式員問問題的情狀也基本可以看出一個程式員的治學態度。如果他問問題的時候,看得出他經過深思熟慮,經過自己的debug和剖析,這個程式員多半還是一個不錯的程式員,或者正在走向“不錯”。反之,一眼看出,其問的問題就沒有經過大腦,甚至連問題的現場都描述不清楚,這樣的程式員多半在程式設計領域難有大的建樹,很可能只是在長年累月地低水平重覆建設。

一個好的程式員,不斷透過解決工程問題,查閱資料,總結知識,提升認知來逐步把自己變成高手。一個不好的程式員,有點什麼事情就開始到處問,也從來不深入地去自己研究,其實,就算是工作20年,水平恐怕也還是不行,因為他陷入了簡單的經驗式重覆。高手不完全是知識面廣或深,很大程度上高在其本身的認知層面,而認知層面的提高就在於他從來都是在思考。

 

我是一個碼農,也號稱是一老濕,接觸的碼農比較多,平生閱碼農無數(哈哈,吹牛的,閱人莫道千千萬,識得真心在最初)。 好的程式員,基本上,問問題的時候,給你春風拂面的感覺,讓你心裡生出一些敬佩;不好的程式員,很多時候問的問題,你根本無言以對,心裡一萬個草泥馬奔湧而過。

麻品有問題


下麵我們來總結一些不好的程式員問問題的時候的幾個典型的“問題”。

不除錯就問

之前有時候坐在office的時候,有些人問我這樣的問題,“我的程式hang死了,請教一下hang死在哪裡了?”我這個時候只想說,程式是你的,你都不知道哪裡hang死了,我怎麼就會知道?能不能拿gdb attach上去看啊?你拿gdb attach上去看,最後程式是拿不到mutex呢?還是進while(1)了呢?還是怎麼的?

任何時候,問問題之前,先問自己一個問題,“我除錯過沒有?”沒有的話,趕緊先閉嘴。

下麵我們用一個最簡單的例子來解釋這個過程,看下麵一個程式,2個執行緒拿2個鎖,但是順序相反,程式肯定會死鎖。死鎖之後,gdb attach上去看,不要問! 

pthread_mutex_tmutex_1;

pthread_mutex_tmutex_2;


void *child1(void*arg)

{

       while(1) {

              pthread_mutex_lock(&mutex;_1);

              sleep(3);

              pthread_mutex_lock(&mutex;_2);

              printf(“thread 1 get running\n”);

              pthread_mutex_unlock(&mutex;_2);

              pthread_mutex_unlock(&mutex;_1);

              sleep(5);

       }

}


void *child2(void*arg)

{

       while(1) {

              pthread_mutex_lock(&mutex;_2);

              pthread_mutex_lock(&mutex;_1);

              printf(“thread 2 get running\n”);

              pthread_mutex_unlock(&mutex;_1);

              pthread_mutex_unlock(&mutex;_2);

              sleep(5);

       }

}


int main(int argc,char *argv[])

{

       int tid1,tid2;


       pthread_mutex_init(&mutex;_1,NULL);

       pthread_mutex_init(&mutex;_2,NULL);

       pthread_create(&tid1;,NULL,child1,NULL);

       pthread_create(&tid2;,NULL,child2,NULL);

       …

}

上述程式跑著一定會死鎖,之後我們gdb attach它的pid上去除錯就可以分析出來它是怎麼樣在死鎖。我們首先看到它有3個執行緒:

(gdb) i threads

  Id  Target Id         Frame

  3   Thread 0xb75e9b40 (LWP 15809) “a.out” …

  2   Thread 0xb7deab40 (LWP 15808) “a.out”…

* 1    Thread 0xb7deb700 (LWP 15804)”a.out”…

切到執行緒2,看一下backtrace:

(gdb) thread 2

[Switching to thread 2 (Thread 0xb7deab40 (LWP 15808))]

(gdb) bt

#4 0x08048627 in child1 (arg=0x0) at deadlock.c:12

這個PID為15808的執行緒在程式碼的第12行,child1()這個函式,等一個mutex,等誰呢?

(gdb) l child1

12                 pthread_mutex_lock(&mutex;_2);

看出程式碼12行在等mutex_2,看下mutex_2的owner是誰?

(gdb) p mutex_2

$1 = {__data ={__lock = 2, __count = 0, __owner = 15809, __kind = 0, …

我們目前看出來,15808執行緒在等一個mutex,這個mutex目前被15809拿著。我們現在切到執行緒3,然後看backtrace:

(gdb)thread 3

[Switchingto thread 3 (Thread 0xb75e9b40 (LWP 15809))]

(gdb) bt

#4  0x08048677 in child2 (arg=0x0) at deadlock.c:24

同樣看出15809在等一個mutex,在程式碼的第24行。第24行在等誰呢?

(gdb) l child2

24                 pthread_mutex_lock(&mutex;_1);

第24行在等mutex_1,它的owner又是誰呢?

(gdb) p mutex_1

$2 = {__data ={__lock = 2, __count = 0, __owner = 15808, __kind = 0…

由此可見,15808在等一個mutex,該mutex被15809拿著;15809在等一個mutex,該mutex被15808拿著。顯然是死鎖了。

上述例子非常簡單,但足夠說明我們除錯問題的方法和思維問題的出發點。

很多時候,我們依賴於backtrace(棧回溯),比如kernel的lockup、kernel的OOPS(哎呀!)、應用軟體的hang死不動。


沒有回溯崩潰或hang死現場的問問題都是耍流氓!

無背景就問

任何問題,都必有它的特定背景和再現流程,不然就反證了它是一個general或者common的問題,general的問題本身不是問題,不存在需要debug的問題,基本你透過無數渠道都可以獲得答案。


所以,任何時候,問問題,應該描述清楚你所有的平臺背景,以及你是如何再現的,缺乏足夠資訊的問題,就和缺乏足夠資訊的報bug是一回事。比如,作為一個測試工程師,你報的bug是說手機會宕機,但是沒有說什麼手機,什麼安卓版本,哪天的release,再現的流程,這種bug的報法,屬於無效bug,只會被RD直接ignore。


下麵來看一段真實的對話,它突顯了我霸氣和焦躁的不良品質 🙂



問題情況都說不清楚的情況下,你問問題,顯然就是討罵。比如你說RAMDISK在板子上面跑CRC錯誤,那麼你先有沒有在自己電腦上面解開過,比如執行mount -o loop或者cpio提取?


如果你電腦上面自己都沒有試過,都不確定RAMDISK是不是OK的,問板子還有什麼意義?另外,問板子的問題,就應該描述清楚自己板子的記憶體大小,RAMDISK大小,存放的記憶體為位置,RAMDISK讀出的介質如NAND/SD驅動有無問題等一系列情況,首先排除一些最基本的可能性。

罵完了我心理也有點難受了,感覺自己有點過了。但是我的好基友說,好老濕和好醫生是一樣的,醫者父母心。上來就罵同學,肯定是招人恨啊,但是一輩子讓你不修正自己的思維方式,那麼豈不是更加地不負責任?


沒有背景資訊和再現流程的問問題都是耍流氓!

不學習就問

很多問題,你其實稍微學習一下就沒有必要問了。比如,“請問,一個Linux普通行程多久被排程到?”這樣的問題本來就是很吐血,認真學習後,不會問這樣的問題。明確地說,不知道!裝逼的說法,就是“depend on …”,依賴的東西太多。再裝逼的說法,就是“一言難盡”,但這也是大實話。

普通的Linux行程為人比較善良,我們一般用nice來形容它們的優先順序,nice越高,優先順序越低(你越nice,就越喜歡在地鐵讓座,當然越坐不到座位)。普通行程的跑法,並不是nice低的一定堵著nice高的(要不然還說什麼“善良”),它是按照如下公式進行:

vruntime=  pruntime*NICE_0_LOAD / weight

其中NICE_0_LOAD是1024,也就是NICE是0的行程的weight。vruntime是行程的虛擬執行時間,pruntime是物理執行時間,weight是權重,權重完全由nice決定,如下表:

在RT行程都睡過去之後(有一個特例就是RT沒睡也會跑普通行程,那就是RT加起來跑地實在太久太久,普通行程必須喝點湯了),Linux開始跑NORMAL的,它傾向於排程vruntime(虛擬執行時間)最小的普通行程,根據我們小學數學知識,vruntime要小,要麼分子小(喜歡睡,I/O型行程,pruntime不容易長大),要麼分母大(nice值低,優先順序高,權重大)。這樣一個簡單的公式,就同時照顧了普通行程的優先順序和CPU/IO消耗情況。

比如有4個普通行程,如下表,目前顯然T1的vruntime最小(這是它喜歡睡的結果),然後T1被排程到。

pruntime

Weight

vruntime

T1

8

1024(nice=0)

10*1024/1024=8

T2

10

526(nice=3)

10*1024/526 =19

T3

20

1024(nice=0)

20*1024/1024=20

T4

20

820(nice=1)

20*1024/820=24

然後,我們假設T1被排程再執行12個pruntime,它的vruntime將增大delta*1024/weight這裡delta是12,weight是1024),於是T1的vruntime成為20,那麼這個時候vruntime最小的反而是T2(為19),此後,Linux將傾向於排程T2(儘管T2的nice值大於T1,優先順序低於T1,但是它的vruntime現在只有19)。

所以,普通行程的排程,是一個綜合考慮你喜歡幹活還是喜歡睡和你的nice值是多少的結果。鑒於此,我們去問一個普通行程的排程延遲究竟有多大,這個問題,本身意義就不是特別大,它完全取決於當前的系統裡面還有誰在跑,取決於你喚醒的行程的nice和它前面喜歡不喜歡睡覺。

類似的知識性問題還有很多很多,這些問題,完全可以透過提前學習來解答。沒有必要非得問。


知識性的問題,多半也很容易在網上找到資料。比如,搞不清楚PHP、node.js和Python做後端誰好,隨便一百度,知乎網友回答的一大堆。如果再去問google,那會拿到更多的答案。“敏而好學,不恥下問”固然重要,但“敏而好學,恥於上問”也同樣重要。


沒有經過基本的學習就問問題都是耍流氓!


做一個好麻友


我的帶頭大哥,現在跟好朋友打麻將,都是直接支付寶付款,每局誰胡牌後,支付寶直接轉賬,麻品那是相當地好。然後打2個小時的牌,產生了幾千上萬的GDP,為國家和人民做了很大的貢獻。


下麵建議Linux程式員建立如下好習慣,做一個願賭服輸的好男人:

有問題,找男人

有問題,先man。“請問fread的第二個引數怎麼設定?”鬼曉得?你man啊!

$ man fread

我man過了,我man open的時候出來的居然不是open(),而是openvt:

於是就要

$ man 2 open

這個時候出來的才是open,請問這個2是個什麼鬼?那麼,可以man一下man。

$ man man

所以,有問題,找男人,男人如果有問題,繼續找男人

不知道找哪個男人,只記得函式裡面有個單詞叫timer。那麼先定位,可以用類似apropos的工具:

baohua@baohua-VirtualBox:~/develop$ apropos timer

getitimer (2)        – get or set value of an interval timer

IPC::Run::Timer (3pm) – – Timer channels for IPC::Run.

setitimer (2)        – get or set value of an interval timer

time (7)             – overview of time and timers

timer_create (2)     – create a POSIX per-process timer

timer_delete (2)     – delete a POSIX per-process timer

再man那個函式即可。

提示


歡迎單位的女程式員有問題後直接請教男同事,

美女程式員直接忽略本文的所有條款,

美女程式員不受任何問問題規則約束  🙂



有問題,找Google

有的程式員又跳出來了,“報告,我google上不去”,相信我,一個不會翻牆的程式員,多半不會是一個好程式員。你連牆都不會翻(或者懶得翻的可能性更大),又如何查閱到最合適你的資料?

“報告,翻牆要花錢”,那麼,吃飯要不要花錢?對於程式員來說,翻牆的需求,基本和吃飯的需求是一樣的。翻牆越多,水平越高(翻牆看YY網站除外,那無非是多認識幾個老師)。


我們不是嘲笑baidu一定不行,而是很多時候,工作的時候,碰到的kernel panic,碰到的segmentation fault,很多的其他的現場,它都是以英文的形式來呈現,很可能你在google裡面能直接搜尋到別人跟你碰到同樣的問題。拋開baidu賣狗皮膏藥的道德層面問題不談,baidu在英文方面的搜尋能力顯然是遠遠落後google的。


下麵我們搜尋Kernel hard lockup,百度顯示如下(應該說還不錯):

Google顯示如下(精準地定位到了Linux kernel官網的檔案):

我們不是崇洋媚外,我們只是正視血淋淋的現實。

感恩回答者

很多程式員問別人問題,覺得別人天經地義應該回答你。而且問了很stupid的問題,別人沒有好好回答,還覺得回答者在裝逼。其實反過來想,一定要明白,回答我們問題的人,其實真的是在給我們工作。幾年前,我在LKML問Greg K.H.下一個LTSI(工業級穩定版Linux) 版本是什麼,他給我的回答非常簡單直接,體現了一個正確的回答問題的風範:


“you are asking me to do work for you, right?”,你問別人問題的時候,你在讓別人為你工作(而且這個工作還是免費的),對不對?

 

最好的問問題的方法,就是儘量不要問問題。如果要問題,要問能打動被提問者的問題。如何打動?你問問題,顯示你已經經過足夠的思辨。建議每一個人,可以深度學習一下《How-To-Ask-Questions-The-Smart-Way》這個檔案,它是一個git專案哦:

https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way


最後的話

他回憶說:“我的名字是媽媽取的,我問過她為什麼給我取這麼奇怪的名字,她說是王勃的《滕王閣序》’雄州霧列,俊採星馳,臺隍枕夷夏之交,賓主盡東南之美’這句來的。”


哪怕是一個龍套,我們也要認真地去做。不然就會永遠是個龍套。因為,這是一個演員的自我修養。

不會問問題(意味著從來不試圖自己去解決問題),我們恐怕永遠是一個龍套。當你碰到一個bug,你去問別人,然後別人幫你快速解決了,自己的所學是有限的。而自己陷入思考,真正去debug,去查閱資料,去絞盡腦汁,這個過程的獲得,除了這個修複bug本身以外,更可能學到了很多的知識,獲取了很多的經驗,提升了自己的認知。而這種認知,是你問一萬個問題,並且立即獲得了答案,也無法取得的。


小貼士


三個耍流氓


不除錯就問

無背景就問

不學習就問



三個好習慣


有問題,找男人

有問題,找Google

感恩回答者


贊(0)

分享創造快樂