That is one thing that is harder than it should be.
With newer linux kernels, the index of the slave pty paired with a master can be gathered from the tty-index
entry from /proc/PID/fdinfo/FD
. See this commit.
With older kernels, the only way you can get that is by attaching with a debugger to a process holding a master pty, and call ptsname(3)
(or directly ioctl(TIOCGPTN)
) on the file descriptor.
[but both methods will run into issues on systems using multiple devpts mounts, see below]
With that info, you can build a list of master-slave pairings, which will also allow you to look up the master starting up from the slave.
Here is a silly script that should do that; it will first try the tty-index
way, and if that doesn't work it will fall back to gdb
. For the latter case, it needs a working gdb
(not gdb-minimal
or another half-broken gdb
most distros ship with) and because of its use of gdb
, it will be very slow.
For each pty pair, it will print something like:
/dev/pts/1
1227 3 t/ct_test
1228 +* t/ct_test
1230 + t/ct_test
/dev/pts/3
975 9 'sshd: root [priv]' '' '' '' '' '' '' '' ''
978 14,18,19 'sshd: root@pts/3' '' '' '' '' '' '' ''
979 -*0,1,2,255 -bash
1222 1 tiocsti
1393 -0,1,2 sleep 3600
1231 +0,2 perl ptys.pl
1232 +1,2 cut -b1-60
the two sshd
processes (pids 975 and 978) have open handles to the master side (one as its 9 fd and the other as its 14, 18 and 19 fds). sleep
and -bash
have open handles to the slave side as their standard (0,1 and 2) fds. The session leader (bash
) is also marked with a *
, the processes in the foreground (perl
and cut
) with a +
, and those in the background (less
and -bash
) with a -
.
The t/ct_test
processes are using the pty as their controlling terminal without having any fd open to it. tiocsti
has an open handle to it without it being its controlling terminal.
Tested on Debian 9 and Fedora 28. Info about the magic numbers it's using can be found in procfs(5)
and Documentation/admin-guide/devices.txt
in the linux kernel source.
This will fail on any system using chroots or namespace containers; that's not fixable without some changes to the kernel, since there's no reliable way to match the tty
field from /proc/PID/stat
to a pty, and a fd opened via /dev/ptmx
to the corresponding /dev/pts
mount. See here for a rant about it.
This will also not link in any fd opened via /dev/tty
; the real tty could be worked out by attaching to the process and calling ioctl(fd, TIOCGDEV, &dev)
, but that will mean another dirty heavy use of gdb, and it will run into the same issues as above with the major, minor numbers being ambiguous for pseudo-tty slaves.
ptys.pl:
my (%pty, %ctty);
for(</proc/*[0-9]*/{fd/*,stat}>){
if(my ($pid, $fd) = m{/proc/(\d+)/fd/(\d+)}){
next unless -c $_;
my $rdev = (stat)[6]; my $maj = $rdev >> 8 & 0xfff;
if($rdev == 0x502){ # /dev/ptmx or /dev/pts/ptmx
$pty{ptsname($pid, $fd, readlink $_)}{m}{$pid}{$fd} = 1;
}elsif($maj >= 136 && $maj <= 143){ # /dev/pts/N
$pty{readlink $_}{s}{$pid}{$fd} = 1;
}
}else{
my @s = readfile($_) =~ /(?<=\().*(?=\))|[^\s()]+/gs;
$ctty{$s[6]}{$s[0]} = # ctty{tty}{pid} =
($s[4] == $s[7] ? '+' : '-'). # pgrp == tpgid
($s[0] == $s[5] ? '*' : ''); # pid == sid
}
}
for(sort {length($a)<=>length($b) or $a cmp $b} keys %pty){
print "$_\n";
pproc(4, $pty{$_}{m}); pproc(8, $pty{$_}{s}, $ctty{(stat)[6]});
}
sub readfile { local $/; my $h; open $h, '<', shift and <$h> }
sub cmdline {
join ' ', map { s/'/'\\''/g, $_ = "'$_'" if m{^$|[^\w./+=-]}; $_ }
readfile("/proc/$_[0]/cmdline") =~ /([^\0]*)\0/g;
}
sub pproc {
my ($px, $h, $sinfo) = @_;
exists $$h{$_} or $$h{$_} = {''} for keys %$sinfo;
return printf "%*s???\n", $px, "" unless $h;
for my $pid (sort {$a<=>$b} keys %$h){
printf "%*s%-5d %s%-3s %s\n", $px, "", $pid, $$sinfo{$pid},
join(',', sort {$a<=>$b} keys %{$$h{$pid}}),
cmdline $pid;
}
}
sub ptsname {
my ($pid, $fd, $ptmx) = @_;
return '???' unless defined(my $ptn = getptn($pid, $fd));
$ptmx =~ m{(.*)(?:/pts)?/ptmx$} ? "$1/pts/$ptn" : "$ptmx ..?? pts/$ptn"
}
sub getptn {
my ($pid, $fd) = @_;
return $1 if
readfile("/proc/$pid/fdinfo/$fd") =~ /^tty-index:\s*(\d+)$/m;
return gdb_ioctl($pid, $fd, 0x80045430); # TIOCGPTN
}
sub gdb_ioctl {
my ($pid, $fd, $ioctl) = @_;
my $cmd = qq{p (int)ioctl($fd, $ioctl, &errno) ? -1 : errno};
qx{exec 3>&1; gdb -batch -p $pid -ex '$cmd' 2>&1 >&3 |
grep -v '/sysdeps/.*No such file or directory' >&2}
=~ /^\$1 *= *(\d+)$/m ? $1 : undef;
}