下文是在看csapp的時候引起的一些思考,其實以前看anup的時候也有所瞭解,不過期間有點長了,因此有點忘記了,當再次在csapp看到這部份內容的時候有了更多的理解。安全
可重入函數
當一個被捕獲的信號被一個進程處理時,進程執行的普通的指令序列會被一個信號處理器暫時地中斷。它首先執行該信號處理程序中的指令。若是從信號處理程序返回(例如沒有調用exit或longjmp),則繼續執行在捕獲到信號時進程正在執行的正常指令序列(這和當一個硬件中斷髮生時所發生的事情類似)。可是在信號處理器裏,咱們並不知道當信號被捕獲時進程正在執行哪裏的代碼。數據結構
若是進程正使用malloc在它的堆上分配額外的內存,而此時因爲捕捉到信號而插入執行該信號處理程序,其中又調用了malloc,這會發生什麼呢?或者,若是進程正調用一個把結果存儲在一個靜態區域裏的函數到一半,好比 getpwnam,而咱們在信號處理器裏調用相同的函數,又會發生什麼呢?在malloc的例子裏,進程可能會遭到嚴重破壞,由於malloc一般維護它 全部分配過的區域的鏈表,而插入執行信號處理程序時,進程可能正在更改此連接表。多線程
在getpwnam的例子裏,返回給普通調用者的信息可能被返回給信號處理器的信息覆蓋。併發
SUS規定了必須保證是能夠再入的函數。下表列出了這些再入函數:app
一個可重入的函數簡單來講就是能夠被中斷的函數,也就是說,能夠在這個函數執行的任什麼時候刻中斷它,轉入OS 調度下去執行另一段代碼,而返回控制時不會出現什麼錯誤。可重入(reentrant)函數能夠由多於一個任務併發使用,而沒必要擔憂數據錯誤。相反, 不可重入(non-reentrant)函數不能由超過一個任務所共享,除非能確保函數的互斥 (或者使用信號量,或者在代碼的關鍵部分禁用中斷)。less
可重入函數能夠在任意時刻被中斷, 稍後再繼續運行,不會丟失數據。可重入函數要麼使用本地變量,要麼在使用全局變量時 保護本身的數據。異步
信號安全,其實也就是異步信號安全,是說線程在信號處理函數當中,無論以任何方式調用你的這個函數若是不死鎖不修改數據,那就是信號安全的。所以,我認爲可重入與異步信號安全是一個概念 。async
線程安全
線程安全:一個函數被稱爲線程安全的,當且僅當被多個併發線程反覆的調用時,它會一直產生正確的結果。函數
有一類重要的線程安全函數,叫作可重入函數,其特色在於它們具備一種屬性:當它們被多個線程調用時,不會引用任何共享的數據。ui
儘管線程安全和可重入有時會( 不正確的 )被用作同義詞,可是它們之間仍是有清晰的技術差異的。可重入函數是線程安全函數的一個真子集。
可重入與線程安全的區別及聯繫
可重入函數:重入即表示重複進入,首先它意味着這個函數能夠被中斷,其次意味着它除了使用本身棧上的變量之外不依賴於任何環境(包括static ),這樣的函數就是purecode (純代碼)可重入,能夠容許有該函數的多個副本在運行,因爲它們使用的是分離的棧,因此不會互相干擾。
可重入函數是線程安全函數,可是反過來,線程安全函數未必是可重入函數。
實際上,可重入函數不多,APUE 10.6 節中描述了Single UNIX Specification 說明的可重入的函數,只有115 個;APUE 12.5 節中描述了POSIX.1 中不能保證線程安全的函數,只有89 個。
信號就像硬件中斷同樣,會打斷正在執行的指令序列。信號處理函數沒法判斷捕獲到信號的時候,進程在何處運行。若是信號處理函數中的操做與打斷的函數的操做相同,並且這個操做中有靜態數據結構等,當信號處理函數返回的時候(固然這裏討論的是信號處理函數能夠返回),恢復原先的執行序列,可能會致使信號處理函數中的操做覆蓋了以前正常操做中的數據。
不可重入的幾種狀況
使用靜態數據結構,好比getpwnam,getpwuid:若是信號發生時正在執行getpwnam,信號處理程序中執行getpwnam可能覆蓋原來getpwnam獲取的舊值
- 調用malloc或free:若是信號發生時正在malloc(修改堆上存儲空間的連接表),信號處理程序又調用malloc,會破壞內核的數據結構
- 使用標準IO函數,由於好多標準IO的實現都使用全局數據結構,好比printf(文件偏移是全局的)
- 函數中調用longjmp或siglongjmp:信號發生時程序正在修改一個數據結構,處理程序返回到另一處,致使數據被部分更新。
即便對於可重入函數,在信號處理函數中使用也須要注意一個問題就是errno 。一個線程中只有一個errno 變量,信號處理函數中使用的可重入函數也有可能 會修改errno 。例如,read 函數是可重入的,可是它也有可能會修改errno 。所以,正確的作法是在信號處理函數開始,先保存errno ;在信號處 理函數退出的時候,再恢復errno 。
例如,程序正在調用printf 輸出,可是在調用printf 時,出現了信號,對應的信號處理函數也有printf 語句,就會致使兩個printf 的輸出混雜在一塊兒。
若是是給printf 加鎖的話,一樣是上面的狀況就會致使死鎖。對於這種狀況,採用的方法通常是在特定的區域屏蔽必定的信號。
屏蔽信號的方法:
signal(SIGPIPE, SIG_IGN); // 忽略一些信號 sigprocmask();// sigprocmask 只爲單線程定義的 pthread_sigmask(); // pthread_sigmasks 能夠在多線程中使用
如今看來信號異步安全和可重入的限制彷佛是同樣的,因此這裏把它們等同看待;
線程安全:若是一個函數在同一時刻能夠被多個線程安全的調用,就稱該函數是線程安全的。Malloc 函數是線程安全的。
不須要共享時,請爲每一個線程提供一個專用的數據副本。若是共享很是重要,則提供顯式同步,以確保程序以肯定的方式操做。經過將過程包含在語句中來鎖定和解除鎖定互斥,可使不安全過程變成線程安全過程,並且能夠進行串行化。
不少函數並非線程安全的,由於他們返回的數據是存放在靜態的內存緩衝區中的。經過修改接口,由調用者自行提供緩衝區就可使這些函數變爲線程安全的。
操做系統實現支持線程安全函數的時候,會對POSIX.1 中的一些非線程安全的函數提供一些可替換的線程安全版本。
例如,gethostbyname() 是線程不安全的,在Linux 中提供了gethostbyname_r() 的線程安全實現。
函數名字後面加上 _r ,以代表這個版本是可重入的(對於線程可重入,也就是說是線程安全的,但並非說對於信號處理函數也是可重入的,或者是異步信號安全的)。
多線程程序中常見的疏忽性問題:
- 將指針做爲新線程的參數傳遞給調用方棧。
- 在沒有同步機制保護的狀況下訪問全局內存的共享可更改狀態。
- 兩個線程嘗試輪流獲取對同一對全局資源的權限時致使死鎖。其中一個線程控制第一種資源,另外一個線程控制第二種資源。其中一個線程放棄以前,任何一個線程都沒法繼續操做。
- 嘗試從新獲取已持有的鎖(遞歸死鎖)。
- 在同步保護中建立隱藏的間隔。若是受保護的代碼段包含的函數釋放了同步機制,而又在返回調用方以前從新獲取了該同步機制,則將在保護中出現此間隔。結果具備誤導性。對於調用方,表面上看全局數據已受到保護,而實際上未受到保護。
- 將UNIX 信號與線程混合時,使用sigwait(2) 模型來處理異步信號。
- 調用setjmp(3C) 和longjmp(3C) ,而後長時間跳躍,而不釋放互斥鎖。
- 從對_cond_wait() 或 _cond_timedwait() 的調用中返回後沒法從新評估條件。
總結
- 判斷一個函數是否是可重入函數,在於判斷其可否能夠被打斷,打斷後恢復運行可以獲得正確的結果。(打斷執行的指令序列並不改變函數的數據)
- 判斷一個函數是否是線程安全的,在於判斷其可否在多個線程同時執行其指令序列的時候,保證每一個線程都可以獲得正確的結果。
- 若是一個函數對多個線程來講是可重入的,則說這個函數是線程安全的,但這並不能說明對信號處理程序來講該函數也是可重入的。
- 若是函數對異步信號處理程序的重入是安全的,那 麼就能夠說函數是」 異步-信號安全 」 的。
可重入與線程安全是兩個獨立的概念, 都與函數處理資源的方式有關。
首先,可重入和線程安全是兩個並不等同的概念,一個函數能夠是可重入的,也能夠是線程安全的,能夠二者均知足,能夠二者皆不知足( 該描述嚴格的說存在漏洞,參見第二條) 。
其次,從集合和邏輯的角度看,可重入是線程安全的子集,可重入是線程安全的充分非必要條件。可重入的函數必定是線程安全的,然過來則不成立。
第三,POSIX 中對可重入和線程安全這兩個概念的定義:
- Reentrant Function :A function whose effect, when called by two or more threads,is guaranteed to be as if the threads each executed thefunction one after another in an undefined order, even ifthe actual execution is interleaved.
- Thread-Safe Function :A function that may be safely invoked concurrently by multiple threads.
- Async-Signal-Safe Function :A function that may be invoked, without restriction fromsignal-catching functions. No function is async-signal -safe unless explicitly described as such
以上三者的關係爲:可重入函數 必然 是 線程安全函數 和 異步信號安全函數;線程安全函數不必定是可重入函數。
可重入與線程安全的區別體如今可否在signal 處理函數中被調用的問題上, 可重入函數在signal 處理函數中能夠被安全調用,所以同時也是 Async-Signal-Safe Function ;而線程安全函數不保證能夠在signal 處理函數中被安全調用,若是經過設置信號阻塞集合等方法保證一個非可重入函數不被信號中斷,那麼它也是Async-Signal-Safe Function。
值得一提的是POSIX 1003.1 的 System Interface 缺省是 Thread-Safe 的,但不是Async-Signal-Safe 的。Async-Signal-Safe 的須要明確表示,好比fork () 和signal() 。
一個非可重入函數一般( 儘管不是全部狀況下) 由它的外部接口和使用方法便可進行判斷。例如:strtok() 是非可重入的,由於它在內部存儲了被標記分割的字符串;ctime() 函數也是非可重入的,它返回一個指向靜態數據的指針,而該靜態數據在每次調用中都被覆蓋重寫。
一個線程安全的函數經過加鎖的方式來實現多線程對共享數據的安全訪問。線程安全這個概念,只與函數的內部實現有關,而不影響函數的外部接口。在 C 語言中,局部變量是在棧上分配的。所以,任何未使用靜態數據或其餘共享資源的函數都是線程安全的。
目前的 AIX 版本中,如下函數庫是線程安全的:
- C 標準函數庫
- 與BSD 兼容的函數庫
使用全局變量( 的函數) 是非線程安全的。這樣的信息應該以線程爲單位進行存儲,這樣對數據的訪問就能夠串行化。一個線程可能會讀取由另一個線程生成的錯誤代碼。在AIX 中,每一個線程有獨立的errno 變量。
最後讓咱們來構想一個線程安全但不可重入的函數:
假設函數func() 在執行過程當中須要訪問某個共享資源,所以爲了實現線程安全,在使用該資源前加鎖,在不須要資源解鎖。
假設該函數在某次執行過程當中,在已經得到資源鎖以後,有異步信號發生,程序的執行流轉交給對應的信號處理函數;再假設在該信號處理函數中也須要調用函數 func() ,那麼func() 在此次執行中仍會在訪問共享資源前試圖得到資源鎖,然而咱們知道前一個func() 實例已然得到該鎖,所以信號處理函數阻塞——另外一方面,信號處理函數結束前被信號中斷的線程是沒法恢復執行的,固然也沒有釋放資源的機會,這樣就出現了線程和信號處理函數之間的死鎖局面。
所以,func() 儘管經過加鎖的方式能保證線程安全,可是因爲函數體對共享資源的訪問,所以是非可重入。