Linux Debugging(八): core真的那么难以追踪吗?

本周遇到了好几个core都很有典型性。在这里和大家分享下。

相信有过Linux编程经验的人,肯定都遇到过。感觉周围人很多对core有天然的恐惧感,尤其对刚入行不久的同学来说。当然了,也有工作好几年看到core也束手无策的。今天就分析一下,core,其实大部分都是很容易解决的。如果一个core很难以复现,那么说明还是很复杂的,算是Corner case,可能需要很长时间,脑子里要有很好的运行时状态才可以(阅读源码,学习的是逻辑;将源码对应到运行时的状态,分析一些状态机的转换,再去分析可能会发生的情况)。相信前几篇文章会对这种Corner case的分析与解决打下比较好的基础。

相反,那种每次必现,或者复现比率非常高的case,是非常容易解决的。

多线程必然出core?

如果是你新加入的代码引入的core,实际上非常容易解决的,简单的对比一下修改的diff,然后看一下是否有比较低级的错误。如果发现不了,看是否是多线程的问题?单线程如果没有出core,改成多线程就出core,那么就说明多线程竞争某些变量了:同时修改某些变量导致出问题。这个时候你可能第一反应会加锁。我本人非常反感加锁;即使你加锁的粒度很小,作用域也够小,但是只要是加锁,就代表有阻塞,就代表维护起来会很麻烦。这些共享变量真的那么值得加锁吗?可否换成局部变量?如果他是一块动态内存,为了调用某个接口时不要频繁申请释放内存(比如这个接口每秒几千次的调用),那么初始化时候申请一块内存是绝对合理的:请把它设置为线程变量吧。每个线程初始化时候申请这块内存。

当然了你如果实现的是一个框架或者架构调用的接口,这个接口要做到线程安全的。那么看起来你并不能控制这个线程什么时候启动;线程数目会是多少个,那么就没有办法了吗?

实际上,方法有很多,比如,你可以在一个map中维护一个“线程”变量的对应关系

__gnu_cxx::hash_map< pid_t, void *> thread_data_map;
void * thread_buffer;
std::map<pid_t, void *>::iterator it;
lock
it = thread_data_map.find(pid);
if (it == thread_data_map.end()) {
    //init "thread data"
    thread_data_map[pid] = create_buffer();
} else {
    thread_buffer = it->second;
}
unlock

这里不得不使用了一个锁。实际上由于线程数是有限的,因此这个效率还好。我本周实现了一个qps可以达到2000+的在线应用,基本上锁的代价在整个的call stack中可以忽略不计。

当然了,比较好的框架可能会提供OnThreadInit这种接口,那么在这边申请线程变量吧:

int pthread_setspecific (pthread_key_t key, const void *value);

在实现逻辑的函数获取该变量即可:

void *pthread_getspecific (pthread_key_t key);

什么时候要使用线程变量?看多线程下是否对该变量有写操作,如果有就要申请线程变量(或者加锁),否则必然出core。

不要给自己埋下一个坑:

今天一个同学的core看起来是做了一个“优化”,节省了申请变量的时间。

void init() {
    my_struct * some_var;
    ...
    some_var->res = new some_res;
    some_var->res->set_value1(some_common_value1);
    some_var->res->set_value2(some_common_value2);
}

void * thread_func(my_struct * some_var) {

    some_var->res->set_value3(value_3);
   ...
}

set_value3就是一个出core的原因。这个也是一个典型的多线程必然出core的case。实际上,res没必要提前申请吧。把它改成一个局部变量,性能几乎没有的损耗。当然了,如果这个资源很大,那么就当成线程变量吧。

那么如何分析core?

实际上,上述的场景出现的core如果仅从core本身,可能一时不好排查问题出在哪里;而且core的位置可能还不一样,不可避免的出现替罪羊;比如调用第三方的模块,实际上是自己的全局变量导致到了第三方的调用栈时出core了。因此一定要排查自己的处理是否正确。

确定调用的接口是否线程安全;如果是你的同事,你得确定他说的是对的。就像今天又另外的一个core,调用者坚称他写的是线程安全的,仿佛不是线程安全的就像是代码写的不好似得;最后排查出他写的代码不是线程安全时,他问你什么是线程安全的。

那么回答一下,如何分析core?

首先了解清楚core出现的应用场景,比如长句子处理会有core;多线程处理会有core;特殊字符会有core;这个QA会给你一个比较清楚的说明。然后通过core的call stack去大概确定大概的源码位置。

源码面前,了无秘密。

你读源码,它展现的是逻辑;但是你脑中,要有个runtime的运行时调用栈,多线程的调度;天马行空。

当然了,还是解决不了,开始从core得到更详细的信息吧!看看调用栈的参数是什么,切换一个线程看看其他的几个线程的frame是什么。

还解决不了?

那么它是Corner case,只要把应用在core后立即启动就行了。记得EMC有个bug的选项是unable to root cause, 形容的很贴切。同时不要忘记自我安慰:码的码多了,肯定有bug,谁能保证服务100%呢?

时间: 2024-07-31 21:37:49

Linux Debugging(八): core真的那么难以追踪吗?的相关文章

Linux Debugging(五): coredump 分析入门

        作为工作几年的老程序猿,肯定会遇到coredump,log severity设置的比较高,导致可用的log无法分析问题所在. 更悲剧的是,这个问题不好复现!所以现在你手头唯一的线索就是这个程序的尸体:coredump.你不得不通过它,来寻找问题根源.       通过上几篇文章,我们知道了函数参数是如何传递的,和函数调用时栈是如何变化的:当然了还有AT&T的汇编基础,这些,已经可以使我们具备了一定的调试基础.其实,很多调试还是需要经验+感觉的.当然说这句话可能会被打.但是你不得不

Linux Debugging(三): C++函数调用的参数传递方法总结(通过gdb+反汇编)

         上一篇文章<Linux Debugging:使用反汇编理解C++程序函数调用栈>没想到能得到那么多人的喜爱,因为那篇文章是以32位的C++普通函数(非类成员函数)为例子写的,因此只是一个特殊的例子.本文将函数调用时的参数传递方法进行一下总结.总结将为C++普通函数.类成员函数:32位和64位进行总结.         建议还是读一下Linux Debugging:使用反汇编理解C++程序函数调用栈,这样本文的结论将非常容易理解,将非常好的为CoreDump分析开一个好头.而且

Linux Debugging(二): 熟悉AT&amp;amp;T汇编语言

    没想到<Linux Debugging:使用反汇编理解C++程序函数调用栈>发表了收到了大家的欢迎.但是有网友留言说不熟悉汇编,因此本书列了汇编的基础语法.这些对于我们平时的调试应该是够用了. 1 AT&T与Intel汇编语法对比     本科时候大家学的基本上都是Intel的8086汇编语言,微软采用的就是这种格式的汇编.GCC采用的是AT&T的汇编格式, 也叫GAS格式(Gnu ASembler GNU汇编器). 1.寄存器命名不同 AT&T Intel 说

linux下用core和gdb查询出现&quot;段错误&quot;的地方【转】

转自:http://blog.chinaunix.net/uid-30091091-id-5754288.html 原文地址:linux下用core和gdb查询出现"段错误"的地方 作者:草根老师   有些时候我们在一段C代码的时候,由于对一个非法内存进行了操作,在程序运行的过程中,出现了"段错误".   呵呵,这种问题我想很多人会经常遇到.遇到这种问题是非常无语的,只是提示了"段错误",接着什么都没 有,如果我们一味的去看代码找太疼苦了,因为我

Linux Debugging(六): 动态库注入、ltrace、strace、Valgrind

实际上,Linux的调试方法非常多,针对不同的问题,不同的场景,不同的应用,都有不同的方法.很难去概括.本篇文章主要涉及本专栏还没有涵盖,但是的确有很重要的方法.本文主要包括动态库注入调试:使用ltrace命令处理动态库的调试:使用strace调试系统调用的问题:Valgrind的简要介绍. 1. 动态库注入       如何排除其他library的调用问题?动态库注入(library injection)有可能会让你事半功倍. 一个大型的软件系统,会用到非常多的动态库.那么如果该动态库的一个a

适用于所有发行版的 Linux 应用程序——是否真的有好处呢?

让我们回顾一下 Linux 社区最新的愿景--推动去中心化的应用来解决发行版的碎片化. 继上周的文章:"Snap.Flatpak 这种通吃所有发行版的打包方式真的有用吗?" 之后,一系列新观点浮出水面,其中可能包含关于这样应用是否有用的重要信息. 缺点 就这个话题在这里的评论,一个叫 Till 的 Gentoo 使用者,对于上一次我们未能完全解释的问题给出了一些新的观点. 对于上一次我们选择仅仅称之为膨胀的的东西,Till 从另一方面做了剖析膨胀将来的发展,这可以帮助我们更好的理解它的

Linux Debugging(七): 使用反汇编理解动态库函数调用方式GOT/PLT

     本文主要讲解动态库函数的地址是如何在运行时被定位的.首先介绍一下PIC和Relocatable的动态库的区别.然后讲解一下GOT和PLT的理论知识.GOT是Global Offset Table,是保存库函数地址的区域.程序运行时,库函数的地址会设置到GOT中.由于动态库的函数是在使用时才被加载,因此刚开始GOT表是空的.地址的设置就涉及到了PLT,Procedure Linkage Table,它包含了一些代码以调用库函数,它可以被理解成一系列的小函数,这些小函数的数量其实就是库函数

Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)

      前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transformation Semantics (程序转换语义学) The Semantics of Copy Constructors(拷贝构造函数之编译背后的行为) The Semantics of Constructors: The Default Constructor (默认构造函数什么时候会被创建出

Linux Debugging(一): 使用反汇编理解C++程序函数调用栈

        拿到CoreDump后,如果看到的地址都是????,那么基本上可以确定,程序的栈被破坏掉了.GDB也是使用函数的调用栈去还原"事故现场"的.因此理解函数调用栈,是使用GDB进行现场调试或者事后调试的基础,如果不理解调用栈,基本上也从GDB得不到什么有用的信息.当然了,也有可能你非常"幸运", 一个bt就把哪儿越界给标出来了.但是,大多数的时候你不够幸运,通过log,通过简单的code walkthrough,得不到哪儿出的问题:或者说只是推测,不能确