最近做了一個高通平臺安卓的需求,功能使得data磁區在第一次啟動時,自動適配emmc/ufs的實際大小,在此程序中對init的執行以及.rc檔案的決議流程有了一些理解,但是對于一些細節的東西還不清楚,在這里提出幾個自己疑惑的關鍵問題,趁熱打鐵!!梳理并尋找答案!!!
這里以高通平臺為例,基于最新的安卓11,init這塊的代碼mtk與高通基本是一模一樣的(差異很小),都是中間層的東西;
1,init行程在第二初始階段如何加載init.rc,在整個工程中有幾個地方存放rc檔案,存放有什么規律??
2,根據目前的理解,rc檔案中的cmd最終映射到了對應的函式執行,如下圖,他們是什么時候被呼叫的?? 例如:rc檔案中cmd mount_all 最侄訓映射執行do_mount_all()函式;

3,*.rc檔案加載的時候就伴隨執行嗎,還是是分開的??
4,目前看磁區并不是一次性掛載的,磁區分了幾次掛載,這幾次區分有什么規律??
對于init在整個系統中(宏觀)的執行流程想必大家都很清楚了,init行程是linux內核啟動后創建的第一個行程,地位非常重要,init行程在初始化程序中會啟動很多重要的守護行程,因此理解init行程的啟動程序可以使我們更好的理解安卓系統,init本身也是一個守護行程,linux內核加載完畢后,會首先啟動init行程,啟動程序中會決議linux配置腳本init.rc檔案,根據init.rc檔案的內容,init行程會裝載Android的檔案系統,創建系統目錄,初始化屬性系統,啟動android系統重要的守護行程, 這些行程包括USB守護行程,adb守護行程,vold守護行程,rild守護行程等;
最后init行程也會作為守護行程來執行修改屬性請求,重啟崩潰的行程操作;
init行程初始化程序
init行程的原始碼位于目錄system/core/init 下,程式的入口函式main()位于檔案main.cpp中;
main函式的流程;
int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
__asan_set_error_report_callback(AsanReportCallback);
#endif
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);//uevent的初始化
}
if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
android::base::Initialling(argv, &android::base::KernelLogger);
const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();//命令映射表
return SubcontextMain(argc, argv, &function_map);
}
if (!strcmp(argv[1], "selinux_setup")) {//selinux的初始化設定
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv); //init的第二階段初始化
}
}
return FirstStageMain(argc, argv); //init的第一階段初始化
}
system/core/init/main.cpp
main函式一開始,首先初始化了守護行程uevent,然后緊接著的就是init的初始化程序;
init引導序列分為三個主要階段:1 first_stage_init;
2 selinux_setup;(可選)
3 second_stage_init;
其中 first_stage_init負責設定最低限度的基本需求用以加載系統其余部分,具體來說,包括“/dev”,“/proc”的掛載,掛載“early mount”磁區(這包括所有包含系統代碼的磁區,例如system和vendor)對于有ramdisk的設備,將system.img掛載到“/”;
一旦first_stage_init完成接著執行 execs /system/bin/init 并以“selinux_setup”作為引數,在這個階段,SELinux可選地編譯并加載到系統中,這個階段主要是加載初始化selinux相關的東西,關于此更多的細節在Selinux.cpp中;
最后,一旦該階段結束,它將再次使用"second_stage"引數執行/system/bin/init,此時,init的主要階段將運行并通過init繼續引導init.rc腳本,
由此我們可以知道rc檔案的決議執行均在init的第二階段,因此我們需要重點關注init初始化的第二階段;
second_stage_init;
...
初始化屬性系統;
初始化信號;
LoadBootScripts(am, sm); //加載*.rc檔案
進入while(1)回圈,監聽處理到達的事件;
init.rc語言
這部分將介紹init.rc檔案的格式這是理解決議程序的根本;
Android Init語言由5大類陳述句組成:
Actions, Commands, Services, Options, 和 Imports.
以下是對各個陳述句的簡單解釋
actions:
actions其實就是以序列的commands的集合,每個actions都有一個trigger,它用于決定action的執行時機,當一個符合action觸發條件的事件發生了,此action會加入到執行佇列的末尾,除非它已經在佇列里;
每一個action都將以此從佇列中取出,此action的每個command都將依次執行,在這些命令執行時init還同時處理這其他活動(設備節點的創建和銷毀,設定屬性,重啟行程);
services:
services是一個后臺程式,在init中啟動,如果退出了可以由系統重啟(可選);
options:
options是services的修飾項,它們決定一個services何時以及如何運行;
triggers:
Triggers是一個用于匹配某種事件型別的字串,它將使對應的actions執行;
觸發器分為事件觸發器和屬性觸發器,
事件觸發器
是由'trigger'命令或init可執行檔案中的QueueEventTrigger()函式觸發的字串,它們采用簡單字串的形式,如'boot'或'late-init',
屬性觸發器
是指指定屬性將值更改為給定的新值或指定屬性將值更改為任何新值時觸發的字串,它們分別采用'property:='和'property:=\*'的形式,屬性觸發器將在init的初始引導階段額外計算并相應地觸發,
一個操作可以有多個屬性觸發器,但可能只有一個事件觸發器,
commands:
command是actions的命令串列中的命令,或者是service的選項引數命令;
import:
一般用作 “import <path>”,擴展當前配置,如果path是一個目錄,該目錄中的每個檔案都被決議為一個組態檔,它不是遞回的,嵌套的目錄將不會被決議,
import關鍵字不是命令,而是它自己的部分,這意味著它不是作為action的一部分發生的,而是在決議檔案時處理匯入;
第一級安裝設備的實際順序是:
1. 決議/init.rc,然后遞回地決議它的每個匯入(此處遞回是import的遞回,不是檔案夾的遞回,檔案夾不支持遞回);
2. /system/etc/init/的內容按字母順序排列并按順序決議,在每個檔案決議后遞回地進行匯入;
3. 步驟2重復/vendor/etc/init,然后是/odm/etc/init;
-----------------
/init.rc是主要的.rc檔案,由init可執行檔案在開始執行時加載,它負責系統的初始設定,
在加載主目錄/init.rc后,init立即加載包含在/{system,vendor,odm}/etc/init/目錄中的所有檔案,
rc檔案的存放目錄以及目的:
1 /system/etc/init/ 用于核心系統項,例如 SurfaceFlinger, MediaService和logd,
2 /vendor/etc/init/ 是針對SoC供應商的專案,如SoC核心功能所需的actions或守護行程,
3 /odm/etc/init/ 用于設備制造商的專案,如actions或運動傳感器或其他外圍功能所需的守護行程,
以下是個人認為理解init.rc腳本重要的幾點內容:
1,init.rc檔案是以section為單位組織的,一個section可以包含多行,section可以分為兩大類,一類是action,另一類是service;action以關鍵字on開始,表示一組命令的集合,service以關鍵字service開始,表示啟動某個行程的方式和引數;
2,section以on或service開始,直到下一個on或者service結束,中間的所有行都屬于這一個section(空行或者注釋不具有分割作用),截取init.rc部分如下
on property:apexd.status=ready && property:ro.product.cpu.abilist32=*
exec_start boringssl_self_test_apex32
on property:apexd.status=ready && property:ro.product.cpu.abilist64=*
exec_start boringssl_self_test_apex64
service boringssl_self_test32 /system/bin/boringssl_self_test32
setenv BORINGSSL_SELF_TEST_CREATE_FLAG true # Any nonempty value counts as true
reboot_on_failure reboot,boringssl-self-check-failed
stdio_to_kmsg
根據1,2可以得出,上圖有3個section,其中兩個是action,一個是service;
3,無論是action還是service,并不是按照檔案的編排順序執行的,他們只是一份定義,至于執行與否以及什么時候執行取決于init在運行時的操作.
腳本檔案的決議程序:
init行程啟動時最重要的作業就是決議并執行啟動檔案init.rc,本節介紹init行程決議腳本檔案的流程;
這里我們主要以決議action為例講解,service的決議程序同理,首先講解基本流程,然后列舉實體進行決議分析;
init.rc檔案以及*.rc檔案加載是從下面這個函式開始的:
static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
Parser parser = CreateParser(action_manager, service_list);
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
parser.ParseConfig("/system/etc/init/hw/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
// late_import is available only in Q and earlier release. As we don't
// have system_ext in those versions, skip late_import for system_ext.
parser.ParseConfig("/system_ext/etc/init");
if (!parser.ParseConfig("/product/etc/init")) {
late_import_paths.emplace_back("/product/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
} else {
parser.ParseConfig(bootscript);
}
}
以下是主要的決議流程:

ParseConfig 函式傳入需要決議的rc檔案的路徑,如果是目錄,則遍歷該目錄取出所有的 rc 檔案并呼叫 ParseConfigFile 函式進行決議,如果是檔案路徑,則直接呼叫 ParseConfigFile 函式進行決議,
從代碼中可以看出,init 決議 rc 檔案的程序中,首先呼叫 ReadFile 函式將 rc 檔案的內容全部保存為字串,存在 data 中,然后呼叫 ParseData 進行決議,ParseData 函式會根據關鍵字決議出service和action,最終掛在到 service_list 與 action_manager 的向量(vector)上,
下面分析一下 ParseData 函式,根據關鍵字的不同會呼叫不同的 parser 去決議(多型),action 使用 ActionParser,而 service 使用 ServiceParser 決議,該部分定義在LoadBootScrip()函式的第一行,parser.AddSectionParser()方法為parser的map成員section_parsers_創建了三個SectionParser,分別用來決議service,on,import的section;

下面重點分析ParseData函式:
next_token(&parse_state)處理資料型別使用parse_state的構體回傳:
struct parse_state
{
char *ptr; // 要決議的字串
char *text; // 決議到的字串,可以理解為回傳一行的資料
int line; // 決議到第行數
int nexttoken; // 決議狀態,有 T_EOF T_NEWLINE T_TEXT
};//其中 T_EOF 表示字串決議結束,T_NEWLINE 表示決議完一行的資料,T_TEXT 表示決議到一個單詞
其中 T_EOF 表示字串決議結束,T_NEWLINE 表示決議完一行的資料,T_TEXT 表示決議到一個單詞,
void Parser::ParseData(const std::string& filename, std::string* data) {
data->push_back('\n'); // TODO: fix tokenizer
data->push_back('\0');
parse_state state;
state.line = 0;
state.ptr = data->data();
state.nexttoken = 0;
SectionParser* section_parser = nullptr;
int section_start_line = -1;
std::vector<std::string> args;
// If we encounter a bad section start, there is no valid parser object to parse the subsequent
// sections, so we must suppress errors until the next valid section is found.
bool bad_section_found = false;
auto end_section = [&] {//lambda類似于一個函式指標
bad_section_found = false;
if (section_parser == nullptr) return;
//同樣是呼叫相應的section的EndSection()結束該section的決議;
if (auto result = section_parser->EndSection(); !result.ok()) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << section_start_line << ": " << result.error();
}
section_parser = nullptr;
section_start_line = -1;
};
for (;;) {
switch (next_token(&state)) {
case T_EOF:
end_section();
for (const auto& [section_name, section_parser] : section_parsers_) {
section_parser->EndFile();//決議到檔案末尾,則結束
}
return;
case T_NEWLINE: { // 開始處理新的一行
state.line++;
if (args.empty()) break;
// If we have a line matching a prefix we recognize, call its callback and unset any
// current section parsers. This is meant for /sys/ and /dev/ line entries for
// uevent.
/* auto line_callback = std::find_if(
line_callbacks_.begin(), line_callbacks_.end(),
[&args](const auto& c) { return android::base::StartsWith(args[0], c.first); });
if (line_callback != line_callbacks_.end()) {
end_section();
if (auto result = line_callback->second(std::move(args)); !result.ok()) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
}
*/ //這部分是uevent的暫時跳過
//section_parsers_ 是一個map,是在LoadBootScrip()第一行創建了三個鍵值對 "on" ---> ActionParser
// "import" ---> ImportParser
// "service" ---> ServiceParser
//args[0]為某一行的第一個單詞,該行由可能為section的開頭,也有可能為command行,count()方法判斷該行的第一個單詞是不是為on/import/serivce,如果是則回傳1
} else if (section_parsers_.count(args[0])) {
end_section();//在處理新的section前,結束之前的section;
section_parser = section_parsers_[args[0]].get(); //依據args[0]是on/import/service取出其對應的決議方法的地址ActionParser/ImportParser/ServiceParser
section_start_line = state.line;
if (auto result =
section_parser->ParseSection(std::move(args), filename, state.line); //呼叫相應的section決議函式(ActionParser/ImportParser/ServiceParser)決議section
!result.ok()) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
section_parser = nullptr;
bad_section_found = true;
}
} else if (section_parser) {//該行的第一個單詞不是新的section的開頭,則使用上一次的決議方法的函式ActionParser/ImportParser/ServiceParser決議其命令列
if (auto result = section_parser->ParseLineSection(std::move(args), state.line);//決議命令
!result.ok()) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
}
} else if (!bad_section_found) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line
<< ": Invalid section keyword found";
}
args.clear();
break;
}
case T_TEXT: //決議到一個單詞,就把它存入args中
args.emplace_back(state.text);
break;
}
}
}
next_token()函式的作用就是尋找單詞結束或者行結束標志,如果是單詞結束標志就將單詞push到args中,如果是行結束標志,則根據第一個單詞來判斷是否是一個section,section的標志只有三個"on","service","import",如果是"section",則呼叫相應的ParseSection()來處理一個新的section,否則把這一行繼續作為前“section”所屬的行來處理,
ParseSection()被用來決議一個新的section,ParseLineSection()被用來決議該section下的命令列,依據注釋內容我們可以繪制出決議的基本流程圖如下:

針對action,import,service分別定義了其對應的三個函式(不列舉import了),這三個函式在決議中扮演了重要的角色;
ActionParser::ParseSection() ServiceParser::ParseSection() ....
ActionParser::ParseLineSection() ServiceParser::ParseLineSection()
ActionParser::EndSection() ServiceParser::EndSection()
還是以下面action為例進行說明,
on boot
ifup io
start sshd
首先next_token決議到 on boot這一行,根據args[0] 值為“on” 取出ActionParser的指標,首先呼叫ActionParser::ParseSection處理這個新的action;
Result<void> ActionParser::ParseSection(std::vector<std::string>&& args,
const std::string& filename, int line) {
std::vector<std::string> triggers(args.begin() + 1, args.end());
if (triggers.size() < 1) {
return Error() << "Actions must have a trigger";
}
Subcontext* action_subcontext = nullptr;
if (subcontext_ && subcontext_->PathMatchesSubcontext(filename)) {
action_subcontext = subcontext_;
}
std::string event_trigger;
std::map<std::string, std::string> property_triggers;
if (auto result =
ParseTriggers(triggers, action_subcontext, &event_trigger, &property_triggers);
!result.ok()) {
return Error() << "ParseTriggers() failed: " << result.error();
}
#ifdef G1122717
for (const auto& [property, _] : property_triggers) {
action_manager_->StartWatchingProperty(property);
}
#endif
auto action = std::make_unique<Action>(false, action_subcontext, filename, line, event_trigger,
property_triggers);
action_ = std::move(action);
return {};
}
ActionParser::ParseSection函式首先決議trigger,以trigger作為引數(triggle分為事件觸發和屬性觸發,都會被賦值給action物件的相應事件成員)構造一個結構action 并賦值給ActionParser的成員函式action_,之后使用ActionParser::ParseLineSection處理下一行命令,需要處理該on boot下的行命令ifup io,呼叫ActionParser::ParseLineSection對命令列進行處理,這個函式首先會根據ifup這個命令查詢BuiltinFunctionMap表找到ifup對應的處理函式,
Result<void> Action::AddCommand(std::vector<std::string>&& args, int line) {
if (!function_map_) {
return Error() << "no function map available";
}
auto map_result = function_map_->Find(args);
if (!map_result.ok()) {
return Error() << map_result.error();
}
commands_.emplace_back(map_result->function, map_result->run_in_subcontext, std::move(args),
line);
return {};
}

如上圖ifup對應的處理函式為do_ifup,找到該函式后,根據該函式以及其引數 “io” 構造出一個command實體,并把它push到第一步構造出的action_的成員commands_中,接著繼續呼叫ActionParser::ParseLineSection()處理下一行命令start sshd同樣最后把其push到commands_中;
Result<void> ActionParser::EndSection() {
if (action_ && action_->NumCommands() > 0) {
action_manager_->AddAction(std::move(action_));
}
return {};
}
此時,ActionParser的成員函式action_ 就代表這個決議出來的action的整體,(這里可以看出action類就是對一個整個action的抽象)最后呼叫ActionParser::EndSection()把這個決議出來的整體的action_添加到ActionParser的單例成員(ActionManager型別)action_manager_中,ActionManager單例成員action_manager_中包含了所有決議出來的action,相當于之前使用的鏈表,這就是一個action的決議程序;
執行action
action和service決議完成之后,所有的action和service都被掛在ActionParser->action_manager_->actions_ 和ServiceParser->service_list_->services_ 這兩個向量里;向量中的每一個action和service都完整的描述了一個action和service的section,在決議完成之后SecondStageMain()函式會呼叫如下代碼:
am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
am.QueueBuiltinAction(TestPerfEventSelinuxAction, "TestPerfEventSelinux");
am.QueueEventTrigger("early-init");
// Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
// ... so that we can start queuing up actions that require stuff from /dev.
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
Keychords keychords;
am.QueueBuiltinAction(
[&epoll, &keychords](const BuiltinArguments& args) -> Result<void> {
for (const auto& svc : ServiceList::GetInstance()) {
keychords.Register(svc->keycodes());
}
keychords.Start(&epoll, HandleKeychord);
return {};
},
"KeychordInit");
// Trigger all the boot actions to get us started.
am.QueueEventTrigger("init");
am.QueueEventTrigger("early-init");意為early-init時間已經到來,可以執行triggle只為early-init的action了,QueueEventTrigger()函式只把triggle加入到ActionManager的event_queue_中,對于 am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups") 該函式在創建action的同時,把該事件觸發添加到ActionManager的事件佇列中,后續會遍歷ActionManager的event_queue_找到的triggle對應的action的command會被依次執行,程序如下:
while (true) {
// By default, sleep until something happens.
// 決定timeout的時間
auto epoll_timeout = std::optional<std::chrono::milliseconds>{};
auto shutdown_command = shutdown_state.CheckShutdown();
// 判斷是否執行了關機
if (shutdown_command) {
HandlePowerctlMessage(*shutdown_command);
}
//判斷是否有事件需要處理
if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
am.ExecuteOneCommand();
}
if (!IsShuttingDown()) {
auto next_process_action_time = HandleProcessActions();
// If there's a process that needs restarting, wake up in time for that.
if (next_process_action_time) {
epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
*next_process_action_time - boot_clock::now());
if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
}
}
if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
// If there's more work to do, wake up again immediately.
if (am.HasMoreCommands()) epoll_timeout = 0ms;
}
auto pending_functions = epoll.Wait(epoll_timeout);
if (!pending_functions.ok()) {
LOG(ERROR) << pending_functions.error();
} else if (!pending_functions->empty()) {
// We always reap children before responding to the other pending functions. This is to
// prevent a race where other daemons see that a service has exited and ask init to
// start it again via ctl.start before init has reaped it.
ReapAnyOutstandingChildren();
for (const auto& function : *pending_functions) {
(*function)();
}
}
if (!IsShuttingDown()) {
HandleControlMessages();
SetUsbController();
}
}
return 0;
}
init最侄訓進入了無限回圈的監聽狀態,可以看到這里面一個核心函式就是 am.ExecuteOneCommand();該函式具體如下:
void ActionManager::ExecuteOneCommand() {
{
auto lock = std::lock_guard{event_queue_lock_};
// 編譯event_queue_ 佇列直到有事件處理
while (current_executing_actions_.empty() && !event_queue_.empty()) {
//遍歷action的向量(鏈表),包括所有決議出來的action,每一個action都包含了完整的資訊(command,triggle等)
for (const auto& action : actions_) {
// 一個action是否要執行,事件trigger和屬性trigger都必須要滿足,這里檢查event_queue_的第一個元素的屬性事件是不是滿足,會從屬性map表里查找其值,如果滿足才會執行下一步
if (std::visit([&action](const auto& event) { return action->CheckEvent(event); },
event_queue_.front())) {
//如果滿足這證明該action需要執行,把action壓入current_executing_actions_當前執行佇列中;
current_executing_actions_.emplace(action.get());
}
}
event_queue_.pop();
}
}
if (current_executing_actions_.empty()) {
return;
}
// 從當前需要執行的action佇列中取出第一個要執行的action
auto action = current_executing_actions_.front();
if (current_command_ == 0) {
std::string trigger_name = action->BuildTriggersString();
LOG(INFO) << "processing action (" << trigger_name << ") from (" << action->filename()
<< ":" << action->line() << ")";
}
// 開始執行action
action->ExecuteOneCommand(current_command_);
// 如果這是當前需要執行的action的最后一個命令,則從current_executing_actions_佇列中移除該action
// 如果這個action只執行依次,則從actions_向量中移除它
++current_command_;
if (current_command_ == action->NumCommands()) {
current_executing_actions_.pop();
current_command_ = 0;
if (action->oneshot()) {
auto eraser = [&action](std::unique_ptr<Action>& a) { return a.get() == action; };
actions_.erase(std::remove_if(actions_.begin(), actions_.end(), eraser),
actions_.end());
}
}
}
在上一步中,QueueEventTrigger("early-init")把early-init加入到event_queue_的佇列中,ExecuteOneCommand()一開始就遍歷之前決議的action向量表,使用每一個action自己的eventtrigger和event_queue_佇列中的第一個trigger對比,如果一樣,則繼續判斷該action中的PropertyTriggers,遍歷PropertyTriggers的map表查找當前action的PropertyTriggers值是否滿足條件,如果在eventtrigger相同且PropertyTriggers滿足條件的情況下,就把當前action push到current_executing_actions_佇列中;
接下來,從current_executing_actions_佇列中取出第一個要執行的action,呼叫action->ExecuteOneCommand(current_command_); 執行該命令,如下代碼
Result<void> Command::InvokeFunc(Subcontext* subcontext) const {
if (subcontext) {
if (execute_in_subcontext_) {
return subcontext->Execute(args_);
}
auto expanded_args = subcontext->ExpandArgs(args_);
if (!expanded_args.ok()) {
return expanded_args.error();
}
return RunBuiltinFunction(func_, *expanded_args, subcontext->context());
}
return RunBuiltinFunction(func_, args_, kInitContext);
}
該action下的所有命令被執行完成,;
當一個 action 物件所有的 command 均執行完畢后,再執行下一個action,
當一個 trigger 觸發對應的所有 action 物件均執行完畢后(一個action有且僅有一個eventtrigger,但是一個eventtrigger可以對應多個action,他們可以由不同的屬性),再執行下一個 trigger 對應 action,
到此就是一個action被決議以及到被執行的整個程序!!因為service和action存在自有的特性,因此決議執行稍有不同,需要理解的可以自行分析;
文章一開始的四個問題,前三個都在決議分析的程序中解開了;
關于磁區掛載的后續碰到再另行補充吧!
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/246586.html
標籤:其他
