errno 的多線程問題

大多數系統調用都遵循這一過程,errno 是一個整數,可以用 perror 或 strerror 獲得對應的文字描述信息。

不過,也有幾個特殊的系統調用,和上述使用方法存在些許差異。比如,其中有個函數會在調用之前將 errno 重置爲 0,調用後,通過檢查 errno 判斷執行是否成功。此類函數只有非常少數的幾個,使用之前,看看幫助頁,就知道如何使用了。

系統調用的使用規範就介紹到這裏。此時,你可能有個疑問,每個系統調用失敗後都會設置 errno,如果在多線程程序中,不同線程中的系統調用設置的 errno 會不會互相干擾呢?

如果 errno 是一個全局變量,答案是肯定的。如果真是這樣的話,那系統調用的侷限性也就太大了,總不能在每個系統調用之前都加鎖保護吧。優秀的 Linux 肯定不會這麼弱,那麼,這個 errno 的問題又是怎麼解決的呢?

根據 man 手冊,要使用 errno,首先需要包含 errno.h 這個頭文件。我們先看看 errno.h 裏面有什麼東西。

vim /usr/include/errno.h

執行以上代碼,會發現該文件中有這樣幾行關鍵內容:

#include <bits/errno.h>
.......
#ifndef errno
extern int errno;
#endif

根據官方提供的代碼註釋,bits/errno.h 中應該有一個 errno 的宏定義。如果沒有,則會在外部變量中尋找一個名爲 errno 的整數,它自然也就成了全局整數。否則,這個 errno 只是一個 per-thread 變量,每個線程都會獨立拷貝一份,所以在多線程程序中使用它是不會相互影響的。

實現原理

在its/errno.h 中

<bits/errno.h>
# ifndef __ASSEMBLER__
/* Function to get address of global `errno' variable.  */
extern int *__errno_location (void) __THROW __attribute__ ((__const__));

#  if !defined _LIBC || defined _LIBC_REENTRANT
/* When using threads, errno is a per-thread value.  */
#   define errno (*__errno_location ())
#  endif
# endif /* !__ASSEMBLER__ */
#endif /* _ERRNO_H */

原來,當 libc 被定義爲可重入時,errno 就會被定義成一個宏,該宏調用外部 __errno_location 函數返回的內存地址中所存儲的值。在 GCC 源碼中,我們還發現一個測試用例中定義了 __errno_location 函數的 Stub,是這樣寫的:

extern __thread int __libc_errno __attribute__ ((tls_model ("initial-exec")));
int * __errno_location (void)
{
  return &__libc_errno;
}

這一簡單的測試用例充分展現了 errno 的實現原理。errno 被定義爲 per-thread(用 __thread 標識的線程局部存儲類型)變量 __libc_errno,之後 __errno_location 函數返回了這個線程局部變量的地址。所以,在每個線程中獲取和設置 errno 的時候,操作的是本線程內的一個變量,不會與其他線程相互干擾。

至於 __thread 這個關鍵字,需要在很“嚴苛”的條件下才能生效——需要 Linux 2.6 以上內核、pthreads 庫、GCC 3.3 或更高版本的支持。不過,放到今天,這些條件已成爲標配,也就不算什麼了。

參考:

https://app9gmhpq4e1680.pc.xiaoe-tech.com/detail/i_5d65f90c372f9_W1jIKFd4/1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章