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