Pseudo-terminals, and what tmux and htop needed around them

- /dev/ptmx and /dev/pts/N: Unix98 pairs with a full line discipline,
  window sizes, job control and hangup (kernel/dev/pty.c).
- The console speaks scroll regions, insert/delete line, SU/SD, RI/IND/NEL,
  DECSC/DECRC; swallows charset designations and private CSI sequences;
  and LF only returns the carriage under ONLCR.
- setsid() drops the controlling terminal, so a pane's pty becomes its ctty.
- pipe2() honours O_NONBLOCK and O_CLOEXEC (libevent's signal pipe).
- File modes are stored and enforced: chmod, fchmod(at), umask, creation
  modes; /tmp is 1777.
- /proc/stat has real user/system/idle; /proc/<pid>/task; kthread flag.
- SO_PEERCRED, IOV_MAX 1024.
- /proc/strace: name a program and its syscalls go to the kernel log.
- World gains htop, btop, tmux, mc, ncdu, fastfetch, games, X toys, NetSurf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-25 02:11:10 +01:00
commit fd9d392416
28 changed files with 1577 additions and 47 deletions

View file

@ -38,6 +38,7 @@
#include "fs/file.h"
#include "uapi/errno.h"
#include "fs/blkdev.h"
#include "dev/pty.h"
// Inode numbering. Two directories and a flat table of nodes, so the number is
// either a directory id or an index into devnodes[] offset past them.
@ -49,6 +50,12 @@
// device number, and inode DEVFS_BLK0 plus that number. Not in devnodes[],
// because they come and go: a stick plugged in appears, pulled out vanishes.
#define DEVFS_BLK0 4096ULL
// Pseudo-terminals: the /dev/pts directory, its slaves by pair number, and the
// masters, which have inodes so that an open file can hold one but appear in no
// directory -- the only way to one is opening /dev/ptmx.
#define DEVFS_PTS 3ULL
#define DEVFS_PTS0 8192ULL
#define DEVFS_PTM0 12288ULL
// Which directory a node lives in. Only two exist and a third would want a real
// tree rather than a flag, which is the point at which this file should grow
@ -110,6 +117,8 @@ static const struct devnode devnodes[] = {
// Audio, spelled the way UNIX spells it. See include/dev/audio.h.
{ "dsp", 12, 0, D_ROOT },
{ "mouse", 3, 1, D_ROOT },
// Opening it makes a new pseudo-terminal pair; see kernel/dev/pty.c.
{ "ptmx", PTMX_MAJOR, 0, D_ROOT },
// Raw disks. Created whether or not a drive is present: opening one with no
// disk behind it fails with ENXIO, which is a better answer than a missing
@ -143,9 +152,22 @@ devfs_iload(struct inode *ip)
ip->major = ip->minor = 0;
ip->size = 0;
if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT) {
if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT ||
ip->inum == DEVFS_PTS) {
ip->type = T_DIR;
ip->nlink = 2;
} else if (ip->inum >= DEVFS_PTS0 && ip->inum < DEVFS_PTS0 + NPTY &&
pty_used((int)(ip->inum - DEVFS_PTS0))) {
ip->type = T_DEV;
ip->nlink = 1;
ip->major = PTS_MAJOR;
ip->minor = (short)(ip->inum - DEVFS_PTS0);
ip->mode = 0620;
} else if (ip->inum >= DEVFS_PTM0 && ip->inum < DEVFS_PTM0 + NPTY) {
ip->type = T_DEV;
ip->nlink = 1;
ip->major = PTM_MAJOR;
ip->minor = (short)(ip->inum - DEVFS_PTM0);
} else if (i >= 0) {
ip->type = T_DEV;
ip->nlink = 1;
@ -196,6 +218,32 @@ devfs_dirlookup(struct inode *dp, char *name, uint *poff)
if (dp->inum == DEVFS_ROOT && streq(name, "input"))
return iget(DEVFS_DEV, DEVFS_INPUT);
if (dp->inum == DEVFS_ROOT && streq(name, "pts"))
return iget(DEVFS_DEV, DEVFS_PTS);
// /dev/pts/N: a pair that exists.
if (dp->inum == DEVFS_PTS) {
int n = 0;
const char *s = name;
if (!*s)
return 0;
for (; *s; s++) {
if (*s < '0' || *s > '9')
return 0;
n = n * 10 + (*s - '0');
if (n >= NPTY)
return 0;
}
return pty_used(n) ? iget(DEVFS_DEV, DEVFS_PTS0 + (uint64)n) : 0;
}
// /dev/tty is the caller's controlling terminal. When that is a pty, it is
// the slave itself -- what a program inside xterm or tmux opens to talk to
// its terminal must be that terminal, not the console.
if (dp->inum == DEVFS_ROOT && streq(name, "tty") && myproc() &&
myproc()->ctty >= PTY_CTTY && myproc()->ctty < PTY_CTTY + NPTY &&
pty_used(myproc()->ctty - PTY_CTTY))
return iget(DEVFS_DEV, DEVFS_PTS0 + (uint64)(myproc()->ctty - PTY_CTTY));
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
for (int i = 0; i < NDEVNODES; i++)
@ -258,7 +306,8 @@ devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
int used = 0;
uint64 n = *poff;
if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT)
if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT &&
dp->inum != DEVFS_PTS)
return -1;
if (n == 0) {
@ -271,15 +320,36 @@ devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
n++;
}
// /dev/input is a directory entry of the root, and it is emitted between the
// dots and the nodes so that its ordinal is stable.
// /dev/pts: the dots, then the pairs that exist. Ordinal 2 + N is pair N,
// whether or not the ones before it exist, so a pair opened between two
// calls does not shift the rest.
if (dp->inum == DEVFS_PTS) {
for (; n >= 2 && n < 2 + NPTY; n++) {
int k = (int)(n - 2);
char nm[4];
if (!pty_used(k))
continue;
nm[0] = k >= 10 ? '0' + k / 10 : '0' + k;
nm[1] = k >= 10 ? '0' + k % 10 : 0;
nm[2] = 0;
if (emit(out, cap, &used, DEVFS_PTS0 + (uint64)k, 2, nm) < 0) goto out;
}
goto out;
}
// /dev/input and /dev/pts are directory entries of the root, emitted
// between the dots and the nodes so that their ordinals are stable.
if (dp->inum == DEVFS_ROOT && n == 2) {
if (emit(out, cap, &used, DEVFS_INPUT, 4, "input") < 0) goto out;
n++;
}
if (dp->inum == DEVFS_ROOT && n == 3) {
if (emit(out, cap, &used, DEVFS_PTS, 4, "pts") < 0) goto out;
n++;
}
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
uint64 base = dp->inum == DEVFS_ROOT ? 3 : 2;
uint64 base = dp->inum == DEVFS_ROOT ? 4 : 2;
for (int i = 0; i < NDEVNODES; i++) {
if (devnodes[i].dir != want)
@ -339,3 +409,12 @@ devfs_mount(const char *target)
iput(dp);
return r < 0 ? -EBUSY : 0;
}
// The inode of pty master N, for ptmx_open() to hand to the file it opens.
struct inode *
devfs_pty_master(int n)
{
if (n < 0 || n >= NPTY)
return 0;
return iget(DEVFS_DEV, DEVFS_PTM0 + (uint64)n);
}

View file

@ -535,6 +535,8 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
if(*s == '/')
last = s+1;
safestrcpy(myproc()->name, last, sizeof(myproc()->name));
if (strace_wanted(myproc()->name))
myproc()->strace = 1;
safestrcpy(myproc()->exe, path, sizeof(myproc()->exe));
for(int i = 0; i < NOFILE; i++){

View file

@ -10,6 +10,7 @@
#include "uapi/posix.h"
#include "uapi/errno.h"
#include "core/kmem.h"
#include "dev/pty.h"
extern struct devsw devsw[];
@ -422,6 +423,10 @@ file_poll(struct file *f, int events)
return rev;
}
if(f->ip && f->ip->type == T_DEV &&
(f->ip->major == PTM_MAJOR || f->ip->major == PTS_MAJOR))
return pty_poll(f->ip, events, f->readable, f->writable);
if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){
int rev = 0;
if((events & POLLIN) && f->readable && console_input_ready(f->ip ? f->ip->minor : 1))

View file

@ -624,8 +624,10 @@ ilock(struct inode *ip)
acquiresleep(&ip->lock);
if (ip->valid == 0)
if (ip->valid == 0) {
ip->mode = 0;
ip->ops->iload(ip);
}
}
void

View file

@ -34,6 +34,7 @@
#include "uapi/stat.h"
#include "core/uaccess.h"
#include "uapi/errno.h"
#include "dev/pty.h"
// ---------------------------------------------------------------------------
// Inode numbering
@ -68,7 +69,8 @@
#define PF_EXE 5
#define PF_SCHEDSTAT 6
#define PF_FD 7 // the directory /proc/<pid>/fd
#define PF_MAX 7
#define PF_TASK 8 // the directory /proc/<pid>/task
#define PF_MAX 8
#define PF_FDENT 0x80 // | fd: one link in it
#define INO_PID(ino) ((int)((ino) >> 8))
@ -109,6 +111,9 @@ static const struct global_file globals[] = {
// Disks and partitions, in Linux's format. See blkdev_info().
{ "partitions", 21 },
{ "kmsg", 17 },
// A program's name: from its next exec on, it and everything it starts log
// every syscall. Empty to stop. See strace_call() in kernel/sys/syscall.c.
{ "strace", 31 },
};
#define NGLOBALS ((int)(sizeof(globals) / sizeof(globals[0])))
@ -132,6 +137,11 @@ static const struct global_file pidfiles[] = {
// One symlink per open descriptor, named by its number and pointing at the
// path it was opened by, or at "socket:[ino]" and the like.
{ "fd", PF_FD },
// The process's threads, one directory each, named by thread id. A process
// here is one thread, so task/ holds a single entry, the pid, and it *is*
// /proc/<pid> -- the same inode. htop reads every process through it and
// shows nothing at all without it.
{ "task", PF_TASK },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
@ -201,8 +211,16 @@ gen_pid_stat(struct sb *s, const struct procinfo *pi)
// header and nothing else: ps with no arguments shows only the processes on
// *this* terminal, and nothing could ever match. `ps -e` always worked.
sbputc(s, ' ');
sbputi(s, pi->ctty ? ((1 << 8) | (10 + pi->ctty)) : 0);
sbputs(s, " -1 0"); // 8 tpgid, 9 flags
// A pty is Linux's (136, N), the same number stat() gives /dev/pts/N.
if (pi->ctty >= PTY_CTTY)
sbputi(s, (136 << 8) | (pi->ctty - PTY_CTTY));
else
sbputi(s, pi->ctty ? ((1 << 8) | (10 + pi->ctty)) : 0);
// 8 tpgid, 9 flags. PF_KTHREAD (0x00200000) on a kernel thread is how htop
// and ps tell one from a process and hide it; without it acpi and dbgd
// listed as programs of four bytes each.
sbputs(s, " -1 ");
sbputi(s, pi->kthread ? 0x00200000 : 0);
// 10..13: minflt cminflt majflt cmajflt. There is no fault accounting, and
// a fabricated number is worse than none -- top would draw a plausible graph
@ -320,18 +338,20 @@ procfs_iload(struct inode *ip)
ip->valid = 1;
ip->nlink = 1;
ip->major = ip->minor = 0;
// Read-only, with two exceptions: /proc/sysrq is the deliberate write target
// (`echo c > /proc/sysrq` to crash on purpose), and /proc/schinit is where
// PID 1 publishes its job table. Marking everything 2 made filewrite()
// Read-only, with three exceptions: /proc/sysrq is the deliberate write
// target (`echo c > /proc/sysrq` to crash on purpose), /proc/schinit is where
// PID 1 publishes its job table, and /proc/strace takes a name to trace. Marking everything 2 made filewrite()
// refuse with "permission denied", which the USB suite caught immediately --
// three of its checks drive the disk through sysrq.
ip->rwmode = (INO_IS_GLOBAL(ino) &&
((ino & 0xFF) == 10 || (ino & 0xFF) == 16)) ? 3 : 2;
((ino & 0xFF) == 10 || (ino & 0xFF) == 16 ||
(ino & 0xFF) == 31)) ? 3 : 2;
ip->showmode = 1;
ip->atime = ip->mtime = ip->ctime = 0;
if (ino == PROCFS_ROOT ||
(!INO_IS_GLOBAL(ino) && (INO_FILE(ino) == PF_DIR || INO_FILE(ino) == PF_FD))) {
(!INO_IS_GLOBAL(ino) && (INO_FILE(ino) == PF_DIR || INO_FILE(ino) == PF_FD ||
INO_FILE(ino) == PF_TASK))) {
ip->type = T_DIR;
ip->size = 0;
ip->nlink = 2;
@ -460,6 +480,8 @@ procfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
// should be doing, and PID 1 already knows when the table changed.
if (INO_IS_GLOBAL(ip->inum) && (ip->inum & 0xFF) == 16)
return proc_write_schinit(user_src, src, n);
if (INO_IS_GLOBAL(ip->inum) && (ip->inum & 0xFF) == 31)
return proc_write_strace(user_src, src, n);
(void)off;
return -EROFS;
}
@ -495,8 +517,16 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff)
if (namecmp(name, ".") == 0)
return iget(dp->dev, dp->inum);
if (namecmp(name, "..") == 0) // /proc/<pid>/.. is /proc
return iget(dp->dev, INO_FILE(dp->inum) == PF_FD ? (dp->inum & ~0xFFULL)
: PROCFS_ROOT);
return iget(dp->dev, INO_FILE(dp->inum) == PF_FD ||
INO_FILE(dp->inum) == PF_TASK ? (dp->inum & ~0xFFULL)
: PROCFS_ROOT);
// Inside /proc/<pid>/task/: the one thread, which is the process.
if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_TASK) {
if (name_to_pid(name) != INO_PID(dp->inum))
return 0;
return iget(dp->dev, (dp->inum & ~0xFFULL) | PF_DIR);
}
// Inside /proc/<pid>/fd/: a number that is an open descriptor.
if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_FD) {
@ -631,12 +661,18 @@ procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
utoa((uint)i, name);
EMIT((dp->inum & ~0xFFULL) | PF_FDENT | i, DT_LNK_, name);
}
} else if (INO_FILE(dp->inum) == PF_TASK) {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(dp->inum & ~0xFFULL, DT_DIR_, "..");
utoa((uint)INO_PID(dp->inum), name);
EMIT((dp->inum & ~0xFFULL) | PF_DIR, DT_DIR_, name);
} else {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NPIDFILES; i++)
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id,
pidfiles[i].id == PF_FD ? DT_DIR_ : pidfiles[i].id == PF_EXE ? DT_LNK_ : DT_REG_,
pidfiles[i].id == PF_FD || pidfiles[i].id == PF_TASK ? DT_DIR_ :
pidfiles[i].id == PF_EXE ? DT_LNK_ : DT_REG_,
pidfiles[i].name);
}

View file

@ -100,6 +100,7 @@ schfs_vfs_iload(struct inode *ip)
ip->nlink = in.nlink > 0x7fffffffu ? 0x7fffffff : (int)in.nlink;
ip->size = in.size;
ip->rwmode = RW;
ip->mode = in.mode & 07777; // 0 on disk: never recorded
ip->supermode = 0;
ip->showmode = 1;
ip->atime = (uint)in.atime;
@ -134,6 +135,8 @@ schfs_vfs_istore(struct inode *ip)
in.mode = ip->type == T_DIR ? 0755 : 0644;
}
if (ip->mode)
in.mode = ip->mode & 07777;
in.type = type_from_vfs(ip->type);
in.rdev_major = (uint16_t)ip->major;
in.rdev_minor = (uint16_t)ip->minor;