CentOS7 pthread并发编程常见段错误定位
段错误一蹦出来,整个终端只剩一行冷冰冰的Segmentation fault,多线程程序尤其如此。CentOS7下用pthread写并发代码,不少人被这种崩溃折腾到凌晨。把锅甩给“并发太复杂”并不解决问题,真正有用的是把现场原封不动地抓下来,再一步步拆成能看懂的小片段。下面这套办法,都是线上踩坑攒下的土办法,没花哨术语,照着做就能把段错误摁在地上。

先让程序“死得明白”
默认情况下,线程一越界,内核只给一句话,连哪一行都没提。先开两个开关,让程序崩溃瞬间留下全尸:
1 编译时加 -g -O0 -pthread,把符号表原封不动地塞进可执行文件,优化级别降到0,防止指令重排把行号搅乱。
2 打开系统core dump:ulimit -c unlimited,崩溃后当前目录会躺下一个core.pid文件,体积可能几百MB,却是真正的“死亡现场”。
改完重新跑,段错误再现时,用gdb ./yourbin core.pid进去,直接bt,调用栈立刻列出来,99%能定位到具体线程、具体函数、具体行号。
线程栈被踩爆的两种典型死法

CentOS7默认线程栈只有8 MB,递归稍深或者局部数组开得大手一抖就越界。第一种死法是“自己踩自己”:局部变量定义太大,gcc在栈上直接分配,运行期一写就越界,把返回地址糊掉,gdb里看到的现象是libcstartcallmain之后直接??。解决也干脆:把大数组扔堆里,或者pthreadattr_setstacksize把线程栈扩到16 MB甚至32 MB,再跑一遍,段错误消失。
第二种死法是“别人踩我”:线程A申请了一块buffer,线程B拿到指针后free掉,线程C还在写,典型的use-after-free。这种崩溃地址往往落在堆区,gdb里打印指针值,再用heap命令看对应chunk状态,基本能看到“free’d”字样。修复办法是给裸指针加引用计数,或者干脆换成std::shared_ptr,让最后一次引用自动归位。
锁没配对也能触发SIGSEGV
很多人以为锁只是逻辑问题,最多死锁,其实不配对的unlock一样能让程序收到段错误。pthreadmutexunlock对同一把锁连续调两次,glibc会检测到并主动raise SIGSEGV,防止数据继续腐烂。gdb里能看到pthreadmutexunlock_usercnt在栈顶,下面紧跟着raise。定位办法:在每次lock/unlock附近加一行打印,把线程ID和锁地址打出来,跑高压测试,十分钟左右就能抓到谁多unlock一次。修复更简单:把lock/unlock包进RAII结构,让C++析构函数保证只走一次。
野指针的“幽灵崩溃”怎么逮
多线程场景下,野指针最难缠,因为崩溃地址每次都不一样。线上环境不可能全用Valgrind拖慢十倍,可以换AddressSanitizer:编译时加-fsanitize=address -fno-omit-frame-pointer,再跑一遍测试,崩溃瞬间ASan会打印出“heap-buffer-overflow”或“heap-use-after-free”,并给出读写线程的栈。CentOS7自带gcc就能开,不需要额外装包。性能损失大概两倍,压测环境完全扛得住。ASan报出来的第一行就是元凶,改完重新编译,基本一锤定音。

core文件太小?systemd帮你截胡
开了ulimit -c unlimited还是拿不到core,多半是systemd的默认策略把大小掐死了。检查一下/etc/systemd/system.conf里的DefaultLimitCORE,改成infinity,再执行systemctl daemon-reload,重新启动服务,core文件就能正常落地。别忘把core文件名模板也改一下:echo "core.%p.%e" > /proc/sys/kernel/core_pattern,省得多个进程同时崩溃互相覆盖。
一条命令让线程栈自动打印
程序在客户现场运行,不可能随时抱着gdb。写个SIGSEGV处理器,注册sigaction,在handler里用backtrace+backtracesymbolsfd把当前线程栈直接写到stderr,再raise SIGABRT结束自己。这样就算没core,也能在日志里看到崩溃栈。注意handler里只能调用异步信号安全函数,malloc、printf都不能用,backtrace系列恰好是安全的。上线后每次段错误都会留下一行/home/user/crash.log,运维同学再也不用半夜催你装gdb。
实战:一个数组越界引发的连锁雪崩
某次压测,CentOS7服务器上一组worker线程随机崩溃,日志里只有Segmentation fault,没core。打开ASan重新跑,十分钟后ASan报:heap-buffer-overflow in thread 3,大小只有1字节。定位到代码里,发现一段解析网络包的逻辑把长度字段当成uint16,但协议里允许长度等于0xFFFF,加1后溢出成0,后续malloc(0)返回的是合法指针,可代码继续按0xFFFF+1去写,直接踩到下一个chunk的元数据,glibc检测到内部结构损坏,立刻abort。修复办法:长度判断加一行if(len == 0xFFFF) return;,再跑十万并发,零崩溃。
把定位流程固化成脚本
每次手工敲gdb太费时间,写个shell脚本:先检查core文件,存在就用gdb -batch -ex bt -ex quit,把结果发邮件;没有core就检查stderr是否带backtrace日志;再没有就触发测试环境ASan版本自动跑回归。脚本放CI里,PR合并前自动跑一遍,有段错误直接拒绝合并,把崩溃扼杀在上线前。
CentOS7下pthread段错误看似玄学,其实套路就这几板斧:先让程序留下core,再用gdb或ASan把栈和堆状态摆到眼前,接着检查栈大小、锁配对、野指针三大高发区,最后把流程脚本化,持续集成里自动跑。照着做,下次再看到Segmentation fault,十分钟内就能揪出元凶,再也不用对着黑屏发呆。
