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

@ -636,6 +636,11 @@ oom:
return 0;
}
// CR3 loads per processor, and what each has loaded; see "Pages another
// processor may still have in its TLB" below.
static volatile uint64 cr3_loads[NCPU];
static pml4e_t *volatile cpu_pgdir[NCPU];
void
switchuvm(struct proc *p)
{
@ -650,6 +655,8 @@ switchuvm(struct proc *p)
// every process that is not hosting a driver.
ioperm_switch(p);
cr3_loads[cpunum()]++;
cpu_pgdir[cpunum()] = p->pgdir;
lcr3(v2p(p->pgdir));
popcli();
}
@ -695,9 +702,109 @@ walkpgdir(pml4e_t *pml4, const void *va, int alloc)
return (pte_t*)&pgtab[PTX(va)];
}
// --- Pages another processor may still have in its TLB -----------------
//
// Threads put one address space on several processors at once, and there is
// no shootdown IPI: unmapping a page here invalidates it in this TLB only.
// Freed at once, it could be handed to someone else while a thread on another
// CPU still reaches it through a stale entry -- a write into a stranger's
// memory with no fault to show for it.
//
// So a page unmapped from a shared address space waits. Every CR3 load is
// counted per processor, and a batch of pages is freed once every processor
// has reloaded CR3 since the batch was closed, or is not running a user
// address space at all. A processor reloads at every switch, so a batch waits
// a scheduling slice or two.
#define DEFER_MAX 2048
struct defer_batch {
int n;
uint64 snap[NCPU];
uint64 pa[DEFER_MAX];
};
static struct defer_batch defer_q[2];
static int defer_open; // which of the two is filling
static int defer_closed_valid; // the other one is waiting to be freed
static struct spinlock deferlock;
static int defer_init;
static int
defer_ready(struct defer_batch *b)
{
for (int c = 0; c < ncpu; c++)
if (cr3_loads[c] == b->snap[c] && cpu_pgdir[c] != 0)
return 0;
return 1;
}
// With deferlock. Frees the closed batch if it can, then closes the open one.
static void
defer_reclaim(void)
{
struct defer_batch *closed = &defer_q[defer_open ^ 1];
if (defer_closed_valid && defer_ready(closed)) {
for (int i = 0; i < closed->n; i++)
kfree((char*)P2V(closed->pa[i]));
closed->n = 0;
defer_closed_valid = 0;
}
struct defer_batch *open = &defer_q[defer_open];
if (!defer_closed_valid && open->n > 0) {
for (int c = 0; c < ncpu; c++)
open->snap[c] = cr3_loads[c];
defer_closed_valid = 1;
defer_open ^= 1;
}
}
void
vm_defer_tick(void)
{
if (!defer_init)
return;
acquire(&deferlock);
defer_reclaim();
release(&deferlock);
}
static void
defer_free(uint64 pa)
{
if (!defer_init) {
initlock(&deferlock, "vmdefer");
defer_init = 1;
}
for (;;) {
acquire(&deferlock);
struct defer_batch *open = &defer_q[defer_open];
if (open->n == DEFER_MAX)
defer_reclaim();
open = &defer_q[defer_open];
if (open->n < DEFER_MAX) {
open->pa[open->n++] = pa;
release(&deferlock);
return;
}
release(&deferlock);
yield(); // both batches full: let the CPUs switch
}
}
// Free a user page just unmapped from pgdir: now, or once no TLB can hold it.
static void
user_page_free(pml4e_t *pgdir, uint64 pa)
{
struct proc *p = myproc();
if (p && p->pgdir == pgdir && mm_shared(p->mm))
defer_free(pa);
else
kfree((char*)P2V(pa));
}
void
switchkvm(void)
{
cr3_loads[cpunum()]++;
cpu_pgdir[cpunum()] = 0;
lcr3(v2p(kpml4));
}
@ -733,9 +840,9 @@ vm_unmap_range(pml4e_t *pgdir, uint64 va, uint64 length)
pte_t *pte = walkpgdir(pgdir, (void*)a, 0);
if (pte && (*pte & PTE_P)) {
uint64 pa = PTE_ADDR(*pte);
kfree((char*)P2V(pa));
*pte = 0;
invlpg((void*)a);
user_page_free(pgdir, pa);
}
}
}
@ -883,11 +990,10 @@ deallocuvm(pml4e_t *pgdir, uint64 oldsz, uint64 newsz)
pa = PTE_ADDR(*pte);
if(pa == 0)
panic("kfree");
if(pa < PHYSTOP) {
char *v = P2V(pa);
kfree(v);
}
*pte = 0;
invlpg((void*)a);
if(pa < PHYSTOP)
user_page_free(pgdir, pa);
}
}
return newsz;