问题
最近维护一套祖传代码,测试时,在遍历hash表时,概率
产生了coredump。 直接原因是访问hash冲突链上的node是个非法指针。很自然的想法,是不是删除节点的时候遗漏摘除链表,导致是野指针残留在hash表上?请 2分钟
想一想还有哪些可能。
先来回顾下hash表的基础知识。
1、hash表简述
链表的结构
hash表实现千差万别,常见的是基于数组拉链法。如下图,hash桶是个数组,冲突链用双向链表实现。
双向链表
在链表中,每一个结点的结构都包括了两部分的内容:数据域和指针域。
arduino
/* Doubly linked list head or element. */
struct list_node {
struct list_node *prev; /* Previous list element. */
struct list_node *next; /* Next list element. */
};
/* 业务数据 */
struct user_data_t {
struct list node; /* 用来存在全局hash表的节点 */
char name[32]; /* user data. */
......
};
链表遍历
查找hash节点时,首先根据hash key
计算hash index
, 即可以定位到数组下表。其次遍历hash冲突链。 如果在遍历的过程中节点可能有增删(其他线程),需采用安全遍历的宏。
scss
/* 链表头 */
struct list_head{
list_node* pstFirst; /* the first element */
} ;
#define LIST_FOREACH_SAFE_RCU(pstList, pstNode, pstNextNode) \
for ((pstNode) = rcu_dereference((pstList)->pstFirst); \
(null != (pstNode)) && ({(pstNextNode) = rcu_dereference((pstNode)->pstNext); true;}); \
(pstNode) = (pstNextNode))
回顾了基本的数据结构后,再来看下hash表上残留释放后内存的几种可能。
2、问题分析
释放后内存
分析coredump文件,如下图,变量hash冲突链 3 时,A,B节点均正常。C节点查看内存头
已经不对。但也能正确访问,根据C节点
的获取下一个节点D(D = C->next
),却不是一个有效指针,直接报非法地址错误
,产生core.
原因分析
1、释放节点未摘除链表
如果存在释放内存,却没有摘除节点,问题是必现流程。 实际排除代码,所有free内存
的地方都删除了节点。链表节点删除代码如下:
rust
void list_remove(struct list_node *node){
node->prev->next = node->next;
node->next->prev = node->prev;
return;
}
2、踩内存
场景一
:当前业务申请的C节点内存,是第三者申请和释放后内存、但是第三者继续持有。第三者异常写操作写坏内存的next域 和 内存头。 这种场景还有个特点,因为第三者这个内存可以被不同业务随机申请走,会有一些其他异常现象。
场景二
:当前内存为C节点持有,但是有其他业务飞踩
,踩到C节点的next域。比如越界写,写到C内存区域。一般这种场景也有个特点,C的整个内存区域可能都不对。不太会是next(8字节)不对,其他都是正常数据。
一般踩内存,是定位问题最后怀疑的方向。踩内存出现的异常,都不是第一现场,定位难度就大多了。
结合问题本身:查看了C的整个内存区域,其他数据域内容没有明显异常。内存头也是个释放后内存特征。且概率复现了几次,都是该业务节点。 所以踩内存的概率就很低。
3、链表重复添加
该问题 最终定位原因就是同一个节点C被重复添加到hash表中。第二次添加节点时,没有检测是否已经存在。
步骤1: 正常添加节点,如上图有A--B--C--D四个节点
步骤2: 再次添加节点C,采用链表头插法。数据结构就变成下面。
有两个问题:1、B的下一个节点是C,成环了。如果遍历会出现死循环。2、D变成了游离节点。
步骤3: 再次把C节点删除和释放。B的next还记录着C节点,就是个释放后内存。就有可能业务拿到C节点内存正常写内容。就会出现异常。
总结
链表作为最简单的数据结构,如果不能正确使用,遇到坑问题就会很难定位。
上面问题解决后,又有另外一个类似问题。 遍历list时访问异常指针,coredump文件,却显示冲突链找不到该节点。当前冲突链,只有两个节点,比如A,B,B的next指针是null。 下一篇继续分析。