1. 程式人生 > >Linux內核project導論——網絡:Filter(LSF、BPF、eBPF)

Linux內核project導論——網絡:Filter(LSF、BPF、eBPF)

linux內核 空間使用 自己 ket iat cls number 那種 機制

概覽

LSF(Linux socket filter)起源於BPF(Berkeley Packet Filter)。基礎從架構一致。但使用更簡單。LSF內部的BPF最早是cBPF(classic)。後來x86平臺首先切換到eBPF(extended)。但因為非常多上層應用程序仍然使用cBPF(tcpdump、iptables),而且eBPF還沒有支持非常多平臺,所以內核提供了從cBPF向eBPF轉換的邏輯,而且eBPF在設計的時候也是沿用了非常多cBPF的指令編碼。

可是在指令集合寄存器。還有架構設計上有非常大不同(比如eBPF已經能夠調用C函數,而且能夠跳轉到另外的eBPF程序)。
可是新的eBPF一出來就被玩壞了,人們非常快發現了它在內核trace方面的意義,它能夠保證絕對安全的獲取內核運行信息。是內核調試和開發人員的不二選擇。所以針對這個方面,比如kprobe、ktap、perf eBPF等優秀的工作逐漸產生。反而包過濾部門關註的人不夠多。

tc(traffic controll)是使用eBPF的一角優秀的用戶端程序,它同意不用又一次編譯模塊就能夠動態加入刪除新的流量控制算法。netfilter的xtable模塊,配合xt_bpf模塊。就能夠實現將eBPF程序加入到hook點。來實現過濾。當然,內核中提供了從cBPF到eBPF編譯的函數,所以,不論什麽情況下想要使用cBPF都能夠。內核會自己主動檢測和編譯。

bpf主要用途

其核心原理是對用戶提供了兩種SOCKET選項:SO_ATTACH_FILTER和SO_ATTACH_BPF。同意用戶在某個sokcet上加入一個自己定義的filter,僅僅有滿足該filter指定條件的數據包才會上發到用戶空間。

因為sokect有非常多種,你能夠在各個維度的socket加入這樣的filter。假設加入在raw socket。就能夠實現基於全部IP數據包的過濾(tcpdump就是這個原理),假設你想做一個http分析工具,就能夠在基於80port(或其它http監聽port)的socket加入filter。另一種使用方式離線式的。使用libpcap抓包存儲在本地,然後能夠使用bpf代碼對數據包進行離線分析,這對於實驗新的規則和測試bpf程序非常有幫:SO_ATTACH_FILTER插入的是cBPF代碼。SO_ATTACH_BPF插入的是eBPF代碼。eBPF是對cBPF的增強,眼下用戶端的tcpdump等程序還是用的cBPF版本號,其載入到內核中後會被內核自己主動的轉變為eBPF。

echo 2 > /proc/sys/net/core/bpf_jit_enable

通過像這個寫入0/1/2能夠實現關閉、打開、調試日誌等bpf模式。

在用戶空間使用。最簡單的辦法是使用libpcap的引擎。因為bpf是一種匯編類型的語言,自己寫難度比較高,所以libpcap提供了一些上層封裝能夠直接調用。然而libpcap並不能提供全部需求,比方bpf模塊開發人員的測試需求,還有高端的自己定義bpf腳本的需求。這樣的情況下就須要自己編寫bpf代碼,然後使用內核tools/net/文件夾下的工具進行編譯成bpf匯編代碼。再使用socket接口傳入這些代碼就可以。

bpf引擎在內核中實現。可是bpf程序的工作地點非常多須要額外的模塊來支持。經常使用的有netfilter自帶的xtable、xt_bpf 能夠實如今netfilter的hook點運行bpf程序、cls_bpf和act_bpf能夠實現對流量進行分類和丟棄(qos).
內核對bpf的完整支持是從3.9開始的,作為iptables的一部分存在,默認使用的是xt_bpf,用戶端的庫是libxt_bpf。iptables一開始對規則的管理方式是順序的一條條的運行。這樣的運行方式難免在匹配數目多的時候帶來性能瓶頸,加入了bpf支持後。靈活性大大提升。

以上全部提到的能夠使用bpf的地方均指同一時候可使用eBPF和cBPF。因為內核在運行前會自己主動檢查是否須要轉換編碼。

其它的BPF程序

前面說的bpf程序是用來做包過濾的,那麽bpf代碼僅僅能用來做包過濾嗎?非也。內核的bpf支持是一種基礎架構。僅僅是一種中間代碼的表達方式。是向用戶空間提供一個向內核註入可運行代碼的公共接口。僅僅是眼下的大部分應用是使用這個接口來做包過濾。其它的如seccomp BPF能夠用來實現限制用戶進程可使用的系統調用,cls_bpf能夠用來將流量分類,PTP dissector/classifier(幹啥的還不知道)等都是使用內核的eBPF語言架構來實現各自的目的,並不一定是包過濾功能。

用戶空間bpf支持

工具:tcpdump、tools/net、cloudfare、seccomp BPF、IO visitor、ktap

cBPF匯編架構分析

cBPF中每一條匯編指令都是例如以下格式:

struct sock_filter {    /* Filter block */
    __u16   code;   /* Actual filter code */
    __u8    jt; /* Jump true */
    __u8    jf; /* Jump false */
    __u32   k;      /* Generic multiuse field */
};

一個列子:op:16, jt:8, jf:8, k:32
code是真實的匯編指令,jt是指令結果為true的跳轉,jf是為false的跳轉,k是指令的參數,依據指令不同不同。

一個bpf程序編譯後就是一個sock_filter的數組,而能夠使用相似匯編的語法進行編程,然後使用內核提供的bpf_asm程序進行編譯。
bpf在內核中實際上是一個虛擬機。有自己定義的虛擬寄存器組。和我們熟悉的java虛擬機的原理一致。

這個虛擬機的設計是lsf的成功的所在。cBPF有3種寄存器:

  A           32位,全部載入指令的目的地址和全部指令運算結果的存儲地址
  X           32位。二元指令計算A中參數的輔助寄存器(比如移位的位數。除法的除數)
  M[]         0-151632位寄存器。能夠自由使用

我們最常見的使用方法莫過於從數據包中取某個字的數據內來做推斷。依照bpf的規定,我們能夠使用偏移來指定數據包的不論什麽位置,而非常多協議非經常常使用而且固定。比如port和ip地址等,bpf就為我們提供了一些提前定義的變量。僅僅要使用這個變量就能夠直接取值到相應的數據包位置。比如:

  len                                   skb->len
  proto                                 skb->protocol
  type                                  skb->pkt_type
  poff                                  Payload start offset
  ifidx                                 skb->dev->ifindex
  nla                                   Netlink attribute of type X with offset A
  nlan                                  Nested Netlink attribute of type X with offset A
  mark                                  skb->mark
  queue                                 skb->queue_mapping
  hatype                                skb->dev->type
  rxhash                                skb->hash
  cpu                                   raw_smp_processor_id()
  vlan_tci                              skb_vlan_tag_get(skb)
  vlan_avail                            skb_vlan_tag_present(skb)
  vlan_tpid                             skb->vlan_proto
  rand                                  prandom_u32()

更可貴的是這個列表還能夠由用戶自己去擴展。各種bpf引擎的詳細實現還會定義各自的擴展。

eBPF匯編架構分析

因為用戶能夠提交cBPF的代碼,首先是將用戶提交來的結構體數組進行編譯成eBPF代碼(提交的是eBPF就不用了)。然後再將eBPF代碼轉變為可直接運行的二進制。cBPF這在非常多平臺還在使用,這個代碼就和用戶空間使用的那種匯編是一樣的。可是在X86架構。如今在內核態已經都切換到使用eBPF作為中間語言了。也就是說x86在用戶空間使用的匯編和在內核空間使用的並不一樣。

可是內核在定義eBPF的時候已經盡量的復用cBPF的編碼,有的指令的編碼和意義,如BPF_LD都是全然一樣的。然而在還不支持eBPF的平臺,cBPF則是唯一能夠直接運行的代碼。不須要轉換為eBPF。


eBPF對每個bpf語句的表達與cBPF稍有不同,例如以下定義:

struct bpf_insn {
    __u8    code;       /* opcode */
    __u8    dst_reg:4;  /* dest register */
    __u8    src_reg:4;  /* source register */
    __s16   off;        /* signed offset */
    __s32   imm;        /* signed immediate constant */
};

其寄存器也不同:

    * R0    - return value from in-kernel function, and exit value for eBPF program
    * R1 - R5   - arguments from eBPF program to in-kernel function
    * R6 - R9   - callee saved registers that in-kernel function will preserve
    * R10   - read-only frame pointer to access stack

為了配合更強大的功能,eBPF匯編架構使用的寄存器有所添加,上述的寄存器的存在。充分體現了函數調用的概念。而不再是載入處理的原始邏輯。有了函數調用的邏輯設置能夠直接調用內核內部的函數(這是一個安全隱患。可是內部有規避機制)。不但如此,因為這樣的寄存器架構與x86等CPU的真實寄存器架構非常像,實際的實現正是實行了直接的寄存器映射,也就是說這些虛擬的寄存器實際上是使用的同功能的真實的寄存器。這無疑是對效率的極大提高。而且。在64位的計算機上這些計算機將會有64位的寬度。完美的發揮硬件能力。可是眼下的64位支持還不太完好。但已經可用。


眼下的內核實現。僅僅能夠在eBPF程序中調用預先定義好的內核函數,不能夠調用其它的eBPF程序(可是能夠通過map的支持跳轉到其它eBPF程序。然後再跳回來,後面有介紹)。這看起來無關緊要。可是卻是一個極大的能力。這就意味著你能夠使用C語言來實現eBPF程序邏輯。eBPF僅僅須要調用這個C函數就好了。

eBPF的數據交互:map

eBPF不可是程序,還能夠訪問外部的數據,重要的是這個外部的數據能夠在用戶空間管理。這個k-v格式的map數據體是通過在用戶空間調用bpf系統調用創建、加入、刪除等操作管理的。


用戶能夠同一時候定義多個map,使用fd來訪問某個map。

有一個特殊種類的map。叫program arry,這個map存儲的是其它eBPF程序的fd,通過這個map能夠實現eBPF之間的跳轉,跳轉走了就不會跳轉回來,最大深度是32,這樣就防止了無限循環的產生(也就是能夠使用這個機制實現有限循環)。更重要的是,這個map在運行時能夠通過bpf系統調用動態的改變,這就提供了強大的動態編程能力。比方能夠實現一個大型過程函數的中間某個過程的改變。實際上一共同擁有3種map:

BPF_MAP_TYPE_HASH, //hash類型
BPF_MAP_TYPE_ARRAY,  //數組類型
BPF_MAP_TYPE_PROG_ARRAY,  //程序表類型

eBPF的直接編程方法

除了在用戶空間通過nettable和tcpdump來使用bpf,在內核中或者在其它通用的編程中能夠直接使用C寫eBPF代碼。可是須要LLVM支持,樣例。


技術分享
在用戶空間通過使用bpf系統調用的BPF_PROG_LOAD方法。就能夠發送eBPF的代碼進內核,如此發送的代碼不須要再做轉換,因為其本身就是eBPF格式的。假設要在內核空間模塊使用eBPF,能夠直接使用相應的函數接口插入eBPF程序到sk_buff,提供強大的過濾能力。


Linux提供的系統調用bpf,用於操作eBPF相關的內核部分:

#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);

bpf man page
這個函數的第一個參數cmd就是內核支持的操作種類。包含BPF_MAP_CREATE、BPF_MAP_LOOKUP_ELEM、BPF_MAP_UPDATE_ELEM、BPF_MAP_DELETE_ELEM、BPF_MAP_GET_NEXT_KEY、BPF_PROG_LOAD 6種。然而,從名字上就能夠發現,有5種是用來操作map的。

這個map前面說過。是用戶程序和內核eBPF程序通信的唯一方式。這5個調用類型都是給用戶空間的程序使用的。最後一個BPF_PROG_LOAD 方法用來向內核中載入eBPF代碼體。


第二個參數attr則是cmd參數的詳細參數了,依據cmd的不同而不同,假設load的話還包含了完整的eBPF程序。
值得註意的是。每個map和eBPF都是一個文件。都有相應的fd,這個fd在用戶空間看來與其它fd無異。能夠釋放能夠通過unix domain socket在進程間傳遞。假設定義一個raw類型的socket。在其上附上eBPF程序過濾程序。其甚至就能夠直接充當iptable的規則使用。

內核中與bpf相關的內核模塊子系統

act_bpf
cls_bpf
IO visitor:這可能是基於eBPF相關的最大型的系統了。由多個廠商參與。
xtable、xt_bpf

bpf用於內核TRACING

我們知道eBPF有map數據結構,有程序運行能力。那麽這就是完美的跟蹤框架。比方通過kprobe將一個eBPF程序插入IO代碼,監控IO次數。然後通過map向用戶空間匯報詳細的值。用戶端僅僅須要每次使用bpf系統調用查看這個map就能夠得到想要統計的內容了。那麽為何要用eBPF,而不是直接使用kprobe的c代碼本身呢?這就是eBPF的安全性。其機制設計使其永遠不會crash掉內核。不會與正常的內核邏輯發生交叉影響。

能夠說,通過工具選擇避免了可能發生的非常多問題。

更可貴的是eBPF是原生的支持tracepoint,這就為kprobe不穩定的情況提供了可用性。

業界對eBPF的tracing使用

Brendan Gregg’s Blog 描寫敘述了一個使用eBPF進行kprobe測試的樣例。
ktap創造性的使用eBPF機制實現了內核模塊的腳本化,使用ktap,你能夠直接使用腳本編程。無須要編譯內核模塊,就能夠實現內核代碼的追蹤和插入。這背後就是eBPF和內核的tracing子系統。
bpf subcommand to perf:華為也在為bpf加入perf腳本的支持能力。
能夠看出來,eBPF起源於包過濾,可是眼下在trace市場得到越來越廣泛的應用。

意義和總結

也就是說眼下使用傳統的bpf語法和寄存器在用戶空間寫bpf代碼。代碼在內核中會被編譯成eBPF代碼,然後編譯為二進制運行。傳統的bpf語法和寄存器簡單,更面向業務,相似於高層次的編程語言,而內核的eBPF語法和寄存器復雜。相似於真實的匯編代碼。


那麽為何內核要大費周章的實現如此一個引擎呢?因為輕量級、安全性和可移植性。

因為是中間代碼,可移植性不必說,可是使用內核模塊調用內核的函數接口一般也是可移植的。所以這個並非非常重要的理由。eBPF代碼在運行的過程中被嚴格的限制了禁止循環和安全審查,使得eBPF被嚴格的定位於提供過程式的運行語句塊,甚至連函數都算不上,最大不超過4096個指令。所以這就是其定位:輕量級、安全、不循環。
上面說了幾個bpf的用途。但遠不至於此。

http://www.tcpdump.org/papers/bpf-usenix93.pdf
http://lwn.net/Articles/498231/
https://www.kernel.org/doc/Documentation/networking/filter.txt

Linux內核project導論——網絡:Filter(LSF、BPF、eBPF)