SchOS/kernel/sys/sys_proc.c
Nexoniarz fd9d392416 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
2026-10-25 02:11:10 +01:00

562 lines
16 KiB
C

#include "core/types.h"
#include "core/defs.h"
#include "core/param.h"
#include "arch/memlayout.h"
#include "core/hwinfo.h"
#include "arch/mmu.h"
#include "core/proc.h"
#include "arch/x86.h"
#include "core/syscall.h"
#include "uapi/posix.h"
#include "core/uaccess.h"
#include "uapi/errno.h"
#include "dev/acpi.h"
#include "fs/fat.h"
#include "core/spinlock.h"
#include "core/sleeplock.h"
#include "fs/fs.h"
#include "fs/buf.h"
addr_t sys_fork(void) { return fork(); }
addr_t sys_wait(void) { return wait(); }
// _exit(status)/exit_group(status): the low 8 bits of the argument are what
// wait() reports back through WEXITSTATUS.
addr_t
sys_exit(void)
{
int code = 0;
argint(0, &code);
exit(W_EXITCODE(code, 0));
return 0;
}
addr_t sys_exit_group(void) { return sys_exit(); }
addr_t
sys_kill(void)
{
int pid, sig;
argint(0, &pid);
argint(1, &sig);
// POSIX target selection:
// pid > 0 -> that process
// pid == 0 -> every process in the caller's own group
// pid == -1 -> every process we may signal (approximated as our group)
// pid < -1 -> the process group with id -pid
if (pid == 0 || pid == -1)
return sigsend_pgrp(myproc()->pgid, sig) < 0 ? -ESRCH : 0;
if (pid < -1)
return sigsend_pgrp(-pid, sig) < 0 ? -ESRCH : 0;
// sig == 0: existence check only
if (sig == 0)
return proc_exists(pid) ? 0 : -ESRCH;
// Raise the signal for real: pending bit + wakeup, handler delivery on
// the target's next syscall boundary, default action if unhandled.
return sigsend(pid, sig) < 0 ? -ESRCH : 0;
}
// Defined with getrusage() further down; wait4() needs it too.
static void rusage_fill(struct rusage *ru, uint64 ut, uint64 st);
addr_t sys_getpid(void) { return myproc()->pid; }
addr_t sys_getppid(void) { return myproc()->parent ? myproc()->parent->pid : 1; }
addr_t sys_gettid(void) { return myproc()->pid; } // single-threaded: tid == pid
addr_t
sys_waitpid(void)
{
int pid, options;
addr_t statusp;
// status is optional (NULL is standard, valid usage — waitpid(pid, NULL,
// WNOHANG) — so this must NOT go through argptr(), which rejects a NULL/
// unmapped pointer outright).
if (argint(0, &pid) < 0 || argaddr(1, &statusp) < 0 || argint(2, &options) < 0)
return -1;
int status;
int wpid = waitpid(pid, &status, options, 0, 0);
if (wpid > 0 && statusp)
put_user(status, statusp);
return wpid;
}
// wait4(2) -- and its fourth argument is a `struct rusage *`.
//
// It was read off the stack and thrown away. So a caller that passed a buffer
// got it back untouched, and read whatever had been there before: zsh's
// `time` reported 16777216.01s user for every command, which is 2^24 seconds,
// which is uninitialised memory that happened to look like a number. The
// figure did not vary with the workload because it was never written.
//
// wait3() is the same syscall with pid -1, and it is the one shells actually
// call, so this is the path every `time` goes through.
addr_t
sys_wait4(void)
{
int pid, options;
addr_t statusp, rup;
uint64 ut = 0, st = 0;
argint(0, &pid);
argaddr(1, &statusp);
argint(2, &options);
argaddr(3, &rup);
int status;
int wpid = waitpid(pid, &status, options, &ut, &st);
if (wpid <= 0)
return wpid;
if (statusp)
put_user(status, statusp);
if (rup) {
struct rusage *ru = (struct rusage*)user_ptr(rup, sizeof(*ru));
if (ru)
rusage_fill(ru, ut, st);
}
return wpid;
}
addr_t
sys_clone(void)
{
int flags;
argint(0, &flags);
if (flags & (CLONE_VM | CLONE_THREAD | CLONE_SIGHAND))
return -ENOSYS;
return fork();
}
addr_t
sys_sleep(void)
{
int n;
uint ticks0;
if (argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while (ticks - ticks0 < (uint)n) {
if (myproc()->killed) { release(&tickslock); return -1; }
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
addr_t
sys_uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
addr_t sys_reboot(void)
{
cprintf("System is rebooting...\n");
// A stick left mounted should come back readable, and marked clean.
schfs_ksync_all();
fat_ksync_all(1);
// The FADT's reset register first: it is the one the board's designer
// wired up. Then the PCI reset port and the keyboard controller, which
// are what nearly every PC has had since there were PCs.
acpi_reboot();
outb(0xCF9, 0x06);
outb(0x64, 0xFE);
return 0;
}
addr_t sys_shutdown(void)
{
cprintf("System is shutting down...\n");
// S5, through the firmware's own PM1 registers and its _PTS. The port
// writes after it are QEMU's, Bochs' and VirtualBox's fixed addresses,
// kept for a machine whose AML did not load; a real board answers none
// of them.
schfs_ksync_all();
fat_ksync_all(1);
acpi_poweroff();
outw(0x604, 0x2000);
outw(0xB004, 0x2000);
outw(0x4004, 0x3400);
return 0;
}
// --- Sessions and process groups ---------------------------------------
// Job control is defined entirely in terms of these. They used to be stubs
// returning the caller's own pid, which made every process look like its own
// group leader and left a shell's wait loop unable to make progress.
addr_t sys_setsid(void) { return proc_setsid(); }
addr_t
sys_getsid(void)
{
int pid;
argint(0, &pid);
if (pid == 0 || pid == myproc()->pid)
return myproc()->sid;
return proc_exists(pid) ? (addr_t)myproc()->sid : (addr_t)-ESRCH;
}
addr_t
sys_getpgid(void)
{
int pid;
argint(0, &pid);
if (pid == 0 || pid == myproc()->pid)
return myproc()->pgid;
int g = proc_pgid(pid);
return g < 0 ? (addr_t)-ESRCH : (addr_t)g;
}
addr_t
sys_setpgid(void)
{
int pid, pgid;
argint(0, &pid);
argint(1, &pgid);
return proc_setpgid(pid, pgid);
}
addr_t sys_getpgrp(void) { return myproc()->pgid; }
// --- User and group identity -------------------------------------------
// Tracked for real now. Nothing *enforces* permissions with these yet -- the
// on-disk format has no owner field until SchFS -- but they are no longer
// invented, so getuid() after setuid() reports what actually happened.
addr_t sys_getuid(void) { return myproc()->uid; }
addr_t sys_geteuid(void) { return myproc()->euid; }
addr_t sys_getgid(void) { return myproc()->gid; }
addr_t sys_getegid(void) { return myproc()->egid; }
// Privileged (euid 0) callers may set any id; an unprivileged caller may only
// switch between its real and saved ids. Standard POSIX rule.
addr_t
sys_setuid(void)
{
int uid;
argint(0, &uid);
if (uid < 0)
return -EINVAL;
if (myproc()->euid == 0) {
myproc()->uid = myproc()->euid = myproc()->suid = (uint)uid;
return 0;
}
if ((uint)uid != myproc()->uid && (uint)uid != myproc()->suid)
return -EPERM;
myproc()->euid = (uint)uid;
return 0;
}
addr_t
sys_setgid(void)
{
int gid;
argint(0, &gid);
if (gid < 0)
return -EINVAL;
if (myproc()->euid == 0) {
myproc()->gid = myproc()->egid = myproc()->sgid = (uint)gid;
return 0;
}
if ((uint)gid != myproc()->gid && (uint)gid != myproc()->sgid)
return -EPERM;
myproc()->egid = (uint)gid;
return 0;
}
addr_t sys_umask(void)
{
int mask;
argint(0, &mask);
uint old = myproc()->umask;
myproc()->umask = (uint)mask & 0777;
return old;
}
addr_t
sys_setreuid(void)
{
int ruid, euid;
argint(0, &ruid); argint(1, &euid);
if (myproc()->euid != 0)
return -EPERM;
if (ruid >= 0) myproc()->uid = (uint)ruid;
if (euid >= 0) myproc()->euid = (uint)euid;
myproc()->suid = myproc()->euid;
return 0;
}
addr_t
sys_setregid(void)
{
int rgid, egid;
argint(0, &rgid); argint(1, &egid);
if (myproc()->euid != 0)
return -EPERM;
if (rgid >= 0) myproc()->gid = (uint)rgid;
if (egid >= 0) myproc()->egid = (uint)egid;
myproc()->sgid = myproc()->egid;
return 0;
}
addr_t
sys_setresuid(void)
{
int r, e, sv;
argint(0, &r); argint(1, &e); argint(2, &sv);
if (myproc()->euid != 0)
return -EPERM;
if (r >= 0) myproc()->uid = (uint)r;
if (e >= 0) myproc()->euid = (uint)e;
if (sv >= 0) myproc()->suid = (uint)sv;
return 0;
}
addr_t
sys_setresgid(void)
{
int r, e, sv;
argint(0, &r); argint(1, &e); argint(2, &sv);
if (myproc()->euid != 0)
return -EPERM;
if (r >= 0) myproc()->gid = (uint)r;
if (e >= 0) myproc()->egid = (uint)e;
if (sv >= 0) myproc()->sgid = (uint)sv;
return 0;
}
// Linux-specific; the fs id follows the effective id here.
addr_t sys_setfsuid(void) { return myproc()->euid; }
addr_t sys_setfsgid(void) { return myproc()->egid; }
addr_t
sys_getresuid(void)
{
addr_t rp, ep, sp;
argaddr(0, &rp); argaddr(1, &ep); argaddr(2, &sp);
if (rp) put_user(myproc()->uid, rp);
if (ep) put_user(myproc()->euid, ep);
if (sp) put_user(myproc()->suid, sp);
return 0;
}
addr_t
sys_getresgid(void)
{
addr_t rp, ep, sp;
argaddr(0, &rp); argaddr(1, &ep); argaddr(2, &sp);
if (rp) put_user(myproc()->gid, rp);
if (ep) put_user(myproc()->egid, ep);
if (sp) put_user(myproc()->sgid, sp);
return 0;
}
addr_t sys_getgroups(void)
{
int size;
argint(0, &size);
return 0; // no supplementary groups
}
addr_t sys_setgroups(void) { return 0; }
// --- Process/scheduling extensions ---
// A real yield. It returned 0 without giving anything up, which cost nothing
// while every timer tick was a yield anyway; with the fair scheduler a tick
// only preempts once the slice is spent, so a spin-and-yield loop (musl's
// locks, a polling driver in userland) would hold the CPU for all of it.
addr_t sys_sched_yield(void) { yield(); return 0; }
// Every processor that started, since nothing here pins a process anywhere.
//
// This wrote "CPU 0" and returned 0 -- and the return value is the number of
// bytes of mask the kernel wrote, which musl uses to clear the rest. Zero
// meant all of it, so the mask came back empty and nproc, make -j and every
// thread pool sized from sysconf(_SC_NPROCESSORS_ONLN) saw one processor.
addr_t sys_sched_getaffinity(void)
{
int pid, cpusetsize;
addr_t maskp;
uchar m[(NCPU + 63) / 64 * 8];
argint(0, &pid); argint(1, &cpusetsize); argaddr(2, &maskp);
if (cpusetsize < (int)sizeof m || (cpusetsize & 7))
return -EINVAL;
memset(m, 0, sizeof m);
for (int i = 0; i < ncpu && i < NCPU; i++)
if (cpus[i].started)
m[i / 8] |= 1 << (i % 8);
if (copy_to_user(maskp, m, sizeof m) < 0)
return -EFAULT;
return sizeof m;
}
addr_t sys_sched_setaffinity(void) { return 0; }
addr_t sys_prctl(void)
{
int op;
addr_t arg2;
argint(0, &op);
argaddr(1, &arg2);
if (op == PR_SET_NAME) {
char *name;
if (argstr(1, &name) < 0) return -1;
safestrcpy(myproc()->name, name, sizeof(myproc()->name));
return 0;
}
if (op == PR_GET_NAME) {
char *buf = (char*)user_ptr(arg2, 16);
if (!buf) return -EFAULT;
safestrcpy(buf, myproc()->name, 16);
return 0;
}
return 0; // all other prctl ops: silently succeed
}
addr_t sys_tgkill(void)
{
int tgid, tid, sig;
argint(0, &tgid); argint(1, &tid); argint(2, &sig);
// In our single-threaded model tgid == tid == pid.
if (tgid <= 0) return 0;
if (sig == 0) return proc_exists(tgid) ? 0 : -ESRCH;
return sigsend(tgid, sig) < 0 ? -ESRCH : 0;
}
addr_t sys_tkill(void)
{
int tid, sig;
argint(0, &tid); argint(1, &sig);
if (tid <= 0) return 0;
if (sig == 0) return proc_exists(tid) ? 0 : -ESRCH;
return sigsend(tid, sig) < 0 ? -ESRCH : 0;
}
addr_t sys_pause(void)
{
// Sleep until next timer tick and return -EINTR (no real signal delivery).
acquire(&tickslock);
uint t0 = ticks;
while (ticks == t0 && !myproc()->killed)
sleep(&ticks, &tickslock);
release(&tickslock);
return -EINTR;
}
// getrusage(2).
//
// This used to ignore `who` entirely and report the machine's *uptime* as the
// caller's user time. Every process of every age answered with the same
// number, and RUSAGE_CHILDREN answered with it too -- so a shell timing a
// command got the age of the machine, and `time` on a pipeline was
// meaningless rather than merely imprecise.
//
// Ticks, converted at the boundary. The kernel counts whole timer ticks
// because that is what the sampling gives it; microseconds are what the
// interface asks for, so the multiply happens here rather than pretending to a
// resolution the counter does not have.
static void
rusage_fill(struct rusage *ru, uint64 ut, uint64 st)
{
memset(ru, 0, sizeof(*ru));
ru->ru_utime.tv_sec = (long)(ut / HZ);
ru->ru_utime.tv_usec = (long)((ut % HZ) * (1000000 / HZ));
ru->ru_stime.tv_sec = (long)(st / HZ);
ru->ru_stime.tv_usec = (long)((st % HZ) * (1000000 / HZ));
}
addr_t sys_getrusage(void)
{
int who;
struct rusage *ru;
argint(0, &who);
if (argptr(1, (char**)&ru, sizeof(*ru)) < 0) return -EFAULT;
switch (who) {
case RUSAGE_SELF:
rusage_fill(ru, myproc()->utime, myproc()->stime);
return 0;
case RUSAGE_CHILDREN:
rusage_fill(ru, myproc()->cutime, myproc()->cstime);
return 0;
default:
// RUSAGE_THREAD is -1 and would be RUSAGE_SELF here, since a process
// has exactly one thread. It is refused rather than aliased: answering
// it would imply threads exist.
return -EINVAL;
}
}
addr_t sys_times(void)
{
addr_t bufaddr;
argaddr(0, &bufaddr);
// The tms fields are in clock ticks, which is what the kernel counts, so
// this one needs no conversion -- USER_HZ and HZ are both 100. They used
// to be memset to zero, which is why `times()` agreed with nothing.
if (bufaddr) {
struct tms *t = (struct tms*)user_ptr(bufaddr, sizeof(*t));
if (t) {
t->tms_utime = (long)myproc()->utime;
t->tms_stime = (long)myproc()->stime;
t->tms_cutime = (long)myproc()->cutime;
t->tms_cstime = (long)myproc()->cstime;
}
}
// USER_HZ units, measured from the TSC.
//
// musl answers sysconf(_SC_CLK_TCK) with 100 unconditionally, so a shell's
// `time` builtin divides whatever this returns by 100. Deriving it from
// the tick count made `time` under-report whenever ticks were being lost
// -- which is how a 20-second command came to print 3.12s.
uint64 tsec, tusec;
monotonic(&tsec, &tusec);
return (addr_t)(tsec * USER_HZ + (tusec * USER_HZ) / 1000000);
}
addr_t sys_sysinfo(void)
{
addr_t bufaddr;
argaddr(0, &bufaddr);
struct sysinfo *si = (struct sysinfo*)user_ptr(bufaddr, sizeof(*si));
if (!si) return -EFAULT;
memset(si, 0, sizeof(*si));
// TSC, like everything else that reports elapsed time.
uint64 upsec, upusec;
monotonic(&upsec, &upusec);
si->uptime = (long)upsec;
// Real values: the RAM the page allocator was given, and how much of it is
// still on the free list. These used to be the constants 256MB/128MB, and
// then PHYSTOP, an address, which counted the PCI hole as memory.
si->totalram = ram_total_bytes();
si->freeram = kalloc_free_bytes();
// The buffer cache, so free(1) does not count it as used by nothing.
uint nbuf, maxbuf;
bcache_size(&nbuf, &maxbuf);
si->bufferram = (uint64)nbuf * BSIZE;
si->mem_unit = 1;
si->procs = (uint16)proc_num();
return 0;
}
addr_t sys_waitid(void)
{
// Simplified: wait for any child, ignore idtype/id/options.
int wpid = wait();
if (wpid < 0) return -ECHILD;
// siginfo_t pointer (arg 2) — zero-fill if provided.
addr_t infop;
argaddr(2, &infop);
if (infop) {
char *p = (char*)user_ptr(infop, 128); // sizeof(siginfo_t)
if (p) memset(p, 0, 128);
}
return 0;
}