自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文讀懂eBPF | 即時編譯(JIT)實現(xiàn)原理

開發(fā) 前端
eBPF 使用 JIT 技術(shù)來解決執(zhí)行中間碼效率不高的問題。JIT 技術(shù)就是在執(zhí)行中間碼前,先把中間碼編譯成對應(yīng)的機器碼,然后緩存起來,運行時直接通過執(zhí)行機器碼即可。

什么是 JIT

JIT(Just In Time)的中文意思是 即時編譯,主要為了解決虛擬機運行中間碼時效率不高的問題。

在《eBPF實現(xiàn)原理》一文中,我們介紹過 eBPF 是使用虛擬機來執(zhí)行 eBPF 字節(jié)碼的。但執(zhí)行字節(jié)碼是一個模擬 CPU 執(zhí)行機器碼的過程,所以比執(zhí)行機器碼的效率低很多。

我們來看看中間碼與機器碼執(zhí)行的區(qū)別,如下圖所示:

(圖一 機器碼執(zhí)行過程)

(圖二 中間碼執(zhí)行過程)

從上圖可以看出,執(zhí)行中間碼時,虛擬機需要將中間碼解析成機器碼來執(zhí)行,而這個解析的過程就需要消耗更多的 CPU 時間。

eBPF 使用 JIT 技術(shù)來解決執(zhí)行中間碼效率不高的問題。JIT 技術(shù)就是在執(zhí)行中間碼前,先把中間碼編譯成對應(yīng)的機器碼,然后緩存起來,運行時直接通過執(zhí)行機器碼即可。這樣就解決了每次執(zhí)行中間碼都需要解析的過程,如下圖所示:

(圖三 JIT執(zhí)行過程)

eBPF JIT 實現(xiàn)原理

當(dāng) eBPF 字節(jié)碼被加載到內(nèi)核時,內(nèi)核會根據(jù)是否開啟了 JIT 功能選項,來決定是否將 eBPF 字節(jié)碼編譯成機器碼。

由于不同架構(gòu) CPU 的指令集并不相同(也就是運行的機器碼不相同),所以對于不同架構(gòu)的 CPU,把 eBPF 字節(jié)碼編譯成機器碼的過程并不相同。

本文以 x86 架構(gòu)的 CPU 進(jìn)行分析,使用的內(nèi)核版本是 3.18.1。

我們來看看內(nèi)核是怎么將 eBPF 字節(jié)碼編譯成機器碼的。

內(nèi)核是通過 bpf_prog_load() 函數(shù)來加載 eBPF 字節(jié)碼,如下所示:

static int bpf_prog_load(union bpf_attr *attr)
{
...
bpf_prog_select_runtime(prog);
...
}

其中,bpf_prog_load() 會調(diào)用 bpf_prog_select_runtime() 函數(shù)來為 eBPF 選擇一個運行時。

什么是 eBPF 的運行時?說白了就是使用虛擬機運行還是使用 JIT 運行。我們來看看 bpf_prog_select_runtime() 函數(shù)的實現(xiàn):

void bpf_prog_select_runtime(struct bpf_prog *fp)
{
fp->bpf_func = (void *) __bpf_prog_run;

bpf_int_jit_compile(fp);
bpf_prog_lock_ro(fp);
}

bpf_prog 結(jié)構(gòu)用來保存 eBPF 程序的信息,其 bpf_func 字段用于指向 eBPF 字節(jié)碼的執(zhí)行函數(shù)。

bpf_prog_select_runtime() 函數(shù)首先會將其設(shè)置為 __bpf_prog_run() 函數(shù),表示使用 __bpf_prog_run() 函數(shù)來執(zhí)行 eBPF 字節(jié)碼。

接著 bpf_prog_select_runtime() 函數(shù)會調(diào)用 bpf_int_jit_compile() 函數(shù)來判斷是否需要將 eBPF 字節(jié)碼編譯成機器碼,bpf_int_jit_compile() 函數(shù)的實現(xiàn)如下(x86 架構(gòu)):

void bpf_int_jit_compile(struct bpf_prog *prog)
{
...
struct jit_context ctx = {};
u8 *image = NULL; // 用于保存 eBPF 字節(jié)碼編譯后的機器碼
...

// 如果沒有開啟 JIT 功能, 那么不需要將 eBPF 字節(jié)碼編譯成機器碼
if (!bpf_jit_enable)
return;

...
for (pass = 0; pass < 10; pass++) {
// 將 eBPF 字節(jié)碼編譯成本地機器碼
proglen = do_jit(prog, addrs, image, oldproglen, &ctx);
...
}

if (bpf_jit_enable > 1) // 打印 eBPF 字節(jié)碼編譯后的機器碼
bpf_jit_dump(prog->len, proglen, 0, image);

// 如果成功將 eBPF 字節(jié)碼編譯成本地機器碼
if (image) {
...
// 那么將 eBPF 字節(jié)碼執(zhí)行函數(shù)設(shè)置成編譯后的機器碼
prog->bpf_func = (void *)image;
prog->jited = true;
}
...
}

bpf_int_jit_compile() 函數(shù)首先會判斷內(nèi)核是否打開了 eBPF 的 JIT 功能(也就是 bpf_jit_enable 全局變量是否大于 0),如果沒有開啟,那么內(nèi)核將不會對 eBPF 字節(jié)碼進(jìn)行 JIT 處理。

如果打開了 JIT 功能,那么 bpf_int_jit_compile() 函數(shù)將會調(diào)用 do_jit() 函數(shù)把 eBPF 字節(jié)碼編譯成本地機器碼,然后將 bpf_prog 結(jié)構(gòu)的 bpf_func 字段設(shè)置成編譯后的字節(jié)碼。

這樣,當(dāng)內(nèi)核調(diào)用 bpf_func 字段指向的函數(shù)時,就能直接執(zhí)行 eBPF 字節(jié)碼編譯后的機器碼。

eBPF 字節(jié)碼編譯過程

我們來分析一下 do_jit() 函數(shù)的實現(xiàn),如下所示(do_jit() 函數(shù)的實現(xiàn)有點兒復(fù)雜,所以這里只對其進(jìn)行大概分析):

static int
do_jit(struct bpf_prog *bpf_prog, int *addrs, u8 *image,
int oldproglen, struct jit_context *ctx)
{
struct bpf_insn *insn = bpf_prog->insnsi;
int insn_cnt = bpf_prog->len;
bool seen_ld_abs = ctx->seen_ld_abs | (oldproglen == 0);
u8 temp[BPF_MAX_INSN_SIZE + BPF_INSN_SAFETY];
int i;
int proglen = 0;
u8 *prog = temp;
// 計算棧空間大小
int stacksize = MAX_BPF_STACK +
32 /* space for rbx, r13, r14, r15 */ +
8 /* space for skb_copy_bits() buffer */;

EMIT1(0x55); // 保存 %rbp 寄存器的值到棧:push %rbp
EMIT3(0x48, 0x89, 0xE5); //%rsp 寄存器的值保存到 %rbp 寄存器中:mov %rbp, %rsp

// 申請??臻g指令:sub %rsp, stacksize
EMIT3_off32(0x48, 0x81, 0xEC, stacksize);

// 保存 %rbx 寄存器的值到棧
EMIT3_off32(0x48, 0x89, 0x9D, -stacksize);
// 保存 %r13 寄存器的值到棧
EMIT3_off32(0x4C, 0x89, 0xAD, -stacksize + 8);
// 保存 %r14 寄存器的值到棧
EMIT3_off32(0x4C, 0x89, 0xB5, -stacksize + 16);
// 保存 %r15 寄存器的值到棧
EMIT3_off32(0x4C, 0x89, 0xBD, -stacksize + 24);

EMIT2(0x31, 0xc0); /* 對 %eax 寄存器清零,相對于:xor %eax, %eax */
EMIT3(0x4D, 0x31, 0xED); /* 對 %r13 寄存器清零,相對于:xor %r13, %r13 */

...
// 遍歷 eBPF 字節(jié)碼,開始將 eBPF 字節(jié)碼編譯成本地機器碼
for (i = 0; i < insn_cnt; i++, insn++) {
...
switch (insn->code) { // 通過一個 switch 語句來對 eBPF 字節(jié)碼進(jìn)行不同的編譯過程
...
/* 編譯:mov dst, src */
case BPF_ALU64 | BPF_MOV | BPF_X:
EMIT_mov(dst_reg, src_reg);
break;
...
}

ilen = prog - temp;
...
if (image) {
...
memcpy(image + proglen, temp, ilen);
}

proglen += ilen;
addrs[i] = proglen;
prog = temp;
}

return proglen;
}

由于 eBPF 程序會被編譯成一個函數(shù)調(diào)用,所以 do_jit() 函數(shù)首先會構(gòu)建一個函數(shù)調(diào)用的環(huán)境,如:申請函數(shù)??臻g,把一些寄存器壓棧等操作。

然后 do_jit() 函數(shù)會遍歷 eBPF 字節(jié)碼,并且對其進(jìn)行編譯成本地機器碼。

例如對于 eBPF 的 BPF_ALU64|BPF_MOV|BPF_X 字節(jié)碼,內(nèi)核會將其編譯成 mov %目標(biāo)寄存器, %源寄存器 指令的機器碼,其他 eBPF 字節(jié)碼的編譯過程類似。

所以,當(dāng)內(nèi)核沒有開啟 JIT 功能時,將會使用 __bpf_prog_run() 函數(shù)來執(zhí)行 eBPF 字節(jié)碼。

而當(dāng)內(nèi)核開啟了 JIT 功能時,內(nèi)核首先會將 eBPF 字節(jié)碼編譯成本地機器碼,然后直接執(zhí)行機器碼即可。

這樣就加速了 eBPF 程序的執(zhí)行效率。

責(zé)任編輯:武曉燕 來源: Linux內(nèi)核那些事
相關(guān)推薦

2021-12-16 14:45:09

https架構(gòu)服務(wù)端

2023-01-09 08:14:08

GoHttpServer

2022-05-12 10:53:42

keepalivevrrp協(xié)議

2021-06-23 10:00:46

eBPFKubernetesLinux

2023-12-22 19:59:15

2021-08-04 16:06:45

DataOps智領(lǐng)云

2021-10-20 07:18:51

Linux延時隊列

2021-10-15 14:28:30

React 組件渲染

2023-03-03 08:26:32

負(fù)載均衡算法服務(wù)

2017-08-21 10:00:23

遺傳算法Python生物學(xué)

2021-02-26 05:24:35

Java垃圾回收

2021-04-30 19:53:53

HugePages大內(nèi)存頁物理

2023-02-24 15:24:14

MySQL數(shù)據(jù)庫管理分庫分表

2024-04-10 10:34:34

Cache系統(tǒng)GPU

2018-09-28 14:06:25

前端緩存后端

2022-09-22 09:00:46

CSS單位

2025-04-03 10:56:47

2022-11-06 21:14:02

數(shù)據(jù)驅(qū)動架構(gòu)數(shù)據(jù)

2021-08-11 10:10:26

Linux定時器數(shù)組

2020-05-14 14:52:05

HDFS數(shù)據(jù)集架構(gòu)
點贊
收藏

51CTO技術(shù)棧公眾號