add a kernel that can run code in userspace
parent
ff3f9c666f
commit
ca37976828
@ -0,0 +1,258 @@
|
||||
--our own boot.lua gives us the boot [address, path] as arguments...
|
||||
local bootfs = ...
|
||||
-- ...but [lua bios, openloader] don't, so fall back to the usual way
|
||||
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
|
||||
--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 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 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
|
||||
|
||||
|
||||
--completely disable the environment, ensuring the kernel only uses local variables, to prevent userspace
|
||||
--mischief (somehow editing _G through a mirror) from affecting kernelspace
|
||||
_ENV = setmetatable({}, {
|
||||
__index = function(self, key) return error(format('accessed global %q', key), 2) end,
|
||||
__newindex = function(self, key) return error(format('assigned global %q', key), 2) end,
|
||||
})
|
||||
|
||||
local function readfileK(path, fs)
|
||||
fs = fs or bootfs
|
||||
local text, chunk, fd, err = '', nil, invoke(fs, 'open', path)
|
||||
if not fd then
|
||||
local msg = format('can\'t open %s on %s', path, fs:sub(1, 3))
|
||||
if not rawequal(path, err) then msg = msg .. ': ' .. err end
|
||||
return nil, msg
|
||||
end
|
||||
|
||||
repeat
|
||||
chunk, err = invoke(fs, 'read', fd, big)
|
||||
text = text .. (chunk or '')
|
||||
until not chunk
|
||||
invoke(fs, 'close', fd)
|
||||
if err then return nil, format('reading %s: %s', path, err) end
|
||||
return text
|
||||
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
|
||||
|
||||
|
||||
--[=====[ 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
|
||||
|
||||
--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`
|
||||
local mirrors = setmetatable({}, { __mode = 'k' }) --key is mirror given to userspace, value is what it mirrors
|
||||
|
||||
local function mirrorNext(self, prev)
|
||||
local reflected = assert(mirrors[self], 'mirror reflects nothing')
|
||||
local key = next(reflected, prev)
|
||||
return key, self[key] --ensure all accesses go through the mirror so table values are mirrored too
|
||||
end
|
||||
|
||||
local mirrormt = {
|
||||
__metatable = '<mirror>', --hide the metatable to prevent replacing or modifying it
|
||||
__index = function(self, key)
|
||||
local reflected = assert(mirrors[self], 'mirror reflects nothing')
|
||||
local val = reflected[key] --if mirrors[self] is somehow nil, erroring is fine
|
||||
if type(val) == 'table' then val = mirror(val) end --recursively mirror tables on first access
|
||||
if val ~= nil then rawset(self, key, val) end
|
||||
return val
|
||||
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)
|
||||
return mirrorNext, self
|
||||
end,
|
||||
--mirrors allow assignment but don't do anything with it
|
||||
}
|
||||
|
||||
function mirror(target, mirror)
|
||||
mirror = mirror or {}
|
||||
mirrors[mirror] = target
|
||||
setmetatable(mirror, mirrormt)
|
||||
return mirror
|
||||
end
|
||||
|
||||
function fillEnv(process, env)
|
||||
--<https://ocdoc.cil.li/tutorial:custom_oses#what_s_available>
|
||||
mirror(envBase, env)
|
||||
|
||||
local function env_load(chunk, name, mode, fenv)
|
||||
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)
|
||||
end
|
||||
local function env_loadfile(path, mode, fenv)
|
||||
checkArg(1, path, 'string')
|
||||
checkArg(2, mode, 'string', 'nil')
|
||||
checkArg(3, newenv, 'table', 'nil')
|
||||
|
||||
local code, err = readfileK(path)
|
||||
if not code then return nil, err end
|
||||
|
||||
code, err = env_load(code, '@' .. path, mode, fenv)
|
||||
return code, err
|
||||
end
|
||||
local function env_dofile(path, ...)
|
||||
checkArg(1, path, 'string')
|
||||
local fn = assert(env_loadfile(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)
|
||||
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
|
||||
|
||||
|
||||
--[=====[ 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)
|
||||
|
||||
|
||||
--[=====[ init, pid 1 ]=====]--
|
||||
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 initThread = co_create(initFn)
|
||||
processes[initThread] = { env = initEnv }
|
||||
runnable[initThread] = { initPath, n = 1 }
|
||||
end
|
||||
|
||||
|
||||
--[=====[ scheduler main loop ]=====]--
|
||||
--logic after co_resume(process, ...) returns, in its own function to use varargs instead of table.pack
|
||||
local function postResume(co, ok, reason, ...)
|
||||
if co_status(co) == 'dead' then
|
||||
processes[co], runnable[co], schedDeadline[co], schedSignal[co] = nil
|
||||
--TODO bubble to parent process
|
||||
|
||||
--tostring could run userspace code and even yield, but at this point we're crashing so it doesn't matter
|
||||
if not ok then error('process crashed: ' .. tostring(reason), 0) end
|
||||
elseif not reason then
|
||||
--userspace called coroutine.yield(); TODO figure out what that's supposed to do
|
||||
--for now... yield until another process resumes it?
|
||||
schedDeadline[co], schedSignal[co] = nil
|
||||
elseif rawequal(reason, 'signal') then
|
||||
local timeout = ...
|
||||
schedDeadline[co] = timeout and (uptime() + timeout)
|
||||
schedSignal[co] = true
|
||||
elseif rawequal(reason, 'deadline') then
|
||||
local timeout = ...
|
||||
schedDeadline[co] = uptime() + timeout
|
||||
schedSignal[co] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--logic after pullSignalK 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
|
||||
if uptime() >= deadline then
|
||||
schedDeadline[proc] = nil
|
||||
runnable[proc] = args
|
||||
end
|
||||
end
|
||||
|
||||
--intentionally override {resumed by deadline} with {resumed by signal}
|
||||
if signal then
|
||||
args = pack(signal, ...)
|
||||
for proc in pairs(schedSignal) do
|
||||
schedDeadline[proc], schedSignal[proc] = nil
|
||||
runnable[proc] = args
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--goto instead of while-true for vague performance
|
||||
::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)))
|
||||
end
|
||||
|
||||
local deadline = nil
|
||||
for proc, pdeadline in pairs(schedDeadline) do
|
||||
if not deadline or pdeadline < deadline then
|
||||
deadline = pdeadline
|
||||
end
|
||||
end
|
||||
|
||||
if next(schedSignal) then
|
||||
deadline = deadline or huge
|
||||
end
|
||||
|
||||
if not deadline then
|
||||
--no processes are scheduled? are any still runnable somehow?
|
||||
if next(runnable) then
|
||||
--tail-return early to jump back to the run queue
|
||||
goto tickScheduler
|
||||
end
|
||||
|
||||
--nothing scheduled, nothing runnable; have all processes exited?
|
||||
if not next(processes) then
|
||||
return shutdown()
|
||||
end
|
||||
|
||||
--this seems like a deadlock; the scheduler will never resume any process again
|
||||
error('all processes are idle', 0)
|
||||
end
|
||||
|
||||
|
||||
--`deadline` is set, so at least one process is waiting for a deadline or signal
|
||||
postPull(pullSignalK(deadline - uptime()))
|
||||
|
||||
--then do it again
|
||||
goto tickScheduler
|
||||
|
||||
|
||||
-- vi: set ts=2:
|
Loading…
Reference in New Issue