通常我们认为一旦内存写溢出,程序就很容易崩溃。所以服务器上通常会对一些重要进程做脚本保护,一旦崩溃立即重新拉起。
  最近发现我们一个公共服务内存写溢出时程序没有崩溃,而是卡死了。
  为了深入分析原因,我们仔细review了glibc的代码,并发现一个较为隐蔽的bug。
    
  先来看这个卡死的程序堆栈(64位环境,下同):

  可以看到在free函数中使用了锁。

  那么再来看看glibc中free函数的主要代码:

  这段代码相当简单,不用过多解释。

  再对比上面的堆栈,可以推测流程大概是这样的:frame 9释放内存时发现内存数据校验有误所以进行出错输出,当写syslog时需要取本地时间,而在取时区信息的函数里面也有free函数调用,所以到frame 2释放内存想要再次获取锁的时候程序死锁了。
  这应该属于glibc的bug了,虽然这个bug首先要由程序员的bug来触发。
 
  为了进一步确认glibc的这个问题,我们继续深入阅读glibc的代码以重现之。
  首先,为什么内存写越界会导致free出错?解答这个问题前我们先简单说说一些相关的malloc分配内存原理。
  跟一些人想象不同的是,并不是每次malloc调用一定导致内存分配,因为当内存释放时glibc会将内存先保留到空闲队列当中,下次有malloc调用时可以找一个合适的内存块直接返回,这样就避免了真正从系统分配内存的系统调用开销。glibc需要管理这些空闲内存块,那么就需要一个相应的数据结构,这个数据结构定义如下:

  映射到内存示意图上如下图所示:

  可以看到,我们每次malloc返回的指针并不是内存块的首指针,前面还有两个size_t大小的参数,对于非空闲内存而言size参数最为重要。size参数存放着整个chunk的大小,由于物理内存的分配是要做字节对齐的,所以size参数的低位用不上,便作为flag使用。

  内存写溢出,通常就是把后一个chunk的size参数写坏了。
  size被写坏,有两种结果。一种是free函数能检查出这个错误,程序就会先输出一些错误信息然后abort;一种是free函数无法检查出这个错误,程序便往往会直接crash。
  根据最上面的堆栈推测,诱发bug的是前一种情况。我们的测试程序将会直接分配两块内存,并对第二块内存chunk的size参数做简单修改,以触发情况一。
  顺便说一句,windows内存分配跟linux比较类似,也是将内存块大小存放在malloc返回的指针位置之前。DEBUG模式下,编译器还会在实际分配内存的两端放两个特殊值,这样在内存回收时就可以检测到内存写溢出的问题。
    
  其次,当free函数检查到size异常以后,会调用malloc_printerr输出一些错误信息,但它并不一定会写syslog。
  查看__libc_message的代码可以发现,出现错误以后,glibc会先尝试将错误信息写入到stderr或tty,如果写入失败,才会去写syslog(代码有点啰嗦就不贴了)。
  要模拟这个情况,只需将环境变量"LIBC_FATAL_STDERR_"设为1迫使出错时写stderr,然后将stderr关闭即可。通常daemon程序很容易处在这样的状态。
    
  再次,查看tzset_internal的代码,我们发现导致free操作的原因是静态变量static char* old_tz释放导致的。
  old_tz存放了上一次调用tzset_internal时使用的时区字符串。如果再次调用tzset_internal时,时区不变就复用;如果不同,则free掉旧的字符串,strdup新的字符串,而strdup里面malloc了新字符串所需的内存块。
  要模拟这个情况只需先设法给old_tz一个初值,然后再做内存释放触发free(old_tz)即可。要给old_tz设初值只需先调用相关的时间函数即可,例如localtime这个函数经常就被用到,old_tz会初始化为默认值"/etc/localtime"。当malloc_printerr一步步调用到tzset_internal时,glibc会从环境变量"TZ"读取新的时区字符串,通常大多数服务器是没设置这个环境变量的,所以新tz就是空,从而导致"free(old_tz); old_tz = NULL;"这样的操作。
    
  所以我们的简单演示代码如下:

  g++ -pg -g test.cpp编译得到可执行程序a.out。

  使用gdb运行此程序,如预期般的死锁。
  查看堆栈如下:

  程序堆栈跟文首的完全相同。至此问题得到确认。

    
  我简单查看了一下glibc的历史版本代码,这个bug在2.4到2.8的版本上都存在。当然这个bug首先需要程序员犯了内存写溢出错误才会诱发,所以这并不是严重bug,大家只要知道了自然也可结合实际情况做防范。比如检查进程是否正常不能光看进程是否存在,还需用工具做收发包检测,或者查看定时日志是否一直有输出之类。
  就这个问题本身来看,glibc确实还可以做得更好,例如应该进一步缩小锁的作用域,既提升并发性能,又可降低作用域内其他函数交叉调用引发的死锁风险;另外,个人认为tzset_internal中完全没必要动态分配内存,给old_tz一个固定大小的内存比如256byte应该基本上就可以了。

深入分析glibc内存释放时的死锁bug的更多相关文章

  1. SetProcessWorkingSetSize 和内存释放

    http://hi.baidu.com/taobaoshoping/item/07410c4b6d6d9d0d6dc2f084 在应用程序中,往往为了释放内存等,使用一些函数,其实,对于内存操作函数要 ...

  2. glibc 内存申请和释放及堆连续检查

    C语言有两种内存申请方式: 1.静态申请:当你声明全局或静态变量的时候,会用到静态申请内存.静态申请的内存有固定的空间大小.空间只在程序开始的时候申请一次,并且不再释放(除非程序结束). 2.自动申请 ...

  3. glibc内存管理那些事儿

    本文转载自glibc内存管理那些事儿 Linux内存空间简介 32位Linux平台下进程虚拟地址空间分布如下图: 进程虚拟地址空间分布 图中,0xC0000000开始的最高1G空间是内核地址空间,剩下 ...

  4. FreeOnTerminate 的线程在线程管理类的Destroy释放时手工释放的问题

    这个问题折腾了我整整一天. 有一个线程管理类,集中管理所有新建的线程, 线程统一在创建时标识 FreeOnTerminate 为 True. 因为有的线程是不限次循环的,所以在管理类最后 Destro ...

  5. glibc内存泄露以及TCmalloc 简单分析

    最近开发一个私人程序时碰到了严重的内存问题,具体表现为:进程占用的内存会随着访问高峰不断上升,直到发生OOM被kill为止.我们使用valgrind等工具进行检查发现程序并无内存泄露,经过仔细调查我们 ...

  6. 关于ie的内存泄漏与javascript内存释放

    最近做一个公司的业务系统,公司要求能尽可能的与c/s近似,也就是如c/s一样,点击文本框可以弹出此项目的相关内容,进行选择输入.     我使用了弹出窗口,然后在子窗口双击选中项目,把选中的值返回给父 ...

  7. Javascript IE 内存释放

    一个内存释放的实例 <SCRIPT LANGUAGE="JavaScript"><!--strTest = "1";for ( var i = ...

  8. 《Glibc内存管理》笔记DAY5

    目录 分箱式内存管理 Unsorted bin Fast bins 核心结构体分析 malloc_state 内容来源 分箱式内存管理 Unsorted bin   Unsorted bin 可以看作 ...

  9. 《Glibc内存管理》笔记DAY2

    目录 Ptmalloc内存管理设计 Main_arena 与 non_main_arena chunk 的组织 空闲 chunk 容器 sbrk 与 mmap 内存分配概述 内存回收概述 边界标记法 ...

随机推荐

  1. [Go]结构体及其方法

    结构体类型可以包含若干字段,每个字段通常都需要有确切的名字和类型.也可以不包含任何字段,这样并不是没有意义的,因为还可以为这些类型关联上一些方法,这里可以把方法看作事函数的特殊版本. 函数事独立的程序 ...

  2. hdu 2295 dlx重复覆盖+二分答案

    题目大意: 有一堆雷达工作站,安放至多k个人在这些工作站中,找到一个最小的雷达监控半径可以使k个工作人所在的雷达工作站覆盖所有城市 二分半径的答案,每次利用dlx的重复覆盖来判断这个答案是否正确 #i ...

  3. bzoj 1500 [NOI 2005] 维修数列

    题目大意不多说了 貌似每个苦逼的acmer都要做一下这个splay树的模版题目吧 还是有很多操作的,估计够以后当模版了.... #include <cstdio> #include < ...

  4. [luoguP2513] [HAOI2009]逆序对数列(DP)

    传送门 f[i][j]表示前i个数,逆序对数为j的答案 则DP方程为: f[1][0] = 1; for(i = 2; i <= n; i++) for(j = 0; j <= m; j+ ...

  5. 洛谷P2414 - [NOI2011]阿狸的打字机

    Portal Description 首先给出一个只包含小写字母和'B'.'P'的操作序列\(s_0(|s_0|\leq10^5)\).初始时我们有一个空串\(t\),依次按\(s_0\)的每一位进行 ...

  6. 学习日常笔记<day15>mysql基础

    1.数据库入门 1.1数据库软件 数据库:俗称数据的仓库,方便管理数据的软件(或程序) 1.2市面上数据库软件 Oracle,甲骨文公司的产品. 当前最流行应用最广泛的数据库软件.和java语言兼容非 ...

  7. redis连接数据库进行操作

    该项目需要的类目录 1.首先我们需要创建我们的实体类 2.放置我们的dao层,在里面写入方法 3.配置类Appconfig需要加入我们的JdbcTemplate方法,因为我们用的是spring,所以需 ...

  8. html5 拖拽元素

    利用html5实现元素的拖拽,让拖动元素在指定的容器中拖动. 注意点:1.被拖元素要设置拖动属性.draggable="true" 2.容器元素要设置阻止默认事件处理 实现效果图如 ...

  9. SqlSugar最容易使用的ORM

    SqlSugar官网

  10. 集群FULL GC导致服务不可用

    FULL GC会导致stop-the-world,频繁的FULL GC会影响系统的可用性.stackoverflow上有个提问是这么描述这个问题的:当服务器集群批量启动后,执行FULL GC的频率和时 ...