余超颖又一个WordPress站点

浏览: 741

【藏经阁】C/C++程序性能案例分析SegmentFault案例分析-百度QA我们的产品历史上发生了性能/稳定性相关的问题,理论上我们每个人都站在前辈的肩膀上,??


【藏经阁】C/C++程序性能案例分析SegmentFault案例分析-百度QA
我们的产品历史上发生了性能/稳定性相关的问题,理论上我们每个人都站在前辈的肩膀上,遇到问题时应该可以更快的解决。可是现实是,我们不断在重复走之前走过的弯路!
理论上,我们现在的性能测试方案应该非常的完美,因为产品线上已经有了无数的积累,可是还是我们的方案还是有这样那样的缺陷。我们还在爬别人爬过的坑!
我们需要对历史的经验进行收集、整理、分析,帮助我们自己重新站在前辈的肩膀上!
我们需要建立这类经验的总结模式,长长久久的做下去,发挥更大更长远的作用!
我们需要踏踏实实的从小事做起,从现在做起,为后辈的同学种树,让他们乘凉!
本文中的案例都是从各种产品线中收集的各类真实的案例,通过总结分析,希望大家能避免这些问题在自己的产品线再发生!
2 SegmentFault案例分析
对于C/C++程序员而言,在开发程序的过程中,遇到最多的问题莫过于程序core dump了,也就是常说的程序core了监狱大亨4。那么程序出core的情况有哪些的?如果程序core了之后,我们应该如何对这类问题进行定位呢?经过大量的案例收集、筛选和分析,产出了这份分析报告,希望大家从中了解程序出core的常见原因和定位方法。
▌2.1 SegmengFault问题分类及定位方法
为了给大家一个直观的认识,我们首先分析一下程序出core的常见原因及分类方法。通过这些分类,我们可以对分core的原因、定位方法有初步的认识。
通常在程序出core之后,最常用的工具就是gdb,通过gdb工具分析core文件,可以得到程序在崩溃前的堆栈信息。通过对堆栈信息的分析,我们可以了解到程序的运行状态,从而分析判断程序崩溃的原因。
根据core文件中程序的堆栈信息的完整程度、稳定复现的程度,可以将定位方法分为三类。每类问题的定位方法和原因也有所区别。
▌2.1.1堆栈完整且稳定
大多数core文件都保留了完整的堆栈,我们可以使用gdb看到core在哪里,这类问题的定位通常都比较快速而容易。常见的错误有:
堆栈行本身代码有错误
很多时候,结合出core文件,从堆栈行中可以直接看出错误原因。如,除零错误、参数为空指针、迭代器失效等问题。这类问题通过堆栈信息,直接可以看出core的原因和为止。
通常在出现这类错误时,在gdb时程序会给出明确的提示信息。如,除零错误,gdb时提示的信息是“Program terminated with signal 8, Arithmetic exception”。结合堆栈信息中的函数调用关系灭茶苦茶,可以很快的定位问题。
堆栈行所在函数上下文有错误
有些错误,并不是在有bug的代码处立即出core,而是在后面的逻辑中出错。只需要从出core的代码行往上多看几行,往往就行找到问题代码。比如:变量未初始化、内存溢出导致部分变量值被破坏。
堆栈行所在函数没有错误
我们也经常会碰到堆栈行所在函数是稳定版本的基础库或某些久经考验的代码段,亦或是简单得几乎不可能出bug的代码九霄惊魂,这时,我们就要看堆栈中更上层调用者的代码了。
对于基础库等稳定代码陈俊言,有必要了解其使用规则,检查应用场景和使用方式是否违反了基础库的要求。务必记得查看基础库的相关wiki资料或联系其提供者,很有可能基础库提供者早已公布了用户可能踩到的坑以及对应的解决方案,这比自己慢慢摸索的效率要高的多。
另外一种可能就是野指针,特别是在使用系统库时。一个理论上不会失败的操作居然core了,那么在review代码时需要
1)重点检查是否存在有野指针的风险;
2)升级后的代码是否使用了可能被别人释放的资源;
3)升级后的代码是否释放了别人可能使用的资源。通常需要通过增加开关和日志甚至回滚代码的方式进行分析;
▌2.1.2堆栈完整但不稳定
有的时候,程序时好时坏,出core时堆栈的内容也并不稳定,这类问题的根因和堆栈行可能并没有紧密关联,排查有一些困难。
这类问题,往往和多线程相关,可以尝试将并发数改为1,观察能否避免出现此类问题从而明确问题类型。应当重点检查各次出core的堆栈行是否存在多线程冲突风险,适当加锁加日志可以有效辅助问题排查。不过因为日志打印需要消耗一些性能,有可能增加详细日志打印后反而不再出core,这时可以该日志所在上下文中重点review。
如果在串行时仍然出现此类随机core问题,则很有可能是某处存在内存越界问题陆苹,写坏了少量数据。此类问题的排查方式和前述的野指针问题有些相似,重点在于review近期改动的代码,通过增加开关和日志来辅助定位。
▌2.1.3堆栈被破坏
最坏的情况无非是堆栈完全被写坏,这类问题的根因很固定,但具体到代码上却五花八门,排查极为困难。
1)编译环境与运行环境如果近期存在对依赖库和编译参数的变更,则它们的嫌疑非常大,建议首先尝试回滚相关修改,验证问题是否恢复。对于新的程序或新的机器,编译环境和运行环境的差异往往也是此类问题的罪魁祸首,应当仔细排查其差异,有理数是什么 或使用一致的环境进行验证。2)内存越界排除环境问题后,堆栈破坏的根本原因基本上可以确定是内存越界,通常是代码某处不小心写坏内存,而且是直接连函数调用堆栈都写坏了,这类问题的排查某种程度上可以说是基本靠猜。
排查方式与前述的排查野指针以及内存越界的方法比较相似,但一个比较大的难点是,没有堆栈信息,想猜都难以下手。
二分回滚代码是比较推荐的一种排查方式,这样可以将问题锁定到某一些具体的代码变更,之后再仔细review这部分代码诸神学徒 ,就比较有希望找到问题的根因了。
▌2.2案例分析
C/C++程序出core的行为相对固定,通过对我们收集问题的分析,共将程序出core的原因分为了5类。下面将对这些问题进行分类,发现问题的共性和特性,为以后的问题定位提供思路。由于篇幅问题,本次我们先介绍3类问题。
▌2.2.1异常处理不合理
程序中总是有很多地方涉及异常处理,稍有不慎就会给程序埋下出core的隐患。
函数的返回值做适当的处理
对于所有可能失败的操作,都必须检查返回值,当返回值不符合预期时,应当进行合适的处理,通常进行的操作有:
记录日志
中断操作
必要的数据清理
跳转到到合适的位置
例如代码片段2-1中,sqas_res是一个线程级数据结构,线程在处理每个query时,将中间数据存储在线程数据中。在函数ub_client_originvite执行成功时,sqas_res->knowledge被正确赋值;而当该函数执行失败时,程序在打印日志后,直接退出函数调用。而sqas_res->knowledge的值将是线程处理上个query的值。如果执行过程中使用了这个字段的值,将会用到脏数据。而如果这个情况是发生在初始化阶段,那么程序就可能出core。

代码片段2-1
指针为空
在C/C++代码中,最容易出问题的就是指针,比如申请内存或打开文件等操作,没有判断返回的指针是否为空就直接使用;比如清理数据时,释放了指针所指向的内存地址,但没有将指针置空,导致其成为野指针。
异常未正常捕获
有的时候,执行失败是通过抛出异常来传递而不是通过返回值传递,这时应当正确进行捕获和异常处理,避免程序直接崩溃。
▌2.2.2多线程/多进程随着多核CPU的普及,单线程的程序现在已经基本绝迹了,而多线程/多进程编程,很考验开发者的基本功,在大型程序中,一不小心就会在并发上栽跟头。
使用不可重入函数
最常见的问题,是在多线程中使用了非线程安全的库函数藤黄阁序,比如strtok、gethostbyname等,在多线程编程中,应当时刻保持警惕:这个函数是否是线程安全的陈财明?它有没有对应的线程安全版本?通常来讲,多数非线程安全的库函数,都会有一个带_r后缀的线程安全版本。
临界区保护不到位
试图在多线程中访问修改公共资源时,必须合理设计临界区,既要保证多个线程互不冲突,又要尽量减小临界区以防止性能下降,还要避免死锁等问题。代码片段2-2是一段没有进行恰当的临界区保护的代码。

代码片段2-2
代码片段2-2中的代码会在多个线程中执行,执行这段代码的第一个线程会执行list的destroy操作,而后面的线程再执行这段代码时,会重复执行destory操作,从而导致程序出core。显然上面的代码,应该根据list的状态设置保护区。
▌2.2.3第三方库
实际开发工作中,代码复用是非常有必要的,开发者往往需要使用第三方库(包括外部开源库以及由其它部门或产品团队开发的公共库),这时十三狼,应当至少阅读对应的文档和wiki,熟悉接口,明确使用时需要遵守的规则,并且对其可能存在的坑有所了解。
protobuf相关
google出品的protobuf在业界广泛使用,时常见到有开发者直接修改了proto文件中一些字段的类型,并导致上线后上下游无法兼容产生问题,如果遵守protobuf相关的增删改字段的规则,根本不会发生这种问题。
代码片段2-3就是一个例子。代码升级的过程中,将UNKNOWN_ERROR字段的含义进行了变更,下游程序在解析数据时会发生语义错误。
▌2.2.4程序设计不合理
很多时候,算法设计或实现上考虑不周,会直接导致程序出问题。
程序OOM
最常见的问题,就是申请内存却忘记释放,导致内存泄漏,最终OOM出core。这包括直接的new出对象没有free,或者创建vector不断添加元素却忘记在该清空时进行清空。
代码片段2-3就是一个没有释放内存的例子,在函数调用失败时,没有进行适当的操作,导致内存泄露七怪成神之路 。快速的内存泄露会导致程序OOM,从而出core。

代码片段2-3
数值类型错误
类型问题也是程序设计中容易出错的地方。
比如在有符号和无符号整数之间转换,很可能导致丢失状态蜀山异闻录。下面的代码片段是一个案例中导致程序出core的原因。

代码片段2-4
在代码片段2-4中,lmpos是无符号类型,取值永远不会小于0,所以第17行中的条件判断永远不成立。一旦create_dynword函数调用失败,将给程序的运行引入不确定性的影响乐活家庭 。
pthread_t的类型
对于pthread_t等类型,在不同位数的机器上,其占用的内存是有差异的,如果将其强制转换为32位整数,则在64位机器上会出错。

代码片段2-5
在代码片段2-5中,pthread_t类型赋值给了u_int32,而pthread_t的类型为long,在32为系统中long与int型都存储为4字节,取值范围相同。而在64微系统上,察猜long类型存储为8字节。在64位系统中,当线程id的数值大于MAX_INT时,转换为int时会出现负数,从而导致程序出core解婕翎。
死循环
涉及递归和循环时,如果设置的退出条件不合理,很可能引起无限递归或者死循环,一方面造成性能下降,另一方面,由于代码中或多或少,经常涉及到内存申请,无限次的执行会很快导致栈溢出或OOM,程序会直接崩溃。

代码片段2-6
代码片段2-6中,使用u_int8类型的下标遍历vector,由于u_int8的最大取值为255,如果vector.size()大于255,那么此处将形成死循环。如果在死循环内部有内存申请操作,将导致OOM(堆上申请内存)或者栈溢出(栈上申请空间),这可能导致内存泄露和程序出core。
程序的初始化和退出逻辑
程序的初始化和退出逻辑,如果不精心设计,也可能存在问题,比如全局变量初始化顺序和退出时的资源释放顺序,一旦与依赖关系不一致,就会导致程序崩溃。

代码片段2-7
在代码2-7中,在实际执行的过程中有很大的概率会出core。根本原因为,main函数在退出时,先关闭主线程的句柄,然后再清理子线程。在子线程中使用了已经关闭了的句柄,导致程序出core。
▌2.2.5系统库
C/C++提供了很多非常方便的基础库,但是在使用过程中,如果用法不当,也很容易引发问题。
排序函数
最常见的一种错误就是排序函数,用户自定义的cmp函数,必须保证两个相等的值比较时返回false,否则将导致快排过程中不断自增,直至越界。在我们收集的案例中,与sort函数相关的问题就有4例。希望大家特别的注意。
printf函数的使用
printf是一个常用的函数,很多人用它输出一个字符串变量内容时,会直接写printf(str),这是很危险的,如果字符串中包含了%,将被识别为格式化参数,并可能输出预期外的值甚至直接出core芈琰,正确的写法是传入%s作为fmt,将要输出的字符串作为参数:printf(“%s”, str)。
C语言库的字符串函数
字符串拷贝是个很常见的操作,我们也有strcpy、strncpy等系统库来完成相关工作,但是,这些函数都是很危险的,在有些场合下会出core。

代码片段2-8
在代码片段2-8所示的两个case中,如果拷贝或输出的源数据大于目标地址空间,那么就出出现数据越界。幸运的话,程序会core出来。通常,我们应当使用更安全的snprintf来实现相关的功能。
容器的迭代器失效
vector等容器都提供了迭代器以方便遍历操作,必须注意的是,不能在迭代过程中对容器进行增删操作,这会导致迭代器失效。
比如在代码片段2-9中,程序在遍历vector中数据元素的同时,会删除vector中的元素。每一次删除数据时,都会导致迭代器失效,从而可能产生越界访问或漏掉数据的可能性。当需要对数据进行删除时,可以考虑使用list,map等不存在迭代器失效的数据结构刘艺晗。另外,从效率方面考虑,对vector类型进行大量的删除操作,也是一种非常低效的做法。

代码片段2-9
select函数的使用
因为linux上的fd从1开始,使用中可能会使用1-1024手指速算法,但是,select通常只能处理到1023,一旦传入1024,将可能出core。

代码片段2-10
nginx插件
nginx是一款广泛使用的http和反向代理服务器梵妳卡波,很多时候我们会根据需求开发nginx插件,曾有开发者使用nginx共享内存时,没有检查同名内存池直接开辟新内存池,导致reload时发生内存泄漏,最终OOM。
函数返回值
有些公共库,它的一个函数在成功时返回可用的指针,在失败时返回的却不是空指针,而是某个错误码,需要能够通过给定的函数解析出错误的原因。如果使用者对使用方法不熟悉,只进行了空指针检查,导致程序出错。例如在公共库的mcpack中就有这样一系列函数,如pt = mc_pack_get_str(req_pack, "pt")这样的调用。pt可以是一个指针,也可以是一个错误码,如果不做判断,直接把pt当指针使用在不合适的地方,程序就出出core。
对于公共库的问题,建议开发者多查阅文档,了解各种公共库的标准用法。
下期预告
C/C++程序性能案例分析-内存问题分析
我们整理了一些内存问题,希望通过对这些问题的分析,大家能够了解到程序内存问题的类型以及一些基础知识。敬请期待~~
作者介绍

祝兴昌
百度资深测试攻城狮,多年从事广告检索系统的性能方向研究,短期目标是打造后端系统的性能质量保障体系。

全文详见:https://www.6596.org/10112.html

TOP