kernel: split init.lua into separate files, concatenated by build.lua
also properly implements forwarding os yieldsmain
parent
563c63a1c8
commit
5014e23401
@ -1,298 +0,0 @@
|
||||
--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
|
||||
--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, 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 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 kreadfile
|
||||
|
||||
|
||||
--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 kreadfile(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 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.1.1' --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`
|
||||
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, for vague feelings of less memory usage (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(proc, uenv)
|
||||
mirror(envBase, uenv)
|
||||
uenv._G = uenv
|
||||
|
||||
local function uload(chunk, name, mode, env)
|
||||
checkArg(1, chunk, 'function', 'string')
|
||||
checkArg(2, name, 'string', 'nil')
|
||||
checkArg(3, mode, 'string', 'nil')
|
||||
checkArg(4, env, 'table', 'nil')
|
||||
return kload(chunk, name, mode, env or uenv)
|
||||
end
|
||||
local function uloadfile(path, mode, env)
|
||||
checkArg(1, path, 'string')
|
||||
checkArg(2, mode, 'string', 'nil')
|
||||
checkArg(3, env, 'table', 'nil')
|
||||
|
||||
local code, err = kreadfile(path)
|
||||
if not code then return nil, err end
|
||||
|
||||
code, err = uload(code, '@' .. path, mode, env)
|
||||
return code, err
|
||||
end
|
||||
local function udofile(path, ...)
|
||||
checkArg(1, path, 'string')
|
||||
local fn = assert(uloadfile(path))
|
||||
return fn(...)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
function ukitn.createProcess(path, ...)
|
||||
checkArg(1, path, 'string')
|
||||
local env = {}
|
||||
local fn, err = uloadfile(path, nil, env)
|
||||
if not fn then return nil, err end
|
||||
|
||||
local newProc = co_create(fn)
|
||||
fillEnv(newProc, env)
|
||||
processes[newProc] = { env = env, parent = proc, path = path }
|
||||
runnable[newProc] = pack(...)
|
||||
return newProc
|
||||
end
|
||||
|
||||
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 ]=====]--
|
||||
do
|
||||
local initEnv = {}
|
||||
--since init is intended to be configured per-computer, it lives in /etc
|
||||
local initPath = '/etc/init.lua'
|
||||
local initCode = assert(kreadfile(initPath))
|
||||
local initFn = assert(kload(initCode, '@' .. initPath, 'bt', initEnv))
|
||||
local initThread = co_create(initFn)
|
||||
fillEnv(initThread, initEnv)
|
||||
processes[initThread] = { env = initEnv, source = initPath }
|
||||
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], 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 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
|
||||
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 feeling of performance (TODO benchmark this)
|
||||
::tickScheduler::
|
||||
|
||||
--[[clear the run queue
|
||||
this is a delicate formulation. if proc creates new threads, those are added as new keys to runnable, which
|
||||
makes next's behavior undefined*; removing proc before resuming causes the next loop iteration to crash with
|
||||
"invalid key to 'next'". unconditionally removing proc after is also wrong, because proc may have rescheduled
|
||||
itself immediately. instead we assign proc a value it should never have (false) before, test if it's still set
|
||||
to that value after, and remove it if so.
|
||||
|
||||
the outer loop is for the occasional case that some of the child processes end up before their parent in next's
|
||||
order and so wouldn't be resumed until after another signal was pulled otherwise.
|
||||
|
||||
*is this c-like ub that can cause memory corruption and segfaults? the manual doesn't say! (TODO make this not ub)
|
||||
]]
|
||||
while next(runnable) do
|
||||
for proc, args in pairs(runnable) do
|
||||
runnable[proc] = false
|
||||
postResume(proc, kresume(proc, unpack(args, 1, args.n)))
|
||||
if not runnable[proc] then
|
||||
runnable[proc] = nil
|
||||
end
|
||||
end
|
||||
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(kpullSignal(deadline - uptime()))
|
||||
|
||||
--then do it again
|
||||
goto tickScheduler
|
||||
|
||||
|
||||
-- vi: set ts=2:
|
@ -0,0 +1,29 @@
|
||||
--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
|
||||
--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, 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 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 kreadfile
|
||||
|
||||
|
||||
--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,
|
||||
})
|
||||
|
||||
-- vi: set ts=2:
|
@ -0,0 +1,38 @@
|
||||
--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`
|
||||
|
||||
do
|
||||
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, for vague feelings of less memory usage (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
|
||||
end
|
||||
|
||||
-- vi: set ts=2:
|
@ -0,0 +1,59 @@
|
||||
local function kreadfile(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
|
||||
|
||||
|
||||
--[[table of process information. key is the main thread's coroutine, value is a table with:
|
||||
- array part: coroutines for child threads and processes
|
||||
- `parent`: coroutine of parent process
|
||||
- `env`: process environment
|
||||
- `path`: filesystem path the process was loaded from (for diagnostics)
|
||||
]]
|
||||
local processes = {}
|
||||
|
||||
--run queue. key is coroutine (process or thread), value is pack()ed args to resume it with.
|
||||
--`runqueue[co]` is temporarily set to `false` while the thread is running.
|
||||
local runqueue = {}
|
||||
|
||||
--tasks waiting for a timeout. key is coroutine (process or thread), value is absolute deadline (from uptime()).
|
||||
local schedDeadline = {}
|
||||
|
||||
--tasks waiting for a signal. key is coroutine (process or thread), value is `true` for all signals (may support specific signals in the future).
|
||||
local schedSignal = {}
|
||||
|
||||
|
||||
local fillEnv --function(proc:thread, env:table):() -- given a process and an empty environment, fill it in before the process runs
|
||||
|
||||
local function createProcess(path, parent)
|
||||
local code, err = kreadfile(path)
|
||||
if not code then return nil, err end
|
||||
local env = {}
|
||||
|
||||
--using kload here is okay because we always pass a non-nil environment
|
||||
local f, err = kload(code, '@' .. path, 'bt', env)
|
||||
if not f then return nil, err end
|
||||
|
||||
local co = co_create(f)
|
||||
fillEnv(co, env)
|
||||
|
||||
local info = { env = env, parent = parent, path = path }
|
||||
processes[co] = info
|
||||
|
||||
return co, info
|
||||
end
|
||||
|
||||
-- vi: set ts=2:
|
@ -0,0 +1,115 @@
|
||||
--[[environments for new processes
|
||||
given <https://ocdoc.cil.li/tutorial:custom_oses#what_s_available>:
|
||||
- replace load to default to the current process's environment, not the global environment
|
||||
- provide [loadfile, dofile] using the boot filesystem and the current process's environment
|
||||
- replace computer.pullSignal and provide os.sleep to yield from the current coroutine, not the entire computer
|
||||
- replace coroutine.yield to differentiate its yields from os yields (pullSignal/sleep)
|
||||
- replace coroutine.resume to pass os yields through, suspending the os thread and not just the current coroutine
|
||||
- provide kitn namespace for os functions
|
||||
]]
|
||||
|
||||
|
||||
envBase._OSVERSION = 'kitn 0.1.2' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver"
|
||||
envBase.kitn = {}
|
||||
envBase._G, envBase.load = nil
|
||||
|
||||
do
|
||||
--[[propogating os yields
|
||||
when a process calls coroutine.yield(), we prepend `false` to the yielded values. os yields start with a truthy value.
|
||||
in coroutine.resume(), if the coroutine yielded (ie isn't dead) and the first value is truthy (ie an os yield),
|
||||
we also yield the current coroutine, unwinding the current stack of running coroutines back to the os thread.
|
||||
when the current coroutine is resumed, we pass the values back into the inner coroutine and repeat.
|
||||
]]
|
||||
envBase.coroutine.yield = function(...) return kyield(false, ...) end
|
||||
|
||||
local forwardYield
|
||||
envBase.coroutine.resume = function(co, ...)
|
||||
checkArg(1, co, 'thread')
|
||||
|
||||
return forwardYield(co, kresume(co, ...))
|
||||
end
|
||||
|
||||
function forwardYield(co, ok, ...)
|
||||
if co_status(co) ~= 'suspended' then --it's not a sysyield if the coroutine didn't yield (because it returned/errored)
|
||||
return ok, ...
|
||||
elseif (...) then --firest arg is truthy for os yield, false for coroutine.yield()
|
||||
--yield to the scheduler, pass the results (ie signal) back to `co`, then repeat for `co`'s next yield
|
||||
return forwardYield(co, kresume(co, kyield(...)))
|
||||
else --`co` called coroutine.yield(), so return its values to the waiting coroutine.resume()
|
||||
return ...
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
function fillEnv(proc, uenv)
|
||||
mirror(envBase, uenv)
|
||||
uenv._G = uenv
|
||||
|
||||
--define these in local variables so the process deleting `load` doesn't break its `loadfile`
|
||||
local function uload(chunk, name, mode, env)
|
||||
checkArg(1, chunk, 'function', 'string')
|
||||
checkArg(2, name, 'string', 'nil')
|
||||
checkArg(3, mode, 'string', 'nil')
|
||||
checkArg(4, env, 'table', 'nil')
|
||||
return kload(chunk, name, mode, env or uenv)
|
||||
end
|
||||
local function uloadfile(path, mode, env)
|
||||
checkArg(1, path, 'string')
|
||||
checkArg(2, mode, 'string', 'nil')
|
||||
checkArg(3, env, 'table', 'nil')
|
||||
|
||||
local code, err = kreadfile(path)
|
||||
if not code then return nil, err end
|
||||
|
||||
code, err = uload(code, '@' .. path, mode, env)
|
||||
--`return code, err` here would return 2 values on success, unlike vanilla loadfile
|
||||
if not code then return nil, err end
|
||||
return code
|
||||
end
|
||||
local function udofile(path, ...)
|
||||
checkArg(1, path, 'string')
|
||||
local fn = assert(uloadfile(path))
|
||||
return fn(...)
|
||||
end
|
||||
|
||||
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 co = co_create(fn)
|
||||
local info = processes[proc]
|
||||
info[#info+1] = co --add it as a thread of the current process
|
||||
runnable[co] = pack(...)
|
||||
return co
|
||||
end
|
||||
|
||||
function ukitn.createProcess(path, ...)
|
||||
checkArg(1, path, 'string')
|
||||
local co, info = createProcess(path, proc)
|
||||
if not co then return nil, info end
|
||||
runqueue[co] = pack(...)
|
||||
return co
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
-- vi: set ts=2:
|
@ -0,0 +1,103 @@
|
||||
--init is intended to be configured per-computer, so it lives in /etc/
|
||||
local initCo, initInfo = createProcess('/etc/init.lua', nil) --no parent
|
||||
if not initCo then
|
||||
error(initInfo, 0)
|
||||
end
|
||||
runqueue[initCo] = { initInfo.path, n = 1 }
|
||||
|
||||
|
||||
local reschedule, dispatchSignal
|
||||
|
||||
--given the yield values of a coroutine (process or thread), reschedule it as necessary or mark the process as crashed.
|
||||
local function postResume(co, ok, reason, ...)
|
||||
if co_status(co) == 'dead' then
|
||||
processes[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 kpullSignal returns, in its own function to use varargs instead of table.pack
|
||||
local function postPull(signal, ...)
|
||||
local args
|
||||
for proc, deadline in pairs(schedDeadline) do
|
||||
if uptime() >= deadline then
|
||||
args = args or { n = 1 } --resume with a single nil
|
||||
schedDeadline[proc] = nil
|
||||
runqueue[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
|
||||
schedSignal[proc] = nil
|
||||
runqueue[proc] = args
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--goto instead of while-true for vague feeling of performance (TODO benchmark this)
|
||||
::tickScheduler::
|
||||
|
||||
--[[clear the run queue
|
||||
this is a delicate formulation. if proc creates new threads, those are added as new keys to runnable, which
|
||||
makes next's behavior undefined*; removing proc before resuming causes the next loop iteration to crash with
|
||||
"invalid key to 'next'". unconditionally removing proc after is also wrong, because proc may have rescheduled
|
||||
itself immediately. instead we assign proc a value it should never have (false) before, test if it's still set
|
||||
to that value after, and remove it if so.
|
||||
|
||||
the outer loop is for the occasional case that some of the child processes end up before their parent in next's
|
||||
order and so wouldn't be resumed until after another signal was pulled otherwise.
|
||||
|
||||
*is this c-like ub that can cause memory corruption and segfaults? the manual doesn't say! (TODO make this not ub)
|
||||
]]
|
||||
while next(runqueue) do
|
||||
for proc, args in pairs(runqueue) do
|
||||
runqueue[proc] = false
|
||||
postResume(proc, kresume(proc, unpack(args, 1, args.n)))
|
||||
if not runqueue[proc] then
|
||||
runqueue[proc] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local deadline = nil --absolute deadline to wait until for a signal; only set to nonnil if there is something waiting
|
||||
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 deadline then --at least one process/thread is waiting
|
||||
postPull(kpullSignal(deadline - uptime()))
|
||||
goto tickScheduler --then do it again
|
||||
elseif next(runqueue) then --nothing is scheduled; is something still runnable somehow?
|
||||
goto tickScheduler --jump back to drain the runqueue again
|
||||
elseif next(processes) then --nothing scheduled or runnable; are there any processes at all?
|
||||
error('all processes are idle', 0) --deadlock
|
||||
else --no processes remaining, everything exited cleanly
|
||||
shutdown()
|
||||
end
|
||||
|
||||
-- vi: set ts=2:
|
Loading…
Reference in New Issue