smp: 64 processors, and say how many there are

NCPU was 16, under what a desktop has today. 64 costs 48 more TSS
I/O bitmaps and scheduler stacks, about 800 KB of BSS. Past 254 the
APIC would need x2APIC mode, which is a separate change.

With more than 16 there were two other ceilings. sched_getaffinity
wrote "CPU 0" and returned 0, and musl clears every byte past the
count returned -- so the mask came back empty and nproc, make -j and
sysconf(_SC_NPROCESSORS_ONLN) all said 1, on any machine. It returns
the mask of processors that started, and its size, now. And /proc
rendered every file into 2 KB, which /proc/cpuinfo ran out of at the
eighteenth processor without a word; /proc/pci, /proc/interrupts and
/proc/mounts were each one bigger machine from the same cut. There is
one 64 KB buffer for it now behind a sleeplock, since there is no
allocator for contiguous pages, and the generators that write raw
are given the real capacity instead of a constant.

Checked under QEMU with -smp 24: nproc 24, /proc/cpuinfo whole.

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-23 13:50:08 +02:00
commit c1af31f75a
5 changed files with 69 additions and 36 deletions

View file

@ -43,7 +43,34 @@
#include "blkdev.h"
#include "fat.h"
#define PROC_BUFSZ 2048
// Every generated file is rendered whole into one buffer and then sliced, so
// this is the largest any of them can be. It was 2048, which a 24-processor
// /proc/cpuinfo passed at the eighteenth CPU, and /proc/pci, /proc/mounts and
// /proc/interrupts were each one larger machine away from the same quiet cut.
//
// One buffer, not one per read: 64 KB is sixteen pages and there is no
// allocator for contiguous pages. The lock serialises /proc reads, which are
// short and not a hot path -- top reads a few files a second.
#define PROC_BUFSZ (64 * 1024)
static char genbuf[PROC_BUFSZ] __attribute__((aligned(4096)));
static struct sleeplock genlock = {
.lk = { .name = "procgen" }, .name = "procgen",
};
char *
proc_genbuf_get(int *cap)
{
acquiresleep(&genlock);
*cap = PROC_BUFSZ;
return genbuf;
}
void
proc_genbuf_put(void)
{
releasesleep(&genlock);
}
// The formatting helpers these files are built out of now live in
// include/sbuf.h, because the debugger's introspection registry needs exactly
@ -383,18 +410,18 @@ proc_gen_global(int id, char *buf, int cap)
case PROC_FILESYSTEMS: gen_filesystems(&sb); break;
// SchFS renders straight into the buffer rather than through sbuf, because
// it lives in kernel/fs/schfs and must not depend on this file's helpers.
case PROC_SCHFS: sb.len = schfs_kinfo(buf, PROC_BUFSZ); break;
case PROC_SCHFS: sb.len = schfs_kinfo(buf, cap); break;
// Same arrangement as SchFS above: pci.c renders its own lines so that it
// does not depend on this file's helpers.
case PROC_PCI: sb.len = pci_info(buf, PROC_BUFSZ); break;
case PROC_PCI: sb.len = pci_info(buf, cap); break;
// Likewise irq.c: it owns the table, so it renders it.
case PROC_INTERRUPTS: sb.len = irq_info(buf, PROC_BUFSZ); break;
case PROC_INTERRUPTS: sb.len = irq_info(buf, cap); break;
case 16: gen_schinit(&sb); break;
// And likewise net.c, which is the only file here allowed to include lwIP's
// headers -- reaching into the stack from this one would put them on every
// kernel file's include path.
case PROC_NETIF: sb.len = net_info(buf, PROC_BUFSZ); break;
case PROC_RESOLV: sb.len = net_resolv(buf, PROC_BUFSZ); break;
case PROC_NETIF: sb.len = net_info(buf, cap); break;
case PROC_RESOLV: sb.len = net_resolv(buf, cap); break;
// How this machine was booted, for the programs that start during one.
//
// schinit and the jobs it runs have the same problem the kernel does -- most
@ -412,7 +439,7 @@ proc_gen_global(int id, char *buf, int cap)
break;
case PROC_DEVICES: gen_devices(&sb); break;
// The block layer renders its own table, like PCI and SchFS above.
case PROC_PARTITIONS: sb.len = blkdev_info(buf, PROC_BUFSZ); break;
case PROC_PARTITIONS: sb.len = blkdev_info(buf, cap); break;
default: return 0;
}
return sb.len;
@ -433,22 +460,19 @@ procread(struct inode *ip, uint64 off, char *dst, int n)
// above has warned about exactly that since it was written. It stayed a
// local because a page from kalloc() for 2 KB felt worse; kmalloc is the
// answer that was missing.
char *buf = kmalloc(PROC_BUFSZ);
int len;
if (!buf)
return -ENOMEM;
len = proc_gen_global(ip->minor, buf, PROC_BUFSZ);
int cap;
char *buf = proc_genbuf_get(&cap);
int len = proc_gen_global(ip->minor, buf, cap);
if (len <= 0 || (int)off >= len) {
kmfree(buf);
proc_genbuf_put();
return 0; // EOF
}
int avail = len - (int)off;
if (n > avail)
n = avail;
memmove(dst, buf + off, n);
kmfree(buf);
proc_genbuf_put();
return n;
}

View file

@ -53,12 +53,6 @@
// readable in a debugger, which matters more than density here.
// ---------------------------------------------------------------------------
// Size of the scratch buffer every generator renders into. Must stay equal to
// PROC_BUFSZ in kernel/dev/procfs.c: the SchFS and PCI generators there write
// up to PROC_BUFSZ regardless of the cap they are handed, so a smaller value
// here would be a buffer overrun rather than a truncation.
#define PROCFS_GENBUF 2048
#define PROCFS_ROOT 1ULL
#define PROCFS_GLOBAL 0x10000ULL
@ -378,15 +372,14 @@ procfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
return r;
}
// kmalloc, not kalloc: this buffer is PROCFS_GENBUF and kalloc's unit is a
// whole 4 KB page. The comment above about a page not being wasteful was
// written when a page was the only thing there was to ask for.
if ((buf = kmalloc(PROCFS_GENBUF)) == 0)
return -ENOMEM;
len = gen_file(ip->inum, buf, PROCFS_GENBUF);
// The shared buffer in kernel/dev/procfs.c, 64 KB, held until the copy out
// is done. It was 2 KB from kmalloc, which cut /proc/cpuinfo off at the
// eighteenth processor.
int cap;
buf = proc_genbuf_get(&cap);
len = gen_file(ip->inum, buf, cap);
if (off >= (uint64)len) {
kmfree(buf);
proc_genbuf_put();
return 0;
}
if (off + n > (uint64)len)
@ -400,7 +393,7 @@ procfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
memmove((char *)dst, buf + off, n);
}
kmfree(buf);
proc_genbuf_put();
return r;
}

View file

@ -360,16 +360,27 @@ addr_t sys_setgroups(void) { return 0; }
addr_t sys_sched_yield(void) { 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 (maskp && cpusetsize > 0) {
char *m = (char*)user_ptr(maskp, (uint64)cpusetsize);
if (m) { memset(m, 0, cpusetsize); m[0] = 1; } // CPU 0 only
}
return 0;
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; }