這周 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 時也必須留意更多細節,以免未來踩到更多地雷……。