Android DNS之查询结果缓存

这篇博客介绍了Android系统中DNS查询结果的缓存机制,包括核心数据结构、缓存项添加、缓存表查询以及查询失败时的处理。重点讨论了struct resolv_cache_info结构、MRU双向链表以及如何在查询失败时通知缓存机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

DNS响应数据包中都带有一个TTL字段,表示了本次查询结果的有效期,在没有到期之前,如果还需要获取同样一个查询结果,那么无需真的向DNS服务器查询,使用之前的即可。为了实现这种功能,libc有责任将查询结果进行缓存,并且在结果过期的时候将缓存信息删除。这篇笔记就介绍下libc中的这种DNS查询缓存机制。

1. 核心数据结构

在看代码逻辑之前,先来过一下相关的数据结构。

1.1 struct resolv_cache_info

系统中每使能一张网卡都会创建一个该结构,用于保存该网卡相关的DNS配置信息,以及在该网卡上进行的DNS查询结果缓存信息,系统中所有网卡的该结构被组织成一个单链表。

struct resolv_cache_info {
	//网卡的netid
	unsigned                    netid;
	//DNS查询缓存信息
	Cache*                      cache;
	//系统中所有网卡的struct resolv_cache_info组织成一个列表
	struct resolv_cache_info*   next;
	//配置的DNS服务器地址的数目,即nameservers[]中有几个DNS服务器地址
	int                         nscount;
	//DNS服务器地址,当前限制最多可以设置4个DNS服务器地址
	char*                       nameservers[MAXNS];
	//转换后的DNS服务器地址信息,用于查询过程
	struct addrinfo*            nsaddrinfo[MAXNS];
	//见注释,DNS服务器地址每变更一次,该成员的值加1
	int                         revision_id; // # times the nameservers have been replaced
	//惩罚机制相关的一组配置参数,见相关笔记
	struct __res_params         params;
	struct __res_stats          nsstats[MAXNS];
	//这两个参数用于域名搜索,具体见hostname(7),Android中基本上不使用,可以忽略
	char                        defdname[MAXDNSRCHPATH];
	int                         dnsrch_offset[MAXDNSRCH+1];  // offsets into defdname
};

1.1.1 链表初始化

// Head of the list of caches.  Protected by _res_cache_list_lock.
static struct resolv_cache_info _res_cache_list;

static void _res_cache_init(void)
{
    memset(&_res_cache_list, 0, sizeof(_res_cache_list));
    pthread_mutex_init(&_res_cache_list_lock, NULL);
}

初始化resolv_cache_info链表的表头结构以及其互斥锁。_res_cache_list结构本身只作为表头使用,并不保存任何网卡的cache信息,即链表中真正的第一个cache_info信息是从_res_cache_info.next开始的。

另外,resolv_cache_info结构的创建是在设置DNS地址的时候完成的,具体可以参考笔记Android DNS之DNS参数设置

1.2 查询结果cache表头

缓存信息被组织成一个哈希表,但是还需要一个结构来从整体上描述该哈希表的信息,姑且称之为cache表头吧。

typedef struct resolv_cache {
	//Cache中最多可以容纳多少项
	int              max_entries;
	//Cache中当前已容纳多少项
	int              num_entries;
	//MRU表头
	Entry            mru_list;
	int              last_id;
	//Cache表,表的分配时在设置DNS地址的时候完成的
	Entry*           entries;
	//当多个线程同时请求同一个域名查询时,实际上只有第一个会触发网络查询,
	//其它后续请求都会阻塞等待第一个查询请求返回,见下文分析
	PendingReqInfo   pending_requests;
} Cache;

1.2.1 cache表头的创建

cache的创建是和resolv_cache_info结构一起创建的,所以其创建过程也是在设置DNS地址的时候执行的,创建cahce的接口是_resolv_cache_create(),其代码如下:

#define  CONFIG_MAX_ENTRIES    64 * 2 * 5

static int _res_cache_get_max_entries( void )
{
    //系统cache大小为64*2*5
    int cache_size = CONFIG_MAX_ENTRIES;
    //非Netd调用者是不会分配cache的
    const char* cache_mode = getenv("ANDROID_DNS_MODE");
    if (cache_mode == NULL || strcmp(cache_mode, "local") != 0) {
        // Don't use the cache in local mode. This is used by the proxy itself.
        cache_size = 0;
    }

    XLOG("cache size: %d", cache_size);
    return cache_size;
}

static struct resolv_cache* _resolv_cache_create( void )
{
    struct resolv_cache*  cache;

    //分配cache表头结构
    cache = calloc(sizeof(*cache), 1);
    if (cache) {
        //为cache哈希表分配内存
        cache->max_entries = _res_cache_get_max_entries();
        cache->entries = calloc(sizeof(*cache->entries), cache->max_entries);
        if (cache->entries) {
            //初始化MRU链表为空
            cache->mru_list.mru_prev = cache->mru_list.mru_next = &cache->mru_list;
            XLOG("%s: cache created\n", __FUNCTION__);
        } else {
            free(cache);
            cache = NULL;
        }
    }
    return cache;
}

抛开_res_cache_list链表不说(很简单了),cache的组织结构如下图所示:
cache组织结构

1.2.2 MRU双向链表

从上面的cache结构图中可以看出,缓存项除了用哈希表管理外,还额外链接成一个双向链表,从指针名字看,我们姑且称之为MRU(the Most Recently Update)链表,该链表是有序链表,实际维护时是按照最近访问的时间倒叙排列,即最近访问的缓存项会被放在表头,这样设计是为了在缓存项已满,但是又需要加入新的缓存项时,可以快速的移除最旧的(移除MRU链表末尾结点即可)。

MRU链表的作用就这一点,相关代码就是基本的双向链表操作,这里不再赘述。

1.3 查询结果缓存项Entry

该结构才是实实在在的缓存项,代表了一个查询结果,如上图,它被组织成一个哈希表。

/* cache entry. for simplicity, 'hash' and 'hlink' are inlined in this
 * structure though they are conceptually part of the hash table.
 *
 * similarly, mru_next and mru_prev are part of the global MRU list
 */
typedef struct Entry {
    //该hash值是根据查询报文内容计算出来的
    unsigned int     hash;   /* hash value */
    //指向冲突链中的下一个成员
    struct Entry*    hlink;  /* next in collision chain */
    //MRU列表
    struct Entry*    mru_prev;
    struct Entry*    mru_next;

    //query和answer分别为查询报文和响应报文
    const uint8_t*   query;
    int              querylen;
    const uint8_t*   answer;
    int              answerlen;
    //DNS响应报文的有效期,记录的是墙上时钟,即当前系统时间超过expires,则认为失效
    time_t           expires;   /* time_t when the entry isn't valid any more */
    int              id;        /* for debugging purpose */
} Entry;

2. 缓存项的添加

在res_nsend()中,如果完成一次成功的查询,那么会将查询结果进行缓存,这通过调用_resolv_cache_add()完成。

@netid:在哪个网卡上发起的查询
@query:查询报文
@querylen:查询报文缓存区长度
@answer:响应报文
@answerlen:响应报文缓存区长度
void _resolv_cache_add( unsigned              netid,
                   const void*           query,
                   int                   querylen,
                   const void*           answer,
                   int                   answerlen )
{
    Entry    key[1];
    Entry*   e;
    Entry**  lookup;
    u_long   ttl;
    Cache*   cache = NULL;

    //根据查询报文,初始化key,key的类型就是Entry,所以从这里可以看出,
    //缓存项就是用查询报文信息索引的
    /* don't assume that the query has already been cached */
    if (!entry_init_key( key, query, querylen )) {
        XLOG( "%s: passed invalid query ?", __FUNCTION__);
        return;
    }

    pthread_mutex_lock(&_res_cache_list_lock);
    //找到该netid的cache信息头部,即该netid对应的resolv_cache_info结构中的Cache成员
    //寻找方法也非常简单,就是遍历_res_resolv_list链表,寻找指定netid的结点
    cache = _find_named_cache_locked(netid);
    if (cache == NULL) {
        goto Exit;
    }
    //在添加之前首先查一下是否已经有了,这样可以避免添加重复项
    lookup = _cache_lookup_p(cache, key);
    e      = *lookup;
    //cache中已有,这应该不太可能发生,因为调用者只会在cache没有命中的情况下才添加
    if (e != NULL) { /* should not happen */
        XLOG("%s: ALREADY IN CACHE (%p) ? IGNORING ADD",
             __FUNCTION__, e);
        goto Exit;
    }
    //到这里,说明当前cache表里没有本次新的查询结果,那么需要将其添加到cache表中
    
    //如果缓存已满,为了将新的cache放入缓存,那么需要移除最旧的
    if (cache->num_entries >= cache->max_entries) {
    	//先将所有过期限的cache项移除掉
        _cache_remove_expired(cache);
        //如果没有过期的cache项,那么还需要移除那些最旧的,即最近都没有被访问过的
        if (cache->num_entries >= cache->max_entries) {
            _cache_remove_oldest(cache);
        }
        //这里为什么要再查一遍,不理解...
        lookup = _cache_lookup_p(cache, key);
        e      = *lookup;
        if (e != NULL) {
            XLOG("%s: ALREADY IN CACHE (%p) ? IGNORING ADD",
                __FUNCTION__, e);
            goto Exit;
        }
    }
    //从响应报文中获取本次查询结果中指定的查询结果的有效期
    ttl = answer_getTTL(answer, answerlen);
    if (ttl > 0) {
    	//ttl大于0,表示该地址可以保留一段时间,那么创建一个新的cache项,
        //然后设定其有效期,并将其加入到cache中
        e = entry_alloc(key, answer, answerlen);
        if (e != NULL) {
            e->expires = ttl + _time_now();
            _cache_add_p(cache, lookup, e);
        }
    }

Exit:
    if (cache != NULL) {
    	//向所有等待结果的线程发送广播,该机制见下文的分析
      	_cache_notify_waiting_tid_locked(cache, key);
    }
    pthread_mutex_unlock(&_res_cache_list_lock);
}

3. cache表查询

在res_nsend()真正向DNS服务器发起DNS查询请求之前,会首先向自己的cache查询,如果cache可以命中,那么直接返回,否则才继续向DNS服务器查询。该查询过程是通过_resolv_cache_lookup()完成的。

//函数返回值
typedef enum {
    //返回这种值表示一种错误
    RESOLV_CACHE_UNSUPPORTED,  /* the cache can't handle that kind of queries */
                               /* or the answer buffer is too small */
    //查询过程没有问题,但是cache没有命中
    RESOLV_CACHE_NOTFOUND,     /* the cache doesn't know about this query */
    //查询过程没有问题,而且命中了
    RESOLV_CACHE_FOUND         /* the cache found the answer */
} ResolvCacheStatus;

/*
 * @netid:cache是基于网卡保存的
 * @query&querylen:查询报文和查询报文长度
 * @answer&answersize:响应报文和响应报文长度
 * @ret: cache查询结果
 */
ResolvCacheStatus _resolv_cache_lookup( unsigned              netid,
                      const void*           query,
                      int                   querylen,
                      void*                 answer,
                      int                   answersize,
                      int                  *answerlen )
{
    Entry      key[1];
    Entry**    lookup;
    Entry*     e;
    time_t     now;
    Cache*     cache;

    ResolvCacheStatus  result = RESOLV_CACHE_NOTFOUND;

    XLOG("%s: lookup", __FUNCTION__);
    XLOG_QUERY(query, querylen);

    //下面几个步骤和前面_resolv_cache_add()一样
    if (!entry_init_key(key, query, querylen)) {
        XLOG("%s: unsupported query", __FUNCTION__);
        return RESOLV_CACHE_UNSUPPORTED;
    }

    pthread_once(&_res_cache_once, _res_cache_init);
    pthread_mutex_lock(&_res_cache_list_lock);

    cache = _find_named_cache_locked(netid);
    if (cache == NULL) {
        result = RESOLV_CACHE_UNSUPPORTED;
        goto Exit;
    }

    /* see the description of _lookup_p to understand this.
     * the function always return a non-NULL pointer.
     */
    lookup = _cache_lookup_p(cache, key);
    e      = *lookup;

    //cache中没有待查询的请求,下面这段逻辑很重要,会影响本次查询到底会不会真的发起
    if (e == NULL) {
        XLOG( "NOT IN CACHE");
        // calling thread will wait if an outstanding request is found
        // that matching this query
        //返回0,表示没有请求发出,这时直接返回,这种情况下会项DNS服务器发起查询请求
        //返回1,表示是阻塞返回
        if (!_cache_check_pending_request_locked(&cache, key, netid) || cache == NULL) {
            goto Exit;
        } else {
            //阻塞返回,重新查询cache表,因为查询结果可能已经加入到了cache中了,
            //见_cache_check_pending_request_locked
            lookup = _cache_lookup_p(cache, key);
            e = *lookup;
            if (e == NULL) {
                goto Exit;
            }
        }
    }

    //到这里,说明是阻塞调用返回的,而且响应结果不是自己查询出来的。由于中间因为调度等因素,
    //查询结果有可能已经无效了,所以这里需要判断查询结果是否还在有效期内
    now = _time_now();
    //查询结果无效,返回没有查询到结果,这种情况下也会向DNS服务器发起查询请求
    if (now >= e->expires) {
        XLOG( " NOT IN CACHE (STALE ENTRY %p DISCARDED)", *lookup );
        XLOG_QUERY(e->query, e->querylen);
        _cache_remove_p(cache, lookup);
        goto Exit;
    }
    //ok,到这里说明cache中的结果没问题,开始组织查询结果

    //提供的接收缓冲区过小,返回错误
    *answerlen = e->answerlen;
    if (e->answerlen > answersize) {
        /* NOTE: we return UNSUPPORTED if the answer buffer is too short */
        result = RESOLV_CACHE_UNSUPPORTED;
        XLOG(" ANSWER TOO LONG");
        goto Exit;
    }
    //都ok,拷贝响应报文到调用者提供的缓存中
    memcpy( answer, e->answer, e->answerlen );

    //由于该cache项被访问了,所以需要将其更新到MRU链表的首部,表示该cache项是被最新的,
    //这样可避免该cache项被_cache_remove_oldest()删除
    /* bump up this entry to the top of the MRU list */
    if (e != cache->mru_list.mru_next) {
        entry_mru_remove( e );
        entry_mru_add( e, &cache->mru_list );
    }
    //返回查询成功
    XLOG( "FOUND IN CACHE entry=%p", e );
    result = RESOLV_CACHE_FOUND;

Exit:
    pthread_mutex_unlock(&_res_cache_list_lock);
    return result;
}

/*
 * Return 0 if no pending request is found matching the key.
 * If a matching request is found the calling thread will wait until
 * the matching request completes, then update *cache and return 1.
 */
//从上面的注释中可以看出该函数的作用
static int _cache_check_pending_request_locked( struct resolv_cache** cache, Entry* key, unsigned netid )
{
    struct pending_req_info *ri, *prev;
    int exist = 0;

    if (*cache && key) {
    	//检查pending_request,寻找看下是否有与查询报文hash值一样的结点
        //hash值是基于查询报文内容算出来的,所以hash值相等意味着两次查询请求完全相同
        ri = (*cache)->pending_requests.next;
        prev = &(*cache)->pending_requests;
        while (ri) {
            if (ri->hash == key->hash) {
                exist = 1;
                break;
            }
            prev = ri;
            ri = ri->next;
        }
        //如果没有找到,说明没有挂起的请求,那么创建一个请求,然后将其加入到pending_request列表中
        if (!exist) {
            ri = calloc(1, sizeof(struct pending_req_info));
            if (ri) {
                ri->hash = key->hash;
                pthread_cond_init(&ri->cond, NULL);
                prev->next = ri;
            }
        } else {
            //如果找到了,说明之前已经有相同请求发出去了,没有必要同时发起两次相同的请求,
            //所以block当前线程,使其阻塞等待前面的查询结果
            struct timespec ts = {0,0};
            XLOG("Waiting for previous request");
            //最多等待20s,该值超过了配置的DNS请求超时时间,应该是足够了
            ts.tv_sec = _time_now() + PENDING_REQUEST_TIMEOUT;
            //调用线程会阻塞到这里
            pthread_cond_timedwait(&ri->cond, &_res_cache_list_lock, &ts);
            /* Must update *cache as it could have been deleted. */
            //等待期间,网卡可能已经被销毁了,这时其cache表也被释放了,所以这里需要重新查询下
            *cache = _find_named_cache_locked(netid);
        }
    }
    //返回值表示是否已经有相同的请求被发送出去了
    return exist;
}

4. 查询失败时缓存相关处理

从上面的cache查询中,可以看出有些请求是会加入到pending_request中并阻塞等待的,所以如果在res_nsend()中发起了一次DNS查询,但是查询失败了,那么必须将查询失败的结果也告诉缓存机制,缓存机制需要将这些继续等待的线程唤醒。这个过程是通过调用_resolv_cache_query_failed()实现的。

/* notify the cache that the query failed */
void _resolv_cache_query_failed( unsigned netid, const void* query, int querylen)
{
    Entry    key[1];
    Cache*   cache;

    if (!entry_init_key(key, query, querylen))
        return;

    pthread_mutex_lock(&_res_cache_list_lock);

    cache = _find_named_cache_locked(netid);

    if (cache) {
    	//前面的步骤已经很熟悉了,重点看这一步
        _cache_notify_waiting_tid_locked(cache, key);
    }

    pthread_mutex_unlock(&_res_cache_list_lock);
}

/* notify any waiting thread that waiting on a request
 * matching the key has been added to the cache */
static void _cache_notify_waiting_tid_locked( struct resolv_cache* cache, Entry* key )
{
    struct pending_req_info *ri, *prev;

    if (cache && key) {
        ri = cache->pending_requests.next;
        prev = &cache->pending_requests;
        while (ri) {
            //向所有等待本次查询结果的线程发送广播,唤醒这些阻塞的线程
            if (ri->hash == key->hash) {
                pthread_cond_broadcast(&ri->cond);
                break;
            }
            prev = ri;
            ri = ri->next;
        }

        // remove item from list and destroy
        if (ri) {
            prev->next = ri->next;
            pthread_cond_destroy(&ri->cond);
            free(ri);
        }
    }
}

5. 其它

5.1 _cache_lookup_p()

前面多次用到该函数,该函数的作用是从Cache表(cache参数指定)中寻找是否有指定的缓存项(key参数指定)。

/* This function tries to find a key within the hash table
 * In case of success, it will return a *pointer* to the hashed key.
 * In case of failure, it will return a *pointer* to NULL
 *
 * So, the caller must check '*result' to check for success/failure.
 *
 * The main idea is that the result can later be used directly in
 * calls to _resolv_cache_add or _resolv_cache_remove as the 'lookup'
 * parameter. This makes the code simpler and avoids re-searching
 * for the key position in the htable.
 *
 * The result of a lookup_p is only valid until you alter the hash
 * table.
 */
//见注释,如果找到key,那么返回指向缓存项的指针的地址;如果没有找到,那么返回指向NULL的指针
//也就是说,调用者应该判断*ret,ret为返回值
static Entry** _cache_lookup_p( Cache* cache, Entry* key )
{
    //哈希算法也非常简单,就是求余
    int      index = key->hash % cache->max_entries;
    Entry**  pnode = (Entry**) &cache->entries[ index ];

    //遍历冲突链
    while (*pnode != NULL) {
        Entry*  node = *pnode;

        if (node == NULL)
            break;
        //hash值要一致;查询报文要一致,关于查询报文的比较不再赘述,关心的可以继续往下跟
        if (node->hash == key->hash && entry_equals(node, key))
            break;

        pnode = &node->hlink;
    }
    return pnode;
}
<think>好的,用户需要知道如何在不同操作系统中清除DNS缓存。首先,我得回忆一下之前学过的相关知识,特别是参考用户提供的引用内容。 根据引用[2],不同系统的命令有所不同。Windows用的是ipconfig命令,Linux则根据服务不同可能使用systemd-resolved或者其他工具。比如Ubuntu可能用systemd-resolve命令。另外,用户提到的引用[1]是关于Ubuntu的,命令是systemd-resolve,但要注意服务和命令名称的差异,比如没有最后的d字母。 接下来需要分系统整理步骤。首先Windows,用户可能需要查看和清除的命令。然后是Linux的不同发行版,比如使用systemd-resolved的系统和旧版本可能使用nscd或者dnsmasq。MacOS的话,我记得可能有不同的方法,比如用sudo killall -HUP mDNSResponder这样的命令。 需要确保每个系统的步骤准确,特别是命令的正确性。比如在Linux部分,要区分是否需要使用sudo权限,以及不同服务的处理方法。同时,用户可能遇到的问题比如权限不足或者服务未运行,需要给出检查服务的建议,比如用systemctl status systemd-resolved.service。 还要注意用户可能对命令行不熟悉,所以步骤要详细,比如给出具体的命令示例,并提醒可能需要管理员权限。另外,是否所有Linux发行版都适用这些命令?比如CentOS可能使用不同的服务,可能需要提到这一点,或者建议用户根据系统版本来调整。 最后,生成相关问题时要考虑用户可能进一步的需求,比如DNS缓存的作用,验证是否清除成功,或者自动清除的方法。这些问题能帮助用户更深入理解DNS缓存管理。 总结一下,回答的结构应该是分操作系统列出步骤,每个步骤包括查看和清除命令,并附加注意事项。然后相关问题围绕DNS缓存的更多方面展开。需要确保引用正确,比如引用[2]提供了Windows和Linux的命令,引用[1]补充了Ubuntu的具体情况。</think>### 如何在不同操作系统中清除DNS缓存 DNS缓存用于加速域名解析过程,但有时需要强制清除以获取最新记录。以下是常见操作系统的清除方法: --- #### **Windows系统** 1. **查看DNS缓存** 打开命令提示符,输入: ```bash ipconfig /displaydns ``` 2. **清除DNS缓存** 在命令提示符中运行: ```bash ipconfig /flushdns ``` 成功后显示`已成功刷新 DNS 解析缓存`[^2]。 --- #### **Linux系统(以systemd-resolved为例)** 1. **查看DNS缓存统计** 终端输入: ```bash sudo systemd-resolve --statistics ``` 2. **清除DNS缓存** 执行命令: ```bash sudo systemd-resolve --flush-caches ``` 若服务未运行,可使用`sudo systemctl restart systemd-resolved.service`重启服务。 **注意**: - 旧版本Linux可能使用`nscd`或`dnsmasq`服务,需执行`sudo service nscd restart`或`sudo systemctl restart dnsmasq`[^1]。 --- #### **macOS系统** 1. **清除DNS缓存** 终端输入: ```bash sudo killall -HUP mDNSResponder ``` 需输入管理员密码确认。 --- #### **Android/iOS移动设备** - 通常通过**重启设备**或**启用飞行模式**临时清除DNS缓存。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值