背景
最近在项目中遇到一个比较棘手的问题:在有些场景中使用malloc函数申请几M内存失败。
使用free命令发现还有足够的物理内存。通过cat /proc/pid/maps
将我们的主程序的虚拟地址空间打出来,发现虚拟内存已经被用完,导致无法申请到连续几M的虚拟地址。
分析
通过分析maps发现,除了一些必要的动态库和必要大数组使用的空间以外,有大量的线程栈占用(大约有150个线程)。如下所示:
34cf3000-350f2000 rwxp 00000000 00:00 0 [stack:1785]
由于我们线程创建函数默认没有指明栈空间:
pthread_create(&thread_id, NULL, thread_function, NULL);
开机脚本使用 ulimit -s 4096
命令将所有线程栈空间设置为4M,导致150多个线程占用600多M虚拟地址空间,非常浪费。
故我们决定修改创建线程时候传递的参数,将不需要太多栈空间的线程通过 attr
参数指定它的栈大小,如下:
pthread_attr_t attr;
size_t stack_size = 1 * 1024 * 1024; // 1M
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, stack_size);
pthread_create(&thread_id, &attr, thread_function, NULL);
如果所有线程都设置为1M,则可以省下大约450M的虚拟地址空间。
导致线程占用栈空间过大一般有如下因素:
- 大数组
- 复杂的数据结构
- 函数递归调用
如果在程序的生命周期中所有线程都使用不超过1M的空间则没什么问题。
但如果在某一时刻有的线程占用了大量栈空间,比如3M,那么将其设置成1M就会由于栈溢出导致不可预料的后果。
那么问题来了,当前的150多个线程是由各部门的同事创建并维护的,分别实现不同模块的功能。我要怎么才能知道它们实际使用了多少栈空间呢?
手动计算每个线程的栈空间使用情况
上一篇博客:linux下c语言多线程程序使用gdb查看每个线程占用的栈空间,从而可以计算出线程的栈使用率 中提到,使用gdb工具可以得出每个线程当前的sp指针,再和maps结合即可算出当前的栈空间使用量。
但是手动操作还是比较麻烦,实际操作下来得到结果通常得十多分钟。如果要观察多个时刻则处理起来比较麻烦。
pstack脚本介绍
pstack实际上是一个由gdb封装的Shell脚本程序,它可以打印出指定进程下每个线程的栈追踪信息。查看脚本发现它进入gdb,执行了命令thread apply all
并筛选出关键信息打印。
基于pstack脚本修改
基于此脚本,我们可以修改一下:
- 先执行命令
cat /proc/pid/maps
查看当前进程的maps信息,筛选出stack相关的行保存至文件1 - 再执行命令
thread apply all info registers
打印所有线程的寄存器信息,筛选出sp寄存器指向的地址保存至文件2 - 将2个文件的数据进行处理,计算出各线程的栈空间使用率
使用介绍和效果
这里写一个简单的程序来演示:
程序开辟了5个线程,
前4个线程分别设置栈空间大小为3M、4M、6M、8M,各线程执行的函数分别开辟数组大小为1M、2M、3M、4M。
第5个线程没有使用大数组,而是执行其他流程。
先使用ulimit -s 4096
设置系统默认的栈空间为4M,这是个软限制。
再使用ps
命令打印进程的pid。
执行脚本并传入进程的pid即可得出各线程的栈空间使用率。
可以看到操作系统分配的虚拟地址空间分别是3M、4M、6M、8M、4M(attr未指定,由操作系统根据ulimit分配),而每个线程的使用量分别是1M、2M、3M、4M、5k,符合预期。
在不同阶段多次执行此脚本即可得知不同的线程大概会使用多少栈空间,从而来合理地分配栈空间大小,节省虚拟地址空间使用量的同时尽量避免栈空间不足导致溢出的情况。
最终的脚本
#/bin/bash
if test $# -ne 1; then
echo "Usage: `basename $0 .sh` <process-id>" 1>&2
exit 1
fi
if test ! -r /proc/$1; then
echo "Process $1 not found." 1>&2
exit 1
fi
# GDB doesn't allow "thread apply all bt" when the process isn't
# threaded; need to peek at the process to determine if that or the
# simpler "bt" should be used.
info_registers="info registers"
if test -d /proc/$1/task ; then
# Newer kernel; has a task/ directory.
if test `/bin/ls /proc/$1/task | /usr/bin/wc -l` -gt 1 2>/dev/null ; then
info_registers="thread apply all info registers"
fi
elif test -f /proc/$1/maps ; then
# Older kernel; go by it loading libpthread.
if /bin/grep -e libpthread /proc/$1/maps > /dev/null 2>&1 ; then
info_registers="thread apply all info registers"
fi
fi
# Output the maps information and filter for lines containing "stack"
#echo "Maps information:"
if test -f /proc/$1/maps; then
cat /proc/$1/maps | grep "stack" | awk -F ' ' '{print $1 " " $6}' > maps.txt
else
echo "No maps file found for process $1."
fi
#echo "Stack information:"
GDB=${GDB:-/usr/bin/gdb}
# Run GDB, strip out unwanted noise.
# --readnever is no longer used since .gdb_index is now in use.
$GDB --quiet -nx $GDBARGS /proc/$1/exe $1 <<EOF 2>&1 |
set width 0
set height 0
set pagination no
$info_registers
EOF
awk '
/Thread/ {
match($0, /LWP [0-9]+/);
split(substr($0, RSTART, RLENGTH), array, " ");
thread = array[2];
}
$1 == "sp" {
print thread ":" $2
}' > thread_ps_info.txt
# Read the thread info
while IFS=: read -r thread sp; do
# Find the corresponding line in the maps file
mapline=$(grep "\[stack:$thread\]" maps.txt)
# Extract the start and end addresses
start=${mapline%-*}
end=${mapline% *}
end=${end#*-}
# Check if start, end, or sp is empty or invalid
if [[ -z "$start" ]] || [[ -z "$end" ]] || [[ -z "$sp" ]] || [[ "$start" == "0x" ]] || [[ "$end" == "0x" ]] || [[ "$sp" == "0x" ]]; then
echo "Thread $thread: Error - Invalid or missing data"
continue
fi
# Convert hex to decimal
start_dec=$(printf "%d" "0x$start")
end_dec=$(printf "%d" "0x$end")
sp_dec=$(printf "%d" "$sp")
# Calculate the total and used stack sizes
total=$(( (end_dec-start_dec) / 1024 ))
used=$(( (end_dec-sp_dec) / 1024 ))
# Check if total is zero to avoid division by zero
if [[ "$total" -eq 0 ]]; then
echo "Thread $thread: Error - Total stack size is zero"
continue
fi
# Calculate the usage rate
rate=$(( used * 100 / total ))
# Print the results
printf "Thread %s: total=%sKB used=%sKB rate=%s%%\n" "$thread" "$total" "$used" "$rate"
done < thread_ps_info.txt