在项目中涉及到网络功能时,经常会用到gethostbyname函数来实现域名到IP地址的解析。但是该函数通过dns解析域名时是阻塞方式的行为,因为当程序运行环境网络不通时,调用它的进程就会阻塞,这在单进程环境下不是问题,但在多线程环境下时,这将导致整个整个进程的阻塞,常常不是期望的行为。最近项目开发中刚好遇到了这个问题,所以思考了一下它的阻塞超时实现,也许不是很完美但测试能用。
实现通过使用alarm函数发出的定时信号和siglongjmp函数来解除gethostbyname函数的阻塞,因为涉及到线程与信号的复杂关系,实现也就稍显复杂了。首先需要注意的几点是:
alarm定时器是进程资源,所有的线程共享相同的alarm。所以在进程中的多个线程不可能互不干扰地使用闹钟定时器[APUE P335]。因此多个线程只能使用一个alarm定时器。
进程中的信号是被递送到单个线程的。如果信号与硬件故障或计时器超时相关,该信号就被发送到引起该事件的线程中去,而其它的信号则被发送到任意一个线程[APUE P334]。因此需要控制信号的发送,进程中使用sigprocmask来阻止信号发送,而线程应该使用pthread_sigmask来实现同样的目的。
sigsetjmp/siglongjmp与setjmp/longjmp的区别在于对信号掩码的保存与恢复,由于信号处理程序是异步执行的,以及上述两点,必须使用sigsetjmp/siglongjmp来实现跳转返回。
gethostbyname和inet_ntoa函数都是不可重入的,所以必须进行同步控制,加锁处理。
接下来看代码实现,首先是一些静态变量与信号处理函数的定义:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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来保证信号屏蔽字的恢复。
实现的执行结果测试如下:
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函数的线程)。