什么是JITJIT(JustInTime)中文意思是即时编译,主要解决虚拟机运行中间代码时效率低的问题。在《eBPF实现原理》一文中,我们介绍了eBPF使用虚拟机来执行eBPF字节码。但是执行字节码是模拟CPU执行机器码的过程,所以效率比执行机器码要低很多。我们先看一下中间码和机器码执行的区别,如下图所示:(图1机器码执行流程)(图2中间码执行流程)从上图可以看出,在执行中间代码,虚拟机需要将中间代码解析成机器码执行,这个解析过程会消耗较多的CPU时间。eBPF使用JIT技术来解决中间代码执行效率低下的问题。JIT技术是在执行中间代码之前先将中间代码编译成对应的机器码,然后缓存起来,运行时直接执行机器码。这样就解决了每次执行都要解析中间代码的过程,如下图所示:(图3JIT执行过程)eBPFJIT实现原理当eBPF字节码加载到内核时,内核会检查是否JIT函数启用选项决定是否将eBPF字节码编译成机器码。由于不同架构的CPU指令集不同(即运行的机器码不同),因此不同架构的CPU将eBPF字节码编译成机器码的过程是不同的。本文分析的是x86架构的CPU,使用的内核版本是3.18.1。我们来看看内核是如何将eBPF字节码编译成机器码的。内核通过bpf_prog_load()函数加载eBPF字节码,如下:staticintbpf_prog_load(unionbpf_attr*attr){...bpf_prog_select_runtime(prog);...}其中,bpf_prog_load()会调用bpf_prog_select_runtime()函数为eBPF选择一个runtime。什么是eBPF运行时?说白了,就是用虚拟机运行,或者用JIT运行。我们看一下bpf_prog_select_runtime()函数的实现:bpf_int_jit_compile(fp);bpf_prog_lock_ro(fp);}bpf_prog结构体用于保存eBPF程序信息,其bpf_func字段用于指向eBPF字节码的执行函数。bpf_prog_select_runtime()函数会先将其设置为__bpf_prog_run()函数,表示__bpf_prog_run()函数是用来执行eBPF字节码的。然后bpf_prog_select_runtime()函数会调用bpf_int_jit_compile()函数来判断eBPF字节码是否需要编译成机器码。bpf_int_jit_compile()函数的实现如下(x86架构):voidbpf_int_jit_compile(structbpf_prog*prog){...structjit_contextctx={};u8*图像=空;//用于保存eBPF字节码编译后的机器码...//如果没有开启JIT功能,则不需要将eBPF字节码编译成机器码if(!bpf_jit_enable)return;...for(pass=0;pass<10;pass++){//将eBPF字节码编译成本地机器码proglen=do_jit(prog,addrs,image,oldproglen,&ctx);...}if(bpf_jit_enable>1)//打印eBPF字节码编译后的机器码bpf_jit_dump(prog->len,proglen,0,image);//如果成功将eBPF字节码编译成本地机器码if(image){...//然后将eBPF字节码执行函数设置为编译后的机器码prog->bpf_func=(void*)image;prog->jited=true;}...}bpf_int_jit_compile()函数会先判断内核是否开启了eBPF的JIT功能(即bpf_jit_enable全局变量是否大于0),如果没有开启,内核将不会进行JIT处理eBPF字节码。如果开启了JIT功能,bpf_int_jit_compile()函数会调用do_jit()函数将eBPF字节码编译成本地机器码,然后将bpf_prog结构体的bpf_func字段设置为编译后的字节码。这样,当内核调用bpf_func字段指向的函数时,就可以直接执行由eBPF字节码编译出来的机器码。我们来分析一下eBPF字节码编译过程中do_jit()函数的实现,如下图(do_jit()函数的实现有点复杂,这里只做一个大概的分析):staticintdo_jit(structbpf_prog*bpf_prog,int*addrs,u8*image,intoldproglen,structjit_context*ctx){structbpf_insn*insn=bpf_prog->insnsi;intinsn_cnt=bpf_prog->len;boolseen_ld_abs=ctx->seen_ld_abs|(旧程序==0);u8temp[BPF_MAX_INSN_SIZE+BPF_INSN_SAFETY];诠释我;intproglen=0;u8*程序=温度;//计算堆栈空间大小intstacksize=MAX_BPF_STACK+32/*rbx,r13,r14,r15的空间*/+8/*skb_copy_bits()缓冲区的空间*/;发射1(0x55);//将%rbp寄存器的值保存到堆栈:push%rbpEMIT3(0x48,0x89,0xE5);//将%rsp寄存器的值保存到%rbp寄存器中:mov%rbp,%rsp//申请栈空间的命令:sub%rsp,stacksizeEMIT3_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);/*for%eax清除寄存器,相对于:xor%eax,%eax*/EMIT3(0x4D,0x31,0xED);/*清除%r13寄存器,相对于:xor%r13,%r13*/...//遍历eBPF字节码,开始将eBPF字节码编译成本地机器码for(i=0;i
