1

A pseudoterminal has a pair of master and slave.

How can we find out the master device file from a slave device file (e.g. /etc/pts/3)? I only find /dev/ptmx and /dev/pts/ptmx, but they can't be shared by multiple slaves.

Given one of the processes working on the master and slave, how can we find out the other? For example, ps provides information about the controlling tty of each process. Can it be helpful?

Thanks.

  • @StephenKitt wields the dupehammer, so he can edit the list of duplicates if needed. There's no need to reopen this just for that. – muru Oct 9 '19 at 1:44
  • @mosvy what I meant is that a single user (Stephen Kitt here) can do what would otherwise take at least 5 users. – muru Oct 9 '19 at 5:19
  • FTR, the question this was initially flagged a duplicate of is Are all the slaves paired with the same master? (The link will disappear once this is re-opened. I don’t have a dupe hammer here now since linux was removed from the question.) – Stephen Kitt Dec 20 '19 at 15:22
  • @StephenKitt for the record: the original Q was not tagged [linux] (the tag was edited in later by a moderator), and there's no other Q&A which explain how to do this on FreeBSD, Solaris, etc. – mosvy Dec 20 '19 at 17:52
  • @mosvy ah right, it was added before I saw the question in the review queue, so my vote closed it with the gold tag badge. The comments above suggested that I could re-open the question in one go, which was true at the time but no longer is (I have however voted to re-open). I didn’t see any of the comments at the time unfortunately. – Stephen Kitt Dec 20 '19 at 18:47
7

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;
}
  • Nice... Just curious, how does gdb tend to be broken, as shipped in distros? – Stephen Kitt Jan 4 '19 at 22:57
  • Thanks. Given one of the processes working on the master and slave, how can we find out the other? – Tim Jan 6 '19 at 4:35
  • Thanks. sorry, could you tell me how to run your script? I don't know perl. – Tim Jan 6 '19 at 12:30
4

On Linux, using devpts, there is no master device file. The process on the master end uses a file descriptor, which it gets by opening ptmx, but there’s no corresponding device node.

See the ptmx manpage for details.

(With BSD-style ptys on Linux, there are matching device pairs, such as /dev/ptyp1 and /dev/ttyp1, respectively on the master and slave side.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.