gethostbyname函數阻塞超時實現

在項目中涉及到網絡功能時,常常會用到gethostbyname函數來實現域名到IP地址的解析。可是該函數經過dns解析域名時是阻塞方式的行爲,由於當程序運行環境網絡不通時,調用它的進程就會阻塞,這在單進程環境下不是問題,但在多線程環境下時,這將致使整個整個進程的阻塞,經常不是指望的行爲。最近項目開發中恰好遇到了這個問題,因此思考了一下它的阻塞超時實現,也許不是很完美但測試能用。 shell

實現經過使用alarm函數發出的定時信號和siglongjmp函數來解除gethostbyname函數的阻塞,由於涉及到線程與信號的複雜關係,實現也就稍顯複雜了。首先須要注意的幾點是: ubuntu

  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來保證信號屏蔽字的恢復。 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函數的線程)。
相關文章
相關標籤/搜索