Android DNS之查询结果缓存
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之查询结果缓存相关推荐
- Android DNS解析的过程
Android DNS解析的过程 DNS解析概念 DNS的全称是domain name system,即域名系统.DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的去访问 ...
- (两百九十二) Android DNS缓存学习
参考文档 Android DNS缓存时长的探索_cc0410的博客-CSDN博客_android dns缓存时间 https://blog.csdn.net/xiaoyu_750516366/cate ...
- Android DNS机制
1,DNS DNS全称Domain Name System(域名系统),顾名思义,就是把域名解析为指向的IP,让人们通过注册的域名可以方便的访问到网址的一种服务.域名解析就是域名到IP地址的转换过程. ...
- Android DNS解析过程
前言 一次排查接口404问题,引伸的Android DNS解析过程,简单分析总结一下 1.首先明白DNS解析流程 操作系统检查自身本地的hosts文件是否有这个网址的映射关系,如果有,直接返回完成域名 ...
- linux关闭dns迭代查询,DNS查询和响应过程递归和迭代的使用
需要了解DNS查询过程的递归和迭代的机制,找到了RFC的相关章节, 对这个进行了简单的翻译(水平有限),可以留下来做个参考. RFC 1034 4.3.1. Queries and responses ...
- Android学习系列(27)--App缓存管理
随笔- 53 文章- 10 评论- 1064 Android学习系列(27)--App缓存管理 无论大型或小型应用,灵活的缓存可以说不仅大大减轻了服务器的压力,而且因为更快速的用户体验而方便了用户. ...
- 四、基于HTTPS协议的12306抢票软件设计与实现--水平DNS并发查询分享
一.基于HTTPS协议的12306抢票软件设计与实现--实现效果 二.基于HTTPS协议的12306抢票软件设计与实现--相关接口以及数据格式 三.基于HTTPS协议的12306抢票软件设计与实现- ...
- 网络是怎样连接的--DNS服务器查询原理
文章目录 3.1 DNS服务器基本工作 3.2 寻找相应的DNS服务器并获取ip地址 3.3 通过缓存加快DNS服务器的响应 3.1 DNS服务器基本工作 DNS服务器的基本工作就是接收来自客户端的查 ...
- Windows下模拟dns迭代查询过程
目录 dns解析过程 迭代与递归 dns解析过程 当在浏览器的搜索栏输入URL(统一资源定位符)时,浏览器的解析过程 完整解析 当在浏览器输入某一IP地址时 (1)浏览器查看查看缓存表里有没有对应的域 ...
最新文章
- Windows10 C盘爆满如何清理
- OKR为何要跟绩效考核脱离关系?
- 现代神经网络要这么用才能创造智能
- oracle 强制 断开,ORA-01092 ORACLE 实例终止。强制断开连接 解决方案(下)
- UITableView cell自定义视图中插入Table实现复杂界面
- 字节跳动-文远知行杯”广东工业大学第十四届程序设计竞赛
- TestNg的IReporter接口的使用
- 银行存款都有哪些误区,你都有踩坑吗?
- Android开发中gitignore文件模板添加
- 推荐一款windows下好用的文件夹加密、文件加密软件(含使用说明)
- android 坐标度分秒转换工具,经纬度格式转换定位工具
- iOS面试题:Socket原理
- Vue工程测试Element-UI插件是否可用步骤
- 魔界/指环王三部曲(加长版)在线观看免费bt下载
- Python实现草莓熊手拿风车和鲜花
- 浅谈加密算法 aes
- 制作html语言网站全攻略,(网页制作HTML代码全攻略.doc
- mt2503 [ShapeEngine]泰语音标字符发生偏移
- 新版Edge浏览器怎么长截图?
- 8万条数据告诉你:跟着大股东和高管买他家股票,能赚钱吗?【邢不行|量化小讲堂系列60-实战篇】
热门文章
- 3D空间转换(位移、旋转、立体呈现)
- flink on yarn集群搭建
- Java 常见的面试题(设计模式)
- Electron打包为.exe格式安装包
- springboot引入liquibase
- Markdown表格嵌套
- 基于 SheetJS js-xlsx 将 Excel 中的表格转为 html 代码
- Spring Boot接入Graylog
- Django REST Framework教程(10): 限流(throttle)详解与示例
- mysql sql 取树结构_MySQL 树形结构 根据指定节点 获取其所有叶子节点