add apis for userspace to spawn new processes/threads

main
cinder 7 months ago
parent ca37976828
commit 3e5cfde820

@ -5,18 +5,18 @@ bootfs = bootfs or computer.getBootAddress()
--store all functions we need in locals so userspace mischief can't replace them
--suffix 'K' (for kernel) on any functions userspace will also have a version of to ensure kernelspace
--prefix 'k' (for kernel) on any functions userspace will also have a version of to ensure kernelspace
--function usage is intentional (ie prevent accidentally using the kernel's `load` in a userspace `loadfile`)
local envBase = _G --used for creating process environments
local assert, checkArg, error, loadK, ipairs, next, pairs, rawequal, rawset, setmetatable, tostring, type = assert, checkArg, error, load, ipairs, next, pairs, rawequal, rawset, setmetatable, tostring, type
local assert, checkArg, error, kload, ipairs, next, pairs, rawequal, rawset, setmetatable, tostring, type = assert, checkArg, error, load, ipairs, next, pairs, rawequal, rawset, setmetatable, tostring, type
local invoke = component.invoke
local pullSignalK, shutdown, uptime = computer.pullSignal, computer.shutdown, computer.uptime
local co_create, co_status, co_resumeK, co_yieldK = coroutine.create, coroutine.status, coroutine.resume, coroutine.yield
local kpullSignal, shutdown, uptime = computer.pullSignal, computer.shutdown, computer.uptime
local co_create, co_status, kresume, kyield = coroutine.create, coroutine.status, coroutine.resume, coroutine.yield
local huge = math.huge
local format = string.format
local pack, unpack = table.pack, table.unpack
local big = math.maxinteger or huge --some large amount to read at once in readfileK
local big = math.maxinteger or huge --some large amount to read at once in kreadfile
--completely disable the environment, ensuring the kernel only uses local variables, to prevent userspace
@ -26,7 +26,7 @@ _ENV = setmetatable({}, {
__newindex = function(self, key) return error(format('assigned global %q', key), 2) end,
})
local function readfileK(path, fs)
local function kreadfile(path, fs)
fs = fs or bootfs
local text, chunk, fd, err = '', nil, invoke(fs, 'open', path)
if not fd then
@ -47,15 +47,38 @@ end
--[=====[ process internals ]=====]--
local processes = {} --key is thread, value is a table of process state (ie env)
local fillEnv --function(process:thread, env:table):() -- given a process and an empty environment, fill it in before the process runs
local runnable = {} --key is thread, value is pack(...)ed args to resume it with
local schedDeadline = {} --key is thread, value is absolute deadline of uptime()
local schedSignal = {} --key is thread, value is `true` (may allow specific signal names in the future)
local fillEnv --function(proc:thread, env:table):() -- given a process and an empty environment, fill it in before the process runs
--[=====[ process environments ]=====]--
--<https://ocdoc.cil.li/tutorial:custom_oses#what_s_available>
-- _G (through envBase) is used as the base for process environments, so clear it of anything process-specific
envBase._OSVERSION = 'kitn 0.0.0' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver"
envBase._G, envBase.load, envBase.coroutine.resume, envBase.coroutine.yield, envBase.computer.pullSignal = nil
envBase._OSVERSION = 'kitn 0.1.0' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver"
envBase._G, envBase.load = nil
envBase.coroutine.resume = function(co, ...)
checkArg(1, co, 'thread')
local results = pack(co_resume(co, ...)) --ok, os_reason, ...
if results[1] and results[2] then
return kyield(unpack(results, 2, results.n))
else
return unpack(results, 1, results.n)
end
end
envBase.coroutine.yield = function(...) return kyield(false, ...) end
envBase.os.sleep = function(n)
checkArg(1, n, 'number')
return kyield('deadline', n)
end
envBase.computer.pullSignal = function(timeout)
checkArg(1, timeout, 'number', 'nil')
return kyield('signal', timeout)
end
envBase.kitn = {} --our specific goodies
--environment sandboxing uses mirrors: tables that provide a read-only "reflection" of what's on the other side
local mirror --function(target:table[, mirror:table]):table -- create a mirror (or make a table into one) that reflects `target`
@ -78,7 +101,7 @@ local mirrormt = {
end,
__pairs = function(self)
--reuse a single function with the mirror as the control variable, instead of creating a new function with
--upvalues every time, because that seems vaguely less memory-intensive (TODO benchmark that)
--upvalues every time, for vague feelings of less memory usage (TODO benchmark that)
return mirrorNext, self
end,
--mirrors allow assignment but don't do anything with it
@ -91,65 +114,65 @@ function mirror(target, mirror)
return mirror
end
function fillEnv(process, env)
--<https://ocdoc.cil.li/tutorial:custom_oses#what_s_available>
mirror(envBase, env)
function fillEnv(proc, uenv)
mirror(envBase, uenv)
uenv._G = uenv
local function env_load(chunk, name, mode, fenv)
local function uload(chunk, name, mode, env)
checkArg(1, chunk, 'function', 'string')
checkArg(2, name, 'string', 'nil')
checkArg(3, mode, 'string', 'nil')
checkArg(4, fenv, 'table', 'nil')
return loadK(chunk, name, mode, fenv or env)
checkArg(4, env, 'table', 'nil')
return kload(chunk, name, mode, env or uenv)
end
local function env_loadfile(path, mode, fenv)
local function uloadfile(path, mode, env)
checkArg(1, path, 'string')
checkArg(2, mode, 'string', 'nil')
checkArg(3, newenv, 'table', 'nil')
checkArg(3, env, 'table', 'nil')
local code, err = readfileK(path)
local code, err = kreadfile(path)
if not code then return nil, err end
code, err = env_load(code, '@' .. path, mode, fenv)
code, err = uload(code, '@' .. path, mode, env)
return code, err
end
local function env_dofile(path, ...)
local function udofile(path, ...)
checkArg(1, path, 'string')
local fn = assert(env_loadfile(path))
local fn = assert(uloadfile(path))
return fn(...)
end
env.load, env.loadfile, env.dofile = env_load, env_loadfile, env_dofile
env.coroutine = mirror(envBase.coroutine, {
resume = function(co, ...)
local results = pack(co_resume(co, ...)) --ok, os_reason, ...
if results[1] and results[2] then
return co_yieldK(unpack(results, 2, results.n))
else
return unpack(results, 1, results.n)
uenv.load, uenv.loadfile, uenv.dofile = uload, uloadfile, udofile
--env will automatically mirror envBase.kitn here
local ukitn = uenv.kitn
function ukitn.createThread(fn, ...)
checkArg(1, fn, 'function')
local newThread = co_create(fn)
processes[newThread] = { thread = true, parent = proc } --mark as thread of same process
runnable[newThread] = pack(...)
return newThread
end
end,
yield = function(...) return co_yieldK(false, ...) end,
})
env.os = mirror(envBase.os, {
sleep = function(n)
checkArg(1, n, 'number')
return co_yieldK('deadline', n)
end,
})
env.computer = mirror(envBase.computer, {
pullSignal = function(timeout)
checkArg(1, timeout, 'number', 'nil')
return co_yieldK('signal', timeout)
end,
})
end
function ukitn.createProcess(path, ...)
checkArg(1, path, 'string')
local env = mirror(envBase)
local fn, err = uloadfile(path, nil, env)
if not fn then return nil, err end
local newProc = co_create(fn)
processes[newProc] = { env = env, parent = proc, path = path }
runnable[newProc] = pack(...)
return newProc
end
--[=====[ scheduler ]=====]--
local runnable = {} --key is thread, value is pack(...)ed args to resume it with
local schedDeadline = {} --key is thread, value is absolute deadline of uptime()
local schedSignal = {} --key is thread, value is `true` (may allow specific signal names in the future)
function ukitn.running()
--TODO return name, child processes, child threads, ...
--TODO return information for the current thread within the process too
return proc, nil
end
end
--[=====[ init, pid 1 ]=====]--
@ -157,11 +180,11 @@ do
local initEnv = {}
--since init is intended to be configured per-computer, it lives in /etc
local initPath = '/etc/init.lua'
local initCode = assert(readfileK(initPath))
local initFn = assert(loadK(initCode, '@' .. initPath, 'bt', initEnv))
fillEnv(initFn, initEnv)
local initCode = assert(kreadfile(initPath))
local initFn = assert(kload(initCode, '@' .. initPath, 'bt', initEnv))
local initThread = co_create(initFn)
processes[initThread] = { env = initEnv }
fillEnv(initThread, initEnv)
processes[initThread] = { env = initEnv, source = initPath }
runnable[initThread] = { initPath, n = 1 }
end
@ -190,7 +213,7 @@ local function postResume(co, ok, reason, ...)
end
end
--logic after pullSignalK returns, in its own function to use varargs instead of table.pack
--logic after kpullSignal returns, in its own function to use varargs instead of table.pack
local function postPull(signal, ...)
local args = pack(nil)
for proc, deadline in pairs(schedDeadline) do
@ -211,13 +234,13 @@ local function postPull(signal, ...)
end
--goto instead of while-true for vague performance
--goto instead of while-true for vague feeling of performance (TODO benchmark this)
::tickScheduler::
--clear the run queue
for proc, args in pairs(runnable) do
runnable[proc] = nil --the process is no longer runnable, unless it reschedules itself
postResume(proc, co_resumeK(proc, unpack(args, 1, args.n)))
postResume(proc, kresume(proc, unpack(args, 1, args.n)))
end
local deadline = nil
@ -249,7 +272,7 @@ end
--`deadline` is set, so at least one process is waiting for a deadline or signal
postPull(pullSignalK(deadline - uptime()))
postPull(kpullSignal(deadline - uptime()))
--then do it again
goto tickScheduler

Loading…
Cancel
Save