探索eBPF:Linux内核的黑科技(下)

简介: 探索eBPF:Linux内核的黑科技

三、eBPF在Android平台的使用

经过上面枯燥的讲解,大家应该对eBPF有了基础的认识,下面我们就来通过android平台上的一个监控性能的小例子来实操下。这个小例子的需求是统计系统中每个应用在一段时间内系统调用的次数。

3.1android系统对eBPF的编译支持

目前android编译系统已经对eBPF进行了集成,通过android.bp就能很方便的在android源代码中编译eBPF的字节码。

android.bp示例:

640.png

相关的编译代码在soong的bpf.go,虽然google关于soong的文640.png档很少,但是至少代码是比较清晰的。


这里的$ccCmd一般是clang, 所以它的编译命令主要是clang --target=bpf。和普通的bpf编译没有区别。

3.2eBPF钩子代码实现

解决了编译问题,下一步我们开始实现钩子代码,我们准备使用tracepoint钩子,首先要找到我们需要的tracepoint函数sys_enter和sys_exit。函数定义在include/trace/events/syscalls.h文件sys_enter的trace参数是id 和长度为6的数组。

  • sys_exit的trace参数是两个长整形数 id 和ret。

找到了钩子后,下一步就可以编写钩子处理代码了:

640.png

定义map保存系统调用统计信息,在DEFINE_BPF_MAP声明map的同时,也会生成删,改,查的宏函数,例如本例中会生成如下函数:

bpf_pid_syscall_map_lookup_elem

bpf_pid_syscall_map_update_elem

bpf_pid_syscall_map_delete_elem

  • 定义回调函数参数类型,需要参考前面的tracepoint的定义。
  • 指定监听的tracepoint事件。
  • 使用bpf_trace_printk函数打印debug信息,会直接打印信息到ftrace中。
  • 在map中查找指定key。
  • 更新指定的key的值。

3.3加载钩子代码

我们只需要把我们编译出来的*.o文件push到手机的system/etc/bpf目录下,重启手机,系统会自动加载我们的钩子文件,加载成功后会在 /sys/fs/bpf目录下显示我们定义的map及prog文件。

系统加载代码在system/bpf/bpfloader中,代码很简单。

主要有如下操作:

1)在early-init阶段向下面两个节点写1

–  /proc/sys/net/core/bpf_jit_enable

使能eBPF JIT,当内核设定BPF_JIT_ALWAYS_ON的时候,默认为1

– /proc/sys/net/core/bpf_jit_kallsyms

使特权用户可以通过kallsyms节点读取kernel的symbols

2)启动bpfloader service

– 读取system/etc/bpf目录下的*.o文件,调用libbpf_android.so中的loadProg函数加载进内核。

– 生成相应的/sys/fs/bpf/节点。

– 设置属性bpf.progs_loaded为1

sys节点分为map节点和prog节点两种, 分别为map_<filename>_<mapname>, prog_<filename>_<mapname>

下面是Android Q版本上的节点信息。

640.png

可以使用下面的命令调试动态加载

640.png

3.4用户空间程序实现

下面我们需要编写用户空间的显示程序,本质上就是在用户态通过系统调用把BPF map给读出来

640.png

640.png

  • 1)eBPF统计只有在调用bpf_attach_tracepoint只有才会起作用。bpf_attach_tracepoint是bcc里面的函数,android将bcc的一部分内容打包成了libbpf,放到了系统库里面。
  • 2)取得map的fd, bpf_obj_get会直接调用bpf的系统调用。
  • 3)将fd包装成BpfMap,android在BpfMap.h中定义了很多方便的函数。
  • 4)遍历map回调函数。返回值必须是android::netdutils::status::ok(在android的新版本中已经进行修改)。

3.5 运行结果查看

直接在目录下执行mm,将编译出来的bpf.o push到/system/etc/bpf目录下,将统计程序push到/system/bin目录下,重启,看下结果。

640.png

前面的是pid, 后面的是系统调用次数。至此,如何在android平台使用eBPF实现统计系统中每个pid在一段时间内系统调用的次数的功能就介绍完了。

此外还有很多技术细节没有深入研究,不过毕竟只是初探,就先讲到这里了,后续有时间再进一步深入研究。研究的时间还是比较短,如果有任何错误的地方欢迎指正。

四、seccomp 概述

下面内容来自Linux官方文档

4.1历史

seccomp首个版本在2005年合入Linux 2.6.12版本。通过在 /proc/PID/seccomp中写入1启用该功能。一旦启用,进程只能使用4个系统调用read(), write(), exit()和sigreturn(),如果进程调用其他系统调用将会导致SIGKILL。该想法和补丁来自andreaarcangeli,作为一种安全运行他人代码的方法。然而,这个想法一直没有实现。

在2007年,内核2.6.23中改变了启用seccomp的方式。添加了 prctl()操作方式(PR_SET_SECCOMP和 SECCOMP_MODE_STRICT参数),并移除了 /proc 接口。PR_GET_SECCOMP操作的行为比较有趣:如果进程不处于seccomp模式,则会返回0,否则会发出SIGKILL信号(原因是prctl()不是一个允许的系统调用)。Kerrisk说,这证明了内核开发人员确实有幽默感。

在接下来的五年左右,seccomp领域的情况一直很平静,直到2012年linux3.5中加入了seccomp模式2(或“seccomp过滤模式”)。为seccomp添加了第二个模式:SECCOMP_MODE_FILTER。使用该模式,进程可以指定允许哪些系统调用。通过mini的BPF程序,进程可以限制整个系统调用或特定的参数值。现在已经有很多工具使用了seccomp过滤,包括 Chrome/Chromium浏览器, OpenSSH, vsftpd, 和Firefox OS。此外,容器中也大量使用了seccomp。

2013年的3.8内核版主中,在/proc/PID/status中添加了一个“Seccomp”字段。通过读取该字段,进程可以确定其seccomp模式(0为禁用,1为严格,2为过滤)。Kerrisk指出,进程可能需要从其他地方获取一个文件的文件描述符,以确保不会收到SIGKILL。

2014 年3.17版本中加入了 seccomp()系统调用(不会再使得prctl()系统调用变得更加复杂)。seccomp()系统调用提供了现有功能的超集。它还增加了将一个进程的所有线程同步到同一组过滤器的能力,有助于确保即使是在安装过滤器之前创建的线程也仍然受其影响。

4.2BPF

seccomp的过滤模式允许开发者编写BPF程序来根据传入的参数数目和参数值来决定是否可以运行某个给定的系统调用。只有值传递有效(BPF虚拟机不会取消对指针参数的引用)。

可以使用seccomp() 或prctl()安装过滤器。首先必须构造BPF程序,然后将其安装到内核。之后每次执行系统调用时都会触发过滤代码。也可以移除已经安装的过滤器(因为安装过滤器实际上是一种声明,表明任何后续执行的代码都是不可信的)。

BPF语言几乎早于Linux(Kerrisk)。首次出现在1992年,被用于tcpdump程序,用于监听网络报文。但由于报文数目比较大,因此将所有的报文传递到用于空间再进行过滤的代价相当大。BPF提供了一种内核层面的过滤,这样用户空间只需要处理其感兴趣的报文。

seccomp过滤器开发人员发现可以使用BPF实现其他类型的功能,后来BPF演化为允许过滤系统调用。内核中的小型内核内虚拟机用于解释一组简单的BPF指令。

BPF允许分支,但仅允许向前的分支,因此不能出现循环,通过这种方式保证出现能够结束。BPF程序的指令限制为4096个,且在加载期间完成有效性校验。此外,校验器可以保证程序能够正常退出,并返回一条指令,告诉内核针对该系统调用应该采取何种动作。

BPF的推广正在进行中,其中eBPF已经添加到了内核中,可以针对tracepoint(Linux 3.18)和raw socket(3.19)进行过滤,同时在4.1版本中合入了针对perf event的eBPF代码。

BPF有一个累加器寄存器,一个数据区(用于seccomp,包含系统调用的信息),以及一个隐式程序计数器。所有的指令都是64位长度,其中16比特用于操作码,两个8bit字段用于跳转目的地,以及一个32位的字段保存依赖操作码解析出的值。

BPF使用的基本的指令有:load,stora,jump,算术和逻辑运算,以及return。BPF支持条件和非条件跳转指令,后者使用32位字段作为其偏移量。条件跳转会在指令中使用两个跳转目的字段,每个字段都包含一个跳转偏移量(具体取决于跳转为true还是false)。

由于具有两个跳转目的,BPF可以简化条件跳转指令(例如,可以使用"等于时跳转",但不能使用"不等于时跳转"),如果需要另一种意义上的比较,可以将这两种偏移互换。目的地即是偏移量,0表示"不跳转"(执行下一跳指令),由于它们是8比特的值,最大支持跳转255条指令。正如前面所述,不允许负偏移量,避免循环。

给seccomp使用的BPF数据区(struct seccomp_data)有几个不同的字段来描述正在进行的系统调用:系统调用号,架构,指令指针,以及系统调用参数。它是一个只读buffer,程序无法修改。

4.3编写过滤器

可以使用常数和宏编写BPF程序,例如:

BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, arch)))

上述命令将会创建一个加载(BPF_LD)字(BPF_W)的操作,使用指令中的值作为数据区的偏移量(BPF_ABS)。该值是architecture字段与数据区域的偏移量,因此最终结果是一条指令,该指令会根据架构加载累加器(来自AUDIT.h中的AUDIT_ARCH_*值)。下一条指令为:

BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K ,AUDIT_ARCH_X86_64 , 1, 0)

上述命令会创建一个jump-if-equal指令(BPF_JMP | BPF JEQ),将指令中的值(BPF_K)与累加器中的值进行比较。如果架构为x86-64,该跳转会忽略吓一跳指令(跳转的指令数为"1"),否则会继续执行(跳转为false,"0")。

BPF程序应该首先对其架构进行校验,确保系统调用与程序所期望的一致。BPF程序可能是在与它允许的架构不同的架构上创建的。

一旦创建了过滤器,在每次系统调用时都会允许该程序,同时也会对性能造成一定影响。每个程序在退出时必须返回一条指令,否则,校验器会返回EINVAL。返回的内容为一个32位的数值。高16比特指定了内核的动作,其他比特返回与动作相关的数据。

程序可以返回5个动作:SECCOMP_RET_ALLOW表示允许运行系统调用;SECCOMP_RET_KILL表示终止进程,就像该进程由于SIGSYS(进程不会捕获到该信号)被杀死一样;SECCOMP_RET_ERRNO会告诉内核尝试通知一个ptrace()跟踪器,使其有机会获得控制权;SECCOMP_RET_TRAP告诉内核立即发送一个真实的SIGSYS信号,进程会在期望时捕获到该信号。

可以使用seccomp() (since Linux 3.17) 或prctl()安装BPF程序,这两种情况下都会传递一个 struct sock_fprog指针,包含指令数目和一个指向程序的指针。为了成功执行指令,调用者要么需要具有CAP_SYS_ADMIN权限,要么给进程设置PR_SET_NO_NEW_PRIVS属性(使用execve()执行新的程序时会忽略set-UID, set-GID, 和文件capabilities)。

如果过滤器运行程序调用 prctl() 或seccomp(),那么就可以安装更多的过滤器,它们将以与添加顺序相反的顺序运行,最终返回过滤器中具有最高优先级的值(KILL的优先级最高,ALLOW的优先级最低)。如果筛选器允许调用fork()、clone()和execve(),则会在调用这些命令时保留筛选器。

seccomp过滤器的两个主要用途是沙盒和故障模式测试。前者用于限制程序,特别是需要处理不可信输入的系统调用,通常会用到白名单。对于故障模式测试,可以使用seccomp给程序注入各种不可预期的错误来帮助查找bugs。

目前有很多工具和资源可以简化seccomp过滤器和BPF的开发。Libseccomp提供了一组高级API来创建过滤器。libseccomp项目给出了很多帮助文档,如seccomp_init()

最后,内核有一个just-in-time (JIT)编译器,用于将BPF字节码转化为机器码,通过这种方式可以提升2-3倍的性能。JIT编译器默认是禁用的,可以通过在下面文件中写入1启用。

/proc/sys/net/core/bpf_jit_enable

4.4XDP

概述

XDP是Linux网络路径上内核集成的数据包处理器,具有安全、可编程、高性能的特点。当网卡驱动程序收到数据包时,该处理器执行BPF程序。XDP可以在数据包进入协议栈之前就进行处理,因此具有很高的性能,可用于DDoS防御、防火墙、负载均衡等领域。

XDP数据结构

XDP程序使用的数据结构是xdp_buff,而不是sk_buff,xdp_buff可以视为sk_buff的轻量级版本。两者的区别在于:sk_buff包含数据包的元数据,xdp_buff创建更早,不依赖与其他内核层,因此XDP可以更快的获取和处理数据包。

xdp_buff数据结构定义如下:

// /linux/include/net/xdp.h
struct xdp_rxq_info {
struct net_device *dev;
u32 queue_index;
u32 reg_state;
struct xdp_mem_info mem;
} ____cacheline_aligned; /* perf critical, avoid false-sharing */

struct xdp_buff {
void *data;
void *data_end;
void *data_meta;
void *data_hard_start;
unsigned long handle;
struct xdp_rxq_info *rxq;
};

sk_buff数据结构定义如下:

// /include/linux/skbuff.h
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;

union {
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
};
struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */
struct list_head list;
};

union {
struct sock *sk;
int ip_defrag_offset;
};

union {
ktime_t tstamp;
u64 skb_mstamp_ns; /* earliest departure time */
};
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48] __aligned(8);

union {
struct {
unsigned long _skb_refdst;
void (*destructor)(struct sk_buff *skb);
};
struct list_head tcp_tsorted_anchor;
};

#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
unsigned long _nfct;
#endif
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;

/* Following fields are _not_ copied in __copy_skb_header()
* Note that queue_mapping is here mostly to fill a hole.
*/
__u16 queue_mapping;

/* if you move cloned around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define CLONED_MASK (1 << 7)
#else
#define CLONED_MASK 1
#endif
#define CLONED_OFFSET() offsetof(struct sk_buff, __cloned_offset)

__u8 __cloned_offset[0];
__u8 cloned:1,
nohdr:1,
fclone:2,
peeked:1,
head_frag:1,
xmit_more:1,
pfmemalloc:1;
#ifdef CONFIG_SKB_EXTENSIONS
__u8 active_extensions;
#endif
/* fields enclosed in headers_start/headers_end are copied
* using a single memcpy() in __copy_skb_header()
*/
/* private: */
__u32 headers_start[0];
/* public: */

/* if you move pkt_type around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_TYPE_MAX (7 << 5)
#else
#define PKT_TYPE_MAX 7
#endif
#define PKT_TYPE_OFFSET() offsetof(struct sk_buff, __pkt_type_offset)

__u8 __pkt_type_offset[0];
__u8 pkt_type:3;
__u8 ignore_df:1;
__u8 nf_trace:1;
__u8 ip_summed:2;
__u8 ooo_okay:1;

__u8 l4_hash:1;
__u8 sw_hash:1;
__u8 wifi_acked_valid:1;
__u8 wifi_acked:1;
__u8 no_fcs:1;
/* Indicates the inner headers are valid in the skbuff. */
__u8 encapsulation:1;
__u8 encap_hdr_csum:1;
__u8 csum_valid:1;

#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_VLAN_PRESENT_BIT 7
#else
#define PKT_VLAN_PRESENT_BIT 0
#endif
#define PKT_VLAN_PRESENT_OFFSET() offsetof(struct sk_buff, __pkt_vlan_present_offset)
__u8 __pkt_vlan_present_offset[0];
__u8 vlan_present:1;
__u8 csum_complete_sw:1;
__u8 csum_level:2;
__u8 csum_not_inet:1;
__u8 dst_pending_confirm:1;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
__u8 ndisc_nodetype:2;
#endif

__u8 ipvs_property:1;
__u8 inner_protocol_type:1;
__u8 remcsum_offload:1;
#ifdef CONFIG_NET_SWITCHDEV
__u8 offload_fwd_mark:1;
__u8 offload_l3_fwd_mark:1;
#endif
#ifdef CONFIG_NET_CLS_ACT
__u8 tc_skip_classify:1;
__u8 tc_at_ingress:1;
__u8 tc_redirected:1;
__u8 tc_from_ingress:1;
#endif
#ifdef CONFIG_TLS_DEVICE
__u8 decrypted:1;
#endif

#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#endif

union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
__u32 priority;
int skb_iif;
__u32 hash;
__be16 vlan_proto;
__u16 vlan_tci;
#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)
union {
unsigned int napi_id;
unsigned int sender_cpu;
};
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif

union {
__u32 mark;
__u32 reserved_tailroom;
};

union {
__be16 inner_protocol;
__u8 inner_ipproto;
};

__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;

__be16 protocol;
__u16 transport_header;
__u16 network_header;
__u16 mac_header;

/* private: */
__u32 headers_end[0];
/* public: */

/* These elements must be at the end, see alloc_skb() for details.  */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
unsigned int truesize;
refcount_t users;

#ifdef CONFIG_SKB_EXTENSIONS
/* only useable after checking ->active_extensions != 0 */
struct skb_ext *extensions;
#endif
};

4.5XDP与eBPF的关系

XDP程序是通过bpf()系统调用控制的,bpf()系统调用使用程序类型BPF_PROG_TYPE_XDP进行加载。

XDP操作模式

XDP支持3种工作模式,默认使用native模式:

  • Native XDP:在native模式下,XDP BPF程序运行在网络驱动的早期接收路径上(RX队列),因此,使用该模式时需要网卡驱动程序支持。
  • Offloaded XDP:在Offloaded模式下,XDP BFP程序直接在NIC(Network Interface Controller)中处理数据包,而不使用主机CPU,相比native模式,性能更高
  • Generic XDP:Generic模式主要提供给开发人员测试使用,对于网卡或驱动无法支持native或offloaded模式的情况,内核提供了通用的generic模式,运行在协议栈中,不需要对驱动做任何修改。生产环境中建议使用native或offloaded模式

XDP操作结果码

  • XDP_DROP:丢弃数据包,发生在驱动程序的最早RX阶段
  • XDP_PASS:将数据包传递到协议栈处理,操作可能为以下两种形式:1、正常接收数据包,分配愿数据sk_buff结构并且将接收数据包入栈,然后将数据包引导到另一个CPU进行处理。他允许原始接口到用户空间进行处理。这可能发生在数据包修改前或修改后。2、通过GRO(Generic receive offload)方式接收大的数据包,并且合并相同连接的数据包。经过处理后,GRO最终将数据包传入“正常接收”流
  • XDP_TX:转发数据包,将接收到的数据包发送回数据包到达的同一网卡。这可能在数据包修改前或修改后发生
  • XDP_REDIRECT:数据包重定向,XDP_TX,XDP_REDIRECT是将数据包送到另一块网卡或传入到BPF的cpumap中
  • XDP_ABORTED:表示eBPF程序发生错误,并导致数据包被丢弃。自己开发的程序不应该使用该返回码

XDP和iproute2加载器

iproute2工具中提供的ip命令可以充当XDP加载器的角色,将XDP程序编译成ELF文件并加载他。

  • 编写XDP程序xdp_filter.c,程序功能为丢弃所有TCP连接包,程序将xdp_md结构指针作为输入,相当于驱动程序xdp_buff的BPF结构。程序的入口函数为filter,编译后ELF文件的区域名为mysection。

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>

#define SEC(NAME) __attribute__((section(NAME), used))

SEC("mysection")
int filter(struct xdp_md *ctx) {
   int ipsize = 0;
   void *data = (void *)(long)ctx->data;
   void *data_end = (void *)(long)ctx->data_end;
   struct ethhdr *eth = data;
   struct iphdr *ip;

   ipsize = sizeof(*eth);
   ip = data + ipsize;

   ipsize += sizeof(struct iphdr);
   if (data + ipsize > data_end) {
       return XDP_DROP;
   }

   if (ip->protocol == IPPROTO_TCP) {
       return XDP_DROP;
   }

   return XDP_PASS;
}

  • 将XDP程序编译为ELF文件

clang -O2 -target bpf -c xdp_filter.c -o xdp_filter.o

  • 使用ip命令加载XDP程序,将mysection部分作为程序的入口点

sudo ip link set dev ens33 xdp obj xdp_filter.o sec mysection

没有报错即完成加载,可以通过以下命令查看结果:

$ sudo ip a show ens33
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric/id:56 qdisc fq_codel state UP group default qlen 1000
   link/ether 00:0c:29:2f:a8:41 brd ff:ff:ff:ff:ff:ff
   inet 192.168.136.140/24 brd 192.168.136.255 scope global dynamic noprefixroute ens33
      valid_lft 1629sec preferred_lft 1629sec
   inet6 fe80::d411:ff0d:f428:ce2a/64 scope link noprefixroute
      valid_lft forever preferred_lft forever

其中,xdpgeneric/id:56说明使用的驱动程序为xdpgeneric,XDP程序id为56

  • 验证连接阻断效果
  1. 使用nc -l 8888监听8888 TCP端口,使用nc xxxxx 8888连接发送数据,目标主机未收到任何数据,说明TCP连接阻断成功
  2. 使用nc -kul 9999监听UDP 9999端口,使用nc -u xxxxx 9999连接发送数据,目标主机正常收到数据,说明UDP连接不受影响
  • 卸载XDP程序

$ sudo ip link set dev ens33 xdp off

卸载后,连接8888端口,发送数据,通信正常。

XDP和BCC

编写C代码xdp_bcc.c,当TCP连接目的端口为9999时DROP:

#define KBUILD_MODNAME "program"
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>

int filter(struct xdp_md *ctx) {
   int ipsize = 0;
   void *data = (void *)(long)ctx->data;
   void *data_end = (void *)(long)ctx->data_end;
   struct ethhdr *eth = data;
   struct iphdr *ip;

   ipsize = sizeof(*eth);
   ip = data + ipsize;

   ipsize += sizeof(struct iphdr);
   if (data + ipsize > data_end) {
       return XDP_DROP;
   }

   if (ip->protocol == IPPROTO_TCP) {
       struct tcphdr *tcp = (void *)ip + sizeof(*ip);
       ipsize += sizeof(struct tcphdr);
       if (data + ipsize > data_end) {
           return XDP_DROP;
       }

       if (tcp->dest == ntohs(9999)) {
           bpf_trace_printk("drop tcp dest port 9999\n");
           return XDP_DROP;
       }
   }

   return XDP_PASS;
}

与使用ip命令加载XDP程序类似,这里编写python加载程序实现对XDP程序的编译和内核注入。

#!/usr/bin/python

from bcc import BPF
import time

device = "ens33"
b = BPF(src_file="xdp_bcc.c")
fn = b.load_func("filter", BPF.XDP)
b.attach_xdp(device, fn, 0)

try:
 b.trace_print()
except KeyboardInterrupt:
 pass

b.remove_xdp(device, 0)

验证效果,使用nc测试,无法与目标主机9999端口实现通信

$ sudo python xdp_bcc.py

<idle>-0       [003] ..s. 22870.984559: 0: drop tcp dest port 9999
<idle>-0       [003] ..s. 22871.987644: 0: drop tcp dest port 9999
<idle>-0       [003] ..s. 22872.988840: 0: drop tcp dest port 9999
<idle>-0       [003] ..s. 22873.997261: 0: drop tcp dest port 9999
<idle>-0       [003] ..s. 22875.000567: 0: drop tcp dest port 9999
<idle>-0       [003] ..s. 22876.002998: 0: drop tcp dest port 9999
<idle>-0       [003] ..s. 22878.005414: 0: drop tcp dest port 9999
<idle>-0       [003] ..s. 22882.018119: 0: drop tcp dest port 9999

4.5Rings

有4类不同类型的ring:FILL, COMPLETION, RX 和TX,所有的ring都是单生产者/单消费者,因此用户空间的程序需要显示地同步对这些rings进行读/写的多进程/线程。

UMEM使用2个ring:FILL和COMPLETION。每个关联到UMEM的socket必须有1个RX队列,1个TX队列或同时拥有2个队列。如果配置了4个socket(同时使用TX和RX),那么此时会有1个FILL ring,1个COMPLETION ring,4个TX ring和4个RX ring。

ring是基于首(生产者)尾(消费者)的结构。一个生产者会在结构体xdp_ring的producer成员指出的ring索引处写入数据,并增加生产者索引;一个消费者会结构体xdp_ring的consumer成员指出的ring索引处读取数据,并增加消费者索引。

可以通过_RING  setsockopt系统调用配置和创建ring,使用mmap(),并结合合适的偏移量,将其映射到用户空间

ring的大小需要是2次幂。

4.6UMEM Fill Ring

FILL ring用于将UMEM帧从用户空间传递到内核空间,同时将UMEM地址传递给ring。例如,如果UMEM的大小为64k,且每个chunk的大小为4k,那么UMEM包含16个chunk,可以传递的地址为0到64k。

传递给内核的帧用于ingress路径(RX  rings)。

用户应用也会在该ring中生成UMEM地址。注意,如果以对齐的chunk模式运行应用,则内核会屏蔽传入的地址。即,如果一个chunk大小为2k,则会屏蔽掉log2(2048) LSB的地址,意味着2048, 2050 和3000都将引用相同的chunk。如果用户应用使用非对其的chunk模式运行,那么传入的地址将保持不变。

4.7UMEM Completion Ring

COMPLETION Ring用于将UMEM帧从内核空间传递到用户空间,与FILL ring相同,使用了UMEM索引。

已经发送的从内核空间传递到用户空间的帧还可以被用户空间使用。

用户应用会消费该ring种的UMEM地址。

4.8RX Ring

RX ring位于socket的接收侧,ring中的每个表项都是一个xdp_desc 结构的描述符。该描述符包含UMEM偏移量(地址)以及数据的长度。

如果没有帧从FILL ring传递给内核,则RX ring中不会出现任何描述符。

用户程序会消费该ring中的xdp_desc描述符。

4.9TX Ring

TX Ring用于发送帧。在填充xdp_desc(索引,长度和偏移量)描述符后传递给该ring。

如果要启动数据传输,则必须调用sendmsg(),未来可能会放宽这种限制。

用户程序会给TX ring生成xdp_desc 描述符。

4.10XSKMAP / BPF_MAP_TYPE_XSKMAP

在XDP侧会用到类型为BPF_MAP_TYPE_XSKMAP 的BPF map,并结合bpf_redirect_map()将ingress帧传递给socket。

用户应用会通过bpf()系统调用将socket插入该map。

注意,如果一个XDP程序尝试将帧重定向到一个与队列配置和netdev不匹配的socket时,会丢弃该帧。即,如果一个AF_XDP socket绑定到一个名为eth0,队列为17的netdev上时,只有当XDP程序指定到eth0且队列为17时,才会将数据传递给该socket。参见samples/bpf/获取例子

4.11配置标志位和socket选项

XDP_COPY 和XDP_ZERO_COPY bind标志

当绑定到一个socket时,内核会首先尝试使用零拷贝进行拷贝。如果不支持零拷贝,则会回退为使用拷贝模式。即,将所有的报文拷贝到用户空间。但如果想强制指定一种特定的模式,则可以使用如下标志:如果给bind调用传递了XDP_COPY,则内核将强制进入拷贝模式;如果没有使用拷贝模式,则bind调用会失败,并返回错误。相反地,XDP_ZERO_COPY 将强制socket使用零拷贝或调用失败。

XDP_SHARED_UMEM bind 标志

该表示可以使多个socket绑定到系统的UMEM,但仅能使用系统的队列id。这种模式下,每个socket都有其各自的RX和TX ring,但UMEM只能有一个FILL ring和一个COMPLETION ring。为了使用这种模式,需要创建第一个socket,并使用正常模式进行绑定。然后创建第二个socket,含一个RX和一个TX(或二者之一),但不会创建FILL 或COMPLETION ring(与第一个socket共享)。在bind调用中,设置XDP_SHARED_UMEM选项,并在sxdp_shared_umem_fd中提供初始socket的fd。以此类推。

那么当接收到一个报文后,应该上送到那个socket呢?答案是由XDP程序来决定。将所有的socket放到XDP_MAP中,然后将报文发送给数组中索引对应的socket。下面展示了一个简单的以轮询方式分发报文的例子:

#include <linux/bpf.h>
#include "bpf_helpers.h"

#define MAX_SOCKS 16

struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(max_entries, MAX_SOCKS);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
} xsks_map SEC(".maps");

static unsigned int rr;

SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)
{
    rr = (rr + 1) & (MAX_SOCKS - 1);

    return bpf_redirect_map(&xsks_map, rr, XDP_DROP);
}

注意,由于只有一个FILL和一个COMPLETION ring,且是单生产者单消费者的ring,需要确保多处理器或多线程不会同时使用这些ring。libbpf没有提供原子同步功能。

当多个socket绑定到相同的umem时,libbpf会使用这种模式。然而,需要注意的是,需要在xsk_socket__create调用中提供XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD libbpf_flag,然后将其加载到自己的XDP程序中(因为libbpf没有内置路由流量功能)。

XDP_USE_NEED_WAKEUP bind标志

该选择支持在FILL ring和TX ring中设置一个名为need_wakeup的标志,用户空间作为这些ring的生产者。当在bind调用中设置了该选项,如果需要明确地通过系统调用唤醒内核来继续处理报文时,会设置need_wakeup 标志。

如果将该标志设置给FILL ring,则应用需要调用poll(),以便在RX ring上继续接收报文。如,当内核检测到FILL ring中没有足够的buff,且NIC的RX HW RING中也没有足够的buffer时会发生这种情况。此时会关中断,这样NIC就无法接收到任何报文(由于没有足够的buffer),由于设置了need_wakeup,这样用户空间就可以在FILL ring上增加buffer,然后调用poll(),这样内核驱动就可以将这些buffer添加到HW ring上继续接收报文。

如果将该标志设置给TX ring,意味着应用需要明确地通知内核发送位于TX ring上的报文。可以通过调用poll(),或调用sendto()完成。

可以在samples/bpf/xdpsock_user.c中找到例子。在TX路径上使用libbpf辅助函数的例子如下:

if (xsk_ring_prod__needs_wakeup(&my_tx_ring))
  sendto(xsk_socket__fd(xsk_handle), NULL, 0, MSG_DONTWAIT, NULL, 0);

建议启用该模式,由于减少了TX路径上的系统调用的数目,因此可以在应用和驱动运行在同一个(或不同)core的情况下提升性能。

XDP_{RX|TX|UMEM_FILL|UMEM_COMPLETION}_RING setsockopts

这些socket选项分别设置RX, TX, FILL和COMPLETION ring的描述符数量(必须至少设置RX或TX ring的描述符大小)。如果同时设置了RX和TX,就可以同时接收和发送来自应用的流量;如果仅设置了其中一个,就可以节省相应的资源。如果需要将一个UMEM绑定到socket,需要同时设置FILL ring和COMPLETION ring。如果使用了XDP_SHARED_UMEM标志,无需为除第一个socket之外的socket创建单独的UMEM,所有的socket将使用共享的UMEM。注意ring为单生产者单消费者结构,因此多进程无法同时访问同一个ring。参见XDP_SHARED_UMEM章节。

使用libbpf时,可以通过给xsk_socket__create函数的rx和tx参数设置NULL来创建Rx-only和Tx-only的socket。

如果创建了一个Tx-only的socket,建议不要在FILL ring中放入任何报文,否则,驱动可能会认为需要接收数据(但实际上并不是这样的),进而影响性能。

XDP_UMEM_REG setsockopt

该socket选项会给一个socket注册一个UMEM,其对应的区域包含了可以容纳报文的buffer。该调用会使用一个指向该区域开始处的指针,以及该区域的大小。此外,还有一个UMEM可以切分的chunk大小参数(目前仅支持2K或4K)。如果一个UMEM区域的大小为128K,且chunk大小为2K,意味着该UMEM域最大可以有128K / 2K = 64个报文,且最大的报文大小为2K。

还有一个选项可以在UMEM中设置每个buffer的headroom。如果设置为N字节,意味着报文会从buffer的第N个字节开始,为应用保留前N个字节。最后一个选项为标志位字段,会在每个UMEM标志中单独处理。

XDP_STATISTICS getsockopt

获取一个socket丢弃信息,用于调试。支持的信息为:

struct xdp_statistics {
      __u64 rx_dropped; /* Dropped for reasons other than invalid desc */
      __u64 rx_invalid_descs; /* Dropped due to invalid descriptor */
      __u64 tx_invalid_descs; /* Dropped due to invalid descriptor */
};

XDP_OPTIONS getsockopt

获取一个XDP socket的选项。目前仅支持XDP_OPTIONS_ZEROCOPY,用于检查是否使用了零拷贝。

从AF_XDP的特性上可以看到其局限性:不能使用XDP将不同的流量重定向的多个AF_XDP socket上,原因是每个AF_XDP socket必须绑定到物理接口的TX队列上。大多数的物理和仿真HW的每个接口仅支持一个RX/TX队列,因此当该接口上绑定了一个AF_XDP后,后续的绑定操作都将失败。仅有少数HW支持多RX/TX队列,且通常仅有2/4/8个队列,无法扩展给cloud中的上百个容器使用。

更多细节参见AF_XDP官方文档以及这篇论文

五、TC

除了XDP,BPF还可以在网络数据路径的内核tc(traffic control)层之外使用。上文已经给出了XDP和TC的区别。

  • ingress hook:__netif_receive_skb_core() -> sch_handle_ingress()
  • egress hook:__dev_queue_xmit() -> sch_handle_egress()

640.png

运行在tc层的BPF程序使用的是 cls_bpf (cls即Classifiers的简称)分类器。在tc中,将BPF的附着点描述为一个"分类器",这个词有点误导,因此它少描述了cls_bpf的所支持的功能。即一个完整的可编程的报文处理器不仅可以读取skb的元数据和报文数据,还可以对其进行任意修改,最后终止tc的处理,并返回裁定的action(见下)。cls_bpf可以认为是一个自包含的,可以管理和执行tc BPF程序的实体。

cls_bpf可以包含一个或多个tc BPF程序。通常,在传统的tc方案中,分类器和action模块是分开的,每个分类器可以附加一个或多个action,一旦匹配到分类器时就会执行action。但在现代软件数据路径中使用这种模式的tc处理复杂的报文时会遇到扩展性问题。由于附加到cls_bpf的tc BPF程序是完全自包含的,因此可以有效地将解析和操作过程融合到一个单元中。幸好有了cls_bpf的direct-action模式,该模式下,仅需要返回tc action裁定结果并立即结束处理流即可,可以在网络数据流中实现可扩展的可编程报文处理流程,同时避免了action的线性迭代。cls_bpf是tc层中唯一能够实现这种快速路径的“分类器”模块。

与XDP BPF程序类似,tc BPF程序可以在运行时通过cls_bpf自动更新,而不会中断任何网络流或重启服务。

cls_bpf可以附加的tc ingress和egree钩子都通过一个名为sch_clsact的伪qdisc进行管理。由于该伪qdisc可以同时管理ingress和egress的tc钩子,因此它是ingress qdisc的超集(也可直接替换)。对于__dev_queue_xmit()中的tc的egress钩子,需要注意的是,它不是在内核的qdisc root锁下运行的。因此,tc ingress和egress钩子都以无锁的方式运行在快速路径中,且这两个钩子都禁用了抢占,并运行在RCU读取侧。

通常在egress上会存在附着到网络设备上的qdisc,如sch_mq,sch_fq,sch_fq_codel或sch_htb,其中有些是可分类的qdisc(包含子类),因此会要求一个报文分类机制来决定在哪里解复用数据包。该过程通过调用tcf_classify()进行处理,进而调用tc分类器(如果存在)。cls_bpf也可以附加并用于如下场景:一些在qdisc root锁下的操作可能会收到锁竞争的影响。sch_clsact qdisc的egress钩子出现在更早的时间点,但它不属于这个锁的范围,因此作完全独立于常规的egress  qdiscs。因此,对于sch_htb这样的情况,sch_clsact qdisc可以通过qdisc root锁之外的tc BPF执行繁重的包分类工作,通过在这些 tc BPF 程序中设置 skb->mark 或 skb->priority ,这样 sch_htb 只需要一个简单的映射即可,不需要在root锁下执行代价高昂的报文分类工作,通过这种方式可以减少锁竞争。

在sch_clsact结合cls_bpf的场景下支持offloaded tc BPF程序,这种情况下,先前加载的BPF程序是从SmartNIC驱动程序jit生成的,以便在NIC上以本机方式运行。只有在direct-action模式下运行的cls_bpf程序才支持offloaded。cls_bpf仅支持offload一个单独的程序(无法offload多个程序),且只有ingress支持offload BPF程序。

一个cls_bpf实例可以包含多个tc BPF程序,如果是这种情况,那么TC_ACT_UNSPEC程序返回码可以继续执行列表中的下一个tc BPF程序。然而,这样做的缺点是,多个程序需要多次解析相同的报文,导致性能下降。

5.1返回码

tc的ingress和egress钩子共享相同的action来返回tc BPF程序使用的裁定结果,定义在 linux/pkt_cls.h系统头文件中:

#define TC_ACT_UNSPEC         (-1)
#define TC_ACT_OK               0
#define TC_ACT_SHOT             2
#define TC_ACT_STOLEN           4
#define TC_ACT_REDIRECT         7

系统头文件中还有一些以TC_ACT_*开头的action变量,可以被两个钩子使用。但它们与上面的语义相同。即,从tc BPF的角度来看TC_ACT_OK和TC_ACT_RECLASSIFY的语义相同,三个TC_ACT_stelled、TC_ACT_QUEUED和TC_ACT_TRAP操作码的语义也是相同的。因此,对于这些情况,我们只描述 TC_ACT_OK 和 TC_ACT_STOLEN 操作码。

从TC_ACT_UNSPEC开始,表示"未指定的action",用于以下三种场景:i)当一个offloaded tc程序的tc ingress钩子运行在cls_bpf的位置,则该offloaded程序将返回TC_ACT_UNSPEC;ii)为了在多程序场景下继续执行cls_bpf中的下一个BPF程序,后续的程序需要与步骤i中的offloaded tc BPF程序配合使用,但出现了一个非offloaded场景下运行的tc BPF程序;iii)TC_ACT_UNSPEC还可以用于单个程序场景,用于告诉内核继续使用skb,不会产生其他副作用。TC_ACT_UNSPEC与TC_ACT_OK类似,两者都会将skb通过ingress向上传递到网络栈的上层,或者通过egress向下传递到网络设备驱动程序,以便在egress进行传输。与TC_ACT_OK的唯一不同之处是,TC_ACT_OK基于tc BPF程序设定的classid来设置skb->tc_index,而 TC_ACT_UNSPEC 是通过 tc BPF 程序之外的 BPF上下文中的 skb->tc_classid 进行设置。

TC_ACT_SHOT通知内核丢弃报文,即网络栈上层将不会在ingress的skb中看到该报文,类似地,这类报文也不会在egress中发送。TC_ACT_SHOT和TC_ACT_STOLEN本质上是相似的,仅存在部分差异:TC_ACT_SHOT会通知内核已经通过kfree_skb()释放skb,且会立即给调用者返回NET_XMIT_DROP;而TC_ACT_STOLEN会通过consume_skb()释放skb,并给上层返回NET_XMIT_SUCCESS,假装传输成功。perf的报文丢弃监控会记录kfree_skb()的操作,因此不会记录任何因为TC_ACT_STOLEN丢弃的报文,因为从语义上说,这些 skb 是被消费或排队的而不是被丢弃的。

最后TC_ACT_REDIRECT action允许tc BPF程序通过bpf_redirect()辅助函数将skb重定向到相同或不同的设备ingress或egress路径上。通过将报文导入其他设备的ingress或egress方向,可以最大化地实现BPF的报文转发功能。使用该方式不需要对目标网络设备做任何更改,也不需要在目标设备上运行另外一个cls_bpf实例。

5.2加载tc BPF程序

假设有一个名为prog.o的tc BPF程序,可以通过tc命令将该程序加载到网络设备山。与XDP不同,它不需要依赖驱动将BPF程序附加到设备上,下面会用到一个名为em1的网络设备,并将程序附加到em1的ingress报文路径上。

# tc qdisc add dev em1 clsact
# tc filter add dev em1 ingress bpf da obj prog.o

第一步首先配置一个clsact qdisc。如上文所述,clsact是一个伪造的qdisc,与ingress qdisc类似,仅包含分类器和action,但不会提供实际的队列功能,它是附加bpf分类器所必需的。clsact 提供了两个特殊的钩子,称为ingress和egress,分类器可以附加到这两个钩子上。ingress和egress钩子都位于网络数据路径的中央接收和发送位置,每个经过设备的报文都会经过此处。ingees钩子通过内核的__netif_receive_skb_core() -> sch_handle_ingress()进行调用,egress钩子通过__dev_queue_xmit() -> sch_handle_egress()进行调用。

将程序附加到egress钩子上的操作为:

# tc filter add dev em1 egress bpf da obj prog.o

clsact qdisc以无锁的方式处理来自ingress和egress方向的报文,且可以附加到一个无队列虚拟设备上,如连接到容器的veth设备。

在钩子之后,tc filter命令选择使用bpf的da(direct-action)模式。推荐使用并指定da模式,基本上意味着bpf分类器不再需要调用外部tc action模块,所有报文的修改,转发或其他action都可以通过附加的BPF程序来实现,因此处理速度更快。

到此位置,已经附加bpf程序,一旦有报文传输到该设备后就会执行该程序。与XDP相同,如果不使用默认的section名称,则可以在加载期间进行指定,例如,下面指定的section名为foobar:

# tc filter add dev em1 egress bpf da obj prog.o sec foobar

iptables2的BPF加载器允许跨程序类型使用相同的命令行语法。

附加的程序可以使用如下命令列出:

# tc filter show dev em1 ingress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[ingress] direct-action id 1 tag c5f7825e5dac396f

# tc filter show dev em1 egress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[egress] direct-action id 2 tag b2fd5adc0f262714

prog.o:[ingress]的输出说明程序段ingress通过文件prog.o进行加载,且bpf运行在direct-action模式下。上面两种情况附加了程序id和tag,其中后者表示对指令流的hash,该hash可以与目标文件或带有堆栈跟踪的perf report等相关。最后,id表示系统范围内的BPF程序的唯一标识符,可以使用bpftool来查看或dump附加的BPF程序。

tc可以附加多个BPF程序,它提供了其他可以链接在一起的分类器。但附加一个BPF程序已经可以完全满足需求,因为通过da(direct-action)模式可以在一个程序中实现所有的报文操作,意味着BPF程序将返回tc action裁定结果,如TC_ACT_OK, TC_ACT_SHOT等。为了获得最佳性能和灵活性,推荐使用这种方式。

在上述show命令中,在BPF的相关输出旁显示了pref 49152 和handle 0x1。如果没有通过命令行显式地提供,会自动生成的这两个输出。perf表明了一个优先级数字,即当附加了多个分类器时,将会按照优先级上升的顺序执行这些分类器。handle表示一个标识符,当一个perf加载了系统分类器的多个实例时起作用。由于在BPF场景下,一个程序足矣,perf和handle通常可以忽略。

只有在需要自动替换附加的BPF程序的情况下,才会推荐在初始化加载前指定pref和handle,这样在以后执行replace操作时就不必在进行查询。创建方式如下:

# tc filter add dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar

# tc filter show dev em1 ingress
filter protocol all pref 1 bpf
filter protocol all pref 1 bpf handle 0x1 prog.o:[foobar] direct-action id 1 tag c5f7825e5dac396f

对于原子替换,可以使用(来自文件prog.o中的foobar section的BPF程序)如下命令来更新现有的ingress钩子上的程序

# tc filter replace dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar

最后,为了移除所有ingress和egress上附加的程序,可以使用如下命令:

# tc filter del dev em1 ingress
# tc filter del dev em1 egress

为了移除网络设备上的整个clsact qdisc,即移除掉ingress和egress钩子上附加的所有程序,可以使用如下命令:

# tc qdisc del dev em1 clsact

如果NIC和驱动也像XDP BPF程序一样支持offloaded,则tc BPF程序也可以是offloaded的。Netronome的nfp同时支持两种类型的BPF offload。

# tc qdisc add dev em1 clsact
# tc filter replace dev em1 ingress pref 1 handle 1 bpf skip_sw da obj prog.o
Error: TC offload is disabled on net device.
We have an error talking to the kernel

如果出现了如上错误,则表示首先需要通过ethtool的hw-tc-offload来启动tc硬件offload:

# ethtool -K em1 hw-tc-offload on
# tc qdisc add dev em1 clsact
# tc filter replace dev em1 ingress pref 1 handle 1 bpf skip_sw da obj prog.o
# tc filter show dev em1 ingress
filter protocol all pref 1 bpf
filter protocol all pref 1 bpf handle 0x1 prog.o:[classifier] direct-action skip_sw in_hw id 19 tag 57cd311f2e27366b

in_hw标志表示程序已经offload到了NIC中,注意不能同时offload tc和XDP BPF,必须且只能选择其中之一。


相关文章
|
20天前
|
Linux C语言
Linux内核队列queue.h
Linux内核队列queue.h
|
21天前
|
存储 Linux
linux查看系统版本、内核信息、操作系统类型版本
linux查看系统版本、内核信息、操作系统类型版本
56 9
|
1月前
|
Ubuntu Linux
linux查看系统版本及内核信息
在Linux中检查系统版本和内核信息,可使用`uname -r`查看内核版本,`uname -a`获取详细信息,或者查看`/proc/version`。要了解发行版版本,尝试`lsb_release -a`(如果安装了)或查阅`/etc/os-release`。Red Hat家族用`/etc/redhat-release`,Debian和Ubuntu系用`/etc/issue`及相关文件。不同发行版可能需不同命令。
32 3
|
1天前
|
弹性计算 网络协议 Shell
自动优化Linux 内核参数
【4月更文挑战第29天】
5 1
|
2天前
|
弹性计算 网络协议 Linux
自动优化 Linux 内核参数
【4月更文挑战第28天】
9 0
|
13天前
|
算法 Linux 调度
深入理解Linux内核的进程调度机制
【4月更文挑战第17天】在多任务操作系统中,进程调度是核心功能之一,它决定了处理机资源的分配。本文旨在剖析Linux操作系统内核的进程调度机制,详细讨论其调度策略、调度算法及实现原理,并探讨了其对系统性能的影响。通过分析CFS(完全公平调度器)和实时调度策略,揭示了Linux如何在保证响应速度与公平性之间取得平衡。文章还将评估最新的调度技术趋势,如容器化和云计算环境下的调度优化。
|
19天前
|
算法 Linux 调度
深度解析:Linux内核的进程调度机制
【4月更文挑战第12天】 在多任务操作系统如Linux中,进程调度机制是系统的核心组成部分之一,它决定了处理器资源如何分配给多个竞争的进程。本文深入探讨了Linux内核中的进程调度策略和相关算法,包括其设计哲学、实现原理及对系统性能的影响。通过分析进程调度器的工作原理,我们能够理解操作系统如何平衡效率、公平性和响应性,进而优化系统表现和用户体验。
|
26天前
|
负载均衡 算法 Linux
深度解析:Linux内核调度器的演变与优化策略
【4月更文挑战第5天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分——内核调度器。文章将首先回顾Linux内核调度器的发展历程,从早期的简单轮转调度(Round Robin)到现代的完全公平调度器(Completely Fair Scheduler, CFS)。接着,分析当前CFS面临的挑战以及社区提出的各种优化方案,最后提出未来可能的发展趋势和研究方向。通过本文,读者将对Linux调度器的原理、实现及其优化有一个全面的认识。
|
26天前
|
Ubuntu Linux
Linux查看内核版本
在Linux系统中查看内核版本有多种方法:1) 使用`uname -r`命令直接显示版本号;2) 通过`cat /proc/version`查看内核详细信息;3) 利用`dmesg | grep Linux`显示内核版本行;4) 如果支持,使用`lsb_release -a`查看发行版及内核版本。
36 6
|
28天前
|
Linux 内存技术
Linux内核读取spi-nor flash sn
Linux内核读取spi-nor flash sn
20 1