do_exit的hungtask问题及coredump的实验及原理分析一

一、背景

coredump是程序员人人都知道的东西,在进行core的dump时,系统正在疯狂地写磁盘,如果core比较大,排队写磁盘的任务拥挤,加上系统上近期已经累计了不少脏页要写,就可能会出发coredump时的hungtask,在下面第二章,我们分析一次相关的hungtask的栈,继而提出几个疑问,在第三章里,我们做一些coredump的实验及针对实验结果结合代码进行原理分析。

二、do_exit的hungtask问题的堆栈分析

抓到的这次hungtask的堆栈显示的是:

常用的底层调试用的不容易记住的命令整理(持续更新) 里第二章里的命令:

bash 复制代码
aarch64-linux-gnu-objdump -S vmlinux --start-address=0xffff80000809af80 --stop-address=0xffff80000809b920 > vmlinux_do_exit.txt

可以如下图看到,得到的elf文件看do_exit函数的大小与堆栈里的函数的大小是匹配的:

看一下offset到0xec是在哪里:

搜索b06c附近的代码:

代码里就唯一一处,在exit.c里:

可以看到是coredump_task_exit函数这里的下图里的逻辑,进行schedule等待被唤醒:

而coredump_task_exit是coredump流程里的重要函数,是除了负责coredump文件生成core的线程之外的其他线程都要经历这个函数,去等最终负责生成core的线程完成core的生成后,再通知回来,再走完do_exit的流程。

在下面第三章里,我们通过实验来说明这个流程。

三、coredump的相关实验及原理分析

3.1 设置coredump格式及配置生成coredump

在通过下面的命令设置coredump的生成路径及格式前,需要配置打开coredump:

bash 复制代码
ulimit -c unlimited

来设置core的文件大小不受限制。

通过下面的命令我想让生成的文件输出到当前目录,并带上一些必要的信息:

bash 复制代码
echo "./core.%e.%p.%h.%m.%d.%s" > /proc/sys/kernel/core_pattern

如下图,可以成功生成coredump:

上图执行时用的testcoredump.cpp的源文件如下:

cpp 复制代码
int main() {
	int *p = (int *)0;
	*p = 1;
	return 1;
}

3.2 分析生成coredump的整个流程的调度栈的程序

下一步我们抓一下生成coredump期间的进程内的各个线程是如何依次处理coredump相关的信号的。

为了能分析多线程的情况,我们改写一下testcoredump.cpp文件,增加两个线程,两个线程分别执行一个很长时间的sleep:

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>
#include <unistd.h>

void sleepFor100Seconds(int n) {
	printf("thread %d, tid=%d\n", n, gettid());
    while (1) {
    	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	}
}

int main() {
	printf("MAIN thread, tid=%d\n", gettid());
    // 创建两个线程
    std::thread thread1(sleepFor100Seconds, 0);
    std::thread thread2(sleepFor100Seconds, 1);

	std::this_thread::sleep_for(std::chrono::seconds(1));

	int *p = (int *)0;
	*p = 1;

    // 等待两个线程完成
    thread1.join();
    thread2.join();

    std::cout << "Both threads have finished!" << std::endl;
    return 0;
}

然后用ftracestart和ftracestop抓取testcoredump程序启动到最后触发coredump的完整的调度情况。(有关ftracestart和ftracestop见之前博客 常用的底层调试用的不容易记住的命令整理(持续更新) 里 第六章)。

写一个测试脚本,来无缝的开始和结束ftrace:

bash 复制代码
#!/bin/bash

ftrace_start
sleep 0.1
./testcoredump
sleep 0.1
ftrace_stop

运行这个测试脚本:

过滤出 perfetto可识别的 trace文件,如何过滤见之前博客 常用的底层调试用的不容易记住的命令整理(持续更新) 里 第六章)。

3.3 分析trace,拆解一步步coredump流程

抓到的触发coredump到进程进行coredump落盘,再到最后进程退出的ftrace在perfetto里解析出来的调度图:

我们依次分析一个个步骤:

3.3.1 主线程完成睡眠,执行会触发coredump的逻辑

从主线程完成睡眠,开始执行会触发coredump的逻辑开始,ftrace里下面这次:

对应的ftrace里的抓取:

可以看到时间是匹配的,是timer的唤醒,对应源码里下图部分:

然后就执行会触发coredump的逻辑了:

3.3.2 触发coredump的线程唤醒其他线程

接下来就是触发coredump的线程在do_coredump的流程里唤醒进程内的其他线程,如下图是主线程唤醒2373950线程(大概率是thread1):

时间和线程id都能对上,看一下do_coredump里唤醒进程内其他线程的逻辑,还是根据elf里去找上图里的do_coredump下一级的调用signal_wake_up_state,找到如下的部分:

搜索上图里的if (t != current && !(t->flags & PF_POSTCOREDUMP)) {的这段代码,找到唯一一处,在zap_process里:

signal_wake_up就是在zap_process(所谓的清理进程)这个函数里调用的,看一下zap_process在do_coredump里如何被使用,是下面的调用链:

do_coredump

coredump_wait

zap_threads

zap_process

如下图,进行coredump的线程在执行do_coredump时,判断coredump_wait的返回值,如果返回值小于0,则直接返回,因为已经有一个线程在执行coredump了(因为可能有并发的进程内多个线程的地址非法访问):

3.3.3 coredump_wait的实现

看一下coredump_wait的实现:

如上图,coredump_wait的实现分为三步:

1)zap_threads去通知其他线程要醒来进入inactive状态

2)等待core_state->nr_threads减为0(这是在非处理core的其他各个线程里做和判断),减为0则被唤醒往下走

3)等待其他线程进入inactive状态

可以从上图里看到,能让coredump_wait返回小于0从而不去走do_coredump的接下来的core进行dumpd的逻辑是建立在zap_threads小于0的情况下,这一点会在下面 3.3.4 一节里介绍。

另外,zap_threads里有上面 3.3.2 分析的触发唤醒其他线程的逻辑,这一点会在下面 3.3.6 一节里介绍。

在介绍zap_threads的逻辑前,先快速看一下wait_task_inactive的注释:

另外,上面说的2)里的非处理core的其他各个线程里做和判断,是在下图里的exit.c里的coredump_task_exit的逻辑里:

要注意,atomic_dec_and_test减到0是返回true。

3.3.4 zap_threads何时返回小于0

我们来看一下zap_threads的实现,刚才说到zap_threads返回小于0,do_coredump里就不会再去生成core文件了(因为已经有人正在生成了),看看zap_threads什么时候回返回小于0,如下图看到nr初始时是设置成了-EAGAIN,也就是下面的if判断不满足就会返回负值:

因为上图里的nr = zap_process的逻辑只会赋值成正数:

回到zap_threads里,何时会走不进下图里的红色框出的逻辑:

也就是上图里的signal->flags & SIGNAL_GROUP_EXIT是true,要不就是signal->group_exec_task是非空,意思就是任务已经退出状态了,因为后入do_coredump的函数,要进入的时候,前面进入do_coredump函数的线程已经完成了zap_threads的工作,zap_threads里会设置上SIGNAL_GROUP_EXIT状态表示已经开始退出流程了:

3.3.5 group_exec_task什么时候是非空呢

那么什么情况下group_exec_task是非空呢,那是在执行exec时,(由于已经执行exec了,那么执行exec时的进程里的所有线程都不再需要),会设置这个group_exec_task变量:

调用链是:

begin_new_exec

de_thread

而exec执行期间所引起的segment fault是不处理的,在exec执行完之后会把group_exec_task重新置成NULL:

3.3.6 zap_threads里唤醒非生成core的以外的别的线程

zap_threads里在判断出满足需要进行进程退出逻辑的条件(也就是zap_threads返回值不小于0情况时)会调用下图的zap_process函数,从而调用signal_wake_up函数去唤醒非生成core的以外的别的线程:

3.3.7 总结一下:触发coredump后第一次唤醒其他线程的调用栈

do_coredump

coredump_wait

zap_threads

zap_process

signal_wake_up

signal_wake_up_state

wake_up_state

try_to_wake_up

trace里的接下来的步骤放到之后的"coredump的实验及原理分析二"博文里去展开,虽然这篇博客里在分析上面列出来的coredump_wait时也已经分析了不少细节了。

相关推荐
BigBigHang1 天前
【docker】cloudbeaver的docker-compose及一些踩坑
运维·docker·容器
pengdott1 天前
Linux进程数据结构与组织方式深度解析
linux·运维·服务器
Java 码农1 天前
gitlab gitrunner springboot 多环境多分支部署 (非容器方式,使用原生linux 环境)
linux·spring boot·gitlab
小快说网安1 天前
等保测评通过后,如何持续满足安全运维要求?
运维·安全·网络安全·等保测评
闲过信陵饮~1 天前
无头服务器 + Vulkan + Docker 问题
运维·docker·容器
科技块儿1 天前
【需求:GDPR合规下做地域定向】解决方案:仅用IP离线库输出国家码,不存原始IP?
服务器·网络·tcp/ip
LongQ30ZZ1 天前
Linux的常见指令
linux·服务器
走向IT1 天前
vdbench在Centos系统上联机测试环境搭建
linux·运维·centos
阳宗德1 天前
基于CentOS Linux release 7.1实现了Oracle Database 11g R2 企业版容器化运行
linux·数据库·docker·oracle·centos