gethostbyname函数阻塞超时实现

原创
2013/11/18 21:17
阅读数 8.6K

在项目中涉及到网络功能时,经常会用到gethostbyname函数来实现域名到IP地址的解析。但是该函数通过dns解析域名时是阻塞方式的行为,因为当程序运行环境网络不通时,调用它的进程就会阻塞,这在单进程环境下不是问题,但在多线程环境下时,这将导致整个整个进程的阻塞,常常不是期望的行为。最近项目开发中刚好遇到了这个问题,所以思考了一下它的阻塞超时实现,也许不是很完美但测试能用。

实现通过使用alarm函数发出的定时信号和siglongjmp函数来解除gethostbyname函数的阻塞,因为涉及到线程与信号的复杂关系,实现也就稍显复杂了。首先需要注意的几点是:

  1. alarm定时器是进程资源,所有的线程共享相同的alarm。所以在进程中的多个线程不可能互不干扰地使用闹钟定时器[APUE P335]。因此多个线程只能使用一个alarm定时器。
  2. 进程中的信号是被递送到单个线程的。如果信号与硬件故障或计时器超时相关,该信号就被发送到引起该事件的线程中去,而其它的信号则被发送到任意一个线程[APUE P334]。因此需要控制信号的发送,进程中使用sigprocmask来阻止信号发送,而线程应该使用pthread_sigmask来实现同样的目的。
  3. sigsetjmp/siglongjmp与setjmp/longjmp的区别在于对信号掩码的保存与恢复,由于信号处理程序是异步执行的,以及上述两点,必须使用sigsetjmp/siglongjmp来实现跳转返回。
  4. gethostbyname和inet_ntoa函数都是不可重入的,所以必须进行同步控制,加锁处理。

接下来看代码实现,首先是一些静态变量与信号处理函数的定义:

#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来保证信号屏蔽字的恢复。

实现的执行结果测试如下:

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函数的线程)。
展开阅读全文
打赏
1
4 收藏
分享
加载中
更多评论
打赏
0 评论
4 收藏
1
分享
返回顶部
顶部