OpenWrt 基礎軟體模組之procd

hzlarm發表於2020-10-13

OpenWrt 基礎軟體模組之procd

Openwrt 支援模組化程式設計,增加新功能非常簡單。但是一些通用的基礎模組必須包含,他們是OpenWrt核心。
如:實用基礎庫libubox、系統匯流排ubus、網路介面管理模組netifd、核心工具模組ubox、服務管理模組procd。

服務管理模組procd

通常的嵌入式系統均有一個守護程式,該守護程式監控系統程式的狀態,如果某些系統程式異常退出,將再次啟動這些程式。procd 就是這樣一個程式,它是使用C語言編寫 的,一個新的OpenWrt程式管理服務。

它通過init指令碼來將程式資訊加入到procd的資料庫中來管理程式啟動,這是通過ubus匯流排呼叫來實現,可以防止程式的重複啟動呼叫

procd的程式管理功能主要包含3個部分:

  • reload_config: 檢查配置檔案是否發生變化,如果有變化則通知procd程式
  • procd守護程式: 接收使用者的請求,增加或刪除所管理的程式,並監控程式的狀態,如果發現程式退出,則再次啟動程式
  • procd.sh: 提供函式封裝procd提供系統匯流排方法,呼叫者可以非常便利的使用procd提供的方法

程式碼:https://git.openwrt.org/?p=project/procd.git;a=tree

reload_config

工作原理:當在命令列執行reload_config時,會對系統中的所有配置檔案生成MD5值,並且和應用程式使用的配置檔案MD5值進行比較(對/var/run/config.md5檔案進行比較),如果不同就通過ubus匯流排通知procd配置檔案發生改變,如果應用程式在啟動時,向procd註冊了配置觸發服務,那就將呼叫 reload函式重新讀取配置檔案,通常是程式退出再啟動。如果配置檔案沒有改變將不會呼叫,這將節省系統CPU資源

兩點注意事項

  • 配置檔案的真實配置內容發生改變之後才會呼叫,如果 增加空行和註釋並不會引起 配置檔案的實質內容改變
  • 系統啟動時 ,會執行reload_config將 初始配置檔案摘要值 儲存為**/var/run/config.md5** 檔案中,下次再執行reload_config就是與這檔案裡面的MD5值進行比較的

工作原理詳解:

我們以防火牆的配置檔案發生改變為例來說明

  • ①當手動執行reload_config時,首先將目錄/etc/config目錄下的所有檔案通過“uci show”命令輸出其配置到“/var/run/config.check” 目錄下,這個命令將過濾配置檔案增加空行和註釋的情況
  • ②初始系統啟動時的配置檔案摘要值儲存在檔案/var/run/config.md5 中,我們通過 “md5sum –c”命令來從檔案中讀取MD5值並驗證是否和現有的配置檔案MD5是否一致, 如果不一致則就呼叫ubus方法通知procd程式配置檔案發生改變
  • ③當procd知道配置檔案發生改變後,procd就會呼叫/etc/init.d/firewall reload來處理配置檔案改變,其他配置檔案沒有改變的程式,系統將不會花費資源進行處理
  • ④最後將現在執行中的配置檔案MD5值儲存到/var/run/config.md5

procd程式

procd程式向ubus匯流排註冊了 service和system物件 .ubus list命令可以才看到。

service物件

serveis物件提供的方法,主要有3部分功能: 程式的管理、檔案觸發器(trigger)、配置驗證服務(validate)

方 法含 義
set程式如果存在,則修改已經註冊的程式資訊,如果不存在則增加,最後啟動註冊的程式
add增加註冊的程式
list如果不帶引數,則列出所有註冊的程式和其資訊
delete刪除指定服務程式,在結束程式時呼叫,例如停止防火牆會進行以下呼叫: ubus call service delete ‘{“name”:”firewall”}’
event發出事件,例如 reload_config 就使用該方法來通知配置發生改變
validate檢視所有的驗證服務
  • set方法:

  • 上面的3個功能都是通過set方法增加到procd儲存的記憶體資料庫中。資料庫以服務名稱作為其主鍵

  • 共有5個引數:第一個引數為被管理的服務程式名稱;第二個引數為啟動指令碼絕對路徑;第三個引數為程式例項資訊,例如可執行程式路徑和程式的啟動引數等;第四個引數為觸發器;第五個引數為配置驗證項(前3個引數是必須要傳遞的,後面兩個引數可選。)

  • delete方法:

  • 在刪除時使用 delete 方法

  • 共有兩個引數:第一個引數為服務名稱,第二個引數為程式例項名稱,可以不指定例項名稱

  • list方法:

  • 查詢時使用 list 方法

  • 共有兩個引數:第一個引數為服務名稱,第二個引數是布林值,表示是否輸出其詳細資訊,預設為不輸出詳細資訊(該方法可以不帶任何引數,表示查詢所有註冊的服務資訊)

ubus -v list service
 
 'service' @7e08a163
      "set":{"name":"String","script":"String","instances":"Table","triggers":"Array","validate":"Array","autostart":"Boolean","data":"Table"}
      "add":{"name":"String","script":"String","instances":"Table","triggers":"Array","validate":"Array","autostart":"Boolean","data":"Table"}
         "list":{"name":"String","verbose":"Boolean"}
         "delete":{"name":"String","instance":"String"}
         "signal":{"name":"String","instance":"String","signal":"Integer"}
         "update_start":{"name":"String"}
         "update_complete":{"name":"String"}
         "event":{"type":"String","data":"Table"}
         "validate":{"package":"String","type":"String","service":"String"}
         "get_data":{"name":"String","instance":"String","type":"String"}
         "state":{"spawn":"Boolean","name":"String"}

例如:

  • ①增加程式: 如果hello程式需要procd來管理,那麼我們使用ubus命令將hello程式加入的procd的記憶體資料庫中。下面命令傳遞了4個引數,第一個引數設定被管理的服務程式名稱為“hello”。第二個引數設定啟動指令碼絕對路徑“/etc/init.d/hello”。第三個引數設定了程式例項資訊,例項的啟動命令為“/bin/hello”,啟動引數為“-f -c bjbook.net”,並設定程式意外退出的重生引數(respawn)為預設值。第四個引數為觸發器,收到檔案“hello” 的“config.change”訊息後執行指令碼“/ect/init.d/hello”並傳遞“reload”引數
ubus call service add '{"name":"hello", "script":"/etc/init.d/hello","instances":{"instance1":{ "command":["/bin/hello","-f","-c","bjbook.net"],"respawn":[ ] }},"triggers": [ ["config.change",["if", ["eq","package", "hello" ], ["run_script","/ect/init.d/hello", "reload" ] ] ] ]}'
  • ②刪除程式: 引數傳遞程式的名字即可
ubus call service delete '{"name":"hello"}'
  • ③檢視註冊的程式資訊: 也可以不指定名稱,將輸出所有的管理列表。“verbose”為真,表示輸出其詳細資訊
ubus call service list '{"name":"hello","verbose":true}'
  • ④傳送事件: 第一個引數含義為事件型別,現在只支援“config.change”事件訊息; 第二個參數列示檔案“hello”,是指在目錄“/etc/config”下的檔案。在配置檔案發生改變 時呼叫。通知 procd 程式配置檔案 hello 發生了改變
ubus call service event '{"type":"config.change","data":{"package":"hello"}}'
system物件
方 法含 義
board系統軟硬體版本資訊,包含 4 個部分,分別為核心版本、主機名稱、系統 CPU 類 型資訊和版本資訊,版本資訊從/etc/openwrt_release 檔案讀出
info當前系統資訊,包含 5 部分,分別為系統啟動時間、系統當前時間、系統負載情況、 記憶體和交換分割槽佔用情況等
upgrade設定 service_update 為 1
watchdog設定 watchdog 資訊,還存在問題,例如如果本身為 0 的情況
signal向指定 pid 的程式發訊號,是通過 kill 函式來實現的
nandupgrade執行升級

procd.sh

  • 使用ubus方法來進行管理時其傳遞引數複雜並且容易出錯,procd.sh將這些引數拼接組織功能封裝為函式,每一個需要被procd管理的程式都使用它提供的函式進行註冊 。
  • 這些函式組織為JSON格式 的訊息然後通過ubus匯流排向procd程式傳送訊息。這些函式將不同功能封裝為不同的函式,構建特定的JSON訊息來表達特定的功能用法
  • 函式命名規範: procd.sh提供的API命名非常規範,除了有一個uci_validate_section函式 用於驗證UCI 配置檔案以外,其他所有的函式均是以“procd_”開頭

procd_open_ trigger 函式建立一個觸發器陣列,在增加了所有的觸發器之後,呼叫procd_close_trigger函式 來結束觸發器陣列的增加

procd_add_reload_trigger:增加配置檔案觸發器,每次配置檔案的修改,如果呼叫了reload_config時,當前例項都被重啟。有一個可選的引數為配置檔名稱。其實它在內部是呼叫 procd_open_trigger、procd_add_config_trigger 和 procd_close_trigger 這3個函式來增加觸發器

procd_open_instance: 開始增加一個服務例項

procd_set_param: 設定服務例項的引數值。通常會有以下幾種型別的引數:(每次只能使用一種型別引數,其後是這個型別引數的值)

  • command:服務的啟動命令列
  • respawn:程式意外退出的重啟機制及策略,它需要有 3 個設定值。第一個設定為 判斷異常失敗邊界值(threshold),預設為 3600 秒,如果小於這個時間退出,則 會累加重新啟動次數,如果大於這個臨界值,則將重啟次數置 0。第二個設定為 重啟延遲時間(timeout),將在多少秒後啟動程式,預設為 5 秒。第三個設定是總 的失敗重啟次數(retry),是程式永久退出之前的重新啟動次數,超過這個次數進 程退出之後將不會再啟動。預設為 5 次。也可以不帶任何設定,那這些設定都是 預設值
  • env:程式的環境變數
  • file:配置檔名,比較其檔案內容是否改變
  • netdev:繫結的網路裝置(探測 ifindex 更改)
  • limits:程式資源限制

**procd_close_instance:**完成程式例項的增加

例如 rpcd對procd函式的使用,這個示例可以用於大多數應用程式。PROG變數在前面已設定為/bin/rpcd。該示例將最終呼叫以下命令完成程式的增加 :

ubus call service set '{"name":"rpcd", "script":"/etc/init.d/rpcd",
"instances": {"instance1":{ "command": ["/bin/rpcd"] } } }'
procd_open_instance
procd_set_param command "$PROG"
procd_close_instance

procd_open_validate: 開啟一個驗證陣列,是和 procd_close_validate 函式一起使用
procd_close_validate: 關閉一個驗證陣列。

演示案例:

下面是軟體包firewall使用 procd 來對防火牆配置的觸發器和驗證

procd_add_reload_trigger firewall
 
procd_open_validate
validate_firewall_redirect
validate_firewall_rule
procd_close_validate

procd_open_service(name, [script]): 至少需要一個引數,第一個引數是例項名稱, 第二個引數是可選引數為啟動指令碼。該函式僅在在 rc.common 中呼叫,用於建立一個新的 procd 程式服務訊息
procd_close_service: 該函式不需要引數,僅在 rc.common 中呼叫,完成程式管理服務的增加

procd_kill: 殺掉服務例項(或所有的服務例項)。至少需要一個引數,第一個參 數是服務名稱,通常為程式名,第二個是可選引數,是程式例項名稱,因為可能有多個進 程示例,如果不指定所有的例項將被關閉。該函式在 rc.common 中呼叫,使用者從命令列調 用 stop 函式時會使用該函式殺掉程式

uci_validate_section: 呼叫 validate_data 命令註冊為驗證服務。在配置發生改變 後對配置檔案的配置項合法性進行校驗。驗證服務是在程式啟動時通過 ubus 匯流排註冊到 procd 程式中

#輸入以下命令,可以看到系統所有註冊的驗證服務
ubus call service validate

{
        "firewall": {
                "redirect": {
                        "dest": "string",
                        "dest_ip": "cidr",
                        "dest_port": "or(port, portrange)",
                        "proto": "or(uinteger, string)",
                        "src": "string",
                        "src_dport": "or(port, portrange)",
                        "src_ip": "cidr",
                        "target": "or(SNAT, DNAT)"
                },
                "rule": {
                        "dest": "string",
                        "dest_port": "or(port, portrange)",
                        "proto": "or(uinteger, string)",
                        "src": "string",
                        "src_port": "or(port, portrange)",
                        "target": "string"
                }
        },
...............
  • 這些驗證服務是在啟動指令碼中增加驗證服務來實現,如下程式碼所示,service_triggers 函式是預定義好的回撥函式,在每一個增加服務結束後會自動呼叫,使用者不必關注如何 呼叫。validate_cron_section 函式是真正的將驗證服務加入 procd 的驗證服務中。它呼叫 uci_validate_section 函式,而 uci_validate_section 函式進一步呼叫 validate_data 程式
validate_cron_section() {
    uci_validate_section system system "${1}" \
    'cronloglevel:uinteger'
}
 
service_triggers()
{
    procd_add_validation validate_cron_section
    procd_add_reload_trigger "hello"
}

rc.common

rc.common在1209及之前的版本中並不支援procd啟動,在1407版本中增加了專門針對procd的啟動,該指令碼向前相容

在軟體模組的啟動指令碼中如果沒有定義USE_PROCD變數:則啟動流程和之前完全相同

如果定義了 USE_PROCD變數:對start、stop 和 reload函式進行重新定義,在呼叫這些函式時,將呼叫start_service、stop_service和 reload_service函式等

  • procd預定義的函式如下:

    如果在自己的啟動指令碼中定義了USE_PROCD那就呼叫這些函式。在rc.common中重新定義了start函式,相當於過載了這些函式

函 數含 義
start_service向 procd 註冊並啟動服務,是將在 services 所管理物件裡面增加了一項
stop_service讓 procd 解除註冊,並關閉服務, 是將在 services 中的管理物件刪除
service_triggers配置檔案或網路介面改變之後觸發服務重新讀取配置
service_running查詢服務的狀態
reload_service重啟服務,如果定義了該函式,在 reload 時將呼叫該函式,否則再次呼叫 start 函式
service_started用於判斷程式是否啟動成功

編寫一個procd啟動指令碼(注意與init指令碼不一樣)

官方參考以及與init區別

#!/bin/sh /etc/rc.common  
#使用/etc/rc.common來解釋指令碼
 
USE_PROCD=1		#表示使用procd來管理程式
START=15
STOP=85
PROG=/bin/hello	#PROG變數用來給程式的啟動指令碼賦值,用於啟動應用程式

#validate_hello_section函式:驗證了配置檔案hello中的delay變數否為整型值,並且在合理的(1~200)範圍內
validate_hello_section()
{
	uci_validate_section hello system globe \
	'delay:uinteger(1:200)' 
}

#start_service函式:負責程式的啟動
start_service() {
	echo "start HelloRoute!"
	validate_hello_section || {
	echo "hello validattion failed!"
	return 1
	}
 
 #在引數驗證完成後,呼叫procd_open_instance 數發起例項增加,接著呼叫了procd_set_param函式來設定了啟動命令和啟動引數,再接著respawn設定其程式意外退出的重啟機制及策略為預設值,最後呼叫procd_close_instance函式完成例項的增加。注意procd管理的程式需要執行在前臺,即不能呼叫daemon或類似函式
	procd_open_instance
	procd_set_param command "$PROG" –f -w bjbook.net
	procd_set_param respawn
	procd_close_instance
}

#service_triggers函式:增加觸發器,我們增加了對配置檔案hello的觸發服務。當hello檔案發生改變後,如果呼叫了 reload_config命令,將觸發呼叫reload_service函式
service_triggers()
{
	procd_add_reload_trigger "hello"
}

#reload_service函式:在傳遞reload引數時進行呼叫,如果沒有該函式,將會呼叫預設start函式
reload_service()
{
	stop
	start
}

備註: 在執行該啟動指令碼時,如果需要對procd指令碼進行除錯,可以設定PROCD_DEBUG變數為 1,這樣可以輸出向ubus匯流排呼叫的引數資訊。例如: PROCD_DEBUG=1 /etc/init.d/hello start

相關文章