在項目中涉及到網絡功能時,常常會用到gethostbyname函數來實現域名到IP地址的解析。可是該函數經過dns解析域名時是阻塞方式的行爲,由於當程序運行環境網絡不通時,調用它的進程就會阻塞,這在單進程環境下不是問題,但在多線程環境下時,這將致使整個整個進程的阻塞,經常不是指望的行爲。最近項目開發中恰好遇到了這個問題,因此思考了一下它的阻塞超時實現,也許不是很完美但測試能用。 shell
實現經過使用alarm函數發出的定時信號和siglongjmp函數來解除gethostbyname函數的阻塞,由於涉及到線程與信號的複雜關係,實現也就稍顯複雜了。首先須要注意的幾點是: ubuntu
接下來看代碼實現,首先是一些靜態變量與信號處理函數的定義: 網絡
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <fcntl.h> #include <unistd.h> #include <resolv.h> #include <arpa/nameser.h> #include <errno.h> #include <setjmp.h> #include <time.h> #include <sys/time.h> #include <signal.h> #include <pthread.h> #define RET_FAILURE (-1) #define RET_SUCCESS 0 #define PLOG(level,format,args...) \ do{printf("[%s]",#level);printf(format,##args);}while(0) static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static sigjmp_buf jmpbuf; static volatile sig_atomic_t canjump; static void alarm_handle(int signo) { if(canjump == 0) return; canjump = 0; siglongjmp(jmpbuf,1); }
線程鎖用來保證一次只有一個線程調用gethostbyname。原子變量canjump用來保證siglongjmp跳轉以前,已經成功執行過sigsetjmp設置好了jmpbuf跳轉緩衝。 多線程
下面是gethostbyname的包裝函數實現: 異步
int gethostbyname_proc2(char *name,char *ip) { int ret = RET_SUCCESS; struct hostent *host = NULL; int timeout = 5; if(name == NULL || ip == NULL) { PLOG(ERR,"invalid params!\n"); return RET_FAILURE; } pthread_mutex_lock(&lock); sigset_t mask,oldmask; sigemptyset(&mask); sigaddset(&mask,SIGALRM); pthread_sigmask(SIG_UNBLOCK,&mask,&oldmask); #if 1 signal(SIGALRM, alarm_handle); alarm(timeout); if(sigsetjmp(jmpbuf,1)!=0) { PLOG(ERR,"gethostbyname timeout\n"); alarm(0); signal(SIGALRM,SIG_IGN); pthread_mutex_unlock(&lock); pthread_sigmask(SIG_SETMASK,&oldmask,NULL); return RET_FAILURE; } canjump = 1; /* sigsetjmp() is ok */ #endif res_init(); /* clear dns_cache */ host = gethostbyname(name); int i = 0; while(1) { printf(">>>i=%d\n",i++);//host = NULL; sleep(1); } /* cancel signal handle if return */ alarm(0); // cancel timer signal(SIGALRM,SIG_IGN); if (host == NULL) {// use h_errno not errno variable PLOG(ERR, "get host %s err:%s!\n", name, hstrerror(h_errno)); ret = RET_FAILURE; } else {// only get the first ipv4 addr if host has many ipv4 addrs inet_ntop(AF_INET,(struct in_addr *)host->h_addr,ip,INET_ADDRSTRLEN); PLOG(DBG, "gethostbyname %s success,ip:%s!\n",name,ip); ret = RET_SUCCESS; } pthread_sigmask(SIG_SETMASK,&oldmask,NULL); pthread_mutex_unlock(&lock); return ret; }
首先解除線程的SIGALRM信號阻塞以並接收該信號,而後設置跳轉緩衝以及超時後的處理邏輯,while(1)代碼段是爲了模擬gethostbyname執行阻塞超時(模擬網絡不通環境,僅爲測試),在gethostbyname執行成功後取消定時器並轉換IP地址。這裏用可重入的inet_ntop函數代替inet_ntoa函數。 函數
測試線程與主程序代碼: 測試
void *get_host_addr(void *arg) { int ret = 0; char name[32] = "www.baidu.com"; char ip[16]={0}; while(1) { printf("++++++++++++++[%s]time1 = %lu +++++++++++\n",(char*)arg,time(NULL)); ret = gethostbyname_proc2(name,ip); printf("++++++++++++++[%s]time2 = %lu +++++++++++\n",(char*)arg,time(NULL)); usleep(100000); } return (void*)ret; } int main(int argc, char *argv[]) { int ret = 0; char name[32] = "www.baidu.com"; char ip[16]={0}; pthread_t tid1,tid2; sigset_t mask,oldmask; sigemptyset(&mask); sigaddset(&mask,SIGALRM); pthread_sigmask(SIG_BLOCK,&mask,&oldmask); pthread_create(&tid1,NULL,get_host_addr,"T_11"); pthread_create(&tid2,NULL,get_host_addr,"T_22"); pthread_join(tid1,NULL); pthread_join(tid2,NULL); sigprocmask(SIG_SETMASK,&oldmask,NULL); return ret; }建立兩個線程不斷去獲取百度的IP地址,在主線程中首先阻止SIGALRM信號的發送,而使用pthread_create函數建立新線程時,新建線程會繼承現有的信號屏蔽字。因此只有在線程調用gethostbyname函數時纔會接收到SIGALRM信號。
當執行信號處理函數時,系統會屏蔽掉SIGALRM信號的接收,若是使用setjmp/longjmp函數則跳轉回去後SIGALRM信號依然被屏蔽,這顯然是不合適的,因此必須用sigsetjmp/siglongjmp來保證信號屏蔽字的恢復。 atom
實現的執行結果測試以下: spa
hong@ubuntu:~/test/test-example$ ./gethostbyname_proc ++++++++++++++[T_11]time1 = 1384779930 +++++++++++ ++++++++++++++[T_22]time1 = 1384779930 +++++++++++ >>>i=0 >>>i=1 >>>i=2 >>>i=3 >>>i=4 [ERR]gethostbyname timeout ++++++++++++++[T_11]time2 = 1384779935 +++++++++++ >>>i=0 ++++++++++++++[T_11]time1 = 1384779935 +++++++++++ >>>i=1 >>>i=2 >>>i=3 >>>i=4 [ERR]gethostbyname timeout ++++++++++++++[T_22]time2 = 1384779940 +++++++++++ >>>i=0 ++++++++++++++[T_22]time1 = 1384779940 +++++++++++ >>>i=1 >>>i=2 >>>i=3 >>>i=4 [ERR]gethostbyname timeout ++++++++++++++[T_11]time2 = 1384779945 +++++++++++ >>>i=0 ++++++++++++++[T_11]time1 = 1384779945 +++++++++++ >>>i=1 >>>i=2 ^C若是不進行SIGALRM信號的線程屏蔽,則在調用一次gethostbyname_proc2後就會出線段錯誤。緣由是siglongjmp跳轉到了未初始化的棧內存中,而更深層致使跳轉錯誤的緣由應該是SIGALRM信號隨機發送到了不一樣的線程,而該線程沒有執行sigsetjmp函數(不是正在調用gethostbyname_proc函數的線程)。