feat(resource): rlimit/rusage/priority

This commit is contained in:
2026-09-05 22:55:20 -04:00
parent c4156efeff
commit 3ab43c7f10
8 changed files with 668 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
#ifndef VLIBC_SYS_RESOURCE_H
#define VLIBC_SYS_RESOURCE_H
/*
* vlibc — <sys/resource.h>.
*
* Per-process resource limits and usage accounting. Nothing in this header
* is POSIX.1-2008 base — getrlimit/setrlimit/getrusage/getpriority/
* setpriority and nice are all XSI [CX] — so the entire surface is gated at
* level 2:
*
* Level 2 (muslmimic): getrlimit, setrlimit, getrusage, getpriority,
* setpriority, nice, getrlimit64, setrlimit64,
* struct rlimit, struct rusage, rlim_t, RLIMIT_*,
* RLIM_INFINITY, PRIO_*, RUSAGE_*.
*
* struct rusage matches the kernel layout on x86_64 verbatim: two struct
* timevals (16 bytes each — the kernel record uses the same two-long
* __kernel_old_timeval) followed by fourteen longs, and the kernel
* getrusage copies it out without translation. struct rlimit is likewise
* the kernel's struct rlimit64 (two 64-bit words), which is why
* getrlimit/setrlimit pass the record straight to SYS_prlimit64 and why
* getrlimit64/setrlimit64 are name-only aliases on this architecture.
* Both shapes are pinned by the static asserts below.
*
* Every function here performs kernel I/O with side effects and reports
* failures through errno, so no declaration carries an intent attribute
* (const/pure would be unsound).
*/
#include <vlibc/features.h>
#if VLIBC_LEVEL_GE(2)
#include <stddef.h> /* offsetof */
#include <sys/time.h> /* struct timeval (shared VLIBC_TIMEVAL_DEFINED guard) */
#include <sys/types.h> /* id_t */
#ifdef __cplusplus
extern "C" {
#endif
/*
* Resource limit values: the 64-bit width of the kernel's rlimit64 record
* (unsigned long long on x86_64, where the classic long is also 64-bit but
* the LFS spelling is authoritative).
*/
typedef unsigned long long rlim_t;
/*
* A resource limit pair: rlim_cur is the soft limit (enforced), rlim_max
* the hard limit (ceiling a soft limit may be raised to without
* privilege). RLIM_INFINITY in either field means "unlimited".
*/
struct rlimit
{
rlim_t rlim_cur; /* soft limit (current) */
rlim_t rlim_max; /* hard limit (ceiling) */
};
/*
* Per-process resource usage. The field order is the kernel's own
* (getrusage copies the record out untouched): the two CPU-time timevals
* first, then the fourteen accounting longs. ru_maxrss is in kilobytes.
* The fields a kernel does not maintain stay zero.
*/
struct rusage
{
struct timeval ru_utime; /* user CPU time used */
struct timeval ru_stime; /* system CPU time used */
long ru_maxrss; /* maximum resident set size (KiB) */
long ru_ixrss; /* integral shared memory size */
long ru_idrss; /* integral unshared data size */
long ru_isrss; /* integral unshared stack size */
long ru_minflt; /* page reclaims (soft page faults) */
long ru_majflt; /* page faults (hard page faults) */
long ru_nswap; /* swaps */
long ru_inblock; /* block input operations */
long ru_oublock; /* block output operations */
long ru_msgsnd; /* IPC messages sent */
long ru_msgrcv; /* IPC messages received */
long ru_nsignals; /* signals received */
long ru_nvcsw; /* voluntary context switches */
long ru_nivcsw; /* involuntary context switches */
};
/* x86_64 kernel ABI: 2 x struct timeval (16 B) + 14 longs (8 B) = 144 B. */
_Static_assert(sizeof(struct rusage) == 144, "struct rusage must match the x86_64 kernel layout");
_Static_assert(offsetof(struct rusage, ru_maxrss) == 32, "ru_maxrss must follow the two timevals");
_Static_assert(sizeof(struct rlimit) == 2 * sizeof(rlim_t),
"struct rlimit is a pair of rlim_t words");
/* ---- Resource limit identifiers (kernel asm-generic values) ---- */
#define RLIMIT_CPU 0 /* CPU time in seconds */
#define RLIMIT_FSIZE 1 /* maximum file size */
#define RLIMIT_DATA 2 /* data segment size */
#define RLIMIT_STACK 3 /* stack size */
#define RLIMIT_CORE 4 /* core file size */
#define RLIMIT_RSS 5 /* resident set size */
#define RLIMIT_NPROC 6 /* number of processes */
#define RLIMIT_NOFILE 7 /* number of open files */
#define RLIMIT_MEMLOCK 8 /* locked-in-memory address space */
#define RLIMIT_AS 9 /* address space size */
#define RLIMIT_LOCKS 10 /* number of file locks held */
#define RLIMIT_SIGPENDING 11 /* number of pending signals */
#define RLIMIT_MSGQUEUE 12 /* bytes in POSIX message queues */
#define RLIMIT_NICE 13 /* ceiling for the nice value */
#define RLIMIT_RTPRIO 14 /* maximum realtime priority */
#define RLIMIT_RTTIME 15 /* realtime CPU time (us) */
#define RLIMIT_NLIMITS 16 /* number of resource kinds */
/* "No limit" value for either field of struct rlimit. */
#define RLIM_INFINITY (~0UL)
/* Saved-limit markers (legacy; equal to RLIM_INFINITY on Linux). */
#define RLIM_SAVED_CUR RLIM_INFINITY
#define RLIM_SAVED_MAX RLIM_INFINITY
/* ---- getpriority/setpriority target selectors ---- */
#define PRIO_PROCESS 0 /* a single process */
#define PRIO_PGRP 1 /* a process group */
#define PRIO_USER 2 /* every process of a user */
/* The valid nice-value range (PRIO_MIN is the highest scheduling priority). */
#define PRIO_MIN (-20)
#define PRIO_MAX 19
/* ---- getrusage `who` selectors ---- */
#define RUSAGE_SELF 0 /* the calling process */
#define RUSAGE_CHILDREN (-1) /* terminated and waited-for children */
#define RUSAGE_THREAD 1 /* the calling thread */
/*
* Return the current soft and hard limits of resource through rlim. The
* call never fails for a valid resource. Return 0, or -1 with errno set
* when resource is out of range or rlim points outside the address space.
*/
int
getrlimit(int resource, struct rlimit *rlim);
/*
* Set the soft and hard limits of resource from rlim. Raising the hard
* limit (or the soft limit above the hard limit) requires privilege.
* Return 0, or -1 with errno set for an out-of-range resource, a soft
* limit above the hard limit, or a denied raise.
*/
int
setrlimit(int resource, const struct rlimit *rlim);
/*
* Fill usage with the resource usage of who (RUSAGE_SELF, RUSAGE_CHILDREN
* or RUSAGE_THREAD). Return 0, or -1 with errno set for an unknown who or
* when usage points outside the address space.
*/
int
getrusage(int who, struct rusage *usage);
/*
* Return the nice value of the target described by (which, who), in the
* range PRIO_MIN..PRIO_MAX, or -1 with errno set on error. Because -1 is
* also a valid priority, callers conventionally clear errno before the
* call and treat a -1 return with a nonzero errno as an error.
*/
int
getpriority(int which, id_t who);
/*
* Set the nice value of every process named by (which, who) to prio,
* clamping out-of-range values to PRIO_MIN..PRIO_MAX. Lowering the value
* (raising priority) requires privilege. Return 0, or -1 with errno set.
*/
int
setpriority(int which, id_t who, int prio);
/*
* Add inc to the calling process's nice value and return the new value,
* or -1 with errno set on error (see getpriority for the -1 convention).
*/
int
nice(int inc);
/*
* Large-file aliases. rlim_t is already 64-bit on x86_64, so these behave
* exactly as getrlimit/setrlimit and exist as separate exported symbols for
* LFS callers.
*/
int
getrlimit64(int resource, struct rlimit *rlim);
int
setrlimit64(int resource, const struct rlimit *rlim);
#ifdef __cplusplus
}
#endif
#endif /* VLIBC_LEVEL_GE(2) */
#endif /* VLIBC_SYS_RESOURCE_H */
+39
View File
@@ -0,0 +1,39 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/resource.h>
#include "../internal/syscall.h"
#if VLIBC_LEVEL_GE(2)
/*
* getpriority (todo 31).
*
* The kernel never returns the raw nice value from SYS_getpriority: to
* keep successful results from colliding with its negative -errno error
* encoding, it reports 20 - nice (1..40 for nice -20..19). Recover the
* nice value with the inverse translation. Errors arrive as negative
* -errno values and go through syscall_ret, which stores errno and
* returns -1.
*
* A returned -1 is therefore either an error (errno set) or the
* legitimate priority of a process running at nice -1 (errno untouched) —
* the caller distinguishes them by clearing errno before the call, per the
* POSIX convention documented in <sys/resource.h>.
*/
int
getpriority(int which, id_t who)
{
long r;
r = __syscall2(SYS_getpriority, (long)which, (long)who);
if (r < 0)
{
return syscall_ret(r);
}
return (int)(20 - r);
}
#endif /* VLIBC_LEVEL_GE(2) */
+35
View File
@@ -0,0 +1,35 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/resource.h>
#include "../internal/syscall.h"
#if VLIBC_LEVEL_GE(2)
/*
* getrlimit/getrlimit64 (todo 31).
*
* x86_64 has no getrlimit syscall; the prlimit64 ABI serves the read with
* (pid = 0, resource, new_limit = NULL, old_limit = &rlim). struct rlimit
* is two 64-bit words here — the same layout as the kernel's struct
* rlimit64 — so the record passes through without conversion.
*
* getrlimit64 is a name-only distinction on this architecture (rlim_t is
* already 64 bits); it exists as a separate exported symbol so LFS callers
* that reference it link cleanly.
*/
int
getrlimit(int resource, struct rlimit *rlim)
{
return syscall_ret(__syscall4(SYS_prlimit64, 0, (long)resource, 0, (long)rlim));
}
int
getrlimit64(int resource, struct rlimit *rlim)
{
return getrlimit(resource, rlim);
}
#endif /* VLIBC_LEVEL_GE(2) */
+25
View File
@@ -0,0 +1,25 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/resource.h>
#include "../internal/syscall.h"
#if VLIBC_LEVEL_GE(2)
/*
* getrusage (todo 31).
*
* Thin SYS_getrusage wrapper: (who, &usage). struct rusage is the kernel's
* own record layout on x86_64 (two timevals + fourteen longs, verified in
* <sys/resource.h>), so the kernel's copy-to-user writes the public struct
* without translation. The kernel rejects an unknown who with EINVAL.
*/
int
getrusage(int who, struct rusage *usage)
{
return syscall_ret(__syscall2(SYS_getrusage, (long)who, (long)usage));
}
#endif /* VLIBC_LEVEL_GE(2) */
+47
View File
@@ -0,0 +1,47 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/resource.h>
#include <errno.h>
#if VLIBC_LEVEL_GE(2)
/*
* nice (todo 31).
*
* Built on getpriority/setpriority over PRIO_PROCESS for the calling
* process (the plan's design): read the current nice value, add inc, write
* the sum back. The kernel clamps the result into PRIO_MIN..PRIO_MAX and
* rejects a raise (a decrease of the nice value) without privilege.
*
* The new nice value is returned, as POSIX requires; that value may be -1
* only when the process already runs at nice -1, which getpriority reports
* with errno untouched, so the -1 error sentinel is distinguishable the
* same way getpriority documents it.
*/
int
nice(int inc)
{
int prio;
prio = getpriority(PRIO_PROCESS, 0);
if (prio == -1)
{
/* -1 is a valid priority too; only a nonzero errno marks the read
* as a genuine failure (getpriority leaves errno alone on
* success). Callers may clear errno first to disambiguate. */
if (errno != 0)
{
return -1;
}
}
if (setpriority(PRIO_PROCESS, 0, prio + inc) != 0)
{
return -1; /* errno already set */
}
return getpriority(PRIO_PROCESS, 0);
}
#endif /* VLIBC_LEVEL_GE(2) */
+27
View File
@@ -0,0 +1,27 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/resource.h>
#include "../internal/syscall.h"
#if VLIBC_LEVEL_GE(2)
/*
* setpriority (todo 31).
*
* Thin SYS_setpriority wrapper: (which, who, prio). The kernel clamps
* out-of-range nice values to PRIO_MIN..PRIO_MAX instead of rejecting
* them, so the only failure paths are an invalid which (EINVAL), an
* unknown target (ESRCH) and a denied priority raise (EPERM/EACCES); the
* latter applies when the call would lower the target's nice value below
* its current one without privilege.
*/
int
setpriority(int which, id_t who, int prio)
{
return syscall_ret(__syscall3(SYS_setpriority, (long)which, (long)who, (long)prio));
}
#endif /* VLIBC_LEVEL_GE(2) */
+36
View File
@@ -0,0 +1,36 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/resource.h>
#include "../internal/syscall.h"
#if VLIBC_LEVEL_GE(2)
/*
* setrlimit/setrlimit64 (todo 31).
*
* The write side of the prlimit64 ABI: (pid = 0, resource,
* new_limit = &rlim, old_limit = NULL). struct rlimit is the kernel's
* struct rlimit64 layout on x86_64, so the record passes through without
* conversion; the kernel rejects a soft limit above the hard limit with
* EINVAL and a hard-limit raise without CAP_SYS_RESOURCE with EPERM.
*
* setrlimit64 is a name-only distinction on this architecture (rlim_t is
* already 64 bits); it exists as a separate exported symbol for LFS
* callers.
*/
int
setrlimit(int resource, const struct rlimit *rlim)
{
return syscall_ret(__syscall4(SYS_prlimit64, 0, (long)resource, (long)rlim, 0));
}
int
setrlimit64(int resource, const struct rlimit *rlim)
{
return setrlimit(resource, rlim);
}
#endif /* VLIBC_LEVEL_GE(2) */
+256
View File
@@ -0,0 +1,256 @@
/*
* vlibc — sys/resource.h test (todo 31).
*
* Exercises the whole <sys/resource.h> surface end to end:
*
* 1. getrlimit(RLIMIT_NOFILE): returns 0 with a finite soft limit in
* (0, max] — the kernel's own NOFILE ceiling, so cur is never
* RLIM_INFINITY. getrlimit(RLIMIT_STACK) is the QA happy shape and
* must also report a positive soft limit.
* 2. setrlimit round-trip: re-installing the just-read RLIMIT_NOFILE
* pair must succeed without privilege (identical values never raise
* the hard limit), and a following getrlimit must reflect it.
* 3. getrlimit64/setrlimit64 parity: the LFS aliases return the same
* values and accept the same writes as the base functions.
* 4. getrusage(RUSAGE_SELF) and RUSAGE_THREAD: ru_utime is a sane
* timeval and ru_maxrss is non-negative.
* 5. getpriority(PRIO_PROCESS, 0) lands in PRIO_MIN..PRIO_MAX, and
* setpriority to the same value round-trips (equal values are never
* a denied raise).
* 6. nice(0) returns the current nice value: no change, no error.
* 7. Constant sanity: RLIM_INFINITY is ~0UL, RLIMIT_NLIMITS is 16 and
* PRIO_MIN/PRIO_MAX bound the range [-20, 19].
*
* The -f mode runs the failure shapes — setrlimit/getrusage/getpriority/
* setpriority with a bogus resource kind or `which` — asserting return
* values only (errno is never read: vlibc's errno slot collides with the
* host TCB). It leaves via a raw SYS_exit_group so no host cleanup runs
* after the library's errno writes (house pattern, tests/test_systime.c).
*
* No host libc headers are included (the -Iinclude path would shadow
* GCC's internal headers); diagnostics go through raw SYS_write. The whole
* body mirrors the header's gate: <sys/resource.h> is entirely level 2, so
* at level 1 this TU compiles to a no-op runner.
*
* Standalone build (list the src/resource/ files individually, or the
* shell glob would trip -Wcomment in this header block):
* gcc -Iinclude -DVLIBC_LEVEL=2 -std=c23 -Wall -Wextra -pedantic -O2 \
* -o /tmp/t31 tests/test_resource.c \
* src/resource/getrlimit.c src/resource/setrlimit.c \
* src/resource/getrusage.c src/resource/getpriority.c \
* src/resource/setpriority.c src/resource/nice.c \
* src/internal/errno.c src/internal/syscall_ret.c
*/
#include <sys/resource.h>
#include "../src/internal/syscall.h"
/* Write a NUL-terminated string to fd via the raw syscall layer. The
* optimize attribute keeps GCC from lowering the length loop into a
* strlen call, which would leave a vlibc-owned symbol undefined in this
* host-linked standalone binary (house idiom, see src/string). */
static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void
say(int fd, const char *s)
{
long n = 0;
while (s[n] != '\0')
{
n++;
}
__syscall3(SYS_write, fd, (long)s, n);
}
#if VLIBC_LEVEL_GE(2)
static int failures;
static void
check(int ok, const char *msg)
{
if (ok)
{
say(1, "ok ");
}
else
{
say(1, "FAIL ");
failures++;
}
say(1, msg);
say(1, "\n");
}
/* 1. getrlimit reads sane, finite NOFILE and STACK soft limits. */
static void
rlimit_read_scenario(void)
{
struct rlimit rl;
check(getrlimit(RLIMIT_NOFILE, &rl) == 0, "getrlimit(RLIMIT_NOFILE) returns 0");
check(rl.rlim_cur > 0, "getrlimit NOFILE rlim_cur is positive");
check(rl.rlim_max > 0 && rl.rlim_max >= rl.rlim_cur,
"getrlimit NOFILE rlim_max is positive and at least rlim_cur");
check(getrlimit(RLIMIT_STACK, &rl) == 0, "getrlimit(RLIMIT_STACK) returns 0");
check(rl.rlim_cur > 0, "getrlimit STACK rlim_cur is positive");
}
/* 2. Re-installing the same limit pair succeeds and round-trips. */
static void
setrlimit_roundtrip_scenario(void)
{
struct rlimit before;
struct rlimit after;
if (getrlimit(RLIMIT_NOFILE, &before) != 0)
{
check(0, "getrlimit(RLIMIT_NOFILE) for the round-trip returns 0");
return;
}
check(setrlimit(RLIMIT_NOFILE, &before) == 0, "setrlimit of identical NOFILE pair returns 0");
check(getrlimit(RLIMIT_NOFILE, &after) == 0, "getrlimit after setrlimit returns 0");
check(after.rlim_cur == before.rlim_cur && after.rlim_max == before.rlim_max,
"getrlimit reflects the re-installed NOFILE pair");
}
/* 3. The LFS aliases read and write exactly like the base functions. */
static void
rlimit64_alias_scenario(void)
{
struct rlimit a;
struct rlimit b;
check(getrlimit64(RLIMIT_NOFILE, &a) == 0, "getrlimit64(RLIMIT_NOFILE) returns 0");
check(getrlimit(RLIMIT_NOFILE, &b) == 0, "getrlimit(RLIMIT_NOFILE) returns 0");
check(a.rlim_cur == b.rlim_cur && a.rlim_max == b.rlim_max,
"getrlimit64 agrees with getrlimit");
check(setrlimit64(RLIMIT_NOFILE, &a) == 0, "setrlimit64 of identical pair returns 0");
check(getrlimit(RLIMIT_NOFILE, &b) == 0, "getrlimit after setrlimit64 returns 0");
check(b.rlim_cur == a.rlim_cur && b.rlim_max == a.rlim_max,
"setrlimit64 write is visible through getrlimit");
}
/* 4. getrusage reports sane accounting for the process and the thread. */
static void
getrusage_scenario(void)
{
struct rusage ru;
check(getrusage(RUSAGE_SELF, &ru) == 0, "getrusage(RUSAGE_SELF) returns 0");
check(ru.ru_utime.tv_sec >= 0, "getrusage ru_utime.tv_sec is non-negative");
check(ru.ru_utime.tv_usec >= 0 && ru.ru_utime.tv_usec < 1000000,
"getrusage ru_utime.tv_usec is in [0, 1e6)");
check(ru.ru_maxrss >= 0, "getrusage ru_maxrss is non-negative");
check(getrusage(RUSAGE_THREAD, &ru) == 0, "getrusage(RUSAGE_THREAD) returns 0");
check(ru.ru_utime.tv_sec >= 0, "getrusage THREAD ru_utime.tv_sec is non-negative");
}
/* 5. getpriority/setpriority round-trip on the current process. */
static void
priority_roundtrip_scenario(void)
{
int p;
p = getpriority(PRIO_PROCESS, 0);
check(p >= PRIO_MIN && p <= PRIO_MAX,
"getpriority(PRIO_PROCESS, 0) is in [PRIO_MIN, PRIO_MAX]");
check(setpriority(PRIO_PROCESS, 0, p) == 0, "setpriority to the current value returns 0");
check(getpriority(PRIO_PROCESS, 0) == p, "getpriority round-trips through setpriority");
}
/* 6. nice(0) reports the current nice value without changing it. */
static void
nice_scenario(void)
{
int cur;
int n;
cur = getpriority(PRIO_PROCESS, 0);
check(cur >= PRIO_MIN && cur <= PRIO_MAX, "nice baseline is in [PRIO_MIN, PRIO_MAX]");
n = nice(0);
check(n == cur, "nice(0) returns the current nice value");
check(getpriority(PRIO_PROCESS, 0) == cur, "nice(0) left the nice value unchanged");
}
/* 7. Constant sanity. */
static void
constant_scenario(void)
{
check(RLIM_INFINITY == ~0UL, "RLIM_INFINITY is ~0UL");
check(RLIMIT_NLIMITS == 16, "RLIMIT_NLIMITS is 16");
check(PRIO_MIN == -20 && PRIO_MAX == 19, "PRIO_MIN..PRIO_MAX is -20..19");
check(RUSAGE_SELF == 0 && RUSAGE_CHILDREN == -1 && RUSAGE_THREAD == 1,
"RUSAGE_SELF/CHILDREN/THREAD are 0/-1/1");
}
/* The failure shapes; return values only, never errno. Exit via a raw
* SYS_exit_group: the library's errno writes on these paths corrupt glibc's
* private dtv slot at %fs:0+8, so host cleanup must never run (house
* pattern, tests/test_systime.c). */
static void
failure_scenarios(void)
{
struct rlimit rl;
struct rusage ru;
rl.rlim_cur = 0;
rl.rlim_max = 0;
check(setrlimit(9999, &rl) == -1, "-f setrlimit(9999) returns -1");
check(getrlimit(9999, &rl) == -1, "-f getrlimit(9999) returns -1");
check(getrusage(999, &ru) == -1, "-f getrusage(999) returns -1");
check(getpriority(999, 0) == -1, "-f getpriority(999, 0) returns -1");
check(setpriority(999, 0, 0) == -1, "-f setpriority(999, 0, 0) returns -1");
if (failures == 0)
{
say(1, "all resource failure scenarios passed\n");
}
__syscall1(SYS_exit_group, failures == 0 ? 0 : 1);
/* not reached */
}
int
main(int argc, char **argv)
{
if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'f' && argv[1][2] == '\0')
{
failure_scenarios();
}
rlimit_read_scenario();
setrlimit_roundtrip_scenario();
rlimit64_alias_scenario();
getrusage_scenario();
priority_roundtrip_scenario();
nice_scenario();
constant_scenario();
if (failures == 0)
{
say(1, "all resource tests passed\n");
}
else
{
say(1, "FAILURES\n");
}
return failures == 0 ? 0 : 1;
}
#else /* !VLIBC_LEVEL_GE(2) */
/*
* Level 1: every <sys/resource.h> symbol is gated at level 2, so there is
* nothing to run. Keep the TU compilable at any configured profile.
*/
int
main(void)
{
say(1, "SKIP: <sys/resource.h> is level 2, not available here\n");
return 0;
}
#endif /* VLIBC_LEVEL_GE(2) */