Core 文件是当一个进程在收到某些信号后终止时产生的文件, 其中包含进程终止时刻进程内存的镜像. 我们可以使用 gdb 从该镜像中观察进程终止时处于什么状态, 用于追踪排查定位问题.
如下示例, 其中 / usr/share/core_pipe/test 是 crash 程序, core.29873 就是 core 文件, 其中 29873 是 crash 进程的 PID.
- # gdb /usr/share/core_pipe/test core.29873GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-gitCopyright (C) 2018 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "x86_64-linux-gnu".
- Type "show configuration" for configuration details.
- For bug reporting instructions, please see:
- <http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at:
- <http://www.gnu.org/software/gdb/documentation/>.For help, type "help".
- Type "apropos word" to search for commands related to "word"...
- Reading symbols from /usr/share/core_pipe/test...done.
- [New LWP 34]
- warning: .dynamic section for "/lib64/ld-linux-x86-64.so.2" is not at the expected address (wrong library or version mismatch?)warning: Could not load shared library symbols for /lib64/libc.so.6.
- Do you need "set solib-search-path" or "set sysroot"?
- Core was generated by `./test'.
- Program terminated with signal SIGSEGV, Segmentation fault.
- #0 0x0000556039e756aa in main () at main.c:7
- 7 printf("this is null p %d \n", *intp);
(gdb)
宿主机进程 Core Dump
在讲容器进程 Core Dump 之前, 我们先来简单回顾下宿主机进程 Core Dump 在什么条件下产生. 这里的宿主机泛指物理机和虚拟主机, 宿主机进程特指运行在系统主机 Initial Namespace 的进程,
容器进程特指运行在容器 Namespace 中的进程.
前文说到当进程收到某种信号才会产生, 那么什么才是 "某种信号" 呢? 这些信号大约有十多个, 这里只列出我们常见比较熟悉的几个:
- SIGQUIT 数值 2 从键盘输入 Ctrl+'\'可以产生此信号
- SIGILL 数值 4 非法指令
- SIGABRT 数值 6 abort 调用
- SIGSEGV 数值 11 非法内存访问
- SIGTRAP 数值 5 调试程序时使用的断点
其中 SIGSEGV 应该是我们最常接触的, 尤其是使用 C/C++ 程序的同学更是常见.
- main.c
- #include <stdio.h>int main(){ int *p = NULL;
- printf("hello world! \n");
- printf("this will cause core dump p %d", *p);
}
使用下面命名编译即可产生运行时触发非法内存访问 SIGSEGV 信号, 其中 - g 选项是编译添加调试信息, 对于 gdb 调试非常有用.
# gcc -g main.c -o test
除了上述产生信号的方法外, 使用我们经常使用的 kill 命令可以非常方便的产生这些信号, 另外还有 http://man7.org/linux/man-pages/man1/gcore.1.html 命令可以在不终止进程的前提下产生 core 文件.
那么只要进程收到这些信号就一定会有 core 文件吗? 显然不是, Linux 在这方面提供了相关配置. 那么这些配置都有哪些呢?
1, 设置 Core 文件大小上限
- ulimit
- # ulimit -c 0 // 0 表示不产生 core 文件# ulimit -c 100 // 100 表示设置 core 文件最大为 100k, 当然可以根据自己需要设置, 注意设置单位是 KB# ulimit -c unlimited // 不限制 core 文件大小
使用上述命令设置 core 文件大小只对当前 shell 环境有效, 系统重启或者退出当前 shell 设置就会失效, 恢复默认设置.
若想永久生效可以把该 shell 放到 / etc/profile 文件中, 系统重新启动初始化环境时会执行其中的命令, 该设置就会在全局范围内生效, 达到永久生效的效果. 也可以使用 source /etc/profile 命令立即全局生效.
# echo "unlimit -c unlimited">> /etc/profile // 配置添加到 / etc/profile 中# source /etc/profile // 立即生效
2, 设置 Core 文件存储目录
# echo "/var/core-dir/core-%e-%p-%t"> /proc/sys/kernel/core_pattern
该命令可以控制 core 文件保存位置和文件名格式. 注意需要使用 root 权限执行, 并且存储路径必须是绝对路径, 不能是相对路径
其中 %e 表示添加用户程序名称,%p 表示添加程序进程 PID,%t 表示添加产生 core 文件时的时间戳, 还有其他一些非常有用的格式, 可以参阅 CORE(5) http://man7.org/linux/man-pages/man5/core.5.html 文档.
这样修改后系统重启后也会消失, 同样也有永久生效的办法
修改 / etc/sysctl.conf 文件在其中修改或者添加
/etc/sysctl.conf
kernel.core_pattern = /var/core-dir/core-%e-%p-%t
然后执行下面命令配置立即生效, 这样系统重启后配置依然有效
# sysctl -p /etc/sysctl.conf
3, 宿主机 Core 文件小结
宿主机产生 core 文件可以使用如下步骤
1. ulimit -c 命令设置 core 文件大小 2. 修改 core_pattern 选项配置 core 文件存储目录
具备上述两个条件后, 当进程在一定条件 core 后, 就会存储 core 文件到我们配置的目录中.
Core 文件产生流程
大概说一下从进程出现异常到生成 core 文件的流程, 不会涉及太多 Linux 系统实现细节, 具体细节可以参考相关文档和 Linux 内核源码.
进程运行时发生一个异常, 比如非法内存地址访问(即段错误), 相应硬件会上报该异常, CPU 检测到该异常时会进入异常处理流程, 包括保存当前上下文, 跳转到对应的中断向量执行入口等
在异常处理流程中如果判断该异常发生时是处于用户态, 则该异常只会影响当前进程, 此时向用户态进程发送相应的信号, 如段错误就会发送 SIGSEGV 信号
当用户态进程被调度时会检查 pending 的信号, 如果发现 pending 的信号是 SIG_KERNEL_COREDUMP_MASK 中的一个, 就会进入 core 文件内容收集存储流程, 然后根据配置 (core_pattern 等) 生成 core 文件
容器进程 Core Dump
那么如果我们要收集在容器里运行的进程的 core 文件应该如何设置呢?
答案是上述宿主机针对 core 文件的设置对容器中的进程依然有效.
众所周知, 宿主机上所有的容器都是共享系统内核的,/proc/sys 文件下只有一小部分支持 namespace 隔离, 而 core_pattern 恰巧不支持隔离的, 所以无论是从宿主机还是容器里修改 core_pattern, 最终修改的是同一个设置, 并且全局生效, 不管是对宿主机还是对容器都是有效的.
一般情况下每个容器都有自己的 mount namespace, 其中的文件系统与宿主机和其他容器相隔离, 那么在 core_pattern 指定的 core 文件存储目录是容器中的文件目录还是宿主机中呢? 不妨推测一二, 刚才我们已经说过这个 core_pattern 是全局生效, 如果该目录是针对某个容器的文件目录, 那么肯定是不合理的, 因为如果宿主机上进程 Core Dump 时就会找不到对应的目录, 无法保存.
实际上有效的 core_pattern 中的目录必须是宿主机中的绝对目录, 更准确的描述是宿主机 Initial Namespace 中的绝对路径.
另外一个问题是, 每个容器都有自己 pid namespace, 我们再 core_pattern 中设置的获取 crash 进程的各种信息比如 PID, 可执行文件名, 是容器 namespace 中的还是宿主机 namespace 中的呢? 从相关文档和实验得知, 可以同时获取 crash 进程在容器中的 PID(通过 %p 格式指定)和在宿主机 Initial Namespace 中的 PID(通过 %P 格式指定), 可执行文件名称 (通过 %e 或 %E 格式指定) 是容器的 namespace 中的.
之所以造成上述情况, 根本原因是 Core Dump 流程中内核进程最后负责处理 core 文件存储的, 而内核进程运行在宿主机 Initial Namespace 中, 实际上所有的容器进程在宿主机 Initial Namespace 都有映射, 对内核来讲, 宿主机进程和容器进程可以统一处理, 并没有本质区别.
1, 使用管道解决容器进程 Core Dummp 问题
上文中我们得知了容器进程 core 文件产生的方法, 但是有一个问题就是上述方法的设置是对宿主机和容器内所有的进程都生效的. 无法针对特定容器进程特定设置. 比如说我们希望宿主机进程 core 文件保存到 / var/crash 目录, 而对容器的 core 文件保存在 / var/container/crash 目录, 或者我要限制某个容器产生 core 文件的总存储大小, 而不是单个 core 文件的大小; 如果我们做一个服务平台对其他用户开放 Core Dump 功能的话, 我们肯定还希望获取一下 crash 进程的其他额外信息比如进程当前环境变量, 当前用户, 当前进程有效 UID 和 GID, 任务名称属性; 如果我们希望针对 core 事件进行统计分析的话, 可能还需要各种回调通知等等操作.
显然上述简单的设置 core 文件存储目录的方法无法满足我们的需求的, 那么我们还有另外一个选择, 就是使用 Linux 的 piping 技术转储 core 文件.
从 Linux 内核版本 2.6.19 之后, 内核就开支支持在 / proc/sys/kernel/core_pattern 文件中指定一个管道程序来实际处理 core 文件存储. core 文件内容会作为该管道程序的标准输入传输给管道程序, 管道程序就接管了接下来的 core 文件内容的所有处理. 如下设置可以使用 piping 技术转储 core 文件
- # echo "|/usr/share/core_pipe/core_pipe core -H=%h -p=%p -i=%i -s=%s -c=%c> /proc/sys/kernel/core_pattern
- # cat /proc/sys/kernel/core_pattern
|/usr/share/core_pipe/core_pipe core -H=%h -p=%p -i=%i -s=%s -c=%c
其中 / usr/share/core_pipe/core_pipe 是我们的管道程序, 需要注意的是必须以 | 开发, | 之后必须紧接管道程序的路径, 没有空格. 当有进程 core 时, 就会调用该管道程序进行处理.
我们可以开发自己的管道处理程序, 从管道程序启动的参数获取 crash 的进程信息, 从管道程序的标准输入获取 core 文件的内容.
我们现在知晓该管道程序什么时候被调用(进程 Core Dump 时), 那么管道程序是由谁来调用呢?
既然管道程序是我们自己开发的, 我们就可以获取管道程序的父进程是谁, 也就是被谁调用的, 通过实验我们可一知道父进程的 PID 是 2, 当我们再看该进程的父进程是谁:
# ps -ef -q 2UID PID PPID C STIME TTY TIME CMD
root 2 0 0 Jan20 ? 00:00:00 [kthreadd]
进程 PID2 的父进程是 PID 0, 而 PID 0 代表的是 Linux 系统内核 idle 进程, Linux 系统中共有三个特殊进程, 分别是 idle(PID 0), init(PID 1), kthreadd(PID 2), 而 kthreadd 是所有内核进程的父进程, 也就是说我们的管道程序是作为内核线程在运行的, 运行在内核态, 并且在宿主机 Initial Namespace 中以 root 用户身份运行, 不在任何容器内.
2,Socket Activation 应用到容器进程 Core Dummp
上文说了管道程序运行在内核态, 而且是在宿主机的 Initial Namespace 中运行, 容器的各种限制对其不起作用, 比如 core 文件大小有可能超过容器的硬盘空间限制. 当然我们管道程序可以通过 crash 进程的 PID 拿到 crash 进程的容器 namespace 以及各种 cgroup 限制, 然后针对性处理. 这样显然对容器极有侵入性, 代码写起来也不够优雅. 如果处理 core 文件存储程序在容器中运行, 就能较优雅的解决好这个问题. 管道程序已经作为内核线程运行在宿主机的 Initial Namespace 了, 虽然有办法可以动态的加入和退出某个 namespace 和 cgroup, 但是考虑的边界条件多, 易出错, 并不优雅.
如果管道程序能够和容器内某个程序进行交互, 可以解决上述问题, 同一个宿主机进程通信的方式有很多, 比如共享内存, 管道, 消息队列等. 但是这里的两个程序是分布在不同的 namespace 中, 而且彼此并不知道什么时候可以交互, 我们为了低概率的 core 文件长时间让容器内某个进程空跑占用资源吗? 那么 socket activation 技术可以用来解决这个问题.
socket activation 并不是一种新技术, 其技术理念和原理早就被应用到 Linux 和 MacOS 中, 关于 socket activation 技术原理细节又是需要另一篇的长篇大论, 这里暂且不再详述, 简单来说, 就是由系统 init 进程 (对于目前大多数 Linux 系统来说是 https://www.freedesktop.org/wiki/Software/systemd/ ) 来为普通应用进程监听特定 socket, 此时应用进程并未启动, 当有连接到达该 socket 后, 由 init 进程接管该连接并跟进配置文件启动相应的应用进程, 然后把连接传递给应用进程来处理, 主要好处是当没有连接到达时, 应用进程无需常驻后台空跑耗费系统资源. 非常适合像 Core Dump 这种低频服务.
我们可以设置一个 unix socket 来把管道程序的文件描述符传递到容器内进程, 完成传递后, 管道程序就可以退出, 由容器内进程处理 core 文件的存储.
下面是一个 socket activation 示例, 其中 / usr/share/core_pipe/core_pipe 是我们的 core 文件处理程序, /run/core_pipe.socket 是我们 unix socket 文件, 存在容器中, 该文件我们在 Initial Namespace 中的管道程序可以通过 / proc/${crash pid}/root/run/core_pipe.socket 拿到, 然后与之交互.
- core_pipe-forward.socket
- # 此为 Unit 文件, 保存内容为文件到 /etc/systemd/system/core_pipe-forward.socket
- [Unit]
- Description=Unix socket for core_pipe crash forwarding
- ConditionVirtualization=container
- [Socket]
- ListenStream=/run/core_pipe.socket
- SocketMode=0600Accept=yes
- MaxConnections=10Backlog=5PassCredentials=true[Install]
WantedBy=sockets.target
- # 此为 service 文件, 保存内容到 /etc/systemd/system/core_pipe-forward.service
- [Unit]
- Description=Core Pipe crash forwarding receiver
- Requires=core_pipe-forward.socket
- [Service]
- Type=oneshot
ExecStart=/usr/share/core_pipe/core_pipe
core_pipe-forward.socket
执行下面命令使得 socket 生效
- # systemctl enable core_pipe-forward.socket
- # systemctl start core_pipe-forward.socket
- # systemctl status core_pipe-forward.socket
上述命令如果是在容器内的 init 进程不是 systemd 情况下会出错, 大多数情况下容器内的 init 进程并不是 systemd, 此时可以退一步使用容器内常驻进程的方式来实现 core 文件的处理.
总结
本文简单说明了实现容器进程 Core Dump 的方法, 概况一下主要有三点:
使用 ulimit -c 和 / proc/sys/kernel/core_pattern 设置 Core Dump 文件大小限制和存储位置
使用管道程序增强 Core Dump 文件处理能力
使用管道程序和容器内进程结合的方式完成内核态转到用户态, 在容器内处理 Core 文件存储
参考文献:
- CORE(5) http://man7.org/linux/man-pages/man5/core.5.html
- GETRLIMIT(2)
- GDB(1) http://man7.org/linux/man-pages/man1/gdb.1.html
- KILL(2) http://man7.org/linux/man-pages/man2/kill.2.html
- SIGNAL(7) http://man7.org/linux/man-pages/man7/signal.7.html
- NAMESPACE(7)
- BASH(1) http://man7.org/linux/man-pages/man1/bash.1.html
- go-systemd https://github.com/coreos/go-systemd
- systemd-socket-activation-in-go
Core Dump 流程分析
- Socket activation in systemd
- socket activation
来源: https://juejin.im/post/5c80cd30e51d45104c40d421