A thread is a struct proc that shares three counted objects with others of
its group: the address space (struct mm: page tables, heap top, VMAs and a
lock for changing them), the descriptor table, and the signal dispositions.
clone() shares what its flags say and fork() copies.

- clone(): pthread_create's shape (VM, FILES, SIGHAND, THREAD, SETTLS,
  PARENT_SETTID, CHILD_CLEARTID) and posix_spawn's (VM, VFORK, a stack).
- futex: WAIT, WAIT_BITSET with timeouts, WAKE, REQUEUE, CMP_REQUEUE, keyed
  by the word's kernel address so shared pages meet. set_tid_address keeps
  the word that exit zeroes and wakes, for pthread_join.
- exit ends a thread; exit_group, a fatal signal, or the leader exiting ends
  them all. reapd frees exited threads; the leader is reaped by its parent
  once the rest are gone. getpid is the group, gettid the thread.
- Stops and resumes reach every thread; other process signals the leader.
- exec kills the other threads and unshares the dispositions.
- No TLB shootdown, so pages unmapped from a shared address space are freed
  only once every CPU has reloaded CR3 or is idle.
- procfs: /proc lists processes, task/ lists threads, comm, Threads:, Tgid.
- The console honours VMIN 0 / VTIME (btop's input drain hung on it).

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 03:03:50 +01:00
commit f4d7f9100d
19 changed files with 1300 additions and 248 deletions

View file

@ -369,7 +369,8 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
uint64 sz, sp;
struct elfhdr elf;
struct inode *ip;
pml4e_t *pgdir, *oldpgdir;
pml4e_t *pgdir;
struct mm *oldmm, *newmm;
// The arguments are measured before anything is loaded, so that too many
// of them is E2BIG with nothing to undo -- and so the stack can be sized to
@ -540,10 +541,10 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
safestrcpy(myproc()->exe, path, sizeof(myproc()->exe));
for(int i = 0; i < NOFILE; i++){
if(myproc()->ofile[i] && (myproc()->ofile_flags[i] & FD_CLOEXEC)){
fileclose(myproc()->ofile[i]);
myproc()->ofile[i] = 0;
myproc()->ofile_flags[i] = 0;
if(myproc()->files->ofile[i] && (myproc()->files->flags[i] & FD_CLOEXEC)){
fileclose(myproc()->files->ofile[i]);
myproc()->files->ofile[i] = 0;
myproc()->files->flags[i] = 0;
}
}
@ -557,18 +558,19 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
myproc()->map_size = 0;
}
// Detach shared segments before anything forgets which mappings they were.
//
// This has to happen while myproc()->pgdir and myproc()->vma[] still describe the
// *old* image: the memset below erases the bookkeeping, and freevm() at the
// end would then free a shared memory segment's pages as though they were
// ordinary heap. Doing it here rather than there is the difference between
// detaching a segment and corrupting whoever else has it mapped.
proc_release_shared(myproc());
oldpgdir = myproc()->pgdir;
// A new image is a new address space: a fresh mm, and the old one dropped
// at the end. mm_put() detaches shared segments before freeing anything,
// and does nothing at all while a vfork parent is still using it.
if ((newmm = mm_alloc()) == 0)
goto bad;
proc_exec_threads();
newmm->pgdir = pgdir;
newmm->sz = sz;
oldmm = myproc()->mm;
myproc()->mm = newmm;
myproc()->pgdir = pgdir;
myproc()->sz = sz;
pgdir = 0; // newmm owns it now; not bad:'s to free
// New address space: reset all per-process mmap/signal/TLS state.
// sig_handler/sig_restorer point into the *old* image — POSIX resets
@ -579,26 +581,25 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
// Ignored signals stay ignored, and the mask is inherited (execve(2)):
// neither points into the old image. `nohup` and every program that ignores
// SIGPIPE before running a helper depend on it.
memset(myproc()->vma, 0, sizeof(myproc()->vma));
for (int i = 0; i < 32; i++)
if (myproc()->sig_handler[i] != 1)
myproc()->sig_handler[i] = 0;
memset(myproc()->sig_restorer, 0, sizeof(myproc()->sig_restorer));
if (myproc()->sighand->handler[i] != 1)
myproc()->sighand->handler[i] = 0;
memset(myproc()->sighand->restorer, 0, sizeof(myproc()->sighand->restorer));
myproc()->fs_base = 0;
myproc()->pending_signals = 0;
myproc()->in_signal = 0;
myproc()->sigsusp_active = 0;
myproc()->mmap_next = 0;
myproc()->mm->mmap_next = 0;
// The interpreter is ordinary memory to everything after this: fork copies
// it, exit frees it. mmap carries on above it.
if (interp[0]) {
myproc()->vma[0].addr = INTERP_BASE;
myproc()->vma[0].length = PGROUNDUP(interp_end);
myproc()->vma[0].used = 1;
myproc()->vma[0].no_free = 0;
myproc()->vma[0].shmid = -1;
myproc()->mmap_next = INTERP_BASE + PGROUNDUP(interp_end);
myproc()->mm->vma[0].addr = INTERP_BASE;
myproc()->mm->vma[0].length = PGROUNDUP(interp_end);
myproc()->mm->vma[0].used = 1;
myproc()->mm->vma[0].no_free = 0;
myproc()->mm->vma[0].shmid = -1;
myproc()->mm->mmap_next = INTERP_BASE + PGROUNDUP(interp_end);
}
myproc()->tf->rax = 0;
@ -637,7 +638,8 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
// old one's registers.
memmove(myproc()->fpu, fpu_default, sizeof(myproc()->fpu));
fxrstor(myproc()->fpu);
freevm(oldpgdir);
mm_put(oldmm);
proc_exec_done(); // a vfork parent may have its memory back
kfree((char*)scratch);
kfree(bounce);
fsstat.exec_cycles += rdtsc() - _t0;

View file

@ -70,7 +70,8 @@
#define PF_SCHEDSTAT 6
#define PF_FD 7 // the directory /proc/<pid>/fd
#define PF_TASK 8 // the directory /proc/<pid>/task
#define PF_MAX 8
#define PF_COMM 9
#define PF_MAX 9
#define PF_FDENT 0x80 // | fd: one link in it
#define INO_PID(ino) ((int)((ino) >> 8))
@ -137,11 +138,13 @@ 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
// The process's threads, one directory each, named by thread id; each is
// /proc/<tid> -- the same inode. htop reads every process through it and
// shows nothing at all without it.
{ "task", PF_TASK },
// The name alone, a line of it. btop reads this and nothing else for the
// Program column.
{ "comm", PF_COMM },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
@ -236,7 +239,9 @@ gen_pid_stat(struct sb *s, const struct procinfo *pi)
sbputc(s, ' '); sbputi(s, (int64)pi->cutime);
sbputc(s, ' '); sbputi(s, (int64)pi->cstime);
sbputs(s, " 20 0 1 0 0 "); // priority nice num_threads itrealvalue starttime
sbputs(s, " 20 0 "); // priority nice
sbputi(s, pi->nthreads); // 20 num_threads
sbputs(s, " 0 0 "); // itrealvalue starttime
sbputi(s, (int64)pi->sz); // 23 vsize, in bytes
sbputc(s, ' ');
sbputi(s, (int64)(pi->sz / PGSIZE)); // 24 rss, in pages
@ -248,7 +253,7 @@ gen_pid_status(struct sb *s, const struct procinfo *pi)
{
sbputs(s, "Name:\t"); sbputs(s, pi->name); sbputc(s, '\n');
sbputs(s, "State:\t"); sbputc(s, pi->stopped ? 'T' : state_char(pi->state));
sbputs(s, "\nTgid:\t"); sbputi(s, pi->pid);
sbputs(s, "\nTgid:\t"); sbputi(s, pi->tgid);
sbputs(s, "\nPid:\t"); sbputi(s, pi->pid);
sbputs(s, "\nPPid:\t"); sbputi(s, pi->ppid);
sbputs(s, "\nUid:\t"); sbputi(s, pi->uid);
@ -256,7 +261,7 @@ gen_pid_status(struct sb *s, const struct procinfo *pi)
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputs(s, "\nVmSize:\t"); sbputi(s, (int64)(pi->sz / 1024));
sbputs(s, " kB\nThreads:\t1\n");
sbputs(s, " kB\nThreads:\t"); sbputi(s, pi->nthreads); sbputc(s, '\n');
// Where Linux keeps them, under the names Linux uses. The two are worth
// telling apart: a process with a high voluntary count is waiting on
@ -315,6 +320,7 @@ gen_file(uint64 ino, char *buf, int cap)
case PF_CMDLINE: gen_pid_cmdline(&s, &pi); break;
case PF_STATM: gen_pid_statm(&s, &pi); break;
case PF_EXE: sbputs(&s, pi.exe[0] ? pi.exe : pi.name); break;
case PF_COMM: sbputs(&s, pi.name); sbputc(&s, '\n'); break;
case PF_SCHEDSTAT:
sbputi(&s, (int64)pi.vruntime); sbputc(&s, ' ');
sbputi(&s, (int64)pi.nice); sbputc(&s, ' ');
@ -521,11 +527,13 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff)
INO_FILE(dp->inum) == PF_TASK ? (dp->inum & ~0xFFULL)
: PROCFS_ROOT);
// Inside /proc/<pid>/task/: the one thread, which is the process.
// Inside /proc/<pid>/task/: a thread of this process, which is /proc/<tid>.
if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_TASK) {
if (name_to_pid(name) != INO_PID(dp->inum))
int tid = name_to_pid(name);
if (tid <= 0 || proc_snapshot_pid(tid, &pi) < 0 ||
pi.tgid != (int)INO_PID(dp->inum))
return 0;
return iget(dp->dev, (dp->inum & ~0xFFULL) | PF_DIR);
return iget(dp->dev, ((uint64)tid << 8) | PF_DIR);
}
// Inside /proc/<pid>/fd/: a number that is an open descriptor.
@ -647,7 +655,11 @@ procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
EMIT(PROCFS_SELF, DT_LNK_, "self");
for (i = 0; i < NGLOBALS; i++)
EMIT(PROCFS_GLOBAL | globals[i].id, DT_REG_, globals[i].name);
// Processes only. A thread is reachable as /proc/<tid>, as on Linux,
// but listed under its process's task/ rather than here.
for (i = 0; proc_snapshot(i, &pi) == 0; i++) {
if (pi.tgid != pi.pid)
continue;
utoa((uint)pi.pid, name);
EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name);
}
@ -664,8 +676,12 @@ procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
} 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);
for (i = 0; proc_snapshot(i, &pi) == 0; i++) {
if (pi.tgid != (int)INO_PID(dp->inum))
continue;
utoa((uint)pi.pid, name);
EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name);
}
} else {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");