Skip to content

Instantly share code, notes, and snippets.

@ytoshima
Last active October 7, 2015 03:18
Show Gist options
  • Save ytoshima/3096900 to your computer and use it in GitHub Desktop.
Save ytoshima/3096900 to your computer and use it in GitHub Desktop.
Print process' stack use on modern Linux
#!/usr/bin/perl
use strict;
#use warnings;
=head1 NAME
pstk.pl - shows estimated thread stack use
=head1 SYNOPSIS
perl pstk.pl <pid>
=head1 DESCRIPTION
pstk.pl shows estimated thread stack use in the specified process.
=cut
# Print process information for the passed pid using ps -ef.
# args: pid
# return: nothing
sub showPidInfo {
my $pid = shift;
unless (defined($pid)) {
print "E: pid was not passed to showPidInfo.\n";
return undef;
}
open(IN, "ps -fp $pid |");
while (<IN>) {
print $_;
}
close(IN);
}
# Get text file content of the file path which was passed as
# an argument.
# args: a file path
# return: String contents of the file
sub getFileContent {
unless (defined($_[0])) {
my ($package, $filename, $line) = caller;
die "Caller $package:$filename:$line " .
"did not pass filename to getFileContent.\n";
}
my $path = $_[0];
open(IN, $path) || die "getFileContent:open($path):$!";
my $str = '';
while (<IN>) {
$str .= $_;
}
close(IN);
return $str;
}
# For given pid, returns a map of task id to path of
# /proc/<pid>/task/<task>/stat.
# args: a pid
# return: a map of task id to stat file.
sub getTaskStatsMapForPid {
unless (defined($_[0])) {
my ($package, $filename, $line) = caller;
die "Caller $package:$filename:$line " .
"did not pass pid to getTaskStatsPathsForPid.\n";
}
my $pid = $_[0];
my $taskPath = "/proc/" . $pid . "/task/";
opendir(DIR, $taskPath) || die "opendir failed: $!";
my @subs = grep(/\d+/, readdir(DIR));
closedir(DIR);
my %task2stat =
map {$_ => "/proc/" . $pid . "/task/" . $_ . "/stat"} @subs;
%task2stat;
}
# Process /proc/<pid>/smaps contents passed as a string and returns
# a list of hashes. Each hash represents memory segments read from
# smaps. smaps has lines which represents memory segment's address
# range, permission, etc, which is same as the content of
# /proc/<pid>/maps. Following the line,
# some additional attributes follow like Size, Rss, etc.
# The line same as maps and following attributes are put into
# a single hash and they're returned in a list.
# args: String content of /proc/<pid>/smaps
# returns: a list of references to hashes which represent memory
# segment information.
sub processSmaps {
my $str = shift;
my @lines = split(/[\r\n]+/, $str);
# find the positions of reg lines
my @reglist = ();
my $i = 0;
my @reglindices = ();
my $l = undef;
for (my $i = 0; $i <= $#lines; $i++) {
$l = $lines[$i];
if ($l =~ /([0-9a-f]+)-([0-9a-f]+)\s+([r-][w-][x-][ps])\s+([0-9a-f]+)\s+(\S+)\s+(\d+)\s*(.*)/) {
push(@reglindices, $i);
}
}
# add a dummy entry so that we can determine the range of the
# attributes for the last segment information.
push(@reglindices, $#lines+1);
for (my $i = 0; $i < $#reglindices; $i++) {
$l = $lines[$reglindices[$i]];
my %regl = ();
if ($l =~ /([0-9a-f]+)-([0-9a-f]+)\s+([r-][w-][x-][ps])\s+([0-9a-f]+)\s+(\S+)\s+(\d+)\s*(.*)/) {
# following two lines cause
# "Hexadecimal number > 0xffffffff non-portable" warn.
$regl{"begin"} = hex $1;
$regl{"end"} = hex $2;
$regl{"params"} = $3;
$regl{"offset"} = hex $4;
$regl{"dev"} = $5;
$regl{"inode"} = $6;
$regl{"pathname"} = $7;
for (my($j) = $reglindices[$i]+1; $j < $reglindices[$i+1]; $j++) {
$l = $lines[$j];
if ($l =~ /([A-Z][A-Za-z0-9_]+):\s+(\d+)\s+kB/) {
$regl{$1} = $2;
} else {
print "E: unexpected line at $j: $l";
}
}
my %copy = %regl;
push(@reglist, \%copy);
} else {
print "E: unexpected line at $i: $l\n";
}
}
@reglist;
}
my $kstackspidx = 28;
# Convert tid to /proc/<pid>/task/stat content map to tid to sp map.
# args: tid to stat content map
# returns: tid to sp map
sub getTidSpMap {
my %tid2Stat = @_;
my %tid2sp = ();
while (my($tid, $cont) = each(%tid2Stat)) {
my @flds = split(/\s+/, $cont);
$tid2sp{$tid} = $flds[$kstackspidx];
}
%tid2sp;
}
# Returns region information map for given address.
# args: addr -- a virtual address
# regs -- list of region information map references.
# returns: matching region information map or undef if there was no
# matching region.
sub regForAddr {
my($addr, @regs) = @_;
# @regs is a list of regions. Each element is a reference to a hash
# which contains key like 'begin', 'end', etc.
my $e = ();
foreach $e (@regs) {
my %h = %$e;
if ($addr >= $h{'begin'} && $addr < $h{'end'}) {
return %h;
}
}
();
}
# Print stack use information for given pid.
# args: a pid
# returns: nothing
sub showStackUse {
my $pid = shift;
unless (defined($pid)) {
print "E: pid was not passed to showStackUse().";
return;
}
my $smaps = getFileContent("/proc/" . $pid . "/smaps");
my @regions = processSmaps($smaps);
my %task2statPath = getTaskStatsMapForPid($pid);
my %task2statCont = ();
while (my($tid, $path) = each(%task2statPath)) {
$task2statCont{$tid} = getFileContent($path);
}
my %tidSpMap = getTidSpMap(%task2statCont);
while (my($tid, $sp) = each(%tidSpMap)) {
my(%reg) = regForAddr($sp, @regions);
printf("tid %5d sp %#x ", $tid, $sp);
if (scalar(keys(%reg)) > 0) {
printf("%x-%x %5s k %4s k %s %s\n",
$reg{'begin'}, $reg{'end'},
$reg{'Size'}, $reg{'Rss'},
$reg{'params'}, $reg{'pathname'});
} else {
print "No region\n";
}
}
}
sub ownTaskKsp {
my %task2statPath = getTaskStatsMapForPid($$);
if (scalar(keys(%task2statPath)) > 0) {
my @stpaths = values(%task2statPath);
my $mytstat = $stpaths[0];
if (-e $mytstat) {
my $mytstatCont = getFileContent($mytstat);
my @flds = split(/\s+/, $mytstatCont);
my $ksp = $flds[$kstackspidx];
return $ksp;
} else {
return undef;
}
} else {
return undef
}
}
# check whether this can lookup own sp
# This does not work for running thread...
sub canLookupOwnSp {
my $ksp = ownTaskKsp();
if (defined($ksp)) {
my $smaps = getFileContent("/proc/" . $$ . "/smaps");
my @regions = processSmaps($smaps);
my %reg = regForAddr($ksp, @regions);
if (scalar(keys(%reg)) > 0) {
return 1;
} else {
return undef;
}
} else {
return undef;
}
}
sub ownSpLooksOk {
my $ksp = ownTaskKsp();
# System which does not provide valid ksp seems to return -1L as
# a string of long. Unfortunately, perl is not good at handling
# 64-bit integer like 0xffffffff or 0xffffffffffffffff and it does
# not have literal to denote long int value. So, I use string
# literal to check such cases.
if ($ksp =~ /^0$/ ||
$ksp =~ /^18446744073709551615$/ ||
$ksp =~ /^4294967295$/) {
return undef;
} else {
return 1;
}
}
sub isLinux {
return $^O =~ /^[Ll]inux/;
}
sub usage {
print <<END_OF_MSG;
usage: perl pstk.pl <pid>
END_OF_MSG
}
=head1 EXIT STATUS
For normal completion, exits with 0. For usage error, exits with 1.
If kernel does not seem to provide required feature
(/proc/<pid>/task/<task>/stat's 29-th field does not have valid
kernel stack pointer).
=head1 EXAMPLES
$ perl pstk.pl 5171
tid 5176 sp 0xf14fecbc f1482000-f1500000 504 k 8 k rwxp
tid 5171 sp 0xffa26464 ff82f000-ffa2b000 2036 k 40 k rwxp [stack]
tid 5173 sp 0xf1cfebf4 f1c82000-f1d00000 504 k 20 k rwxp
tid 5175 sp 0xf16ff0f0 f1682000-f1700000 504 k 4 k rwxp
tid 5178 sp 0xf10feeac f1080000-f1100000 512 k 8 k rwxp
tid 5177 sp 0xf12fedcc f1282000-f1300000 504 k 8 k rwxp
tid 5174 sp 0xf1afec44 f1a82000-f1b00000 504 k 20 k rwxp
tid 5172 sp 0xf1eeff1c f1e71000-f1efd000 560 k 36 k rwxp
The fourth column is the stack pointer of a thread(task). The fifth
column is the virtual address range of the stack memory segment found
in /proc/<pid>/smaps. The sixth field is the size of the stack
segment in k-bytes. The eighth field is the size of resident part of
the stack segment (Rss). These fields come from /proc/<pid>/smaps
entry like below:
f1c82000-f1d00000 rwxp 00000000 00:00 0
Size: 504 kB
Rss: 20 kB
Pss: 20 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 20 kB
Referenced: 20 kB
Anonymous: 20 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
This tool is based on the assumption that pages for used stack area
are usually resident on memory. Thus, the output may not reflect
the used stack area if the system is running out of physical memory
pages and pagings are happening.
=head1 ERRORS
This program tries to objtain stack pointer from
/proc/<pid>/task/<taskid>/stat's 29-th field(kstkesp, see proc(5)).
That field is valid in relatively new kernel, e.g. 2.6.32 (RHEL6).
Older kernel like 2.6.18 (RHEL5) does not. This program checks if
the feature is available by checking its own sp and shows following
error if the kernel does not seem to provide the information.
E: could not lookup own sp. proc module in kernel looks old.
=cut
# main
# printf("my ksp %#x\n", ownTaskKsp());
if (!isLinux()) {
print "E: This tool is for Linux.\n";
exit(1);
}
if (ownSpLooksOk()) {
if ($#ARGV >= 0) {
showStackUse($ARGV[0]);
exit(0);
} else {
usage();
exit(1);
}
} else {
printf("E: could not lookup own sp. proc module in kernel looks old.\n");
exit(2);
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.math.BigInteger;
import java.util.regex.*;
public class StackUse {
static void usage() {
P("usage: java StackUse <pid>");
}
static abstract class SmapsLine {
boolean isRegLine() { return false; }
}
static class RegLine extends SmapsLine {
final BigInteger begin;
final BigInteger end;
final String params;
final BigInteger offset;
final String dev;
final int inode;
final String pathname;
public RegLine(BigInteger begin, BigInteger end, String params,
BigInteger offset,
String dev, int inode, String pathname) {
this.begin = begin;
this.end = end;
this.params = params;
this.offset = offset;
this.dev = dev;
this.inode = inode;
this.pathname = pathname;
}
boolean isRegLine() { return true; }
}
static class AttrLine extends SmapsLine {
final String key;
final long kb;
public AttrLine(String key, long kb) {
this.key = key;
this.kb = kb;
}
}
static class RegInfo {
final RegLine line;
final Map<String,Long> attrs;
public RegInfo(RegLine line, Map<String,Long> attrs) {
this.line = line;
this.attrs = attrs;
}
boolean contains(BigInteger addr) {
return (addr.compareTo(line.begin) >= 0 && addr.compareTo(line.end) < 0);
}
static Object null2dash(Object o) { return (o == null ? "-" : o); }
public String toString() {
return String.format("%x-%x %4sk %4sk %s %s",
line.begin, line.end,
null2dash(attrs.get("Size")),
null2dash(attrs.get("Rss")),
line.params, line.pathname);
}
}
static String getFileContent(String path) throws Exception {
File f = new File(path);
BufferedReader br = new BufferedReader(new FileReader(f));
StringBuilder sb = new StringBuilder();
String s = null;
while ((s = br.readLine()) != null) {
sb.append(s + "\n");
}
br.close();
return sb.toString();
}
static BigInteger hexStr2BigInt(String s) { return new BigInteger(s, 16); }
static List<RegInfo> parseSmapsData(String smaps) {
Pattern reglPtn = Pattern.compile("([0-9a-f]+)-([0-9a-f]+)\\s+([r-][w-][x-][ps])\\s+([0-9a-f]+)\\s+(\\S+)\\s+(\\d+)\\s*(.*)");
Pattern attrPtn = Pattern.compile("([A-Z][A-Za-z0-9_]+):\\s+(\\d+)\\s+kB");
List<SmapsLine> sll = new ArrayList<SmapsLine>();
for (String l : smaps.split("[\r\n]+")) {
Matcher mRegl = reglPtn.matcher(l);
Matcher mAttr = attrPtn.matcher(l);
if (mRegl.matches()) {
sll.add(new RegLine(hexStr2BigInt(mRegl.group(1)),
hexStr2BigInt(mRegl.group(2)),
mRegl.group(3),
hexStr2BigInt(mRegl.group(4)),
mRegl.group(5),
Integer.parseInt(mRegl.group(6)),
mRegl.group(7)));
} else if (mAttr.matches()) {
sll.add(new AttrLine(mAttr.group(1), Long.parseLong(mAttr.group(2))));
} else {
P("E: unexpected line: " + l);
}
}
// Convert SmapsLine list to RegInfo list
List<RegInfo> ril = new ArrayList<RegInfo>();
RegLine rllast = null;
Map<String, Long> amap = null;
for (SmapsLine sl : sll) {
if (sl.isRegLine()) {
// process previous ents
if (rllast != null) {
ril.add(new RegInfo(rllast, amap));
amap = null;
}
// create new ent
rllast = (RegLine)sl;
} else if (sl instanceof AttrLine) {
assert(rllast != null);
if (amap == null) amap = new HashMap<String, Long>();
AttrLine al = (AttrLine)sl;
amap.put(al.key, al.kb);
}
}
return ril;
}
static Map<Integer,BigInteger> taskStatsToTaskSpMap(Map<Integer,String> taskStats) {
final int kstackspidx = 28;
Map<Integer,BigInteger> tspmap = new HashMap<Integer,BigInteger>();
for (Integer tid: taskStats.keySet()) {
tspmap.put(tid, new BigInteger(taskStats.get(tid).split("\\s+")[kstackspidx]));
}
return tspmap;
}
static void ana1(int pid, String smaps, Map<Integer,String> taskStats) {
List<RegInfo> smsd = parseSmapsData(smaps);
Map<Integer,BigInteger> taskSpMap = taskStatsToTaskSpMap(taskStats);
for (Integer tid: taskSpMap.keySet()) {
BigInteger sp = taskSpMap.get(tid);
System.out.print(String.format("tid: %5d sp: %#x ", tid, sp));
for (RegInfo ri: smsd) {
if (ri.contains(sp)) {
System.out.println(ri);
}
}
}
}
static void doit(int pid) {
try {
String smapsData = getFileContent("/proc/" + pid + "/smaps");
File taskd = new File("/proc/" + pid + "/task");
int[] tids = getNumbers(taskd.list());
Map<Integer,String> taskStatData = new TreeMap<Integer,String>();
for (int stid: tids) {
String statPath = "/proc/" + pid + "/task/" + stid + "/stat";
String cont = getFileContent(statPath);
taskStatData.put(stid, cont);
}
ana1(pid, smapsData, taskStatData);
} catch (Throwable t) { P(t); t.printStackTrace(); }
}
/**
* Picks up decimal numbers in args and return them in int[].
* @param args
* @return
*/
static int[] getNumbers(String[] args) {
int count = 0;
for (String a : args) {
try {
Integer.parseInt(a);
count++;
} catch (NumberFormatException nfe) {}
}
int[] ra = new int[count];
int idx = 0;
for (String s: args) {
try {
ra[idx] = Integer.parseInt(s);
idx++;
} catch (NumberFormatException nfe) {}
}
return ra;
}
static void P(Object s) { System.out.println(s); }
public static void main(String[] args) {
int[] pids = getNumbers(args);
if (pids.length == 0) {
usage();
} else {
for (int pid: pids) {
doit(pid);
}
}
}
}
/**
* Print thread stack use for each thread.
* This is for linux which has /proc/<pid>/smaps and /proc/<pid>/task/<tid>/stat.
*/
object StackUse extends App {
import scala.util.control.Exception._
import scala.io.Source
import java.io.File
/**
* Parse (pid, task stat string) tuple and returns (pid, sp) tuple.
*/
def parseTaskStat(e: (Int, String)): (Int,BigInt) = {
val (tid, stat) = (e._1, e._2)
val kstackspidx = 28
val sp = BigInt("""\s+""".r.split(stat)(kstackspidx))
(tid, sp)
}
class SmapsLine {
def isRegLine = false
}
case class RegLine(begin: BigInt, end: BigInt,
params: String, offset: BigInt, dev: String,
inode: Int, pathname: String) extends SmapsLine {
override def isRegLine = true
}
case class AttrLine(key: String, kb: Long) extends SmapsLine
case class RegInfo(line: RegLine, attrs: Map[String,Long]) {
def contains(addr: BigInt) = addr >= line.begin && addr < line.end
override def toString() = "%x-%x %5sk %4sk %s %s".format(
line.begin, line.end,
allCatch withApply {t=>"-"} apply { attrs("Size").toString },
allCatch withApply {t=>"-"} apply { attrs("Rss").toString },
line.params, line.pathname)
}
def hexStr2BigInt(s: String) = BigInt(s, 16)
def parseSmapsData(smaps: String) = {
val reglPtn = """([0-9a-f]+)-([0-9a-f]+)\s+([r-][w-][x-][ps])\s+([0-9a-f]+)\s+(\S+)\s+(\d+)\s*(.*)""".r
val attrPtn = """([A-Z][A-Za-z0-9_]+):\s+(\d+)\s+kB""".r
// Convert each line to a RegLine or an AttrLine using regex matching
val sls: Array[Option[SmapsLine]] =
for (l <- """[\r\n]+""".r.split(smaps)) yield l match {
case reglPtn(begin, end, params, offset, dev, inode, pathname) =>
Some(RegLine(hexStr2BigInt(begin), hexStr2BigInt(end), params,
hexStr2BigInt(offset), dev, inode.toInt, pathname))
case attrPtn(key, kb) =>
Some(AttrLine(key, kb.toLong))
case x => {
println("E: unexpected line: " + x)
None
}
}
val vsls = sls.flatMap(e=>e).toList
// Convert SmapsLine list to RegInfo list
def processSmapsLines(l: List[SmapsLine]): List[RegInfo] = l match {
case Nil => List()
case x::xs => {
require(x.isRegLine)
val attrs = xs.takeWhile(!_.isRegLine).collect{
case AttrLine(key, kb) => (key, kb)
}.toMap
RegInfo(x.asInstanceOf[RegLine], attrs) ::
processSmapsLines(xs.dropWhile(!_.isRegLine))
}
}
val ril = processSmapsLines(vsls)
ril
}
def ana1(pid: Int, smaps: String, taskStats: Map[Int,String]) {
val smsd: List[RegInfo] = parseSmapsData(smaps)
val taskSpMap = taskStats.map(parseTaskStat).toMap
for ((tid, sp) <- taskSpMap) {
print("tid: %5d sp: %#x ".format(tid, sp))
// for now, it is linear search
smsd.filter(_.contains(sp)) match {
case Nil => println("region for sp was not found")
case x::xs => println(x)
}
}
}
def doit(pid: Int) {
allCatch withApply { t => println(t) } apply {
val smapsData = Source.fromFile("/proc/" + pid + "/smaps").mkString
val taskd = new File("/proc/" + pid + "/task")
require(taskd.isDirectory)
val tids = taskd.list
val taskStatData = tids.map(stid => (stid.toInt,
Source.fromFile("/proc/" + pid + "/task/" + stid + "/stat").
mkString)).toMap
ana1(pid, smapsData, taskStatData)
}
}
def usage {
val m = """usage: scala StackUse <pid>"""
println(m)
}
def getNumbers(args: Array[String]) =
args.map(a => allCatch opt {a.toInt}).flatMap(e=>e)
val pids = getNumbers(args)
if (pids.size == 0) {
usage
} else {
for (pid <- pids) doit(pid)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment