luci框架相关笔记

luci架构

LuCI 架构采用了MVC(Model-View-Controller)设计模式,各个目录的作用如下:

  • model(模型): 位于 /usr/lib/lua/luci/model 下,存放了与系统配置相关的模型脚本。这些脚本负责与底层系统的交互,如读取、验证和更新配置文件。模型层封装了对系统资源的访问和业务逻辑处理。
  • view(视图): 位于 /usr/lib/lua/luci/view 下,包含了各类HTML模板文件,它们使用Lua脚本来动态生成页面内容。视图层负责将模型中的数据以合适的方式呈现给用户,如显示网络设置、系统状态等信息。
  • controller(控制器): 位于 /usr/lib/lua/luci/controller 下,包含了处理HTTP请求的Lua脚本,也就是路由和控制器脚本。控制器层接收用户的HTTP请求,调用相应的模型获取数据,然后选择合适的视图进行渲染,将处理结果返回给用户。

LuCI 将网页中的每一个菜单视作一个节点,当用户通过浏览器点击节点,向路由器发起请求,LuCI 会从 controller 目录下的 index.lua 开始组织这些节点。index.lua 中定义了根节点 root,其他所有的节点都挂在这个根节点上。

通俗来讲,可以将 LuCI 管理界面看作一棵树状结构的菜单系统。管理路由器设置的网页就像一棵大树,每一层菜单就是一个节点。

当点击网页上的任何一个菜单选项时,就像是在触摸大树上的一片叶子或枝干。这时,浏览器会发送一个请求给路由器,告诉它想访问哪个菜单节点。

LuCI 框架接收到这个请求后,会从一个叫做 controller 目录下的 index.lua 文件开始处理。这个 index.lua 文件就好比是整棵树的根部,它定义了整个菜单系统的基础结构和访问规则。

在这个根节点之下,LuCI 会根据在 index.lua 中定义的 entry 函数逐步组织起其他的子菜单节点。也就是说,每当你在 index.lua 中添加一个类似于 entry({"admin", "example", "first"}, call("first_action"), "First") 的语句时,你就相当于在菜单树上挂了一个新的子节点------"First"。

当用户点击网页上的 "First" 菜单时,LuCI 就会调用与之关联的 first_action 函数,呈现相应的页面内容或者执行相应的操作。就这样,通过一步步递归展开,整个复杂的菜单系统就被建立起来了,并能够在用户交互时动态响应用户的请求。

为界面添加节点

entry(path, callback, title, order):用于定义 LuCI 管理界面的菜单项及其关联的操作。

path 指定该节点的位置(例如 node1.node2.node3)

target 指定当该节点被调度(即用户点击)时的行为,主要有三种:call、template 和 cbi。

title:标题,即我们在网页中看到的菜单

order:同一级节点之间的顺序,越小越靠前,反之越靠后(可选)

Map (config, title, description)

config:配置文件的层级名称,比如 example 对应 /etc/config/exampl

title:配置界面的标题,即我们在网页中看到的菜单

description:配置界面的描述信息

s = m:section(TypedSection, "example_instance", "Section Title", "Section Description")

TypedSection 或 SimpleSection:Section 的类型,TypedSection 可以为区段分配类型,并支持默认配置。

example_instance:区段实例的名称,用于区分配置文件中的不同区段实例。

Section Title:区段在界面上显示的标题。

Section Description(可选):区段的描述信息。

o = s:option(Value, "option_name", "Option Label", "Option Description")

Value 或其他选项类型(如 ListValue、Flag 等):选项的数据类型。

option_name:选项在配置文件中的键名。

Option Label:选项在界面上显示的标签文本。

Option Description(可选):选项的描述信息

1.通过call

进入 /usr/lib/lua/luci/controller/ 目录下,创建 lua 脚本文件 example.lua,其内容如下

Lua 复制代码
--[[module 是 LuCI 自定义的函数,用于定义一个新的模块。这里的模块名为 "luci.controller.example",
表示这是一个 LuCI 控制器模块,主要用于定义路由和处理用户请求。
package.seeall 是 Lua 标准库中的一个函数,它用于打开模块内的全局访问权限。
这意味着在这个模块内部定义的所有函数和变量都将被视为全局的,可以从外部访问]]

module("luci.controller.example", package.seeall)

--[[定义了 LuCI 控制器的入口点(entry points)。
entry({"admin", "example"}, firstchild(), "Example", 60) 表示注册一个路由。
当用户在 Web 管理界面访问 /admin/example 时,LuCI 将会调用 firstchild() 函数来决定下一步跳转的位置。
这里 firstchild() 通常是指导航菜单的第一个子页面,即指向 "admin", "example", "first" 的路由。
'模板', 60 分别表示在菜单中显示的标题("模板")和菜单排序优先级(60)。]]
--[[这里的 firstchild() 是一种特殊的回调函数引用。
当LuCI接收到匹配到该路由 "admin", "example" 的请求时,它不会直接执行某个特定的动作函数,
而是查找该路由下第一个有效的子节点(即 "admin", "example", * 中的 * 部分),
并将控制权传递给这个子节点对应的处理函数]]
--
function index()
	entry({"admin", "example"},  firstchild(),"模板", 60)
	entry({"admin", "example", "first"}, call("first_action"), "第一")
	entry({"admin", "example", "second"}, call("second_action"), "第二")
end

--[[当用户访问 /admin/example/first 时,LuCI 将调用 first_action() 函数进行处理。
luci.template.render("header") 表示渲染一个名为 "header" 的模板文件,
通常这个模板文件包含了页面的头部元素,如导航栏、样式表引用等。
这个模板文件位于 /usr/lib/lua/luci/view,如果在其他目录下则更改参数为header.htm路径
luci.http.write("<h1>Hello World</h1>") 用于直接向客户端发送 HTML 数据,
这里是发送一个 <h1> 标签,显示 "Hello World",即页面的主要内容。]]

function first_action()
	luci.template.render("header")
	luci.http.write("<h1> 一级标题 hello</h1>")
	luci.http.write("<h2> 二级标题 hello</h2>")
end

function second_action()
	luci.template.render("header")
	luci.http.write("<h1> 一级标题 hello</h1>")
	luci.http.write("<h2> 二级标题 hello</h2>")
end

刷新网页

2.通过template

更改example.lua文件

Lua 复制代码
module("luci.controller.example", package.seeall)

--[[更改call为template 在view目录下直接调用html页面
添加order参数为子菜单排序
order参数加不加引号系统都会识别为数字]]
function index()
	entry({"admin", "example"},  firstchild(),"模板", 60)
	entry({ "admin", "example", "third" }, template("example/third"), "第三","30")
	entry({ "admin", "example", "fourth" }, template("example/fourth"), "第四","35")
	entry({"admin", "example", "first"}, call("first_action"), "第一","10")
	entry({"admin", "example", "second"}, call("second_action"), "第二","20")

end
function first_action()
	luci.template.render("header")
	luci.http.write("<h1> 一级标题 hello</h1>")
	luci.http.write("<h2> 二级标题 hello</h2>")
end

function second_action()
	luci.template.render("header")
	luci.http.write("<h1> 一级标题 hello</h1>")
	luci.http.write("<h2> 二级标题 hello</h2>")
end

创建模板目录 /usr/lib/lua/luci/view/example/在模板目录下创建文件 third.htm/fourth.htm其内容如下

--third
<%+header%>
<h1>Hello World</h1>
--fourth
<%+header%>
<h1>Hello World</h1>

刷新网页

3.通过cbi

更改example.lua文件

Lua 复制代码
module("luci.controller.example", package.seeall)

function index()
	entry({ "admin", "example" }, firstchild(), "模板", 60)
	entry({ "admin", "example", "third" }, template("example/third"), "第三", "30")
	entry({ "admin", "example", "fourth" }, template("example/fourth"), "第四", "35")
	entry({ "admin", "example", "first" }, call("first_action"), "第一", "10")
	entry({ "admin", "example", "second" }, call("second_action"), "第二", "20")
  --[[
如果配置文件/etc/config/example 存在,则创建 Example 的子节点,
当节点被调度时,LuCI 会将
/usr/lib/lua/luci/model/cbi/example/fifth.lua 这个脚本转换成 html 页面
发给客户端。
--]]
	if nixio.fs.access("/etc/config/example") 
	then
		entry({ "admin", "example", "fifth" }, cbi("example/fifth"), "第五", 40)
	end
end

function first_action()
	luci.template.render("header")
	luci.http.write("<h1> 一级标题 hello</h1>")
	luci.http.write("<h2> 二级标题 hello</h2>")
end

function second_action()
	luci.template.render("header")
	luci.http.write("<h1> 一级标题 hello</h1>")
	luci.http.write("<h2> 二级标题 hello</h2>")
end

创建配置文件/etc/config/example

新建 lua 脚本文件:/usr/lib/lua/luci/model/cbi/example/fifth.lua,内容如下

Lua 复制代码
--[[
  Map (config, title, description)
]]
m = Map("example", "cbi示例", "这是cbi的一个非常简单的例子")
return m

刷新网页

修改/usr/lib/lua/luci/model/cbi/example/fifth.lua

Lua 复制代码
m = Map("example", "cbi示例", "这是cbi的一个非常简单的例子")
--[[
在 Map 对象 m 中创建一个名为 s 的 Section(配置区段),类型为 TypedSection。
第一个参数 TypedSection 表示这是一个带有类型属性的配置区段;
第二个参数 是区段的名称,这也将成为 /etc/config/example 文件中区段的标识;
第三个参数 是区段的标题;
第四个参数 是区段的描述信息。
]]
s = m:section(TypedSection, "example", "模板", "此部分为模板")

--[[
设置 Section 对象 s 允许用户在界面上添加和移除区段实例。
]]
s.addremove = true

--[[
设置 Section 对象 s 不是匿名区段,意味着每个实例都需要在配置文件中拥有一个唯一的标识(id)。
]]
s.anonymous = false

--[[
在 Section 对象 s 中创建一个名为 n 的 Option(选项),类型为 Value(值类型)。
第一个参数 Value 表示这是一个可以输入任意值的选项;
第二个参数 "num" 是选项在配置文件中的键名;
第三个参数 "Number" 是选项在界面上显示的标签文本。
]]
n = s:option(Value, "num", "Number")

--[[
设置 Option 对象 n 允许用户清空其值,如果用户在界面上删除了输入值,保存时也会将配置文件中的相应值清空。
]]
n.rmempty = true
return m

刷新网页

在文本框中输入 first,然后单击 Add,如下所示

在 Number 后面随便输入一个数字,比如 12,然后单击 Save & Apply,如下所示

在路由器开发板上查看一些配置文件

n.rmempty = true表示当用户对该选项的输入值为空值时,LuCI 会将该选项从配置文件中移除。

将 Number 的值删除,再单击 Save & Apply

现在再来查看配置文件

添加启动脚本

在 OpenWrt系统中,LuCI 作为 Web 管理界面,允许用户通过网页图形界面编辑系统的各项配置。当用户在 LuCI 中修改了配置,并单击"Save & Apply"按钮后,会发生以下过程:

LuCI 会首先将用户在网页上所做的更改保存到对应的配置文件中。例如,网络相关的配置会保存到 /etc/config/network 文件。(上述保存到创建的配置文件/etc/config/example)

保存完成后,LuCI 通常会调用相应的 UCI(Unified Configuration Interface)命令行工具(如 ubus 或 uci)来通知系统配置已更改,并触发重新加载配置

系统收到配置更改通知后,会根据配置文件的变化情况,调用相应的启动脚本执行配置更新操作。这些启动脚本通常位于 /etc/init.d/ 目录下,例如对于网络配置,对应的启动脚本就是 /etc/init.d/network,如果为上述示例在/etc/init.d/目录下新建启动脚本文件,当示例的配置文件变化后,在/etc/init.d/目录下新建的启动脚本也会运行。

启动脚本接收到诸如 reload 的参数后,会停止当前的相关服务,应用新的配置,然后再启动这些服务,从而使更改生效。

为配置文件 example 创建一个启动脚本/etc/init.d/example,同时为其添加可执行权限。其内容如下:

#!/bin/sh /etc/rc.common 
START=50 
start() 
{ 
 echo "start example" > /dev/ttyS0 
} 
reload()
{ 
 echo "reload example" > /dev/ttyS0 
}

LuCI 通过以配置文件名作为参数调用/sbin/luci-reload 来使配置生效,而 luci-reload 会解析另一个配置文件 /etc/config/ucitrack,需要将 example 添加进去。用vi打开/etc/config/ucitrack,在最后添加如下内容:

config example

option init example

当用户在 LuCI 管理界面单击 "Save & Apply" 保存并应用配置变更后,LuCI 通常会执行 /sbin/luci-reload 命令,并传入对应的配置模块名称(即执行/sbin/luci-reload example)。luci-reload 会识别出与该模块相关的启动脚本,并调用其 reload 函数来重新加载配置并应用更改。

这里,example表示要跟踪的配置文件,init 选项指定了与该配置文件关联的启动脚本名称(即 /etc/init.d/example)

当 /etc/config/example 文件发生变化时,ucitrack 会监测到变化并调用 /etc/init.d/example 脚本的 reload 函数(如果存在),以重新加载并应用新的配置。这样就能确保当用户通过 LuCI 管理界面单击 "Save & Apply" 后,配置变更能够被系统及时识别并应用。

为了确保 /etc/init.d/example 脚本在系统启动时被自动运行,以及能够响应 luci-reload 命令,需要执行以下命令启用该服务:

chmod +x /etc/init.d/example  #首先确保有执行权限 否则报错Permission denied
root@OpenWrt:/# /etc/init.d/example enable

这条命令会创建一个符号链接,将 /etc/init.d/example 链接到 /etc/rc.d/S50example(这里的数字 50 可能根据实际的 START 变量值有所不同),这样每次启动或执行 reload 时,系统都会自动运行 /etc/init.d/example 脚本中的相应函数。

当使用 reload 参数调用启动脚本时,其目的是让系统在不完全重启服务的情况下,仅重新加载并应用新的配置。这对于许多服务来说是十分重要的,因为它可以在不影响服务整体运行的前提下实现配置的热更新。例如,在网络配置变更时,调用 /etc/init.d/network reload 就可以让系统在不关闭网络连接的前提下,重新读取 /etc/config/network 配置文件并按照新配置调整网络设置。这样做的好处是可以避免因服务重启带来的短暂网络中断。相比之下,如果是使用其他参数(如 start 或 stop),它们分别代表启动或停止服务,这会导致服务状态的显著改变,很可能会影响到依赖于该服务的其他功能。而 reload 参数提供了更平滑的配置过渡方式,更适合于实时生效的配置更新场景。

现在单击网页中的 Save & Apply,打开串口助手可以看到开发板中输出了如下内容:(配置更改才会执行,即填入不同数字才会输出reload example )

reload example

说明确实执行了/etc/init.d/example 中的 reload 函数。

将软件包添加到路由器(以dtu程序为例)

dtu文件夹下Makefile

include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/package.mk #引入全局变量规则和包构建规则

PKG_NAME:=dtu
PKG_VERSION:=1.0
define Package/$(PKG_NAME)
	CATEGORY:=My Package
	#DEPENDS:= 如果需要依赖其他软件包,在这里添加
	TITLE:=DTU program
endef                           #定义包的基本信息,包括名称 版本号 类别 标题

#编译前执行
define Build/Prepare
	mkdir -p $(PKG_BUILD_DIR)   
	$(CP) ./src/* $(PKG_BUILD_DIR)/
endef                          #在编译前运行 将src源文件复制到编译目录PKG_BUILD_DIR下

#安装时执行
define Package/$(PKG_NAME)/install
	$(INSTALL_DIR) $(1)/bin     
	$(INSTALL_BIN) $(PKG_BUILD_DIR)/dtu $(1)/bin/
endef                         #创建bin目录,将PKG_BUILD_DIR下可执行文件安装到/bin下


$(eval $(call BuildPackage,dtu))

将源文件放入src文件并在src下创建Makefile文件

all:
	$(CC) dtu.c -o dtu

make menuconfig勾选dtu,M生成单独软件包,*包含进固件

运行 make ./package/dtu/compile V=s后在/bin/packages/mipsel_24kc/base下生成dtu_1.0_mipsel_24kc.ipk 运行scp dtu_1.0_mipsel_24kc.ipk root@192.168.1.1:/root 将软件包拷贝到root目录下

运行opkg install dtu_1.0_mipsel_24kc.ipk安装软件包 在/bin下生成dtu可执行程序

实现软件包开机自启

在路由器上实现

在/etc/init.d下创建脚本server_init

#!/bin/sh /etc/rc.common
START=99
STOP=10
start()
{
        /bin/dtu &      
}
stop()
{
        killall dtu
}

添加执行权限

chmod +x server_init

在/etc/rc.d/rc*.d下创建链接 系统开机后会按照预定的优先级依次启动

./server_init enable

reboot重启ps查看

在软件包上实现

在package/dtu下创建files文件夹储存启动脚本文件server_init

在dtu目录下的Makefile文件中添加代码,将files下的启动脚本文件拷贝到/etc/init.d下

并启动开机启动

#安装时执行
define Package/$(PKG_NAME)/install
	$(INSTALL_DIR) $(1)/bin $(1)/etc/init.d/         
	$(INSTALL_BIN) $(PKG_BUILD_DIR)/dtu $(1)/bin/
	$(INSTALL_BIN) ./files/server_init $(1)/etc/init.d/
endef    									
            #创建bin目录和/etc/init.d/目录,将PKG_BUILD_DIR下可执行文件安装到/bin下  
            #将server_init脚本安装到/etc/init.d/下

#安装后执行 					
define Package/$(PKG_NAME)/postinst
#!/bin/sh
# check if we are on real system
if [ -z "$${IPKG_INSTROOT}" ]; then
	echo "Enabling rc.d symlink for dtu"
if [ -e /etc/rc.d/S??dtu ];then
	rm /etc/rc.d/S??dtu
fi
if [ -e /etc/rc.d/K??dtu ];then
	rm /etc/rc.d/K??dtu
fi
	/etc/init.d/server_init enable
fi
exit 0
endef                       #设置开机自启动,检查是否存在S启动脚本和K关闭脚本
                            #存在则关闭,并创建新的服务

#卸载前执行 
define Package/$(PKG_NAME)/prerm
	#!/bin/sh
	# check if we are on real system
	if [ -z "$${IPKG_INSTROOT}" ]; then
	echo "Removing rc.d symlink for dtu"
	/etc/init.d/server_init disable
fi
exit 0
endef                     #判断是否为真实的系统环境(非临时安装环境)
                          #并禁用server_init服务

相关资料

hostname():获取主机名。

loadavg():获取系统负载平均值。

luci.model.uci模块:

cursor():创建一个UCI数据库游标。

changes():获取最近的UCI更改。

apply():应用UCI配置更改。

游标对象的 get、set、add、delete 等方法用于操作UCI配置。

luci.template模块:

render(template, ...):渲染指定的Lua模板文件。

process(template, context):处理模板并输出内容。

luci.util模块:

split(str, sep):分割字符串。

trim(s):去除字符串两侧的空白字符。

ip.IPv4(ipstr):IP地址解析。

ip.IPv6(ipstr):IPv6地址解析。

软件包:

PKG_NAME - 用于指定软件包的名称,通常是唯一标识该软件包的关键字符串。

PKG_VERSION - 表示软件包的版本号,这是构建系统用来区分不同版本软件的重要信息。

PKG_RELEASE - 这是编译发布的版本信息,可能反映了软件包在同一个版本基础上的不同编译版本或修订版本,比如补丁级别。

PKG_BUILD_DIR - 指定编译该软件包的工作目录,默认情况下,会在构建系统的临时目录(如$(BUILD_DIR))下为每个软件包创建一个单独的子目录,子目录名由软件包名和版本号组成。

PKG_SOURCE - 指定要下载的源代码包的文件名,构建系统会根据这个信息从指定位置下载源代码。

PKG_SOURCE_URL - 提供源代码包的下载地址,构建系统会从这个URL下载指定的源码包。

PKG_MD5SUM - 源代码包的MD5校验和,用于验证下载的源代码包是否完整无误。

PKG_CAT - 指定解压源代码包的方式,比如使用zcat解压.gz文件,bzcat解压.bz2文件,或者unzip解压.zip文件。

PKG_BUILD_DEPENDS - 指定该软件包在编译过程中依赖的其他软件包,这些依赖的软件包必须先于当前软件包被成功编译。这通常用于编译时依赖关系,与运行时依赖(DEPENDS)有所区别,尽管两者语法可能相似。

SECTION - 软件包分类,目前未使用,未来可能会引入分类功能。

CATEGORY - 指定软件包在menuconfig菜单中的位置,如果这个类别之前未被使用过,menuconfig会自动创建一个新的菜单来容纳该类别的软件包。

TITLE - 软件包的简短描述,用于在menuconfig中显示软件包的基本信息。

URL - 提供软件包源代码的官方网站或仓库地址,方便开发者获取更多信息。

MAINTAINER - 软件包的维护者联系信息,便于用户报告问题或寻求帮助。

DEPENDS - 定义软件包在编译和安装时所需的依赖关系。具体语法包括:

  • DEPENDS:=+foo:当前软件包和foo软件包一起联动,一荣俱荣,一损俱损,如果当前软件包被选中或取消选中,foo软件包的状态也会随之同步改变。

  • DEPENDS:=foo:当前软件包依赖于foo软件包,只有当foo软件包被选中时,当前软件包才会出现在menuconfig中。

  • DEPENDS:=@FOO:当前软件包依赖于全局配置项CONFIG_FOO,只有当CONFIG_FOO被启用时,当前软件包才会出现在menuconfig中。

  • DEPENDS:=+FOO:bar:当前软件包和bar软件包联动,同时取决于全局配置项CONFIG_FOO,当CONFIG_FOO被启用时,当前软件包依赖于bar软件包。

  • DEPENDS:=@FOO:bar:当前软件包是否依赖bar软件包取决于全局配置项CONFIG_FOO,只有当CONFIG_FOO被启用且bar软件包被选中时,当前软件包才会出现在menuconfig中。

o:value("wan","WAN")

在 OpenWrt/LEDE LuCI 的上下文中,o:value("wan","WAN") 这行代码的作用是在表单选项 o 中添加一个选项值。这里的 o 是一个 Value 类型的表单选项对象。

  • "wan" 是选项的值,即当用户选择这个选项时,表单提交时实际存储的数据值。
  • "WAN" 是选项的显示文本,即用户在界面上看到的文字描述。

固件的etc的文件受源代码哪些文件的影响?

OpenWrt固件中/etc目录下的文件受到源代码树中多个位置的影响:

  1. 基础系统配置文件:
    • /package/base-files/files/etc/:这个目录包含了OpenWrt的基本系统配置文件,如/etc/config/*系列的网络配置文件、防火墙规则文件、系统启动脚本等。
  1. 特定软件包的配置文件:
    • 各个软件包目录下的files/etc/子目录:例如,当编译安装某个软件包时,该软件包的源代码目录中可能包含files/etc文件夹,其中的配置文件会在编译打包阶段被复制到固件的/etc目录下。

feeds是软件包仓库的目录,那package是什么

在OpenWRT环境中,feeds 和 package 目录都与软件包管理有关,它们的作用有所不同:

  1. feeds
    • 是一个或多个软件包仓库的集合,这些仓库通常位于远程服务器上或者本地文件系统中,并且通过版本控制系统(如Git)维护。feeds中包含了一系列预编译的软件包定义,它们提供了扩展OpenWRT功能的各种附加软件包。当执行./scripts/feeds update和./scripts/feeds install命令时,feeds中的软件包信息会被下载到本地的feeds目录中,但实际的源代码并不会直接放置在feeds目录下。
  1. package
    • 位于OpenWRT源代码树的顶级目录或其子目录中,它存储的是OpenWRT系统本身自带的软件包源代码以及由feeds安装后提取出来的第三方软件包源代码。当你通过feeds安装了某个软件包后,相应的源代码将会被解压并放入到package目录下相应的位置,以便在编译OpenWRT固件时能够将这些软件包一同编译进去。

简而言之,在OpenWRT编译流程中,feeds是软件包仓库列表和软件包元数据的来源,而package则是实际存放编译时所需软件包源代码的地方。

相关推荐
康熙38bdc1 小时前
Linux 进程优先级
linux·运维·服务器
hhzz1 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
只是有点小怂1 小时前
parted是 Linux 系统中用于管理磁盘分区的命令行工具
linux·运维·服务器
三枪一个麻辣烫2 小时前
linux基础命令
linux·运维·服务器
cuisidong19972 小时前
如何在 Kali Linux 上安装 Google Chrome 浏览器
linux·运维·chrome
光通信学徒3 小时前
ubuntu图形界面右上角网络图标找回解决办法
linux·服务器·ubuntu·信息与通信·模块测试
南种北李3 小时前
Linux自动化构建工具Make/Makefile
linux·运维·自动化
小飞猪Jay3 小时前
面试速通宝典——10
linux·服务器·c++·面试
暗恋 懒羊羊4 小时前
Linux 生产者消费者模型
linux·开发语言·ubuntu
安红豆.5 小时前
Linux基础入门 --13 DAY(SHELL脚本编程基础)
linux·运维·操作系统