2016年9月16日 星期五

Q&A: sleep 與 system call

Hi 大家好,小弟之前有去某一家嵌入式晶片供應商公司上課,令人印象深刻時候有個重點。
-> 不要使用太多sleep(),但是他沒講說為什麼不能使用sleep(),有其他代替方案嗎?
-> 不要使用太多system call(),會需要fork而消耗系統資源,
有專門的C code就優先使用。
請問先進們針對這兩個問題有什麼特別要注意的地方 or 見解嗎?
因為不是很清楚為什麼要避免使用這樣的情境,謝謝大家。

======================= 分隔線 ================================
如果供應商 FAE 真的是這樣講,而且沒有進一步解釋,個人認為相當不負責任。但是換個角度想,或許 FAE 覺得這是常識。那為什麼客戶會有這樣的疑問呢?

小弟大膽猜測,會有這樣疑問的人,要不是沒有學過作業系統或至少深入了解過一些小型 RTOS(如uC/OS-II, FreeRTOS),要不就是從 8 位元單晶片轉戰上來的。尤其後者轉戰到 32 位元 CPU + OS 時很容易出現不適應的症狀,因為他們很習慣直接跟硬體溝通,甚至常常他們的結論就是 OS 爛,綁手綁腳讓我很難做事。

但隨著 32bit SoC + Linux 幾乎一統天下的態勢,想要忽略作業系統幾乎是不可能的事,尤其是甩掉Linux 等於甩掉數百萬人辛勤工作的成果,前些日子跟一位老硬體工程師深談,他居然還有「重新發明輪子」的想法,不才小弟待過不少公司,親眼看過這些公司自己發明的輪子(OS)到最後都淡出了,因為相較於 Linux 這種有幾百萬甚至幾千萬使用者為基礎錘鍊出來的產品,光是穩定度就差一大截,而且以上面的功能多樣性,軟體多樣性,除了微軟這樣的公司外很少有公司能辦得到,更別說原本就是軟體弱國的台灣。如果台灣讓這樣的老人(不是指年紀老,而是思維)繼續掌權,也不能怪年輕人往外逃了。

sleep 與 system call


關於原 po 的問題,我只能說現在看書的人真的變少了,大家都用 Google 湊答案,湊到能動就好。因為原 po 的問題其實只要讀下面三本書就能找到答案:

這三本還有中譯本,翻譯的也不差,但小弟知道很多人懶得看書,這邊就給個速成解答。

sleep()


Linux sleep() 不只有一種,還有 usleep, nanosleep, select() 也可以充作 sleep。如果你去看 Qt source code,你會看到 QThread::msleep() 還用 pthread synchronize API 實作了另外一種方案。時間的複雜度讓 LSP、TLPI 都特別開闢章節專門講時間。而且 Linux 的時間來源有 5 個,你知道用的是哪一個嗎?

sleep 相關 system call 要關注什麼?以筆者的經驗來看,有兩樣事情特別需要關注:

  1. 精度
  2. signal 的影響

精度


在討論串中,有人以為 select() 能到微秒級(10^-6),那 nanosleep 不就有奈秒級(10^-9)?這些只是 system call 的參數規格,但理想與實際總是有差距,POSIX 定義了 clock_getres() 可以取得時間源的精度(LSP 11.3):

#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 真的這樣講...請趕快換供應商!

4 則留言:

  1. 謝謝你,大大很熱心;)

    回覆刪除
  2. 謝謝 釐清我原本錯誤的觀念

    回覆刪除
  3. 過了快一年,回頭看這篇文章,當初覺得自己怎麼這麼蠢問這個問題XD

    回覆刪除