Operating Systems Module 1 – Foundations: what an operating system does
System calls: the kernel's front door
How a system call travels from a C function through a trap and the kernel's table, how failures come back as errno, and what one costs, measured.
What you will learn
- Explain how a system call travels from a C library function through a trap and the kernel's table, and back
- Use return values and errno correctly when a system call fails
- Classify system calls into process, file, device, information, communication and protection groups
- Explain where Linux and macOS each keep the stable interface that programs rely on
- Measure what a system call costs and explain why some calls never enter the kernel
Before you start
What you’ll learn · Before you start
What you will learn
- Explain how a system call travels from a C library function through a trap and the kernel's table, and back
- Use return values and errno correctly when a system call fails
- Classify system calls into process, file, device, information, communication and protection groups
- Explain where Linux and macOS each keep the stable interface that programs rely on
- Measure what a system call costs and explain why some calls never enter the kernel
Before you start
On this page
On the machine that ran this lesson’s programs, asking the kernel for a process’s parent took well over 100 nanoseconds, while reading the clock took tens and an ordinary function call about one. Every request a program makes to the kernel, to open a file, start a process or send a packet, pays a cost like the first, because it travels the same narrow path: the system call.
This lesson follows that path from a C library function through the trap and the kernel’s table and back, shows how a failed call reports its error, sorts the calls into six groups, and explains where Linux and macOS each keep the interface that programs can rely on. It ends by measuring the cost and the calls that avoid it.
From a C function to the kernel and back
A program never calls the kernel’s code directly. It calls a function of the C library, such as write, and the
library’s wrapper does the rest: it puts the system call’s number and arguments into registers and executes the
trap instruction of the processor, which syscall(2) lists for each architecture: svc on Arm, syscall on x86-64,
ecall on RISC-V. In the kernel, the number selects a handler from the
system-call table: The kernel's table of handlers, indexed by system-call number. A trap with a number the table does not have is refused with ENOSYS.. Follow one call through it:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The call. The program calls write with descriptor 1, a buffer and a length of 6. So far this is an ordinary function call into the C library, still in user mode.
- The wrapper. The library's wrapper puts the system call's number and the three arguments into the registers that syscall(2) lists for the processor, then executes the trap instruction, svc on Arm or syscall on x86-64.
- Into the kernel. The trap switches the processor to kernel mode and jumps to the kernel's entry point for system calls. The program cannot choose any other address.
- The table. The kernel saves the program's registers and uses the number to pick the write handler from its system-call table. A number with no entry gets the error ENOSYS.
- The checks. The handler checks that descriptor 1 is open for writing and that the 6 bytes lie in memory the process may read, then copies them in. A bad descriptor gets EBADF, a bad address EFAULT.
- The work and the result. The bytes go to the terminal or file behind descriptor 1. The handler leaves the result in the return register, 6 here, or minus an error number when something failed, and the kernel returns to user mode.
- errno. Back in the wrapper, a negative result would become -1, with its absolute value stored in errno. This one is 6, so write returns 6 and leaves errno alone.
The kernel trusts nothing that arrives: the number is checked against the table, the descriptor against the process’s
open files, the buffer against its address space. On Linux the result comes back in a register, and intro(2) gives
the convention: on error most system calls return a negative error number, and the wrapper copies its absolute value
into errno and returns −1. The model below plays the whole path in 14 languages, with five calls that succeed or
fail in different ways:
Python 3.14 · main.py
EBADF, EFAULT, ENOSYS = 9, 14, 38 # Linux's numbers
screen: list[str] = [] # what standard output received
def sys_write(fd: int, buf: str | None, count: int) -> int:
"""The kernel checks every argument first."""
if fd != 1: # only descriptor 1 is open
return -EBADF
if buf is None: # an address the process cannot read
return -EFAULT
screen.append(buf[:count])
return count
def sys_getpid(fd: int, buf: str | None, count: int) -> int:
return 4242 # the process's ID
TABLE = [sys_write, sys_getpid] # call 0 is write, 1 is getpid
def trap(number: int, fd: int, buf: str | None, count: int) -> int:
"""Kernel mode: find the handler."""
if not 0 <= number < len(TABLE):
return -ENOSYS
return TABLE[number](fd, buf, count)
def wrapper(call: str, number: int, fd: int, buf: str | None, count: int) -> int:
"""The C library: a negative result means -1 and errno."""
raw = trap(number, fd, buf, count)
if raw < 0:
print(f"{call}: kernel returns {raw}, wrapper returns -1 with errno {-raw}")
return -1
print(f"{call}: kernel returns {raw}, wrapper returns {raw}")
return raw
wrapper('write(1, "hello", 5)', 0, 1, "hello", 5)
wrapper('write(7, "hi", 2)', 0, 7, "hi", 2)
wrapper("write(1, bad address, 4)", 0, 1, None, 4)
wrapper("getpid()", 1, 0, "", 0)
wrapper("call number 99", 99, 0, "", 0)
print(f"standard output received: {''.join(screen)}") In Python: The table is a list of functions, so TABLE[number] picks the handler; None stands for an address the process cannot read.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
// Linux's numbers
const EBADF = 9;
const EFAULT = 14;
const ENOSYS = 38;
let screen = ""; // what standard output received
// The kernel checks every argument first.
function sysWrite(fd, buf, count) {
if (fd !== 1) return -EBADF; // only descriptor 1 is open
if (buf === null) return -EFAULT; // an address the process cannot read
screen += buf.slice(0, count);
return count;
}
const sysGetpid = () => 4242; // the process's ID
const TABLE = [sysWrite, sysGetpid]; // call 0 is write, 1 is getpid
// Kernel mode: find the handler.
function trap(number, fd, buf, count) {
if (number < 0 || number >= TABLE.length) return -ENOSYS;
return TABLE[number](fd, buf, count);
}
// The C library: a negative result means -1 and errno.
function wrapper(call, number, fd, buf, count) {
const raw = trap(number, fd, buf, count);
if (raw < 0) {
console.log(`${call}: kernel returns ${raw}, wrapper returns -1 with errno ${-raw}`);
return -1;
}
console.log(`${call}: kernel returns ${raw}, wrapper returns ${raw}`);
return raw;
}
wrapper('write(1, "hello", 5)', 0, 1, "hello", 5);
wrapper('write(7, "hi", 2)', 0, 7, "hi", 2);
wrapper("write(1, bad address, 4)", 0, 1, null, 4);
wrapper("getpid()", 1, 0, "", 0);
wrapper("call number 99", 99, 0, "", 0);
console.log(`standard output received: ${screen}`); In JavaScript: Functions are values, so the table is an array of them; null stands for an address the process cannot read.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.List;
public class Main {
static final int EBADF = 9; // Linux's numbers
static final int EFAULT = 14;
static final int ENOSYS = 38;
static final StringBuilder SCREEN = new StringBuilder(); // what standard output received
interface Handler {
int handle(int fd, String buf, int count);
}
/** The kernel checks every argument first. */
static int sysWrite(int fd, String buf, int count) {
if (fd != 1) { // only descriptor 1 is open
return -EBADF;
}
if (buf == null) { // an address the process cannot read
return -EFAULT;
}
SCREEN.append(buf, 0, count);
return count;
}
static int sysGetpid(int fd, String buf, int count) {
return 4242; // the process's ID
}
static final List<Handler> TABLE = List.of(Main::sysWrite, Main::sysGetpid); // 0 and 1
/** Kernel mode: find the handler. */
static int trap(int number, int fd, String buf, int count) {
if (number < 0 || number >= TABLE.size()) {
return -ENOSYS;
}
return TABLE.get(number).handle(fd, buf, count);
}
/** The C library: a negative result means -1 and errno. */
static int wrapper(String call, int number, int fd, String buf, int count) {
int raw = trap(number, fd, buf, count);
if (raw < 0) {
System.out.println(
call + ": kernel returns " + raw + ", wrapper returns -1 with errno " + -raw);
return -1;
}
System.out.println(call + ": kernel returns " + raw + ", wrapper returns " + raw);
return raw;
}
public static void main(String[] args) {
wrapper("write(1, \"hello\", 5)", 0, 1, "hello", 5);
wrapper("write(7, \"hi\", 2)", 0, 7, "hi", 2);
wrapper("write(1, bad address, 4)", 0, 1, null, 4);
wrapper("getpid()", 1, 0, "", 0);
wrapper("call number 99", 99, 0, "", 0);
System.out.println("standard output received: " + SCREEN);
}
} In Java: The Handler interface lets method references such as Main::sysWrite go into one list.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <array>
#include <optional>
#include <print>
#include <string>
// EBADF, EFAULT and ENOSYS (Linux's numbers); <errno.h> takes their names
namespace err {
constexpr int BADF = 9;
constexpr int FAULT = 14;
constexpr int NOSYS = 38;
} // namespace err
std::string screen; // what standard output received
using Buffer = std::optional<std::string>; // empty: an address it cannot read
using Handler = int (*)(int fd, const Buffer &buf, int count);
// The kernel checks every argument first.
int sys_write(int fd, const Buffer &buf, int count) {
if (fd != 1) { // only descriptor 1 is open
return -err::BADF;
}
if (!buf) {
return -err::FAULT;
}
screen += buf->substr(0, count);
return count;
}
int sys_getpid(int, const Buffer &, int) {
return 4242; // the process's ID
}
constexpr std::array<Handler, 2> TABLE = {sys_write, sys_getpid}; // 0 and 1
// Kernel mode: find the handler.
int trap(int number, int fd, const Buffer &buf, int count) {
if (number < 0 || number >= static_cast<int>(TABLE.size())) {
return -err::NOSYS;
}
return TABLE[number](fd, buf, count);
}
// The C library: a negative result means -1 and errno.
int wrapper(const std::string &call, int number, int fd, const Buffer &buf,
int count) {
const int raw = trap(number, fd, buf, count);
if (raw < 0) {
std::println("{}: kernel returns {}, wrapper returns -1 with errno {}",
call, raw, -raw);
return -1;
}
std::println("{}: kernel returns {}, wrapper returns {}", call, raw, raw);
return raw;
}
int main() {
wrapper("write(1, \"hello\", 5)", 0, 1, "hello", 5);
wrapper("write(7, \"hi\", 2)", 0, 7, "hi", 2);
wrapper("write(1, bad address, 4)", 0, 1, std::nullopt, 4);
wrapper("getpid()", 1, 0, "", 0);
wrapper("call number 99", 99, 0, "", 0);
std::println("standard output received: {}", screen);
} In C++: <errno.h> already defines EBADF, EFAULT and ENOSYS as macros, so the model names its constants err::BADF and so on; an empty std::optional is the bad address.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
using System.Globalization;
using System.Text;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
const int EBADF = 9; // Linux's numbers
const int EFAULT = 14;
const int ENOSYS = 38;
var screen = new StringBuilder(); // what standard output received
// The kernel checks every argument first.
int SysWrite(int fd, string? buf, int count)
{
if (fd != 1) // only descriptor 1 is open
{
return -EBADF;
}
if (buf is null) // an address the process cannot read
{
return -EFAULT;
}
screen.Append(buf, 0, count);
return count;
}
static int SysGetpid(int fd, string? buf, int count) => 4242; // the process's ID
Func<int, string?, int, int>[] table = [SysWrite, SysGetpid]; // call 0 is write, 1 is getpid
// Kernel mode: find the handler.
int Trap(int number, int fd, string? buf, int count) =>
number >= 0 && number < table.Length ? table[number](fd, buf, count) : -ENOSYS;
// The C library: a negative result means -1 and errno.
int Wrapper(string call, int number, int fd, string? buf, int count)
{
int raw = Trap(number, fd, buf, count);
if (raw < 0)
{
Console.WriteLine($"{call}: kernel returns {raw}, wrapper returns -1 with errno {-raw}");
return -1;
}
Console.WriteLine($"{call}: kernel returns {raw}, wrapper returns {raw}");
return raw;
}
Wrapper("write(1, \"hello\", 5)", 0, 1, "hello", 5);
Wrapper("write(7, \"hi\", 2)", 0, 7, "hi", 2);
Wrapper("write(1, bad address, 4)", 0, 1, null, 4);
Wrapper("getpid()", 1, 0, "", 0);
Wrapper("call number 99", 99, 0, "", 0);
Console.WriteLine($"standard output received: {screen}"); In C#: The handlers are local functions held in an array of Func delegates; SysWrite is not static because it appends to screen.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stddef.h>
#include <stdio.h>
#include <string.h>
// EBADF, EFAULT and ENOSYS (Linux's numbers); <errno.h> takes their names
enum { ERR_BADF = 9, ERR_FAULT = 14, ERR_NOSYS = 38 };
static char screen[64]; // what standard output received
// The kernel checks every argument first.
static int sys_write(int fd, const char *buf, int count) {
if (fd != 1) { // only descriptor 1 is open
return -ERR_BADF;
}
if (buf == NULL) { // an address the process cannot read
return -ERR_FAULT;
}
strncat(screen, buf, (size_t)count);
return count;
}
static int sys_getpid(int fd, const char *buf, int count) {
(void)fd, (void)buf, (void)count;
return 4242; // the process's ID
}
typedef int (*Handler)(int fd, const char *buf, int count);
static const Handler TABLE[] = {sys_write, sys_getpid}; // 0 and 1
// Kernel mode: find the handler.
static int trap(int number, int fd, const char *buf, int count) {
if (number < 0 || (size_t)number >= sizeof TABLE / sizeof TABLE[0]) {
return -ERR_NOSYS;
}
return TABLE[number](fd, buf, count);
}
// The C library: a negative result means -1 and errno.
static int wrapper(const char *call, int number, int fd, const char *buf,
int count) {
int raw = trap(number, fd, buf, count);
if (raw < 0) {
printf("%s: kernel returns %d, wrapper returns -1 with errno %d\n", call,
raw, -raw);
return -1;
}
printf("%s: kernel returns %d, wrapper returns %d\n", call, raw, raw);
return raw;
}
int main(void) {
wrapper("write(1, \"hello\", 5)", 0, 1, "hello", 5);
wrapper("write(7, \"hi\", 2)", 0, 7, "hi", 2);
wrapper("write(1, bad address, 4)", 0, 1, NULL, 4);
wrapper("getpid()", 1, 0, "", 0);
wrapper("call number 99", 99, 0, "", 0);
printf("standard output received: %s\n", screen);
return 0;
} In C: <errno.h> already defines EBADF, EFAULT and ENOSYS as macros, so the model calls its constants ERR_BADF and so on; the table is an array of function pointers.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
const EBADF = 9; // Linux's numbers
const EFAULT = 14;
const ENOSYS = 38;
final class Kernel
{
public static string $screen = ''; // what standard output received
/** The kernel checks every argument first. */
public static function write(int $fd, ?string $buf, int $count): int
{
if ($fd !== 1) { // only descriptor 1 is open
return -EBADF;
}
if ($buf === null) { // an address the process cannot read
return -EFAULT;
}
self::$screen .= substr($buf, 0, $count);
return $count;
}
public static function getpid(int $fd, ?string $buf, int $count): int
{
return 4242; // the process's ID
}
/** Kernel mode: find the handler. */
public static function trap(int $number, int $fd, ?string $buf, int $count): int
{
$table = [self::write(...), self::getpid(...)]; // call 0 is write, 1 is getpid
return isset($table[$number]) ? $table[$number]($fd, $buf, $count) : -ENOSYS;
}
}
/** The C library: a negative result means -1 and errno. */
function wrapper(string $call, int $number, int $fd, ?string $buf, int $count): int
{
$raw = Kernel::trap($number, $fd, $buf, $count);
if ($raw < 0) {
echo "{$call}: kernel returns {$raw}, wrapper returns -1 with errno " . -$raw . "\n";
return -1;
}
echo "{$call}: kernel returns {$raw}, wrapper returns {$raw}\n";
return $raw;
}
wrapper('write(1, "hello", 5)', 0, 1, 'hello', 5);
wrapper('write(7, "hi", 2)', 0, 7, 'hi', 2);
wrapper('write(1, bad address, 4)', 0, 1, null, 4);
wrapper('getpid()', 1, 0, '', 0);
wrapper('call number 99', 99, 0, '', 0);
echo 'standard output received: ' . Kernel::$screen . "\n"; In PHP: The model kernel is a final class with static methods, and write(...) makes a first-class callable out of a method.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
const EBADF: i32 = 9; // Linux's numbers
const EFAULT: i32 = 14;
const ENOSYS: i32 = 38;
/// None stands for an address the process cannot read; the last argument is standard output.
type Handler = fn(i32, Option<&str>, usize, &mut String) -> i32;
/// The kernel checks every argument first.
fn sys_write(fd: i32, buf: Option<&str>, count: usize, screen: &mut String) -> i32 {
if fd != 1 {
return -EBADF; // only descriptor 1 is open
}
let Some(buf) = buf else {
return -EFAULT;
};
screen.push_str(&buf[..count]);
count as i32
}
fn sys_getpid(_: i32, _: Option<&str>, _: usize, _: &mut String) -> i32 {
4242 // the process's ID
}
const TABLE: [Handler; 2] = [sys_write, sys_getpid]; // call 0 is write, 1 is getpid
/// Kernel mode: find the handler.
fn trap(number: usize, fd: i32, buf: Option<&str>, count: usize, screen: &mut String) -> i32 {
match TABLE.get(number) {
Some(handler) => handler(fd, buf, count, screen),
None => -ENOSYS,
}
}
/// The C library: a negative result means -1 and errno.
fn wrapper(call: &str, number: usize, args: (i32, Option<&str>, usize), screen: &mut String) {
let raw = trap(number, args.0, args.1, args.2, screen);
if raw < 0 {
println!(
"{call}: kernel returns {raw}, wrapper returns -1 with errno {}",
-raw
);
} else {
println!("{call}: kernel returns {raw}, wrapper returns {raw}");
}
}
fn main() {
let mut screen = String::new(); // what standard output received
wrapper(
r#"write(1, "hello", 5)"#,
0,
(1, Some("hello"), 5),
&mut screen,
);
wrapper(r#"write(7, "hi", 2)"#, 0, (7, Some("hi"), 2), &mut screen);
wrapper("write(1, bad address, 4)", 0, (1, None, 4), &mut screen);
wrapper("getpid()", 1, (0, Some(""), 0), &mut screen);
wrapper("call number 99", 99, (0, Some(""), 0), &mut screen);
println!("standard output received: {screen}");
} In Rust: Standard output is a String passed down to the handlers rather than a global, as Rust prefers; TABLE.get returns None for a number past the end.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
type Handler = (fd: number, buf: string | null, count: number) => number;
// Linux's numbers
const EBADF = 9;
const EFAULT = 14;
const ENOSYS = 38;
let screen = ""; // what standard output received
// The kernel checks every argument first.
const sysWrite: Handler = (fd, buf, count) => {
if (fd !== 1) return -EBADF; // only descriptor 1 is open
if (buf === null) return -EFAULT; // an address the process cannot read
screen += buf.slice(0, count);
return count;
};
const sysGetpid: Handler = () => 4242; // the process's ID
const TABLE: Handler[] = [sysWrite, sysGetpid]; // call 0 is write, 1 is getpid
// Kernel mode: find the handler.
function trap(number: number, fd: number, buf: string | null, count: number): number {
const handler = TABLE[number];
return handler === undefined ? -ENOSYS : handler(fd, buf, count);
}
// The C library: a negative result means -1 and errno.
function wrapper(call: string, number: number, fd: number, buf: string | null, count: number) {
const raw = trap(number, fd, buf, count);
if (raw < 0) {
console.log(`${call}: kernel returns ${raw}, wrapper returns -1 with errno ${-raw}`);
return -1;
}
console.log(`${call}: kernel returns ${raw}, wrapper returns ${raw}`);
return raw;
}
wrapper('write(1, "hello", 5)', 0, 1, "hello", 5);
wrapper('write(7, "hi", 2)', 0, 7, "hi", 2);
wrapper("write(1, bad address, 4)", 0, 1, null, 4);
wrapper("getpid()", 1, 0, "", 0);
wrapper("call number 99", 99, 0, "", 0);
console.log(`standard output received: ${screen}`); In TypeScript: Indexing past the end of the array gives undefined, which trap turns into ENOSYS.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import (
"fmt"
"strings"
)
// Linux's numbers
const (
ebadf = 9
efault = 14
enosys = 38
)
var screen strings.Builder // what standard output received
// sysWrite checks every argument first. A nil buf is an address the process cannot read.
func sysWrite(fd int, buf *string, count int) int {
if fd != 1 { // only descriptor 1 is open
return -ebadf
}
if buf == nil {
return -efault
}
screen.WriteString((*buf)[:count])
return count
}
func sysGetpid(int, *string, int) int {
return 4242 // the process's ID
}
var table = []func(int, *string, int) int{sysWrite, sysGetpid} // call 0 is write, 1 is getpid
// trap is kernel mode: find the handler.
func trap(number, fd int, buf *string, count int) int {
if number < 0 || number >= len(table) {
return -enosys
}
return table[number](fd, buf, count)
}
// wrapper is the C library: a negative result means -1 and errno.
func wrapper(call string, number, fd int, buf *string, count int) int {
raw := trap(number, fd, buf, count)
if raw < 0 {
fmt.Printf("%s: kernel returns %d, wrapper returns -1 with errno %d\n", call, raw, -raw)
return -1
}
fmt.Printf("%s: kernel returns %d, wrapper returns %d\n", call, raw, raw)
return raw
}
func main() {
hello, hi, empty := "hello", "hi", ""
wrapper(`write(1, "hello", 5)`, 0, 1, &hello, 5)
wrapper(`write(7, "hi", 2)`, 0, 7, &hi, 2)
wrapper("write(1, bad address, 4)", 0, 1, nil, 4)
wrapper("getpid()", 1, 0, &empty, 0)
wrapper("call number 99", 99, 0, &empty, 0)
fmt.Println("standard output received: " + screen.String())
} In Go: A nil *string stands for the bad address, and the table is a slice of function values.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
let ebadf = 9 // Linux's numbers
let efault = 14
let enosys = 38
var screen = "" // what standard output received
/// The kernel checks every argument first.
func sysWrite(_ fd: Int, _ buf: String?, _ count: Int) -> Int {
guard fd == 1 else { return -ebadf } // only descriptor 1 is open
guard let buf else { return -efault } // an address the process cannot read
screen += buf.prefix(count)
return count
}
func sysGetpid(_ fd: Int, _ buf: String?, _ count: Int) -> Int {
4242 // the process's ID
}
let table: [(Int, String?, Int) -> Int] = [sysWrite, sysGetpid] // call 0 is write, 1 is getpid
/// Kernel mode: find the handler.
func trap(_ number: Int, _ fd: Int, _ buf: String?, _ count: Int) -> Int {
table.indices.contains(number) ? table[number](fd, buf, count) : -enosys
}
/// The C library: a negative result means -1 and errno.
@discardableResult
func wrapper(_ call: String, _ number: Int, _ fd: Int, _ buf: String?, _ count: Int) -> Int {
let raw = trap(number, fd, buf, count)
guard raw >= 0 else {
print("\(call): kernel returns \(raw), wrapper returns -1 with errno \(-raw)")
return -1
}
print("\(call): kernel returns \(raw), wrapper returns \(raw)")
return raw
}
wrapper("write(1, \"hello\", 5)", 0, 1, "hello", 5)
wrapper("write(7, \"hi\", 2)", 0, 7, "hi", 2)
wrapper("write(1, bad address, 4)", 0, 1, nil, 4)
wrapper("getpid()", 1, 0, "", 0)
wrapper("call number 99", 99, 0, "", 0)
print("standard output received: \(screen)") In Swift: guard let unwraps the buffer, and table.indices.contains checks the number before it is used.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
EBADF = 9 # Linux's numbers
EFAULT = 14
ENOSYS = 38
SCREEN = +'' # what standard output received
# The kernel checks every argument first.
def sys_write(descriptor, buffer, count)
return -EBADF unless descriptor == 1 # only descriptor 1 is open
return -EFAULT if buffer.nil? # an address the process cannot read
SCREEN << buffer[0, count]
count
end
def sys_getpid(_descriptor, _buffer, _count)
4242 # the process's ID
end
TABLE = [method(:sys_write), method(:sys_getpid)].freeze # call 0 is write, 1 is getpid
# Kernel mode: find the handler.
def trap(number, descriptor, buffer, count)
return -ENOSYS unless number.between?(0, TABLE.size - 1)
TABLE[number].call(descriptor, buffer, count)
end
# The C library: a negative result means -1 and errno.
def wrapper(call, number, descriptor, buffer, count)
raw = trap(number, descriptor, buffer, count)
if raw.negative?
puts "#{call}: kernel returns #{raw}, wrapper returns -1 with errno #{-raw}"
return -1
end
puts "#{call}: kernel returns #{raw}, wrapper returns #{raw}"
raw
end
wrapper('write(1, "hello", 5)', 0, 1, 'hello', 5)
wrapper('write(7, "hi", 2)', 0, 7, 'hi', 2)
wrapper('write(1, bad address, 4)', 0, 1, nil, 4)
wrapper('getpid()', 1, 0, '', 0)
wrapper('call number 99', 99, 0, '', 0)
puts "standard output received: #{SCREEN}" In Ruby: method(:sys_write) turns each handler into a Method object, so the table can call it.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
const val EBADF = 9 // Linux's numbers
const val EFAULT = 14
const val ENOSYS = 38
val screen = StringBuilder() // what standard output received
// The kernel checks every argument first.
fun sysWrite(
fd: Int,
buf: String?,
count: Int,
): Int {
if (fd != 1) return -EBADF // only descriptor 1 is open
if (buf == null) return -EFAULT // an address the process cannot read
screen.append(buf.take(count))
return count
}
@Suppress("UNUSED_PARAMETER")
fun sysGetpid(
fd: Int,
buf: String?,
count: Int,
): Int = 4242 // the process's ID
val table: List<(Int, String?, Int) -> Int> = listOf(::sysWrite, ::sysGetpid) // 0 and 1
// Kernel mode: find the handler.
fun trap(
number: Int,
fd: Int,
buf: String?,
count: Int,
): Int = table.getOrNull(number)?.invoke(fd, buf, count) ?: -ENOSYS
// The C library: a negative result means -1 and errno.
fun wrapper(
call: String,
number: Int,
fd: Int,
buf: String?,
count: Int,
): Int {
val raw = trap(number, fd, buf, count)
if (raw < 0) {
println("$call: kernel returns $raw, wrapper returns -1 with errno ${-raw}")
return -1
}
println("$call: kernel returns $raw, wrapper returns $raw")
return raw
}
fun main() {
wrapper("write(1, \"hello\", 5)", 0, 1, "hello", 5)
wrapper("write(7, \"hi\", 2)", 0, 7, "hi", 2)
wrapper("write(1, bad address, 4)", 0, 1, null, 4)
wrapper("getpid()", 1, 0, "", 0)
wrapper("call number 99", 99, 0, "", 0)
println("standard output received: $screen")
} In Kotlin: Function references such as ::sysWrite fill the table, and getOrNull gives null for a number past the end.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
const ebadf = 9; // Linux's numbers
const efault = 14;
const enosys = 38;
final screen = StringBuffer(); // what standard output received
/// The kernel checks every argument first.
int sysWrite(int fd, String? buf, int count) {
if (fd != 1) return -ebadf; // only descriptor 1 is open
if (buf == null) return -efault; // an address the process cannot read
screen.write(buf.substring(0, count));
return count;
}
int sysGetpid(int fd, String? buf, int count) => 4242; // the process's ID
// call 0 is write, 1 is getpid
final table = <int Function(int, String?, int)>[sysWrite, sysGetpid];
/// Kernel mode: find the handler.
int trap(int number, int fd, String? buf, int count) =>
number >= 0 && number < table.length
? table[number](fd, buf, count)
: -enosys;
/// The C library: a negative result means -1 and errno.
int wrapper(String call, int number, int fd, String? buf, int count) {
final raw = trap(number, fd, buf, count);
if (raw < 0) {
print('$call: kernel returns $raw, wrapper returns -1 with errno ${-raw}');
return -1;
}
print('$call: kernel returns $raw, wrapper returns $raw');
return raw;
}
void main() {
wrapper('write(1, "hello", 5)', 0, 1, 'hello', 5);
wrapper('write(7, "hi", 2)', 0, 7, 'hi', 2);
wrapper('write(1, bad address, 4)', 0, 1, null, 4);
wrapper('getpid()', 1, 0, '', 0);
wrapper('call number 99', 99, 0, '', 0);
print('standard output received: $screen');
} In Dart: The table's type, int Function(int, String?, int), is a function type, so the handlers go into a typed list.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
write(1, "hello", 5): kernel returns 5, wrapper returns 5 write(7, "hi", 2): kernel returns -9, wrapper returns -1 with errno 9 write(1, bad address, 4): kernel returns -14, wrapper returns -1 with errno 14 getpid(): kernel returns 4242, wrapper returns 4242 call number 99: kernel returns -38, wrapper returns -1 with errno 38 standard output received: hello
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 10 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 151 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 3 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 8 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 23 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 718 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 181 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 10 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
The kernel served the first write and getpid. It refused the second write with −9, because descriptor 7 is not
open; the third with −14, because the buffer’s address cannot be read; and call number 99 with −38, because the table
has no such entry. These are the numbers Linux’s own error headers give EBADF, EFAULT and ENOSYS. In each case the
wrapper returned −1 and left the number in errno.
Key idea
A system call is a function call into the C library, a trap into the kernel, a lookup in the kernel’s table and a handler that checks every argument; the result comes back in a register, and the wrapper turns a failure into −1 and errno.
Checkpoint
Check: The path of a system call
Checking for failure: return values and errno
A failed call announces itself in its return value, usually −1, and leaves the reason in errno: The number of the last error a system call or library function reported, such as ENOENT or EBADF. It is meaningful only right after a call whose return value said it failed, and each thread has its own.. errno(3) adds two rules that are easy to miss: the value means something only when the return value indicated an error, and no system call or library function ever sets it to zero. This program makes calls that fail in three ways, then shows what happens when errno is read after a success:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void report(const char *call, int result) {
if (result == -1) {
printf("%-26s -> -1, errno %d: %s\n", call, errno, strerror(errno));
} else {
printf("%-26s -> %d\n", call, result);
}
}
int main(void) {
report("open(\"missing.txt\", ...)", open("missing.txt", O_RDONLY));
report("open(\".\", O_WRONLY)", open(".", O_WRONLY));
report("close(42)", close(42));
int fd = open("notes.txt", O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd == -1) {
perror("open notes.txt");
return 1;
}
report("write(fd, \"hello\\n\", 6)", (int)write(fd, "hello\n", 6));
report("close(fd)", close(fd));
unlink("notes.txt");
// A successful call does not reset errno: only the return value says
// whether a call failed.
if (open("missing.txt", O_RDONLY) == -1) {
(void)getpid(); // getpid always succeeds
printf("after a failed open and a successful getpid, errno is %d (%s)\n",
errno, errno == ENOENT ? "still ENOENT" : "something else");
}
return 0;
} Output
open("missing.txt", ...) -> -1, errno 2: No such file or directory
open(".", O_WRONLY) -> -1, errno 21: Is a directory
close(42) -> -1, errno 9: Bad file descriptor
write(fd, "hello\n", 6) -> 6
close(fd) -> 0
after a failed open and a successful getpid, errno is 2 (still ENOENT)
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined errno_demo.c -o errno_demo && ./errno_demo
The three failures each left a different number: 2 (ENOENT) for a missing file, 21 (EISDIR) for opening a directory
for writing, 9 (EBADF) for closing a descriptor that was never opened. The write returned 6, the number of bytes it
wrote. The last line is the trap: after a failed open, a successful getpid left errno at 2, so a program that read
errno without looking at the return value would report an error that never happened. errno is also thread-local,
errno(3) says, so one thread’s failure never shows up in another thread’s errno.
Reading errno first
if (errno != 0) … after a call reports old errors as new. Test the return value first, then read errno at once,
before any other call: int fd = open(path, O_RDONLY); if (fd == -1) perror("open");.
Checkpoint
Check: Return values and errno
Six groups of system calls
POSIX defines the portable calls and Linux’s syscalls(2) lists hundreds of its own, but sorting them by purpose, a skill CS2023 lists among its learning outcomes, leaves six groups:
| Group | What the calls do | Examples |
|---|---|---|
| Process control | Create, run, wait for and end processes | fork, execve, waitpid, _exit, kill |
| File management | Create, open, read, write, rename and remove files | open, read, write, close, rename, unlink |
| Device management | Control devices beyond reading and writing them | ioctl on Linux, tcsetattr for terminals |
| Information maintenance | Read or set the time, IDs and system data | getpid, getppid, clock_gettime, uname |
| Communication | Move data between processes | pipe, socket, send, recv, shared mmap |
| Protection | Change who may do what | chmod, chown, umask, setuid |
Some calls sit on a border: kill is process control, though it also carries a small message, and read and
write serve files, pipes, sockets and devices alike, because the kernel makes all of them look like files. The
groups help most when reading a trace of an unfamiliar program: its calls of each group show what it was asking the
operating system to do.
Checkpoint
Check: The groups of system calls
Where each system keeps its stable interface
A program built today should still run after the next operating-system update, so each system promises some interface to stay the same. They put the promise in different places:
| System | The stable interface | What that means for programs |
|---|---|---|
| Linux | The system calls themselves | A statically linked program, which makes its system calls without a shared C library, keeps working across kernel updates |
| macOS | The system libraries, libSystem and the frameworks | Programs should make system calls through libSystem; the numbers of the kernel’s calls may change |
CS2023 names the contrast directly, Linux’s “perma-stable” system-call ABI against kernels whose ABI changes with every release. Apple’s technical note QA1118 says that a statically linked binary assumes compatibility at the system-call interface, where Apple makes no guarantees, while it strives for compatibility in each dynamically linked system library. Go’s runtime followed that rule from Go 1.12 on: its release notes say libSystem is now used for system calls on Darwin, to stay forward-compatible with future versions of macOS and iOS.
Checkpoint
Check: Where the stable interface lives
What a system call costs, and calls that skip the kernel
Each trip into the kernel pays for the trap, the kernel’s checks and the return. The program below times three things
one million times each, keeping the best of three runs: an ordinary function call, a clock read, and getppid, which
Rust’s standard library offers as parent_id and which enters the kernel on every call:
use std::hint::black_box;
use std::os::unix::process::parent_id;
use std::time::Instant;
const CALLS: u32 = 1_000_000; // calls timed in each run
const RUNS: usize = 3; // the best of 3 runs: other programs can only slow a run
fn ns_per_call(mut call: impl FnMut()) -> f64 {
let mut best = f64::MAX;
for _ in 0..RUNS {
let start = Instant::now();
for _ in 0..CALLS {
call();
}
best = best.min(start.elapsed().as_nanos() as f64 / f64::from(CALLS));
}
best
}
#[inline(never)]
fn plain_function(x: u32) -> u32 {
x.wrapping_mul(3)
}
fn main() {
let function = ns_per_call(|| {
black_box(plain_function(black_box(7)));
});
let clock = ns_per_call(|| {
black_box(Instant::now());
});
let parent = ns_per_call(|| {
black_box(parent_id()); // getppid: a real system call every time
});
println!("a plain function call: {function:6.1} ns");
println!("reading the clock: {clock:6.1} ns");
println!("getppid, a system call: {parent:6.1} ns");
println!(
"getppid took {:.0} times as long as a clock read",
parent / clock
);
} Output
a plain function call: 1.1 ns reading the clock: 22.6 ns getppid, a system call: 174.5 ns getppid took 8 times as long as a clock read
This output changes from run to run: the times depend on the machine and on what else it is running
Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O syscall_cost.rs && ./syscall_cost
The function call cost about a nanosecond, the clock read tens of nanoseconds, and getppid well over a hundred,
several times the clock read. The clock read is so much cheaper because it does not make the round trip. vdso(7)
describes how Linux does it: the kernel maps a small library, the vDSO: A small library that the Linux kernel maps into every process, so that calls such as clock_gettime run as ordinary function calls without entering the kernel., into every process, so
that a call to gettimeofday changes from a system call to “a normal function call and a few memory accesses”. The
recorded run on macOS shows the same pattern, a clock read far too cheap to include a trip through the kernel.
That cost is why the C library buffers output. A program that prints a thousand short lines to a file makes one
write per buffer-full instead of one per line, and the exercise below counts the difference.
Checkpoint
Check: What a system call costs
Practise: count the write calls that buffering saves
Write a function that counts the write system calls a program makes when it prints through an unbuffered, a
line-buffered or a fully buffered stream, and run the sample tests:
Exercise · Easy · Python
Count the write calls that buffering saves
A program prints its output in pieces, through the C library's buffer, and every time the library hands bytes to the kernel it makes one write system call. Write count_writes(lines, buffer_size, mode) in writes.py, which returns how many write calls the program makes.
lineslists the program's lines; each line is a list of the lengths, in bytes, of the pieces printed for it. The last piece of a line ends with its newline.modeis"unbuffered","line"or"full":- unbuffered: every piece is written at once, one
writeper piece; - line: the pieces of a line collect in the buffer, which is written out when the line ends and also whenever it fills up (
buffer_sizebytes); - full: pieces collect across lines, the buffer is written whenever it fills up, and once more at the end if anything is left in it.
- unbuffered: every piece is written at once, one
For example, two lines printed as [[5, 3, 1], [10, 1]] with a buffer of 8 bytes take 5 writes unbuffered, 4 with line buffering (2 for each line) and 3 with full buffering (20 bytes in total).
Starter code · writes.py
def count_writes(lines, buffer_size, mode):
"""The number of write system calls for the pieces in lines, buffered as mode says."""
# Replace this line with your code.
return 0 The sample tests · test_writes.py
from writes import count_writes
EXAMPLE = [[5, 3, 1], [10, 1]]
def test_the_example():
"""gives 5 writes unbuffered, 4 line buffered and 3 fully buffered"""
assert count_writes(EXAMPLE, 8, "unbuffered") == 5
assert count_writes(EXAMPLE, 8, "line") == 4
assert count_writes(EXAMPLE, 8, "full") == 3
def test_a_thousand_short_lines():
"""turns a thousand one-piece lines of 50 bytes into 13 writes with a 4096-byte buffer"""
lines = [[50]] * 1000
assert count_writes(lines, 4096, "unbuffered") == 1000
assert count_writes(lines, 4096, "line") == 1000
assert count_writes(lines, 4096, "full") == 13
def test_exact_fits():
"""writes a buffer that fills exactly once, with nothing left over"""
assert count_writes([[8]], 8, "line") == 1
assert count_writes([[8]], 8, "full") == 1
assert count_writes([[4, 4], [8]], 8, "full") == 2
assert count_writes([[4, 4], [8]], 8, "line") == 2
def test_long_lines():
"""splits a line longer than the buffer into several writes"""
assert count_writes([[20]], 8, "line") == 3
assert count_writes([[3, 3, 3]], 4, "line") == 3
assert count_writes([[3, 3, 3]], 4, "unbuffered") == 3
def test_nothing_printed():
"""makes no write at all when nothing is printed"""
for mode in ["unbuffered", "line", "full"]:
assert count_writes([], 8, mode) == 0 A hint
With full buffering only the total matters: total bytes fill the buffer total // buffer_size times, plus one last write if there is a remainder, which is math.ceil(total / buffer_size). Line buffering applies the same rule to each line on its own.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Finish with the lesson’s questions, asked one by one, starting where your answers were weakest:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Here is a warm-up, answered completely. Harder interview questions from this lesson sit in the module’s bank: a logger that writes one byte at a time, and a statically linked program that behaves differently on Linux and macOS.
-
Fresher · Warm-up
What does errno hold, and when may a program read it?
errno holds the number of the last error that a system call or library function reported, such as ENOENT for a missing file or EBADF for a bad descriptor. It is meaningful only right after a call whose return value said it failed, usually -1, so a program checks the return value first and reads errno at once, before any other call can change it. A successful call never sets errno to zero, so reading it after a success can show an old error. On Linux the kernel returns a negative error number and the C library's wrapper stores its absolute value in errno. errno is thread-local, so each thread sees only its own errors. strerror and perror turn the number into a message.
Key takeaways
- A system call goes from a C library wrapper, through a trap instruction, to a handler the kernel picks from its table by number; the handler checks every argument before it does anything.
- On Linux the kernel returns a negative error number, and the wrapper turns it into −1 and errno; the model returned −9, −14 and −38 for a closed descriptor, a bad address and an unknown call.
- Test the return value first and read errno only after a failure: a successful getpid left errno at 2 from an earlier failed open.
- System calls fall into six groups by purpose: process control, file management, device management, information maintenance, communication and protection.
- Linux keeps its system calls stable, while macOS keeps its system libraries stable, which is why Go calls libSystem on macOS.
- A function call cost about a nanosecond, a clock read tens of nanoseconds and getppid well over a hundred, so libraries buffer output and some calls, such as the clock, avoid the kernel altogether.
Terms in this lesson
- errno
- The number of the last error a system call or library function reported, such as ENOENT or EBADF. It is meaningful only right after a call whose return value said it failed, and each thread has its own.
- system call
- A request from a program to the kernel, made with a trap instruction that switches the processor into kernel mode at an entry point the kernel chose.
- system-call table
- The kernel's table of handlers, indexed by system-call number. A trap with a number the table does not have is refused with ENOSYS.
- vDSO
- A small library that the Linux kernel maps into every process, so that calls such as clock_gettime run as ordinary function calls without entering the kernel.
References
- intro(2): introduction to system calls (Linux man-pages project (man7.org))
- syscall(2): indirect system call (architecture calling conventions) (Linux man-pages project (man7.org))
- syscalls(2): Linux system calls (Linux man-pages project (man7.org))
- errno(3): number of last error (Linux man-pages project (man7.org))
- write(2): write to a file descriptor (Linux man-pages project (man7.org))
- vdso(7): overview of the virtual ELF dynamic shared object (Linux man-pages project (man7.org))
- POSIX.1-2024 System Interfaces: general information and error numbers (The Open Group and IEEE)
- Technical Q&A QA1118: statically linked binaries on Mac OS X (Apple)
- XNU error numbers (bsd/sys/errno.h) (Apple (open-source distributions))
- Linux error numbers (include/uapi/asm-generic/errno-base.h) (The Linux Kernel Organization (source code))
- Linux error numbers (include/uapi/asm-generic/errno.h, ENOSYS) (The Linux Kernel Organization (source code))
- Go 1.12 release notes (Darwin uses libSystem) (The Go Authors)
- Function std::os::unix::process::parent_id (The Rust Project)
- Computer Science Curricula 2023: Operating Systems knowledge area (Version Gamma) (ACM, IEEE Computer Society and AAAI (CS2023 Task Force))
Report a problem with this lesson
Kept only in this browser. Your Learn progress