BEAM
是一個最近隨著gleam和riot等新技術的出現而被廣泛討論的虛擬機,使用它作為編譯目標,甚至重新建立它的一些功能。最酷的部分是,這個虛擬機器非常古老,是由愛立信於1986 年為Erlang語言建立的,這些技術是電信環境中應用程式的主要主角,每秒以可靠和高效能的方式處理無數的請求。
幾十年後, José Valim首次考慮將此 VM 用於 Web,他於 2011 年年中建立了Elixir語言,以現代語言充分利用BEAM
的強大功能,並專注於開發體驗。這種語言也促進了虛擬機器的普及,如果不是它,您可能不會閱讀本文,不是嗎? 👀
但畢竟…為什麼要建立這個虛擬機器呢?它的優勢、力量是什麼,最重要的是:它的秘密是什麼?在這篇文章中我們將簡單探討這個VM的細節,來吧!
BEAM
是構成OTP系統的部分之一,其作用類似於 Java 的 JVM,其主要目標是效能、並發性、容錯性以及使用輕量級執行緒來盡可能並發地執行運算。
與此虛擬機器的互動可以透過兩種主要方式完成:
*.beam
*.beam
字節碼(與elixir的情況相同)儘管經常被稱為最後一層執行,但BEAM
只是構成 Erlang/Elixir/Gleam 應用程式整個執行過程的元件之一。
正如您所看到的,BEAM 在 Erlang 執行時 (ERTS) 內執行,因此我們所有的應用程式程式碼都作為虛擬機器中的獨立node*
執行。
node*可以簡單理解為作業系統執行緒。每個節點可以同時執行數千個進程,這些進程是模仿本機作業系統行為的輕量級線程,具有以下功能:透過訊號相互通訊、執行計算和啟動其他進程。
好吧,我可以簡單地告訴您,BEAM 是一台具有堆疊機功能的錄音機,但這不是很有用,對吧?那麼讓我們回到開頭並理解每件事是什麼:
我們可以說堆疊機是建立解釋器最常見的方式,正是因為它有一個簡單的心理模型來理解,並且因為最終生成的程式碼非常容易被人類理解,可以將此模型視為一個 FIFO 隊列,其中我們有兩個主要行動:
push
:將一個值插入佇列pop
:從佇列中刪除一個值但僅此而已?是的!有了這個概念,我們可以將範例表達式2 + 2
翻譯為堆疊機👇:
push 2
push 2
add
我知道,那裡有一個詞add
,對嗎?但它與堆疊概念完全一致,不用擔心,在這種情況下add
表示對函數的呼叫,它所做的就是從堆疊中刪除參數(使用pop
),然後將操作結果加回排隊(使用push
),下面我們可以更詳細地看到這個過程:
push 2 #=> Adiciona 2 a stack
push 2 #=> Adiciona 2 a stack
add #=> detalhe da execução abaixo
pop #=> Remove argumento da stack
pop #=> Remove argumento da stack
push 4 #=> Adiciona o resultado 4 a stack
與堆疊機不同,暫存器使用暫存器來儲存參數值及其結果,並且這些值都不會被刪除,因為它能夠使用多個暫存器來儲存不同的內容(BEAM 使用X0
暫存器來儲存計算結果)。
透過具有使用更多暫存器的能力,暫存器機具有更多的操作運算符,例如move
、 swap
以及以特定方式存取暫存器值的方法,如下所示:
{function, add, 2, 2}.
{label,1}.
{line,[{location,"add.erl",4}]}.
{func_info,{atom,add},{atom,add},2}.
{label,2}.
{allocate,1,2}. #=> Prepara um lugar na memoria para eventualmente armazenar os valores
{move,{x,1},{y,0}}. #=> Copia o argumento da função (salvo automaticamente no registrador x) para um registrador temporario y
{call,1,{f,4}}.
{swap,{y,0},{x,0}}. #=> Inverte os valores de cada registrador
{call,1,{f,4}}.
{gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}. #=> Soma os numeros nos registradores x e y, sobrescrevendo então o registrador x com o resultado
{deallocate,1}.
return.
PS:本文的目的並不是要確切地教您寄存器(更不用說彙編)如何運作(因為即使我對此也不太了解😅),而是要概述該模型的行為方式。
現在我相信我們可以理解開頭的句子了吧? BEAM 是一個暫存器機,在特定暫存器(在本例中為Y
)中具有堆疊,因為這使得傳輸參數值變得更簡單。
如果您對 Elixir 或生態系統中的任何其他語言有足夠的了解,您就會聽說 BEAM 預設是同步的,但這意味著什麼?簡而言之,這意味著BEAM語言提供了處理並發運算的原生且智慧的方法。
正如我們之前所看到的,我們的整個應用程式執行在其中一個 VM 節點上,並且該節點可以有數千個進程(也稱為輕量級進程)同時執行計算。這些進程是我們在應用程式期間保證並發性的主要方式,我們本身俱有諸如spawn
類的函數來啟動新進程,並且使用這個主要函數,我們擁有完整的抽象,例如GenServer
和Task
,它們允許在特定上下文中使用進程。
並發應用程式的存在不僅僅是透過啟動新進程,對嗎? BEAM 語言也提供原生建構子來處理透過郵箱的進程間訊息交換。這些郵箱是像switch case
一樣工作的區塊,每個分支都可以使用如下所示的模式進行匹配:
靈丹妙藥
iex(1)> defmodule Listener do
...(1)> def call do
...(1)> receive do
...(1)> {:hello, msg} -> IO.puts("Received inside the mail: #{msg}")
...(1)> end
...(1)> end
...(1)> end
{:module, Listener,
<<70, 79, 82, 49, 0, 0, 6, 116, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 240,
0, 0, 0, 25, 15, 69, 108, 105, 120, 105, 114, 46, 76, 105, 115, 116, 101,
110, 101, 114, 8, 95, 95, 105, 110, 102, 111, ...>>, {:call, 0}}
iex(2)> pid = spawn(&Listener.call/0)
#PID<0.115.0>
iex(3)> send(pid, {:hello, "Hello World"})
Received inside the mail: Hello World
{:hello, "Hello World"}
iex(4)>
得到它
1> c("mailbox.erl").
{ok,mailbox}
2> mailbox:module_info().
[{module,mailbox},
{exports,[{call,0},{module_info,0},{module_info,1}]},
{attributes,[{vsn,[330096396114390727100476047769825248960]}]},
{compile,[{version,"8.4.3"},
{options,[]},
{source,"/private/tmp/NKI90h/mailbox.erl"}]},
{md5,<<248,86,64,221,149,120,150,9,30,225,159,226,217,
253,6,192>>}]
3> Pid = spawn(fun mailbox:call/0).
<0.93.0>
4> Pid ! {hello, "Hello World"}.
Received inside the mail: "Hello World"
{hello,"Hello World"}
5>
有了這個,我們就有了所有必要的工具來理解 BEAM 語言如何默認提供並發性,只需從一開始就同時定義它們的抽象,很酷吧?
額外提示:我有一系列文章討論 Elixir 語言中的這些抽象:https://dev.to/cherryramatis/handling-state- Between-multiple-instances-with-elixir-4jm1
讓我們記住一些很酷的事情,首先我們知道當我們啟動一個新的Erlang/Elixir 應用程式時,它在BEAM 節點上執行,其次我們知道我們有本地語言構造函數來操縱在該節點內執行的進程,並且可以透過訊息進行通訊交換對嗎?
現在如果我告訴你節點也可以通訊怎麼辦?這是正確的! BEAM 確實是分散式軟體的完美技術,在著名的蜜罐影片 José Valim 中解釋了我將在下面描述的這種交互,該影片可以在: https://www.youtube.com/watch?v = lxYFOM3UJzo從分鐘開始4:41
為了公正地對待 José Valim,我們將按照他在影片中的方式在 Elixir 中執行範例,但也可以使用 Erlang 以相同的方式執行此範例。
要在 BEAM 節點上啟動 Elixir 系統,我們將使用主機名稱啟動互動式 REPL,並定義一個在螢幕上列印「Hello world」的範例模組:
🍒 iex --name cherry
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]
Interactive Elixir (1.17.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex([email protected])1> defmodule Hello do
...([email protected])1> def world do
...([email protected])1> IO.puts("Hello World")
...([email protected])1> end
...([email protected])1> end
{:module, Hello,
<<70, 79, 82, 49, 0, 0, 5, 72, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 184,
0, 0, 0, 19, 12, 69, 108, 105, 120, 105, 114, 46, 72, 101, 108, 108, 111, 8,
95, 95, 105, 110, 102, 111, 95, 95, 10, ...>>, {:world, 0}}
iex([email protected])2> Hello.world
Hello World
:ok
iex([email protected])3>
在另一個終端中,讓我們使用另一個名稱啟動一個新的 REPL ,並嘗試執行上面實例中定義的模組:
🍒 iex --name kalane
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]
Interactive Elixir (1.17.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex([email protected])1> Hello.world
** (UndefinedFunctionError) function Hello.world/0 is undefined (module Hello is not available)
Hello.world()
iex:1: (file)
iex([email protected])1>
嗯...沒有那麼簡單,對吧?事實上,我們需要使用Node
模組來使這種通訊成為可能,現在讓我們在與之前相同的 REPL 中執行以下行:
iex([email protected])1> Node.spawn(:"[email protected]", fn -> Hello.world() end)
Hello World
#PID<13525.123.0>
iex([email protected])2>
你的頭爆炸了嗎?我希望如此,因為這意味著我們正在同一網路中以完全語言原生的方式通訊兩個獨立的 Elixir 實例(因此 Kubernetes /j)。
借助這項技術,我們可以建立整個發布訂閱和通知系統,而無需依賴專用的 SaaS,只需使用 VM 已提供的功能即可。
在這篇文章中,我盡力回顧了我對 BEAM 的最新研究,特別是回顧了我的熱情和發現時刻,因為在我閱讀和研究時,這無疑是一系列持續的「精神打擊」。
我的研究主要涉及嘗試從虛構的語言重新建立.beam
文件,該語言僅定義返回靜態值的函數,該專案在 elixir 中有一個初始 rust 版本和一個 WIP 版本,可以在連結中找到: https ://github 。
願原力與你同在🍒
原文出處:https://dev.to/cherryramatis/beam-vm-the-good-the-bad-and-the-ugly-9d6