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的组织结构如下图所示:
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;
}