Hi 大家好,小弟之前有去某一家嵌入式晶片供應商公司上課,令人印象深刻時候有個重點。
-> 不要使用太多sleep(),但是他沒講說為什麼不能使用sleep(),有其他代替方案嗎?
-> 不要使用太多system call(),會需要fork而消耗系統資源,
有專門的C code就優先使用。
有專門的C code就優先使用。
請問先進們針對這兩個問題有什麼特別要注意的地方 or 見解嗎?
因為不是很清楚為什麼要避免使用這樣的情境,謝謝大家。
======================= 分隔線 ================================
小弟大膽猜測,會有這樣疑問的人,要不是沒有學過作業系統或至少深入了解過一些小型 RTOS(如uC/OS-II, FreeRTOS),要不就是從 8 位元單晶片轉戰上來的。尤其後者轉戰到 32 位元 CPU + OS 時很容易出現不適應的症狀,因為他們很習慣直接跟硬體溝通,甚至常常他們的結論就是 OS 爛,綁手綁腳讓我很難做事。
但隨著 32bit SoC + Linux 幾乎一統天下的態勢,想要忽略作業系統幾乎是不可能的事,尤其是甩掉Linux 等於甩掉數百萬人辛勤工作的成果,前些日子跟一位老硬體工程師深談,他居然還有「重新發明輪子」的想法,不才小弟待過不少公司,親眼看過這些公司自己發明的輪子(OS)到最後都淡出了,因為相較於 Linux 這種有幾百萬甚至幾千萬使用者為基礎錘鍊出來的產品,光是穩定度就差一大截,而且以上面的功能多樣性,軟體多樣性,除了微軟這樣的公司外很少有公司能辦得到,更別說原本就是軟體弱國的台灣。如果台灣讓這樣的老人(不是指年紀老,而是思維)繼續掌權,也不能怪年輕人往外逃了。
sleep 與 system call
關於原 po 的問題,我只能說現在看書的人真的變少了,大家都用 Google 湊答案,湊到能動就好。因為原 po 的問題其實只要讀下面三本書就能找到答案:
- The Linux Programming Interface(後簡稱TLPI)
- Linux System Programming(後簡稱LSP)
- Advanced Programming in the UNIX Environment(後簡稱APUE)
這三本還有中譯本,翻譯的也不差,但小弟知道很多人懶得看書,這邊就給個速成解答。
sleep()
Linux sleep() 不只有一種,還有 usleep, nanosleep, select() 也可以充作 sleep。如果你去看 Qt source code,你會看到 QThread::msleep() 還用 pthread synchronize API 實作了另外一種方案。時間的複雜度讓 LSP、TLPI 都特別開闢章節專門講時間。而且 Linux 的時間來源有 5 個,你知道用的是哪一個嗎?
sleep 相關 system call 要關注什麼?以筆者的經驗來看,有兩樣事情特別需要關注:
- 精度
- signal 的影響
精度
#include <time.h> int clock_getres(clockid_t clock_id, struct timespec *res);
下面給出的範例為 LSP 11.3 的加長版,可以確認系統有沒有支援 1ms 的精度:
#include <unistd.h> #include <sys/time.h> #include <stdio.h> #include <time.h> #include <errno.h> typedef long long int64; typedef unsigned long ulong; void MySleep(ulong ms) { struct timespec req; struct timespec rem; ulong tmp = ms * 1000000; req.tv_sec = tmp / 1000000000; req.tv_nsec = tmp % 1000000000; retry_sleep: int ret = ::nanosleep(&req, &rem); if(ret) { if(errno == EINTR){ req.tv_sec = rem.tv_sec; req.tv_nsec = rem.tv_nsec; goto retry_sleep; } } } int64 currentMSecsSinceEpoch() { // posix compliant system // we have milliseconds struct timeval tv; gettimeofday(&tv, 0); return static_cast<int64>(tv.tv_sec) * static_cast<int64>(1000) + tv.tv_usec / 1000; } int main() { clockid_t clocks[]={ CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID, static_cast<clockid_t>(-1)}; for(int i = 0; clocks[i] != static_cast<clockid_t>(-1); ++i){ struct timespec res; int ret; ret = clock_getres(clocks[i], &res); if(ret) perror("clock_getres"); else printf("clock=%d sec=%ld nsec=%ld\n", clocks[i], res.tv_sec, res.tv_nsec); } int64 before = currentMSecsSinceEpoch(); MySleep(1); int64 after = currentMSecsSinceEpoch(); printf("%d ms\n", static_cast<int>(after - before)); return 0; }
如果你得到的結果跟書上一樣:
clock=0 sec=0 nsec=4000250
clock=1 sec=0 nsec=4000250
...
很抱歉,那無論你用 usleep, nanosleep, select() 都只能做到 4ms 的精度(4000250*10^-9=4ms)。有些人會說那沒差啊,反正 FAE 叫我們少用...但您有沒有想過,如果很少人用,幹嘛弄出一堆 sleep 兄弟姊妹呢?再說,以筆者的實際經驗,在與某些設備通訊時,如果沒有以正確的時序送出封包,設備是不會理你的。
而且假如沒有 sleep 會怎樣?如果你對某台通訊設備不停的 polling,該設備收到命令馬上回應,那你這邊的 CPU loading 會馬上爆衝,這對 x86 可能還感覺不到(以4核心來說,可能最多也才 25%),但對小型設備來說就會非常明顯,你輸入 top 指令就可以觀察到。
signal
Linux 程設有點程度的人都知道有些 system call 結束後要檢查 errno == EINTR 的情況,上面這些 sleep 兄弟姊妹也一樣,被 signal 中斷後,要怎麼「補足」缺少的睡眠,每個 system call 都有不同的處理方式,這邊就不多說了請自行查書。
何時不該用 sleep?
有一種情形是最不該使用 sleep,就是想用 sleep 影響 threads 以某種順序執行。這差不多已經是 multi-threading 常識了。另外一種情形是你想在 sleep 時仍然能被打斷接收事件, 那你可能就要改用 select(), epoll()...,當然以筆者的經驗來說,這樣的程式並不好寫就是了。禪宗不是有大師說睡覺時乖乖睡覺,吃飯時乖乖吃飯嗎?哈哈哈
System Call
該 FAE 說要少用 system call,這大體上沒有問題,但是沒講清楚 system call 是怎麼實作出來的。這在 TLPI 已經講得很清楚,就是 software interrupt,每個 system call 對應到一個編號。Linux 還提供一個用法(其實就是 software interrupt + 手動指定編號),讓 system call 還沒有正式放到系統程式庫時(提供 header file, .a .so)也能被呼叫,這邊抓個 libevent 做為例子:
#include <sys/epoll.h> #include <unistd.h> int epoll_create(int size) { //__NR_epoll_create 為 system call 編號 return (syscall(__NR_epoll_create, size)); }
每次 system call 都至少包含下面 3 個動作:
- user mode 切換到 kernel mode
- user mode 與 kernel mode 交換資料
- kernel mode 切回 user mode
所以 system call 慢於 in-process 函數呼叫,但你說完全不用 system call 有可能嗎?那要怎麼用才經濟?很抱歉沒有一個簡單的答案,APUE 的作者 Steven 大師在寫 read() 範例時用了各種 buffer size 測試,但你很可能無法直接套用他的結果,因為你跟他的硬體配置一定不一樣(他老人家已經做仙很久了),而且很可能他是在磁碟上,你是在 Flash Rom 上,豈可隨意套用?
另外在 Effective TCP/IP Programming 有一 Tip 也是強烈建議 TCP 封包寫出時最好一次寫出(send()),看起來真的是寫越少次越好?奉勸各位看官先放下這樣的想法,回想一下過早最佳化是萬病之源,請先不傷身體再強調療效,弄清楚 system call 的用法,從系統角度調校測試軟體才是良策。
而原 po 應該是聽錯 FAE 的描述,因為 system call 與 fork() 沒關係,而 system() 這個 C 函數才與 fork() 有關系,因為 system() 是 fork() 與 exec() 的組合,APUE、TLPI、LSP 幾乎都有教你實作 system() 的範例。如果該位 FAE 真的這樣講...請趕快換供應商!
而原 po 應該是聽錯 FAE 的描述,因為 system call 與 fork() 沒關係,而 system() 這個 C 函數才與 fork() 有關系,因為 system() 是 fork() 與 exec() 的組合,APUE、TLPI、LSP 幾乎都有教你實作 system() 的範例。如果該位 FAE 真的這樣講...請趕快換供應商!
謝謝你,大大很熱心;)
回覆刪除謝謝 釐清我原本錯誤的觀念
回覆刪除不客氣,教學相長~
刪除過了快一年,回頭看這篇文章,當初覺得自己怎麼這麼蠢問這個問題XD
回覆刪除