linux可重新進入、非同步訊號安全和安全執行緒

來源:互聯網
上載者:User

標籤:指標   linux   lan   gets   proc   私人   總結   分離   集合   

一 可重新進入函數

當一個被捕獲的訊號被一個進程處理時,進程執行的普通的指令序列會被一個訊號處理器暫時地中斷。它首先執行該訊號處理常式中的指令。如果從訊號處理常式返回(例如沒有調用exit或longjmp),則繼續執行在捕獲到訊號時進程正在執行的正常指令序列(這和當一個硬體中斷髮生是所發生的事情相似。)但是在訊號處理器裡,我們並不知道當訊號被捕獲時進程正在執行哪裡的代碼。

如果進程正使用malloc在它的堆上分配額外的記憶體,而此時由於捕捉到訊號而插入執行該訊號處理常式,其中又調用了malloc,這會發生什麼呢?或者,如果進程正調用一個把結果儲存在一個靜態地區裡的函數到一半,比如 getpwnam,而我們在訊號處理器裡調用相同的函數,又會發生什麼呢?在malloc的例子裡,進程可能會遭到嚴重破壞,因為malloc通常維護它 所有分配過的地區的鏈表,而插入執行訊號處理常式時,進程可能正在更改此連結資料表。在getpwnam的例子裡,返回給普通調用者的資訊可能被返回給訊號處理器的資訊覆蓋。

SUS規定了必須保證是可以再入的函數。下表列出了這些再入函數:

 

一個訊號處理器可能調用的再入函數
accept fchmod lseek sendto stat
access fchown lstat setgid symlink
aio_error fcntl mkdir setpgid sysconf
aio_return fdatasync mkfifo setsid tcdrain
aio_suspend fork open setsockopt tcflow
alarm fpathconf pathconf setuid tcflush
bind fstat pause shutdown tcgetattr
cfgetispeed fsync pipe sigaction tcgetpgrp
cfgetospeed ftruncate poll sigaddset tcsendbreak
cfsetispeed getegid posix_trace_event sigdelset tcsetattr
cfsetospeed geteuid pselect sigemptyset tcsetpgrp
chdir getgid raise sigfillset time
chmod getgroups read sigismenber timer_getoverrun
chown getpeername readlink signal timer_gettime
clock_gettime getpgrp recv sigpause timer_settime
close getpid recvfrom sigpending times
connect getppid recvmsg sigprocmask umask
creat getsockname rename sigqueue uname
dup getsockopt rmdir sigset unlink
dup2 getuid select sigsuspend utime
execle kill sem_post sleep wait
execve link send socket waitpid
_Exit & _exit listen sendmsg socketpair write
 一個可重新進入的函數簡單來說就是可以被中斷的函數,也就是說,可以在這個函數執行的任何時刻中斷它,轉入OS 調度下去執行另外一段代碼,而返回控制時不會出現什麼錯誤。可重新進入(reentrant)函數可以由多於一個任務並發使用,而不必擔心資料錯誤。相反, 不可重新進入(non-reentrant)函數不能由超過一個任務所共用,除非能確保函數的互斥 (或者使用訊號量,或者在代碼的關鍵區段禁用中斷)。可重新進入函數可以在任意時刻被中斷, 稍後再繼續運行,不會遺失資料。可重新進入函數要麼使用本地變數,要麼在使用全域變數時 保護自己的資料。訊號安全,其實也就是非同步訊號安全,是說線程在訊號處理函數當中,不管以任何方式調用你的這個函數如果不死結不修改資料,那就是訊號安全的。因此,我認為可重新進入與非同步訊號安全是一個概念 。二 安全執行緒

安全執行緒:一個函數被稱為安全執行緒的,若且唯若被多個並發線程反覆的調用時,它會一直產生正確的結果。

有一類重要的安全執行緒函數,叫做可重新進入函數,其特點在於它們具有一種屬性:當它們被多個線程調用時,不會引用任何共用的資料。

儘管安全執行緒和可重新進入有時會( 不正確的 )被用做同義字,但是它們之間還是有清晰的技術差別的。可重新進入函數是安全執行緒函數的一個真子集。

三 可重新進入與安全執行緒的區別及聯絡

 

可重新進入函數:

重入即表示重複進入,首先它意味著這個函數可以被中斷,其次意味著它除了使用自己棧上的變數以外不依賴於任何環境(包括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 加鎖的話,同樣是上面的情況就會導致死結。對於這種情況,採用的方法一般是在特定的地區屏蔽一定的訊號

 

屏蔽訊號的方法:
1> signal(SIGPIPE, SIG_IGN); // 忽略一些訊號
2> sigprocmask()
sigprocmask 只為單線程定義的
3> pthread_sigmask()
pthread_sigmasks 可以在多線程中使用

現在看來訊號非同步安全和可重新進入的限制似乎是一樣的,所以這裡把它們等同看待;

安全執行緒:
      安全執行緒:如果一個函數在同一時刻可以被多個安全執行緒的調用,就稱該函數是安全執行緒的。 Malloc 函數是安全執行緒的。

不需要共用時,請為每個線程提供一個專用的資料副本。如果共用非常重要,則提供顯式同步,以確保程式以確定的方式操作。通過將過程包含在語句中來鎖定和解除鎖定互斥,可以使不安全過程變成安全執行緒過程,而且可以進行序列化。

很多函數並不是安全執行緒的,因為他們返回的資料是存放在靜態記憶體緩衝區中的。通過修改介面,由調用者自行提供緩衝區就可以使這些函數變為安全執行緒的。
作業系統實現支援安全執行緒函數的時候,會對POSIX.1 中的一些非安全執行緒的函數提供一些可替換的安全執行緒版本。
例如,gethostbyname() 是線程不安全的,在Linux 中提供了gethostbyname_r() 的安全執行緒實現。
函數名字後面加上"_r" ,以表明這個版本是可重新進入的(對於線程可重新進入,也就是說是安全執行緒的,但並不是說對於訊號處理函數也是可重新進入的,或者是非同步訊號安全的)。

多線程程式中常見的疏忽性問題
1> 將指標作為新線程的參數傳遞給調用方棧。
2> 在沒有同步機制保護的情況下訪問全域記憶體的共用可更改狀態。
3> 兩個線程嘗試輪流擷取對同一對全域資源的許可權時導致死結。其中一個線程式控制制第一種資源,另一個線程式控制制第二種資源。其中一個線程放棄之前,任何一個線程都無法繼續操作。
4> 嘗試重新擷取已持有的鎖(遞迴死結)。
5> 在同步保護中建立隱藏的間隔。如果受保護的程式碼片段包含的函數釋放了同步機制,而又在返回調用方之前重新擷取了該同步機制,則將在保護中出現此間隔。結果具有誤導性。對於調用方,表面上看全域資料已受到保護,而實際上未受到保護。
6> 將UNIX 訊號與線程混合時,使用sigwait(2) 模型來處理非同步訊號。
7> 調用setjmp(3C) 和longjmp(3C) ,然後長時間跳躍,而不釋放互斥鎖。
8> 從對*_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() 儘管通過加鎖的方式能保證安全執行緒,但是由於函數體對共用資源的訪問,因此是非可重新進入。

改寫函數庫

下面強調了將現存函數庫改寫為可重新進入和安全執行緒版本的主要步驟,只適用於C 語言的函數庫。

*識別出由函數庫匯出的所有全域變數。這些全域變數通常是在標頭檔中由export 關鍵字定義的。

匯出的全域變數應該被封裝起來。每個變數應該被設為函數庫所私人的( 通過static 關鍵字實現) ,然後建立全域變數的訪問函數來執行對全域變數的訪問。

* 識別出所有靜態變數和其他共用資源。靜態變數通常是由static 關鍵字定義的。

每個共用資源都應該與一個鎖關聯起來,鎖的粒度( 也就是鎖的數量) ,影響著函數庫的效能。為了初始化所有鎖,可能需要一個僅被調用一次的初始化函數。

* 識別所有非可重新進入函數,並將其轉化為可重新進入。參見函數可重新進入化

* 識別所有非安全執行緒函數,並將其轉化為安全執行緒。參見函數安全執行緒化。

使用非安全執行緒函數的解決方案

通過某種解決方案,非安全執行緒函數是可以被多個線程調用的。這在某些情況下或許是有用的,特別是當在多線程程式中使用一個非安全執行緒函數庫的時候——或者是出於測試的目的,或者是由於沒有相應的安全執行緒版本可用。這種解決方案會增加開銷,因為它需要將對某個或一組函數的調用進行序列化。

使用作用於整個函數庫的鎖,在每次使用該函數庫( 調用庫中的某個函數或是訪問庫中的全域變數) 時加鎖,如下面的虛擬碼所示:
              /* this is pseudo-code! */

             lock(library_lock);

              library_call();

              unlock(library_lock);

      

              lock(library_lock);

              x = library_var;

             unlock(library_lock);

      該解決方案有可能會造成效能瓶頸,因為在任意時刻,只有一個線程能任意的訪問或是用該庫。只有在該庫很少被使用的情況下,或是作為一種快速的實現方式,該方法才是可接受的。

使用作用於單個庫組件( 函數或是全域變數) 或是一組組件的鎖,如下面的虛擬碼所示
            /* this is pseudo-code! */

            lock(library_moduleA_lock);

            library_moduleA_call();

            unlock(library_moduleA_lock);

            lock(library_moduleB_lock);

            x = library_moduleB_var;

            unlock(library_moduleB_lock);

      這種方法與前者相比要複雜一些,但是能提高效能

    由於該類解決方式只應該在應用程式而不是函數庫中使用,可以使用互斥鎖(mutex) 來為整個庫加鎖。

linux可重新進入、非同步訊號安全和安全執行緒

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.