C³: Common Coding Conventions

C³: Common Coding Conventions

https://google.github.io/eng-practices/

https://github.com/tum-esi/common-coding-conventions

The goal of these conventions is to be concise, universal, and remarkable. It targets emerging code enthusiasts under time pressure and covers 7 topics:

  1. General Clarifications,
  2. Architecture,
  3. Implementation,
  4. Naming,
  5. Code Layout,
  6. Documentation, and
  7. Languages.

To follow this guide, you should already have heard about Object Oriented Programming and know basic programming rules, such as writing loops and meaningful functions instead of copy pasting instructions. In this Readme, we will shortly summarize the most important rules for every topic.

eBPF 指令集规范

1 BPF Instruction Set Specification, v1.0 — The Linux Kernel documentation

寄存器和调用约定

eBPF有10个通用寄存器和一个只读的帧指针寄存器,所有的寄存器都是64bits位长。

eBPF的调用约定如下:

  • R0: 函数调用的返回值和eBPF程序的退出值
  • R1 - R5: 函数调用的参数
  • R6 - R9: callee saved registers,由函数调用负责保存到栈上
  • R10: 只读的帧指针,用于访问栈 R0 - R5是临时寄存器,eBPF程序在函数调用时如需使用需要将它们保存/填充。

指令编码

eBPF使用64bit指令,编码如下:

32位(最高有效位) 16位 4位 4位 8位(最低有效位)
immediate offset source register dest register opcode
  • imm: 有符号整型立即数
  • offset: 有符号整型数,用于指针算术
  • src_reg: 除开特殊指令的编码外(如64位立即数指令)指的是源寄存器(R0 - R10)
  • dst_reg: 目标寄存器(R0 - R10)
  • opcode: 操作码

绝大多数指令不会使用所有的字段。没有使用的字段应当被置0。多字节的字段(如imm和offset)在大端BPF中以大端字节序存储,小端BPF中以小端字节序存储。

opcode                  offset imm          assembly
       src_reg dst_reg
07     0       1        00 00  44 33 22 11  r1 += 0x11223344 // little
       dst_reg src_reg
07     1       0        00 00  11 22 33 44  r1 += 0x11223344 // big

除了这些基本指令编码外,eBPF还有一种宽指令编码,在基本指令之后使用第2个64位立即数做扩展。第2个立即数包括一个伪指令,和基本指令的格式一样,不过所有的字段(除了imm)都置为0。它的imm作为整个宽指令中的imm64值的高32位:

eBPF 全局变量的实现

Linux内核在v5.2支持了eBPF全局变量(见v5.2)。

全局变量会被Clang编译器放入.bss, .data, .rodata sections。那么支持全局变量即如何把这些sections中的数据加载进内核,并且在relocation时,给访问这些变量的指令正确的地址。

早期 workaround

关于 eBPF 静态变量支持的讨论在Linux Plumbers Conference 2018 由Cilium提出。早期的 workaround 如下:

#include <linux/bpf.h>

typedef unsigned int __u32;
typedef long long unsigned int __64;

#ifndef __section
# define __section(NAME)				\
	__attribute__((section(NAME), used))
#endif
#ifndef __fetch
# define __fetch(x) (__u32)(__u64)(&(x))
#endif

__u32 foo = 42; 			// .data section
// __u32 foo;   			// .bss section
// const __u32 foo = 42; 	// .rodata section

int __main(struct __sk_buff *skb)
{
    skb->mark = __fetch(foo);
    return 0;
}

char __license[] __section("license") = "";

编译后,foo变量存储在.data section内。

# llvm-readelf -S test.o
There are 8 section headers, starting at offset 0x148:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .strtab           STRTAB          0000000000000000 0000fa 00004b 00      0   0  1
  [ 2] .text             PROGBITS        0000000000000000 000040 000028 00  AX  0   0  8
  [ 3] .rel.text         REL             0000000000000000 0000e8 000010 10      7   2  8
  [ 4] .data             PROGBITS        0000000000000000 000068 000004 00  WA  0   0  4
  [ 5] license           PROGBITS        0000000000000000 00006c 000001 00  WA  0   0  1
  [ 6] .llvm_addrsig     LLVM_ADDRSIG    0000000000000000 0000f8 000002 00   E  7   0  1
  [ 7] .symtab           SYMTAB          0000000000000000 000070 000078 18      1   2  8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

.rel.text section中也存在foo变量的relocation信息:

Linux 收包和发包流程

流程图

From 《Understanding Linux Network Internals》

image-20220128172744059

image-20220129171450456

收包流程

TL; DR

image-20220129171450456

  • 收包NET_RX_SOFTRQ的软中断处理函数是net_rx_action
  • net_rx_action中会调用网卡驱动注册的poll回调函数处理
  • poll回调函数将数据帧从网卡ring buffer中取出,构造skb:
    • 运行xdpdrv上的bpf program,得到action result
    • 如果是XDP_PASS,构造skb,并初始化skb中一些metadata字段
  • 调用内核的GRO和RPS处理流程
  • 进入__netif_receive_skb_core,处理skb:
    • 运行xdpgeneric上的bpf program,得到action result
    • 如果是XDP_PASS,遍历ptype_alldev->ptype_all,进行抓包处理
    • tc ingress 处理sch_handle_ingress
    • 查找ptype_basedev->ptype_specific,交由对应的三层协议栈回调函数处理

NAPI

**NAPI的思想是从完全的中断收包模型,改用中断和polling混合。**如果内核在处理旧的数据帧时,收到了新的数据帧,网卡设备没有必要再触发一个中断。内核继续处理设备input queue里的数据(该设备的interrupt禁止了),在队列为空时重新使能中断。

从内核的角度,NAPI有如下的优势:

  • 降低CPU的负载(更少的中断)
  • 更多的设备处理公平性

以ixgbe网卡为例,描述下NAPI处理流程。

注册

ixgbe驱动在初始化中断向量时会调用netif_napi_add初始化NAPI,==ixgbe_poll函数注册到napi结构体,并将napi加入到设备的napi_list内==:

/**
 * ixgbe_alloc_q_vector - Allocate memory for a single interrupt vector
 * @adapter: board private structure to initialize
 * @v_count: q_vectors allocated on adapter, used for ring interleaving
 * @v_idx: index of vector in adapter struct
 * @txr_count: total number of Tx rings to allocate
 * @txr_idx: index of first Tx ring to allocate
 * @xdp_count: total number of XDP rings to allocate
 * @xdp_idx: index of first XDP ring to allocate
 * @rxr_count: total number of Rx rings to allocate
 * @rxr_idx: index of first Rx ring to allocate
 *
 * We allocate one q_vector.  If allocation fails we return -ENOMEM.
 **/
static int ixgbe_alloc_q_vector(struct ixgbe_adapter *adapter,
                int v_count, int v_idx,
                int txr_count, int txr_idx,
                int xdp_count, int xdp_idx,
                int rxr_count, int rxr_idx)
{
	/* ... */

    /* initialize NAPI */
    netif_napi_add(adapter->netdev, &q_vector->napi,
               ixgbe_poll, 64);
}

中断处理函数

ixgbe驱动收到中断后,会调用ixgbe_msix_clean_rings

make命令和编译

Makefile

标准格式

target:dependencies1 dependencies2 ...
   recipe

.PHONY: clean
clean:
   -rm ...

注意事项

  • 运行make时如果没有指定target,那么make会构建Makefile中的第一个target
  • 通常情况下,目标名即生成的文件名。如果一条规则的目标文件存在并且该文件比它所有的依赖都要新,那么make会跳过recipe;如果目标文件不存在,那么目标文件的 timestamp 为开始的时间;否则timestamp为相应文件的修改时间。
  • 每次运行 make clean,”clean“中的recipe都会被执行,因为clean文件永远都不会被创建。(可以使用 .PHONY 创建伪目标使Makefile可读性更高)
  • recipes必须用tab缩进
  • 可以通过并行的方式运行recipes:make -j 4(指定并行的任务数)
  • 如果没有指定规则,Make会自动化创建规则。例如,本地有一个C文件”program.c“,当运行 make program时,Make会自动编译生成 program

变量

  • $@ 目标文件
  • $^ 所有的依赖文件
  • $< 第一个依赖

头文件

环境变量

  • C_INCLUDE_PATH C语言头文件路径
  • CPLUS_INCLUDE_PATH C++ 头文件路径

搜索路径

#include<>

  1. 先搜索 -I 指定的目录
  2. 然后搜索gcc的环境变量 CPLUS_INCLUDE_PATH
  3. 最后搜索gcc的内定目录
    1. /usr/include
    2. /usr/local/include
    3. /usr/lib/gcc/x86_64-redhat-linux/4.1.1/include

#include ""

  • 搜索当前目录,#include<>方式不会搜索当前目录

动态库

环境变量

  • LD_LIBRARY_PATH 动态链接库搜索路径
  • PKG_CONFIG_PATH .pc文件(package config)文件搜索路径

搜索路径

  1. 首先在环境变量 LD_LIBRARY_PATH 所记录的路径中查找
  2. 在程序链接时指定的 rpath 中查找,可以 readelf binfile | grep RPATH
  3. 然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由/sbin/ldconfig命令读取配置文件/etc/ld.so.conf 之后生成(也可以在 ld.so.conf.d 目录下增加 .conf 文件,里面写入库路径,在 ld.so.conf 中 include ld.so.conf.d/.conf )
  4. 如果上述步骤都找不到,则到默认的系统路径中查找,先是/usr/lib然后是/lib

编译参数

-shared

指定生成动态连接库