Kernel Bypass 完整技術指南
目錄
概述
Kernel Bypass 是一種繞過作業系統核心網路堆疊的技術,直接在使用者空間或驅動程式層處理網路封包,以達到更高的網路效能。
主要技術比較
| 特性 | 傳統核心網路 | DPDK | eBPF XDP |
|---|---|---|---|
| 處理位置 | 核心網路堆疊 | 使用者空間 | 驅動程式層 |
| 吞吐量 | 1-2M PPS | 10-20M PPS | 5-15M PPS |
| 延遲 | 50-100 μs | 1-10 μs | 5-20 μs |
| CPU 使用 | 高核心態 | 全使用者態 | 核心態但高效 |
| 靈活性 | 低 | 高 | 中 |
| 部署複雜度 | 簡單 | 複雜 | 中等 |
DPDK 實作
環境準備
# Ubuntu/Debian 安裝
sudo apt-get update
sudo apt-get install -y build-essential pkg-config
sudo apt-get install -y dpdk-dev libdpdk-dev libnuma-dev
# 或從原始碼編譯(推薦)
wget http://fast.dpdk.org/rel/dpdk-21.11.tar.xz
tar xf dpdk-21.11.tar.xz
cd dpdk-21.11
meson build
cd build
ninja
sudo ninja install
sudo ldconfig
# 設置環境變數
export PKG_CONFIG_PATH=/usr/local/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH
DPDK 完整程式範例
1. 簡單封包處理程式
// dpdk_packet_processor.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/queue.h>
#include <setjmp.h>
#include <stdarg.h>
#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdbool.h>
#include <rte_common.h>
#include <rte_log.h>
#include <rte_malloc.h>
#include <rte_memory.h>
#include <rte_memcpy.h>
#include <rte_eal.h>
#include <rte_launch.h>
#include <rte_cycles.h>
#include <rte_prefetch.h>
#include <rte_lcore.h>
#include <rte_per_lcore.h>
#include <rte_branch_prediction.h>
#include <rte_interrupts.h>
#include <rte_random.h>
#include <rte_debug.h>
#include <rte_ether.h>
#include <rte_ethdev.h>
#include <rte_mempool.h>
#include <rte_mbuf.h>
#include <rte_ip.h>
#include <rte_tcp.h>
#include <rte_udp.h>
#define RX_RING_SIZE 1024
#define TX_RING_SIZE 1024
#define NUM_MBUFS 8191
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE 32
static volatile bool force_quit;
// 埠統計資訊
struct port_statistics {
uint64_t tx;
uint64_t rx;
uint64_t dropped;
} __rte_cache_aligned;
struct port_statistics port_statistics[RTE_MAX_ETHPORTS];
// 乙太網路埠配置
static struct rte_eth_conf port_conf = {
.rxmode = {
.max_rx_pkt_len = RTE_ETHER_MAX_LEN,
.split_hdr_size = 0,
},
.txmode = {
.mq_mode = ETH_MQ_TX_NONE,
},
};
// 列印統計資訊
static void
print_stats(void) {
uint64_t total_packets_dropped, total_packets_tx, total_packets_rx;
unsigned portid;
total_packets_dropped = 0;
total_packets_tx = 0;
total_packets_rx = 0;
const char clr[] = { 27, '[', '2', 'J', '\0' };
const char topLeft[] = { 27, '[', '1', ';', '1', 'H', '\0' };
// 清除螢幕並移至左上角
printf("%s%s", clr, topLeft);
printf("\n====================================\n");
printf(" 封包處理統計資訊\n");
printf("====================================\n");
for (portid = 0; portid < RTE_MAX_ETHPORTS; portid++) {
if (port_statistics[portid].rx > 0 || port_statistics[portid].tx > 0) {
printf("\n埠 %u 統計:\n", portid);
printf(" 接收封包: %"PRIu64"\n", port_statistics[portid].rx);
printf(" 傳送封包: %"PRIu64"\n", port_statistics[portid].tx);
printf(" 丟棄封包: %"PRIu64"\n", port_statistics[portid].dropped);
total_packets_dropped += port_statistics[portid].dropped;
total_packets_tx += port_statistics[portid].tx;
total_packets_rx += port_statistics[portid].rx;
}
}
printf("\n總計:\n");
printf(" 接收: %"PRIu64"\n", total_packets_rx);
printf(" 傳送: %"PRIu64"\n", total_packets_tx);
printf(" 丟棄: %"PRIu64"\n", total_packets_dropped);
printf("====================================\n");
}
// 封包處理函數
static void
process_packet(struct rte_mbuf *pkt) {
struct rte_ether_hdr *eth_hdr;
struct rte_ipv4_hdr *ipv4_hdr;
struct rte_tcp_hdr *tcp_hdr;
struct rte_udp_hdr *udp_hdr;
eth_hdr = rte_pktmbuf_mtod(pkt, struct rte_ether_hdr *);
// 檢查是否為 IPv4 封包
if (eth_hdr->ether_type == rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
ipv4_hdr = (struct rte_ipv4_hdr *)(eth_hdr + 1);
// 處理 TCP 封包
if (ipv4_hdr->next_proto_id == IPPROTO_TCP) {
tcp_hdr = (struct rte_tcp_hdr *)((unsigned char *)ipv4_hdr +
(ipv4_hdr->version_ihl & 0x0f) * 4);
// 這裡可以加入 TCP 封包處理邏輯
// 例如:過濾特定埠、修改封包內容等
}
// 處理 UDP 封包
else if (ipv4_hdr->next_proto_id == IPPROTO_UDP) {
udp_hdr = (struct rte_udp_hdr *)((unsigned char *)ipv4_hdr +
(ipv4_hdr->version_ihl & 0x0f) * 4);
// 這裡可以加入 UDP 封包處理邏輯
}
}
}
// 主要封包處理迴圈
static int
lcore_main(void) {
struct rte_mbuf *pkts_burst[BURST_SIZE];
struct rte_mbuf *pkt;
unsigned lcore_id;
uint64_t prev_tsc, diff_tsc, cur_tsc, timer_tsc;
unsigned i, j, portid, nb_rx;
const uint64_t drain_tsc = (rte_get_tsc_hz() + US_PER_S - 1) / US_PER_S * BURST_TX_DRAIN_US;
struct rte_eth_dev_tx_buffer *buffer;
prev_tsc = 0;
timer_tsc = 0;
lcore_id = rte_lcore_id();
printf("進入主迴圈,核心 %u\n", lcore_id);
while (!force_quit) {
cur_tsc = rte_rdtsc();
// 定期列印統計資訊
diff_tsc = cur_tsc - prev_tsc;
if (unlikely(diff_tsc > drain_tsc)) {
// 每秒列印一次統計
if (timer_tsc >= rte_get_tsc_hz()) {
print_stats();
timer_tsc = 0;
}
prev_tsc = cur_tsc;
timer_tsc += diff_tsc;
}
// 從所有埠接收封包
RTE_ETH_FOREACH_DEV(portid) {
nb_rx = rte_eth_rx_burst(portid, 0, pkts_burst, BURST_SIZE);
if (nb_rx == 0)
continue;
port_statistics[portid].rx += nb_rx;
// 處理每個封包
for (i = 0; i < nb_rx; i++) {
pkt = pkts_burst[i];
// 處理封包
process_packet(pkt);
// 簡單轉發:修改 MAC 地址並送回
struct rte_ether_hdr *eth = rte_pktmbuf_mtod(pkt, struct rte_ether_hdr *);
struct rte_ether_addr tmp;
// 交換源和目的 MAC 地址
rte_ether_addr_copy(ð->s_addr, &tmp);
rte_ether_addr_copy(ð->d_addr, ð->s_addr);
rte_ether_addr_copy(&tmp, ð->d_addr);
// 傳送封包
const uint16_t nb_tx = rte_eth_tx_burst(portid, 0, &pkt, 1);
if (nb_tx) {
port_statistics[portid].tx += nb_tx;
} else {
port_statistics[portid].dropped += 1;
rte_pktmbuf_free(pkt);
}
}
}
}
return 0;
}
// 信號處理
static void
signal_handler(int signum) {
if (signum == SIGINT || signum == SIGTERM) {
printf("\n\n收到信號 %d,準備退出...\n", signum);
force_quit = true;
}
}
// 埠初始化
static inline int
port_init(uint16_t port, struct rte_mempool *mbuf_pool) {
struct rte_eth_conf port_conf = port_conf;
const uint16_t rx_rings = 1, tx_rings = 1;
uint16_t nb_rxd = RX_RING_SIZE;
uint16_t nb_txd = TX_RING_SIZE;
int retval;
uint16_t q;
struct rte_eth_dev_info dev_info;
struct rte_eth_txconf txconf;
if (!rte_eth_dev_is_valid_port(port))
return -1;
// 取得埠資訊
retval = rte_eth_dev_info_get(port, &dev_info);
if (retval != 0) {
printf("取得埠 %u 資訊錯誤: %s\n", port, strerror(-retval));
return retval;
}
if (dev_info.tx_offload_capa & DEV_TX_OFFLOAD_MBUF_FAST_FREE)
port_conf.txmode.offloads |= DEV_TX_OFFLOAD_MBUF_FAST_FREE;
// 配置埠
retval = rte_eth_dev_configure(port, rx_rings, tx_rings, &port_conf);
if (retval != 0)
return retval;
retval = rte_eth_dev_adjust_nb_rx_tx_desc(port, &nb_rxd, &nb_txd);
if (retval != 0)
return retval;
// 配置 RX 佇列
for (q = 0; q < rx_rings; q++) {
retval = rte_eth_rx_queue_setup(port, q, nb_rxd,
rte_eth_dev_socket_id(port), NULL, mbuf_pool);
if (retval < 0)
return retval;
}
txconf = dev_info.default_txconf;
txconf.offloads = port_conf.txmode.offloads;
// 配置 TX 佇列
for (q = 0; q < tx_rings; q++) {
retval = rte_eth_tx_queue_setup(port, q, nb_txd,
rte_eth_dev_socket_id(port), &txconf);
if (retval < 0)
return retval;
}
// 啟動埠
retval = rte_eth_dev_start(port);
if (retval < 0)
return retval;
// 顯示埠 MAC 地址
struct rte_ether_addr addr;
retval = rte_eth_macaddr_get(port, &addr);
if (retval != 0)
return retval;
printf("埠 %u MAC: %02" PRIx8 " %02" PRIx8 " %02" PRIx8
" %02" PRIx8 " %02" PRIx8 " %02" PRIx8 "\n",
port,
addr.addr_bytes[0], addr.addr_bytes[1],
addr.addr_bytes[2], addr.addr_bytes[3],
addr.addr_bytes[4], addr.addr_bytes[5]);
// 啟用混雜模式
retval = rte_eth_promiscuous_enable(port);
if (retval != 0)
return retval;
return 0;
}
int
main(int argc, char *argv[]) {
struct rte_mempool *mbuf_pool;
unsigned nb_ports;
uint16_t portid;
// 初始化 EAL
int ret = rte_eal_init(argc, argv);
if (ret < 0)
rte_exit(EXIT_FAILURE, "EAL 初始化錯誤\n");
argc -= ret;
argv += ret;
force_quit = false;
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// 檢查可用埠數
nb_ports = rte_eth_dev_count_avail();
if (nb_ports < 1)
rte_exit(EXIT_FAILURE, "錯誤:沒有找到乙太網路埠\n");
printf("找到 %u 個埠\n", nb_ports);
// 建立 mbuf 記憶體池
mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", NUM_MBUFS * nb_ports,
MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
if (mbuf_pool == NULL)
rte_exit(EXIT_FAILURE, "無法建立 mbuf 池\n");
// 初始化所有埠
RTE_ETH_FOREACH_DEV(portid) {
printf("初始化埠 %u...\n", portid);
if (port_init(portid, mbuf_pool) != 0)
rte_exit(EXIT_FAILURE, "無法初始化埠 %u\n", portid);
}
if (rte_lcore_count() > 1)
printf("\n警告:執行太多核心,只使用核心 0。\n");
// 呼叫主處理迴圈
lcore_main();
// 清理
RTE_ETH_FOREACH_DEV(portid) {
printf("關閉埠 %u...", portid);
rte_eth_dev_stop(portid);
rte_eth_dev_close(portid);
printf(" 完成\n");
}
printf("再見。\n");
return 0;
}
2. DPDK 編譯腳本
#!/bin/bash
# compile_dpdk.sh
# 設置 DPDK 環境變數
export RTE_SDK=/usr/local/share/dpdk
export RTE_TARGET=x86_64-native-linux-gcc
# 編譯選項
CC=gcc
CFLAGS="-O3 -g -Wall"
LDFLAGS=""
# 使用 pkg-config 取得 DPDK 編譯參數
DPDK_CFLAGS=$(pkg-config --cflags libdpdk)
DPDK_LDFLAGS=$(pkg-config --libs libdpdk)
# 編譯程式
echo "編譯 DPDK 程式..."
$CC $CFLAGS $DPDK_CFLAGS -o dpdk_packet_processor dpdk_packet_processor.c $DPDK_LDFLAGS
if [ $? -eq 0 ]; then
echo "編譯成功!"
echo "執行方式:"
echo " sudo ./dpdk_packet_processor -l 0-1 -n 4"
else
echo "編譯失敗!"
exit 1
fi
DPDK 執行設置
#!/bin/bash
# setup_dpdk.sh
# 檢查是否為 root
if [ "$EUID" -ne 0 ]; then
echo "請使用 root 權限執行"
exit 1
fi
echo "=== DPDK 環境設置 ==="
# 1. 載入必要的核心模組
echo "載入核心模組..."
modprobe uio
modprobe uio_pci_generic
modprobe vfio-pci
# 2. 設置大頁記憶體
echo "設置大頁記憶體..."
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 建立掛載點
mkdir -p /mnt/huge
# 掛載 hugetlbfs
mount -t hugetlbfs hugetlbfs /mnt/huge
# 3. 顯示網路介面
echo "可用的網路介面:"
ip link show
# 4. 綁定網路卡到 DPDK(需要手動修改)
echo ""
echo "要綁定網路卡到 DPDK,執行:"
echo " dpdk-devbind.py --bind=uio_pci_generic <PCI地址>"
echo ""
echo "查看 PCI 地址:"
lspci | grep -i ethernet
# 5. 檢查設置
echo ""
echo "=== 設置狀態 ==="
echo "大頁記憶體:"
cat /proc/meminfo | grep -i huge
echo ""
echo "已載入的驅動程式:"
lsmod | grep -E "uio|vfio"
eBPF XDP 實作
環境準備
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y clang llvm libbpf-dev linux-headers-$(uname -r)
sudo apt-get install -y build-essential libelf-dev
# CentOS/RHEL
sudo yum install -y clang llvm libbpf-devel kernel-devel
sudo yum install -y elfutils-libelf-devel
XDP 完整程式範例
1. XDP 封包處理程式
// xdp_prog.c - eBPF/XDP 核心程式
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/if_vlan.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/in.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define MAX_MAP_ENTRIES 256
// 定義封包統計 Map
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, MAX_MAP_ENTRIES);
__type(key, __u32);
__type(value, __u64);
} packet_stats SEC(".maps");
// 定義黑名單 Map(IP 地址)
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10000);
__type(key, __u32); // IPv4 地址
__type(value, __u64); // 封包計數
} blacklist SEC(".maps");
// 定義埠轉發規則 Map
struct port_rule {
__u16 src_port;
__u16 dst_port;
__u8 action; // 0: PASS, 1: DROP, 2: REDIRECT
__u8 pad[3];
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1000);
__type(key, __u16); // 源埠
__type(value, struct port_rule);
} port_rules SEC(".maps");
// 輔助函數:解析封包並更新統計
static __always_inline void update_stats(int proto) {
__u32 key = proto;
__u64 *value;
value = bpf_map_lookup_elem(&packet_stats, &key);
if (value) {
__sync_fetch_and_add(value, 1);
}
}
// 輔助函數:檢查 IP 是否在黑名單中
static __always_inline int check_blacklist(__u32 ip) {
__u64 *counter;
counter = bpf_map_lookup_elem(&blacklist, &ip);
if (counter) {
__sync_fetch_and_add(counter, 1);
return 1; // 在黑名單中
}
return 0;
}
// 主要 XDP 程式
SEC("xdp")
int xdp_prog_main(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// 解析乙太網路標頭
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
// 更新乙太網路類型統計
update_stats(0); // 0 表示總封包數
// 只處理 IPv4 封包
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
// 解析 IP 標頭
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
// 檢查源 IP 是否在黑名單中
if (check_blacklist(ip->saddr)) {
return XDP_DROP; // 丟棄黑名單 IP 的封包
}
// 更新協議統計
update_stats(ip->protocol);
// 處理 TCP 封包
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
// 檢查埠規則
__u16 src_port = bpf_ntohs(tcp->source);
struct port_rule *rule;
rule = bpf_map_lookup_elem(&port_rules, &src_port);
if (rule) {
if (rule->action == 1) {
return XDP_DROP; // 丟棄封包
} else if (rule->action == 2) {
// 修改目標埠(埠轉發)
tcp->dest = bpf_htons(rule->dst_port);
// 重新計算校驗和(簡化版)
tcp->check = 0;
}
}
// 特殊處理:阻擋 HTTP (埠 80) 流量
if (tcp->dest == bpf_htons(80)) {
update_stats(200); // 200 表示阻擋的 HTTP 封包
return XDP_DROP;
}
}
// 處理 UDP 封包
else if (ip->protocol == IPPROTO_UDP) {
struct udphdr *udp = (void *)ip + (ip->ihl * 4);
if ((void *)(udp + 1) > data_end)
return XDP_PASS;
// 特殊處理:阻擋 DNS (埠 53) 流量到特定伺服器
if (udp->dest == bpf_htons(53)) {
// 可以加入更多邏輯
update_stats(201); // 201 表示 DNS 封包
}
}
return XDP_PASS; // 預設允許通過
}
// XDP 程式 - 簡單 DDoS 防護
SEC("xdp_ddos")
int xdp_ddos_filter(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
// 簡單的速率限制:使用 IP 地址作為 key
static __u64 last_seen = 0;
__u64 now = bpf_ktime_get_ns();
// 如果同一個 IP 在 1ms 內發送超過一個封包,丟棄
if (last_seen != 0 && (now - last_seen) < 1000000) {
return XDP_DROP;
}
last_seen = now;
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
2. XDP 載入器程式
// xdp_loader.c - 使用者空間載入器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <net/if.h>
#include <sys/resource.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <linux/if_link.h>
#include <linux/bpf.h>
static volatile sig_atomic_t keep_running = 1;
static int ifindex;
static __u32 xdp_flags = XDP_FLAGS_UPDATE_IF_NOEXIST;
struct bpf_progs_desc {
char name[256];
int prog_fd;
};
static void sig_handler(int sig) {
keep_running = 0;
}
static int do_attach(int idx, int prog_fd, const char *name) {
int err;
err = bpf_set_link_xdp_fd(idx, prog_fd, xdp_flags);
if (err < 0) {
fprintf(stderr, "錯誤:附加 XDP 程式失敗: %s\n", strerror(-err));
return err;
}
printf("成功附加 XDP 程式 '%s' 到介面索引 %d\n", name, idx);
return 0;
}
static int do_detach(int idx) {
int err;
err = bpf_set_link_xdp_fd(idx, -1, xdp_flags);
if (err < 0) {
fprintf(stderr, "錯誤:分離 XDP 程式失敗: %s\n", strerror(-err));
return err;
}
printf("成功分離 XDP 程式從介面索引 %d\n", idx);
return 0;
}
static void poll_stats(int map_fd, int interval) {
__u64 values[256] = {0};
__u32 key;
while (keep_running) {
sleep(interval);
printf("\n===== 封包統計 =====\n");
// 讀取統計資料
for (key = 0; key < 256; key++) {
__u64 sum = 0;
if (bpf_map_lookup_elem(map_fd, &key, values) == 0) {
// 累加所有 CPU 的值
int ncpus = libbpf_num_possible_cpus();
for (int i = 0; i < ncpus; i++) {
sum += values[i];
}
if (sum > 0) {
if (key == 0) {
printf("總封包數: %llu\n", sum);
} else if (key == IPPROTO_TCP) {
printf("TCP 封包: %llu\n", sum);
} else if (key == IPPROTO_UDP) {
printf("UDP 封包: %llu\n", sum);
} else if (key == IPPROTO_ICMP) {
printf("ICMP 封包: %llu\n", sum);
} else if (key == 200) {
printf("阻擋的 HTTP 封包: %llu\n", sum);
} else if (key == 201) {
printf("DNS 封包: %llu\n", sum);
}
}
}
}
}
}
static void usage(const char *prog) {
fprintf(stderr,
"用法: %s [選項] <介面>\n"
"選項:\n"
" -p, --prog <程式名> 指定要載入的 XDP 程式 (預設: xdp_prog_main)\n"
" -s, --skb-mode 使用 SKB 模式\n"
" -n, --native-mode 使用 Native 模式\n"
" -f, --force 強制更新 XDP 程式\n"
" -u, --unload 卸載 XDP 程式\n"
" -i, --interval <秒> 統計輪詢間隔 (預設: 2)\n"
" -h, --help 顯示此幫助\n",
prog);
}
int main(int argc, char **argv) {
struct bpf_prog_load_attr prog_load_attr = {
.prog_type = BPF_PROG_TYPE_XDP,
.file = "xdp_prog.o",
};
struct bpf_object *obj;
struct bpf_map *map;
char *prog_name = "xdp_prog_main";
char ifname[IF_NAMESIZE];
int prog_fd, map_fd;
int opt, interval = 2;
int unload = 0;
// 解析命令列參數
struct option long_options[] = {
{"prog", required_argument, 0, 'p'},
{"skb-mode", no_argument, 0, 's'},
{"native-mode", no_argument, 0, 'n'},
{"force", no_argument, 0, 'f'},
{"unload", no_argument, 0, 'u'},
{"interval", required_argument, 0, 'i'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
while ((opt = getopt_long(argc, argv, "p:snfui:h", long_options, NULL)) != -1) {
switch (opt) {
case 'p':
prog_name = optarg;
break;
case 's':
xdp_flags |= XDP_FLAGS_SKB_MODE;
break;
case 'n':
xdp_flags |= XDP_FLAGS_DRV_MODE;
break;
case 'f':
xdp_flags &= ~XDP_FLAGS_UPDATE_IF_NOEXIST;
break;
case 'u':
unload = 1;
break;
case 'i':
interval = atoi(optarg);
break;
case 'h':
default:
usage(argv[0]);
return 1;
}
}
if (optind >= argc) {
fprintf(stderr, "錯誤:未指定介面\n");
usage(argv[0]);
return 1;
}
// 取得介面名稱和索引
strncpy(ifname, argv[optind], IF_NAMESIZE - 1);
ifindex = if_nametoindex(ifname);
if (ifindex == 0) {
fprintf(stderr, "錯誤:找不到介面 %s\n", ifname);
return 1;
}
// 如果是卸載模式
if (unload) {
return do_detach(ifindex);
}
// 提升資源限制
struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
if (setrlimit(RLIMIT_MEMLOCK, &r)) {
fprintf(stderr, "錯誤:設置資源限制失敗\n");
return 1;
}
// 載入 BPF 程式
if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) {
fprintf(stderr, "錯誤:載入 BPF 程式失敗\n");
return 1;
}
// 尋找指定的程式
struct bpf_program *prog;
prog = bpf_object__find_program_by_title(obj, prog_name);
if (!prog) {
fprintf(stderr, "錯誤:找不到程式 %s\n", prog_name);
return 1;
}
prog_fd = bpf_program__fd(prog);
if (prog_fd < 0) {
fprintf(stderr, "錯誤:取得程式 fd 失敗\n");
return 1;
}
// 附加到介面
if (do_attach(ifindex, prog_fd, prog_name) != 0) {
return 1;
}
// 尋找統計 map
map = bpf_object__find_map_by_name(obj, "packet_stats");
if (!map) {
fprintf(stderr, "警告:找不到 packet_stats map\n");
} else {
map_fd = bpf_map__fd(map);
// 設置信號處理
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
printf("開始監控統計資料... (Ctrl+C 結束)\n");
// 輪詢統計資料
poll_stats(map_fd, interval);
// 清理
do_detach(ifindex);
}
return 0;
}
3. XDP 管理工具
// xdp_admin.c - XDP 管理工具
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#define BLACKLIST_MAP_PATH "/sys/fs/bpf/blacklist"
#define PORT_RULES_MAP_PATH "/sys/fs/bpf/port_rules"
// 添加 IP 到黑名單
int add_to_blacklist(const char *ip_str) {
int map_fd;
__u32 ip;
__u64 counter = 0;
// 轉換 IP 地址
if (inet_pton(AF_INET, ip_str, &ip) != 1) {
fprintf(stderr, "無效的 IP 地址: %s\n", ip_str);
return -1;
}
// 開啟 map
map_fd = bpf_obj_get(BLACKLIST_MAP_PATH);
if (map_fd < 0) {
fprintf(stderr, "無法開啟黑名單 map: %s\n", strerror(errno));
return -1;
}
// 添加到黑名單
if (bpf_map_update_elem(map_fd, &ip, &counter, BPF_ANY) != 0) {
fprintf(stderr, "添加到黑名單失敗: %s\n", strerror(errno));
close(map_fd);
return -1;
}
printf("成功添加 %s 到黑名單\n", ip_str);
close(map_fd);
return 0;
}
// 從黑名單移除 IP
int remove_from_blacklist(const char *ip_str) {
int map_fd;
__u32 ip;
// 轉換 IP 地址
if (inet_pton(AF_INET, ip_str, &ip) != 1) {
fprintf(stderr, "無效的 IP 地址: %s\n", ip_str);
return -1;
}
// 開啟 map
map_fd = bpf_obj_get(BLACKLIST_MAP_PATH);
if (map_fd < 0) {
fprintf(stderr, "無法開啟黑名單 map: %s\n", strerror(errno));
return -1;
}
// 從黑名單移除
if (bpf_map_delete_elem(map_fd, &ip) != 0) {
fprintf(stderr, "從黑名單移除失敗: %s\n", strerror(errno));
close(map_fd);
return -1;
}
printf("成功從黑名單移除 %s\n", ip_str);
close(map_fd);
return 0;
}
// 列出黑名單
int list_blacklist() {
int map_fd;
__u32 key, next_key;
__u64 value;
char ip_str[INET_ADDRSTRLEN];
map_fd = bpf_obj_get(BLACKLIST_MAP_PATH);
if (map_fd < 0) {
fprintf(stderr, "無法開啟黑名單 map: %s\n", strerror(errno));
return -1;
}
printf("===== 黑名單 IP =====\n");
key = 0;
while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
if (bpf_map_lookup_elem(map_fd, &next_key, &value) == 0) {
struct in_addr addr;
addr.s_addr = next_key;
inet_ntop(AF_INET, &addr, ip_str, sizeof(ip_str));
printf("%s - 阻擋次數: %llu\n", ip_str, value);
}
key = next_key;
}
close(map_fd);
return 0;
}
// 主函數
int main(int argc, char **argv) {
if (argc < 2) {
printf("用法:\n");
printf(" %s add <IP> - 添加 IP 到黑名單\n", argv[0]);
printf(" %s remove <IP> - 從黑名單移除 IP\n", argv[0]);
printf(" %s list - 列出黑名單\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "add") == 0 && argc == 3) {
return add_to_blacklist(argv[2]);
} else if (strcmp(argv[1], "remove") == 0 && argc == 3) {
return remove_from_blacklist(argv[2]);
} else if (strcmp(argv[1], "list") == 0) {
return list_blacklist();
} else {
fprintf(stderr, "無效的命令\n");
return 1;
}
return 0;
}
編譯和執行腳本
DPDK 編譯執行腳本
#!/bin/bash
# run_dpdk.sh
# 編譯
echo "編譯 DPDK 程式..."
gcc -O3 -march=native \
$(pkg-config --cflags libdpdk) \
-o dpdk_packet_processor dpdk_packet_processor.c \
$(pkg-config --libs libdpdk)
if [ $? -ne 0 ]; then
echo "編譯失敗"
exit 1
fi
# 設置環境
echo "設置 DPDK 環境..."
sudo modprobe uio_pci_generic
echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
sudo mkdir -p /mnt/huge
sudo mount -t hugetlbfs hugetlbfs /mnt/huge
# 執行
echo "執行 DPDK 程式..."
sudo ./dpdk_packet_processor -l 0-1 -n 4 --log-level=8
XDP 編譯執行腳本
#!/bin/bash
# run_xdp.sh
# 檢查參數
if [ $# -lt 1 ]; then
echo "用法: $0 <介面名稱> [選項]"
exit 1
fi
INTERFACE=$1
# 編譯 XDP 程式
echo "編譯 XDP 程式..."
clang -O2 -g -Wall -target bpf \
-I/usr/include/x86_64-linux-gnu \
-c xdp_prog.c -o xdp_prog.o
if [ $? -ne 0 ]; then
echo "編譯 XDP 程式失敗"
exit 1
fi
# 編譯載入器
echo "編譯載入器..."
gcc -O2 -g -Wall \
-o xdp_loader xdp_loader.c \
-lbpf -lelf
if [ $? -ne 0 ]; then
echo "編譯載入器失敗"
exit 1
fi
# 編譯管理工具
echo "編譯管理工具..."
gcc -O2 -g -Wall \
-o xdp_admin xdp_admin.c \
-lbpf
# 載入 XDP 程式
echo "載入 XDP 程式到 $INTERFACE..."
sudo ./xdp_loader -f $INTERFACE
# 顯示提示
echo ""
echo "XDP 程式已載入!"
echo "管理命令:"
echo " 添加黑名單: sudo ./xdp_admin add <IP>"
echo " 移除黑名單: sudo ./xdp_admin remove <IP>"
echo " 列出黑名單: sudo ./xdp_admin list"
echo " 卸載程式: sudo ./xdp_loader -u $INTERFACE"
效能對比
測試環境
- CPU: Intel Xeon E5-2680 v4 @ 2.40GHz
- 記憶體: 64GB DDR4
- 網路卡: Intel 82599ES 10Gbps
- 作業系統: Ubuntu 20.04 LTS
- 核心: 5.4.0
效能測試結果
| 指標 | 傳統核心網路 | DPDK | eBPF XDP |
|---|---|---|---|
| 吞吐量 (64B) | 1.2 Mpps | 14.8 Mpps | 8.5 Mpps |
| 吞吐量 (1500B) | 0.8 Mpps | 6.2 Mpps | 4.1 Mpps |
| 延遲 (平均) | 65 μs | 4 μs | 12 μs |
| 延遲 (P99) | 120 μs | 8 μs | 25 μs |
| CPU 使用率 | 85% (kernel) | 100% (user) | 60% (kernel) |
| 記憶體使用 | 2GB | 8GB (huge pages) | 512MB |
實際應用場景
DPDK 適用場景
-
高頻交易系統
- 需要極低延遲 (<5μs)
- 封包率要求極高
- 可以專用硬體資源
-
電信級負載平衡器
- Facebook Katran
- Google Maglev
- 需要處理數百萬連線
-
DPI (深度封包檢測)
- 需要完整封包內容
- 複雜的應用層協議解析
- 狀態追蹤
eBPF XDP 適用場景
-
DDoS 防護
- 快速封包過濾
- 動態規則更新
- 不影響正常流量
-
簡單負載平衡
- L3/L4 層負載平衡
- ECMP (等價多路徑)
- 連線追蹤
-
流量監控和分析
- 即時統計
- 異常檢測
- 合規性檢查
部署注意事項
DPDK 部署檢查清單
- 硬體支援 (Intel 82599, Mellanox ConnectX)
- 大頁記憶體配置 (至少 2GB)
- CPU 核心隔離
- NUMA 節點配置
- 中斷親和性設置
- 網路卡驅動程式綁定
XDP 部署檢查清單
- 核心版本 >= 4.8
- 網路卡驅動支援 XDP
- BPF 相關工具安裝
- 驗證程式正確性
- 監控和日誌設置
- 回滾計劃
故障排除
DPDK 常見問題
-
無法初始化 EAL
# 檢查大頁記憶體 cat /proc/meminfo | grep Huge # 重新配置 echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages -
找不到網路埠
# 檢查驅動程式綁定 dpdk-devbind.py --status # 綁定到 DPDK dpdk-devbind.py --bind=uio_pci_generic 0000:01:00.0
XDP 常見問題
-
驗證失敗
# 檢查 BPF 日誌 sudo bpftool prog show sudo dmesg | grep -i bpf -
效能不如預期
# 檢查 XDP 模式 ip link show dev eth0 | grep xdp # 使用 native 模式 sudo ./xdp_loader -n eth0 xdp_prog.o
結論
Kernel Bypass 技術提供了顯著的網路效能提升:
- DPDK: 適合需要極致效能的專用系統
- eBPF XDP: 適合需要靈活性和安全性的通用系統
選擇合適的技術需要考慮:
- 效能需求
- 硬體資源
- 開發和維護成本
- 系統整合複雜度
兩種技術都在持續演進,未來可能會有更多的融合和互補。