Last active
October 7, 2015 03:18
-
-
Save ytoshima/3096900 to your computer and use it in GitHub Desktop.
Print process' stack use on modern Linux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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