**This is an old revision of the document!**

自定义启动时运行程序

OpenOS 提供了多种用于自动运行程序的工具。本文档将讲述所有此类程序、说明如何使用它们,并比较它们的优缺点。请注意,这些选择中的一部分可能不可用,也有一部分不如旧版本操作系统的那么耐用。在所有本模组支持的MC版本之中,1.7.2 版本对于文档中讲述的内容提供了完整支持。下一个 OC 版本,1.7.3 版会包含所有正在开发中的修复。

在我们涉及到自动运行程序的访问点之前,本文档会先回顾前台和后台、阻塞式调用、线程,以及事件注册记录(侦听器和定时器)相关内容。

后台活动程序对比前台的活动程序

前台活动程序会打断或延迟 OpenOS 加载 shell。这种形式的进程天然适合需要等待用户输入的程序。对于开发后台程序,OpenOS 提供了线程和事件注册记录(侦听器和定时器)功能。设计自定义启动时运行程序时,用户可以自由选择前台或后台程序,以满足他们的需要。

阻塞式调用

当构建后台进程时,你需要知道什么是阻塞式调用,以及调用时会阻塞什么。有两种不同的阻塞式调用:机器阻塞和系统退让(system yielding)。

机器阻塞调用是component(组件) API 的一个子集,例如文件系统的读入操作会让整台OC电脑进入等待状态。这些阻塞式调用是不受操作系统控制的,也不受我们控制,是真正的阻塞。因此当我们谈起操作是否被阻塞时指的不是机器阻塞。在机器阻塞调用的过程中,系统中任何东西都不会运行。

作为对比,系统调用 computer.pullSignal 会令当前的线程yield(退让)(event.pullos.sleep 也会调用 computer.pullSignal)。OpenOS 会创建一个init(初始)线程,并且将所有进程都放在这个线程之内运行。如果你创造了多个协程并不断循环resume(恢复)它们,只要某一个协程进行了系统退让调用,你的循环会被阻塞在进行调用的协程处。这个协程不会yield回来。无论如何,这个线程中所有的协程都实质上被暂停了。在开发后台程序时,理解这一点是工作流程中的重要一环。如果你需要让你的应用程序在继续工作之前等待 10 秒,并且你调用了 os.sleep(10),你就同时阻断了前台的应用程序(因为它们运行在同一个线程内)。系统退让方法包括: term.read, os.sleep, 还有 event.pull

线程

回顾一下线程运行库的API文档。设计上的大多数地方都不需考虑系统其他部分的后台应用很适合用线程功能开发。

OpenOS的线程为非阻塞式进程

正如前文所言,进行系统退让调用(任何调用了computer.pullSignal的东西)会暂停当前线程中的所有协程,但是你也可以选择创建你自己的线程。其他进行系统退让调用的线程不会阻塞你的线程,你的线程也可以随意进行系统退让调用而不会阻塞其他线程。如果你希望自己的后台进程每秒钟广播一次网络信息,你可以用简短的while循环很方便地实现:

snippet.lua
thread.create(function()
  while true do
    modem.broadcast(port, msg)
    os.sleep(1)
  end
end)

OpenOS线程不可重入

线程在编写时即完全为单入口点的。这种模式更易于掌握,且允许编程者将代码构建为自己的子系统。当唯一的一个线程函数退出时,线程会死亡,也不能再resume了。还需要注意当你创建自己的线程(如线程文档所述)时它会附着到你的当前进程上。线程会阻塞其父进程的关闭,直到线程自己关闭。如果你想要运行一个完全后台,不附着于任何进程的线程,请调用:detach()

OpenOS线程“与事件协作”

事件信号会被推入队列,然后在你拉取信号时从队列移除。如果你有多个独立的代码片段都在进行pullSignal(或者event.pull)调用,那么一个进程可能会从另一个等待信号的进程那里抢走信号(通过 event.listen 注册的事件处理函数不受此影响)。线程从自身独有的信号队列中(这不会导致额外内存开销,技术层面上讲线程是注册的事件处理函数,不会被抢信号的代码影响)拉取信号,而且在设计运行于OpenOS提供的线程中的后台程序时,你也无需关心系统的其他部分与程序所需信号间的关系。

OpenOS线程不适用于低内存的系统

OpenOS 需要不到 130k 的内存启动和运行交互式shell。一根内存条-T1能提供 196k 大小的内存,这样还能留给你超过 60k 的“回旋余地”。这样的低内存状态已经很严重了。然而,引导启动的过程还没有完全加载可用的系统运行库。加载线程运行库还要额外分配大约 20k 内存,并且每个用户创建的线程也要消耗大约 5k 内存。这些数字都是保守估计,而且老实说,由于Lua虚拟机的特性和它按块分配内存的方式,这个数字不够精确。我承认,线程库可以进行优化以减少内存使用。但是这样做的话线程库的准确性和耐用性会远远不如现在这么好。未来版本可能会减少内存使用。理想情况下,你的系统应该在加载了所需的所有运行库和运行了所需程序后,仍有大于 100k 的闲置内存。

事件注册记录

事件注册记录分为侦听器与事件定时器两类。

请回顾event(事件) 运行库的api文档。事件注册记录是理想的开发后台“响应程序”的解决方案。“响应程序”指为响应某些预期的事件信号而编写的短期任务。

事件注册记录是可重入的

事件处理函数是一个回调函数,每当它名下注册的条件被满足时都将被调用。

snippet.lua
event.listen("key_down", function(...)
  handle_key_down(...)
end)

handle_key_down()函数在每次出现key_down信号时都会被调用。

snippet.lua
  event.timer(1, function()
    onTimeout()
  end, 10)

onTimeout()函数会被每1秒调用一次,总共10次(详见event(事件) api)。

回调函数也可以选择返回falsenil与此不同,必须为false)以注销自身。返回其他值或不返回任何值则不会注销回调函数。定时器会在调用次数达到times(第三个参数)时自动注销。

这种可重入的行为有助于编程以基于事件执行小的重复的任务,但是不便于构建长期在后台运行的,需要完成多种不同任务的系统。事件注册记录可能不适用于需要保持状态的或者完成大量工作的,以及与系统事件完全无关的程序。显然,上述内容并不是规定,只是一些未考虑你特殊需求背景的主观考虑。

事件注册记录是轻量化的

不像线程,事件注册系统的性能开销很少。最基础的注册event.listen("key_down", function()end)可以做到只消耗 400 字节的内存(你可能会觉得这也是很大的开销,那么欢迎进入Lua虚拟机的世界)。OpenOS已经使用了很多事件注册记录,并且系统已经围绕这些注册记录进行了深度优化,以增加可靠性与减少内存开销。

事件注册记录是阻塞式的

如果你刚才跳过了,那么请阅读前文的阻塞式调用部分。在启动时,OpenOS运行在一个init线程上,并且如果线程中的任何部分进行了系统退让调用computer.pullSignal(或者其他调用此函数的方法,例如event.pull),此线程中创建的所有事件注册记录都会暂停。因此,如果你还想让你的系统对前台应用(例如shell)作出响应,那么在事件注册记录里面调用os.sleep就很不明智。

自动程序执行的进入点

下列的进入点就像“钩子”,或者脚本位置。通过这些进入点你可以在电脑启动时运行你的后台或前台应用。

Interactive Shell Startup (.shrc)

The last boot process to load is the OpenOS shell. The shell blocks until a tty output is available. This means that if there is no gpu or no screen, the shell startup will wait.

After a stdout for tty becomes available, the shell will finish loading and will execute /etc/profile.lua which loads aliases and sets environment variables. The last thing /etc/profile.lua will do is source your /home/.shrc file, which by default is an empty file. source does not run lua code, but instead runs each line in the file as a shell command. If you have a script you want to run when the shell loads, put the path to your script in your .shrc. .shrc is run each time the shell is loaded, which may be more than once per boot. The user could type 'exit', or ^d, or even send a hard interrupt signal and kill the shell (and the init process will load a new one).

I recommend editing /home/.shrc rather than /etc/profile.lua purely for organizational purposes.

Runscripts (rc)

Review the rc documentation.

/bin/rc can be used to enable boot level scripts. RC scripts are started even on systems with no shell, no gpu, no screen, no keyboard.

Filesystem Autorun (autorun.lua)

Relative to the root of any filesystem, you can create a file named autorun.lua (or .autorun.lua). When that filesystem component is first detected OpenOS will automatically run the file. Note that /home/autorun.lua is not at the root of rootfs. This also applies to the rootfs. This autorun will execute each and every time the filesystem component is added to the system (e.g. you can remove and re-insert a floppy disk with an autorun).

The feature is enabled by default, and can be disabled on a rw filesystem by either calling filesystem.setAutorunEnabled(false), or by modifying /etc/filesystem.cfg directly: autorun=false.

Boot Scripts (/boot/)

This option is really a non-option, documented here to disuade users with reasonable arguments against doing so.

OpenOS runs boot scripts (sorted by their filenames) in /boot/ for its core operations. While it is possible to install custom boot scripts along side the kernel boot scripts, it is quite unadvisable to do so.

Installing a custom boot script (in /boot/) poses the risk that your boot script may be run before core libraries are available. There is no guarantee that even invoking require in a boot script is safe in the current version OpenOS, or will be safe in future OpenOS updates (as I may change the boot order).

There may not be a fully initialized io, there may be an incomplete init process, there may even be incomplete lua libraries. Depending on the code you execute in your boot script, you may even unintentionally circumvent the event dispatching system causing the system to miss component updates. Yes, there is a lot that the boot process is responsible for.

With all of that said, here are a couple examples of /boot scripts that would probably work now and for the foreseeable future. Prefix your script filename 99_ so that it loads at the end of the boot sequence. If anything doesn't work like you'd expect (such as printing to stdout, or reading from stdin), it isn't a bug and isn't supported. In other words, use the /boot/ script directory at you own risk. If you need stdout, you can also wait for the term_available signal. Again, this is not an officially supported option.

snippet.lua
local event = require("event")
-- the init signal is fired by the boot process, means the system is ready
event.listen("init", function()
  local thread = require("thread")
  thread.create(function()
    --[[
      your custom service code as a background thread
    ]]--
  end):detach()
end)
snippet.lua
local event = require("event")
event.listen("component_added", function(...)
  --[[
    your custom service code as a background event responder
  ]]--
end)

目录