linux usb网卡驱动_Linux 网络大全(二)——网卡驱动

18a51cddfc74bfc65ce46e9f998d6612.png

Network Driver

本文试图从软件的角度解释一个网络请求的今生前世,我们这里选取 RTL8139 作为我们了解网络驱动的硬件设备。

设备数据结构定义:net_device

Linux 内核中设备的抽象对应的结构体是 struct net_device

struct net_device {    char name[IFNAMSIZ];    /*     *  I/O 相关     */    unsigned long mem_end;    unsigned long mem_start;    unsigned long base_addr;    int irq;    // 设备 Index    int ifindex;    // stats 数据    struct net_device_stats stats;    // 接受队列    struct netdev_rx_queue *_rx;    unsigned int num_rx_queues;    unsigned int real_num_rx_queues;    // 发送队列    struct netdev_queue *_tx;    unsigned int num_tx_queues;    unsigned int real_num_tx_queues;};

对于现在硬件设备都会基于 mmio 模式进行工作,因此我们读取数据也是直接和读取内存类似的方式进行读取的。

网卡数据的读取

外部数据的读取会涉及到中断体系,Linux 将 中断 分为了两个部分: top half 与 bottom halftop half是在呼叫 request_irq() 时所指定的 interrupt handler 函数,bottom half 则是真正负责响应中断的 task

硬中断部分

对于数据初次抵达硬件,会触发硬件中断,如下图所示(网卡类似)。

f7c1dd7a73e291a1de24be2b85ea7210.png

因此从中断的入口观察是最好的,下面的代码逻辑从 linux-1.x 版本中截取,比最近的要简单一些,容易理解,另外就是新版的已经改为 NAPI 模式,下文提到再说。

static void rtl8139_interrupt(int irq, void *dev_instance, struct pt_regs *regs) {    struct net_device *dev = (struct net_device *) dev_instance;    struct rtl8139_private *tp = dev->priv;    void *ioaddr = tp->mmio_addr;    unsigned short isr = readw(ioaddr + ISR);    // 处理接受信号,上面略过了发送部分    if (isr & RxOK) {        LOG_MSG("receive interrupt received\n");        while ((readb(ioaddr + CR) & RxBufEmpty) == 0) {            unsigned int rx_status;            unsigned short rx_size;            unsigned short pkt_size;            struct sk_buff *skb;            // 处理环形队列            if (tp->cur_rx > RX_BUF_LEN)                tp->cur_rx = tp->cur_rx % RX_BUF_LEN;            rx_status = *(unsigned int *) (tp->rx_ring + tp->cur_rx);            rx_size = rx_status >> 16;            pkt_size = rx_size - 4;            /* 创建内核态的 skb 储存接受到的数据 */            skb = dev_alloc_skb(pkt_size + 2);            if (skb) {                skb->dev = dev;                skb_reserve(skb, 2); /* 16 byte align the IP fields */        // 拷贝数据并且检查 checksum                eth_copy_and_sum(                        skb, tp->rx_ring + tp->cur_rx + 4, pkt_size, 0);                skb_put(skb, pkt_size);                skb->protocol = eth_type_trans(skb, dev);                netif_rx(skb);                dev->last_rx = jiffies;                tp->stats.rx_bytes += pkt_size;                tp->stats.rx_packets++;            } else {                LOG_MSG("Memory squeeze, dropping packet.\n");                tp->stats.rx_dropped++;            }            /* 移动读取指针  * /            tp->cur_rx = (tp->cur_rx + rx_size + 4 + 3) & ~3;        }    }    return;}

中断模式下,我们收到一个数据包是非常的好理解的,将数据从网卡中 COPY 到我们的内核中即可,netif_rx 就是我们从 Hardware 向 Kernel 转换的核心点。

int netif_rx(struct sk_buff *skb) {    int this_cpu = smp_processor_id();    struct softnet_data *queue;    unsigned long flags;    // 我们为每个 Core 分配了一个 待处理的数据队列,这里的处理如注释所言,  // 代码可能被重排,这里只用当前的 CPU 保证了处理路径的最短化    queue = &softnet_data[this_cpu];    //没有超过backlog的限制就置于队列    if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {        if (queue->input_pkt_queue.qlen) {            if (queue->throttle) // 不然就丢弃 -> 因为还没响应,等于丢包                goto drop;enqueue:            dev_hold(skb->dev);            __skb_queue_tail(&queue->input_pkt_queue, skb);            return softnet_data[this_cpu].cng_level;        }        goto enqueue;    }drop:    netdev_rx_stat[this_cpu].dropped++;    kfree_skb(skb);    return NET_RX_DROP;}

硬中断部分到此结束,我们将数据压入内核的虚拟接受列队中结束。

[选读] 硬中断成本

硬中断 是一个有硬件主导,软件配合的出来体系,但是这样的模式不是没有成本的,打断正在运行的软件进程,我们不得不涉及到进程的切换,还有中断标记的处理等工作。It’s not free. ,有兴趣的可以参考 Profiling I/O Interrupts in Modern Architectures,

截止至今,我们知道了对于从网卡来的数据,我们至少会涉及到 ➊ Hard Interrupt 导致的 进程切换,并且 ➋ 涉及到一次数据从网卡的 Buffer 向内核拷贝的过程。

软中断部分

软中断的逻辑在 net_rx_action 中

static void net_rx_action(struct softirq_action *h) {    int this_cpu = smp_processor_id();    struct softnet_data *queue = &softnet_data[this_cpu];    int bugdet = netdev_max_backlog;        for (;;) {        struct sk_buff *skb;        struct net_device *rx_dev;        // 将数据弹出        skb = __skb_dequeue(&queue->input_pkt_queue);        // 没数据直接结束        if (skb == NULL)            break;        skb->h.raw = skb->nh.raw = skb->data;        {            struct packet_type *ptype, *pt_prev;            unsigned short type = skb->protocol;            pt_prev = NULL;            // 根据 package 类型找到对应的处理函数进行处理            for (ptype = ptype_all; ptype; ptype = ptype->next) {                if (!ptype->dev || ptype->dev == skb->dev) {                    if (pt_prev) {                        if (!pt_prev->data) {                            deliver_to_old_ones(pt_prev, skb, 0);                        } else {                            atomic_inc(&skb->users);                            // 调用预置的回调函数                            pt_prev->func(skb,skb->dev,pt_prev);                        }                    }                    pt_prev = ptype;                }            }        }        return;    }}

回调的函数签名如下:

int (*func) (struct sk_buff *, struct net_device *, struct packet_type *); 很标准的指针函数。不过也很讨厌看这部分的代码,如果不是 Runtime 阶段很难找到这个定义的函数是什么。这里实际上在接收到 TCP/IP 协议包的时候,回调的是 int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt) 函数。在看 IP 处理的之前,我们先看看这个 packet_type 的相关函数。

[选读] Packet Type 钩子

packet_type 的定义很简单

struct packet_type {    unsigned short type;    /*  协议类型  */    struct net_device *dev;    /* 不设置就是通配符  */    int (*func)(struct sk_buff *, struct net_device *, struct packet_type *); /* 回调函数 */    void *data;    /* 私有数据  */    struct packet_type *next;};

在 dev.c 的源码中,我们可以动态的向 ptype_all 注册我们需要的 ptype

void dev_add_pack(struct packet_type *pt) {    if (pt->type == htons(ETH_P_ALL)) {        netdev_nit++;        pt->next = ptype_all;        ptype_all = pt;    } else {        hash = ntohs(pt->type) & 15;        pt->next = ptype_base[hash];        ptype_base[hash] = pt;    }}

因此我们如果需要处理比如 arp 的请求,可以单独增加一个,所以在 arp.c 初始化的时候,就可以出发。

static struct packet_type arp_packet_type = {        type:    __constant_htons(ETH_P_ARP),        func:    arp_rcv,        data:    (void *) 1,};void __init arp_init(void) {    dev_add_pack(&arp_packet_type);}
TCPDUMP

tcpdump 二层抓包,用的是 libpcap 库,它的基本原理是

  1. 先创建socket,内核dev_add_packet()挂上自己的钩子函数

  2. 然后在钩子函数中,把skb放到自己的接收队列中,

  3. 接着系统调用recv取出skb来,把数据包skb->data拷贝到用户空间

  4. 最后关闭socket,内核dev_remove_packet()删除自己的钩子函数

因此代码如下表述

static int packet_create(struct socket *sock, int protocol) {    // 略部分代码    if (protocol) {        sk->protinfo.af_packet->prot_hook.type = protocol;        dev_add_pack(&sk->protinfo.af_packet->prot_hook); /* 增加 Hook 在此协议上 */        sock_hold(sk);        sk->protinfo.af_packet->running = 1;    }    return (0);}

直到此处,我们走到这里算是和硬件越来越远了。再下来的历程,我们要去面对的是更加高纬度的 Socket 抽象。

b6a8ef1d28ef5ad3b97456478157ec76.png

[选读] 软断成本

对于常见的硬件中断来说,比如键入了一个字母,过程简单,很快可以处理完成,对于网络IO就复杂的多,涉及到从驱动拷贝数据,然后到虚拟的网络协议栈,再到Socket的Buffer中,因此才才产生的了软中断的,将中断一分为二,软中断优先级较低但是因为软件中断复杂,成本上是要更多的。

软中断依然需要 ➊ 进程上下文的切换(这里切换到 内核线程 ksoftirqd ),这里只不过相对于硬中断是操作系统自己进行调度的 ➋ 就是和系统开销调用一下,因为对于正常运行的用户态进程,我们需要把当前的上下文保存。

内核线程 ksoftirqd 也会充分利用多核的能力,对于每一个 core 都会启动一个 ksoftirqd,暂且可以把它当做一个普通的进程看待。

NAPI

单纯的看,上面的开销,我们一眼就可以发现硬中断部分有一个非常不合理的情况,考虑如果出现以下情况

网卡每隔 10 ms 获得一个数据包,我们硬中断处理需要 9ms

那会出现一个很神奇的现象就是我们虽然每次都要将中断结束,但是每一次刚刚结束又要唤醒,结果我们会浪费大量的时间在处理中断请求上。针对这一的情况,Linux 2.6 之后提供了一个全新的API来处理这种情况(实际上对于大部分的数据,网络请求从不间断)。

4f62ce13872d887dd1f6613e18adbacf.png

既然每次进中断都很浪费,那我们就不要每一次都进中断了,NAPI 的解决之道就是,当我们接收到请求之后,我们将从此网卡读取作为一个 Task 注册到 napi_scheluder 中,然后屏蔽中断,内核定时的去 poll 数据即可。主要就是减少了硬中断切换的成本。

NAPI 定义
struct napi_struct {    // poll 列表    struct list_head poll_list;    unsigned long state;    int weight;    // 获得数据的 poll 函数    int (*poll)(struct napi_struct *, int);};

核心的逻辑都是在 poll 的回调函数中。

NAPI 之下的硬中断处理
static irqreturn_t rtl8139_interrupt(int irq, void *dev_instance) {    struct net_device *dev = (struct net_device *) dev_instance;    struct rtl8139_private *tp = netdev_priv(dev);    /* Receive packets are processed by poll routine.       If not running start it now.     如注释所言,现在的处理都早 POLL 的子函数中,我们在这里只是向 __napi_schedule 注册我们的 poll 函数  */    if (status & RxAckBits) {        if (napi_schedule_prep(&tp->napi)) {            RTL_W16_F(IntrMask, rtl8139_norx_intr_mask);            __napi_schedule(&tp->napi);        }    }    netdev_dbg(dev, "exiting interrupt, intr_status=%#4.4x\n",               RTL_R16(IntrStatus));    return IRQ_RETVAL(handled);}

对于 poll 函数的实现这里就不做展开了,和传统的体系一样,直接将数据拷贝到内核态没有区别。

软中断唤醒

因此到此版本,网络请求的软中断环境代码也相对应的有所改变

static void net_rx_action(struct softirq_action *h) {    struct softnet_data *sd = &__get_cpu_var(softnet_data);    unsigned long time_limit = jiffies + 2;    int budget = netdev_budget;    void *have;    while (!list_empty(&sd->poll_list)) {        struct napi_struct *n;        int work, weight;        // 获得可运行的 poll        n = list_first_entry(&sd->poll_list,        struct napi_struct, poll_list);        work = 0;        if (test_bit(NAPI_STATE_SCHED, &n->state)) {            work = n->poll(n, weight); // 执行 poll 操作        }    }

只将核心的逻辑展示出来,比如代码中 budget 防止饥饿的操作都留于读者自行理解了, NAPI 解决了我们依靠硬中断导致的大量的进程切换的问题,加上现在的操作系统都是基于 DMA ,因此整个过程中的成本就只有 ➊ 软中断导致的线程切换 ➋ 数据的拷贝

a1df06ad91c474fb9326ee8a09931800.png

关注我们,注入技术原力

1bcef045621e7f0cf2b7dade48fdb82a.png

原力注入

微信号 : Force_Injection

热点文章

Linux 网络大全(一)—— 基础知识

DevOps 平台就需要一个专业的门户提升逼格!

Kubernetes 网络插件工作原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值