Skip to content

Process Management

Galfurian edited this page Aug 19, 2026 · 4 revisions

A guided, code-backed overview of process management in MentOS.

What is a process in MentOS?

A process is the unit of execution in MentOS. It owns:

  • A virtual address space (its mm_struct_t), with a default user stack of DEFAULT_STACK_SIZE = 1 MiB (kernel/inc/process/process.h)
  • A stack and registers (thread_struct_t)
  • Open file descriptors — at most MAX_OPEN_FD = 16 per task (kernel/inc/fs/vfs.h)
  • Identity and group information (PID/PPID/PGID/SID, UID/GID, real and effective)
  • Scheduling metadata (sched_entity)
  • Signal state (handlers, blocked mask, pending set) and timers

From user space, you interact with processes mainly through the POSIX-style APIs in unistd.h and sys/wait.h.

Process vs program vs thread

Three words that are easy to confuse, and where MentOS differs from Linux:

  • A program is a file on disk — an ELF executable such as /bin/ls.
  • A process is a running instance of a program, with its own address space and its own task_struct. Every schedulable entity in MentOS is one of these.
  • A thread is, in Linux, a task that shares an address space with its siblings. MentOS does not currently offer this. __NR_clone has a number in lib/inc/system/syscall_types.h and the dispatcher in kernel/src/system/syscall.c has a special case for it, but syscall_init() never registers a sys_clone handler and no such handler exists in the tree. Treat threading as unimplemented, not as an available API. (The FIXME: When threads will be implemented comment in __load_executable() is a good marker of where the design anticipates it.)

Consequently, in MentOS "process" and "task" can be read as synonyms; in Linux they cannot.

Lifecycle: init → fork → exec → exit

1) The init process (PID 1)

The kernel creates the init process at boot and loads /bin/init — or /bin/runtests when the Multiboot command line is runtests. It also wires standard I/O to /proc/video so user programs can read and write to the screen and keyboard:

  • The init process is created in process_create_init().
  • STDIN/STDOUT/STDERR are attached to /proc/video: vfs_open("/proc/video", ...) is called three times, once read-only and twice write-only, producing descriptors 0, 1 and 2.

This is a MentOS-specific shortcut. In a conventional Unix system the first process inherits descriptors backed by a terminal device (/dev/tty* or /dev/console), and the shell later manages the controlling terminal, job control and terminal modes through it. MentOS instead points all three at a procfs video node. Everything a program does with fds 0/1/2 still works the way you expect, but if you go looking for a tty layer behind them you will not find one.

Relevant code paths:

  • kernel/src/process/process.c
  • kernel/inc/process/process.h

2) Fork: duplicate the caller

fork() creates a new process by cloning the current process state and address space. In MentOS:

  • The child gets a cloned memory map (mm_clone).
  • File descriptors are duplicated (vfs_dup_task).
  • The child sees a return value of 0 — sys_fork() sets proc->thread.regs.eax = 0 in the child's saved frame, while the parent gets the child's PID as the ordinary syscall return.
  • Session, process group, and user/group ids are inherited from the parent.

"Duplicate" needs a qualifier. It is not true that fork() copies everything: the PID is new, the parent PID is the caller, pending signals and accumulated CPU time start fresh, and open files are shared objects reached through duplicated descriptors — the two processes share one file offset per open file, they do not get private copies of the file.

Copy-on-write: present in the VM, not used by fork()

Textbook fork() is fast because it maps the parent's pages read-only into the child and copies a page only when one of them writes to it. MentOS has that machinery: vm_area_clone() takes a cow flag, page table entries carry kernel_cow, and __page_handle_cow() in kernel/src/mem/page_fault.c resolves COW faults.

But sys_fork() does not use it:

sys_fork()  →  mm_clone(current->mm)  →  vm_area_clone(mm, area, /* cow = */ 0, GFP_HIGHUSER)

With cow == 0, vm_area_clone() allocates fresh physical pages and eagerly copies the whole area with vmem_memcpy(). So in the current tree fork() performs a full eager copy of the address space. Keep the COW concept — it is the right thing to learn, and the code to read is already there — but do not describe it as what fork() does today.

User-side API:

#include <unistd.h>

pid_t pid = fork();
if (pid == 0) {
    // child
} else if (pid > 0) {
    // parent
}

3) Exec: replace the process image

execve() replaces the current process image with a new executable. In MentOS:

  • The process identity survives: same PID, same parent, same open descriptors. Only the program image — address space, entry point, argv/envp — is replaced. This is the Unix model, and it is why fork() + execve() is the standard way to start a program.
  • The old address space is destroyed and rebuilt (mm_destroy, mm_create_blank).
  • ELF binaries are loaded by the kernel ELF loader, which accepts only 32-bit, little-endian, EM_386, ET_EXEC images (see elf_check_file_header()).
  • Shebang (#!) scripts are supported, one level deep: __load_executable() reads the first line, restarts with the interpreter path, and returns -ELOOP if that interpreter is itself a script.
  • A NULL envp is accepted and replaced with a default environment (a MentOS convenience, not a POSIX behaviour — on Linux a NULL envp is simply an empty environment):
    • PATH=/bin:/usr/bin
    • HOME=/

Failure semantics differ from mature Unix kernels. POSIX guarantees that if execve() fails, it returns -1 and the calling process is left completely untouched. MentOS checks the cheap preconditions first (the file exists, is executable, and is either an ET_EXEC ELF or a #! script) — but it then calls mm_destroy() and rebuilds a blank address space before elf_load_file() runs. If segment loading fails after that point, the old image is already gone and there is nothing sensible to return to. Do not present this as standard Unix behaviour; it is a known simplification in the current implementation.

User-side API:

#include <unistd.h>

char *argv[] = {"/bin/echo", "hello", NULL};
char *envp[] = {"PATH=/bin:/usr/bin", "HOME=/", NULL};
execve(argv[0], argv, envp);

4) Exit and wait

exit() marks a process as a zombie and notifies the parent with SIGCHLD. The parent should collect the exit status with waitpid().

User-side API:

#include <sys/wait.h>
#include <unistd.h>

pid_t pid = fork();
if (pid == 0) {
    // child
    _exit(42);
}

int status = 0;
pid_t done = waitpid(pid, &status, 0);
if (done > 0 && WIFEXITED(status)) {
    int code = WEXITSTATUS(status);
}

Process identifiers and groups

MentOS tracks the usual UNIX identifiers:

  • PID: process ID
  • PPID: parent process ID
  • PGID: process group ID
  • SID: session ID
  • UID/GID: user and group IDs (real and effective)

User-side APIs (subset):

#include <unistd.h>

pid_t pid  = getpid();
pid_t ppid = getppid();
pid_t pgid = getpgid(0);
pid_t sid  = getsid(0);
setpgid(0, pgid);
setsid();

Notes about waitpid() in MentOS:

  • pid == -1 (any child) and pid > 0 (specific child) are supported.
  • pid == 0 or pid < -1 are not supported.
  • Only WNOHANG and WUNTRACED are accepted; other options return -EINVAL.

Scheduling and process states

Processes are scheduled from a run queue (runqueue_t) and can be in these states:

  • TASK_RUNNING
  • TASK_INTERRUPTIBLE
  • TASK_UNINTERRUPTIBLE
  • TASK_STOPPED
  • TASK_TRACED
  • EXIT_ZOMBIE
  • EXIT_DEAD

MentOS lets you select a scheduling policy at build time via the CMake SCHEDULER_TYPE option, but only SCHEDULER_RR (the default) is fully implemented; the Priority, CFS, EDF, RM and AEDF branches in kernel/src/process/scheduler_algorithm.c are student exercises. Selecting one of them intentionally causes the build to fail at the exercise placeholders until the missing implementation is completed. See Scheduling for a full overview.

Signals (process-level events)

Processes can receive POSIX-like signals. When a process exits, the kernel sends SIGCHLD to the parent.

User-side APIs are defined in <signal.h> and <sys/wait.h> (for exit status macros). See IPC and System Calls for details.

Teaching examples

Example 1: Spawn a child, wait, and print its PID

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid = fork();
    if (pid == 0) {
        printf("child pid=%d\n", getpid());
        _exit(0);
    }

    int status = 0;
    waitpid(pid, &status, 0);
    printf("parent saw child exit\n");
    return 0;
}

Example 2: Replace the child image with another program

#include <unistd.h>

int main(void)
{
    pid_t pid = fork();
    if (pid == 0) {
        char *argv[] = {"/bin/ls", "/", NULL};
        execve(argv[0], argv, NULL); // NULL envp is allowed in MentOS
        _exit(127);
    }

    waitpid(pid, NULL, 0);
    return 0;
}

Developer notes (kernel-side)

Key data structures

  • task_struct describes a process (IDs, state, file descriptors, memory, signals)
  • thread_struct stores register state
  • sched_entity stores scheduling metadata

Important code paths

  • Process creation and exec: kernel/src/process/process.c
  • Scheduler and syscalls like getpid, setpgid, waitpid, exit: kernel/src/process/scheduler.c
  • Wait queues and sleep primitives: kernel/src/process/wait.c

Behaviors worth knowing

  • fork() uses mm_clone() and duplicates the parent file table. mm_clone() copies the kernel half of the page directory from the main page directory and rebuilds the user half from the parent's VMA list, so parent and child never share page tables.
  • execve() enforces executable permission, honours the setuid/setgid bits, and supports one level of shebang indirection.
  • sys_exit() is do_exit(exit_code << 8) — the 8-bit shift mirrors the UNIX wait-status encoding that WEXITSTATUS() undoes.
  • Exiting processes become zombies until the parent calls waitpid().
  • The ELF loader maps every PT_LOAD segment as MM_USER | MM_RW | MM_PRESENT; it does not derive per-segment permissions from the ELF PF_R/PF_W/PF_X flags. In practice this means a program's .text is writable and there is no W^X separation. See Features.

Related pages

Clone this wiki locally