使用shell脚本一键计算linux下C语言多线程程序某一时刻各线程的栈空间使用率

背景

最近在项目中遇到一个比较棘手的问题:在有些场景中使用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
  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值