從 inet_ntoa 看 thread safe 的 API

這周 refactor 一段 code 之後,被同事說:「為什麼 log 的來源 IP 跟目的地 IP 總是一樣呢?」

char *szDestIp = inet_ntoa(destAddr);
char *szSrcIp = inet_ntoa(srcAddr);

後來仔細想了一下 inet_ntoa 的 prototype,我就明白了!如果這個 function 不必特意去 free 回傳的 pointer,那可能在內部有一個 static buffer 去保存這個值。果不期然,FreeBSD 的原始碼是這樣實作的

/*const*/ char *
inet_ntoa(struct in_addr in) {
        static char ret[18];

        strcpy(ret, "[inet_ntoa error]");
        (void) inet_ntop(AF_INET, &in, ret, sizeof ret);
        return (ret);
}

char *
inet_ntoa_r(struct in_addr in, char *buf, socklen_t size)
{
        (void) inet_ntop(AF_INET, &in, buf, size);
        return (buf);
}

到這邊我們大概就知道,inet_ntoa 並不是一個 thread safe 的 function,你可以用下面這段程式測一下會發生什麼事情。

void* thread_func (void *param)
{
    char * addr= (char *)param;
    struct in_addr val;
    inet_aton(addr, &val);

    while (1)
        printf("%s => %s\n", addr, inet_ntoa(val));

    pthread_exit(NULL);
}

int main()
{
    pthread_t thr1, thr2;
    pthread_create(&thr1, NULL, thread_func, (void *)"127.0.0.1");
    pthread_create(&thr2, NULL, thread_func, (void *)"127.0.0.2");

    while(1) {
        sleep (1);
    }

    return 0;
}

輸出大概會像

127.0.0.2 => 127.0.0.1
127.0.0.1 => 127.0.0.2
127.0.0.1 => [inet_ntoa error]
127.0.0.2 => 127.0.0.2

所以,直接使用 inet_ntoa 在 reentrant 上可能會發生問題,如果你必須面對這些問題,最好的方式就是使用 inet_ntoa_r 這個 reentrant 的版本,或者直接使用 inet_ntop。

而這難道非得要踩到地雷,或者從 API 的細節才能窺知這一切嗎?其實並不完全是,當你發現回傳的是一個 char *,又沒有要求你特別 free 他,你是不是就該猜測他是回傳 function 內部的一個 static buffer 了呢?所以,舉凡 ctime, asctime 都不保證是 thread safe 的,這是我們 programmer 該留意的細節。

所以,也許我們以後需要更小心的去寫程式,在選擇 API 時也必須留意更多細節,以免未來踩到更多地雷……。