在開發多線程應用時,開發人員一般都會考慮線程安全,會使用
pthread_mutex
去保護全局變量。如果應用中使用了信號,而且信號的產生不是因為程序運行出錯,而是程序邏輯需要,譬如 SIGUSR1、SIGRTMIN 等,信號在被處理后應用程序還將正常運行。在編寫這類信號處理函數時,應用層面的開發人員卻往往忽略了信號處理函數執行的上下文背景,沒有考慮編寫安全的信號處理函數的一些規則。本文首先介紹編寫信號處理函數時需要考慮的一些規則;然后舉例說明在多線程應用中如何構建模型讓因為程序邏輯需要而產生的異步信號在指定的線程中以同步的方式處理。
Linux 多線程應用中,每個線程可以通過調用
pthread_sigmask()
設置本線程的信號掩碼。一般情況下,被阻塞的信號將不能中斷此線程的執行,除非此信號的產生是因為程序運行出錯如 SIGSEGV;另外不能被忽略處理的信號 SIGKILL 和 SIGSTOP 也無法被阻塞。
當一個線程調用
pthread_create()
創建新的線程時,此線程的信號掩碼會被新創建的線程繼承。
POSIX.1 標準定義了一系列線程函數的接口,即 POSIX threads(Pthreads)。Linux C 庫提供了兩種關于線程的實現:LinuxThreads 和 NPTL(Native POSIX Threads Library)。LinuxThreads 已經過時,一些函數的實現不遵循POSIX.1 規范。NPTL 依賴 Linux 2.6 內核,更加遵循 POSIX..1 規范,但也不是完全遵循。
基于 NPTL 的線程庫,多線程應用中的每個線程有自己獨特的線程 ID,并共享同一個進程ID。應用程序可以通過調用
kill(getpid(),signo)
將信號發送到進程,如果進程中當前正在執行的線程沒有阻礙此信號,則會被中斷,線號處理函數會在此線程的上下文背景中執行。應用程序也可以通過調用
pthread_kill(pthread_t thread, int sig)
將信號發送給指定的線程,則線號處理函數會在此指定線程的上下文背景中執行。
基于 LinuxThreads 的線程庫,多線程應用中的每個線程擁有自己獨特的進程 ID,
getpid()
在不同的線程中調用會返回不同的值,所以無法通過調用
kill(getpid(),signo)
將信號發送到整個進程。
下文介紹的在指定的線程中以同步的方式處理異步信號是基于使用了 NPTL 的 Linux C 庫。請參考“ Linux 線程模型的比較:LinuxThreads 和 NPTL ”和“ pthreads(7) - Linux man page ”進一步了解 Linux 的線程模型,以及不同版本的 Linux C 庫對 NPTL 的支持。
信號的產生可以是:
- 用戶從控制終端終止程序運行,如 Ctrk + C 產生 SIGINT;
- 程序運行出錯時由硬件產生信號,如訪問非法地址產生 SIGSEGV;
-
程序運行邏輯需要,如調用
kill
、raise
產生信號。
因為信號是異步事件,即信號處理函數執行的上下文背景是不確定的,譬如一個線程在調用某個庫函數時可能會被信號中斷,庫函數提前出錯返回,轉而去執行信號處理函數。對于上述第三種信號的產生,信號在產生、處理后,應用程序不會終止,還是會繼續正常運行,在編寫此類信號處理函數時尤其需要小心,以免破壞應用程序的正常運行。關于編寫安全的信號處理函數主要有以下一些規則:
- 信號處理函數盡量只執行簡單的操作,譬如只是設置一個外部變量,其它復雜的操作留在信號處理函數之外執行;
-
errno
是線程安全,即每個線程有自己的errno
,但不是異步信號安全。如果信號處理函數比較復雜,且調用了可能會改變errno
值的庫函數,必須考慮在信號處理函數開始時保存、結束的時候恢復被中斷線程的errno
值;
-
信號處理函數只能調用可以重入的 C 庫函數;譬如不能調用
malloc(),free()
以及標準 I/O 庫函數等; -
信號處理函數如果需要訪問全局變量,在定義此全局變量時須將其聲明為
volatile,
以避免編譯器不恰當的優化。
從整個 Linux 應用的角度出發,因為應用中使用了異步信號,程序中一些庫函數在調用時可能被異步信號中斷,此時必須根據
errno
的值考慮這些庫函數調用被信號中斷后的出錯恢復處理,譬如socket 編程中的讀操作:
rlen = recv(sock_fd, buf, len, MSG_WAITALL); if ((rlen == -1) && (errno == EINTR)){ // this kind of error is recoverable, we can set the offset change //‘rlen’ as 0 and continue to recv } |
如上文所述,不僅編寫安全的異步信號處理函數本身有很多的規則束縛;應用中其它地方在調用可被信號中斷的庫函數時還需考慮被中斷后的出錯恢復處理。這讓程序的編寫變得復雜,幸運的是,POSIX.1 規范定義了
sigwait()、 sigwaitinfo()
和
pthread_sigmask()
等接口,可以實現:
sigwait
- 以同步的方式處理異步信號;
- 在指定的線程中處理信號。
這種在指定的線程中以同步方式處理信號的模型可以避免因為處理異步信號而給程序運行帶來的不確定性和潛在危險。
sigwait()
提供了一種等待信號的到來,以串行的方式從信號隊列中取出信號進行處理的機制。
sigwait(
)只等待函數參數中指定的信號集,即如果新產生的信號不在指定的信號集內,則
sigwait()
繼續等待。對于一個穩定可靠的程序,我們一般會有一些疑問:
- 多個相同的信號可不可以在信號隊列中排隊?
- 如果信號隊列中有多個信號在等待,在信號處理時有沒有優先級規則?
- 實時信號和非實時信號在處理時有沒有什么區別?
筆者寫了一小段測試程序來測試
sigwait
在信號處理時的一些規則。
#include <signal.h> #include <errno.h> #include <pthread.h> #include <unistd.h> #include <sys/types.h> void sig_handler(int signum) { printf("Receive signal. %d/n", signum); } void* sigmgr_thread() { sigset_t waitset, oset; int sig; int rc; pthread_t ppid = pthread_self(); pthread_detach(ppid); sigemptyset(&waitset); sigaddset(&waitset, SIGRTMIN); sigaddset(&waitset, SIGRTMIN+2); sigaddset(&waitset, SIGRTMAX); sigaddset(&waitset, SIGUSR1); sigaddset(&waitset, SIGUSR2); while (1) { rc = sigwait(&waitset, &sig); if (rc != -1) { sig_handler(sig); } else { printf("sigwaitinfo() returned err: %d; %s/n", errno, strerror(errno)); } } } int main() { sigset_t bset, oset; int i; pid_t pid = getpid(); pthread_t ppid; sigemptyset(&bset); sigaddset(&bset, SIGRTMIN); sigaddset(&bset, SIGRTMIN+2); sigaddset(&bset, SIGRTMAX); sigaddset(&bset, SIGUSR1); sigaddset(&bset, SIGUSR2); if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0) printf("!! Set pthread mask failed/n"); kill(pid, SIGRTMAX); kill(pid, SIGRTMAX); kill(pid, SIGRTMIN+2); kill(pid, SIGRTMIN); kill(pid, SIGRTMIN+2); kill(pid, SIGRTMIN); kill(pid, SIGUSR2); kill(pid, SIGUSR2); kill(pid, SIGUSR1); kill(pid, SIGUSR1); // Create the dedicated thread sigmgr_thread() which will handle signals synchronously pthread_create(&ppid, NULL, sigmgr_thread, NULL); sleep(10); exit (0); } |
程序編譯運行在 RHEL4 的結果如下:
從以上測試程序發現以下規則:
- 對于非實時信號,相同信號不能在信號隊列中排隊;對于實時信號,相同信號可以在信號隊列中排隊。
- 如果信號隊列中有多個實時以及非實時信號排隊,實時信號并不會先于非實時信號被取出,信號數字小的會先被取出:如 SIGUSR1(10)會先于 SIGUSR2 (12),SIGRTMIN(34)會先于 SIGRTMAX (64), 非實時信號因為其信號數字小而先于實時信號被取出。
sigwaitinfo()
以及
sigtimedwait()
也提供了與
sigwait()
函數相似的功能。
在基于 Linux 的多線程應用中,對于因為程序邏輯需要而產生的信號,可考慮調用
sigwait()
使用同步模型進行處理。其程序流程如下:
- 主線程設置信號掩碼,阻礙希望同步處理的信號;主線程的信號掩碼會被其創建的線程繼承;
-
主線程創建信號處理線程;信號處理線程將希望同步處理的信號集設為
sigwait()
的第一個參數。 - 主線程創建工作線程。
以下為一個完整的在指定的線程中以同步的方式處理異步信號的程序。
主線程設置信號掩碼阻礙 SIGUSR1 和 SIGRTMIN 兩個信號,然后創建信號處理線程
sigmgr_thread()
和五個工作線程
worker_thread()
。主線程每隔10秒調用
kill()
對本進程發送 SIGUSR1 和 SIGTRMIN 信號。信號處理線程
sigmgr_thread()
在接收到信號時會調用信號處理函數
sig_handler()
。
程序編譯:
gcc -o signal_sync signal_sync.c -lpthread
程序執行:
./signal_sync
從程序執行輸出結果可以看到主線程發出的所有信號都被指定的信號處理線程接收到,并以同步的方式處理。
#include <signal.h> #include <errno.h> #include <pthread.h> #include <unistd.h> #include <sys/types.h> void sig_handler(int signum) { static int j = 0; static int k = 0; pthread_t sig_ppid = pthread_self(); // used to show which thread the signal is handled in. if (signum == SIGUSR1) { printf("thread %d, receive SIGUSR1 No. %d/n", sig_ppid, j); j++; //SIGRTMIN should not be considered constants from userland, //there is compile error when use switch case } else if (signum == SIGRTMIN) { printf("thread %d, receive SIGRTMIN No. %d/n", sig_ppid, k); k++; } } void* worker_thread() { pthread_t ppid = pthread_self(); pthread_detach(ppid); while (1) { printf("I'm thread %d, I'm alive/n", ppid); sleep(10); } } void* sigmgr_thread() { sigset_t waitset, oset; siginfo_t info; int rc; pthread_t ppid = pthread_self(); pthread_detach(ppid); sigemptyset(&waitset); sigaddset(&waitset, SIGRTMIN); sigaddset(&waitset, SIGUSR1); while (1) { rc = sigwaitinfo(&waitset, &info); if (rc != -1) { printf("sigwaitinfo() fetch the signal - %d/n", rc); sig_handler(info.si_signo); } else { printf("sigwaitinfo() returned err: %d; %s/n", errno, strerror(errno)); } } } int main() { sigset_t bset, oset; int i; pid_t pid = getpid(); pthread_t ppid; // Block SIGRTMIN and SIGUSR1 which will be handled in //dedicated thread sigmgr_thread() // Newly created threads will inherit the pthread mask from its creator sigemptyset(&bset); sigaddset(&bset, SIGRTMIN); sigaddset(&bset, SIGUSR1); if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0) printf("!! Set pthread mask failed/n"); // Create the dedicated thread sigmgr_thread() which will handle // SIGUSR1 and SIGRTMIN synchronously pthread_create(&ppid, NULL, sigmgr_thread, NULL); // Create 5 worker threads, which will inherit the thread mask of // the creator main thread for (i = 0; i < 5; i++) { pthread_create(&ppid, NULL, worker_thread, NULL); } // send out 50 SIGUSR1 and SIGRTMIN signals for (i = 0; i < 50; i++) { kill(pid, SIGUSR1); printf("main thread, send SIGUSR1 No. %d/n", i); kill(pid, SIGRTMIN); printf("main thread, send SIGRTMIN No. %d/n", i); sleep(10); } exit (0); } |
在基于 Linux 的多線程應用中,對于因為程序邏輯需要而產生的信號,可考慮使用同步模型進行處理;而對會導致程序運行終止的信號如 SIGSEGV 等,必須按照傳統的異步方式使用
signal()
、
sigaction()
注冊信號處理函數進行處理。這兩種信號處理模型可根據所處理的信號的不同同時存在一個 Linux 應用中:
- 不要在線程的信號掩碼中阻塞不能被忽略處理的兩個信號 SIGSTOP 和 SIGKILL。
- 不要在線程的信號掩碼中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
-
確保
sigwait()
等待的信號集已經被進程中所有的線程阻塞。 -
在主線程或其它工作線程產生信號時,必須調用
kill()
將信號發給整個進程,而不能使用pthread_kill()
發送某個特定的工作線程,否則信號處理線程無法接收到此信號。 -
因為
sigwait()
使用了串行的方式處理信號的到來,為避免信號的處理存在滯后,或是非實時信號被丟失的情況,處理每個信號的代碼應盡量簡潔、快速,避免調用會產生阻塞的庫函數。
在開發 Linux 多線程應用中, 如果因為程序邏輯需要引入信號, 在信號處理后程序仍將繼續正常運行。在這種背景下,如果以異步方式處理信號,在編寫信號處理函數一定要考慮異步信號處理函數的安全; 同時, 程序中一些庫函數可能會被信號中斷,錯誤返回,這時需要考慮對 EINTR 的處理。另一方面,也可考慮使用上文介紹的同步模型處理信號,簡化信號處理函數的編寫,避免因為信號處理函數執行上下文的不確定性而帶來的風險。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
