Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
tutorial:autorun_options:zh [2023/12/02 11:06]
hfsr [自定义启动时运行程序]
tutorial:autorun_options:zh [2023/12/03 10:20] (current)
hfsr [引导启动脚本(/boot/)]
Line 1: Line 1:
-======自定义启动时运行程序======+======自定义启动程序======
  
 OpenOS 提供了多种用于自动运行程序的工具。本文档将介绍所有此类工具、说明如何使用它们,并比较它们的优缺点。请注意,这些选择中的一部分可能不可用,也有一部分不如旧版本操作系统的那么耐用。模组的1.7.2开发版本(对所有本模组支持的MC版本而言)对于文档中讲述的内容提供了完整支持。下一个 OC 版本,1.7.3版会包含开发版本的所有修复。 OpenOS 提供了多种用于自动运行程序的工具。本文档将介绍所有此类工具、说明如何使用它们,并比较它们的优缺点。请注意,这些选择中的一部分可能不可用,也有一部分不如旧版本操作系统的那么耐用。模组的1.7.2开发版本(对所有本模组支持的MC版本而言)对于文档中讲述的内容提供了完整支持。下一个 OC 版本,1.7.3版会包含开发版本的所有修复。
Line 5: Line 5:
 在我们涉及到自动运行程序的访问点之前,本文档会先回顾前台和后台、阻塞式调用、线程,以及事件注册记录(侦听器和定时器)相关内容。 在我们涉及到自动运行程序的访问点之前,本文档会先回顾前台和后台、阻塞式调用、线程,以及事件注册记录(侦听器和定时器)相关内容。
  
-====后台活动程序对比前台的活动程序====+====后台程序前台交互程序====
  
-前台活动程序会打断或延迟 OpenOS 加载 shell。这种式的进程天适合需要等待用户输入的程序。对于开发后台程序,OpenOS 提供了线程和事件注册记录(侦听器和定时器)功能。设计自定义启动时运行程序时,用户可以自由选择前台或后台程序,以满足他们的需要。+前台交互程序会打断或延迟OpenOS加载 shell。这种式的进程天适合需要等待用户输入的程序。对后台程序的开发,OpenOS提供了线程和事件注册记录(侦听器和定时器)功能。设计自启动程序时,用户可以自由选择前台或后台程序,以满足他们的需要。
  
 ====阻塞式调用==== ====阻塞式调用====
  
-构建后台进程时,你需要知道什么是阻塞式调用,以及调用时会阻塞什么。有两种不同的阻塞式调用:机器阻塞和系统退让(system yielding)+构建后台进程时,你需要知道什么是阻塞式调用,以及调用时会阻塞什么。有两种不同的阻塞式调用:机器阻塞和系统退让。
  
-机器阻塞调用component(组件) API 的一个子集,例如文件系统的读入操作会让整台OC电脑进入等待状态。这些阻塞式调用不受操作系统控制,也不受我们控制,是真正的阻塞。因此当我们谈起操作是否被阻塞时指的不是机器阻塞。在机器阻塞调用的过程中,系统中任何东西都不会运行。+机器阻塞调用component(组件) API的一部分函数,例如文件系统的读入操作,这种调用会让整台OC电脑进入等待状态。这些阻塞式调用不受操作系统控制,也不受我们控制,是真正的阻塞。因此当我们讨论操作是否被阻塞时指的不是机器阻塞。在机器阻塞调用的过程中,系统中的一切都不会运行。
  
-作为对比,系统调用 `computer.pullSignal` 会令当前线程yield(退让)(`event.pull` 和 `os.sleep` 也会调用 `computer.pullSignal`)。OpenOS 会创建一个init(初始)线程,并且将所有进程都放在这个线程之内运行。如果你创了多个协程并不断循环resume(恢复)它们,只要某一个协程进行了系统退让调用,你的循环会被阻塞在进行调用的协程处。这个协程不会yield回来。无论如何,这个线程中所有的协程都实质上被暂停了。在开发后台程序时,理解这一点是工作流程中的重要一环。如果你需要让你的应用程序继续工作之前等待 10 秒,并且你调用了 `os.sleep(10)`,你就同时阻断了前台的应用程序(因为它们运行在同一个线程内)。系统退让方法包括: `term.read``os.sleep`还有 `event.pull`。+作为对比,系统调用`computer.pullSignal`会令当前线程yield(退让)(`event.pull`和`os.sleep`也会调用`computer.pullSignal`)。OpenOS会创建一个init(初始)线程,并且将所有进程都放在这个线程之内运行。假设你创了多个协程并不断循环resume(恢复)它们,只要某一个协程进行了系统退让调用,你的循环会被阻塞在进行调用的协程处。这个协程不会yield回来。无论如何,这个线程中所有的协程都实质上被暂停了。在开发后台应用时,理解这一点是工作流程中的重要一环。如果你需要让你的应用程序等待10秒再继续工作,并且你调用了`os.sleep(10)`,你就同时阻断了前台的应用程序(因为它们运行在同一个线程内)。系统退让方法包括:`term.read``os.sleep`还有`event.pull`。
 ====线程==== ====线程====
  
-回顾一下线程运行库的[[api:​thread:​zh|API文档]]。设计上的大多数地方都不需考虑系统其他部分的后台应用很适合用线程功能开发+回顾一下线程运行库的[[api:​thread:​zh|API文档]]。线程适用于开发运行时考虑系统其他部分的后台应用。
  
 ===OpenOS的线程为非阻塞式进程=== ===OpenOS的线程为非阻塞式进程===
  
-正如前文所言,进行系统退让调用(任何调用了`computer.pullSignal`的东西)会暂停当前线程中的所有协程,但是你也可以选择创建你自己的线程。其他进行系统退让调用的线程不会阻塞你的线程,你的线程也可以随意进行系统退让调用而不会阻塞其他线程。如果你希望自己的后台进程每秒钟广播一次网络信息,你可以简短的while循环很方便地实现:+正如前文所言,进行系统退让调用(任何调用了`computer.pullSignal`的事物)会暂停当前线程中的所有协程,但是你也可以选择创建你自己的线程。其他进行系统退让调用的线程不会阻塞你的线程,你的线程也可以随意进行系统退让调用而不会阻塞其他线程。如果你希望自己的后台进程每秒钟广播一次网络信息,你可以写一个简短的while循环并轻易实现:
  
 ```lua ```lua
Line 35: Line 35:
 ===OpenOS线程不可重入=== ===OpenOS线程不可重入===
  
-线程在编写时即完全为单入口点的。这种模式更易于掌握,且允许编程者将代码构建为自己的子系统。当唯一的一个线程函数退出时,线程会死亡,也不能再resume了。还需要注意当你创建自己的线程(如[[api:​thread:​zh|线程文档]]所述)时它会附着到你的当前进程上。线程会阻塞其父进程的关闭,直到线程自己关闭。如果你想要运行一个完全后台,不附着于任何进程的线程,请调用`:​detach()`。+线程在编写时即完全为单入口点的。这种模式更易于掌握,且允许编程者将代码构建为自己的子系统。当唯一的一个线程函数退出时,线程会死亡,也不能再resume了。还需要注意当你创建自己的线程(如[[api:​thread:​zh|线程文档]]所述)时它会附着到当前进程上。线程会阻塞其父进程的关闭,直到线程自己关闭。如果你想要运行一个完全后台,不附着于任何进程的线程,请调用`:​detach()`。
  
 ===OpenOS线程“与事件协作”=== ===OpenOS线程“与事件协作”===
  
-事件信号会被推入队列,然后在你拉取信号时从队列移除。如果你有多个独立的代码片段都在进行`pullSignal`(或`event.pull`)调用,那么一个进程可能会从另一个等待信号的进程那里抢走信号(通过 `event.listen` 注册的事件处理函数不受此影响)。线程从自身独有的信号队列中(这不会导致额外内存开销,技术层面上讲线程是注册的事件处理函数,不会被抢信号的代码影响)拉取信号,而且在设计运行于OpenOS提供的线程中的后台程序时,你也无需关心系统的其他部分与程序所需信号间的关系。+事件信号会被推入队列,然后在你拉取信号时从队列移除。如果你有多个独立的代码片段都在进行`pullSignal`(或`event.pull`)调用,那么一个进程可能会从另一个等待信号的进程那里抢走信号(通过`event.listen`注册的事件处理函数不受此影响)。线程从自身独有的信号队列中(这不会导致额外内存开销,技术层面上讲线程是注册的事件处理函数,不会被抢信号的代码影响)拉取信号,而且在设计运行于OpenOS提供的线程中的后台程序时,你也无需关心系统的其他部分与程序所需信号间的关系。
  
 ===OpenOS线程不适用于低内存的系统=== ===OpenOS线程不适用于低内存的系统===
  
-OpenOS 需要不到 130k 的内存启动和运行交互式shell。一根内存条-T1能提供 196k 大小的内存,这样还能留给你超过 60k 的“回旋余地”。这样的低内存状态已经很严重了。然而,引导启动的过程还没有完全加载可用的系统运行库。加载线程运行库还要额外分配大约 20k 内存,并且每个用户创建的线程也要消耗大约 5k 内存。这些数字都是保守估计,而且老实说,由于Lua虚拟机的特性和它按块分配内存的方式,这个数字不够精确。我承认,线程库可以进行优化以减少内存使用。但是这样做的话线程库的准确性和耐用性会远远不如现在这么好。未来版本可能会减少内存使用。理想情况下,你的系统应该在加载了所需的所有运行库和运行了所需程序后,仍有大于 100k 的闲置内存。+OpenOS需要不到130k的内存即可启动和运行交互式shell。一根1级内存条能提供196k大小的内存,这样还能留给你超过60k的“回旋余地”。如此的低内存状态已经很危险了。然而,引导启动的过程还没有完全加载可用的系统运行库。加载线程运行库还要额外分配大约20k内存,并且每个用户线程也要消耗大约 5k 内存。这些数字都是保守估计,而且老实说,由于Lua虚拟机的特性和它按块分配内存的方式,这个数字不够精确。我承认,线程库可以进行优化以减少内存使用。但是这样做的话线程库的准确性和耐用性会远远不如现在这么好。未来版本可能会减少内存使用。理想情况下,你的系统应该在加载了所需运行库和运行了所需程序后,仍有大于100k的闲置内存。
 ====事件注册记录==== ====事件注册记录====
  
 事件注册记录分为**侦听器**与事件**定时器**两类。 事件注册记录分为**侦听器**与事件**定时器**两类。
  
-请回顾event(事件) 运行库的[[api:​event:​zh|api文档]]。事件注册记录是理想的开发后台“响应程序”的解决方案。“响应程序”指为响应某些预期的事件信号而编写的短期任务。+请回顾event(事件) 运行库的[[api:​event:​zh|API文档]]。事件注册记录是理想的开发后台“响应”的解决方案。“响应”指为响应某些预期的事件信号而启动的短期任务。
  
 ===事件注册记录是可重入的=== ===事件注册记录是可重入的===
Line 72: Line 72:
 回调函数也可以选择返回`false`(`nil`与此不同,必须为`false`)以注销自身。返回其他值或不返回任何值则不会注销回调函数。定时器会在调用次数达到`times`(第三个参数)时自动注销。 回调函数也可以选择返回`false`(`nil`与此不同,必须为`false`)以注销自身。返回其他值或不返回任何值则不会注销回调函数。定时器会在调用次数达到`times`(第三个参数)时自动注销。
  
-这种可重入的行为有助于编程以基于事件执行小的重复的任务,但是不便于构建长期在后台运行的,需要完成多种不同任务的系统。事件注册记录可能不适用于需要保持状态的或者完成大量工作的,以及与系统事件完全无关的程序。显然,上述内容并不是规定,只是一些未考虑你特殊需求背景的主观考虑+这种可重入的行为模式适合基于事件执行小的重复出现的任务,但是不便于构建长期在后台运行的,需要完成多种不同任务的系统。事件注册记录可能不适需要保持状态的或者完成大量工作的,以及与系统事件完全无关的程序。显然,上述内容并不是规定,只是一些未考虑你特殊需求背景的主观意见
  
 ===事件注册记录是轻量化的=== ===事件注册记录是轻量化的===
  
-不像线程,事件注册系统的性能开销很少。最基础的注册`event.listen("​key_down",​ function()end)`可以做到只消耗 400 字节的内存(你可能会觉得这也是很大的开销,那么欢迎进入Lua虚拟机的世界)。OpenOS已经使用了很多事件注册记录,并且系统已经围绕这些注册记录进行了深度优化,以增加可靠性与减少内存开销。+不像线程,事件注册系统的性能开销很少。最基础的注册记录`event.listen("​key_down",​ function()end)`可以做到只消耗400字节的内存(你可能会觉得这也是很大的开销,那么欢迎进入Lua虚拟机的世界)。OpenOS已经使用了很多事件注册记录,并且系统已经围绕这些注册记录进行了深度优化,以增加可靠性与减少内存开销。
  
 ===事件注册记录是阻塞式的=== ===事件注册记录是阻塞式的===
  
-如果你刚才跳过了,那么请阅读前文的[[tutorial:​autorun_options:​zh#​阻塞式调用|阻塞式调用]]部分。在启动时,OpenOS运行在个init线程上,并且如果线程中的任何部分进行了系统退让调用`computer.pullSignal`(或者其他调用此函数的方法,例如`event.pull`),此线程中创建的所有事件注册记录都会暂停。因此,如果你还想让你的系统对前台应用(例如shell)作出响应,那么在事件注册记录里面调用`os.sleep`就很不明智。+如果你刚才跳过了,那么请阅读前文的[[tutorial:​autorun_options:​zh#​阻塞式调用|阻塞式调用]]部分。如前文所述,OpenOS运行在个init线程上,并且如果线程中的任何部分进行了系统退让调用`computer.pullSignal`(或者其他调用此函数的方法,例如`event.pull`),此线程中创建的所有事件注册记录都会暂停。因此,只要你还想让你的系统对前台应用(例如shell)作出响应,那么在事件注册记录里面调用`os.sleep`就很不明智。
 =====用于自动执行程序的进入点===== =====用于自动执行程序的进入点=====
  
 下列的进入点就像“钩子”,或者脚本位置。通过这些进入点你可以在电脑启动时运行你的后台或前台应用。 下列的进入点就像“钩子”,或者脚本位置。通过这些进入点你可以在电脑启动时运行你的后台或前台应用。
  
-====交互式Shell启动运行脚本 (.shrc)====+====交互式Shell启动.shrc====
  
-引导启动的最后一步是加载OpenOS shell。shell会阻塞等待有tty输出可用。这意味着如果没有GPU或屏幕,shell启动将会等待。+引导启动的最后一步是加载OpenOS shell。shell会阻塞等待,直到有tty输出可用。这意味着如果没有GPU或屏幕,shell启动将会进行等待。
  
-在用于tty的stdout可用后,shell会完成加载,并且执行`/​etc/​profile.lua`,此脚本将会加载别名以及设置环境变量。`/​etc/​profile.lua`脚本做的最后一件事是[[https://​ss64.com/​bash/​source.html|“source”]](读取并执行)你的`/​home/​.shrc`文件,此文件默认为空。`source`不会执行Lua代码,而是将文件中的每一行作为shell命令运行。如果你有一个想在shell加载时运行的脚本,那么可以将指向此脚本的路径输入你的`.shrc`文件中。`.shrc`会在每次shell加载时运行,也就是说每次引导启动可能不止执行一次。因为用户可以输入 `exit`、`^d`甚至可以发送硬中断信号来杀死shell程序(然后init进程会加载一个新的shell)。+在用于tty的stdout可用后,shell会完成加载,并且执行`/​etc/​profile.lua`,此脚本将会加载别名以及设置环境变量。`/​etc/​profile.lua`脚本做的最后一件事是[[https://​ss64.com/​bash/​source.html|“source”]](读取并执行)你的`/​home/​.shrc`文件,此文件默认为空。`source`不会执行Lua代码,而是将文件中的每一行作为shell命令运行。如果你有一个想在shell加载时运行的脚本,那么可以将指向此脚本的路径输入你的`.shrc`文件中。`.shrc`会在每次shell加载时运行,也就是说每次启动可能不止执行一次。因为用户可以输入`exit`、`^d`甚至可以发送硬中断信号来杀死shell程序(然后init进程会加载一个新的shell)。
  
-我更建议编辑`/​home/​.shrc`而不是`/​etc/​profile.lua`,仅仅为了有序性。+我更建议编辑`/​home/​.shrc`而不是`/​etc/​profile.lua`,仅出于有序性考虑
  
-====Runscripts ​(rc)====+====Runscriptsrc====
  
 请回顾[[api:​rc:​zh|RC文档]]的内容。 请回顾[[api:​rc:​zh|RC文档]]的内容。
  
 `/​bin/​rc`可被用于启用开机启动脚本。RC脚本甚至在没有shell、没有GPU、没有屏幕、没有键盘的系统上也能启动。 `/​bin/​rc`可被用于启用开机启动脚本。RC脚本甚至在没有shell、没有GPU、没有屏幕、没有键盘的系统上也能启动。
-====文件系统的Autorun脚本(autorun.lua)====+====文件系统的Autorunautorun.lua====
  
-在任何文件系统的根目录中你都可以创建名为`autorun.lua`(或者`.autorun.lua`)的文件。当文件系统组件首次被检测到,OpenOS会自动运行此文件。请注意`/​home/​autorun.lua`不在*rootfs*(根文件系统)的根目录中。本段所说的autorun脚本会在文件系统每次被添加到系统时都执行一次(例如,你把带有autorun脚本的软盘拔出再重新插入就会执行一次)。+在任何文件系统的根目录中你都可以创建名为`autorun.lua`(或者`.autorun.lua`)的文件。当文件系统组件首次被检测到,OpenOS会自动运行此文件。请注意`/​home/​autorun.lua`不在**rootfs**(根文件系统)的根目录中。本段所说的autorun脚本会在文件系统每次被添加到系统时都执行一次(例如,你把带有autorun脚本的软盘拔出再重新插入就会执行一次)。
  
-此特性默认启用,并且可以在rw(可读写)模式文件系统禁用,方式是通过调用`filesystem.setAutorunEnabled(false)`,或者直接修改`/​etc/​filesystem.cfg`并加上`autorun=false`。 ​+此特性默认启用,并且可以在安装于rw(可读写)模式文件系统的操作系统中禁用,方式是通过调用`filesystem.setAutorunEnabled(false)`,或者直接修改`/​etc/​filesystem.cfg`并加上`autorun=false`。 ​
  
 ====引导启动脚本(/​boot/​)==== ====引导启动脚本(/​boot/​)====
  
-此选择强烈不推荐使用,将其在此列出只是为了劝说那些不想这么做的用户放弃自己的想法。+此选择强烈不推荐使用,将其在此列出只是为了劝说那些有异议的用户放弃自己的想法。
  
 OpenOS会运行`/​boot/​`中的引导启动脚本(按照文件名排序)供其核心操作使用。虽然可以将自定义的引导启动脚本安装到内核启动脚本中,但我们很不建议这么做。 OpenOS会运行`/​boot/​`中的引导启动脚本(按照文件名排序)供其核心操作使用。虽然可以将自定义的引导启动脚本安装到内核启动脚本中,但我们很不建议这么做。
  
-安装自定义引导启动脚本(到 `/boot/` 中)会导致风险,你的引导启动脚本可能会在核心运行库还不可用时就开始运行。再当前的OpenOS版本下,我们无法保证哪怕是在你的脚本中调用 `require`的安全性,哪怕现在安全,也无法保证在未来的OpenOS更新中是否安全(因为我可能会改动引导启动顺序)。+安装自定义引导启动脚本(到`/​boot/​`中)会带来风险,你的引导启动脚本可能会在核心运行库还不可用时就开始运行。再当前的OpenOS版本下,我们无法保证哪怕是在你的脚本中调用`require`的安全性,哪怕现在安全,也无法保证在未来的OpenOS更新中是否安全(因为我可能会改动引导启动顺序)。
  
 io功能可能没完全初始化,init进程可能也未完成初始化,甚至Lua原版库都有可能未加载完成。根据你在引导启动脚本中执行的代码不同,你甚至可能无意中绕过事件调度系统,导致系统错过组件更新。是的,引导启动过程需要负责这么多事情。 io功能可能没完全初始化,init进程可能也未完成初始化,甚至Lua原版库都有可能未加载完成。根据你在引导启动脚本中执行的代码不同,你甚至可能无意中绕过事件调度系统,导致系统错过组件更新。是的,引导启动过程需要负责这么多事情。
  
-尽管说了这么多,此处还是给出了一些`/​boot`脚本的样例,它们在当下和不远的未来可能会正常工作。在你的文件名前面加上`99_`,这样它会在引导启动流程的最后加载。如果有任何事物没能按你的预期工作(例如向stdout输出,或者从stdin读取),不意味着出现了bug而是意味着不支持。换句话说,使用 `/boot/` 脚本目录的风险自负。如果你需要stdout,你可以等待`term_available`信号。再强调一次,这不是官方支持的选择。+尽管说了这么多,此处还是给出了一些`/​boot`脚本的样例,它们在当下和不远的未来可能会正常工作。在你的文件名的开头加上`99_`,这样它会在引导启动流程的最后加载。如果有任何事物没能按你的预期工作(例如向stdout输出,或者从stdin读取),不意味着出现了bug而是意味着不支持。换句话说,使用`/​boot/​`脚本目录的风险自负。如果你需要stdout,你可以等待`term_available`信号。再强调一次,这不是官方支持的选择。
  
 ```lua ```lua