文件接口
在Dovetail中,evl希望能够复用Linux上的文件机制 使用和管理支持oob阶段的设备驱动 。但Dovetail不允许 在带外上下文中使用Linux内核的VFS(虚拟文件系统)服务来实现对文件的访问控制,current->files可能无法安全地从带外阶段被访问。Dovetail提供了一种简单机制,在VFS中插入hook函数,evl可以在设备驱动在Linux内核注册时调用该hook函数完成在evl中的注册。hook函数有以下三种:
- install_inband_fd():每当新的文件描述符被安装到当前进程的文件描述符表中时被调用。
- replace_inband_fd():每当现有文件描述符所引用的文件发生变化时被调用。
- uninstall_inband_fd():每当文件描述符被关闭时被调用。
c
void install_inband_fd(unsigned int fd, struct file *filp,
struct files_struct *files)
{
unsigned long flags;
struct evl_fd *efd;
int ret = -ENOMEM;
if (filp->oob_data == NULL)
return;
efd = evl_alloc(sizeof(struct evl_fd));
if (efd) {
efd->fd = fd;
efd->files = files;
efd->efilp = filp->oob_data;
INIT_LIST_HEAD(&efd->poll_nodes);
raw_spin_lock_irqsave(&fdt_lock, flags);
ret = index_efd(efd, filp);
raw_spin_unlock_irqrestore(&fdt_lock, flags);
}
EVL_WARN_ON(CORE, ret);
}
void fd_install(unsigned int fd, struct file *file)
{
struct files_struct *files = current->files;
struct fdtable *fdt;
rcu_read_lock_sched();
if (unlikely(files->resize_in_progress)) {
rcu_read_unlock_sched();
spin_lock(&files->file_lock);
fdt = files_fdtable(files);
BUG_ON(fdt->fd[fd] != NULL);
rcu_assign_pointer(fdt->fd[fd], file);
install_inband_fd(fd, file, files); // 此时在evl中也注册驱动
spin_unlock(&files->file_lock);
return;
}
/* coupled with smp_wmb() in expand_fdtable() */
smp_rmb();
fdt = rcu_dereference_sched(files->fdt);
BUG_ON(fdt->fd[fd] != NULL);
rcu_assign_pointer(fdt->fd[fd], file);
install_inband_fd(fd, file, files);
rcu_read_unlock_sched();
}
为区分设备驱动是否支持oob请求,struct file中包含一个名为oob_data的指针。若oob_data非空,则可以假设该驱动程序支持oob请求,并使用oob_data保存与oob处理相关的任何信息。EVL使用这种跟踪能力来实现其对带外文件操作的接口,如oob_ioctl()、oob_read()和oob_write()。
网络接口
与文件接口类似,evl在Linux网络栈中插入hook函数,以能够实现自己的网络栈。Dovetail提供以下两个方面的支持:
- NIC驱动接口:NIC驱动需要实现evl提供的网络接口,以供evl调用;
- Linux内核hook函数:用于在Linux内核中实现EVL网络栈。hook函数允许evl介入Linux网络栈中与网络相关的关键事件,包括缓冲区管理、套接字接口和设备处理。
在设备级别,带外联网需要:
- 输入分流:将从网卡驱动程序接收到的数据流重定向到evl,以其选择应该传递给带外任务的数据包,其余的则由Linux网络栈进行处理。这是一个按设备(即netdev)的属性,可以动态开启或关闭。
- 带外网络端口:定义网络设备作为应用程序在带外阶段接收和发送数据包的I/O端点。
Dovetail不要求 网卡驱动程序启用带外操作模式。实际上,可以使用未经修改的标准驱动程序与带外网络栈一起使用。在这种情况下,evl网络堆栈会将实际的I/O操作推迟到带内阶段 ,由未经修改的驱动程序来处理它们。如果要实现应用程序和传输硬件之间的完整、端到端的实时支持 ,则必须在网卡驱动程序中提供带外支持。
缓冲区管理
在Linux网络栈中,用于传输网络数据的基本数据结构是套接字缓冲区(socket buffer,简称sk_buff)。Dovetail将其用途扩展到传输带外I/O数据包,sk_buff可以在带内和带外网络栈之间共享。相关改动分为两类:
- 从Dovetail阶段分配和释放sk_buff:用于网卡驱动程序处理带外I/O流量;
- 在Linux中插入hook函数:用于实现完整的带外网络栈。
套接字缓冲区逻辑上分为两部分:有效载荷数据和元数据,元数据存储在sk_buff结构中。Dovetail提供了从带外阶段分配sk_buff结构的服务,并启用了带外操作,用于页面池API分配和释放内存页。
当内核启用了CONFIG_NET_OOB 配置选项,并且在内核中启用带外网络栈 时,会预先分配一个sk_buff池 。预先分配的缓冲区数量在系统启动时固定,默认为1024 个。这个全局池可以从任何执行阶段访问,以分配和释放它们。需要使用该池情况如下,使用**get_oob_skb()和put_oob_skb()**服务分别分配和释放sk_buff实例:
- evl发送网络数据包,需要一个sk_buff结构来存储;
- 当支持带外功能的网络设备驱动程序想要为其DMA接收环(RX ring)补充缓冲区插槽时,也需要从这个池中分配缓冲区。
一旦在内核中启用了CONFIG_NET_OOB(由Dovetail实现),常规的页面池API就支持带外分配模式。通过在page_pool_create()函数的参数块中设置PP_FLAG_PAGE_OOB 标志来启用此模式,适用于该池管理的整个内存空间。
与带内池的主要区别 源于缓冲区分配策略 ,当带外模式启用时,不涉及任何缓存重新填充。必须在池创建时 通过在page_pool_params结构中设置非零的pool_size参数来固定 池在其生命周期内的最大可用缓冲区数量。每个池的快速缓存会立即用最大数量的缓冲区填充。带外池不能处理页面碎片,只能返回完整的页面。
get_oob_skb()从带外池中分配一个sk_buff结构,此调用是线程安全的,并且不受阶段抢占的影响。从带内或带外阶段调用此服务都是安全的,尽管其正常用法是从后者调用。
c
struct sk_buff *get_oob_skb(void)
{
struct skbuff_oob_pool *c = &skbuff_oob_pool;
struct sk_buff *skb = NULL;
unsigned long flags;
raw_spin_lock_irqsave(&c->lock, flags);
if (!list_empty(&c->pool)) {
skb = list_first_entry(&c->pool, struct sk_buff, list);
list_del(&skb->list);
raw_spin_unlock_irqrestore(&c->lock, flags);
memset(skb, 0, offsetof(struct sk_buff, tail));
skb_mark_oob(skb);
} else {
raw_spin_unlock_irqrestore(&c->lock, flags);
}
return skb;
}
put_oob_skb释放skb到带外池;skb_is_oob判断是否为带外池;skb_mark_oob将skb标记为由带外网络栈管理;finalize_skb_inband将sk_buff结构释放到带内池中。
Linux中的页面池(Page Pool)是一种用于高效管理内存页面的机制,主要用于网络驱动 等需要快速分配和回收页面 的场景。页面池的设计目标是通过缓存页面 来提高内存分配的速度,避免频繁调用全局页面分配器。页面池包含一个快速缓存 (Fast Cache)和一个指针环缓存 (Ptr-ring Cache)。
当驱动程序请求页面时,首先从快速缓存中获取页面。如果快速缓存为空,则从指针环缓存中获取。如果指针环缓存也为空,则从全局页面分配器中分配页面。
快速缓存的设计目标是减少锁竞争和内存分配的开销。它通常在软中断上下文中运行,避免了全局页面分配器的锁竞争,从而提高了系统的响应速度
与页面缓存(Page Cache)不同,页面池主要用于快速分配和回收内存页面,特别是在需要频繁进行内存分配和释放的场景中,例如网络驱动中的DMA操作。页面缓存是Linux内核中用于缓存文件系统数据和块设备数据的机制。它将文件数据和块设备的数据存储在内存中,以便快速访问。
IP路由
在处理IP数据包的路由时,带外网络栈有两种选择:
- 实现私有路由表:带外网络栈可以实现自己的私有路由表,这些路由表可以在带外上下文中使用,以确定应该将出站数据包传递到哪个设备以传输到给定的IP目标。这种方法不需要在带内和带外网络栈之间进行同步,但它需要维护一个与带内栈已经提供的路由系统并行的冗余路由系统。
- 复用带内网络栈:带外网络栈可以重用带内网络栈已经提供的基础设施,特别是其路由决策的结果。这种方法要求带外栈能够得知带内栈可能做出的任何感兴趣的路由决策,以便它可以记录下来供后续使用,而无需进一步同步。
Dovetail支持第二种复用的方案 ,提供hook函数通知evl带内网络栈做出的任何路由决策。但不提供带内栈实现的数据包管理基础设施(如网络过滤、NAT或流量整形)的功能。hook函数为ip_learn_oob_route:
c
/*
* in-band hook which receives IPv4 routing decisions which go through
* an oob-enabled device.
*/
void ip_learn_oob_route(struct net *net, struct flowi4 *fl4, struct rtable *rt)
{
evl_net_learn_ipv4_route(net, fl4, rt);
}
struct rtable *ip_route_output_flow(struct net *net, struct flowi4 *flp4,
const struct sock *sk)
{
struct rtable *rt = __ip_route_output_key(net, flp4);
if (IS_ERR(rt))
return rt;
if (flp4->flowi4_proto) {
flp4->flowi4_oif = rt->dst.dev->ifindex;
rt = (struct rtable *)xfrm_lookup_route(net, &rt->dst,
flowi4_to_flowi(flp4),
sk, 0);
}
/*
* If the route goes through an oob-enabled device, the oob
* core might want to learn about the routing decision, pass
* it on.
*/
// 设备为开启oob且路由表未出错
if (!IS_ERR(rt) && netif_oob_port(rt->dst.dev))
ip_learn_oob_route(net, flp4, rt);
return rt;
}
Socket接口
Dovetail为evl提供了一种方式,使其能够在新的带外协议族 (PF_OOB)中实现协议,或者为现有的协议实现扩展带外I/O能力(例如,PF_INET/udp和PF_PACKET)。Dovetail并没有自己的Socket实现 ,只是为常规Socket实现添加了一小部分功能,以便evl实现协议或扩展现有的协议。当应用程序代码在创建Socket时使用SOCK_OOB标志时,会启用这一功能集:
c
/*
*Get a raw socket with out-of-band capabilities.
*/
int s = socket(AF_PACKET, SOCK_RAW | SOCK_OOB, 0);
在INET Socket数据结构(即struct sock)中添加了一个指向带外上下文描述符的指针。当该指针非空时,表示该Socket具有由evl管理的带外上下文,evl应在从Linux网络栈收到连接请求时填充它。

与从带外执行阶段管理文件的逻辑相呼应,Dovetail仅提供必要的支持,专门用于处理来自该阶段的I/O请求,即接收和发送数据。创建Socket、关闭Socket以及所有辅助操作(如绑定、连接、关闭连接等)仍然由常规的内核网络栈负责(换句话说,不要期望Dovetail会支持从带外阶段创建Socket,或者实现一个实时能力的绑定操作,它不会)。
为了使evl能够管理具有带外I/O能力的Socket,Dovetail在Linux内核中添加了几个钩子函数:
- __weak int sock_oob_attach(struct socket *sock):当使用SOCK_OOB标志创建Socket时,Dovetail允许evl通过调用此例程设置自己的上下文数据,并绑定socket到evl文件。
- __weak void sock_oob_release(struct socket *sock):释放与套接字相关的资源。
- __weak void sock_oob_destroy(struct sock *sk):当对INET Socket sk的最后一个引用被释放时,会调用sock_oob_destroy(),这可能在sock_oob_release()被调用后很久才会发生。
- __weak int sock_oob_bind(struct sock *sk, struct sockaddr *addr, int len):绑定socket地址,此调用仅由PF_INET和PF_PACKET域发出。sock_oob_bind()在域的常规绑定操作成功后运行。
- __weak int sock_oob_shutdown(struct sock *sk, int how):关闭socket,此调用仅由PF_INET域发出。sock_oob_shutdown()在域的常规关闭操作成功后运行,但在相关的INET Socket被释放之前。
- __weak int sock_oob_connect(struct sock *sk, struct sockaddr *addr, int len, int flags):接收应该从带外执行阶段处理的IOCTL请求。
- __weak long sock_inband_ioctl_redirect(struct sock *sk, unsigned int cmd, unsigned long arg):用于重定向带有oob扩展的协议的ioctl操作。