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

1. 核心数据结构

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

1.1 struct resolv_cache_info

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

struct resolv_cache_info {//网卡的netidunsigned                    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服务器地址每变更一次,该成员的值加1int                         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 * 5static int _res_cache_get_max_entries( void )
{//系统cache大小为64*2*5int 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_lockedlookup = _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 destroyif (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;
}

Android DNS之查询结果缓存相关推荐

  1. Android DNS解析的过程

    Android DNS解析的过程 DNS解析概念 DNS的全称是domain name system,即域名系统.DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的去访问 ...

  2. (两百九十二) Android DNS缓存学习

    参考文档 Android DNS缓存时长的探索_cc0410的博客-CSDN博客_android dns缓存时间 https://blog.csdn.net/xiaoyu_750516366/cate ...

  3. Android DNS机制

    1,DNS DNS全称Domain Name System(域名系统),顾名思义,就是把域名解析为指向的IP,让人们通过注册的域名可以方便的访问到网址的一种服务.域名解析就是域名到IP地址的转换过程. ...

  4. Android DNS解析过程

    前言 一次排查接口404问题,引伸的Android DNS解析过程,简单分析总结一下 1.首先明白DNS解析流程 操作系统检查自身本地的hosts文件是否有这个网址的映射关系,如果有,直接返回完成域名 ...

  5. linux关闭dns迭代查询,DNS查询和响应过程递归和迭代的使用

    需要了解DNS查询过程的递归和迭代的机制,找到了RFC的相关章节, 对这个进行了简单的翻译(水平有限),可以留下来做个参考. RFC 1034 4.3.1. Queries and responses ...

  6. Android学习系列(27)--App缓存管理

    随笔- 53 文章- 10 评论- 1064 Android学习系列(27)--App缓存管理 无论大型或小型应用,灵活的缓存可以说不仅大大减轻了服务器的压力,而且因为更快速的用户体验而方便了用户. ...

  7. 四、基于HTTPS协议的12306抢票软件设计与实现--水平DNS并发查询分享

    一.基于HTTPS协议的12306抢票软件设计与实现--实现效果  二.基于HTTPS协议的12306抢票软件设计与实现--相关接口以及数据格式 三.基于HTTPS协议的12306抢票软件设计与实现- ...

  8. 网络是怎样连接的--DNS服务器查询原理

    文章目录 3.1 DNS服务器基本工作 3.2 寻找相应的DNS服务器并获取ip地址 3.3 通过缓存加快DNS服务器的响应 3.1 DNS服务器基本工作 DNS服务器的基本工作就是接收来自客户端的查 ...

  9. Windows下模拟dns迭代查询过程

    目录 dns解析过程 迭代与递归 dns解析过程 当在浏览器的搜索栏输入URL(统一资源定位符)时,浏览器的解析过程 完整解析 当在浏览器输入某一IP地址时 (1)浏览器查看查看缓存表里有没有对应的域 ...

最新文章

  1. Windows10 C盘爆满如何清理
  2. OKR为何要跟绩效考核脱离关系?
  3. 现代神经网络要这么用才能创造智能
  4. oracle 强制 断开,ORA-01092 ORACLE 实例终止。强制断开连接 解决方案(下)
  5. UITableView cell自定义视图中插入Table实现复杂界面
  6. 字节跳动-文远知行杯”广东工业大学第十四届程序设计竞赛
  7. TestNg的IReporter接口的使用
  8. 银行存款都有哪些误区,你都有踩坑吗?
  9. Android开发中gitignore文件模板添加
  10. 推荐一款windows下好用的文件夹加密、文件加密软件(含使用说明)
  11. android 坐标度分秒转换工具,经纬度格式转换定位工具
  12. iOS面试题:Socket原理
  13. Vue工程测试Element-UI插件是否可用步骤
  14. 魔界/指环王三部曲(加长版)在线观看免费bt下载
  15. Python实现草莓熊手拿风车和鲜花
  16. 浅谈加密算法 aes
  17. 制作html语言网站全攻略,(网页制作HTML代码全攻略.doc
  18. mt2503 [ShapeEngine]泰语音标字符发生偏移
  19. 新版Edge浏览器怎么长截图?
  20. 8万条数据告诉你:跟着大股东和高管买他家股票,能赚钱吗?【邢不行|量化小讲堂系列60-实战篇】

热门文章

  1. 3D空间转换(位移、旋转、立体呈现)
  2. flink on yarn集群搭建
  3. Java 常见的面试题(设计模式)
  4. Electron打包为.exe格式安装包
  5. springboot引入liquibase
  6. Markdown表格嵌套
  7. 基于 SheetJS js-xlsx 将 Excel 中的表格转为 html 代码
  8. Spring Boot接入Graylog
  9. Django REST Framework教程(10): 限流(throttle)详解与示例
  10. mysql sql 取树结构_MySQL 树形结构 根据指定节点 获取其所有叶子节点