标签 内存 下的文章

遭遇“死循环”

昨天看了“外刊IT评论”上的一篇名为《软件编程21法则》的文章,文章中提到的一条法则是:“软件直到被变成产品运行至少6个月后,它最严重的问题才会被发现”,当时表示认同。不过仅仅相隔一天,这条法则就变成了眼前的现实。

今天上午我们的某版本系统在某省出现了故障,该版本在这个省上线恰好将近6个月^_^,系统上线以来一直运行良好,直到这次故障。故障现象为"挂死":所有进程都挂死在某一把锁的lock上。以前出现这种情况多为某个进程加锁后,在锁内异常退出,未能释放锁而导致其他进程挂死。这种"挂死"多是代码中访问非法内存地址导致的,一般都会有core文件dump出来。不过这次出现挂死后,我们并未找到core文件的影子。查看系统运行日志也无果。通过脚本将所有该应用的子进程的运行栈快照收集到一个文件中,然后对这个数据庞大的文件进行分析,以试图找到一些蛛丝马迹。

分析发现绝大多数子进程都挂起了,其运行栈栈顶多为:
lwp_mutex_timedlock (f1444c28, 0)

不过只有一个进程与众不同,它的栈顶是一个我们自己实现的函数,这里暂且称这个函数为foo_func吧。迅速查看foo_func的源码实现,发现一个while循环,第一时间想到:是不是foo_func进入while死循环了?在故障应用主机上用top查看一下系统运行状态,发现确有一个进程占用cpu很高,而且持续很高。pstack一下这个进程,栈顶端果真就是foo_func,“死循环”的推论是正确的。 即使这个进程死循环了,怎么会连累其他进程也停止工作了呢?原因就在于这个死循环是在这个子进程获得锁之后发生的,因为死循环了,导致无法释放这把锁,其他子进程干着急也无可奈何!

foo_func为何能进入死循环?仔细斟酌一下foo_func的代码也不难得出:代码中混用了int和unsigned char,导致数组下标值变为负数,数组访问溢出,读取到的值是随机值,所以死循环也是随机发生的(之前几个月运行都良好也是因为这个原因)。 C语言不是强类型的,int和unsigned char两个宽度不同的类型可以放在一起使用。C编译器帮你做隐式转换,转换规则虽不十分复杂(C99引入了rank概念后,转换规则略就显复杂了),但也很容易犯错,这也是我们常说的C陷阱之一。另外foo_func代码本身的功能逻辑也有漏洞,这里就不细说了。

一次函数设计讨论

近期在考虑对底层函数库进行一些重构,今天下午花了两个小时考量现有的函数库的接口设计,发现目前函数库的实现存在着一个普遍的问题:与特定的内存分配实现耦合的太紧。

我们的应用是多进程结构的,并使用了共享内存这种最快捷的IPC机制,鉴于此很多同事在实现一些数据结构或者算法时可能只考虑到了我们常见的应用场景-多进程共享,而对非共享内存分配的情况考虑不足。那如何将目前某些库函数实现与内存分配之间的强耦合解开呢?针对这道题我发起了一次mail讨论。

题目再重述一下:“目前底层库中的一些数据结构,比如xx_tree、xx_hash_table等,在它们的实现中都会有“分配内存空间”的需求,现有的实现多是直接调用已有的xx_shm_malloc和xx_shm_free在共享内存上动态申请和释放内存,但实际上有些场合我们并不需要在共享内存上分配内存,进程私有堆上的内存完全可以满足需求。如果让大家考虑修改目前xx_tree的实现或重新设计xx_tree的接口,以达到让xx_tree支持多种内存分配策略的目的,你是如额考虑的,请谈谈你的设计思路。"

Mail没发出去多久,就收到了同事A的回复。同事A近期正在对一个老应用的底层库做”翻新“,收到我这封Mail后他产生了共鸣,因为他也遇到了类似的问题。他目前的解决方法是:在xx_tree_t结构体里增加一个成员shared来表示是否在共享内存上申请内存,然后将原xx_tree_new接口改名为内部static接口new_tree,new_tree在申请内存时通过判断shared变量的值来决定到底使用哪种具体的内存分配方式。同样释放内存时也要根据shared的值来判断如何释放内存。原xx_tree_new外部接口通过将shared置为1,并调用new_tree实现;另外新增一个外部接口xx_tree_new_private,选择在进程堆上分配内存的实现方式。他对于这种修改的评价是:"这种方法的好处是原接口没有变化,其它使用这个接口的程序代码无需修改。"

在某些场合下同事A的这种修改方案确也无可厚非,因为有时的确需要考虑对现有代码的影响。但如果该方案是对于xx_tree的一个全新的设计,那显然它没能很好解决我在题目中提出的耦合问题。xx_tree的创建还是依赖于具体的内存分配实现,虽然目前扩展成支持两种内存分配方式了,但付出了新增一个接口,内部增加若干”if…else“判断逻辑的代价。如果我再提供一种新的yy_malloc内存分配机制,那么这种方案下的xx_tree显然无法很好的应对这种变化。

一转眼间又有几封Mail回来,几位同事的思路居然与同事A的都不谋而合,看来大家对于接口变动带来影响还是心有余悸的。

同事A思维很活跃,很快他的第二种方案又公布出来了。其大致思路就是:可以在多种内存分配接口外面套上一个通用的接口,比如void* common_malloc(size, type)和void common_free(ptr)。xx_tree只需增加一个type字段,并在xx_tree_new中传入相应type,xx_tree内部实现一概使用common_malloc/common_free接口,并将type值传入相应接口。这样xx_tree与具体的内存分配机制实现的耦合就解开了。

这种方案中的common_malloc和common_free则似乎成了一种代理,解除了xx_tree与具体内存分配实现之间的耦合,但是这个代理却需要关心到具体的内存分配机制的实现。如果有新增内存分配方式,common_malloc和common_free依然需要修改,仍未达到理想状态。

同事B的Mail接踵而来。Mail中他谈了他的回调函数方案:
定义两个回调函数指针原型如下,
typedef void* (*hook_malloc)(int size);
typedef void (*hook_free)(void *);
修改xx_tree_new的接口,在接口原型中增加两个参数,一个是hook_malloc malloc_func,一个是hook_free free_func。这样在xx_tree的其他接口实现中只需回调malloc_func和free_func即可,它无须关心这两个指针对应的具体实现。

这个方案其实已经接近我的方案,但是我提出的问题是:如果xx_tree中使用calloc和realloc了怎么办?calloc与realloc从语意上与malloc又不同,函数原型上与malloc也无法兼容?当然了最简单的答案就是再定义两个针对calloc和realloc的函数原型,然后再为xx_tree_new增加两个参数。但这样下来参数数量是不是太多了!如果再增加其他语义的分配行为,xx_tree接口还得修改。

讨论到这里已接近下班时间,这个问题依旧留给大家继续思考。其实这个解耦合问题主要是想要大家知道通过面向接口而不是具体实现的编程可以降低甚至消除一些代码耦合,而C语言中”接口“的概念可以通过函数指针体现出来。

我这里也给出一种看似还不错的方案,(函数设计没有最好只有更好^_^,如果你有更好的想法,欢迎交流)
我定义了一个名为xx_allocator_t的结构体,其成员均为函数指针变量,这个xx_allocator_t就类似于Java里的interface,只定义了接口的行为规范(C的函数指针原型)。所有类似xx_tree需要支持多种内存分配方式的数据结构只要支持xx_allocator_t即可(将xx_allocator_t类型变量作为参数传入)。

/* x_allocator.h */
typedef void* (*x_alloc_func)(size_t size);
typedef void  (*x_free_func)(void *ptr);
typedef void* (*x_calloc_func)(size_t nmemb, size_t size);
typedef void* (*x_realloc_func)(void *ptr, size_t size);

struct xx_allocator_t {
        x_alloc_func         af;
        x_free_func          ff;
        x_calloc_func        cf;
        x_realloc_func       rf;
};

业务层使用xx_tree的方法:
tree = xx_tree_new(&(struct xx_allocator_t){.af = malloc, .ff = free});
这里利用C99中的结构体赋值方式,即使你为xx_allocator_t增加一种新行为,这里的代码也不需要做什么修改。

这次讨论感觉大家都动了脑并参与了进来,达到了预期的效果,大家也很喜欢,我觉得可继续做下去,至于形式上可灵活变通。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats