1.Linux进程照妖镜strace命令
2.Linux查看系统调用学习指南linux查看系统调用
3.mmap的源码系统调用
4.从根上理解IO等待—案例篇
5.Spring Boot引起的“堆外内存泄漏”排查及经验总结
6.Linux神器strace的使用方法及实践
Linux进程照妖镜strace命令
strace是强大的Linux调试分析工具,专用于跟踪程序执行时的源码系统调用和接收的信号。无需访问源代码,源码适用于不可读或无法重新编译的源码程序。系统调用发生在进程尝试访问硬件设备时,源码如读取磁盘文件或接收网络数据。源码网页案例源码网站strace则能记录此过程中的源码系统调用详情,包括参数、源码返回值和执行时间。源码
执行strace命令可揭示进程行为,源码但其无输出并不表示进程阻塞。源码它提供了一种观察程序与系统交互的源码方式。例如,源码对于简单的源码`getcwd`函数调用,strace能显示该函数如何获取当前路径,源码并将结果复制到指定缓冲区。同样,对于`write`函数,strace能追踪其如何处理输出内容。
通过逐步增加`printf`函数的使用,我们发现其系统调用数量实际上保持不变,这表明`printf`在连续打印时进行了优化,利用`mmap`函数执行内存拷贝,最后通过`write`函数输出缓冲区内容。实验中,添加换行符后`printf`调用次数增加至三次,这揭示了在遇到换行符时`printf`会刷新输出缓冲区,执行`write`函数将内容写入输出设备。
常用`strace`命令示例包括:跟踪指定命令的系统调用、跟踪特定进程的系统调用情况、统计指定进程的系统调用次数与用时,这些功能有助于深入理解程序运行时的行为,优化系统性能。
Linux查看系统调用学习指南linux查看系统调用
Linux 是一种开放源代码的操作系统,是众多种操作系统的中最受欢迎的一种,在技术术语中,它也称为一种基于POSIX特性的混合内核操作系统。本文介绍了在Linux环境中查看系统调用的方法。
要在Linux环境下查看系统调用,第一步就是要下载strace,strace是一款用于分析和跟踪系统调用的工具,可以有效的检测出程序的行为。strace的网页中视频源码下载安装非常的简单,只需要输入如下命令即可安装:
`sudo apt-get install strace`
安装完成后,可以通过strace工具查看系统调用,比如可以查看系统中某个应用程序所执行的操作。我们以检测Linux中命令行程序ls的行为为例,只需要输入如下命令即可:
`strace ls –alF`
使用strace可以追踪系统调用,比如它提供了追踪函数调用历史,寄存器和内存状态的功能,可以轻松地获取程序的具体详细执行状态;另外,它还可以实时获取系统的性能状况,可以帮助开发人员更好的调优和系统优化。
系统调用是在Linux操作系统中非常重要的,了解系统调用和控制可以为系统开发编程进程提供有效的参考指导,strace就是帮助查看和跟踪Linux系统调用的有力神器,掌握它的使用方法,就可以轻松查看系统的具体调用状态。
mmap的系统调用
1. 创建内存映射
mmap:进程创建匿名的内存映射,把内存的物理页映射到进程的虚拟地址空间。进程把文件映射到进程的虚拟地址空间,可以像访问内存一样访问文件,不需要调用系统调用read()/write()访问文件,从而避免用户模式和内核模式之间的切换,提高读写文件速度。两个进程针对同一个文件创建共享的内存映射,实现共享内存。
mumap:该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
3. 设置虚拟内存区域的访问权限
mprotect:把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。 prot可以取以下几个值,并且可以用“|”将几个属性合起来使用: 1)PROT_READ:表示内存段内的内容可写; 2)PROT_WRITE:表示内存段内的内容可读; 3)PROT_EXEC:表示内存段中的内容可执行; 4)PROT_NONE:表示内存段中的内容根本没法访问。 需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
0. 查找mmap在内核中的系统调用函数 我现在用的内核版是4..,首先在应用层参考上面解析编写一个mmap使用代码,然后编译成程序,在使用strace工具跟踪其函数调用,可以发现mmap也是易语言大型erp源码调用底层的mmap系统调用,然后我们寻找一下底层的带6个参数的mmap系统调用有哪些:
1.mmap的系统调用 x的位于arch/x/kernel/sys_x_.c文件,如下所示:
arm的位于arch/arm/kernel/sys.c文件,如下所示:
然后都是进入ksys_mmap_pgoff:
然后进入vm_mmap_pgoff:
我们讲解最重要的do_mmap_pgoff函数:
然后进入do_mmap:
do_mmap_pgoff这个函数主要做了两件事,get_unmapped_area获取未映射地址,mmap_region映射。 先看下get_unmapped_area ,他是先找到mm_struct的get_unmapped_area成员,再去执行他:
再看mmap_region的实现:
现在,我们看看匿名映射的函数shmem_zero_setup到底做了什么,其实匿名页实际也映射了文件,只是映射到了/dev/zero上,这样有个好处是,不需要对所有页面进行提前置0,只有当访问到某具体页面的时候才会申请一个0页。
其实说白了,mmap就是在进程mm中创建或者扩展一个vma映射到某个文件,而共享、私有、文件、匿名这些mmap所具有的属性是在哪里体现的呢?上面的源码在不断的设置一些标记位,这些标记位就决定了进程在访问这些内存时内核的行为,mmap仅负责创建一个映射而已。
从根上理解IO等待—案例篇
当系统显示I/O等待指标上升,意味着进程在等待硬件资源响应,进入不可中断睡眠状态。在D状态,进程无法被任何信号中断,即使强制终止也无效。使用ps或top命令可见此类进程。
不同状态的进程如何识别?top和ps工具帮助我们理解。R状态表示运行,D状态是Disk Sleep的缩写,表示进程处于不可中断睡眠状态,常见于等待磁盘I/O。Z状态表示进程终止,是僵尸进程,停留在进程表中直到父进程处理。S状态是可中断的睡眠状态,可被信号中断。I状态则是空闲状态,适用于内核线程。
D状态进程导致平均负载升高,提交成功后返回源码I状态则不会。理解这些状态有助于评估系统性能和进程行为。
除了R、D、Z、S、I状态,进程还有T或t状态,表示暂停或跟踪状态,接收到SIGSTOP信号时出现。X状态是Dead状态,表示进程终止且不在top或ps命令输出中。
案例分析:多进程应用中,大量进程处于D状态,僵尸进程增加,I/O等待高。应用在C语言下开发,通过Docker容器模拟环境。ps命令确认应用启动,显示Ss+和D+状态,s表示领导进程,+为前台进程组。top命令显示平均负载升高至CPU个数,僵尸进程持续增加,CPU使用率不高,但iowait分别为.5%和.6%,用户CPU使用率0.3%。分析后发现,iowait升高与磁盘读请求大相关,应用进程在进行直接磁盘I/O操作。
为了解决iowait问题,首先使用dstat命令查看系统I/O情况,确认问题出在磁盘读操作。使用top命令定位到D状态的可疑进程,再通过pidstat命令获取进程详细信息,发现app进程进行大量磁盘读操作,每秒读取MB数据。使用strace命令跟踪进程系统调用,发现app进程通过sys_read系统调用进行磁盘直接读取,绕过了系统缓存。
为了解决直接读取磁盘的问题,修改应用源代码,软著需要源码吗删除O_DIRECT选项,避免直接磁盘I/O。运行修改后的代码,iowait降低至0.3%,问题得到解决。但僵尸进程问题依然存在,通过pstree命令找到僵尸进程的父进程,检查其源代码,发现wait函数错误地放在循环外部,导致无法正确回收子进程资源。修复wait函数调用位置,确保每次循环都调用wait函数等待子进程结束。停止应用,重新运行修复后的代码,最终僵尸进程消失,iowait降至0,问题解决。
Spring Boot引起的“堆外内存泄漏”排查及经验总结
为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于Spring Boot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=M -XX:MaxMetaspaceSize=M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=m -XX:InitialCodeCacheSize=m, -Xssk -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M”,实际使用的物理内存如下图所示:
使用Java层面的工具定位内存区域(堆内内存、Code区域或者使用unsafe.allocateMemory和DirectByteBuffer申请的堆外内存)。
笔者在项目中添加-XX:NativeMemoryTracking=detailJVM参数重启项目,使用命令jcmd pid VM.native_memory detail查看到的内存分布如下:
发现命令显示的committed的内存小于物理内存,因为jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。
为了防止误判,笔者使用了pmap查看内存分布,发现大量的M的地址;而这些地址空间不在jcmd命令所给出的地址空间里面,基本上就断定就是这些M的内存所导致。
使用系统层面的工具定位堆外内存。
因为已经基本上确定是Native Code所引起,而Java层面的工具不便于排查此类问题,只能使用系统层面的工具去定位问题。
首先,使用了gperftools去定位问题。
从上图可以看出:使用malloc申请的的内存最高到3G之后就释放了,之后始终维持在M-M。笔者第一反应是:难道Native Code中没有使用malloc申请,直接使用mmap/brk申请的?(gperftools原理就使用动态链接的方式替换了操作系统默认的内存分配器(glibc)。)
然后,使用strace去追踪系统调用。
因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪向OS申请内存请求,但是并没有发现有可疑内存申请。
接着,使用GDB去dump可疑内存。
因为使用strace没有追踪到可疑内存申请;于是想着看看内存中的情况。就是直接使用命令gdp -pid pid进入GDB之后,然后使用命令dump memory mem.bin startAddress endAddressdump内存,其中startAddress和endAddress可以从/proc/pid/smaps中查找。然后使用strings mem.bin查看dump的内容,如下:
从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。
再次,项目启动时使用strace去追踪系统调用。
项目启动使用strace追踪系统调用,发现确实申请了很多M的内存空间,截图如下:
使用该mmap申请的地址空间在pmap对应如下:
最后,使用jstack去查看对应的线程。
因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid去查看线程栈,找到对应的线程栈(注意进制和进制转换)如下:
这里基本上就可以看出问题来了:MCC(美团统一配置中心)使用了Reflections进行扫包,底层使用了Spring Boot去加载JAR。因为解压JAR使用Inflater类,需要用到堆外内存,然后使用Btrace去追踪这个类,栈如下:
然后查看使用MCC的地方,发现没有配置扫包路径,默认是扫描所有的包。于是修改代码,配置扫包路径,发布上线后内存问题解决。
为什么堆外内存没有释放掉呢?
虽然问题已经解决了,但是有几个疑问。带着疑问,直接看了一下 Spring Boot Loader那一块的源码。发现Spring Boot对Java JDK的InflaterInputStream进行了包装并且使用了Inflater,而Inflater本身用于解压JAR包的需要用到堆外内存。而包装之后的类ZipInflaterInputStream没有释放Inflater持有的堆外内存。于是以为找到了原因,立马向Spring Boot社区反馈了这个bug。但是反馈之后,就发现Inflater这个对象本身实现了finalize方法,在这个方法中有调用释放堆外内存的逻辑。也就是说Spring Boot依赖于GC释放堆外内存。
使用jmap查看堆内对象时,发现已经基本上没有Inflater这个对象了。于是就怀疑GC的时候,没有调用finalize。带着这样的怀疑,把Inflater进行包装在Spring Boot Loader里面替换成自己包装的Inflater,在finalize进行打点监控,结果finalize方法确实被调用了。于是又去看了Inflater对应的C代码,发现初始化的使用了malloc申请内存,end的时候也调用了free去释放内存。
此时,怀疑free的时候没有真正释放内存,便把Spring Boot包装的InflaterInputStream替换成Java JDK自带的,发现替换之后,内存问题也得以解决了。
再次看gperftools的内存分布情况,发现使用Spring Boot时,内存使用一直在增加,突然某个点内存使用下降了好多(使用量直接由3G降为M左右)。这个点应该就是GC引起的,内存应该释放了,但是在操作系统层面并没有看到内存变化,那是不是没有释放到操作系统,被内存分配器持有了呢?
继续探究,发现系统默认的内存分配器(glibc 2.版本)和使用gperftools内存地址分布差别很明显,2.5G地址使用smaps发现它是属于Native Stack。内存地址分布如下:
到此,基本上可以确定是内存分配器在捣鬼;搜索了一下glibc M,发现glibc从2.开始对每个线程引入内存池(位机器大小就是M内存),原文如下:
按照文中所说去修改MALLOC_ARENA_MAX环境变量,发现没什么效果。查看tcmalloc(gperftools使用的内存分配器)也使用了内存池方式。
为了验证是内存池搞的鬼,就简单写个不带内存池的内存分配器。使用命令gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so生成动态库,然后使用export LD_PRELOAD=zjbmalloc.so替换掉glibc的内存分配器。其中代码Demo如下:
通过在自定义分配器当中埋点可以发现实际申请的堆外内存始终在M-M之间,gperftools监控显示内存使用量也是在M-M左右。但是从操作系统角度来看进程占用的内存差别很大(这里只是监控堆外内存)。
使用不同分配器进行不同程度的扫包,占用的内存如下:
为什么自定义的malloc申请M,最终占用的物理内存在1.7G呢?因为自定义内存分配器采用的是mmap分配内存,mmap分配内存按需向上取整到整数个页,所以存在着巨大的空间浪费。通过监控发现最终申请的页面数目在k个左右,那实际上向系统申请的内存等于k * 4k(pagesize) = 2G。
为什么这个数据大于1.7G呢?因为操作系统采取的是延迟分配的方式,通过mmap向系统申请内存的时候,系统仅仅返回内存地址并没有分配真实的物理内存。只有在真正使用的时候,系统产生一个缺页中断,然后再分配实际的物理Page。
整个内存分配的流程如上图所示。MCC扫包的默认配置是扫描所有的JAR包。在扫描包的时候,Spring Boot不会主动去释放堆外内存,导致在扫描阶段,堆外内存占用量一直持续飙升。当发生GC的时候,Spring Boot依赖于finalize机制去释放了堆外内存;但是glibc为了性能考虑,并没有真正把内存归返到操作系统,而是留下来放入内存池了,导致应用层以为发生了“内存泄漏”。所以修改MCC的配置路径为特定的JAR包,问题解决。在发表这篇文章时,发现Spring Boot的最新版本(2.0.5.RELEASE)已经做了修改,在ZipInflaterInputStream主动释放了堆外内存不再依赖GC;所以Spring Boot升级到最新版本,这个问题也可以得到解决。
Linux神器strace的使用方法及实践
在Linux系统中,strace这个强大的工具如神器般实用,用于诊断、调试和统计程序运行,本文将详细介绍它的使用方法及实践案例。当程序运行异常或系统命令出错而难以通过常规手段定位问题时,strace就能派上用场。
当遇到操作系统运维中程序失败、报错信息无法揭示问题根源时,strace能够让我们在无需内核或代码的情况下,跟踪系统调用过程。它是一种不可或缺的诊断工具,系统管理员只需简单操作,即可在不查看源代码的情况下跟踪系统的调用。
strace的参数选项众多,如在CentOS/EulerOS和Ubuntu系统中安装,以及常用参数如 `-c` 用于统计系统调用时间、次数和错误次数,`-d` 显示调试输出,`-p` 根据进程ID追踪等。例如,通过`-e trace=open,close,read,write` 可以追踪ls命令中的文件系统调用,或者通过`-p`跟踪特定进程的系统活动。
以解决“无法解析域名”问题为例,我们可以通过strace命令查看系统在读取文件时的调用情况,如发现缺失了/lib/libnss_dns.so.2文件,说明问题可能出在相关库文件上。解决方法是安装glibc-devel包以获取缺失的文件。
通过strace,我们不仅能解决系统问题,还能深入了解系统的运作机制,提高运维效率。希望这些实例和参数帮助你更好地利用strace进行Linux系统调用的追踪和调试。
用trace工具 trace trace工具
深入探讨了使用trace工具理解eBPF(eBPF)和trace工具的方法。首先,理解了使用eBPF工具进行调试以及trace工具理解trace原理的两种方式:从代码细节入手,或是先勾画大概,再深入细节。在复杂系统中,直接查看所有代码变得困难,尤其是在云环境中,此现象普遍。接下来,以`reallocarray`为例,创建了一个uprobe。
在探究如何通过trace-bpfcc生成uprobe时,通过strace工具发现使用了`perf_event_open`进行注入。进一步关注`perf_event_open`内部参数`struct perf_event_attr`,了解了`config1`和`config2`的作用:`config1`类似uprobe的路径名,而`config2`是特定偏移量。通过尝试不同方法,最终确认`config1`指向`libc.so`文件路径,`config2`为`reallocarray`在`libc-2..so`中的偏移。
创建uprobe后,编写了小程序来触发其执行。eBPF与uprobe的关联通过`trace trace-bpfcc`实现,最终调用`__uprobe_register`。对于`__uprobe_register`的实现,通过进一步查找代码获取信息。`mymem`触发uprobe的机制大致为程序加载或执行过程中会触发先前创建的uprobe,通过`ftrace`的`function_graph`功能筛选并打印调用函数链。
通过分析uprobe_mmap的调用栈,可以了解到在操作vma时会触发uprobe_mmap。uprobe_mmap内部的关键调用有助于理解其工作流程。总结以上trace分析,得出理解uprobe的实现和工作原理,主要通过trace和源码分析相结合的方式,掌握工具和方法是关键。
通过trace过程演示了使用trace工具的能力和方法,更多关于uprobe的实现细节,可以通过进一步的trace或阅读源码进行深入探索。这一过程展示了如何利用trace工具理解复杂系统中的特定功能和行为,为深入学习和调试提供了一条有效路径。