本文從源代碼出發簡單地分析從在控制台輸入erl按下回車到init完成啟動步驟的過程。本文分析的環境為Unix環境,Erlang/OTP版本為R15B01,針對的虛擬機為SMP風格的虛擬機(也就是在代碼中定義ERTS_SMP宏)。
Erlang虛擬機的啟動
erl實際上是一個shell腳本,設置幾個環境變量之后,調用執行erlexec。erlexec的入口點在 otp_src_R15B01/erts/etc/common/erlexec.c 文件。erlexec的main函數首先分析erl傳入的參數和環境變量,選擇正確版本的beam可執行文件,然后將傳入的參數整理好,加入一些默認參數,最后通過系統調用execv運行beam虛擬機。例如在smp環境中,運行的就是 beam.smp 版本的虛擬機。因此,erl和erlexec都是加載器,最終執行的Erlang虛擬機進程是名字為beam系列的進程。
beam進程的入口點在 otp_src_R15B01/erts/emulator/sys/unix/erl_main.c 。在這個文件中,main函數只有一行:
1 erl_start(argc, argv);
通過erl_start這個函數真正進入Erlang虛擬機的世界了。erl_start函數位於 otp_src_R15B01/erts/emulator/beam/erl_init.c 文件,Erlang虛擬機初始化相關的代碼基本上都在這個文件中。這個函數大約600行,但是結構簡單,大部分代碼都是在處理參數。把erl_start的主干理出來,就是這樣的:
1 void erl_start(int argc, char **argv) 2 { 3 early_init(&argc, argv); 4 處理各種參數 5 設置信號處理函數 6 erl_init(ncpu); 7 init_shared_memory(boot_argc, boot_argv); 8 load_preloaded(); 9 erts_initialized = 1; 10 erl_first_process_otp("otp_ring0", NULL, 0, boot_argc, boot_argv); 11 erts_start_schedulers(); 12 erts_sys_main_thread(); 13 }
early_init()函數進行一些非常底層的初始化工作。erl_init()處理一些和Erlang虛擬機本身的初始化操作,例如各種數據結構的初始化。init_shared_memory()進行一些和內存回收相關的初始化。
load_preloaded()函數將需要預加載的Erlang模塊加載至虛擬機。需要預加載的模塊都在 otp_src_R15B01/erts/preloaded/ebin 目錄下。由於在build Erlang/OTP的時候,本地應該還沒有Erlang編譯器,所以這個目錄下提供的都是編譯好的.beam文件。這些模塊的源碼位於otp_src_R15B01/erts/preloaded/src 目錄。預加載模塊在build的時候由工具程序 make_preload 生成C語言文件硬編碼在虛擬機中了。如果想要修改預加載的文件,例如在里面加上 erlang:display() 表達式打印調試信息,可以修改src中的文件,然后通過編譯器erlc生成.beam文件保存在 otp_src_R15B01/erts/preloaded/ebin目錄下覆蓋原來的文件,再build即可。
在預加載的文件夾中可以看到,預加載的有以下模塊:
- erl_prim_loader:主要加載器,負責所有模塊的加載
- erlang:對虛擬機提供的一些BIF的接口
- init:init進程的代碼
- otp_ring0:Erlang虛擬機中第一個進程的代碼,啟動init
- prim_file:文件操作接口
- prim_inet:網絡操作接口
- prim_zip:壓縮文件操作接口
- zlib:zlib庫
把這些必要模塊都加載至虛擬機之后,通過erl_first_process_otp()函數創建了Erlang虛擬機上的第一個進程,調用 otp_ring0 模塊中的start/2函數。start/2 函數運行init模塊的 boot/1 函數,之后開始Erlang/OTP系統的引導過程。這里先把虛擬機的啟動過程分析完再講述Erlang/OTP的引導過程。
創建了第一個進程之后,進程還不能運行,因為還沒有創建調度器。erts_start_schedulers()根據CPU的核心數和用戶通過參數設置的數值啟動某個數目的調度器線程。每一個調度器都在一個線程中運行。調度器挑選要執行的進程,然后執行進程,當進程的reds用完或進程等待IO掛起的時候再挑選另一個進程執行。以后再撰文詳細分析Erlang調度器的工作原理。運行了erts_start_schedulers()函數之后Erlang虛擬機才真正運轉起來。
啟動調度器之后,調用erts_sys_main_thread()函數,也就是說beam進程的主線程進入了erts_sys_main_thread()函數。下面簡單分析一下erts_sys_main_thread()函數。
1 void erts_sys_main_thread(void) 2 { 3 erts_thread_disable_fpe(); 4 smp_sig_notify(0); /* Notify initialized */ 5 while (1) { 6 /* Wait for a signal to arrive... */ 7 select(0, NULL, NULL, NULL, NULL); 8 } 9 }
這個函數很簡單,屏蔽浮點數異常、通知信號處理線程已經完成了初始化,然后進入一個死循環等待信號。這個select調用表示永遠等待文件IO操作,但是什么文件也不等,只是把線程掛起。但是這個函數在收到信號的時候會返回。這里順便提一下Erlang虛擬機中的信號處理。在之前初始化的時候,設置了信號處理函數,也就是通過函數 init_break_handler() 設置了一些信號的處理函數。這些信號處理函數收到了信號之后實際上將信號通過管道轉發給了一個專門處理信號的線程,之前在調用 early_init() 的時候創建了這個線程,這個信號處理線程運行的函數是 signal_dispatcher_thread_func(),這個函數是一個死循環,等待從管道中讀取值。虛擬機的主線程通過 smp_sig_notify() 函數將通知消息寫入管道發給信號處理線程。
從Erlang虛擬機處理信號的方式可以看出,這種處理方式也是Erlang提倡的進程間通信方式。
下面分析otp_ring0的start/2調用init的boot/1引導Erlang/OTP系統的過程。
init進程的引導過程
init:boot/1的代碼如下:
1 boot(BootArgs) -> 2 register(init, self()), 3 process_flag(trap_exit, true), 4 start_on_load_handler_process(), 5 {Start0,Flags,Args} = parse_boot_args(BootArgs), 6 Start = map(fun prepare_run_args/1, Start0), 7 Flags0 = flags_to_atoms_again(Flags), 8 boot(Start,Flags0,Args).
第2行將當前進程注冊為init,於是我們就有了init進程。第4行啟動了一個新的進程ON_LOAD_HANDLER,這個進程處理一些和加載相關的事件。然后對傳入的參數做一些處理,Start是erl -s參數傳入的要運行的MFA列表,Flags0是調用erl傳入的一些標志,Args是erl -extra 傳入的一些額外參數。接下來這些參數傳入boot/3。下面是boot/3的代碼:
1 boot(Start,Flags,Args) -> 2 BootPid = do_boot(Flags,Start), 3 State = #state{flags = Flags, 4 args = Args, 5 start = Start, 6 bootpid = BootPid}, 7 boot_loop(BootPid,State).
boot/3調用do_boot/2,設置State,然后就進入boot_loop/2循環。下面是do_boot/2的代碼:
1 do_boot(Flags,Start) -> 2 Self = self(), 3 spawn_link(fun() -> do_boot(Self,Flags,Start) end). 4 5 do_boot(Init,Flags,Start) -> 6 process_flag(trap_exit,true), 7 {Pgm0,Nodes,Id,Path} = prim_load_flags(Flags), 8 Root = b2s(get_flag('-root',Flags)), 9 PathFls = path_flags(Flags), 10 Pgm = b2s(Pgm0), 11 _Pid = start_prim_loader(Init,b2a(Id),Pgm,bs2as(Nodes), 12 bs2ss(Path),PathFls), 13 BootFile = bootfile(Flags,Root), 14 BootList = get_boot(BootFile,Root), 15 LoadMode = b2a(get_flag('-mode',Flags,false)), 16 Deb = b2a(get_flag('-init_debug',Flags,false)), 17 catch ?ON_LOAD_HANDLER ! {init_debug_flag,Deb}, 18 BootVars = get_flag_args('-boot_var',Flags), 19 ParallelLoad = 20 (Pgm =:= "efile") and (erlang:system_info(thread_pool_size) > 0), 21 22 PathChoice = code_path_choice(), 23 eval_script(BootList,Init,PathFls,{Root,BootVars},Path, 24 {true,LoadMode,ParallelLoad},Deb,PathChoice), 25 26 %% To help identifying Purify windows that pop up, 27 %% print the node name into the Purify log. 28 (catch erlang:system_info({purify, "Node: " ++ atom_to_list(node())})), 29 30 start_em(Start).
do_boot/2創建了一個負責引導過程的進程(do_boot/3,沒有register,讓我們稱為do_boot),boot/2最后進入了一個boot_loop循環,接受來自do_boot進程的消息。現在系統上有3個進程,如下圖所示:
此時的<0.0.0>在boot_loop循環等待接受<0.2.0>發出的和boot相關的消息,<0.1.0>在等待接收和加載相關的消息。下面看do_boot/3的引導過程。第7-10行從傳入的參數中獲得加載器相關的參數,然后在第11行調用函數start_prim_loader通過erl_prim_loader模塊的start/3函數創建了加載器進程。第13-14行從啟動腳本中獲得啟動指令列表。有關啟動腳本的格式參見文檔 erl -man script ,啟動腳本描述了Erlang運行時系統啟動的過程,包含了啟動過程要執行的一系列指令。如果啟動erl的時候沒有帶-boot Name參數,那么默認使用start.boot啟動腳本。start.boot是由start.script生成的。start.script內容摘要如下所示:
1 {script, 2 {"OTP APN 181 01","R15B01"}, 3 [{preLoaded, 4 [erl_prim_loader,erlang,init,otp_ring0,prim_file,prim_inet,prim_zip,zlib]}, 5 {progress,preloaded}, 6 {path,["$ROOT/lib/kernel/ebin","$ROOT/lib/stdlib/ebin"]}, 7 {primLoad,[error_handler]}, 8 {kernel_load_completed}, 9 {progress,kernel_load_completed}, 10 {path,["$ROOT/lib/kernel/ebin"]}, 11 {primLoad, 12 [application,application_controller,application_master, 13 ... 14 standard_error,user,user_drv,user_sup,wrap_log_reader]}, 15 {path,["$ROOT/lib/stdlib/ebin"]}, 16 {primLoad, 17 [array,base64,beam_lib,binary,c,calendar,dets,dets_server,dets_sup, 18 ... 19 supervisor_bridge,sys,timer,unicode,win32reg,zip]}, 20 {progress,modules_loaded}, 21 {path,["$ROOT/lib/kernel/ebin","$ROOT/lib/stdlib/ebin"]}, 22 {kernelProcess,heart,{heart,start,[]}}, 23 {kernelProcess,error_logger,{error_logger,start_link,[]}}, 24 {kernelProcess,application_controller, 25 {application_controller,start, 26 [{application,kernel, 27 ...}]}}, 28 {progress,init_kernel_started}, 29 {apply, 30 {application,load, 31 [{application,stdlib, 32 ...}]}}, 33 {progress,applications_loaded}, 34 {apply,{application,start_boot,[kernel,permanent]}}, 35 {apply,{application,start_boot,[stdlib,permanent]}}, 36 {apply,{c,erlangrc,[]}}, 37 {progress,started}]}.
do_boot/3中的BootFile就是這個文件,BootList就是從第3行開始的這個列表。列表中的每一項表示一個動作,這些動作包括preLoaded、progress、path、primLoad、kernelProcess和apply,這些動作在erl -man script文檔中有詳細的解釋。do_boot/3的第23行調用eval_script/8函數負責執行這個列表中的每一個動作。下面是eval_script/8的代碼節選:
1 eval_script([{progress,Info}|CfgL],Init,PathFs,Vars,P,Ph,Deb,PathChoice) -> 2 debug(Deb,{progress,Info}), 3 init ! {self(),progress,Info}, 4 eval_script(CfgL,Init,PathFs,Vars,P,Ph,Deb,PathChoice); 5 eval_script([{preLoaded,_}|CfgL],Init,PathFs,Vars,P,Ph,Deb,PathChoice) -> 6 eval_script(CfgL,Init,PathFs,Vars,P,Ph,Deb,PathChoice); 7 eval_script([{path,Path}|CfgL],Init,{Pa,Pz},Vars,false,Ph,Deb,PathChoice) -> 8 ... 9 eval_script(CfgL,Init,{Pa,Pz},Vars,false,Ph,Deb,PathChoice); 10 eval_script([{path,_}|CfgL],Init,PathFs,Vars,P,Ph,Deb,PathChoice) -> 11 eval_script(CfgL,Init,PathFs,Vars,P,Ph,Deb,PathChoice); 12 eval_script([{kernel_load_completed}|CfgL],Init,PathFs,Vars,P,{_,embedded,Par},Deb,PathChoice) -> 13 eval_script(CfgL,Init,PathFs,Vars,P,{true,embedded,Par},Deb,PathChoice); 14 eval_script([{kernel_load_completed}|CfgL],Init,PathFs,Vars,P,{_,E,Par},Deb,PathChoice) -> 15 eval_script(CfgL,Init,PathFs,Vars,P,{false,E,Par},Deb,PathChoice); 16 eval_script([{primLoad,Mods}|CfgL],Init,PathFs,Vars,P,{true,E,Par},Deb,PathChoice) 17 ... 18 eval_script(CfgL,Init,PathFs,Vars,P,{true,E,Par},Deb,PathChoice); 19 eval_script([{primLoad,_Mods}|CfgL],Init,PathFs,Vars,P,{false,E,Par},Deb,PathChoice) -> 20 eval_script(CfgL,Init,PathFs,Vars,P,{false,E,Par},Deb,PathChoice); 21 eval_script([{kernelProcess,Server,{Mod,Fun,Args}}|CfgL],Init, 22 PathFs,Vars,P,Ph,Deb,PathChoice) -> 23 start_in_kernel(Server,Mod,Fun,Args,Init), 24 eval_script(CfgL,Init,PathFs,Vars,P,Ph,Deb,PathChoice); 25 eval_script([{apply,{Mod,Fun,Args}}|CfgL],Init,PathFs,Vars,P,Ph,Deb,PathChoice) -> 26 ... 27 eval_script(CfgL,Init,PathFs,Vars,P,Ph,Deb,PathChoice); 28 eval_script([],_,_,_,_,_,_,_) -> 29 ok; 30 eval_script(What,_,_,_,_,_,_,_) -> 31 exit({'unexpected command in bootfile',What}).
eval_script/8對BootList中的每一個動作進行處理。有一些動作要給init進程發送消息,init進程的boot_loop/2循環接收這些消息。boot_loop/2接收的消息中有以下兩個:
1 boot_loop(BootPid, State) -> 2 receive 3 {BootPid,progress,started} -> 4 {InS,_} = State#state.status, 5 notify(State#state.subscribed), 6 boot_loop(BootPid,State#state{status = {InS,started}, 7 subscribed = []}); 8 ...... 9 {'EXIT',BootPid,normal} -> 10 {_,PS} = State#state.status, 11 notify(State#state.subscribed), 12 loop(State#state{status = {started,PS}, 13 subscribed = []}); 14 ...... 15 end.
BootList最后一條指令是{progress,started},對應了boot_loop/2第3行的消息,在執行完這一條指令之后,eval_script/8結束了執行,因此do_boot/3在結束eval_script/8之后調用start_em/1之后就正常退出了,進程<0.2.0>正常退出,boot_loop/2收到'EXIT'消息,init進程進入loop/1循環。此時,init作為初始化的任務已經完成。這一個默認的啟動腳本啟動了兩個應用程序,kernel和STDLIB,前者是一個普通應用程序,后者只是一個庫應用程序。如果erl沒有傳入-noshell參數,kernel還會啟動shell和用戶交互。這兩個應用程序是Erlang最簡系統的基礎,前者提供了必要的系統服務,例如文件服務、網絡服務和錯誤日志記錄服務等,后者提供了編寫程序需要使用的各種工具、數據結構以及OTP相關的重要模塊。
小結
通過本文的分析可以看出,Erlang虛擬機很像一個運行了操作系統的計算機。erl對應的是BIOS,加載對應bootloader的erlexec。erlexec加載BEAM虛擬機,BEAM虛擬機對應了操作系統。接下來BEAM進行初步的初始化,初始化執行環境,對應了操作系統的初始化。初始化完成之后,BEAM像Linux一樣加載系統中的第一個進程init。init進程讀取啟動列表,執行啟動系統的步驟。執行完這些步驟之后,Erlang成為了一個完全完成了初始化過程可以運行的系統。Erlang像操作系統一樣,有自己的調度系統,內存管理系統,還有和外界交互的I/O系統。只不過內存管理系統更加的智能,可以主動幫助進程進行垃圾回收。I/O系統以系統服務的方式存在,通過Erlang消息通信的方式向其他進程提供服務,因此Erlang的進程只需要通過消息這一種語義就能和外界交換數據。Erlang中的模塊就好像操作系統中的動態共享庫,只要加載到系統中,就可以供所有的進程訪問。多個模塊可以組織為應用程序。Erlang的模塊命名是平坦的,因此不同應用程序中的模塊不能重名。Erlang的應用程序是對模塊和進程的一種組織方式,從一個應用程序可以包含一組進程的角度看,Erlang的應用程序有點類似於Linux系統中的進程。