Solaris 平台上開發多緒程式的一些心得

Last Update: 12/08/2003

前言

公司因為客戶的需求,所以得在 Solaris 上開發一個 web crawler。我就是負責寫這隻程式的師程工,從用的很開心的 Windows 環境突然被丟到異次元去,也許這就是宿命吧 …

開發環境的設定

Solaris 因為使用者少,使用環境封閉,加上樣樣死要錢的傳統,對窮苦小公司和悲情師程工來講,有很多事情需要去克服,第一件難題便是找 C/C++ compiler 來用。說到這個東西,我們第一個想到的便是 GNU gcc 囉,但不幸的是,由於規格規定,必須把 crawler 寫成 multi-threaded (多緒),glibc 的多緒在 Solaris 上問題多多,請參拜咕狗大神便知分曉,所以就不能用 gcc 了。

不能用 gcc 的話,另一個選擇就是 Sun ONE 了。Sun ONE 有試用版,基本上那個保護是玩具,反安裝後殺乾淨再裝一次就可無限制試用了 ... (不要告我啊,我好怕 ...) ... Sun ONE 的環境包含了 compiler suite,用 Java 寫的 IDE,還有其他有的沒的東西。很有趣的是,各個 package 之間的版本並不相容,甚至有衝突的情形,例如若要裝 IDE,就必須裝整個 Forte 4 或 7 Developer,這時就不能用更新的 Sun ONE 8 compiler suite。反過來,若用了 Sun ONE 8 compiler suite,就不能再安裝 Forte 的 IDE 了,完全不能理解這是什麼爛設計 … 最後還是乖乖選擇 Sun ONE 8 compiler suite,用古老的 makefile 來工作。使用 Sun ONE 必須記得在 PATH 中加入 /opt/SUNWspro/bin 和 /usr/ccs/bin,the good old shell huh? …

沒有 IDE 沒關係,vim 是我們的好朋友,但像這類好用的東西,想當然耳是不會在機器裡的,所以要到 http://www.sunfreeware.com 去抓人家已經做好的回來裝才會快。除了 vim 之外,最重要的 ddd 當然也是要抓的。Sun 提供了一個新的 make 程式,名叫 dmake,它可以同時利用多顆 CPU 來工作,缺點是只吃老式的 makefile rule,但這並不是什麼大問題。

由於 Sun 的 C library 對於處理 DOS 的文字檔有問題的關係,所以所有 Sun ONE 的工具只要碰到 0xd 0xa 這種 DOS 換行的文字檔就會死的非常奇怪了。因此,若有文件是這種格式的話,最好先用些技巧處理一下,例如下面的 PERL 程式:

#!/usr/bin/perl
# convert a dos \r\n format to unix \bn
open (FILE, $ARGV[0]);
while (<FILE>)
{
# remove the stupid \r, replace with \n
# some broken editors just put \r
s/\r/\n/g;
# Remove the duplicate \n
s/\n\n/\n/g;
print;
}

 

不過 Sun ONE 也並非是完全沒有優點的,至少我覺得有些東西它要比 GNU/Linux 提供的好用。首先是它的 garbage collection library,你只要在 ld 時加上 -lgc 的參數便可以讓你的程式具備 garbage collection 的能力 (當然,這是「理論上」,實際上照 leak 不誤,只是不那麼嚴重)。另外值得一提的是它提供的兩個除錯器 mdb 和 dbx,一般我們用的是 dbx,它具有不錯的 thread debugging 能力 (當然比起 Visual C++ 的來講還是差的遠啦 …),同時你也可以用 ddd 當成它的介面,只要在命令列下

ddd -dbx

就可以了。

多緒程式設計

基本的資料哪裡找?

Solaris 8/9 在多緒的支援上比 Linux 2.4 系列的 kernel 要強的太多,但它的文件資料整理就遠不如 Linux 了,最令人頭痛的便是不知要用那一本手冊才對。因為 Sun 的官方網站居然是用列舉法列出所有文件的標題讓你來找 … (狠角色吧) …

文件的找法是這樣的,請先參拜咕狗大神,找 Sun multithreaded programming guide,然後找出這些文件中 part number 數字最大的,它就是最新版。 其他類的手冊依此要領類推即可,手冊內容倒是非常詳細,值得仔細研讀。另一個很不錯的入門 link 在此: http://www.cs.cf.ac.uk/Dave/C/CE.html,這是英國 Cardiff University 的 Dave Marshall 教授的上課內容,對於 pthread 和 Solaris thread programming 解釋的非常清楚。

Solaris 有兩種 thread library,一個是 pthread (POSIX thread),另一個是 Solaris thread,兩個程式庫可以安心的混用。Pthread 的 API 文件豐富 (因為是標準的 thread,幾乎和 Linux 的相同),但我自己測試的結果,它的執行速度要比 Solaris thread 慢上 15% 至 50%。因此,我還是放棄 pthread,反正真要 port 到 Linux 還是得改,何必堅持虛偽的 portability 呢?

怎麼用 Solaris thread?

使用 Solaris thread 其實很容易,編譯時加上 -mt 和 -lthread 就可以了,例如:

CC -mt threadtest.cpp -lthread

加上 -mt 之後,compiler 若覺得某個變數不是 thread-safe 的話,它會很雞婆地幫你加上 implicit mutex,但這些 mutex 會非常嚴重地拖慢程式執行的速度。所以說,若你覺得某個地方會有 thread-safe 的問題,但忽略不去理它程式還會動的時候,別太開心,反組譯看看就會看到一堆白痴的 mutex。

自己做 mutex 時,若你與我一樣用的是 detached thread,可以好好利用 thr_yield() 這個 API,對於短暫的 lock,使用 thr_yield() 可以增加不少的執行效率,我自己的程式在用了這個 API 之後快了一倍!猜想它的原理應是這樣:有 n 個 thread 同時試著 lock 時,若沒有 thr_yield() 的話,那麼系統就會等到 timeout 之後,再隨機挑選一個來跑;但若每個 thread 都 thr_yield()的話,系統不必等待 timeout 便知道要挑選其中一個來跑了。

文件的陷阱

Sun 提供的 man 和文件也是不能完全相信的,裡面 BS 的地方不少。在高負載的情形下,用到 file descriptor 的 API 或是 memory copy 等動作是很容易出問題的 (還是我用的 Sun 機器太爛?450 MHz Ultra SPARC IIi…)。底下是一些防止出槌的心得:

1. fclose(): 所有的文件都告訴你這是 thread-safe 的,但有時它就是會丟出 SIGPIPE 出來,若沒有接到的話當然就 core dump 囉,我的做法是 signal(SIGPIPE, SIG_IGN); 給他加持下去,又稱「鋸箭法」。這招不是隨便亂說的喔,你可參考一下 Sun 自己出的範例程式,裡頭也廣泛運用鋸箭法

2. memcpy(): 基本上 memcpy 還算穩定,如果你都把它用在 char buffer 的話。但若像這類常用的 trick:

memcpy((void*)addr.sin_addr, (void*)hent.h_addr, len);

那就很容易引發由 memory alignment 所造成的 SIGSEGV 了,最好是儘量避免這種老式技巧,乖乖使用 explicit type casting 會較好。

3. printf(): 高負載的情況下 (如 thread initialization),寫到 stdout 或 stderr 的輸出有可能被丟掉,就算加了 fflush() 也一樣 (這點 manual 上有說),但它沒說的是輸出的順序可能會錯亂 :D 最好是在輸出的地方加上 timestamp 或 sequence number 之類的東西比較保險。另外呢,若使用 ddd 來除錯,ddd 會直接丟掉它來不及接的 stderr message,所以在寫作的時候,定義 error level 和 error region 就變成很重要的事,因為除錯器太爛所以不能丟出太多訊息 ... (削足適履,I know) ... 不妨參考一下 squid 的 source code,它在 error message 的處理算是經典。

4. sleep(): 如果你在短命的 thread 尾巴加上 sleep(),通常它會多睡上好一陣子。sleep() 是用 semaphore 實作的,若有一堆短命的 thread 同時湧進來,當然會睡了好久好久 ... 因為 semaphore 就像上廁所,FIFO ok?

5. select(): select() 裡可以設 timeout 沒錯,但那真的只是純參考用的,真正要睡多久是 kernel 它老人家決定的。

手冊上沒寫的重要資訊

對一個 process 來講,每個 thread 的 thread id 是獨一無二 (unique) 的。這只能算是個「假設」,因為 thread id 是用 unsigned int 來存的,我還沒試過當這個 number overflow 時會如何 (猜想是歸零後重新累加,所以有可能不為 unique ... )

另外呢,若你使用 dbx 去檢視 segmentation-faulted memory 的話,有 99% 的機率你就再也無法執行 thread-related command 了。所以,在除錯的時候,要記得先執行 thread-related command,再做 memory inspection。


有心得要討論嗎?歡迎 mail 給我。