深入浅出DPDK KNI核心技术

news2024/10/7 16:26:33

一、KNI

KNI全称:Kernel NIC Interface,内核网卡接口,允许用户态程序访问linux控制平面。

在DPDK报文处理中,有些报文需要发送到内核协议栈进行处理,如GTP-C控制报文

如果报文数量较少,可以使用内核提供的TAP/TUN设备,但是鉴于这种设备使用的系统调用的方式,还涉及到copy_to_user()和copy_from_user()的开销,因此,提供了KNI接口用于改善用户态和内核态间报文的处理效率。

二、使用KNI的优势

比 Linux TUN/TAP interfaces的操作快(通过取消系统调用copy_to_user()/copy_from_user())。

可使用Linux标准网络工具ethtool, ifconfig和tcpdump管理DPDK端口。

允许端口使用内核网络协议栈。

kni的功能也是分为用户态KNI和内核态KNI两部分的

用户态的KNI代码在lib\librte_kni目录下

内核态的KNI代码在kernel/linux/kni目录下

三、用户态KNI处理

3、1 加载kni内核模块

在加载kni模块时,可以设置它的内核线程模式

insmod kmod/rte_kni.ko kthread_mode=single
insmod kmod/rte_kni.ko kthread_mode=multiple

single模式(默认):只在内核侧创建一个内核线程,来接收所有kni设备上的数据包,一个线程 vs 所有kni设备

multiple模式:每个kni接口创建一个内核线程,用来接收数据包,一个线程 vs 一个kni设备

dpdk在加载kni模块时,默认是采用的single模式,同时还可以为此内核线程设置cpu亲和性

小伙伴们可能疑问,这里的single和multiple模式是什么意思

参考官网链接:https://doc.dpdk.org/guides/prog_guide/kernel_nic_interface.html

例子程序位于examples/kni/main.c文件

3、2 初始化流程

main(int argc, char** argv)
{
    /* eal初始化 */
    ret = rte_eal_init(argc, argv);
​
    /* 创建mbuf内存池*/
    pktmbuf_pool = rte_pktmbuf_pool_create("mbuf_pool", NB_MBUF,
        MEMPOOL_CACHE_SZ, 0, MBUF_DATA_SZ, rte_socket_id());
​
    /* 初始化KNI子系统*/
    init_kni();
​
    /* 初始化端口port,并调用kni_alloc */
    RTE_ETH_FOREACH_DEV(port) {
        init_port(port);//初始化端口
        kni_alloc(port);//kni申请资源
    }
​
    /* Launch per-lcore function on every lcore */
    // 每个lcore逻辑核心上运行一个函数main_loop,CALL_MASTER表示在master core上运行

    rte_eal_mp_remote_launch(main_loop, NULL, CALL_MASTER);
​
    /* 释放资源 */
    RTE_ETH_FOREACH_DEV(port) {
        kni_free_kni(port);
    }
    return 0;
}

下面重点分析下init_kni,kni_alloc,kni_free_kni,main_loop这四个函数。

3、2、1 init_kni函数

init_kni函数中仅仅调用rte_kni_init。

rte_kni_init(unsigned int max_kni_ifaces __rte_unused)
{
    /* Check FD and open */
    if (kni_fd < 0) {
        kni_fd = open("/dev/" KNI_DEVICE, O_RDWR);//打开/dev/kni设备
    }
​
    return 0;
}

DPDK在初始化阶段会调用rte_kni_init,打开kni设备。

3、2、2 kni_alloc函数

kni_alloc函数主要调用了rte_kni_alloc函数

问题1:rte_kni_alloc对应KNI内核态的什么代码呢?

答:kni_ioctl_create()函数,后面会分析

3、2、3 kni_free_kni函数

kni_free_kni主要是调用rte_kni_release函数

问题2:rte_kni_release对应KNI内核态的什么代码呢?

答:kni_ioctl_release()函数,后面会分析

3、2、4 主逻辑函数main_loop

定位到main_loop逻辑处理函数

static int
main_loop(__rte_unused void *arg)
{
    uint16_t i;
    int32_t f_stop;
    const unsigned lcore_id = rte_lcore_id();
    enum lcore_rxtx {
        LCORE_NONE,
        LCORE_RX,
        LCORE_TX,
        LCORE_MAX
    };
    enum lcore_rxtx flag = LCORE_NONE;
​
    //遍历设备列表,判断当前的lcore逻辑核心是负责rx还是tx
    RTE_ETH_FOREACH_DEV(i) {
        if (kni_port_params_array[i]->lcore_rx == (uint8_t)lcore_id) {
            flag = LCORE_RX;
            break;
        } else if (kni_port_params_array[i]->lcore_tx == lcore_id) {
            flag = LCORE_TX;
            break;
        }
    }
​
    //如果是接收数据,则循环调用kni_ingress,直到f_stop被设置为true跳出循环
    if (flag == LCORE_RX) {
        while (1) {
            kni_ingress(kni_port_params_array[i]);
        }
    } 
    //如果是发送数据,则循环调用kni_egress,直到f_stop被设置为true跳出循环
    else if (flag == LCORE_TX) {
        while (1) {
            kni_egress(kni_port_params_array[i]);
        }
    }
​
    return 0;
}

步骤如下:

获取配置,判断当前lcore是负责rx还是tx

如果lcore负责rx,则死循环调用接口kni_ingress进行报文的收取。

如果lcore负责tx,则死循环调用接口kni_egress进行报文的发送。

因为一个lcore只能负责一个死循环,所以最好给rx和tx都单独分配一个lcore去执行,不然容易出问题

1) kni_ingress函数

疑惑1:数据如何从dpdk网口通过KNI网口发送给内核协议栈?

/**
 * Interface to burst rx and enqueue mbufs into rx_q
 */
static void
kni_ingress(struct kni_port_params *p)
{
    struct rte_mbuf *pkts_burst[PKT_BURST_SZ];
​
    nb_kni = p->nb_kni;
    port_id = p->port_id;
    for (i = 0; i < nb_kni; i++) {
        /* Burst rx from eth */
        nb_rx = rte_eth_rx_burst(port_id, 0, pkts_burst, PKT_BURST_SZ);
        
        /* Burst tx to kni */
        num = rte_kni_tx_burst(p->kni[i], pkts_burst, nb_rx);
    }
}

kni_ingress函数是从dpdk网口接收数据,然后发送给kni接口。

从dpdk网口批量读取数据到mbuf数组pkts_burst中,然后将mbuf批量发送给kni接口。

总结:通过KNI网口发送数据包给内核协议栈

(免费订阅,永久学习)学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂

更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,永久学习,或点击这里加qun免费
领取,关注我持续更新哦! ! 

2) kni_egress函数

kni_egress(struct kni_port_params *p)
{
    struct rte_mbuf *pkts_burst[PKT_BURST_SZ];
​
    nb_kni = p->nb_kni;
    port_id = p->port_id;
    for (i = 0; i < nb_kni; i++) {
        /* Burst rx from kni */
        num = rte_kni_rx_burst(p->kni[i], pkts_burst, PKT_BURST_SZ);
        
        /* Burst tx to eth */
        nb_tx = rte_eth_tx_burst(port_id, 0, pkts_burst, (uint16_t)num);    
    }
}

从kni网口接收数据,存放到pkts_burst数组中,然后批量发送给dpdk网口。

kni_egress函数是从kni接口接收数据,然后发送给dpdk网口。

3、2、5 用户态常用api总结

rte_kni_init

rte_kni_close

rte_kni_alloc

rte_kni_release

kni_allocate_mbufs

kni_free_mbufs

rte_kni_tx_burst

rte_kni_rx_burst

后面我们会针对常用的api做深度的分析。

3、3 KNI用户态和内核态交互

rx_q:从kni thread角度来说,是接收线程。

tx_q:从kni thread角度来说,是发送线程。

链接:https://doc.dpdk.org/guides-1.8/prog_guide/kernel_nic_interface.html

3、3、1 ingress部分

图的上半部分 用例ingress:

1、在 DPDK RX 端,mbuf 由 PMD 在 RX 线程上下文中分配。

2、这个线程将mbuf入队rx_q FIFO。

3、KNI线程将为rx_q轮询所有KNI活动设备。如果一个mbuf出队列,它将被转换为sk_buff结构,并通过netif_rx()函数发送到网络堆栈。4、释放已出队的mbuf指针,并将mbuf指针归还到free_q FIFO队列。

3、3、2 egress部分

图的下半部分 用例egress:

1、调用 kni_net_tx() 回调函数,从 Linux 网络堆栈接收数据包。

2、出队mbuf(无需等待缓存),用sk_buff数据来填充mbuf结构。

DPDK TX 线程将 mbuf 从tx_q队列出列,并将其发送到 PMD(通过 rte_eth_tx_burst())。

四、内核态KNI处理

4、1 内核初始化

1)模块初始化之注册misc设备

static int __init
kni_init(void)
{
    int rc;
    
    rc = register_pernet_subsys(&kni_net_ops);
​
    rc = misc_register(&kni_misc);
​
    /* Configure the lo mode according to the input parameter */
    kni_net_config_lo_mode(lo_mode);
​
    return 0;
}

模块初始化函数kni_init比较简单,核心函数有两个

注册misc设备:misc_register

配置lo_mode:kni_net_config_lo_mode

通过register_pernet_subsys或者register_pernet_gen_subsys,注册了kni_net_ops,保证每个namespace都会调用kni_init_net进行初始化。

注册为misc设备后,其工作机制由注册的misc device决定,即

static const struct file_operations kni_fops = {
    .owner = THIS_MODULE,
    .open = kni_open,
    .release = kni_release,
    .unlocked_ioctl = (void *)kni_ioctl,
    .compat_ioctl = (void *)kni_compat_ioctl,
};
​
static struct miscdevice kni_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = KNI_DEVICE,
    .fops = &kni_fops,
};
#define KNI_DEVICE "kni"

重点关注结构体变量kni_fops的kni_open,kni_release,kni_ioctl函数

kni_open函数

static int
kni_open(struct inode *inode, struct file *file)
{
    struct net *net = current->nsproxy->net_ns;
    struct kni_net *knet = net_generic(net, kni_net_id);
​
    /* kni device can be opened by one user only per netns */
    if (test_and_set_bit(KNI_DEV_IN_USE_BIT_NUM, &knet->device_in_use))
        return -EBUSY;
​
    file->private_data = get_net(net);
    pr_debug("/dev/kni opened\n");
​
    return 0;
}

检查保证一个namespace一个用户只能打开一个kni设备。

打开后,将基于namespace的私有数据赋值给file->private_data。

kni_ioctl函数

如何使用kni设备呢?内核的kni模块,提供了ioctl的支持。

static int
kni_ioctl(struct inode *inode, uint32_t ioctl_num, unsigned long ioctl_param)
{
    int ret = -EINVAL;
    struct net *net = current->nsproxy->net_ns;
​
    switch (_IOC_NR(ioctl_num)) {
    case _IOC_NR(RTE_KNI_IOCTL_CREATE):
        ret = kni_ioctl_create(net, ioctl_num, ioctl_param);
        break;
    case _IOC_NR(RTE_KNI_IOCTL_RELEASE):
        ret = kni_ioctl_release(net, ioctl_num, ioctl_param);
        break;
    }
​
    return ret;
}

有两个有效的case,RTE_KNI_IOCTL_CREATE和RTE_KNI_IOCTL_RELEASE,分别对应DPDK用户态的rte_kni_alloc和rte_kni_release,即申请kni interface和释放kni interface。

rte_kni_alloc vs kni_ioctl_create 对比

在rte_kni_alloc中,关键的代码是kni_reserve_mz申请连续的物理内存,并用其作为各个ring。

kni_ioctl_create函数

在kni_ioctl_create函数中会创建struct kni_dev结构体变量,并给它的成员赋值

static int
kni_ioctl_create(struct net *net, uint32_t ioctl_num,
        unsigned long ioctl_param)
{
    struct kni_net *knet = net_generic(net, kni_net_id);
    int ret;
    struct rte_kni_device_info dev_info;
    struct net_device *net_dev = NULL;
    struct kni_dev *kni, *dev, *n;
​
    //申请netdev并赋值
    net_dev = alloc_netdev(sizeof(struct kni_dev), dev_info.name,kni_net_init);
​
    kni = netdev_priv(net_dev);
​
    //将ring的物理地址转为虚拟地址
    kni->tx_q = phys_to_virt(dev_info.tx_phys);
    kni->rx_q = phys_to_virt(dev_info.rx_phys);
    kni->alloc_q = phys_to_virt(dev_info.alloc_phys);
    kni->free_q = phys_to_virt(dev_info.free_phys);
​
    //注册netdev
    ret = register_netdev(net_dev);
​
    //启动内核接收线程
    ret = kni_run_thread(knet, kni, dev_info.force_bind);
​
    return 0;
}
  • 通过phys_to_virt将ring的物理地址转成虚拟地址使用,这样就保证了KNI的用户态和内核态使用同一片物理地址,从而做到零拷贝。
  • 注册netdev网络设备
  • 启动内核接收线程

kni_run_thread函数

kni_run_thread是内核态要开启内核线程来接收所有kni设备上的数据

static int
kni_run_thread(struct kni_net *knet, struct kni_dev *kni, uint8_t force_bind)
{
    /**
     * Create a new kernel thread for multiple mode, set its core affinity,
     * and finally wake it up.
     */
    if (multiple_kthread_on) {
        //线程函数为kni_thread_multiple
        kni->pthread = kthread_create(kni_thread_multiple,(void *)kni, "kni_%s", kni->name);
    } else {
        //加mutex锁
        mutex_lock(&knet->kni_kthread_lock);
​
        if (knet->kni_kthread == NULL) {
            //创建内核线程,线程函数为kni_thread_single
            knet->kni_kthread = kthread_create(kni_thread_single,
                (void *)knet, "kni_single");
        }
        
        //解mutex锁
        mutex_unlock(&knet->kni_kthread_lock);
    }
​
    return 0;
}

上述代码的思路:

如果KNI为多线程模式,每创建一个kni设备,就创建一个内核线程。

如果KNI为单线程模式,则检查是否已经启动了kni_thread。没有的话,创建唯一的kni内核thread kni_single,有的话,则什么都不做。

我们这里仅分析kni_thread_single线程函数。

kni_thread_single线程函数

static int
kni_thread_single(void *data)
{
    struct kni_net *knet = data;
    int j;
    struct kni_dev *dev;
​
    while (!kthread_should_stop()) {
        down_read(&knet->kni_list_lock);
        //遍历所有kni设备
        for (j = 0; j < KNI_RX_LOOP_NUM; j++) {
            list_for_each_entry(dev, &knet->kni_list_head, list) {
                //接收动作
                kni_net_rx(dev);
                kni_net_poll_resp(dev);
            }
        }
        up_read(&knet->kni_list_lock);
    }
​
    return 0;
}

在持有读锁的情况下,遍历所有的kni设备,执行接收动作kni_net_rx。

kni_net_rx函数如下

/* rx interface */
void
kni_net_rx(struct kni_dev *kni)
{
    /**
     * It doesn't need to check if it is NULL pointer,
     * as it has a default value
     */
    (*kni_net_rx_func)(kni);
}

kni_net_rx内部调用回调函数kni_net_rx_func

 static kni_net_rx_t kni_net_rx_func = kni_net_rx_normal;

默认情况下kni_net_rx_func回调函数注册为kni_net_rx_normal

我们一起看下kni_net_rx_normal函数

kni_net_rx_normal(struct kni_dev *kni)
{
    uint32_t ret;
    uint32_t len;
    uint32_t i, num_rx, num_fq;
    struct rte_kni_mbuf *kva;
    void *data_kva;
    struct sk_buff *skb;
    struct net_device *dev = kni->net_dev;
​
    /* Get the number of free entries in free_q */
    /*检查释放队列是否还有空位,没有的话,意味着读取后的数据无法增加到释放队列,故直接返回。*/
    num_fq = kni_fifo_free_count(kni->free_q);
​
    /* Burst dequeue from rx_q */
    /* 重点!!! 从kni->rx_q中弹出元素*/
    /* 重点!!! 从kni->rx_q中弹出元素*/
    num_rx = kni_fifo_get(kni->rx_q, kni->pa, num_rx);
    if(num_rx == 0)
        return;
​
    /* Transfer received packets to netif */
    // 循环处理收到的kni数据,将数据复制到申请的skb中。
    for (i = 0; i < num_rx; i++) {
        kva = pa2kva(kni->pa[i]);
        len = kva->pkt_len;
        data_kva = kva2data_kva(kva);
        kni->va[i] = pa2va(kni->pa[i], kva);
​
        /* 从内存申请skb */
        skb = dev_alloc_skb(len + 2);
​
        /* 如果只有一个mbuf segment段,则直接拷贝,如果是多个segment段,则分批拷贝*/
        if (kva->nb_segs == 1) {
            memcpy(skb_put(skb, len), data_kva, len);
        }
​
        /*设置skb相关参数*/
        skb->dev = dev;
        skb->protocol = eth_type_trans(skb, dev);
        skb->ip_summed = CHECKSUM_UNNECESSARY;
​
        //调用netif_rx_ni将skb传给内核协议栈处理
        netif_rx_ni(skb);
    }
​
    /* Burst enqueue mbufs into free_q */
    /*将使用后的mbuf指针加入到kni->free_q队列中,等待用户态KNI来进行释放*/
    ret = kni_fifo_put(kni->free_q, kni->va, num_rx);
}

流程如下

1.检查释放队列是否还有空位,没有的话,意味着读取后的数据无法增加到释放队列,故直接返回。

2.从kni->rx_q读取数据到kni->pa中。没有任何报文,则直接返回。

3.循环处理收到的kni数据,将数据复制到申请的skb中。

4.调用netif_rx_ni将skb传给内核协议栈处理

5.将读取的数据追加到free_q队列中,将使用后的kni->va投递到kni->free_q队列中,等待用户态KNI进行释放

这是DPDK app向KNI设备写入数据,也就是发给内核的情况。

而最前面的rte_kni_handle_request是什么作用呢?

rte_kni_handle_request从kni->req_q拿到request,然后根据修改mtu或者设置接口的命令做相应的操作,最后将response放回kni->resp_q。

2)模块初始化之配置lo_mode

void
kni_net_config_lo_mode(char *lo_str)
{
    if (!strcmp(lo_str, "lo_mode_none"))
        pr_debug("loopback disabled");
    else if (!strcmp(lo_str, "lo_mode_fifo")) {
        pr_debug("loopback mode=lo_mode_fifo enabled");
        kni_net_rx_func = kni_net_rx_lo_fifo;
    } else if (!strcmp(lo_str, "lo_mode_fifo_skb")) {
        pr_debug("loopback mode=lo_mode_fifo_skb enabled");
        kni_net_rx_func = kni_net_rx_lo_fifo_skb;
    } else {
        pr_debug("Unknown loopback parameter, disabled");
    }
}
​

配置lo_mode,函数指针kni_net_rx_func指向不同的函数,默认为kni_net_rx_normal。

如果为lo_mode_fifo模式,函数指针kni_net_rx_func设置为kni_net_rx_lo_fifo;

如果为lo_mode_fifo_skb模式,函数指针kni_net_rx_func设置为kni_net_rx_lo_fifo_skb;

4、2 dpdk网口 -> KNI网口方向

4、2、1 用户态KNI处理流程

将pkts_burst中的mbuf报文批量发送给kni接口

unsigned
rte_kni_tx_burst(struct rte_kni *kni, struct rte_mbuf **mbufs, unsigned num)
{
    void *phy_mbufs[num];
    unsigned int ret;
    unsigned int i;
    //记住这里传递的都是指向mbuf的指针
​
    //指向mubuf的地址是虚拟地址,将虚拟地址全部转为物理地址
    for (i = 0; i < num; i++)
        phy_mbufs[i] = va2pa(mbufs[i]);
​
    //将物理地址投递到kni->rx_q队列上
    ret = kni_fifo_put(kni->rx_q, phy_mbufs, num);
​
    /* Get mbufs from free_q and then free them */
    kni_free_mbufs(kni);
​
    return ret;
}

static void
kni_free_mbufs(struct rte_kni *kni)
{
    int i, ret;
    struct rte_mbuf *pkts[MAX_MBUF_BURST_NUM];
​
    ret = kni_fifo_get(kni->free_q, (void **)pkts, MAX_MBUF_BURST_NUM);
    if (likely(ret > 0)) {
        for (i = 0; i < ret; i++)
            rte_pktmbuf_free(pkts[i]);
    }
}

准备rte_mbuf的指针数组pkts,然后调用kni_free_mbufs函数从kni->free_q队列上获取要释放的元素,保存到pkts中。

然后调用rte_pktmbuf_free函数进行释放内存资源

用户态KNI做了两件事: 1.将mbuf投递到kni->rx_q接收队列上

2.批量从kni->free_q队列上取元素,将内存归还给内存池

4、2、2 内核态KNI处理流程

问题3:前文已将数据mbuf投递到kni->rx_q队列中,那么谁来读取kni->rx_q队列上的数据写入内核网络协议栈上呢?

带着上述的疑问,我们来探寻下内核态的KNI处理流程

4、3 KNI网口方向 -> dpdk网口

4、3、1 从kni->tx_q队列读数据

kni_egress函数中调用rte_kni_rx_burst从KNI网口中批量读取数据包,然后调用rte_eth_tx_burst批量发送给dpdk网口

unsigned
rte_kni_rx_burst(struct rte_kni *kni, struct rte_mbuf **mbufs, unsigned num)
{
    unsigned ret = kni_fifo_get(kni->tx_q, (void **)mbufs, num);
​
    /* If buffers removed, allocate mbufs and then put them into alloc_q */
    if (ret)
        kni_allocate_mbufs(kni);
​
    return ret;
}

从kni->tx_q队列取元素存至mbufs指向的数组中,如果buffer已经删除,则调用kni_allocate_mbufs申请mbufs,然后添加到alloc_q队列

static void
kni_allocate_mbufs(struct rte_kni *kni)
{
    int i, ret;
    struct rte_mbuf *pkts[MAX_MBUF_BURST_NUM];
    void *phys[MAX_MBUF_BURST_NUM];
    int allocq_free;
​
    //判断kni->alloc_q队列上还需要申请多少内存资源
    allocq_free = (kni->alloc_q->read - kni->alloc_q->write - 1) \
            & (MAX_MBUF_BURST_NUM - 1);
    for (i = 0; i < allocq_free; i++) {
        /*从内存池申请内存资源*/
        pkts[i] = rte_pktmbuf_alloc(kni->pktmbuf_pool);
​
        //申请到的内存是虚拟地址,将其转换为物理地址,内核才能进行操作
        phys[i] = va2pa(pkts[i]);
    }
​
    //将申请的内存投递到kni->alloc_q队列中
    ret = kni_fifo_put(kni->alloc_q, phys, i);
​
    /* Check if any mbufs not put into alloc_q, and then free them */
    if (ret >= 0 && ret < i && ret < MAX_MBUF_BURST_NUM) {
        int j;
​
        for (j = ret; j < i; j++)
            rte_pktmbuf_free(pkts[j]);
    }
}

上面rte_kni_rx_burst函数中kni_fifo_get(kni->tx_q, (void **)mbufs, num);

kni_fifo_get函数是从kni->tx_q队列取数据,那么谁往kni->tx_q队列投递数据呢?

4、3、2 向kni->tx_q队列写数据

当内核向kni设备发送数据时,最终会调用到net_device_ops->ndo_start_xmit。

net_device_ops结构体的ndo_start_xmit回调函数注册为kni_net_tx(位于kernel/linux/kni/kni_net.c)

static int
kni_net_tx(struct sk_buff *skb, struct net_device *dev)
{
    /* Check if the length of skb is less than mbuf size */
    //对skb报文做长度检查,不能超过mbuf的大小
    if (skb->len > kni->mbuf_size)
        goto drop;
    
    //检测tx_q队列中是否有空闲位置
    //检测alloc_q队列有否有剩余的mbuf
    if (kni_fifo_free_count(kni->tx_q) == 0 ||
            kni_fifo_count(kni->alloc_q) == 0) {
        /**
         * If no free entry in tx_q or no entry in alloc_q,
         * drops skb and goes out.
         */
        goto drop;
    }
​
    /* dequeue a mbuf from alloc_q */
    /* 从alloc_q队列取出一块内存*/
    ret = kni_fifo_get(kni->alloc_q, &pkt_pa, 1);
    if (likely(ret == 1)) {
        void *data_kva;
​
        //将pkt_pa转化为虚拟地址
        pkt_kva = pa2kva(pkt_pa);
        data_kva = kva2data_kva(pkt_kva);
        pkt_va = pa2va(pkt_pa, pkt_kva);
​
        /*skb -> mbuf 将skb的值赋值过去*/
        len = skb->len;
        memcpy(data_kva, skb->data, len);
        if (unlikely(len < ETH_ZLEN)) {
            memset(data_kva + len, 0, ETH_ZLEN - len);
            len = ETH_ZLEN;
        }
        pkt_kva->pkt_len = len;
        pkt_kva->data_len = len;
​
        /* 将pkt_va追加到发送队列 tx_q */
        ret = kni_fifo_put(kni->tx_q, &pkt_va, 1);
        if (unlikely(ret != 1)) {
            /* Failing should not happen */
            pr_err("Fail to enqueue mbuf into tx_q\n");
            goto drop;
        }
    } else {
        /* Failing should not happen */
        pr_err("Fail to dequeue mbuf from alloc_q\n");
        goto drop;
    }
​
    /* 释放skb并更新计数 */
    dev_kfree_skb(skb);
    kni->stats.tx_bytes += len;
    kni->stats.tx_packets++;
​
    return NETDEV_TX_OK;
​
drop:
    /* Free skb and update statistics */
    dev_kfree_skb(skb);
    kni->stats.tx_dropped++;
​
    return NETDEV_TX_OK;
    }

 原文链接:https://zhuanlan.zhihu.com/p/535078429   

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/101786.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lua热更新

Lua热更新解决方案 文章目录Lua热更新解决方案1.AB包1.1 AB包概述1. 从0开始的Lua语法1.1 HelloWorld1.2 数据类型1.3 字符串1.4 运算符1.5 条件语句1.6 循环语句1.7 函数1.8 数组1.9 迭代器1.10 字典&#xff0c;类&#xff0c;对象1.11 多脚本执行1.12 特殊语法1.13 协程1.14…

hive时间和字符串互转,时间函数

hive里经常需要将字符串转化为date或timestamp 或者转化为日期格式的string 先说一个简单的 cast(xx as date/string/timestamp) 这个大多情况都可以用 1.to_date to_date只保留年月日,参数必须是string类型的yyyy-MM-dd HH:mm:ss或者date或timestamp类型,返回值是date类型,…

尚太科技开启招股:预计募资总额22亿元,业绩增长迅猛

12月19日&#xff0c;石家庄尚太科技股份有限公司&#xff08;下称“尚太科技”&#xff0c;SZ:001301&#xff09;开启招股&#xff0c;将在深圳证券交易所主板上市。本次冲刺上市&#xff0c;尚太科技的发行价格为33.88元/股&#xff0c;发行数量为6494.37万股&#xff0c;募…

MySQL Binlog温故知新

目录 一、什么是Binlog 二、Binlog文件记录模式 三、Binlog 日志内容 四、常用的binlog日志操作命令 五、binlog日志中间件 一、什么是Binlog Binlog &#xff08;Binary log&#xff09;是MySQL的二进制日志&#xff0c;以二进制的形式记录了对于数据库的变更&#xff0…

Tesseract-OCR 和cnocr/cnstd

Tesseract-OCR学习系列 Tesseract-OCR学习系列&#xff08;四&#xff09;API - 简书 参考文档&#xff1a;https://github.com/tesseract-ocr/tesseract/wiki/APIExample 这篇文章介绍了GetComponentImages等基础api的用法 Python 自动识别图片文字—保姆级OCR实战教程 Py…

使用界面配置静态路由(保姆级教程)

啰嗦几句 因为写的很详细&#xff0c;保姆级别的&#xff0c;所以看起来内容很多&#xff0c;但是东西就那一点&#xff0c;你自己配个2、3遍就会了。期末考试也不用担心这个实验了。 使用Cisco Packet Tracer这个软件。 原文件下载 补充&#xff1a;下载后使用Cisco Packet T…

有源电力滤波器——低压配电系统

安科瑞 华楠 一、谐波的危害 ● 使公用电网中的设备产生附加谐波损耗&#xff0c;降低电网、输电及用电设备的使用效率。 ● 在三相四线制系统中&#xff0c;由于零线流过大量的3n次谐波电流&#xff0c;造成零线过热。 ● 谐波会产生额外的热效应&#xff0c;引起用电设备…

GitLab创建新分支并同步其它分支的内容(IDEA)

拉取项目到本地 选择对应项目并复制clone地址 打开IDEA&#xff0c;选择File–》New–》Project from Version Control 在弹出的对话框中粘贴刚才复制的地址 完成后项目就被拉取到本地对应目录中了 创建新分支 这里以master分支为例&#xff0c;其它分支同理 首先在GitL…

Linux Red Hat 8.0 find命令练习

find&#xff1a;主要用来查找字符串 常用参数&#xff1a; -name 搜索文件名 -iname 搜索文件名时大小写不敏感 -type d 目录、 f 普通文件、 l 符号链接 -size 指定搜索文件大小 -perm 按执行权限来查找 -user 按…

在线考试网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 模块划分&#xff1a;老师模块、班级模块、学生模块、课程模块、试题模块、试卷模块、 组卷模块、考试模块、答题模块 …

什么是项目风险管理?要如何执行风险管理?

项目风险管理是项目管理中重要组成部分之一&#xff0c;其影响着项目是否能成功最终交付的关键。虽然项目经理都无法准确地预测未来项目结果&#xff0c;但可以应用一种精简的风险管理流程来预测项目管理中的不确定性&#xff0c;并将这些不确定因素的影响降到最低。因此如何执…

基于Python+Django+Vue+MYSQL的邯郸地标美食导游平台

项目介绍 我的家乡是邯郸市。邯郸市我国为数不多的3000年没有改变过名字城市。我的家乡就是邯郸。在我的家乡有非常多的旅游景点和美食。为了让更多的人了解到邯郸的旅游景点和好吃的美食信息&#xff0c;我通过pythonvueelementui开发了本次的邯郸家乡网红旅游景点美食导游平…

CSS高级 DAY2

目录 选择器进阶 复合选择器 后代选择器 子代选择器 并集选择器 交集选择器 hover伪类选择器&#xff08;就是鼠标悬停选择器&#xff09; 背景有关属性 背景颜色 背景图片 背景平铺 背景位置 背景的复合属性 元素显示模式 块级元素 行内元素 行内块元素 元素显示…

SimpleDateFormat线程不安全解析以及解决方案

我们都知道SimpleDateFormat是Java中用来日期格式化的一个类&#xff0c;可以将特定格式的字符转转化成日期&#xff0c;也可将日期转化成特定格式的字符串。比如 将特定的字符串转换成日期 public static void main(String[] args) throws ParseException {SimpleDateFormat…

windows11打开隐藏的gpedit.msc本地组策略编辑器以及禁止自动更新系统

目录打开隐藏的gpedit.msc本地组策略编辑器windows11禁止自动更新系统打开隐藏的gpedit.msc本地组策略编辑器 1.新建.txt文本文件&#xff0c;输入如下代码 echo off pushd “%~dp0” dir /b C:\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtension…

a股行情接口功能特点

a股行情接口功能特点&#xff0c;如下&#xff1a; 1、支持A股历史交易查询&#xff1b; 2、包括股票数据&#xff1b; 3、每日A股收盘后更新当日交易数据&#xff0c;停牌不更新&#xff1b; 4、支持多股历史数据一次查询&#xff1b; 5、支持任何时间段查询&#xff1b;…

Linux进程概念(二)

Linux进程概念进程状态普通操作系统层面理解运行与阻塞挂起与阻塞Linux是怎么做的孤儿进程进程优先级什么是优先级如何改变优先级其他概念进程状态 进程状态分有&#xff1a; 运行 新建 就绪 挂起 阻塞 等待 停止 挂机 死亡… 状态其实就是返回的整形&#xff0c;就像某个数字…

【毕业设计_课程设计】基于 SVM 分类器的动作识别系统(源码+论文)

文章目录0 项目说明1 研究目的2 研究方法2.1 动作采集2.2 动作识别2.3 智能家居模拟3 论文目录4 项目工程0 项目说明 基于 SVM 分类器的动作识别系统 提示&#xff1a;适合用于课程设计或毕业设计&#xff0c;工作量达标&#xff0c;源码开放 1 研究目的 本项目对经典 SVM 二…

从企业关心的重点,带你了解商业智能BI

企业进行信息化建设&#xff0c;能通过业务信息系统以及规范化、标准化的业务流程存储高质量的业务数据&#xff0c;而这些数据则是数字化转型成功的重要条件。只有信息化和数字化共同发展&#xff0c;企业才能成功完成数字化转型&#xff0c;构建全新的商业模式&#xff0c;完…

kafka的客户端限流(资源配额)

前言 本文说明的是Kafka的客户端&#xff08;生产者、消费者&#xff09;与broker之前的限流&#xff0c;不是kafka的broker间topic副本同步的限流。 客户端限流 在kafka的官方文档&#xff0c;不叫限流&#xff0c;叫做资源配额&#xff1a;通过对客户端请求进行配额&#…