对症下药:各种程序问题定位排查技巧

问题定位排查

目录

系统问题分析框架

在这里插入图片描述

异常中止(如重启)
  • kernel panic 内核分析,内核问题则内核进行分析
  • Signal触发, 生成了coredump文件,则GDB调试分析
  • OOM valgrind/mstrace分析(可能内存泄露,有可能是栈内存泄露,比如创建了大量线程没有在线程执行结束后回收(没有detach或pthread_join))
    • Linux下面有个特性叫OOM killer(Out Of Memory killer),这个东西会在系统内存耗尽的情况下跳出来,选择性的干掉一些进程以求释放一些内存。具体的记录日志是在/var/log/messages中,如果出现了out of memory字样,说明系统曾经出现过OOM
    • 为什么泄露会导致OOM , 因为泄露久了,物理内存不够用了,当某个进程申请内存时,不够了,就要杀死某个进程了。定位内存碎片的一种思路是,oom了但剩余内存还没有到触发oom的时候
异常不中止(如卡死)
  • CPU占用率高:线程空转,top分析cpu占用,gdb跟踪程序执行,bt然后发现一直停留在某个函数
  • CPU占用率不高:死锁,gdb或者sysRq分析下堆栈
死锁排查
  • 核心思路是利用死锁时线程堆栈调用不变的特性, 并且死锁线程的堆栈调用中必然包含锁的调用
  • 一个Linux分析死锁的简单方法 , 这篇文章介绍了一种定位死锁位置的方法。核心思路是利用死锁时线程堆栈调用不变的特性。可以使用gdb和pstack来调试, 程序需要用-g编译,调试信息会显示代码行,不用-g编译也能调试。pstack相当于gdb下输入 thread apply all bt , 多次输入比较堆栈信息是否改变,判断线程是否卡死。使用gdb前先知道进程pid, 然后gdb attach pid 进入,info thread, thread which e.g. thread 3, bt查看,f 等定位是哪一行。就这样定位出死锁。注意,这里如果用strace -p pid , 则只会追踪主线程,创建的线程不会追踪,可以用strace -p 线程pid,追踪特定线程。 或者strace -fp pid ,追踪进程下所有线程
  • 死锁可以使用valgrind排查 Linux 性能分析valgrind(三)之DRD(死锁分析利器
句柄泄漏
  • Linux系统的最大文件句柄数(打开文件数,Linux下一切皆文件,这里仅做类比句柄描述),系统默认是1024。用ulimit -n进行查看。
  • 当存在句柄泄露没有释放时,系统会报错:Too many open files。查看进程打开的文件句柄数量
    lsof -n | awk '{print $2}'| sort | uniq -c | sort -nr | grep 过滤条件(可选)
    其中第一列是打开的句柄数,第二列是进程ID。一般泄露的进程句柄数是最多的,这时就能够排查出是哪个进程了,. 或者怀疑某个进程后,观察其句柄是否增长, 利用ll /proc/pid/fd | wc -l, 然后ll /proc/pid/fd 看下是哪个文件。
  • ls /proc/进程PID/fd -l 查看进程打开的文件描述符(会显示描述符值和对应打开的文件,这样就可以看到哪些文件被打开了), /proc/进程PID/fd 目录下是一堆数字, 对应当前进程打开的文件描述符的值, 总的个数即当前打开的描述符的个数。不需要深入线程了,因为该信息足以用来定位到代码段。
    user@Cpl-MT-Traffic-174:~/work/D或A机型$ ls /proc/29337/fd -l
    总用量 0
    lrwx------ 1 user user 64  1月 25 16:22 0 -> /dev/pts/10
    lrwx------ 1 user user 64  1月 25 16:22 1 -> /dev/pts/10
    lrwx------ 1 user user 64  1月 25 16:21 2 -> /dev/pts/10
    l-wx------ 1 user user 64  1月 25 16:22 3 -> /data1/user/work/simpleCode/tmp/testLinux.c
    这里时间后面的 0 ,1,2,3对应描述符值,->对应打开的文件。这里0,1,2对应标准输入、输出、错误输出,3对应文件/data1/user/work/simpleCode/tmp/testLinux.c
    
  • 另外一个方法是lsof, 用法lsof -p pid, pid是进程pid, 详见man lsof. 示例如下,这里也能看到打开了/data1/user/work/simpleCode/tmp/testLinux.c文件
    user@Cpl-MT-Traffic-174:~/work/D或A机型$ lsof -p 29337
    COMMAND   PID       USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
    run     29337 user  cwd    DIR   8,32     4096 98835240 /data1/user/work/simpleCode/tmp
    run     29337 user    0u   CHR 136,10      0t0       13 /dev/pts/10
    run     29337 user    1u   CHR 136,10      0t0       13 /dev/pts/10
    run     29337 user    2u   CHR 136,10      0t0       13 /dev/pts/10
    run     29337 user    3w   REG   8,32        0 98833431 /data1/user/work/simpleCode/tmp/testLinux.c
    
  • 还有就是利用strace 查看系统调用(strace是用来跟踪用户空间进程的系统调用和信号的),因为open是系统调用,看下open之后是否有close即可。sudo strace -p pid 2>&1 | grep -E 'open | close'
问题实例
  1. 创建消息队列时,创建失败,提示too many open files, errno 为24. 消息队列需要调用mqUnlink才能真正回收不使用的消息队列,mqClose只是关闭,不是销毁
  2. 删除通道时,卡死。top调用发现cpu占用不高,初步怀疑是死锁。 通过strace查看卡死在哪里, 以及通过gdb -p 线程id(线程id可通过pthreadInfo获得), 然后通过bt查看线程堆栈卡死在哪里 。后面确认是死锁
  3. 一个重启问题,coredump和addr2line定位问题行,发现是线程栈溢出
  4. 一个切换图像界面后,设备很卡的问题,使用top发现cpu占用极高,手动触发coredump(kill -11 pid), gdb查看线程信息 gdb i thread, 然后bt分析看是哪里
  5. 一个coredump出现 oom-killer 提示信息,内存碎片也有可能出现oom-killer
  6. 设备重启且每次导致重启的位置不一样,coredump定位的行不一样,后面发现是类中指针变量未初始化,后续直接使用了这个指针。
  7. 堆内存越界问题,每次产生的coredump文件定位位置不同

Linux系统常见调试工具

coredump

Core dump(或称为core file)是指在一个进程崩溃或异常终止时,系统会自动生成一个二进制文件,其中包含了该进程在崩溃时的内存映像、寄存器状态、堆栈信息等调试信息。这个文件通常被称为“核心转储文件”(core dump file),也可以简称为“core文件”。

生成原理:当进程发生崩溃时,操作系统会将该进程的内存映像信息、寄存器状态、堆栈信息等调试信息以二进制形式写入一个文件中,这个文件就是core dump文件。通常,这个文件的文件名以“core”开头,后面跟着一个进程ID,例如“core.1234”。

利用core dump来定位问题:通常情况下,core dump文件是用来进行调试和分析的。我们可以使用一些工具来分析core dump文件,以便了解进程崩溃的原因和位置。

如何配置生成coredump?
是不是所有异常重启问题都会生成coredump?
  • 不会,只有段错误才会,coredump本质上就是段错误。
如何手动触发coredump ?
  • kill -11 pid
造成segment fault(或者说core dump)的可能原因?
  1. 内存访问越界
    • 由于使用错误的下标,导致数组访问越界
    • 搜索字符串时,依靠字符串结束符来判断字符串是否结束,但是字符串没有正常的使用结束符
    • 使用strcpy, strcat, sprintf, strcmp, strcasecmp等字符串操作函数,将目标字符串读/写爆。应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界。
  2. 多线程程序使用了线程不安全的函数。
  3. 多线程读写的数据未加锁保护。对于会被多个线程同时访问的全局数据,应该注意加锁保护,否则很容易造成core dump. 因为很可能写的一方将数据销毁,而读的一方却还在继续读,典型的例子比如更新线程执行clear,如果工作线程这时候操作之前的迭代器,则可能内存错误
  4. 非法指针
    • 使用空指针
    • 随意使用指针转换。一个指向一段内存的指针,除非确定这段内存原先就分配为某种结构或类型,或者这种结构或类型的数组,否则不要将它转换为这种结构或类型的指针,而应该将这段内存拷贝到一个这种结构或类型中,再访问这个结构或类型。这是因为如果这段内存的开始地址不是按照这种结构或类型对齐的,那么访问它时就很容易因为bus error而core dump.
  5. 堆栈溢出.不要使用大的局部变量(因为局部变量都分配在栈上),这样容易造成堆栈溢出,破坏系统的栈和堆结构,导致出现莫名其妙的错误。
  6. 算术异常: 如除以0
  7. 操作已关闭或不存在的文件或套接字描述符?
coredump生成原理?
  • 在内核返回用户空间的时候,会调用do_signal()处理信号,在do_signal()中根据信号判断是否触发coredump,当然还跟coredump limit、mm->flags等等相关。满足coredump条件后,由do_coredump()进行coredump文件生成,核心是由binfmt->core_dump()进行的。
  • 在get_signal()中,判断信号是否会导致coredump。这些信号包括SIGQUIT、SIGILL、SIGTRAP、SIGABRT、SIGFPE、SIGSEGV、SIGBUS、SIGSYS、SIGXCPU、SIGXFSZ。具体信号表示的含义见下表

在这里插入图片描述

如何使用coredump文件进行调试和问题定位?
  1. 使用gdb工具打开core dump文件。例如,可以使用“gdb program core”命令来打开core dump文件。
  2. 使用gdb命令来查看崩溃时的调用栈信息、寄存器状态等信息。例如,可以使用“backtrace”命令来查看调用栈信息。
  3. 使用addr2line工具将调用栈地址转换为代码行号,以便定位崩溃发生的位置。例如,可以使用“addr2line -e program -f -a <address>”命令来将地址转换为代码行号。通过查看LR和PC, LR是执行完以后返回的,PC是当前取指的代码行,如果指针在动态库,则无法定位源文件及行数
  4. 使用objdump等工具来查看代码二进制文件的反汇编信息,以便深入分析崩溃原因。 待理解

gdb

多线程调试
  • info thread 查看当前进程的线程。
  • thread <ID> 切换调试的线程为指定ID的线程。
  • break file.c:100 thread all 在file.c文件第100行处为所有经过这里的线程设置断点。
  • set scheduler-locking off|on|step,这个是问得最多的。在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。
    off 不锁定任何线程,也就是所有线程都执行,这是默认值。
    on 只有当前被调试程序会执行。
    step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行
    
  • thread apply all bt 查看所用线程堆栈信息
怎么利用gdb调试变量被谁异常修改了
  1. 在GDB命令行中使用watch命令设置变量的监视点
  2. 当程序运行到监视点时,它会暂停执行,并显示有关变量的信息
  3. 使用backtrace命令查看堆栈跟踪,以确定是哪个函数修改了变量的值
内存泄漏问题:
  • 用GDB的内存泄漏检测功能。例如,在编译可执行文件时,使用-g选项和-O0优化选项,以便在调试时获得更多信息。然后使用valgrind工具或GDB的memory命令检查内存泄漏。

pstack

  • pstack用于展现进程的线程堆栈快照。pstack其实是gdb的一个功能封装, 相当于gdb下输入 thread apply all bt
  • 用法pstack pid ,程序可以是没用-g编译的,但信息更少,无法定位到代码行
  • 对pstack的作用, 大致可以归纳如下:
    1. 查看线程数(比pstree, 包含了详细的堆栈信息)
    2. 能简单验证是否按照预定的调用顺序/调用栈执行
    3. 采用高频率多次采样使用时, 能发现程序当前的阻塞在哪里, 以及性能消耗点在哪里?
    4. 能反映出疑似的死锁现象(多个线程同时在wait lock, 具体需要进一步验证)
    5. 待补充

top

  • 查询进程各线程占用cpu : top -H -p pid,可以通过这个命令知道某个进程下有多少个线程
  • top -p pid 查看特定pid进程

strace

  • 跟踪系统调用,它能够打开应用进程的这个黑盒,通过系统调用的线索,告诉你进程大概在干嘛。可以通过strace调用程序strace ./run,也可以直接跟踪运行的进程,strace -p pid即可. strace也不是真正的万能。当目标进程卡死在用户态时,strace就没有输出了. strace可用来排查句柄泄露等。starce的具体作用参考 https://blog.csdn.net/cs729298/article/details/81906375
  • strace用法: 既然strace是用来跟踪用户空间进程的系统调用和信号的,在进入strace使用的主题之前,我们先理解什么是系统调用。Linux内核目前有300多个系统调用,详细的列表可以通过syscalls手册页查看。这些系统调用主要分为几类:
     文件和设备访问类 比如open/close/read/write/chmod等
     进程管理类 fork/clone/execve/exit/getpid等
     信号类 signal/sigaction/kill 等
     内存管理 brk/mmap/mlock等
     进程间通信IPC shmget/semget * 信号量,共享内存,消息队列等
     网络通信 socket/connect/sendto/sendmsg 等
     其他
    
  • 限制strace只跟踪特定的系统调用
    • 如果你已经知道你要找什么,你可以让strace只跟踪一些类型的系统调用。例如,你需要看看在configure脚本里面执行的程序,你需要监视的系统调 用就是execve。让strace只记录execve的调用用这个命令:strace -f -o configure-strace.txt -e execve ./configure
  • 系统调用的时间
    • 这是一个很有用的功能,strace会将每次系统调用的发生时间记录下来,只要使用-t/tt/ttt三个参数就可以看到效果
  • 系统调用统计
    • strace不光能追踪系统调用,通过使用参数-c,它还能将进程所有的系统调用做一个统计分析给你,下面就来看看strace的统计,这次我们执行带-c参数的strace: strace -c ./test 最后能得到这样的trace结果, 这里很清楚的告诉你调用了那些系统函数,调用次数多少,消耗了多少时间等等这些信息,这个对我们分析一个程序来说是非常有用的。
      oracle@orainst[orcl]:~
      $strace -c ./test
      execve("./test", ["./test"], [/* 41 vars */]) = 0
      % time     seconds  usecs/call     calls    errors syscall
      ------ ----------- ----------- --------- --------- ----------------
       45.90    0.000140           5        27        25 open
       34.43    0.000105           4        24        21 stat64
        7.54    0.000023           5         5           old_mmap
        2.62    0.000008           8         1           munmap
        1.97    0.000006           6         1           uname
        1.97    0.000006           2         3           fstat64
        1.64    0.000005           3         2         1 read
        1.31    0.000004           2         2           close
        0.98    0.000003           3         1           brk
        0.98    0.000003           3         1           mmap2
        0.66    0.000002           2         1           set_thread_area
      ------ ----------- ----------- --------- --------- ----------------
      100.00    0.000305                    68        47 total
      

proc文件系统

  • 查看进程信息,如内存信息等

其它

  • sysRq查询系统信息,包括内存、线程栈、寄存器信息; 这个只在工作项目代码才有效,非linux自带工具
  • pstree: pstree 命令是以树形结构显示程序和进程之间的关系,此命令的基本格式如下:
    pstree [选项] [PID或用户名]。在阿里云中直接输入pstree可以显示下图所示信息:

在这里插入图片描述

  • 这里systemd含义如下: 历史上Linux 的启动一直采用init进程, init进程启动有两个缺点,一是启动时间长。init进程是串行启动,只有前一个进程启动完,才会启动下一个进程。二是启动脚本复杂。init进程只是执行启动脚本,不管其他事情。脚本需要自己处理各种情况,这往往使得脚本变得很长。Systemd 就是为了解决这些问题而诞生的。它的设计目标是,为系统的启动和管理提供一套完整的解决方案。 systemd 取代了initd,成为系统的第一个进程(PID 等于 1),其他进程都是它的子进程。
  • ps也可以查看线程, 在ps命令中,-T选项可以开启线程查看。下面的命令列出了由进程号为<pid>的进程创建的所有线程。$ ps -T -p <pid> ; 公司代码可以通过pthreadinfo直接找到线程的pid,或者知道进程pid后 ,ps -eLf | grep pid https://blog.csdn.net/robertsong2004/article/details/52369873
    UID    PID  PPID  LWP  C  NLWP STIME TTY   TIME   CMD
    root   29150 29147 29150  0   4 10:36 ?     00:00:00 PassengerHelperAgent
    root   29150 29147 29153  0   4 10:36 ?     00:00:00 PassengerHelperAgent
    root   29150 29147 29154  0   4 10:36 ?     00:00:00 PassengerHelperAgent
    root   29150 29147 29157  0   4 10:36 ?     00:00:00 PassengerHelperAgent
     
    PID:进程的进程 ID 号
    PPID:父进程的进程 ID
    LWP:轻量进程(light weight process) 或者线程的ID
    

内存问题定位方法

内存原理

linux进程内存布局图

在这里插入图片描述

在这里插入图片描述

  • 从上面这个图可以看出,进程是有代码段Text segment, BSS段(未初始化的全局,静态变量),数据段(已初始化的全局,静态变量),堆,内存映射区以及栈;
    • 静态库存放在Text区
    • 内存映射区是动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间。对于动态库,用mmap后备文件的私有映射。而当malloc大于128K时,也会用这段区域,这时mmap进行匿名文件的私有映射。后备文件的私有映射和匿名文件的私有映射的区别见 深入理解内存映射mmap
  • 高地址为内核地址, 这里是以4G内存为例,内核image的起始地址为0xc0000000, 从0xc0000000到0xfffffff的共1G的空间都是内核区。
  • 栈往低地址,堆往上。这样来记: 堆的话自然是越堆越高,也就是往高地址走。
内存分配机制
  • 进程得到的分配地址是虚拟内存的地址,真正怎么分配物理内存,由操作系统来实现
  • char *p = (char*)malloc(sizeof(char)*128); 只会分配虚拟内存,使用后才会分配物理内存.
  • 深入探究malloc,见Linux内存管理 (8)malloc 第四小节
    • malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系)
    • malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。这样子做主要是因为::brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。通过mmap进行匿名文件的私有映射实现。匿名文件的私有映射见 深入理解内存映射mmap。mmap分配的内存不是在堆区(heap区),通过cat /proc/<pid>/maps命令查看,会发现malloc分配超过128K内存后,heap区不会变大。
    • 进程调用A=malloc(30K)以后,这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,内核才分配A这块内存对应的物理页。如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的

Linux内存深入
  • 程序员通过总是分配大小为 2 的幂的内存块. 因为linux使用伙伴算法管理内存,而块的大小为2 的幂
  • 深入理解 meminfo 参考这篇文章:/proc/meminfo之谜 , 要点如下:
    • 打印输出中包含内核分配的内存和用户态分配的内存。部分页表是放在内存中的,所以页表会占用内存
    • /proc/meminfo中的 Committed_AS 表示所有进程已经申请的内存总大小,(注意是已经申请的,不是已经分配的),如果 Committed_AS 超过 CommitLimit 就表示发生了 overcommit,超出越多表示 overcommit 越严重。Committed_AS 的含义换一种说法就是,如果要绝对保证不发生OOM (out of memory) 需要多少物理内存。
    • 这里的vmalloc是内核用来分配大块内存的,vmalloc的核心是在vmalloc区域中找到合适的hole,hole是虚拟地址连续的;然后逐页分配内存来从物理上填充hole。分配成功后,返回虚拟地址给进程,这个函数得到的是连续的虚拟地址,物理上地址不连续。分配是在专门的vmalloc区域分配的。
    • 所有page cache里的页面(Cached)都是file-backed pages,不是Anonymous Pages。”Cached”与”AnoPages”之间没有重叠。 注:shared memory 不属于 AnonPages,而是属于Cached,因为shared memory基于tmpfs,所以被视为file-backed、在page cache里,上一节解释过。
    • mmap private anonymous pages属于AnonPages(Anonymous Pages),而mmap shared anonymous pages属于Cached(file-backed pages),因为shared anonymous mmap也是基于tmpfs的,上一节解释过。
    • Anonymous Pages是与用户进程共存的,一旦进程退出,则Anonymous pages也释放,不像page cache即使文件与进程不关联了还可以缓存。
  • 怎样统计所有进程总共占用多少内存?
    • 很多人通过累加 “ps aux” 命令显示的 RSS 列来统计全部进程总共占用的物理内存大小,这是不对的。RSS(resident set size)表示常驻内存的大小,但是由于不同的进程之间会共享内存,所以把所有进程RSS进行累加的方法会重复计算共享内存,得到的结果是偏大的。正确的方法是累加 /proc/[1-9]*/smaps 中的 Pss 。/proc/<pid>/smaps 包含了进程的每一个内存映射的统计值,详见proc(5)的手册页。Pss(Proportional Set Size)把共享内存的Rss进行了平均分摊,比如某一块100MB的内存被10个进程共享,那么每个进程就摊到10MB。这样,累加Pss就不会导致共享内存被重复计算了。命令如下:
      $ grep Pss /proc/[1-9]*/smaps | awk ‘{total+=$2}; END {print total}’需要注意的是,全部进程占用的内存并不等于 free 命令所显示的 “used memory”,因为“used memory”不仅包含了进程所占用的内存,还包含cache/buffer以及kernel动态分配的内存等等。

内存问题诊断
  • 栈内存
    • 栈越界: 安全编译选项(makefile中加 -fstack-protector),出问题时立马死机
    • 栈溢出,一般是线程栈太小
    • 避免方法: 不要申请大的局部变量; 开辟合适线程栈空间; 函数入口参数尽量引用或指针传递
  • 堆内存
    • dmalloc可用于内存泄露和越界检测
    • General Features of the Library , 谷歌翻译一下
    • How the Library Checks Your Program, 谷歌翻译一下
      • 用于跨线程越界和内存泄露
        • 堆内存跨界: 这个堆访问到另一个堆内存, 每次产生的coredump文件定位位置不同, 因为堆的地址不是一样的,dmalloc可以得到越界地址。举例: 一个堆申请了2K,结果将3K数据写入
          char *p = (char*)malloc(2*1024);
          memcpy(p, data, 3*1024*1024);
          
        • 局限性:自身内存开销大
    • 工作原理:内存前后填充只读权限字段,越界访问时触发coredump

如何定位内存泄漏

1. 通过查看信息排查
  • 先确定系统是否存在内存泄露
    • 首先要确定存在内存泄漏,包括top命令、free (free命令查看的是物理内存)、 cat /proc/meminfo ,查看是否增大(某些情况下增大是正常的,比如系统启动后加载各种模块)
    • cat /proc/meminfo为例: 统计MemFree变化曲线、 Committed_AS变化曲线( Committed_AS表示所有进程已经申请的内存总大小,注意是已经申请的,不是已经分配的,即申请了多少虚拟内存)、MemFree + Cached变化曲线。 这个曲线可以通过SecureCRT获取。
    • /proc/meminfo的简单定义 , 深入理解 /proc/meminfo 需要参考/proc/meminfo之谜
      • 核心在于
  • 确定是哪个进程
    • 方法一: 查看内存占用最多的进程,top命令; 关注VIRT和%MEM是否增大, 最主要关注VIRT, 内存泄露是指虚拟内存的泄露

      a PID 进程id
      b PPID 父进程id
      c RUSER Real user name
      d UID 进程所有者的用户id
      e USER 进程所有者的用户名
      f GROUP 进程所有者的组名
      g TTY 启动进程的终端名。不是从终端启动的进程则显示为 ?
      h PR 优先级
      i NI nice值。负值表示高优先级,正值表示低优先级
      j P 最后使用的CPU,仅在多CPU环境下有意义
      k %CPU 上次更新到现在的CPU时间占用百分比
      l TIME 进程使用的CPU时间总计,单位秒
      m TIME+ 进程使用的CPU时间总计,单位1/100秒
      n %MEM 进程使用的物理内存百分比
      o VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
      p SWAP 进程使用的虚拟内存中,被换出的大小,单位kb。
      q RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
      r CODE 可执行代码占用的物理内存大小,单位kb
      s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb
      t SHR 共享内存大小,单位kb
      u nFLT 页面错误次数
      v nDRT 最后一次写入到现在,被修改过的页面数。
      w S 进程状态。
      D=不可中断的睡眠状态
      R=运行
      S=睡眠
      T=跟踪/停止
      Z=僵尸进程
      x COMMAND 命令名/命令行
      y WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名
      z Flags 任务标志,参考 sched.h
      
    • 进一步确定是这个进程的方法

      • cat /proc/<pid>/status、查看进程状态信息。status命令中,Vmsize表示进程占用虚拟内存大小,VmRss进程占用物理内存大小(常驻内存)。这里主要根据VmSize持续增大得知,内存泄露时VmRss不一定持续增大。
      VmPeak 代表当前进程运行过程中占用内存的峰值.
      VmSize 代表进程现在正在占用的内存
      VmLck  代表进程已经锁住的物理内存的大小.锁住的物理内存不能交换到硬盘.
      VmHWM  是程序得到分配到物理内存的峰值.
      VmRSS  是程序现在使用的物理内存.
      VmData: 表示进程数据段的大小.
      VmStk: 表示进程堆栈段的大小.
      VmExe: 表示进程代码的大小.
      VmLib: 表示进程所使用LIB库的大小.
      VmPTE: 占用的页表的大小.
      VmSwap: 进程占用Swap的大小.
      Threads:表 示当前进程组的线程数量.
      SigPnd: 屏蔽位,存储了该线程的待处理信号,等同于线程的PENDING信号.
      ShnPnd: 屏蔽位,存储了该线程组的待处理信号.等同于进程组的PENDING信号.
      SigBlk: 存放被阻塞的信号,等同于BLOCKED信号.
      SigIgn: 存放被忽略的信号,等同于IGNORED信号.
      SigCgt: 存放捕获的信号,等同于CAUGHT信号.
      CapEff: 当一个进程要进行某个特权操作时,操作系统会检查cap_effective的对应位是否有效,而不再是检查进程的有效UID是否为0.
      CapPrm: 表示进程能够使用的能力,在cap_permitted中可以包含cap_effective中没有的能力,这些能力是被进程自己临时放弃的,也可以说cap_effective是cap_permitted的一个子集.
      CapInh: 表示能够被当前进程执行的程序继承的能力.
      CapBnd: 是系统的边界能力,我们无法改变它.
      Cpus_allowed: 3指出该进程可以使用CPU的亲和性掩码,因为我们指定为两块CPU,所以这里就是3,如果该进程指定为4CPU(如果有话),这里就是F(1111).
      Cpus_allowed_list: 0-1指出该进程可以使用CPU的列表,这里是0-1.
      voluntary_ctxt_switches  表示进程主动切换的次数.
      nonvoluntary_ctxt_switches  表示进程被动切换的次数.
      
    • cat /proc/\<pid\>/maps、smaps查看进程内存布局,可以查看某个进程的代码段、栈区、堆区、动态库、内核区对应的虚拟地址, 内存泄露时主要查看heap区是否太大以及内存区是否持续变大。maps只能显示简单的分区,smap文件可以显示每个分区的更详细的内存占用数据。如下是一个new出1M大小后不delete程序的map表,两个信息是相隔一段时间后输出的,可以看到内存映射区中的下划线标红红色区域变大了。
      在这里插入图片描述

    • 进程系统内存映射通过 cat /proc/PID/maps 查看进程内存映射表, 通过SecureCRT获取heap堆区是否发生泄漏, 以及通过AnonPages变化曲线,是否匿名映射大小在膨胀,如果是说明mmap或malloc大块内存分配存在泄漏?

  • 确定是哪个线程
    • 通过 cat /proc/PID/task/TID/stat, 查看min_flt增多的线程,缺页中断会越来越多,当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断的情况一样;若先进行读操作虽然也会产生缺页异常,将被映射给默认的零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
      • PS:产生缺页中断的几种情况
        1、当内存管理单元(MMU)中确实没有创建虚拟物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区(vma)的时候,可以肯定这是一个编码错误,这将杀掉该进程;
        2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页中断,并且可能是栈溢出导致的缺页中断;
        3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断的情况一样;若先进行读操作虽然也会产生缺页异常,将被映射给默认的零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
        4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,它的vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页中断的写时复制;原文链接:https://blog.csdn.net/m0_37962600/java/article/details/81448553
  • 代码段定位
    • 代码走读、比对和静态工具检测、搜索脚本grep所有内存分配语句行,(未配对、根据经验的智能指针)
    • 钩子函数,即替换系统的内存分配函数,然后通过新的内存分配函数增加打印来定位
    • 通过打印,比如内存分配失败的打印
2. 通过开源工具进行排查
  • 进行动态分析,如使用valgrind或者memwatch等工具 , 检测原理请查看下一小节
  • 内存泄露工具
    valgrind --tool=memcheck --leak-check=full --show-leak-kinds=definite   ./output/bin/program_name  ./cfg/lwj_ubuntu.cfg
     
    valgrind --tool=memcheck --leak-check=full --show-leak-kinds=definite  ./output/apps/bin/program_name ./cfg/lwj_ubuntu.cfg
     
    valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all
    
    The default value for the leak kinds to show is --show-leak-kinds=definite,possible.
    To also show the reachable and indirectly lost blocks in addition to the definitely and possibly lost blocks, you can use --show-leak-kinds=all. To only show the reachable and indirectly lost blocks, use --show-leak-kinds=indirect,reachable. The reachable and indirectly lost blocks will then be presented as shown in the following two examples.
    
3. 内存泄漏检测原理和实现方法
  • 记录内存申请时的堆栈和地址,并跟踪对应地址是否在给定时间里释放或者程序终止时是否释放,如果没有就将对应堆栈打印出来
  • valgrind内存泄漏检测原理(https://valgrind.org/docs/manual/design-impl.html):
    1. 内存泄漏检测:Memcheck跟踪程序中的所有内存分配,并记录它们的位置和大小。当程序退出时,Memcheck会检查是否存在未释放的内存,如果存在,则会报告内存泄漏问题。
    2. 影子记忆 让一个 工具记 住关于 每个内存 位置和/或内 存值的 历史。考虑以下 影子记 忆工具 的激励 列表;这些描述 很简短 ,但证 明了影子记忆(a)是强大的 ,(b)可以以各种 各样的方式使用 。Memcheck[21,12]是内 存检 查器 。它 会记 住哪 些 分配/释放 操作影 响 了每 个 内 存 位 置 , 因此 可 以 检 测 对 不 可寻 址 内 存 的 访 问。它还 记得 哪些 值 是未 定义 的(未 初始 化 或从 未定 义的 值 派生) ,因此可以检测未 定义 值的危 险使 用。 Purif y[6]是一 个类 似的 工具。
    3. 内存状态跟踪:Memcheck跟踪程序的每个内存分配和释放,并记录每块内存的状态。当程序释放内存时,Memcheck会确保它不再被访问,以防止野指针问题。当程序访问已释放的内存时,Memcheck会检测到此问题,并进行报告。
    4. 执行跟踪:Memcheck通过记录程序执行的每个指令来跟踪内存访问。当程序在访问内存时,Memcheck会检查内存状态,并报告任何非法或不规范的访问。
    5. 内存映射:Valgrind Memcheck会将程序的可执行文件和所有相关的动态链接库(例如.so文件)都映射到虚拟地址空间中。
    6. 二进制重写:Memcheck会修改程序的可执行文件和相关的动态链接库的二进制代码,使其在执行时会在内存分配和访问操作之前和之后插入一些检查代码。这些检查代码会记录程序对内存的访问,并进行内存状态的跟踪,以便在发现内存问题时进行报告。

如何预防内存泄露

  • 预防:包括智能指针、手动检测和静态工具(如PC-Lint)分析,这是代价最小的调试方法。 以及包括
  • 内存消毒, libasan.so

OOM说明

  • OOM是由于系统内存(常驻内存而非虚拟内存)不足而导致的问题,而内存泄漏则是由于程序内部存在问题而导致的问题。OOM通常是由于程序运行时需要的内存空间超过了系统可以提供的内存空间,而内存泄漏则是由于程序没有及时或正确地释放已经分配的内存空间。因此,需要注意区分OOM和内存泄漏这两个概念,以便更好地排查和解决相关问题。针对OOM问题,需要考虑如何优化程序的内存使用,减少内存的消耗。而针对内存泄漏问题,则需要通过分析程序代码,找出未释放内存的原因,并进行相应的修改和优化。
  • OOM跟内存泄漏无关 , OOM是内存溢出, 内存泄漏有可能触发OOM , OOM还可能是由于程序运行时需要的内存空间超过了系统可以提供的内存空间,或者是系统资源不足、进程过多等原因导致的。因此,需要对程序的内存使用进行监控和优化,及时发现和处理内存泄漏问题,以避免可能的OOM风险
  • 关于malloc,它分配的应该是虚拟内存。应用程序申请大量的虚拟内存并不会立即占用大量系统物理内存。而物理内存飙高的时候,并不是因为程序在大量malloc,而是程序真的在大范围访问已分配的到的虚拟地址空间。
  • oom杀死了哪个进程(常驻内存决定是否杀死而非虚拟内存); 查看是否发生oom方法如下:

在这里插入图片描述

一个会触发OOM的测试程序
 #include<iostream>
 #include<unistd.h>
 #include<vector>
 #include<cstring>
 using std::vector;
 int main()
 {
     std::vector<char*> vec;
     for(;;)
     {
         char *pBuf = nullptr;
         try{
             pBuf = new char[10*1024*1024];
         }catch(std::bad_alloc &e){
             std::cout<<"fatal error"<<std::endl;
             exit(1);
         }
         vec.push_back(pBuf);
         for(auto cp : vec){  
          /* 开始访问前面malloc的 ,这里都是真实访问的物理内存, 
             多次循环后,常驻内存不够,就会导致OOM, */
             memset(cp, rand()%25, 10*1024*1024);
         }
         sleep(1);
     }
     return 0;
 }

问题示例

  1. 把内联改为非内联为什么就可以解决踩内存的问题,这是为什么 ?

    C++ inline 内联函数 - 默行于世 - 博客园 (cnblogs.com)

    内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制) 为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

    以下情况不宜使用内联:(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了 inline 不应该出现在函数的声明中)。

参考资料

网络问题定位方法

netstat
  • 调试网络问题
  • 查看端口是否在监听
  • 查看处于timewait的是不是太多了
  • 显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员等
tcpdump, wireshark抓包分析
其它
  • 查看网络接口状态,ifconfig
  • 验证两台计算机的网络是否连通,ping;例,ping 10 www.baidu.com,一个正常工作的网络会报告零个数据包丢失。一个成功执行的“ ping”命令会意味着网络的各个部件(网卡,电缆,路由,网关)都处于正常的工作状态
  • 跟踪数据包在两台主机之间经过的路由,mtr或traceroute
  • 执行dns查询,host,例host www.baidu.com ; host 65.214.39.152
  • 配置网络接口,ifconfig; 例: 修改mac地址, ifconfig eht0 hw ether 00:14:CC:00:1A:00
  • 查看无线接口的状态, iwconfig
  • 获得特定接口的详细信息, 例,ifconfig eth0可获得eht0接口的详细信息,包括多播地址等(获得本地网络中众多主机的方法之一就是ping多播地址)

其它常见问题

动态库和静态库的区别?

到这里我们大致了解了静态库和动态库的区别了,静态库被使用目标代码最终和可执行文件在一起(它只会有自己用到的),而动态库与它相反,它的目标代码在运行时或者加载时链接。正是由于这个区别,会导致下面所介绍的这些区别。

  • 执行文件大小不一样 , 从前面也可以观察到,静态链接的可执行文件要比动态链接的可执行文件要大得多,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息。
  • 占用磁盘大小不一样 , 如果有多个可执行文件,那么静态库中的同一个函数的代码就会被复制多份,而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大。
  • 扩展性与兼容性不一样 , 如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署。
  • **依赖不一样 **, 静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。即便如此,系统中一班存在一些大量公用的库,所以使用动态库并不会有什么问题。
  • 复杂性不一样,相对来讲,动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活。这也不在本文的讨论范围。
  • 加载速度不一样, 由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。再加上局部性原理,牺牲的性能并不多。
如果程序在动态库中崩溃,如何定位?
  1. 通过map命令查看动态库的起始地址,这个也可以根据map文件找到对应实际地址,编译的时候加上-Wl,-Map,entry.map。即可生成map文件,后续有问题时就不需要提前获取map信息了
  2. 通过backtrace 获取栈调用信息,知道时X行
  3. 通过map获取动态库起始地址,假定是Y
  4. 然后addr2line Y-X .so
  • 示例(这里bt里直接给出了函数,跟上面有所不同,但原理是类似的)
    // 动态库backtrace 打印如下。
    
    Dump stack start...
    backtrace() returned 8 addresses
      [00] ./backtrace(dump+0x1f) [0x400a53]
      [01] ./backtrace(signal_handler+0x31) [0x400b1b]
      [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f8583672150]
      [03]./libTest.so(_ZN13Testt11testFuncER20_VIDEO_VIEW_REQ_INFO+**0x44**) [0xace45d0c]  // 这里已经提示了是testFunc
    Dump stack end...
    
    
    // 在map文件中搜索testFunc
    0x0007fcc8                test::testFunc(_VIDEO_VIEW_REQ_INFO&)
    
    // 可知在代码 0x0x0007fcc8 + 0x44处发生异常。
    addr2line -e libTest.so 0x7FD0C
    testServer.cpp:958
    
    
CPU负载的含义是什么 ?

在这里插入图片描述

  • CPU利用率:显示的是程序在运行期间实时占用的CPU百分比。 比如一个进程,一半时间计算,一半时间等待IO且假定当它需要CPU时就能获得, 则CPU利用率为50%
  • CPU负载:显示的是一段时间内正在使用和等待使用CPU的平均任务数。CPU利用率高,并不意味着负载就一定大。举例来说:如果我有一个程序它需要一直使用CPU的运算功能,那么此时CPU的使用率可能达到100%,但是CPU的工作负载则是趋近于“1”,因为CPU仅负责一个工作嘛!如果同时执行这样的程序两个呢?CPU的使用率还是100%,但是工作负载则变成2了。所以也就是说,当CPU的工作负载越大,代表CPU必须要在不同的工作之间进行频繁的工作切换。
  • CPU负载的一个类比: 判断系统负荷是否过重,必须理解load average的真正含义。下面,我根据"Understanding Linux CPU Load"这篇文章,尝试用最通俗的语言,解释这个问题。
    首先,假设最简单的情况,你的电脑只有一个CPU,所有的运算都必须由这个CPU来完成。
    那么,我们不妨把这个CPU想象成一座大桥,桥上只有一根车道,所有车辆都必须从这根车道上通过。(很显然,这座桥只能单向通行。)
    • 系统负荷为0,意味着大桥上一辆车也没有。
    • 系统负荷为0.5,意味着大桥一半的路段有车。
    • 系统负荷为1.0,意味着大桥的所有路段都有车,也就是说大桥已经"满"了。但是必须注意的是,直到此时大桥还是能顺畅通行的。
    • 系统负荷为1.7,意味着车辆太多了,大桥已经被占满了(100%),后面等着上桥的车辆为桥面车辆的70%。以此类推,系统负荷2.0,意味着等待上桥的车辆与桥面的车辆一样多;系统负荷3.0,意味着等待上桥的车辆是桥面车辆的2倍。总之,当系统负荷大于1,后面的车辆就必须等待了;系统负荷越大,过桥就必须等得越久。
      CPU的系统负荷,基本上等同于上面的类比。大桥的通行能力,就是CPU的最大工作量;桥梁上的车辆,就是一个个等待CPU处理的进程(process)。
  • 1.0是系统负荷的理想值吗? 不一定,系统管理员往往会留一点余地,当这个值达到0.7,就应当引起注意了。经验法则是这样的:
    • 当系统负荷持续大于0.7,你必须开始调查了,问题出在哪里,防止情况恶化。
    • 当系统负荷持续大于1.0,你必须动手寻找解决办法,把这个值降下来。
    • 当系统负荷达到5.0,就表明你的系统有很严重的问题,长时间没有响应,或者接近死机了。你不应该让系统达到这个值。
      对于我的机器,有24个core,那么,load多少合适呢?答案是 0.7*24 = 16.8
如果程序崩溃在动态库中如何处理?
  • 崩溃在动态库话, 要将程序地址PC 减去动态加载的起始地址(/proc/{pid}/maps, 可以在程序里提前放置信号处理函数, 接收到异常信号时就打印出来), 然后在addr2line 结果地址 动态库 得到代码位置. Linux应用程序崩溃定位
如何优化性能 ?
  • 用火焰图进行性能优化. 可以从各种性能分析工具获取,根据不同操作系统列出如下:
  • 火焰图的原理就是间隔一段时间去采样堆栈调用,如果函数仍在执行,则堆栈调用就不会改变。通过这种方式知道当前堆栈执行了多久,以及函数函数调用的关系和次数等
参考资料
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值