diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cde56e..c76109c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -250,7 +250,19 @@ jobs: # examined here, where a build of the modules exists: the test names # every entity SURFACE.txt lists, so a name the modules do not export # fails to compile and the diagnostic names it. - list="$(grep -vE '^[[:space:]]*(#|$)' .spec/SURFACE.txt | sort -u)" + # THE NAMES OF THE INTERFACES THIS IMPLEMENTATION PROVIDES, not every + # name: since openkal 0.20 SURFACE.txt carries `openkal.prof' and + # `openkal.debug', which are a loader's (clause 12.3), and an interface + # an implementation does not provide is absent by clause 6.1. The set is + # the manifest's `provides-interfaces', which the step above checks + # against the artefact, and the self-description every implementation + # exports. + provided="$(sed -n '/^provides-interfaces/,/^]/p' mcpp.toml | grep -oE 'openkal\.[a-z]+'; echo openkal.version)" + list="$(awk -v keep="$provided" ' + BEGIN { n = split(keep, k, "\n"); for (i = 1; i <= n; i++) want[k[i]] = 1 } + /^# openkal\.[a-z]+( |$)/ { group = $2; next } + /^[[:space:]]*(#|$)/ { next } + group in want { print }' .spec/SURFACE.txt | sort -u)" { # THE MODULE LIST IS DERIVED, NOT WRITTEN OUT. # diff --git a/README.md b/README.md index 93c6458..6739a8a 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ for Linux, written on the kernel's own system-call interface. ```toml [dependencies] -openkal = "0.19.0" +openkal = "0.20.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.20.0" +openkal-linux = "0.21.0" ``` ## Why it does not use a C library @@ -42,7 +42,8 @@ second and third. ## Interfaces provided -All eight. `tools/check-surface.sh --complete` in the specification package +Every interface of `SURFACE.txt` except `openkal.prof` and `openkal.debug`, which +are a loader's (clause 12.3). `tools/check-surface.sh --complete` in the specification package compares the exported names against `SURFACE.txt`. ## The stack a started context is given @@ -101,22 +102,57 @@ A program that carries a runtime of its own answers from that runtime instead (`pthread_getattr_np`), because in that arrangement the runtime is what owns the stacks and is what a program above openkal would ask for itself. -## A defect this version repairs - -0.16.0 added the storage this implementation keeps for the region a context -stands on, and that storage made a latent defect of `src/tls.h` visible: the -thread-local segment's alignment was clamped up to sixteen where the block's -SIZE is computed, and the size has to be the one the linker measured every -variable's offset against. On a segment stating `p_align = 8, p_memsz = 56` the -linker laid the variables out at `tp - 56` while the region was built 64 bytes -deep, so the image of the program's thread-local storage sat eight bytes below -the variables that name it. A program whose thread-local variables are -initialised then reads another variable's bytes, and a C++ program's -`thread_local` object can find its guard byte non-zero and never be constructed ---- which is how openkal-llvm-runtime's own probe reported it. - -The clamp now applies to the ALLOCATION, which wants sixteen, and not to the -size, which is the linker's. +## What 0.21 adds (openkal 0.20) + +**Transfers at an offset.** `kal_fs_read_at` and `kal_fs_write_at` are the +kernel's `pread64` and `pwrite64`: one call each, and the file's position is +neither used nor moved. A write at an offset to a file opened for appending lands +at the end, which is this kernel's answer and one the specification names. + +**Regions.** `openkal.region` is provided. With the kernel's default treatment of +memory (`vm.overcommit_memory` 0 or 1) a reservation is a mapping usable at once +(`MAP_NORESERVE`), `kal_region_commit` makes no call, returning the backing is +`madvise(MADV_DONTNEED)` and discarding is `madvise(MADV_FREE)`; +`KAL_REGION_PROP_OVERCOMMIT` is claimed. Where the kernel charges memory when it +is mapped (`vm.overcommit_memory` 2), a reservation is inaccessible and uncharged, +backing is `mprotect`, and returning it is a fresh inaccessible mapping over the +part; the property is not claimed. The mode is read once from /proc. + +**The clock without a system call.** `kal_time_monotonic` and `kal_time_wall` +call the `clock_gettime` the kernel places in every program (the vDSO, found +through `AT_SYSINFO_EHDR`), and the system call remains where there is none. + +**A volume is asked once.** Whether a file may be started is a property of its +volume as well as of its node (`KAL_INFO_EXECUTABLE`), and the volume's answer +(`fstatfs`) is kept by the device number the node reports: a `stat` that wants the +field makes one system call, not two. Eight volumes are kept. + +**What a program and a unit use.** `kal_process_info` reports `KAL_PINFO_MEMORY`: +`VmRSS` and `VmHWM` of /proc//status for a running program, and for a +started program that has ended its peak from the record the kernel keeps until it +is collected. `kal_process_wait` therefore waits without collecting +(`waitid(WNOWAIT)`) and `kal_process_close` collects: a caller that waits and +never releases the handle leaves the program in the kernel's table until it ends +itself, which the handle's ownership (clause 6.7) already asked it not to do. +`kal_timeout_wait_process` waits on the program's descriptor (`pidfd_open`, 5.3 +and later) instead of polling. + +A unit is a process group, and its use is the sum over the entries of /proc +whose group is the unit's --- every program in it, including those its members +started; one read per program on the machine, made only when a caller asks. +`KAL_PROCESS_PROP_JOB_USAGE` is claimed. + +**A limit on a unit is supervised.** `kal_process_job_limit` starts, with the +first limit, a context that observes the limited units and ends a unit with +SIGKILL when it sees its use over the limit: every 500 ms while the use is below +half the limit, 200 ms below four fifths, 100 ms above. The longest interval is +therefore 500 ms. A program a limit ended is reported by `kal_process_wait` with +`KAL_TERMINATED_LIMIT`. `KAL_PROCESS_PROP_JOB_LIMIT_SUPERVISED` is claimed and +`KAL_PROCESS_PROP_JOB_LIMIT_HARD` is not: the kernel's own ceiling is a control +group, a program may make one only where the hierarchy was delegated to it, and +whether it was is the service manager's record rather than the kernel's --- on an +ordinary desktop the program's own slice is writable and is not delegated. A +program that holds a delegation and wants the kernel's ceiling uses it directly. ## Conformance diff --git a/mcpp.toml b/mcpp.toml index ce464e7..8bb8165 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.20.0" +version = "0.21.0" description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one." license = "Apache-2.0" @@ -61,10 +61,11 @@ provides-interfaces = [ "openkal.space", "openkal.timeout", "openkal.map", + "openkal.region", ] [dependencies] -openkal = "0.19.0" +openkal = "0.20.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/src/fs.cpp b/src/fs.cpp index 9ff7b6a..e0dd925 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -220,6 +220,34 @@ namespace okl { // The volume's properties, for the interfaces outside this file that ask one // (openkal.net's rendezvous at a name). kal_uintptr volume_props_of(int fd) { return volume_props(fd); } + +// THE PROPERTIES OF A VOLUME, ONCE PER VOLUME (0.21). An enquiry that wants +// whether a file may be started asks its volume (a format that records no such +// bit answers nothing), and asked `fstatfs' once per enquiry: a compiler run as +// an image made 71 thousand of them in one warm start, one beside every `stat'. +// A volume's format does not change under a device number while it is mounted, +// so the answer is kept by the device the kernel reports for the node; a slot +// a later device needs is taken over (eight are kept). +struct volume_answer { okl_u64 dev; kal_uintptr props; bool known; }; +volume_answer g_volumes[8]; +unsigned g_volume_next = 0; +volatile int g_volume_lock = 0; + +kal_uintptr volume_props_on(okl_u64 dev, int fd) { + while (__atomic_exchange_n(&g_volume_lock, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield); + for (auto const& v : g_volumes) + if (v.known && v.dev == dev) { + const kal_uintptr props = v.props; + __atomic_store_n(&g_volume_lock, 0, __ATOMIC_RELEASE); + return props; + } + __atomic_store_n(&g_volume_lock, 0, __ATOMIC_RELEASE); + const kal_uintptr props = volume_props(fd); + while (__atomic_exchange_n(&g_volume_lock, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield); + g_volumes[g_volume_next++ % 8] = volume_answer{ dev, props, true }; + __atomic_store_n(&g_volume_lock, 0, __ATOMIC_RELEASE); + return props; +} } // namespace okl namespace { @@ -236,7 +264,8 @@ namespace { // The exception is KAL_INFO_EXECUTABLE, which is a property of the volume as // well as of the node: a format that stores no mode reports execute bits the // mount chose, and reporting those would be answering for the volume. The -// volume is asked only when the caller wants the field, and only for a file. +// volume is asked only when the caller wants the field, and only for a file, +// and once per volume. void fill_info(const okl::kstat& st, kal_u32 wanted, kal_node_info* out, int volume_fd) { const kal_u32 self = out->self_size; @@ -259,7 +288,7 @@ void fill_info(const okl::kstat& st, kal_u32 wanted, kal_node_info* out, __builtin_offsetof(kal_node_info, executable) + sizeof v.executable); if ((wanted & KAL_INFO_EXECUTABLE) != 0 && self >= reaches && v.kind == kal_node_file - && (volume_props(volume_fd) & KAL_FS_PROP_EXECUTABLE) != 0) { + && (okl::volume_props_on(st.dev, volume_fd) & KAL_FS_PROP_EXECUTABLE) != 0) { v.present |= KAL_INFO_EXECUTABLE; v.executable = (st.mode & 0111u) != 0 ? 1 : 0; } @@ -487,6 +516,43 @@ int kal_fs_truncate(kal_file f, kal_u64 size) { return okl::failed(r) ? okl::translate(r) : kal_ok; } +// TRANSFERS AT AN OFFSET (openkal 0.20): the kernel's own `pread64' and +// `pwrite64', which neither use nor move the position. A write at an offset to +// a file opened for appending lands at the end, which is this kernel's answer +// and one the specification names. The write is completed as a stream's is, +// and an interruption is retried rather than reported (clause 7.5). +kal_intptr kal_fs_read_at(kal_file f, void* buf, kal_uintptr len, kal_u64 offset) { + const int fd = okl::unpack(f.h); + if (fd < 0) return -kal_err_invalid; + for (;;) { + const okl_long r = okl::sys(okl::nr_pread64, fd, reinterpret_cast(buf), + static_cast(len), static_cast(offset)); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) return -okl::translate(r); + return static_cast(r); + } +} + +kal_intptr kal_fs_write_at(kal_file f, const void* buf, kal_uintptr len, kal_u64 offset) { + const int fd = okl::unpack(f.h); + if (fd < 0) return -kal_err_invalid; + const auto* p = static_cast(buf); + kal_uintptr done = 0; + while (done < len) { + const okl_long r = okl::sys(okl::nr_pwrite64, fd, reinterpret_cast(p + done), + static_cast(len - done), + static_cast(offset + done)); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) { + if (done != 0) return static_cast(done); + return -okl::translate(r); + } + if (r == 0) break; + done += static_cast(r); + } + return static_cast(done); +} + int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_uintptr flags, kal_u32 wanted, kal_node_info* out) { const int b = okl::unpack(base.h); diff --git a/src/process.cpp b/src/process.cpp index 5218427..50e3483 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -2,8 +2,15 @@ #include "handle.h" #include #include +#include namespace okl { extern char** g_envp; extern bool g_pipe_started_ignored; okl_ulong auxval(okl_ulong key); } +// Defined with what a unit uses, below (0.21). +namespace okl { +int child_outcome(okl_long pid, bool block, int* status, int* terminated, bool& ended); +void job_forget(okl_long unit); +bool child_memory(okl_long pid, bool ended, okl_u64& now, okl_u64& peak); +} namespace { @@ -782,26 +789,13 @@ int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) if (h.h == 0) return kal_err_invalid; if (handle_kind(h) == kind_self) return kal_err_invalid; if (handle_kind(h) == kind_opened || handle_kind(h) == kind_observed) return kal_err_not_supported; - int st = 0; - for (;;) { - const okl_long r = okl::sys(okl::nr_wait4, static_cast(h.h), - reinterpret_cast(&st), 0, 0); - if (okl::interrupted(r)) continue; - if (okl::failed(r)) return okl::translate(r); - break; - } - // The encoding is the kernel's: the low seven bits name the signal that - // ended the program and are zero when it ended by returning, in which case - // the next eight bits are what it returned. - const int signalled = st & 0x7f; - if (signalled == 0) { - if (status) *status = (st >> 8) & 0xff; - if (terminated_by_environment) *terminated_by_environment = 0; - } else { - if (status) *status = signalled; - if (terminated_by_environment) *terminated_by_environment = 1; - } - return kal_ok; + // NOT COLLECTED HERE (0.21): the program is waited for and left in the + // kernel's record until `kal_process_close', so that what it used --- its + // peak memory, which the record keeps --- can still be asked about. A + // program ended by its unit's limit is told apart by the unit (openkal 0.20). + bool ended = false; + return okl::child_outcome(static_cast(h.h & 0xffffffffu), true, status, + terminated_by_environment, ended); } // ONE PROGRAM, WHATEVER UNIT IT IS IN. @@ -873,11 +867,21 @@ int kal_process_job_terminate(kal_job j) { // so there is no handle to close --- and the operation exists so that a caller // need not know that. Where the unit IS a resource, releasing it must still not // end its members; the interface says so at the declaration. -void kal_process_job_close(kal_job) { } +void kal_process_job_close(kal_job j) { if (j.h != 0) okl::job_forget(static_cast(j.h)); } // Releasing the handle does not affect the program. A program that has not been // waited for continues, and this environment collects it when the caller exits. void kal_process_close(kal_process h) { + // A started program that has ended is collected now, which `kal_process_wait' + // did not do (0.21); one still running continues, as before. + if (handle_kind(h) == kind_child && h.h != 0) { + alignas(8) unsigned char info[128] = {}; + okl_long r; + do r = okl::sys(okl::nr_waitid, 1 /* P_PID */, handle_pid(h), reinterpret_cast(info), + 4 | 1 /* WEXITED | WNOHANG */, 0); + while (okl::interrupted(r)); + return; + } if (handle_kind(h) == kind_opened) { const okl_long fd = static_cast((h.h >> 32) & 0xffffu) - 1; if (fd >= 0) okl::sys(okl::nr_close, fd); @@ -1208,6 +1212,16 @@ int kal_process_info(kal_process p, kal_u32 wanted, kal_program_info* out) { okl_u64 c = 0; if (cpu_ns(pid, me, c)) { v.cpu_ns = c; v.present |= KAL_PINFO_CPU; } } + // MEMORY (openkal 0.20): resident now and at most, from the kernel's record; + // of a started program that has ended, its peak from the record it keeps + // until it is collected. Only where the structure has room for the fields. + if ((wanted & KAL_PINFO_MEMORY) && self >= sizeof v) { + okl_u64 now = 0, peak = 0; + const bool ok = me ? okl::child_memory(0, false, now, peak) + : handle_kind(p) == kind_child ? okl::child_memory(pid, ended, now, peak) + : (!ended && okl::child_memory(pid, false, now, peak)); + if (ok) { v.memory_bytes = now; v.memory_peak_bytes = peak; v.present |= KAL_PINFO_MEMORY; } + } const kal_u32 n = self < sizeof v ? self : static_cast(sizeof v); okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); return kal_ok; @@ -1244,7 +1258,8 @@ kal_uintptr kal_process_props(void) { | KAL_PROCESS_PROP_EXIT_STATUS | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR | KAL_PROCESS_PROP_BOUND_LIFETIME - | KAL_PROCESS_PROP_JOB + | KAL_PROCESS_PROP_JOB | KAL_PROCESS_PROP_JOB_USAGE + | KAL_PROCESS_PROP_JOB_LIMIT_SUPERVISED | (open_id_available() || stat_state(0) != 0 ? KAL_PROCESS_PROP_OPEN_ID : 0u) // AGREES WITH `kal_process_stop_requested', because the header // defines null there as the absence this position reports. Read and @@ -1255,4 +1270,309 @@ kal_uintptr kal_process_props(void) { ? 0u : KAL_PROCESS_PROP_STOP_REQUESTED); } + +// --- what a program and a unit use, and a limit on a unit (0.20) ------------ +// +// A PROGRAM'S MEMORY is what this kernel says is resident for it, and the most +// that ever was (/proc//status: VmRSS and VmHWM). An ended program that has +// not been collected keeps its peak in the record the kernel holds until it is +// collected, and `waitid' with WNOWAIT reads it from there --- which is why +// `kal_process_wait' leaves the program uncollected and `kal_process_close' +// collects it (0.21): what it used remains readable while its handle is held. +// +// A UNIT IS A PROCESS GROUP, and the kernel records every program's group: the +// programs in a unit are the entries of /proc whose group is the unit's, which +// counts the programs its members started too. The kernel lists no group's +// members, so a unit's use is a walk of /proc --- one read per program on the +// machine, which is why nothing walks it unless a caller asked for a use or +// placed a limit. +// +// A LIMIT IS SUPERVISED. A context of this implementation, started with the +// first limit, watches the limited units and ends one it sees over its limit, +// at an interval of at most 500 ms (500 ms below half the limit, 200 ms below +// four fifths, 100 ms above). +// +// NOT HARD, AND WHY. The kernel holds a set of programs to a ceiling through a +// control group, and a program may make one only in a part of the hierarchy +// that was delegated to it; whether a part was delegated is the manager's +// record, not the kernel's, so an implementation that made groups wherever it +// could write would make them where it was not entitled to --- on an ordinary +// desktop the program's own slice is writable and is not delegated. And where +// a group is delegated, a program inside it may give it children with a +// ceiling only after moving itself out of it, which is the program's decision +// to make and not an interface's. A program that holds a delegation and wants +// the kernel's ceiling uses it directly (clause 1.1). +namespace { + +okl_long read_small(const char* path, char* b, okl_long cap) { + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast(path), + okl::o_rdonly | okl::o_cloexec, 0); + if (okl::failed(fd)) return -1; + okl_long n = 0; + while (n < cap - 1) { + const okl_long r = okl::sys(okl::nr_read, fd, reinterpret_cast(b + n), cap - 1 - n); + if (okl::interrupted(r)) continue; + if (r <= 0) break; + n += r; + } + okl::sys(okl::nr_close, fd); + b[n] = 0; + return n; +} + +okl_uptr length_of(const char* s) { okl_uptr n = 0; while (s[n]) ++n; return n; } + +// The number after `key' in a text of lines `key: number unit', or false. +bool field_value(const char* b, okl_long n, const char* key, okl_u64& out) { + const okl_uptr k = length_of(key); + for (okl_long i = 0; i + static_cast(k) <= n; ++i) { + if ((i == 0 || b[i - 1] == '\n') && same_prefix(b + i, key, k)) { + okl_long j = i + static_cast(k); + while (j < n && (b[j] == ' ' || b[j] == '\t' || b[j] == ':')) ++j; + okl_u64 v = 0; bool any = false; + while (j < n && b[j] >= '0' && b[j] <= '9') { v = v * 10 + static_cast(b[j] - '0'); ++j; any = true; } + out = v; + return any; + } + } + return false; +} + +okl_u64 page_bytes() { + const okl_ulong p = okl::auxval(6 /* AT_PAGESZ */); + return p != 0 ? p : 4096; +} + +// A running program's memory, from /proc//status (pid 0: this one). +bool resident(okl_long pid, okl_u64& now, okl_u64& peak) { + char path[40] = "/proc/"; + char* o = path + 6; + if (pid == 0) { okl::copy(o, "self", 4); o += 4; } + else o = put_decimal(o, static_cast(pid)); + okl::copy(o, "/status", 8); + char b[2048]; + const okl_long n = read_small(path, b, sizeof b); + if (n <= 0) return false; + okl_u64 rss = 0, hwm = 0; + if (!field_value(b, n, "VmRSS", rss)) return false; + if (!field_value(b, n, "VmHWM", hwm)) hwm = rss; + now = rss * 1024; peak = (hwm < rss ? rss : hwm) * 1024; + return true; +} + +// An ended program not yet collected: its record, read without collecting it. +struct k_rusage { okl_long utime_s, utime_us, stime_s, stime_us, maxrss; okl_long rest[13]; }; +bool ended_usage(okl_long pid, k_rusage& ru) { + alignas(8) unsigned char info[128] = {}; + const okl_long r = okl::sys(okl::nr_waitid, 1 /* P_PID */, pid, reinterpret_cast(info), + 4 | 1 | 0x01000000 /* WEXITED | WNOHANG | WNOWAIT */, + reinterpret_cast(&ru)); + if (okl::failed(r)) return false; + int si_pid = 0; + okl::copy(&si_pid, info + 16, sizeof si_pid); + return si_pid != 0; +} + +// --- the units this program limited ----------------------------------------- + +struct limited { + okl_long unit; // the group's identifier; 0 for a free row + okl_u64 limit; // bytes; 0 for none + int ended; // the supervisor ended it for its limit +}; +constexpr int kLimited = 64; +limited g_limited[kLimited]; +volatile int g_limited_lock = 0; +void lock_limited() { while (__atomic_exchange_n(&g_limited_lock, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield); } +void unlock_limited() { __atomic_store_n(&g_limited_lock, 0, __ATOMIC_RELEASE); } + +limited* row_of(okl_long unit) { + for (auto& r : g_limited) if (r.unit == unit) return &r; + return nullptr; +} + +// --- the programs of a unit ----------------------------------------------------- + +// Every program whose group is `unit', with its resident memory summed. +// Returns how many there are. +kal_u32 members_of(okl_long unit, okl_u64& memory) { + memory = 0; + kal_u32 count = 0; + const okl_long dir = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast("/proc"), + okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + if (okl::failed(dir)) return 0; + alignas(8) char buf[8192]; + const okl_u64 page = page_bytes(); + for (;;) { + const okl_long n = okl::sys(okl::nr_getdents64, dir, reinterpret_cast(buf), sizeof buf); + if (n <= 0) break; + for (okl_long at = 0; at < n;) { + const auto* d = reinterpret_cast(buf + at); + at += d->reclen; + const char* name = d->name; + if (name[0] < '1' || name[0] > '9') continue; + okl_long pid = 0; + for (const char* c = name; *c; ++c) pid = pid * 10 + (*c - '0'); + char b[1024]; + const okl_long len = read_stat(pid, b); + if (len <= 0) continue; + bool ok = false; + if (static_cast(stat_number(b, len, 5, ok)) != unit || !ok) continue; + bool rok = false; + const okl_u64 rss = stat_number(b, len, 24, rok); + if (rok) memory += rss * page; + ++count; + } + } + okl::sys(okl::nr_close, dir); + return count; +} + +// --- the supervisor ------------------------------------------------------------- + +int g_supervising = 0; + +void supervise(void*) { + for (;;) { + okl_u64 worst = 0; // the largest use, in thousandths of its limit + bool any = false; + for (int i = 0; i < kLimited; ++i) { + lock_limited(); + const limited r = g_limited[i]; + unlock_limited(); + if (r.unit == 0 || r.limit == 0 || r.ended) continue; + any = true; + okl_u64 memory = 0; + const kal_u32 n = members_of(r.unit, memory); + if (n == 0) continue; + if (memory > r.limit) { + // Marked before it is ended, so that a wait that returns + // because of the end already finds why. + lock_limited(); + if (g_limited[i].unit == r.unit) g_limited[i].ended = 1; + unlock_limited(); + okl::sys(okl::nr_kill, -r.unit, 9 /* SIGKILL */); + continue; + } + const okl_u64 share = memory * 1000 / r.limit; + if (share > worst) worst = share; + } + if (!any) { + __atomic_store_n(&g_supervising, 0, __ATOMIC_RELEASE); + // A limit placed between the last look and the store above found + // a supervisor still running and started none. + lock_limited(); + bool again = false; + for (const auto& r : g_limited) if (r.unit && r.limit && !r.ended) again = true; + unlock_limited(); + if (!again || __atomic_exchange_n(&g_supervising, 1, __ATOMIC_ACQ_REL)) return; + continue; + } + const okl_i64 ms = worst < 500 ? 500 : worst < 800 ? 200 : 100; + okl::ktimespec ts{ 0, ms * 1000000 }; + okl::sys(okl::nr_nanosleep, reinterpret_cast(&ts), 0); + } +} + +// Why a program that ended on SIGKILL ended: its unit's limit, where the +// supervisor ended the unit for it. +bool ended_by_limit(okl_long pid) { + char b[1024]; + const okl_long len = read_stat(pid, b); + if (len <= 0) return false; + bool ok = false; + const okl_long unit = static_cast(stat_number(b, len, 5, ok)); + if (!ok || unit <= 0) return false; + lock_limited(); + const limited* r = row_of(unit); + const limited copy = r ? *r : limited{}; + unlock_limited(); + return copy.unit != 0 && copy.ended != 0; +} + +} // namespace + +extern "C++" { +namespace okl { +void job_forget(okl_long unit) { + lock_limited(); + if (limited* r = row_of(unit)) *r = limited{}; + unlock_limited(); +} + +bool child_memory(okl_long pid, bool ended, okl_u64& now, okl_u64& peak) { + if (!ended) return resident(pid, now, peak); + k_rusage ru{}; + if (!ended_usage(pid, ru)) return false; + now = 0; + peak = static_cast(ru.maxrss) * 1024; + return true; +} + +// How a started program ended, read without collecting it; `block' waits for +// it. Shared with openkal.timeout. +int child_outcome(okl_long pid, bool block, int* status, int* terminated, bool& ended) { + alignas(8) unsigned char info[128] = {}; + okl_long r; + do r = okl::sys(okl::nr_waitid, 1 /* P_PID */, pid, reinterpret_cast(info), + 4 | 0x01000000 | (block ? 0 : 1) /* WEXITED | WNOWAIT [| WNOHANG] */, 0); + while (okl::interrupted(r)); + ended = false; + if (okl::failed(r)) return okl::translate(r); + int si_pid = 0, si_code = 0, si_status = 0; + okl::copy(&si_code, info + 8, sizeof si_code); + okl::copy(&si_pid, info + 16, sizeof si_pid); + okl::copy(&si_status, info + 24, sizeof si_status); + if (si_pid == 0) return kal_ok; // not yet + ended = true; + if (si_code == 1 /* CLD_EXITED */) { + if (status) *status = si_status & 0xff; + if (terminated) *terminated = 0; + } else { + if (status) *status = si_status; + if (terminated) + *terminated = (si_status == 9 && ended_by_limit(pid)) ? KAL_TERMINATED_LIMIT : 1; + } + return kal_ok; +} +} // namespace okl +} // extern "C++" + +int kal_process_job_usage(kal_job j, kal_job_usage* out) { + if (out == nullptr || j.h == 0 || out->self_size < 8) return kal_err_invalid; + const okl_long unit = static_cast(j.h); + kal_job_usage v{}; + v.self_size = out->self_size; + okl_u64 memory = 0; + v.members = members_of(unit, memory); + if (v.members == 0) return kal_err_invalid; + v.memory_bytes = memory; + const kal_u32 n = out->self_size < sizeof v ? out->self_size : static_cast(sizeof v); + okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); + return kal_ok; +} + +int kal_process_job_limit(kal_job j, kal_uintptr kind, kal_u64 value) { + if (kind != KAL_JOB_LIMIT_MEMORY) return kal_err_invalid; + if (j.h == 0) return kal_err_invalid; + const okl_long unit = static_cast(j.h); + + lock_limited(); + limited* r = row_of(unit); + if (r == nullptr && value != 0) r = row_of(0); + if (r == nullptr) { unlock_limited(); return value == 0 ? kal_ok : kal_err_no_memory; } + if (r->unit == 0) *r = limited{ unit, 0, 0 }; + r->limit = value; + unlock_limited(); + + if (value != 0 && !__atomic_exchange_n(&g_supervising, 1, __ATOMIC_ACQ_REL)) { + kal_task t{}; + if (kal_task_start(&supervise, nullptr, &t) != kal_ok) { + __atomic_store_n(&g_supervising, 0, __ATOMIC_RELEASE); + return kal_err_no_memory; + } + } + return kal_ok; +} + } diff --git a/src/region.cpp b/src/region.cpp new file mode 100644 index 0000000..6871900 --- /dev/null +++ b/src/region.cpp @@ -0,0 +1,126 @@ +// openkal.region (openkal 0.20): a range of the address space reserved first +// and backed as it is used. +// +// THIS KERNEL CHARGES BACKING WHEN A PAGE IS FIRST WRITTEN, UNLESS IT HAS BEEN +// TOLD NOT TO. Its default (vm.overcommit_memory 0 or 1) lets a private mapping +// be made larger than the memory it may ever have, and a page is found when it +// is touched; that is KAL_REGION_PROP_OVERCOMMIT, and a reservation is then a +// mapping that may be used at once, `commit' makes no call, and returning the +// backing is `madvise(MADV_DONTNEED)', after which the part reads as zero. +// +// Told not to (vm.overcommit_memory 2), the kernel charges a writable private +// mapping when it is made. A reservation is then a mapping that may not be +// accessed (PROT_NONE, which is not charged), backing is `mprotect' to +// read-write (which charges it), and returning it is a fresh inaccessible +// mapping placed over the part (which uncharges it). The mode is read once, +// from /proc, and the property word states which of the two this program met. +// +// Discarding is `madvise(MADV_FREE)': the kernel takes the pages back when it +// wants them and not before, and a part written meanwhile keeps what was +// written. A kernel before 4.5 refuses the advice, and the part's backing is +// returned instead, which is also a permitted outcome (the contents are not +// specified). +#include "sys.h" +#include +#include + +namespace { + +// -1 not yet read; 0 the kernel overcommits; 1 it charges at mapping. +int g_strict = -1; + +bool strict() { + int v = __atomic_load_n(&g_strict, __ATOMIC_ACQUIRE); + if (v >= 0) return v == 1; + v = 0; + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, + reinterpret_cast("/proc/sys/vm/overcommit_memory"), + okl::o_rdonly | okl::o_cloexec, 0); + if (!okl::failed(fd)) { + char c = 0; + if (okl::sys(okl::nr_read, fd, reinterpret_cast(&c), 1) == 1 && c == '2') v = 1; + okl::sys(okl::nr_close, fd); + } + __atomic_store_n(&g_strict, v, __ATOMIC_RELEASE); + return v == 1; +} + +okl_uptr granularity() { return static_cast(kal_memory_granularity()); } + +bool multiple(okl_uptr v, okl_uptr g) { return (v & (g - 1)) == 0; } + +okl_long map_at(void* at, okl_uptr len, int prot, int extra) { + return okl::sys(okl::nr_mmap, reinterpret_cast(at), static_cast(len), prot, + okl::map_private | okl::map_anonymous | extra, -1, 0); +} + +} // namespace + +extern "C" { + +void* kal_region_reserve(kal_uintptr size, kal_uintptr align, kal_uintptr flags) { + const okl_uptr g = granularity(); + if (size == 0 || !multiple(size, g) || align < g || (align & (align - 1)) != 0) return nullptr; + if ((flags & ~KAL_REGION_COMMIT) != 0) return nullptr; + const bool usable = !strict() || (flags & KAL_REGION_COMMIT) != 0; + const int prot = usable ? (okl::prot_read | okl::prot_write) : okl::prot_none; + const int extra = strict() ? 0 : okl::map_noreserve; + // More than asked for, by the alignment less one granule, and the two ends + // that are not needed released: the kernel places a mapping at its own + // granularity and knows nothing of the caller's. + const okl_uptr total = size + align - g; + if (total < size) return nullptr; + const okl_long r = map_at(nullptr, total, prot, extra); + if (okl::failed(r)) return nullptr; + const okl_uptr base = static_cast(r); + const okl_uptr at = (base + align - 1) & ~(align - 1); + if (at > base) okl::sys(okl::nr_munmap, static_cast(base), static_cast(at - base)); + const okl_uptr end = at + size, mapped_end = base + total; + if (mapped_end > end) okl::sys(okl::nr_munmap, static_cast(end), static_cast(mapped_end - end)); + return reinterpret_cast(at); +} + +int kal_region_commit(void* addr, kal_uintptr len) { + const okl_uptr g = granularity(), a = reinterpret_cast(addr); + if (addr == nullptr || !multiple(a, g) || !multiple(len, g)) return kal_err_invalid; + if (!strict() || len == 0) return kal_ok; + const okl_long r = okl::sys(okl::nr_mprotect, static_cast(a), static_cast(len), + okl::prot_read | okl::prot_write); + return okl::failed(r) ? okl::translate(r) : kal_ok; +} + +int kal_region_decommit(void* addr, kal_uintptr len) { + const okl_uptr g = granularity(), a = reinterpret_cast(addr); + if (addr == nullptr || !multiple(a, g) || !multiple(len, g)) return kal_err_invalid; + if (len == 0) return kal_ok; + if (strict()) { + const okl_long r = map_at(addr, len, okl::prot_none, okl::map_fixed); + return okl::failed(r) ? okl::translate(r) : kal_ok; + } + const okl_long r = okl::sys(okl::nr_madvise, static_cast(a), static_cast(len), + okl::madv_dontneed); + return okl::failed(r) ? okl::translate(r) : kal_ok; +} + +int kal_region_discard(void* addr, kal_uintptr len) { + const okl_uptr g = granularity(), a = reinterpret_cast(addr); + if (addr == nullptr || !multiple(a, g) || !multiple(len, g)) return kal_err_invalid; + if (len == 0) return kal_ok; + okl_long r = okl::sys(okl::nr_madvise, static_cast(a), static_cast(len), + okl::madv_free); + if (r == -okl::e_inval) + r = okl::sys(okl::nr_madvise, static_cast(a), static_cast(len), + okl::madv_dontneed); + return okl::failed(r) ? okl::translate(r) : kal_ok; +} + +void kal_region_release(void* addr, kal_uintptr size) { + if (addr == nullptr || size == 0) return; + okl::sys(okl::nr_munmap, reinterpret_cast(addr), static_cast(size)); +} + +kal_uintptr kal_region_props(void) { + return strict() ? 0u : KAL_REGION_PROP_OVERCOMMIT; +} + +} diff --git a/src/sys.h b/src/sys.h index a90bc86..571b6ec 100644 --- a/src/sys.h +++ b/src/sys.h @@ -129,6 +129,9 @@ enum : okl_long { // number, how one ended, and the request reproduced. nr_renameat2 = 316, nr_pidfd_open = 434, nr_waitid = 247, nr_rt_sigprocmask = 14, + // openkal 0.20: transfers at an offset, a range's backing returned, and + // whether a file may be written by this program (a control group's). + nr_pread64 = 17, nr_pwrite64 = 18, nr_madvise = 28, nr_faccessat = 269, }; #elif defined(__aarch64__) @@ -225,6 +228,8 @@ enum : okl_long { // openkal 0.17, as on the other architecture. nr_renameat2 = 276, nr_pidfd_open = 434, nr_waitid = 95, nr_rt_sigprocmask = 135, + // openkal 0.20, as on the other architecture. + nr_pread64 = 67, nr_pwrite64 = 68, nr_madvise = 233, nr_faccessat = 48, }; #else @@ -324,6 +329,8 @@ enum : okl_long { at_fdcwd = -100, at_removedir = 0x200, at_symlink_nofollow = 0x100, prot_read = 1, prot_write = 2, prot_exec = 4, prot_none = 0, map_shared = 1, map_private = 2, map_anonymous = 0x20, map_stack = 0x20000, + map_fixed = 0x10, map_noreserve = 0x4000, + madv_dontneed = 4, madv_free = 8, ms_async = 1, clock_monotonic = 1, clock_realtime = 0, futex_wait = 0, futex_wake = 1, futex_private = 128, diff --git a/src/time.cpp b/src/time.cpp index 9135f9d..aa19a9c 100644 --- a/src/time.cpp +++ b/src/time.cpp @@ -1,24 +1,119 @@ #include "sys.h" #include +namespace okl { okl_ulong auxval(okl_ulong key); } + namespace { kal_duration nanoseconds(const okl::ktimespec& t) { return static_cast(t.sec) * 1000000000u + static_cast(t.nsec); } + +// THE CLOCK THE KERNEL PLACES IN EVERY PROGRAM (openkal-linux 0.21). +// +// The kernel maps a small shared object into each program it starts --- the +// vDSO, found through the auxiliary vector (AT_SYSINFO_EHDR) --- whose +// `clock_gettime' reads the clock from a page the kernel keeps current, with no +// system call. A C library calls it; this implementation called the system call +// instead, and a compiler run as an image above it made 23 thousand of them in +// one warm start that its own build made none of. The entry is found once, by +// the object's dynamic symbol table; where there is no such object (a kernel +// started without it, an environment that hides the vector) or no such entry, +// the system call remains, as does an answer the entry declines. +using vdso_clock = int (*)(long, okl::ktimespec*); + +struct elf_ehdr { unsigned char ident[16]; unsigned short type, machine; unsigned version; + okl_u64 entry, phoff, shoff; unsigned flags; + unsigned short ehsize, phentsize, phnum, shentsize, shnum, shstrndx; }; +struct elf_phdr { unsigned type, flags; okl_u64 offset, vaddr, paddr, filesz, memsz, align; }; +struct elf_dyn { long long tag; okl_u64 val; }; +struct elf_sym { unsigned name; unsigned char info, other; unsigned short shndx; okl_u64 value, size; }; + +#if defined(__x86_64__) +constexpr const char kClockName[] = "__vdso_clock_gettime"; +#else +constexpr const char kClockName[] = "__kernel_clock_gettime"; +#endif + +bool same_name(const char* a, const char* b) { + while (*a && *a == *b) { ++a; ++b; } + return *a == *b; +} + +// Symbols in a table hashed in the GNU form: the highest index any chain +// reaches, plus one, is the count; the form states no count of its own. +okl_u64 gnu_symbol_count(const unsigned* h) { + const unsigned nbuckets = h[0], symoffset = h[1], bloom = h[2]; + const auto* buckets = h + 4 + bloom * (sizeof(okl_u64) / sizeof(unsigned)); + const unsigned* chain = buckets + nbuckets; + unsigned last = 0; + for (unsigned i = 0; i < nbuckets; ++i) if (buckets[i] > last) last = buckets[i]; + if (last < symoffset) return symoffset; + while ((chain[last - symoffset] & 1u) == 0) ++last; + return static_cast(last) + 1; +} + +vdso_clock find_vdso_clock() { + const okl_ulong base = okl::auxval(33 /* AT_SYSINFO_EHDR */); + if (base == 0) return nullptr; + const auto* eh = reinterpret_cast(base); + if (eh->ident[0] != 0x7f || eh->ident[1] != 'E' || eh->ident[4] != 2 /* ELFCLASS64 */) return nullptr; + const auto* ph = reinterpret_cast(base + eh->phoff); + okl_u64 load = 0; bool have_load = false; + const elf_dyn* dyn = nullptr; + for (unsigned i = 0; i < eh->phnum; ++i) { + if (ph[i].type == 1 /* PT_LOAD */ && !have_load) { load = base + ph[i].offset - ph[i].vaddr; have_load = true; } + if (ph[i].type == 2 /* PT_DYNAMIC */) dyn = reinterpret_cast(base + ph[i].offset); + } + if (!have_load || dyn == nullptr) return nullptr; + const char* strtab = nullptr; const elf_sym* symtab = nullptr; + const unsigned* hash = nullptr; const unsigned* gnu_hash = nullptr; + for (const elf_dyn* d = dyn; d->tag != 0; ++d) { + switch (d->tag) { + case 5: strtab = reinterpret_cast(load + d->val); break; + case 6: symtab = reinterpret_cast(load + d->val); break; + case 4: hash = reinterpret_cast(load + d->val); break; + case 0x6ffffef5: gnu_hash = reinterpret_cast(load + d->val); break; + default: break; + } + } + if (strtab == nullptr || symtab == nullptr || (hash == nullptr && gnu_hash == nullptr)) return nullptr; + const okl_u64 count = hash ? hash[1] : gnu_symbol_count(gnu_hash); + for (okl_u64 i = 0; i < count; ++i) { + const elf_sym& s = symtab[i]; + if ((s.info & 0xf) != 2 /* STT_FUNC */ || s.shndx == 0) continue; + if ((s.info >> 4) != 1 /* STB_GLOBAL */ && (s.info >> 4) != 2 /* STB_WEAK */) continue; + if (same_name(strtab + s.name, kClockName)) return reinterpret_cast(load + s.value); + } + return nullptr; +} + +// 0 not yet looked for; 1 looked for and absent; otherwise the entry. +okl_uptr g_vdso_clock = 0; + +void read_clock(long clock, okl::ktimespec* t) { + okl_uptr v = __atomic_load_n(&g_vdso_clock, __ATOMIC_ACQUIRE); + if (v == 0) { + const vdso_clock found = find_vdso_clock(); + v = found ? reinterpret_cast(found) : 1; + __atomic_store_n(&g_vdso_clock, v, __ATOMIC_RELEASE); + } + if (v != 1 && reinterpret_cast(v)(clock, t) == 0) return; + okl::sys(okl::nr_clock_gettime, clock, reinterpret_cast(t)); +} } // namespace extern "C" { kal_duration kal_time_monotonic(void) { okl::ktimespec t{}; - okl::sys(okl::nr_clock_gettime, okl::clock_monotonic, reinterpret_cast(&t)); + read_clock(okl::clock_monotonic, &t); return nanoseconds(t); } kal_duration kal_time_wall(void) { okl::ktimespec t{}; - okl::sys(okl::nr_clock_gettime, okl::clock_realtime, reinterpret_cast(&t)); + read_clock(okl::clock_realtime, &t); return nanoseconds(t); } diff --git a/src/timeout.cpp b/src/timeout.cpp index 7870b9d..e21aba3 100644 --- a/src/timeout.cpp +++ b/src/timeout.cpp @@ -2,6 +2,9 @@ #include "handle.h" #include "endpoint.h" #include +#include + +namespace okl { int child_outcome(okl_long pid, bool block, int* status, int* terminated, bool& ended); } // openkal.timeout upon ppoll(2) and wait4(2). // @@ -126,41 +129,39 @@ kal_intptr kal_timeout_recv_from(kal_datagram d, void* buf, kal_uintptr len, int kal_timeout_wait_process(kal_process p, kal_u64 ns, int* status, int* terminated) { if (p.h == 0) return kal_err_invalid; - - // WNOHANG AND A POLLING LOOP, BECAUSE THE KERNEL HAS NO BOUNDED WAIT FOR A - // CHILD. wait4 either blocks or does not wait at all; there is no timespec. - // - // The alternative is a signal handler for SIGCHLD, which this implementation - // does not have and would not want: a handler is process-wide state, and an - // implementation that installed one would take a facility away from the - // program above it. The loop is what the environment permits, and the - // interval is bounded below by the granularity this interface reports so - // that the polling cost is stated rather than hidden. - constexpr okl_u64 interval_ns = 1000000ull; // one millisecond + const okl_long pid = static_cast(p.h & 0xffffffffu); + + // THE PROGRAM'S OWN DESCRIPTOR, WHERE THE KERNEL GIVES ONE (5.3 and later, + // openkal-linux 0.21): it becomes readable when the program ends, so the + // wait is the kernel's own bounded wait upon it rather than a loop. Where + // there is none, the loop below remains --- `wait4' either blocks or does + // not wait at all, and a handler for SIGCHLD would take a facility away + // from the program above. Either way the program is not collected here; + // `kal_process_close' collects it, as after `kal_process_wait'. + const okl_long fd = okl::sys(okl::nr_pidfd_open, pid, 0); + constexpr okl_u64 interval_ns = 1000000ull; // one millisecond, without a descriptor okl_u64 waited = 0; - for (;;) { - int st = 0; - const okl_long r = okl::sys(okl::nr_wait4, static_cast(p.h), - reinterpret_cast(&st), - 1 /* WNOHANG */, 0); - if (okl::interrupted(r)) continue; - if (okl::failed(r)) return okl::translate(r); - - if (r != 0) { - const int signalled = st & 0x7f; - if (signalled == 0) { - if (status) *status = (st >> 8) & 0xff; - if (terminated) *terminated = 0; - } else { - if (status) *status = signalled; - if (terminated) *terminated = 1; - } - return kal_ok; + bool ended = false; + const int e = okl::child_outcome(pid, false, status, terminated, ended); + if (e != kal_ok || ended) { + if (!okl::failed(fd)) okl::sys(okl::nr_close, fd); + return e; + } + if (ns != 0 && waited >= ns) { + if (!okl::failed(fd)) okl::sys(okl::nr_close, fd); + return kal_err_again; + } + if (!okl::failed(fd)) { + const okl_u64 left = ns == 0 ? 0 : ns - waited; + struct { int fd; short events, revents; } pfd{ static_cast(fd), 1 /* POLLIN */, 0 }; + okl::ktimespec ts{ static_cast(left / 1000000000u), static_cast(left % 1000000000u) }; + const okl_u64 t0 = kal_time_monotonic(); + okl::sys(okl::nr_ppoll, reinterpret_cast(&pfd), 1, + ns == 0 ? 0 : reinterpret_cast(&ts), 0, 0); + waited += kal_time_monotonic() - t0; + continue; } - - if (ns != 0 && waited >= ns) return kal_err_again; - okl::ktimespec ts{ 0, static_cast(interval_ns) }; okl::sys(okl::nr_nanosleep, reinterpret_cast(&ts), 0); waited += interval_ns; diff --git a/src/version.cpp b/src/version.cpp index ba04fdd..9aab1b8 100644 --- a/src/version.cpp +++ b/src/version.cpp @@ -24,7 +24,7 @@ kal_u64 kal_interfaces(void) { | KAL_IFACE_FS | KAL_IFACE_PROCESS | KAL_IFACE_TASK | KAL_IFACE_EXEC | KAL_IFACE_TERMINAL | KAL_IFACE_NET | KAL_IFACE_DATAGRAM | KAL_IFACE_SPACE | KAL_IFACE_TIMEOUT - | KAL_IFACE_MAP; + | KAL_IFACE_MAP | KAL_IFACE_REGION; } } diff --git a/tests/openkal_0_20.cpp b/tests/openkal_0_20.cpp new file mode 100644 index 0000000..2c32f24 --- /dev/null +++ b/tests/openkal_0_20.cpp @@ -0,0 +1,114 @@ +// What openkal 0.20 adds, as this kernel does it (openkal-linux 0.21). +// +// TRANSFERS AT AN OFFSET FROM SEVERAL CONTEXTS AT ONCE, beside a sequential +// reader of the same file: four contexts read at offsets while the first reads +// the file from start to end through its stream. Composed from a seek, a read +// and a seek back, a transfer at an offset moves the position the sequential +// reader depends on, and the sequential reader then skips or repeats bytes. +// +// THE CLOCK WITHOUT A SYSTEM CALL keeps the clock's properties: the monotonic +// clock never goes back across many reads from two contexts, and the wall clock +// agrees with the kernel's to within a second. +import openkal.fs; +import openkal.stream; +import openkal.task; +import openkal.time; +import openkal.types; + +namespace { +int failures = 0; +void check(bool ok, const char* what) { + if (ok) return; + ++failures; + const char pre[] = "FAIL: "; + kal::write(kal::err(), pre, sizeof(pre) - 1); + kal_uintptr n = 0; while (what[n]) ++n; + kal::write(kal::err(), what, n); + kal::write(kal::err(), "\n", 1); +} + +constexpr kal_uintptr kSize = 1u << 16; +constexpr char kName[] = "openkal-0-20-at-offsets.tmp"; + +unsigned char byte_at(kal_u64 i) { return static_cast(i * 131u + (i >> 8)); } + +struct reader { kal_file f; int rounds; int bad; }; + +void read_at_offsets(void* arg) { + auto* r = static_cast(arg); + unsigned seed = 12345u + static_cast(reinterpret_cast(arg) >> 4); + unsigned char buf[97]; + for (int i = 0; i < r->rounds; ++i) { + seed = seed * 1103515245u + 12345u; + const kal_u64 at = (seed >> 8) % (kSize - sizeof buf); + const kal_intptr n = kal_fs_read_at(r->f, buf, sizeof buf, at); + if (n != static_cast(sizeof buf)) { ++r->bad; continue; } + for (kal_uintptr k = 0; k < sizeof buf; ++k) if (buf[k] != byte_at(at + k)) { ++r->bad; break; } + } +} + +struct clock_reader { int rounds; int backwards; }; + +void read_clock(void* arg) { + auto* c = static_cast(arg); + kal_duration last = kal_time_monotonic(); + for (int i = 0; i < c->rounds; ++i) { + const kal_duration now = kal_time_monotonic(); + if (now < last) ++c->backwards; + last = now; + } +} +} // namespace + +int main() { + const kal_dir here = kal::fs::working(); + { + kal_file f{}; + const auto flags = kal::fs::open::read | kal::fs::open::write + | kal::fs::open::create | kal::fs::open::truncate; + check(kal::fs::open_file(here, kName, sizeof kName - 1, flags, &f) == kal_ok, "the file is made"); + static unsigned char block[kSize]; + for (kal_uintptr i = 0; i < kSize; ++i) block[i] = byte_at(i); + check(kal_fs_write_at(f, block, kSize, 0) == static_cast(kSize), + "the file is written at offset zero, whole"); + kal_u64 at = 1; + check(kal_fs_seek(f, 0, kal::fs::seek_current, &at) == kal_ok && at == 0, + "and its position is still zero"); + + reader r[4]{ { f, 4000, 0 }, { f, 4000, 0 }, { f, 4000, 0 }, { f, 4000, 0 } }; + kal_task t[4]{}; + for (int i = 0; i < 4; ++i) check(kal_task_start(read_at_offsets, &r[i], &t[i]) == kal_ok, "a reader starts"); + // The sequential reader, meanwhile. + kal_u64 seen = 0; + int out_of_order = 0; + unsigned char chunk[251]; + for (;;) { + const kal_intptr n = kal_stream_read(kal_fs_stream(f), chunk, sizeof chunk); + if (n <= 0) break; + for (kal_intptr k = 0; k < n; ++k) if (chunk[k] != byte_at(seen + static_cast(k))) ++out_of_order; + seen += static_cast(n); + } + for (int i = 0; i < 4; ++i) kal_task_join(t[i]); + check(seen == kSize && out_of_order == 0, + "a sequential reader reads every byte in order while others read at offsets"); + int bad = 0; + for (const auto& x : r) bad += x.bad; + check(bad == 0, "every read at an offset reads the bytes at that offset"); + kal_fs_close_file(f); + kal_fs_remove(here, kName, sizeof kName - 1); + } + { + clock_reader c[2]{ { 200000, 0 }, { 200000, 0 } }; + kal_task t{}; + check(kal_task_start(read_clock, &c[1], &t) == kal_ok, "a clock reader starts"); + read_clock(&c[0]); + kal_task_join(t); + check(c[0].backwards == 0 && c[1].backwards == 0, "the monotonic clock never goes back"); + const kal_duration a = kal_time_wall(); + kal_time_sleep(20u * 1000u * 1000u); + const kal_duration b = kal_time_wall(); + check(b > a && b - a >= 19u * 1000u * 1000u && b - a < 1000u * 1000u * 1000u, + "the wall clock advances with a sleep, and by about as much"); + } + return failures == 0 ? 0 : 1; +}