Linux:理解阻塞信號與函數重入


信號的阻塞

指一個信號的遞達,信號依然可以註冊,只是暫時不處理(未決狀態)直到進程解除對此信號的阻塞,才執行遞達的動作.

在內核中的表示

實際執行信號的處理動作稱爲信號遞達(Delivery ),信號從產生到遞達之間的狀態,稱爲信號未決(Pending)。進程可以選擇阻塞(Block)某個信號,被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。

注意:

阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之後可選的一種處理動作。信號在內核中的表示可以看作是這樣的:

在這裏插入圖片描述

每個信號都有兩個標誌位分別表示阻塞和未決,還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標誌,直到信號遞達才清除該標誌。在上圖的例子中,

  1. SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。

  2. SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因爲進程仍有機會改變處理動作之後再解除阻塞。

  3. SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。

未決和阻塞標誌可以用相同的數據類型sigset_t來存儲,sigset_t稱爲信號集,這個類型可以表示每個信號的“有效”或“無效”狀態,,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處於未決狀態。阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這裏的“屏蔽”應該理解爲阻塞而不是忽略。

信號集處理函數

sigset_t類型(64bit)對於每種信號用一個bit表示“有效”或“無效”狀態。

#include <signal.h>

int sigemptyset(sigset_t *set) // 清空set信號集合 - 使用一個變量的時候的初始化過程
int sigfillset(sigset_t *set) //將所有信號添加到set集合中
int sigaddset(sigset_t *set, int signum) // 向set集合中添加指定的信號
int sigdelset(siget_t *set, int signum) // 從set集合中移除指定的信號
int sigismember(const sigset_t set, int signum) // 判斷制定信號是否在set集合中

注意:

  • 在使用sigset_t類型的變量之前,一定要調用sigemptyset或sigfillset做初始化,使信號集處於確定的狀態。
  • 上面四個函數都是成功返回0,出錯返回-1。sigismember是一個布爾函數,用於判斷一個信號集的有效信號中是否包含某種信號,若包含則返回1,不包含則返回0,出錯返回-1。

sigprocmask函數

int sigprocmask(int how, sigset_t *set, sigset_t *old);

參數:

如果old是非空指針,則讀取進程的當前信號屏蔽字通過old參數傳出。如果set是非空指針,則更改進程的信號屏蔽字,參數how指示如何更改。如果old和set都是非空指針,則先將原來的信號屏蔽字備份到old裏,然後根據set和how參數更改信號屏蔽字。

  • how:當前要對block集合進行的操作
    • SIG_BLOCK 將set集合中的信號添加到block進程阻塞信號集合
      block = block | set
      表示阻塞 set集合中的信號以及原有的阻塞信號,並且將原有的阻塞信號返回到old集合中(便於還原)
    • SIG_UNBLOCK 將set集合中的信號從block集合中移除,將set集合中的信號解除阻塞
      block = block & (~set)
    • SIG_SETMASK 將內核中的block集合中的信號設置爲set集合中的信號
      block = set

返回值:若成功則爲0,若出錯則爲-1

sigpending函數

#include <signal.h>

int sigpending(sigset_t *set);

sigpending讀取當前進程的未決信號集,通過set參數傳出。調用成功則返回0,出錯則返回-1。

在所有信號中,有兩個信號不可被阻塞,不可被自定義修改處理方式,也不可被忽略
這兩個信號:SIGKILL-9 / SIGSTOP-19

簡介信號 SIGPIPE、SIGCHLD

  1. SIGPIPE信號

管道的博文中,介紹到了:

所有管道讀端被關閉,則繼續寫入會觸發異常
此時的異常調用的就是SIGPIPE(Mac終端下是PIPE)

  1. SIGCHLD信號:

信號產生:

殭屍進程:子進程退出後,操作系統發送SIGCHLD信號給父進程,但是因爲SIGCHLD信號的默認處理方式就是忽略,因此在之前的程序中並沒有感受到操作系統的通知
因此只能固定的使用進程等待來避免產生殭屍進程,但是在這個過程中父進程是一直阻塞的,只能一直等待子進程退出。

如何讓程序感知到操作系統的通知??

就在程序初始化階段,將SIGCHLD信號的處理方式自定義,並且在自定義函數重調用waitpid,這樣的話就當子進程退出的時候,則自動回調處理了,父進程就不需要一直等待了。

多個子進程同時退出,都會向父進程發送SIGCHLD信號,但是SIGCHLD信號是非可靠信號,有可能會丟失事件,

例如:

三(多)個進程同時退出,但是信號只註冊了一次
意味着只會執行一次回調函數,調用一次waitpid,只能處理一個殭屍進程
非可靠信號的丟失是無法避免的,因此只能在一次信號回調中處理完所有的殭屍進程

解決方法:

while(waitpid(-1, NULL, WNOHANG)>0);  非阻塞循環在一個回調中將所有的殭屍進程全部處理
  • waitpid返回值: >0 -退出子進程的pid / ==0 -沒有子進程退出 / <0 -出錯

  • 循環是爲了若有子進程退出則一直處理,直到沒有子進程退出,則退出循環信號回調完畢

  • WNOHANG-將waitpid設置爲非阻塞,沒有子進程退出的時候返回0,退出循環,不要導致程序流程卡在信號回調函數中

volatile 關鍵字

修飾一個變量,使這個變量保持內存可見性—每次對變量訪問(cpu處理數據)都需要重新從內存加載變量的數據,防止編譯器過度優化(gcc -O0/1/2/3)

  • cpu處理一個數據的過程時從內存中將數據加載到寄存器上進行處理
  • gcc編譯器,在編譯程序的時候,如果使用了代碼優化 -Olevel 選項,發現某個變量使用頻率非常高,爲了提高效率,則直接將變量的值設置爲某個寄存器的值,以後訪問的時候直接從寄存器訪問,則減少了內存訪問的過程提高了效率。
  • 但是這種優化有時候會造成代碼的邏輯混亂

因此使用volatile關鍵字修飾變量,讓cpu無論如何每次都重新倒內存中獲取數據

可重入函數

競態條件: 在多個執行流中,程序的競爭執行

函數的重入

同一個函數被不同的執行流調用,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之爲重入。

函數的可重入與不可重入區別:

  • 函數的可重入:在函數重入之後,不會造成數據二義或者引起程序邏輯混亂,則這個函數是一個可重入函數
  • 函數的不可重入:在函數重入之後,有可能會造成數據二義或者額引起程序邏輯混亂,這個函數是一個不可重入函數

函數是否可重入的判定基準:

  • 一個函數中是否對全局數據進行了非原子性的操作(若有則不可重入)
  • 原子性:操作要不然一次性完成,要不然就不做(操作不可被打斷)

可重入函數:

  • 一個函數若根本就沒有操作全局數據(或者靜態數據),則肯定是可重入的,因爲每個函數調用的時候都有獨立的函數棧
  • 一個函數若對全局數據進行了操作,但是操作時原子性的,則也是可重入的

當我們以後在執行多個執行流中使用別人的函數的時候(包括自己封裝實現函數的時候),就需要考慮這個函數是否重入,所有可能給程序帶來的不確定性。

不可重入函數:

  • malloc/free 都是不可重入函數,因爲malloc也是用全局鏈表來管理堆的。因此在多個執行流中進行操作應該小心
  • 調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構。
  • 可重入函數體內使用了靜態的數據結構

可重入與線程安全聯繫

  • 函數是可重入的,那就是線程安全的
  • 函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題
  • 如果一個函數中有全局變量,那麼這個函數既不是線程安全也不是可重入的。

可重入與線程安全區別

  • 可重入函數是線程安全函數的一種
  • 線程安全不一定是可重入的,而可重入函數則一定是線程安全的。
  • 如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數若鎖還未釋放則會產生死鎖,因此是不可重入的。

如果本篇博文有幫助到您,請留個贊激勵博主吶~~

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