Contents
原文:Linux 程序异常诊断工具(pstack 与 strace 命令使用详解、死锁)_pstack strace_herryone123 的博客 -CSDN 博客
strace 跟踪程序使用的底层系统调用,可输出系统调用被执行的时间点以及各个调用耗时;pstack 工具对指定 PID 的进程输出函数调用栈。
一、strace
1.1 基本概念
strace 是一个可用于诊断、调试和教学的 Linux 用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。
strace 常用来 跟踪进程执行时的系统调用和所接收的信号 。在 Linux 世界, 进程不能直接访问硬件设备 ,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等) 时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace 可以跟踪到一个进程产生的系统调用, 包括参数,返回值,执行消耗的时间。
1.2 使用方法
strace -o output.txt -T -tt -e trace=all -p 28979
跟踪 28979 进程的所有系统调用(-e trace=all),并统计系统调用的花费时间,以及开始时间(并以可视化的时分秒格式显示),最后将记录结果存在 output.txt 文件里面。
参数:
-c 统计每一系统调用的所执行的时间, 次数和出错的次数等.
-d 输出 strace 关于标准错误的调试信息.
-f 跟踪由 fork 调用所产生的子进程.
-ff 如果提供 -o filename, 则所有进程的跟踪结果输出到相应的 filename.pid 中,pid 是各进程的进程号.
-F 尝试跟踪 vfork 调用. 在 - f 时,vfork 不被跟踪.
-h 输出简要的帮助信息.
-i 输出系统调用的入口指针.
-q 禁止输出关于脱离的消息.
-r 打印出相对时间关于,, 每一个系统调用.
-t 在输出中的每一行前加上时间信息.
-tt 在输出中的每一行前加上时间信息, 微秒级.
-ttt 微秒级输出, 以秒了表示时间.
-T 显示每一调用所耗的时间.
-v 输出所有的系统调用. 一些调用关于环境变量, 状态, 输入输出等调用由于使用频繁, 默认不输出.
-V 输出 strace 的版本信息.
-x 以十六进制形式输出非标准字符串
-xx 所有字符串以十六进制形式输出.
-a column
设置返回值的输出位置. 默认 为 40.
-e expr
指定一个表达式, 用来控制如何跟踪. 格式如下:
[qualifier=][!]value1[,value2]…
qualifier 只能是 trace,abbrev,verbose,raw,signal,read,write 其中之一.value 是用来限定的符号或数字. 默认的 qualifier 是 trace. 感叹号是否定符号. 例如:
-eopen 等价于 -e trace=open, 表示只跟踪 open 调用. 而 -etrace!=open 表示跟踪除了 open 以外的其他调用. 有两个特殊的符号 all 和 none.
注意有些 shell 使用! 来执行历史记录里的命令, 所以要使用 \.
-e trace=set
只跟踪指定的系统 调用. 例如:-e trace=open,close,rean,write 表示只跟踪这四个系统调用. 默认的为 set=all.
-e trace=file
只跟踪有关文件操作的系统调用.
-e trace=process
只跟踪有关进程控制的系统调用.
-e trace=network
跟踪与网络有关的所有系统调用.
-e strace=signal
跟踪所有与系统信号有关的 系统调用
-e trace=ipc
跟踪所有与进程通讯有关的系统调用
-e abbrev=set
设定 strace 输出的系统调用的结果集.-v 等与 abbrev=none. 默认为 abbrev=all.
-e raw=set
将指 定的系统调用的参数以十六进制显示.
-e signal=set
指定跟踪的系统信号. 默认为 all. 如 signal=!SIGIO(或者 signal=!io), 表示不跟踪 SIGIO 信号.
-e read=set
输出从指定文件中读出 的数据. 例如:
-e read=3,5
-e write=set
输出写入到指定文件中的数据.
-o filename
将 strace 的输出写入文件 filename
-p pid
跟踪指定的进程 pid.
-s strsize
指定输出的字符串的最大长度. 默认为 32. 文件名一直全部输出.
-u username
以 username 的 UID 和 GID 执行被跟踪的命令
1.3 应用举例
该部分应用参考参考文献【2】。
查看程序 server
# ps -elf | grep server | grep -v grep
0 S root 16739 22642 0 76 0 - 634 1024 14:26 pts/2 00:00:00 ./server
# strace -o server.strace -Ttt -p 16739
Process 16739 attached - interrupt to quit
一段时间后,停止,查看 servers.strace
- 14:46:39.741366 select(8, [3 4], NULL, NULL, {1, 0}) = 1 (in [4], left {0, 1648}) <0.998415>
- 14:46:40.739965 recvfrom(4, "hello", 6, 0, NULL, NULL) = 5 <0.000068>
- 14:46:40.740241 write(1, "hello\n", 6) = 6 <0.000066>
- 14:46:40.740414 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0 <0.000046>
- 14:46:40.740565 rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0 <0.000048>
- 14:46:40.740715 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 <0.000046>
- 14:46:40.740853 nanosleep({1, 0}, {1, 0}) = 0 <1.000276>
- 14:46:41.741284 sendto(4, "hello\0", 6, 0, NULL, 0) = 6 <0.000111>
由此知程序耗时在 nanosleep.
二、pstack
2.1 基本概念
pstack 就是由 gdb 执行的 shell 脚本。用于打印正在运行的进程的栈跟踪信息。它能对潜在的死锁予以提示, 而 pstack 只提供了线索, 需要 gdb 进一步的确定。
2.2 使用方法
pstack 是 gdb 的一部分。此命令允许使用的唯一选项是要检查的进程的 PID
ps auxf | grep <process_name>
pstack $pid
pstack 可以打印出该进程的所有线程的情况,那它自然就可以用来检测死锁。
该部分转载参考文献【1】,写得很详细:
(1)产生死锁的代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex_1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1_proc(void *data)
{pthread_mutex_lock(&mutex_1);
sleep(1);
pthread_mutex_lock(&mutex_2);
pthread_mutex_unlock(&mutex_2);
pthread_mutex_unlock(&mutex_1);
return (void *)0;
}
void *thread2_proc(void *data)
{pthread_mutex_lock(&mutex_2);
sleep(1);
pthread_mutex_lock(&mutex_1);
pthread_mutex_unlock(&mutex_1);
pthread_mutex_unlock(&mutex_2);
return (void *)0;
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread1_proc, NULL);
pthread_create(&tid2, NULL, thread2_proc, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
(2)编译及运行程序:
gcc -g -Wall -Werror dead_lock.c -pthread -o test
./test // 则进程死锁一直卡住了
pstack $pid
Thread 3 (Thread 0x7f0489d8e710 (LWP 3616)):
#0 0x0000003c0f60e034 in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003c0f609345 in _L_lock_870 () from /lib64/libpthread.so.0
#2 0x0000003c0f609217 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x000000000040068e in thread1_proc ()
#4 0x0000003c0f6077e1 in start_thread () from /lib64/libpthread.so.0
#5 0x0000003c0f2e153d in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f048938d710 (LWP 3617)):
#0 0x0000003c0f60e034 in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003c0f609345 in _L_lock_870 () from /lib64/libpthread.so.0
#2 0x0000003c0f609217 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x00000000004006d3 in thread2_proc ()
#4 0x0000003c0f6077e1 in start_thread () from /lib64/libpthread.so.0
#5 0x0000003c0f2e153d in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f0489d90700 (LWP 3615)):
#0 0x0000003c0f60803d in pthread_join () from /lib64/libpthread.so.0
#1 0x000000000040073d in main ()
Thread 2 和 Thread 3 都在等待锁,就是等待别人释放自己想要锁的那把锁,但是并不能看出来是否是死锁,继续使用 gdb 分析。
(3)使用 GDB 分析
# gdb -p $pid
# info thread// 打印所有的线程信息
3 Thread 0x7f645eb23710 (LWP 3687) 0x0000003c0f60e034 in __lll_lock_wait () from /lib64/libpthread.so.0
2 Thread 0x7f645e122710 (LWP 3688) 0x0000003c0f60e034 in __lll_lock_wait () from /lib64/libpthread.so.0
* 1 Thread 0x7f645eb25700 (LWP 3686) 0x0000003c0f60803d in pthread_join () from /lib64/libpthread.so.0
* 表示 gdb 锁定的线程,切换到第二个线程去查看
# thread 2 // 切换到第 2 个线程, 可以看到线程 id 为 0x7f645e122710, 而LWP 指定的值是 gdb 用来唯一标示该进程中线程的,便于调试的时候追踪
[Switching to thread 2 (Thread 0x7f645e122710 (LWP 3688))]#0 0x0000003c0f60e034 in __lll_lock_wait ()
# bt // bt 可以打印函数堆栈,却无法看到函数参数,#0 0x0000003c0f60e034 in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003c0f609345 in _L_lock_870 () from /lib64/libpthread.so.0
#2 0x0000003c0f609217 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x00000000004006d3 in thread2_proc (data=0x0) at dead_lock.c:23
#4 0x0000003c0f6077e1 in start_thread () from /lib64/libpthread.so.0
#5 0x0000003c0f2e153d in clone () from /lib64/libc.so.6
# frame 3 // 打印第三帧信息(#3). 每次函数调用都会有压栈的过程,而 frame 则记录栈中的帧信息
#3 0x00000000004006d3 in thread2_proc (data=0x0) at dead_lock.c:23
23 pthread_mutex_lock(&mutex_1);
# p mutext_1 // 打印 mutex_1 的值 , __owner 表示 gdb 中标示线程的值,即 LWP
$1 = {__data = {__lock = 2, __count = 0, __owner = 3687, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0,
__next = 0x0}}, __size = "\002\000\000\000\000\000\000\000g\016\000\000\001", ‘\000’ <repeats 26 times>, __align = 2}
分析另一个线程 3
# thread 3
# frame 3
# p mutex_2
$2 = {__data = {__lock = 2, __count = 0, __owner = 3688, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0,
__next = 0x0}}, __size = "\002\000\000\000\000\000\000\000h\016\000\000\001", ‘\000’ <repeats 26 times>, __align = 2}
LWP(3688)在等待 LWP(3687)所拥有的 mutex_1, 而同时 LWP(3687)又在等待 LWP(3688)所拥有的 mutex_2, 死锁。
三、死锁的概念
3.1 死锁概念
互斥锁是保护临界资源被线程间(或进程间)互斥的访问临界资源,当一个线程得到锁不释放时另一个线程申请时必须等待。当多个线程因为竞争资源而造成的一种僵局(互相等待),如果不施以援手,这些进程将永远等待。
3.2 产生条件
① 系统资源不足:系统中所拥有的资源其数量不足以满足线程运行的需要,使得在运行过程中,因争夺资源而陷入僵局。
② 线程间推进顺序非法:线程间在运行过程中,申请和释放的顺序不合法。
③ 资源分配不当。
四、常用工具
参考文献
- linux 命令之 pstack:http://www.voidcn.com/article/p-tigeosxb-ph.html
- Linux strace、pstack 命令 使用详解:https://blog.csdn.net/joeyon1985/article/details/72986412
- linux 命令 – pstack 命令(跟踪进程栈):https://www.cnblogs.com/kongzhongqijing/articles/7685699.html
- Linux strace 命令:https://www.cnblogs.com/ggjucheng/archive/2012/01/08/2316692.html
- Linux strace 命令详解:https://www.linuxidc.com/Linux/2012-12/75671.htm
- Linux— 死锁及避免死锁的方法: https://blog.csdn.net/qq_37934101/article/details/81869245
- CPU Flame Graphs – Brendan Gregg