Linux 多線程應(yīng)用中編寫安全的信號處理函數(shù)
在開發(fā)多線程應(yīng)用時,開發(fā)人員一般都會考慮線程安全,會使用 pthread_mutex 去保護(hù)全局變量。如果應(yīng)用中使用了信號,而且信號的產(chǎn)生不是因為程序運(yùn)行出錯,而是程序邏輯需要,譬如 SIGUSR1、SIGRTMIN 等,信號在被處理后應(yīng)用程序還將正常運(yùn)行。在編寫這類信號處理函數(shù)時,應(yīng)用層面的開發(fā)人員卻往往忽略了信號處理函數(shù)執(zhí)行的上下文背景,沒有考慮編寫安全的信號處理函數(shù)的一些規(guī)則。本文首先介紹編寫信號處理函數(shù)時需要考慮的一些規(guī)則;然后舉例說明在多線程應(yīng)用中如何構(gòu)建模型讓因為程序邏輯需要而產(chǎn)生的異步信號在指定的線程中以同步的方式處理。
回頁首
線程和信號
Linux 多線程應(yīng)用中,每個線程可以通過調(diào)用 pthread_sigmask() 設(shè)置本線程的信號掩碼。一般情況下,被阻塞的信號將不能中斷此線程的執(zhí)行,除非此信號的產(chǎn)生是因為程序運(yùn)行出錯如 SIGSEGV;另外不能被忽略處理的信號 SIGKILL 和 SIGSTOP 也無法被阻塞。
當(dāng)一個線程調(diào)用 pthread_create() 創(chuàng)建新的線程時,此線程的信號掩碼會被新創(chuàng)建的線程繼承。
POSIX.1 標(biāo)準(zhǔn)定義了一系列線程函數(shù)的接口,即 POSIX threads(Pthreads)。Linux C 庫提供了兩種關(guān)于線程的實現(xiàn):LinuxThreads 和 NPTL(Native POSIX Threads Library)。LinuxThreads 已經(jīng)過時,一些函數(shù)的實現(xiàn)不遵循POSIX.1 規(guī)范。NPTL 依賴 Linux 2.6 內(nèi)核,更加遵循 POSIX..1 規(guī)范,但也不是完全遵循。
基于 NPTL 的線程庫,多線程應(yīng)用中的每個線程有自己獨(dú)特的線程 ID,并共享同一個進(jìn)程ID。應(yīng)用程序可以通過調(diào)用 kill(getpid(),signo) 將信號發(fā)送到進(jìn)程,如果進(jìn)程中當(dāng)前正在執(zhí)行的線程沒有阻礙此信號,則會被中斷,線號處理函數(shù)會在此線程的上下文背景中執(zhí)行。應(yīng)用程序也可以通過調(diào)用 pthread_kill(pthread_t thread, int sig) 將信號發(fā)送給指定的線程,則線號處理函數(shù)會在此指定線程的上下文背景中執(zhí)行。
基于 LinuxThreads 的線程庫,多線程應(yīng)用中的每個線程擁有自己獨(dú)特的進(jìn)程 ID,getpid() 在不同的線程中調(diào)用會返回不同的值,所以無法通過調(diào)用 kill(getpid(),signo) 將信號發(fā)送到整個進(jìn)程。
下文介紹的在指定的線程中以同步的方式處理異步信號是基于使用了 NPTL 的 Linux C 庫。請參考“Linux 線程模型的比較:LinuxThreads 和 NPTL”和“pthreads(7) - Linux man page”進(jìn)一步了解 Linux 的線程模型,以及不同版本的 Linux C 庫對 NPTL 的支持。
回頁首
編寫安全的異步信號處理函數(shù)
信號的產(chǎn)生可以是:
- 用戶從控制終端終止程序運(yùn)行,如 Ctrk + C 產(chǎn)生 SIGINT;
- 程序運(yùn)行出錯時由硬件產(chǎn)生信號,如訪問非法地址產(chǎn)生 SIGSEGV;
- 程序運(yùn)行邏輯需要,如調(diào)用
kill、raise 產(chǎn)生信號。
因為信號是異步事件,即信號處理函數(shù)執(zhí)行的上下文背景是不確定的,譬如一個線程在調(diào)用某個庫函數(shù)時可能會被信號中斷,庫函數(shù)提前出錯返回,轉(zhuǎn)而去執(zhí)行信號處理函數(shù)。對于上述第三種信號的產(chǎn)生,信號在產(chǎn)生、處理后,應(yīng)用程序不會終止,還是會繼續(xù)正常運(yùn)行,在編寫此類信號處理函數(shù)時尤其需要小心,以免破壞應(yīng)用程序的正常運(yùn)行。關(guān)于編寫安全的信號處理函數(shù)主要有以下一些規(guī)則:
- 信號處理函數(shù)盡量只執(zhí)行簡單的操作,譬如只是設(shè)置一個外部變量,其它復(fù)雜的操作留在信號處理函數(shù)之外執(zhí)行;
errno 是線程安全,即每個線程有自己的 errno,但不是異步信號安全。如果信號處理函數(shù)比較復(fù)雜,且調(diào)用了可能會改變 errno 值的庫函數(shù),必須考慮在信號處理函數(shù)開始時保存、結(jié)束的時候恢復(fù)被中斷線程的 errno 值;
- 信號處理函數(shù)只能調(diào)用可以重入的 C 庫函數(shù);譬如不能調(diào)用
malloc(),free()以及標(biāo)準(zhǔn) I/O 庫函數(shù)等;
- 信號處理函數(shù)如果需要訪問全局變量,在定義此全局變量時須將其聲明為
volatile,以避免編譯器不恰當(dāng)?shù)膬?yōu)化。
從整個 Linux 應(yīng)用的角度出發(fā),因為應(yīng)用中使用了異步信號,程序中一些庫函數(shù)在調(diào)用時可能被異步信號中斷,此時必須根據(jù)errno 的值考慮這些庫函數(shù)調(diào)用被信號中斷后的出錯恢復(fù)處理,譬如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
}
|
回頁首
在指定的線程中以同步的方式處理異步信號
如上文所述,不僅編寫安全的異步信號處理函數(shù)本身有很多的規(guī)則束縛;應(yīng)用中其它地方在調(diào)用可被信號中斷的庫函數(shù)時還需考慮被中斷后的出錯恢復(fù)處理。這讓程序的編寫變得復(fù)雜,幸運(yùn)的是,POSIX.1 規(guī)范定義了sigwait()、 sigwaitinfo() 和 pthread_sigmask() 等接口,可以實現(xiàn):
這種在指定的線程中以同步方式處理信號的模型可以避免因為處理異步信號而給程序運(yùn)行帶來的不確定性和潛在危險。
sigwait
sigwait() 提供了一種等待信號的到來,以串行的方式從信號隊列中取出信號進(jìn)行處理的機(jī)制。sigwait()只等待函數(shù)參數(shù)中指定的信號集,即如果新產(chǎn)生的信號不在指定的信號集內(nèi),則 sigwait()繼續(xù)等待。對于一個穩(wěn)定可靠的程序,我們一般會有一些疑問:
- 如果信號隊列中有多個信號在等待,在信號處理時有沒有優(yōu)先級規(guī)則?
- 實時信號和非實時信號在處理時有沒有什么區(qū)別?
筆者寫了一小段測試程序來測試 sigwait 在信號處理時的一些規(guī)則。
清單 1. sigwait_test.c
#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);
}
|
程序編譯運(yùn)行在 RHEL4 的結(jié)果如下:
圖 1. sigwait 測試程序執(zhí)行結(jié)果
從以上測試程序發(fā)現(xiàn)以下規(guī)則:
- 對于非實時信號,相同信號不能在信號隊列中排隊;對于實時信號,相同信號可以在信號隊列中排隊。
- 如果信號隊列中有多個實時以及非實時信號排隊,實時信號并不會先于非實時信號被取出,信號數(shù)字小的會先被取出:如 SIGUSR1(10)會先于 SIGUSR2 (12),SIGRTMIN(34)會先于 SIGRTMAX (64), 非實時信號因為其信號數(shù)字小而先于實時信號被取出。
sigwaitinfo() 以及 sigtimedwait() 也提供了與 sigwait() 函數(shù)相似的功能。
Linux 多線程應(yīng)用中的信號處理模型
在基于 Linux 的多線程應(yīng)用中,對于因為程序邏輯需要而產(chǎn)生的信號,可考慮調(diào)用 sigwait()使用同步模型進(jìn)行處理。其程序流程如下:
- 主線程設(shè)置信號掩碼,阻礙希望同步處理的信號;主線程的信號掩碼會被其創(chuàng)建的線程繼承;
- 主線程創(chuàng)建信號處理線程;信號處理線程將希望同步處理的信號集設(shè)為
sigwait()的第一個參數(shù)。
- 主線程創(chuàng)建工作線程。
圖 2. 在指定的線程中以同步方式處理異步信號的模型
代碼示例
以下為一個完整的在指定的線程中以同步的方式處理異步信號的程序。
主線程設(shè)置信號掩碼阻礙 SIGUSR1 和 SIGRTMIN 兩個信號,然后創(chuàng)建信號處理線程sigmgr_thread()和五個工作線程 worker_thread()。主線程每隔10秒調(diào)用 kill() 對本進(jìn)程發(fā)送 SIGUSR1 和 SIGTRMIN 信號。信號處理線程 sigmgr_thread()在接收到信號時會調(diào)用信號處理函數(shù) sig_handler()。
程序編譯:gcc -o signal_sync signal_sync.c -lpthread
程序執(zhí)行:./signal_sync
從程序執(zhí)行輸出結(jié)果可以看到主線程發(fā)出的所有信號都被指定的信號處理線程接收到,并以同步的方式處理。
清單 2. signal_sync.c
#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 的多線程應(yīng)用中,對于因為程序邏輯需要而產(chǎn)生的信號,可考慮使用同步模型進(jìn)行處理;而對會導(dǎo)致程序運(yùn)行終止的信號如 SIGSEGV 等,必須按照傳統(tǒng)的異步方式使用 signal()、 sigaction()注冊信號處理函數(shù)進(jìn)行處理。這兩種信號處理模型可根據(jù)所處理的信號的不同同時存在一個 Linux 應(yīng)用中:
- 不要在線程的信號掩碼中阻塞不能被忽略處理的兩個信號 SIGSTOP 和 SIGKILL。
- 不要在線程的信號掩碼中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
- 確保
sigwait() 等待的信號集已經(jīng)被進(jìn)程中所有的線程阻塞。
- 在主線程或其它工作線程產(chǎn)生信號時,必須調(diào)用
kill() 將信號發(fā)給整個進(jìn)程,而不能使用 pthread_kill() 發(fā)送某個特定的工作線程,否則信號處理線程無法接收到此信號。
- 因為
sigwait()使用了串行的方式處理信號的到來,為避免信號的處理存在滯后,或是非實時信號被丟失的情況,處理每個信號的代碼應(yīng)盡量簡潔、快速,避免調(diào)用會產(chǎn)生阻塞的庫函數(shù)。
回頁首
小結(jié)
在開發(fā) Linux 多線程應(yīng)用中, 如果因為程序邏輯需要引入信號, 在信號處理后程序仍將繼續(xù)正常運(yùn)行。在這種背景下,如果以異步方式處理信號,在編寫信號處理函數(shù)一定要考慮異步信號處理函數(shù)的安全; 同時, 程序中一些庫函數(shù)可能會被信號中斷,錯誤返回,這時需要考慮對 EINTR 的處理。另一方面,也可考慮使用上文介紹的同步模型處理信號,簡化信號處理函數(shù)的編寫,避免因為信號處理函數(shù)執(zhí)行上下文的不確定性而帶來的風(fēng)險。
免責(zé)聲明:
- 本文所提出的方式方法僅代表作者個人觀點(diǎn)。
- 本文屬于原創(chuàng)作品,資料來源不超出參考文獻(xiàn)所列范疇,其中任何部分都不會侵犯任何第三方的知識產(chǎn)權(quán)。