16. Essential System Calls - josehu07/hux-kernel GitHub Wiki

This chapters lists the implementation details of a set of essential system calls that control process execution, memory allocation, terminal printing, etc. It is recommended to first take a look at the next chapter on time-sharing scheduler to have an idea of what to do at timer interrupts.

Main References of This Chapter

Scan through them before going forth:

  • Process-related syscalls implementation of xv6: most of the syscall implementations in this chapter inherit from xv6 ✭

Terminal Printing Syscalls

UNIX-flavor systems typically support console input/output through a more general technique called [pipe](https://en.wikipedia.org/wiki/Pipeline_(Unix) (which is part of its general file interface - in UNIX, "everything is file"). In Hux, however, I decide not to support such a general interface, but instead implement a separate terminal printing syscall and keyboard input syscall for simplicity ✭. We will talk more about file I/O in persistence chapters.

tprint()

Print a null-terminated string to the VGA terminal in the given color.

int32_t tprint(uint32_t color, char *str);

Implementation @ src/display/sysdisp.c:

int32_t
syscall_tprint(void)
{
    uint32_t color;
    char *str;

    if (!sysarg_get_uint(0, &color))
        return SYS_FAIL_RC;
    if (color > 15)
        return SYS_FAIL_RC;
    if (sysarg_get_str(1, &str) < 0)
        return SYS_FAIL_RC;

    cprintf((vga_color_t) color, "%s", str);
    return 0;
}

User processes typically want formatted printing as well, so we duplicate the kernel version of printf() @ user/lib/printf.c and hook the low-level printing primitives with the tprintf() syscall. User programs should use printf() and cprintf() for printing and should never need to call the tprintf() syscall directly.

Process Operation Syscalls

Processes are one of most important abstractions provided by an OS kernel. Most of the basic syscalls serve process operations.

getpid()

Return the process ID of the caller process.

int32_t getpid();

Implementation @ src/process/sysproc.c:

int32_t
syscall_getpid(void)
{
    return running_proc()->pid;
}

fork()

Follows the classic POSIX definition of [fork](https://en.wikipedia.org/wiki/Fork_(system_call). Creates a copy (a child process) of the caller process (the parent process). Returns 0 in the new child process, the child PID in the parent process, and -1 in error cases. After fork completes, both processes resume execution at the point in code right after the fork call.

int32_t fork();

Implementation @ src/process/sysproc.c:

int32_t
syscall_fork(void)
{
    return process_fork();
}


// src/process/process.c

/**
 * Fork a new process that is a duplicate of the caller process. Caller
 * is the parent process and the new one is the child process. Returns
 * child pid in parent, 0 in child, and -1 if failed, just like UNIX fork().
 */
int8_t
process_fork(void)
{
    process_t *parent = running_proc();

    /** Get a slot in the ptable. */
    process_t *child = _alloc_new_process();
    if (child == NULL) {
        warn("fork: failed to allocate new child process");
        return -1;
    }

    /**
     * Create the new process's page directory, and then copy over the
     * parent process page directory, mapping all mapped-pages for the
     * child process to physical frames along the way.
     */
    child->pgdir = (pde_t *) salloc_page();
    if (child->pgdir == NULL) {
        warn("fork: cannot allocate level-1 directory, out of kheap memory?");
        sfree_page((char *) child->kstack);     // Maybe use goto.
        child->kstack = 0;
        child->pid = 0;
        child->state = UNUSED;
        return -1;
    }
    memset(child->pgdir, 0, sizeof(pde_t) * PDES_PER_PAGE);

    uint32_t vaddr_btm = 0;     /** Kernel-mapped. */
    while (vaddr_btm < PHYS_MAX) {
        pte_t *pte = paging_walk_pgdir(child->pgdir, vaddr_btm, true);
        paging_destroy_pgdir(child->pgdir);     // Maybe use goto.
        child->pgdir = NULL;
        sfree_page((char *) child->kstack);
        child->kstack = 0;
        child->pid = 0;
        child->state = UNUSED;
        return -1;
        paging_map_kpage(pte, vaddr_btm);

        vaddr_btm += PAGE_SIZE;
    }

    if (!paging_copy_range(child->pgdir, parent->pgdir,
                           USER_BASE, parent->heap_high)
        || !paging_copy_range(child->pgdir, parent->pgdir,
                              parent->stack_low, USER_MAX)) {
        warn("fork: failed to copy parent memory state over to child");
        paging_unmap_range(child->pgdir, USER_BASE, parent->heap_high);
        paging_unmap_range(child->pgdir, parent->stack_low, USER_MAX);
        paging_destroy_pgdir(child->pgdir);     // Maybe use goto.
        child->pgdir = NULL;
        sfree_page((char *) child->kstack);
        child->kstack = 0;
        child->pid = 0;
        child->state = UNUSED;
        return -1;
    }

    child->stack_low = parent->stack_low;
    child->heap_high = parent->heap_high;

    /**
     * Copy the trap state of parent to the child. Child should resume
     * execution at the same where place where parent is at right after
     * `fork()` call.
     */
    memcpy(child->trap_state, parent->trap_state, sizeof(interrupt_state_t));
    child->trap_state->eax = 0;     /** `fork()` returns 0 in child. */

    child->parent = parent;

    strncpy(child->name, parent->name, sizeof(parent->name));

    int8_t child_pid = child->pid;

    child->killed = false;
    child->state = READY;

    return child_pid;               /** Returns child pid in parent. */
}

Hux once used a different, more intuitive way of creating a new process by supporting a pcreate() syscall. However, I later switched back to the UNIX-flavored fork() because it allows a more flexible way of starting multiple processes that run different logic from only one piece of user program code: doing fork followed by if-else statement on the return code.

This is especially useful for early-stage testing when we do not support program loading yet. With fork, we can write our multi-processed testing logic in a single init program.

exit()

Terminate the caller process. All user processes in Hux must end with a call to exit().

void exit();

Implementation @ src/process/sysproc.c:

int32_t
syscall_exit(void)
{
    process_exit();
    return 0;   /** Not reached. */
}


// src/process/process.c

/**
 * Block the running process on the given reason.
 * Must be called with `cli` pushed.
 */
inline void
process_block(process_block_on_t reason)
{
    process_t *proc = running_proc();

    proc->block_on = reason;
    proc->state = BLOCKED;

    yield_to_scheduler();
}

/** Unblock a process by setting it to READY state and clear the reason. */
inline void
process_unblock(process_t *proc)
{
    proc->block_on = NOTHING;
    proc->state = READY;
}

/** Terminate a process. */
void
process_exit(void)
{
    process_t *proc = running_proc();
    assert(proc != initproc);

    /** Parent might be blocking due to waiting. */
    if (proc->parent->state == BLOCKED && proc->parent->block_on == ON_WAIT)
        process_unblock(proc->parent);

    /**
     * A process must have waited all its children before calling `exit()`
     * itself. Any children still in the process table becomes a:
     *   - "Orphan" process, if it is still running. Pass it to the `init`
     *     process for later "reaping";
     *   - "Zombie" process, if it has terminated. In this case, besides
     *     passing to `init`, we should immediately wake up init as well.
     *
     * The `init` process should be executing an infinite loop of `wait()`.
     */
    for (process_t *child = ptable; child < &ptable[MAX_PROCS]; ++child) {
        if (child->parent == proc) {
            child->parent = initproc;
            if (child->state == TERMINATED)
                process_unblock(initproc);
        }
    }

    /** Go back to scheduler context. */
    proc->state = TERMINATED;
    yield_to_scheduler();
    
    error("exit: process gets re-scheduled after termination");
}

sleep()

Sleep for approximately the given number of milliseconds. It is not accurate as the accounting is done purely with timer ticks without any calibration.

int32_t sleep(uint32_t millisecs);

Implementation @ src/process/sysproc.c:

int32_t
syscall_sleep(void)
{
    int32_t millisecs;
    
    if (!sysarg_get_int(0, &millisecs))
        return SYS_FAIL_RC;
    if (millisecs < 0)
        return SYS_FAIL_RC;

    uint32_t sleep_ticks = millisecs * TIMER_FREQ_HZ / 1000;
    process_sleep(sleep_ticks);

    return 0;
}


// src/process/process.c

/** Sleep for specified number of timer ticks. */
void
process_sleep(uint32_t sleep_ticks)
{
    process_t *proc = running_proc();

    uint32_t target_tick = timer_tick + sleep_ticks;
    proc->target_tick = target_tick;

    process_block(ON_SLEEP);

    /** Could be re-scheduled only if `timer_tick` passed `target_tick`. */
}

wait()

Wait for a child process of mine to exit. If any child process has exited, will return the PID of one of them. If all children processes are still running, block and wait for one to exit, and returns its PID. If there are no children, return -1.

int32_t wait();

Implementation @ src/process/sysproc.c:

int32_t
syscall_wait(void)
{
    return process_wait();
}


// src/process/process.c

/**
 * Wait for any child process's exit. A child process could have already
 * exited and becomes a zombie - in this case, it won't block and will
 * just return the first zombie child it sees in ptable.
 *
 * Cleaning up of children ptable entries is done in `wait()` as well.
 *
 * Returns the pid of the waited child, or -1 if don't have kids.
 */
int8_t
process_wait(void)
{
    process_t *proc = running_proc();
    uint32_t child_pid;

    while (1) {
        bool have_kids = false;

        for (process_t *child = ptable; child < &ptable[MAX_PROCS]; ++child) {
            if (child->parent != proc)
                continue;

            have_kids = true;

            if (child->state == TERMINATED) {
                /** Found one, clean up its state. */
                child_pid = child->pid;

                sfree_page((char *) child->kstack);
                child->kstack = 0;

                paging_unmap_range(child->pgdir, USER_BASE, child->heap_high);
                paging_unmap_range(child->pgdir, child->stack_low, USER_MAX);
                paging_destroy_pgdir(child->pgdir);

                child->pid = 0;
                child->parent = NULL;
                child->name[0] = '\0';
                child->state = UNUSED;

                return child_pid;
            }
        }

        /** Dont' have children. */
        if (!have_kids || proc->killed)
            return -1;

        /**
         * Otherwise, some child process is still running. Block until
         * a child wakes me up at its exit.
         */
        process_block(ON_WAIT);

        /** Could be re-scheduled after being woken up. */
    }
}

kill()

Force to kill a process by PID. Does not have any permission checks yet.

int32_t kill(int32_t pid);

Implementation @ src/process/sysproc.c:

int32_t
syscall_kill(void)
{
    int32_t pid;

    if (!sysarg_get_int(0, &pid))
        return SYS_FAIL_RC;
    if (pid < 0)
        return SYS_FAIL_RC;

    return process_kill(pid);
}


// src/process/process.c

/** Force to kill a process by pid. Returns -1 if given pid not found. */
int8_t
process_kill(int8_t pid)
{
    for (process_t *proc = ptable; proc < &ptable[MAX_PROCS]; ++proc) {
        if (proc->pid == pid) {
            proc->killed = true;

            /** Wake it up in case it is blocking on anything. */
            if (proc->state == BLOCKED)
                process_unblock(proc);

            return 0;
        }
    }

    return -1;
}

Actual exiting of a killed process is performed in regular timer interrupts. Please see the next chapter.

uptime()

Return the approximate number of milliseconds passed since booting. Could be used for coarse-grained timing.

int32_t uptime();

Implementation @ src/device/sysdev.c:

int32_t
syscall_uptime(void)
{
    return (int32_t) (timer_tick * 1000 / TIMER_FREQ_HZ);
}

Check that it works!

Keyboard Input Syscalls

Hux treats console input and output as special types of I/O and hooks them directly with the VGA device and the keyboard device. (UNIX systems integrate them into a more general pipe model which is part of the file I/O interface.)

The downside of Hux's choice is that it leads to a more rigid I/O model: supporting a different peripheral device type requires adding in a new set of syscalls coupled with that device type, which would be unacceptable for practical OSes. The upside, of course, is simplicity and better code understandability. By looking at the list of syscalls, you clearly know what types of devices the OS kernel supports.

kbdstr()

Activate keyboard input recording and fetch an input string from keyboard. The input string ends in one of the two conditions: 1. the maximum buffer length has been reached (counting \0), or 2. an ENTER key press event is detected, which translates to a newline symbol.

int32_t kbdstr(char *buf, uint32_t len);

Implementaion @ src/device/sysdev.c:

int32_t
syscall_kbdstr(void)
{
    char *buf;
    int32_t len;

    if (!sysarg_get_uint(1, &len))
        return SYS_FAIL_RC;
    if (!sysarg_get_mem(0, &buf, len))
        return SYS_FAIL_RC;

    return (int32_t) (keyboard_getstr(buf, len));
}

The main keyboard listening logic is implemented @ src/device/keyboard.c and involves modifications to the keyboard interrupt handler:

/** A circular buffer for recording the input string from keyboard. */
#define INPUT_BUF_SIZE 256
static char input_buf[INPUT_BUF_SIZE];

/**
 * These two numbers grow indefinitely. `loc % INPUT_BUF_SIZE` is the
 * actual index in the circular buffer.
 */
static size_t input_put_loc = 0;   // Place to record the next char.
static size_t input_get_loc = 0;   // Start of the first unfetched char.


/** If not NULL, that process is listening on keyboard events. */
static process_t *listener_proc = NULL;


/**
 * Keyboard interrupt handler registered for IRQ #1.
 * Serves keyboard input requests. Interrupts should have been disabled
 * automatically since this is an interrupt gate.
 *
 * Currently only supports lower cased ASCII characters, upper case by
 * holding SHIFT or activating CAPSLOCK, and newline. Assumes that at
 * most one process could be listening on keyboard input at the same time.
 */
static void
keyboard_interrupt_handler(interrupt_state_t *state)
{
    (void) state;   /** Unused. */
    
    keyboard_key_event_t event = NO_KEY;

    /**
     * Read our the event's scancode. Translate the scancode into a key
     * event, following the scancode set 1 mappings.
     */
    uint8_t scancode = inb(0x60);
    if (scancode < 0xE0)
        event = scancode_event_map[scancode];
    else if (scancode == 0xE0) {    /** Is a key in extended set. */
        uint8_t extendcode = inb(0x60);
        if (extendcode < 0xE0)
            event = extendcode_event_map[extendcode];
    }

    /**
     * React only if no overwriting could happen and if a process is
     * listening on keyboard input. Record the char to the circular buffer,
     * unblock it when buffer is full or when an ENTER press happens.
     * Interactively displays the character.
     */
    if (input_put_loc - input_get_loc < INPUT_BUF_SIZE
        && listener_proc != NULL && listener_proc->state == BLOCKED
        && listener_proc->block_on == ON_KBDIN) {
        bool is_enter = !event.ascii && event.info.meta == KEY_ENTER;
        bool is_back = !event.ascii && event.info.meta == KEY_BACK;
        bool is_shift = !event.ascii && event.info.meta == KEY_SHIFT;
        bool is_caps = !event.ascii && event.info.meta == KEY_CAPS;

        if (!shift_held && event.press && is_shift)
            shift_held = true;
        else if (shift_held && !event.press && is_shift)
            shift_held = false;
        capslock_on = (event.press && is_caps) ? !capslock_on : capslock_on;
        bool upper_case = (shift_held != capslock_on);

        if (event.press && (event.ascii || is_enter)) {
            char c = !event.ascii ? '\n' :
                     upper_case ? event.info.codeu : event.info.codel;
            input_buf[(input_put_loc++) % INPUT_BUF_SIZE] = c;
            printf("%c", c);
        } else if (event.press && is_back) {
            if (input_put_loc > input_get_loc) {
                input_put_loc--;
                terminal_erase();
            }
        }

        if ((event.press && is_enter)
            || input_put_loc == input_get_loc + INPUT_BUF_SIZE) {
            process_unblock(listener_proc);
        }
    }
}


/**
 * Listen on keyboard characters, interpret as an input string, and write the
 * string into the given buffer. Returns the length of the string actually
 * fetched, or -1 on errors.
 * 
 * The listening terminates on any of the following cases:
 *   - A total of `len - 1` bytes have been fetched;
 *   - Got a newline symbol.
 */
int32_t
keyboard_getstr(char *buf, size_t len)
{
    assert(buf != NULL);
    assert(len > 0);

    if (listener_proc != NULL) {
        warn("keyboard_getstr: there is already a keyboard listener");
        return -1;
    }

    process_t *proc = running_proc();
    listener_proc = proc;

    input_get_loc = input_put_loc;
    size_t left = len;

    while (left > 1) {
        /** Wait until there are unhandled chars. */
        while (input_get_loc == input_put_loc) {
            if (proc->killed)
                return -1;
            process_block(ON_KBDIN);
        }

        /** Fetch the next unhandled char. */
        char c = input_buf[(input_get_loc++) % INPUT_BUF_SIZE];
        buf[len - left] = c;
        left--;

        if (c == '\n')  /** Newline triggers early break. */
            break;
    }

    /** Fill a null-terminator to finish the string. */
    size_t fetched = len - left;
    buf[fetched] = '\0';

    /** Clear the listener. */
    listener_proc = NULL;

    return fetched;
}

Check that it works!

Under the scheme of time-sharing, you may have noticed a serious problem with this version of implementation: there is no locking ✭. There are no locks to protect the global input buffer state. Even without multi-threading and on a single-CPU, this could be dangerous - imagine multiple processes doing the kbdstr() syscall at roughly the same time. The same issue applies to some of our previous code, such as process operation syscalls that manipulate the global process table state. We will discuss this issue in more depth in the next chapter.

Memory Allocation Syscalls

In Hux, there will be two types of memory allocation made by a user process: explicit allocation on heap and implicit extension of stack.

setheap()

Enlarge the upper boundary virtual address of caller process's heap. New pages will be allocated if necessary. This is similar to the UNIX "set break" (sbrk()) syscall, except that setheap() only supports enlarging the heap and never shrinking it.

int32_t setheap(uint32_t new_top);

Implementation @ src/memory/sysmem.c:

int32_t
syscall_setheap(void)
{
    process_t *proc = running_proc();
    uint32_t new_top;

    if (!sysarg_get_uint(0, &new_top))
        return SYS_FAIL_RC;
    if (new_top < proc->heap_high) {
        warn("setheap: does not support shrinking heap");
        return SYS_FAIL_RC;
    }
    if (new_top > proc->stack_low) {
        warn("setheap: heap meets stack, heap overflow");
        return SYS_FAIL_RC;
    }

    /**
     * Compare with current heap page allocation top. If exceeds the top
     * page, allocate new pages accordingly.
     */
    uint32_t heap_page_high = ADDR_PAGE_ROUND_UP(proc->heap_high);

    for (uint32_t vaddr = heap_page_high;
         vaddr < new_top;
         vaddr += PAGE_SIZE) {
        pte_t *pte = paging_walk_pgdir(proc->pgdir, vaddr, true);
        if (pte == NULL) {
            warn("setheap: cannot walk pgdir, out of kheap memory?");
            paging_unmap_range(proc->pgdir, heap_page_high, vaddr);
            return SYS_FAIL_RC;
        }
        uint32_t paddr = paging_map_upage(pte, true);
        if (paddr == 0) {
            warn("setheap: cannot map new page, out of memory?");
            paging_unmap_range(proc->pgdir, heap_page_high, vaddr);
            return SYS_FAIL_RC;
        }
        memset((char *) paddr, 0, PAGE_SIZE);
    }

    proc->heap_high = new_top;
    return 0;
}

User processes typically want a heap memory allocator that exposes a malloc() + mfree() interface, so we borrow from the kernel heap allocator and make a user library @ user/lib/malloc.c. The only differences is that, instead of assuming a fixed memory region, the allocator now calls the setheap() syscall to ask for more heap memory when the current free chunks cannot satisfy an incoming request.

Implicit Stack Growth Page Fault

Our x86-IA32 architecture defines a downward-growing function stack. Execution of functions and allocation of on-stack local variables/buffers require stack space. Stack growth happens not through a syscall but through a page fault.

Hux gives each process an initial stack of size of one page - the top most page right below USER_MAX. However, a complex user program may introduce deep function calls and may create large on-stack buffers, which grow the stack beyond a single page. As the process tries to access bytes at the new ESP (where the pages at and above it have not been mapped for this process yet), a page fault occurs.

An OS kernel could define its user program stack behavior ABI in one of the two ways ✭:

  • DOS-flavor: Strictly require that any user program must grow its stack "page by page" - the page fault is considered a valid stack growth only if it is no lower than one page below the current stack region bottom;
  • UNIX-flavor: Allow user program to grow the stack by multiple pages at a time, but set a max size limit of any user process's stack (e.g., 8MiB in Linux) - a page fault below the current stack bottom while above that size limit is considered a valid stack growth.

Then, compiler developers build compilers that adhere to their target OS's ABI. Since now Hux relies on GCC as the compiler, it takes the UNIX-flavor. To demonstrate, the following user program:

void
main(void)
{
    char buf[8200];
    buf[0] = 'A';
    buf[1] = '\0';
    printf("%s\n", buf);
}

Compiles to the following machine code:

$ objdump -D user/test.out | less

Disassembly of section .text:

20000000 <main>:
20000000:       8d 4c 24 04             lea    0x4(%esp),%ecx
20000004:       83 e4 f0                and    $0xfffffff0,%esp
20000007:       b8 41 00 00 00          mov    $0x41,%eax
2000000c:       ff 71 fc                push   -0x4(%ecx)
2000000f:       55                      push   %ebp
20000010:       89 e5                   mov    %esp,%ebp
20000012:       51                      push   %ecx
20000013:       81 ec 1c 20 00 00       sub    $0x201c,%esp      # Stack grows by more than
20000019:       66 89 85 f0 df ff ff    mov    %ax,-0x2010(%ebp) # one page at a time.
20000020:       8d 85 f0 df ff ff       lea    -0x2010(%ebp),%eax
20000026:       50                      push   %eax
20000027:       68 a0 1b 00 20          push   $0x20001ba0
2000002c:       e8 7f 11 00 00          call   200011b0 <printf>
20000031:       8b 4d fc                mov    -0x4(%ebp),%ecx
20000034:       83 c4 10                add    $0x10,%esp
20000037:       c9                      leave
20000038:       8d 61 fc                lea    -0x4(%ecx),%esp
2000003b:       c3                      ret

We stipulate that any user program in Hux cannot grow its stack beyond 4MiB in size, hence the new layout macro @ src/process/layout.h:

/** Max stack size limit is 4MiB. */
#define STACK_MIN (USER_MAX - 0x00400000)

And our complete page fault handler @ src/memory/paging.c:

/** Page fault (ISR # 14) handler. */
static void
page_fault_handler(interrupt_state_t *state)
{
    /** The CR2 register holds the faulty address. */
    uint32_t faulty_addr;
    asm ( "movl %%cr2, %0" : "=r" (faulty_addr) : );

    /**
     * Analyze the least significant 3 bits of error code to see what
     * triggered this page fault:
     *   - bit 0: page present -> 1, otherwise 0
     *   - bit 1: is a write operation -> 1, read -> 0
     *   - bit 2: is from user mode -> 1, kernel -> 0
     *
     * See https://wiki.osdev.org/Paging for more.
     */
    bool present = state->err_code & 0x1;
    bool write   = state->err_code & 0x2;
    bool user    = state->err_code & 0x4;
    process_t *proc = running_proc();

    /**
     * If is a valid stack growth page fault (within stack size limit
     * and not meeting the heap upper boundary), then allocate and map
     * the new pages.
     */
    if (!present && user && faulty_addr < proc->stack_low
        && faulty_addr >= STACK_MIN && faulty_addr >= proc->heap_high) {
        uint32_t old_btm = ADDR_PAGE_ROUND_DN(proc->stack_low);
        uint32_t new_btm = ADDR_PAGE_ROUND_DN(faulty_addr);

        uint32_t vaddr;
        for (vaddr = new_btm; vaddr < old_btm; vaddr += PAGE_SIZE) {
            pte_t *pte = paging_walk_pgdir(proc->pgdir, vaddr, true);
            if (pte == NULL) {
                warn("page_fault: cannot walk pgdir, out of kheap memory?");
                break;
            }
            uint32_t paddr = paging_map_upage(pte, true);
            if (paddr == 0) {
                warn("page_fault: cannot map new page, out of memory?");
                break;
            }
            memset((char *) paddr, 0, PAGE_SIZE);
        }

        if (vaddr < old_btm) {
            warn("page_fault: stack growth to %p failed, killing", new_btm);
            process_exit();
        } else
            proc->stack_low = new_btm;

        return;
    }

    /** Other page faults are considered truly harmful. */
    info("Caught page fault {\n"
         "  faulty addr = %p\n"
         "  present: %d\n"
         "  write:   %d\n"
         "  user:    %d\n"
         "} not handled!", faulty_addr, present, write, user);
    process_exit();
}

Check that it works!

Progress So Far

This chapter is a considerably long and content-rich one, where we implement a set of fundamental system calls + a complete page fault handler for user programs to make use of. With these infrastructure-level support, it is finally possible to write useful user programs and run them as orchestrated processes in Hux!

Current repo structure:

hux-kernel
├── Makefile
├── scripts
│   ├── gdb_init
│   ├── grub.cfg
│   └── kernel.ld
├── src
│   ├── boot
│   │   ├── boot.s
│   │   ├── elf.h
│   │   └── multiboot.h
│   ├── common
│   │   ├── debug.c
│   │   ├── debug.h
│   │   ├── port.c
│   │   ├── port.h
│   │   ├── printf.c
│   │   ├── printf.h
│   │   ├── string.c
│   │   ├── string.h
│   │   ├── types.c
│   │   └── types.h
│   ├── device
│   │   ├── keyboard.c
│   │   ├── keyboard.h
│   │   ├── sysdev.c
│   │   ├── sysdev.h
│   │   ├── timer.c
│   │   └── timer.h
│   ├── display
│   │   ├── sysdisp.c
│   │   ├── sysdisp.h
│   │   ├── terminal.c
│   │   ├── terminal.h
│   │   └── vga.h
│   ├── interrupt
│   │   ├── idt-load.s
│   │   ├── idt.c
│   │   ├── idt.h
│   │   ├── isr-stub.s
│   │   ├── isr.c
│   │   ├── isr.h
│   │   ├── syscall.c
│   │   └── syscall.h
│   ├── memory
│   │   ├── gdt-load.s
│   │   ├── gdt.c
│   │   ├── gdt.h
│   │   ├── kheap.c
│   │   ├── kheap.h
│   │   ├── paging.c
│   │   ├── paging.h
│   │   ├── slabs.c
│   │   ├── slabs.h
│   │   ├── sysmem.c
│   │   └── sysmem.h
│   ├── process
│   │   ├── layout.h
│   │   ├── process.c
│   │   ├── process.h
│   │   ├── scheduler.c
│   │   ├── scheduler.h
│   │   ├── switch.s
│   │   ├── sysproc.c
│   │   └── sysproc.h
│   └── kernel.c
├── user
│   ├── lib
│   │   ├── debug.h
│   │   ├── malloc.c
│   │   ├── malloc.h
│   │   ├── printf.c
│   │   ├── printf.h
│   │   ├── string.c
│   │   ├── string.h
│   │   ├── syscall.h
│   │   ├── syscall.s
│   │   ├── syslist.s
│   │   ├── types.c
│   │   └── types.h
│   └── init.c
⚠️ **GitHub.com Fallback** ⚠️