From 07c2376702fb3d508d6ffad6b0ce93b83972ad6e Mon Sep 17 00:00:00 2001 From: Kimplul Date: Thu, 4 Jul 2024 19:25:12 +0300 Subject: add zombie and orphan threads + Should write this down somewhere but the idea is that when a process gets killed, it frees all the memory it can, making all threads within that process orphans. Orphaned threads are assigned to the init process, which will generally call exit() on each one. Zombie threads are threads that own some bit of shared data, and whose reference count is above zero. They may not be swapped to or called, even though they take up space in thread map and reserve their thread ID. --- arch/riscv64/kernel/vmem.c | 11 +++++ include/arch/tcb.h | 7 ++++ include/kmi/ipi.h | 6 +++ include/kmi/orphanage.h | 35 ++++++++++++++++ include/kmi/queue.h | 20 +++++++++ include/kmi/syscalls.h | 12 ++++-- include/kmi/tcb.h | 21 +++++----- include/kmi/uapi.h | 27 +++++++++--- src/ipi.c | 5 +++ src/orphanage.c | 49 ++++++++++++++++++++++ src/tcb.c | 51 +++++++++++++++++++---- src/uapi/conf.c | 1 + src/uapi/dispatch.c | 3 +- src/uapi/ipc.c | 40 ++++++++++-------- src/uapi/proc.c | 100 ++++++++++++++++++++++++++++++++++++--------- 15 files changed, 320 insertions(+), 68 deletions(-) create mode 100644 include/kmi/orphanage.h create mode 100644 src/orphanage.c diff --git a/arch/riscv64/kernel/vmem.c b/arch/riscv64/kernel/vmem.c index d00a6c9..f87c76d 100644 --- a/arch/riscv64/kernel/vmem.c +++ b/arch/riscv64/kernel/vmem.c @@ -512,6 +512,17 @@ void setup_rpc_stack(struct tcb *t) t->arch.rpc_idx = 511; } +void destroy_rpc_stack(struct tcb *t) +{ + size_t pages = order_size(MM_O1) / BASE_PAGE_SIZE; + for (size_t i = 0; i < pages; ++i) { + pm_t page; enum mm_order order; + stat_vpage(t->rpc.vmem, RPC_STACK_BASE + BASE_PAGE_SIZE * i, + &page, &order, NULL); + free_page(page, order); + } +} + vm_t rpc_position(struct tcb *t) { /** @todo we assume rpc_idx is updated on every segfault of the rpc stack */ diff --git a/include/arch/tcb.h b/include/arch/tcb.h index 4e82ab1..115f49d 100644 --- a/include/arch/tcb.h +++ b/include/arch/tcb.h @@ -32,6 +32,13 @@ void tcb_assign(struct tcb *t); */ void setup_rpc_stack(struct tcb *t); +/** + * Free memory backing rpc stack. + * + * @param t Thred whose RPC stack should be destroyed. + */ +void destroy_rpc_stack(struct tcb *t); + /** * Maximum size of one individual RPC stack instance. * diff --git a/include/kmi/ipi.h b/include/kmi/ipi.h index 7a0760a..9c2963f 100644 --- a/include/kmi/ipi.h +++ b/include/kmi/ipi.h @@ -21,6 +21,12 @@ */ void send_ipi(struct tcb *t); +/** + * Remove thread from IPI queue if it is on it. + * @param t Thread to unqueue. + */ +void unqueue_ipi(struct tcb *t); + /** Handle IPI. */ void handle_ipi(); diff --git a/include/kmi/orphanage.h b/include/kmi/orphanage.h new file mode 100644 index 0000000..530d10c --- /dev/null +++ b/include/kmi/orphanage.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: copyleft-next-0.3.1 */ +/* Copyright 2024, Kim Kuparinen < kimi.h.kuparinen@gmail.com > */ + +#ifndef KMI_ORPHANAGE_H +#define KMI_OPRHANAGE_H + +/** + * @file orphanage.h + * + * Stuff related to orphaned threads. + * + * Oprhaned threads are threads whose owning process has been destroyed. + * They are always assigned to the init process, which will generally destroy + * them whenever it gets a chance. + */ + +#include + +/** + * @param t Thread suspected of being an orphan. + * @return \ref true if \p t is an orphan, \ref false otherwise. + */ +bool orphan(struct tcb *t); + +/** + * Assign \p t to the init process and jump to it. + * \p t must be an orphan! + * \p t must be in the process of swapping to its root process, either by + * returning from an rpc or being swapped to. + * + * @param t Orphaned thread. + */ +__noreturn void orphanize(struct tcb *t); + +#endif /* KMI_OPRHANAGE_H */ diff --git a/include/kmi/queue.h b/include/kmi/queue.h index 534318a..c00bf6f 100644 --- a/include/kmi/queue.h +++ b/include/kmi/queue.h @@ -53,4 +53,24 @@ static inline struct queue_head *queue_pop(struct queue_head *head) return container_of(e, struct queue_head, l); } +/** + * @param head Entry to check for queueing status. + * @return \ref true if in queue, \ref false otherwise. + */ +static inline bool in_queue(struct queue_head *head) +{ + return in_list(&head->l); +} + +/** + * Remove entry from queue. + * + * @param head Entry to remove. + */ +static inline void queue_del(struct queue_head *head) +{ + if (in_queue(head)) + list_del(&head->l); +} + #endif /* QUEUE_H */ diff --git a/include/kmi/syscalls.h b/include/kmi/syscalls.h index c78ddae..3619469 100644 --- a/include/kmi/syscalls.h +++ b/include/kmi/syscalls.h @@ -99,8 +99,8 @@ enum sys_code { /** IPC return without visible side effects. */ SYS_IPC_GHOST, - /** IPC notify thread, essentially interrupt or signal. */ - SYS_IPC_NOTIFY, + /** Notify thread, essentially interrupt or signal. */ + SYS_NOTIFY, /** @} */ /** @name Process management. */ @@ -154,6 +154,9 @@ enum sys_code { /** Request a notification handler. */ SYS_REQ_NOTIFICATION, + /** Request a thread exits. */ + SYS_EXIT, + /** @} */ SYS_NUM, @@ -165,11 +168,14 @@ enum sys_user { /** Thread has received one or several notifications, please handle * them. */ SYS_USER_NOTIFY, + + /** Thread has been orphaned. */ + SYS_USER_ORPHANED, }; /** Which notifications have arrived. */ enum notify_flag { - /** A signal (\ref ipc_notify()). */ + /** A signal (\ref sys_notify()). */ NOTIFY_SIGNAL = (1 << 0), /** A timer has expired. */ diff --git a/include/kmi/tcb.h b/include/kmi/tcb.h index b52f5e3..fac9df7 100644 --- a/include/kmi/tcb.h +++ b/include/kmi/tcb.h @@ -98,7 +98,7 @@ struct tcb { /** Arch-specific data. */ struct arch_tcbd arch; - /** Memory mapping data. */ + /** Memory mapping data. Only relevant in root thread. */ struct mem_region_root sp_r; /** Address of callback function in servers. */ @@ -127,16 +127,6 @@ struct tcb { /** RPC context of thread. */ struct tcb_ctx rpc; - /** - * RPC server context of thread. When a thread attaches itself to this - * process, its \ref rpc member is added to the list maintained in this - * variable. This allows the original thread to do rpc calls without - * messing up other threads' rpc status. - * - * I think, more testing required. - */ - struct tcb_ctx server; - /** * Effective process ID. * @@ -334,6 +324,15 @@ void set_return(struct tcb *t, vm_t r); */ bool running(struct tcb *t); +/** + * Check whether \p t is actually dead but just kept around for resource + * management. + * + * @param t \ref tcb to check. + * @return \c true if \p t is a zombie, \c false otherwise. + */ +bool zombie(struct tcb *t); + /** * Add a reference to a process. * Instead of lists of threads that belong to a process, we give the process' diff --git a/include/kmi/uapi.h b/include/kmi/uapi.h index 00fb45a..684f0da 100644 --- a/include/kmi/uapi.h +++ b/include/kmi/uapi.h @@ -584,7 +584,7 @@ SYSCALL_DECLARE0(ipc_ghost); * * Returns \ref OK and 0. */ -SYSCALL_DECLARE1(ipc_notify, tid); +SYSCALL_DECLARE1(notify, tid); /** @} */ /** @name Process handling syscalls. */ @@ -657,18 +657,17 @@ SYSCALL_DECLARE2(exec, bin, interp); SYSCALL_DECLARE2(spawn, bin, interp); /** - * Kill syscall. + * Kill syscall. Requests that all memory in process \p pid is freed and all + * threads are orphaned. If \p pid is not a process, nothing is done. * * @param t Current tcb. - * @param tid Thread ID to kill. 0 if self. + * @param pid Thread ID to kill. 0 if self. * @param b Unused. * @param c Unused. * @param d Unused. * @param e Unused. - * - * Returns \ref OK if not called on itself, otherwise doesn't return. */ -SYSCALL_DECLARE1(kill, tid); +SYSCALL_DECLARE1(kill, pid); /** * Swap syscall. @@ -826,6 +825,22 @@ SYSCALL_DECLARE0(sleep); */ SYSCALL_DECLARE1(irq_req, id); +/** + * Request that a thread exits, i.e. removes itself from the thread list and + * frees all kernel data associated with thread. + * Note that similarly to \ref kill(), some shared resources may cause the + * thread to stay around until their reference counts go to zero. + * + * @param t Current tcb. + * @param tid Thread to swap to once the current thread no longer exists. 0 for + * sleeping. + * @param b Unused. + * @param c Unused. + * @param d Unused. + * @param e Unused. + */ +SYSCALL_DECLARE1(exit, tid); + /** @} */ /** diff --git a/src/ipi.c b/src/ipi.c index e9bc4d3..d00ba1c 100644 --- a/src/ipi.c +++ b/src/ipi.c @@ -26,6 +26,11 @@ void send_ipi(struct tcb *t) cpu_send_ipi(t->cpu_id); } +void unqueue_ipi(struct tcb *t) +{ + queue_del(&t->ipi_queue); +} + void handle_ipi() { struct tcb *t = cur_tcb(); diff --git a/src/orphanage.c b/src/orphanage.c new file mode 100644 index 0000000..7415e8b --- /dev/null +++ b/src/orphanage.c @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: copyleft-next-0.3.1 */ +/* Copyright 2024, Kim Kuparinen < kimi.h.kuparinen@gmail.com > */ + +#include +#include + +#include + +/** + * @file orphanage.c + * + * Stuff related to orphaned threads implementation. + */ + +bool orphan(struct tcb *t) +{ + struct tcb *r = get_rproc(t); + return !r || r->dead; +} + +void orphanize(struct tcb *t) +{ + catastrophic_assert(!is_rpc(t)); + + struct tcb *r = get_tcb(t->rid); + if (r) + unreference_proc(r); + + /* attach to init process */ + struct tcb *init = get_tcb(1); + reference_proc(init); + + t->rid = 1; + t->pid = 1; + t->eid = 1; + + t->proc = init->proc; + use_vmem(t->proc.vmem); + + catastrophic_assert(init->callback); + set_args3(t, 0, SYS_USER_ORPHANED, t->tid); + set_return(t, init->callback); + t->callback = init->callback; + + /** @todo release irqs, here or later? */ + + ret_userspace_fast(); + unreachable(); +} diff --git a/src/tcb.c b/src/tcb.c index c0f82cc..8280deb 100644 --- a/src/tcb.c +++ b/src/tcb.c @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -200,15 +201,23 @@ struct tcb *create_proc(struct tcb *p) */ static stat_t __destroy_thread_data(struct tcb *t) { + catastrophic_assert(t->refcount == 0); + + /* free memory backing rpc stack */ + destroy_rpc_stack(t); + /* free rpc vmem */ destroy_vmem(t->rpc.vmem); + /* remove ourselves from the thread pool */ + tcbs[t->tid] = 0; + + /* forcefully free last struggling bits of memory */ + destroy_uvmem(t); + /* free associated kernel stack and the structure itself */ vm_t bottom = align_down((vm_t)t, order_size(MM_O0)); free_page(MM_O0, (pm_t)bottom); - - /** \todo free stacks */ - return OK; } @@ -217,14 +226,19 @@ stat_t destroy_thread(struct tcb *t) hard_assert(tcbs, ERR_NOINIT); hard_assert(!is_proc(t), ERR_INVAL); - /* remove thread id from list */ - /** @todo what about if thread is in rpc? should it rather just be - * marked dead? */ - tcbs[t->tid] = 0; + /* mark us as zombies */ + t->rid = 0; /* remove reference to root process */ unreference_proc(get_rproc(t)); + unqueue_ipi(t); + + /* someone still relies on us existing, don't actually free thread data + * quite yet */ + if (t->refcount) + return OK; + return __destroy_thread_data(t); } @@ -238,22 +252,31 @@ stat_t destroy_proc(struct tcb *p) unreference_proc(p); catastrophic_assert(destroy_uvmem(p)); - return __destroy_thread_data(p); + + /* don't destroy thread data just yet, let the thread destroy itself + * later */ + return OK; } void reference_proc(struct tcb *p) { + if (!p) + return; + hard_assert(is_proc(p), RETURN_VOID); p->refcount++; } void unreference_proc(struct tcb *p) { + if (!p) + return; + hard_assert(is_proc(p), RETURN_VOID); p->refcount--; if (p->dead && p->refcount == 0) { dbg("thread %d is completely destroyed\n", p->tid); - /** @todo actually destroy */ + __destroy_thread_data(p); } } @@ -308,3 +331,13 @@ bool running(struct tcb *t) { return cpu_tcb(t->cpu_id) == t; } + +bool zombie(struct tcb *t) +{ + /* we shouldn't see any NULLs but they're effectively the same thing */ + if (!t) + return true; + + /* thread doesn't belong to any process, a zombie */ + return t->rid == 0; +} diff --git a/src/uapi/conf.c b/src/uapi/conf.c index 242239c..685d294 100644 --- a/src/uapi/conf.c +++ b/src/uapi/conf.c @@ -12,6 +12,7 @@ #include #include #include +#include #include diff --git a/src/uapi/dispatch.c b/src/uapi/dispatch.c index f0ff724..7cfce4c 100644 --- a/src/uapi/dispatch.c +++ b/src/uapi/dispatch.c @@ -66,7 +66,7 @@ void handle_syscall(sys_arg_t syscall, sys_arg_t a, sys_arg_t b, case SYS_IPC_KICK: sys_ipc_kick(t, a, b, c, d, e); break; case SYS_IPC_RESP: sys_ipc_resp(t, a, b, c, d, e); break; case SYS_IPC_GHOST: sys_ipc_ghost(t, a, b, c, d, e); break; - case SYS_IPC_NOTIFY: sys_ipc_notify(t, a, b, c, d, e); break; + case SYS_NOTIFY: sys_notify(t, a, b, c, d, e); break; case SYS_CREATE: sys_create(t, a, b, c, d, e); break; case SYS_FORK: sys_fork(t, a, b, c, d, e); break; case SYS_EXEC: sys_exec(t, a, b, c, d, e); break; @@ -81,6 +81,7 @@ void handle_syscall(sys_arg_t syscall, sys_arg_t a, sys_arg_t b, case SYS_POWEROFF: sys_poweroff(t, a, b, c, d, e); break; case SYS_SLEEP: sys_sleep(t, a, b, c, d, e); break; case SYS_IRQ_REQ: sys_irq_req(t, a, b, c, d, e); break; + case SYS_EXIT: sys_exit(t, a, b, c, d, e); break; default: error("Syscall %zu outside allowed range [0 - %i]\n", syscall, SYS_NUM - 1); diff --git a/src/uapi/ipc.c b/src/uapi/ipc.c index 069ca59..f3896f5 100644 --- a/src/uapi/ipc.c +++ b/src/uapi/ipc.c @@ -6,6 +6,7 @@ * Interprocess communication syscall implementations. */ +#include #include #include #include @@ -29,9 +30,6 @@ struct call_ctx { /** Current process ID. */ id_t pid; - - /** Whether this context should be skipped when responding. */ - bool kick; }; /** @@ -83,7 +81,9 @@ static void finalize_rpc(struct tcb *t, struct tcb *r, vm_t s) static vm_t enter_rpc(struct tcb *t, struct sys_ret a, enum ipc_kind kind) { - vm_t rpc_stack = rpc_position(t); + /* reuse current rpc stack location if we're being kicked */ + vm_t rpc_stack = (kind == IPC_KICK && + is_rpc(t)) ? t->rpc_stack :rpc_position(t); struct call_ctx *ctx = (struct call_ctx *)(rpc_stack) - 1; ctx->regs = t->regs; @@ -98,9 +98,6 @@ static vm_t enter_rpc(struct tcb *t, struct sys_ret a, ctx->eid = t->eid; ctx->rpc_stack = rpc_stack; - /* only rpcs can be kicked forward */ - ctx->kick = kind == IPC_KICK && is_rpc(t); - /** @todo if we run out of rpc_stack space we should just stop, likely * return a status? except it shouldn't happen after we've run * enough_rpc_stack(). */ @@ -223,17 +220,25 @@ static void leave_rpc(struct tcb *t, struct sys_ret a) struct call_ctx *ctx = (struct call_ctx *)(rpc_stack) - 1; vm_t top = ctx->rpc_stack; - /* find first instance of not kicked context */ - while (ctx->kick) { - rpc_stack = ctx->rpc_stack + BASE_PAGE_SIZE; - ctx = (struct call_ctx *)(rpc_stack) - 1; - unreference_proc(get_tcb(ctx->pid)); - } - t->regs = ctx->regs; /* again, get rid of args as fast as possible */ set_args(t, 6, a); + struct tcb *r = get_tcb(ctx->pid); + while (!r || r->dead) { + /* we unwound back to our root process which is apparently dead, + * we're orphaned :( */ + if (ctx->pid == t->rid) { + orphanize(t); + return; + } + + rpc_stack = ctx->rpc_stack + BASE_PAGE_SIZE; + ctx = (struct call_ctx *)(rpc_stack) - 1; + + r = get_tcb(ctx->pid); + } + set_return(t, ctx->exec); /* if we're returning from a failed rpc, this should essentially be a * no-op */ @@ -301,13 +306,12 @@ static void do_ipc(struct tcb *t, vm_t s = enter_rpc(t, SYS_RET6(t->eid, t->tid, d0, d1, d2, d3), kind); struct tcb *r = get_tcb(pid); - if (unlikely(!r)) { + if (unlikely(!r || !is_proc(r))) { leave_rpc(t, SYS_RET1(ERR_INVAL)); return; } - r = get_rproc(r); - if (unlikely(r->dead)) { + if (unlikely(zombie(r))) { leave_rpc(t, SYS_RET1(ERR_INVAL)); return; } @@ -430,7 +434,7 @@ SYSCALL_DEFINE0(ipc_ghost)(struct tcb *t) * @param tid Thread ID to notify. * @return \ref OK and 0. */ -SYSCALL_DEFINE1(ipc_notify)(struct tcb *t, sys_arg_t tid){ +SYSCALL_DEFINE1(notify)(struct tcb *t, sys_arg_t tid){ if (t->tid != tid && !has_cap(t->caps, CAP_NOTIFY)) return_args1(t, ERR_PERM); diff --git a/src/uapi/proc.c b/src/uapi/proc.c index 1b672ce..3ce808f 100644 --- a/src/uapi/proc.c +++ b/src/uapi/proc.c @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include @@ -145,23 +147,93 @@ SYSCALL_DEFINE2(spawn)(struct tcb *t, sys_arg_t bin, sys_arg_t interp) * Kill syscall handler. * * @param t Current tcb. - * @param tid Thread to kill. + * @param pid Process to kill. * \todo Implement. * * @return ERR_PERM if not capable to kill, otherwise OK. */ -SYSCALL_DEFINE1(kill)(struct tcb *t, sys_arg_t tid) +SYSCALL_DEFINE1(kill)(struct tcb *t, sys_arg_t pid) { struct tcb *c = get_cproc(t); if (!(has_cap(c->caps, CAP_PROC))) return_args1(t, ERR_PERM); - /** @todo implement */ - /** @todo remember to unregister IRQ handlers */ + struct tcb *r = get_tcb(pid); + if (is_proc(r)) + return_args1(t, ERR_INVAL); + destroy_proc(r); return_args1(t, OK); } +/** + * Actual worker of swapping between threads. + * Assumes that both \p t and \p s exist and that \p s isn't a zombie or + * currently running. + * + * @param t Current tcb. + * @param s Thread to swap to. + */ +static void swap(struct tcb *t, struct tcb *s) +{ + /* switch over to new thread */ + use_tcb(s); + + /* if an irq handler is directly swapping to some other thread, + * interpret it as the thread being finished with its critical section */ + enable_irqs(); + + if (!is_rpc(s) && orphan(s)) { + orphanize(s); + return; + } + + /* set return value for current thread */ + set_args1(t, OK); + + /* handle possible queued notification */ + if (s->notify_flags) + notify(s, 0); + + /* no notifications, so get register state for new thread */ + return_args(s, get_args(s)); +} + +/** + * Syscall handler for exit syscall. + * + * @param t Thread that is exiting. + * @param tid Thread to swap to. + * + * @return \ref OK or \ref ERR_INVAL is \p tid is not a thread + * or \ref ERR_NF if \p tid is a zombie + * or \ref ERR_EXT if \p tid is currently running. + */ +SYSCALL_DEFINE1(exit)(struct tcb *t, sys_arg_t tid) +{ + if (tid != 0) { + struct tcb *s = get_tcb(tid); + if (!s) + return_args1(t, ERR_INVAL); + + if (zombie(s)) + return_args1(t, ERR_NF); + + if (running(s)) + return_args1(t, ERR_EXT); + + + swap(t, s); + } + + destroy_thread(t); + + if (tid == 0) { + enable_irqs(); + sleep(); + } +} + /** * Swap syscall handler. * @@ -183,23 +255,11 @@ SYSCALL_DEFINE1(swap)(struct tcb *t, sys_arg_t tid){ if (!s) return_args1(t, ERR_INVAL); + if (zombie(s)) + return_args1(t, ERR_NF); + if (running(s)) return_args1(t, ERR_EXT); - /* switch over to new thread */ - use_tcb(s); - - /* set return value for current thread */ - set_args1(t, OK); - - /* if an irq handler is directly swapping to some other thread, - * interpret it as the thread being finished with its critical section */ - enable_irqs(); - - /* handle possible queued notification */ - if (s->notify_flags) - notify(s, 0); - - /* no notifications, so get register state for new thread */ - return_args(s, get_args(s)); + return swap(t, s); } -- cgit v1.3