Linux 网卡数据收发过程分析

一般来说,网卡主要有两个重要的功能:接收数据发送数据

所以,当网卡接收到数据包后,要通知 Linux 内核有数据需要处理。另外,网卡驱动应该提供让 Linux 内核把数据把发送出去的接口。

net_device 结构是 Linux 为了适配不同类型的网卡设备而抽象出来的对象,不同的网卡驱动只需要按 Linux 的规范来填充 net_device 结构的各个成员变量,Linux 内核就能够识别出网卡,并工作起来。

下面我们将分析网卡设备接收和发送数据包的实现原理。

net_device 结构

net_device 结构是 Linux 内核对网卡设备的抽象,但由于历史原因,net_device 结构的定义十分复杂。

不过本文主要分析网卡设备收发数据的实现,所以不会分析 net_device 结构的所有成员。下面主要列出收发数据相关的成员,如下:


struct net_device
{
    char                name[IFNAMSIZ];  // 设备名字
    ...
    unsigned int        irq;             // 中断号
    ...
    int (*init)(struct net_device *dev); // 设备初始化设备的接口
    ...
    int (*open)(struct net_device *dev); // 打开设备时调用的接口
    int (*stop)(struct net_device *dev); // 关闭设备时调用的接口

    // 发送数据接口
    int (*hard_start_xmit)(struct sk_buff *skb,struct net_device *dev);
    ...
};

下面介绍一下各个成员的作用:

  • name:设备的名字。用于在终端显示设备的名字或者通过设备名字来搜索设备。
  • irq:中断号。当网卡从网络接收到数据包后,需要产生一个中断来通知 Linux 内核有数据包需要处理,而 irq 就是网卡驱动注册到内核中断服务的中断号。
  • init、open、stop:分别为设备的初始化接口,打开接口和关闭接口。
  • hard_start_xmit:当需要通过网卡设备发送数据时,可以调用这个接口来发送数据。

所以,一个网卡驱动必须完成以下两个工作:

  • 通过实现 net_device 结构的 hard_start_xmit 方法来提供发送数据的功能。
  • 通过向内核注册硬件中断服务,来通知内核处理网卡设备接收到的数据包。

也就是说,发送数据的功能是由 net_device 结构的 hard_start_xmit 方法提供,而通知内核处理接收到的数据包的功能是由网卡的硬件中断提供的。

图1 展示了网卡接收和发送数据的过程:

 

 

1 网卡接收和发送数据过程

上图展示的是 NS8390网卡 接收和发送数据的过程(红色括号为接收过程,蓝色括号为发送过程),从上图可以发现,NS8390网卡驱动 完成了两件事情:

  • 将 net_device 结构的 hard_start_xmit 方法设置为 ei_start_xmit。
  • 向 Linux 内核注册了 ei_interrupt 硬件中断服务。

所以,当网卡接收到数据包时,会触发 ei_interrupt 中断服务来通知内核有数据包需要处理。而当需要通过网卡发送数据时,将会调用 ei_start_xmit 方法把数据发送出去。

【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!正在跳转(需要自取)

 

这里推荐大家可以看看Linux C/C++ 高级开发架构的【免费】课程:

https://ke.qq.com/course/417774?flowToken=1017067

课程内容的话和腾讯C++后台开发T8职级技术栈对标,围绕数据结构与算法、数据库、网络、操作系统、网络编程、分布式架构等方面全面提升,值得学习一波~

接收数据过程

当网卡从网络中接收到数据包后,会触发 ei_interrupt 中断服务,我们来看看 ei_interrupt 中断服务的实现:


void ei_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    struct net_device *dev = dev_id;
    long e8390_base;
    int interrupts, nr_serviced = 0;
    struct ei_device *ei_local;

    e8390_base = dev->base_addr;
    ei_local = (struct ei_device *)dev->priv;

    spin_lock(&ei_local->page_lock);
    ...
    // (1) 通过读取网卡的中断类型来进行相应的操作
    while ((interrupts = inb_p(e8390_base + EN0_ISR)) != 0 
            && ++nr_serviced < MAX_SERVICE)
    {
        ...
        // (2) 如果中断类型为接收到数据包
        if (interrupts & (ENISR_RX + ENISR_RX_ERR)) {
            ei_receive(dev); // (3) 则从网卡读取数据
        }
        ...
    }
    ...
    spin_unlock(&ei_local->page_lock);
    return;
}

上面的代码删除了很多硬件相关的操作,因为本文并不是分析网卡驱动的实现。

ei_interrupt 中断服务首先读取中断的类型,保存到 interrupts 变量中。然后判断中断类型是否为接收到数据包,如果是就调用 ei_receive 函数从网卡处读取数据。

我们继续分析 ei_receive 函数的实现:

static void ei_receive(struct net_device *dev)
{
    ...
    while (++rx_pkt_count < 10) 
    {
        int pkt_len;  // 数据包的长度
        int pkt_stat; // 数据包的状态
        ...
        if ((pkt_stat & 0x0F) == ENRSR_RXOK) { // 如果数据包状态是合法的
            struct sk_buff *skb;

            skb = dev_alloc_skb(pkt_len + 2); // 申请一个数据包对象
            if (skb) {
                skb_reserve(skb, 2);
                skb->dev = dev;         // 设置接收数据包的设备
                skb_put(skb, pkt_len);  // 增加数据的长度

                // 从网卡中读取数据(由网卡驱动实现), 并将数据保存到skb中
                ei_block_input(dev, pkt_len, skb, current_offset+sizeof(rx_frame));

                skb->protocol = eth_type_trans(skb, dev); // 从以太网头部中获取网络层协议类型

                netif_rx(skb); // 将数据包上送给内核网络协议栈
                ...
            }
        }
        ...
    }
    ...
    return;
}

ei_receive 函数主要完成以下几个工作:

  • 申请一个 sk_buff 数据包对象,并且设置其 dev 字段为接收数据包的设备。
  • 通过调用 ei_block_input 函数从网卡中读取接收到的数据,并保存到刚申请的 sk_buff 数据包对象中。ei_block_input 函数是由网卡驱动实现的,所以这里不作详细分析。
  • 通过调用 eth_type_trans 函数从数据包的以太网头部中获取网络层协议类型。
  • 调用 netif_rx 函数将数据包上送给内核网络协议栈。

当把数据包上送给内核网络协议栈后,数据包的处理就由内核接管。一般来说,内核网络协议栈会通过网络层的 IP协议 和传输层的 TCP协议 或者 UDP协议 来对数据包进行处理,处理完后就会把数据提交给应用层的进程进行处理。

发送数据过程

当网络协议栈需要通过网卡设备发送数据时,会调用 net_device 结构的 hard_start_xmit 方法,而对于 NS8390网卡 来说,hard_start_xmit 方法会被设置为 ei_start_xmit 函数。

也就是说,使用 NS8390网卡 发送数据时,最终会调用 ei_start_xmit 函数将数据发送出去。我们来看看 ei_start_xmit 函数的实现:

static int ei_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
    ...
    length = skb->len; // 数据包的长度
    ...
    disable_irq_nosync(dev->irq);      // 关闭硬件中断
    spin_lock(&ei_local->page_lock);   // 对设备进行上锁, 避免多核CPU对设备的使用
    ...
    // 使用网卡驱动的发送接口把数据发送出去,skb->data 为数据包的数据部分
    ei_block_output(dev, length, skb->data, ei_local->tx_start_page);
    ...
    spin_unlock(&ei_local->page_lock); // 对设备进行解锁
    enable_irq(dev->irq);              // 打开硬件中断
    ...
    return 0;
}

删减了硬件相关的操作后,ei_start_xmit 函数的实现就非常简单:

  • 首先关闭网卡的硬件中断,防止发送过程中受到硬件中断的干扰。
  • 调用 ei_block_output 函数把数据包的数据发送出去,此函数由网卡驱动实现,这里不作详细分析。
  • 打开网卡的硬件中断,让网卡能够继续通知内核。

总结

本文主要简单的介绍了网卡设备接收和发送数据包的过程,而网卡设备的初始化过程并没有涉及。当然网卡设备的初始化过程也非常重要,可能会在后面的文章继续分析。


作者:JaydenLie
链接:https://juejin.cn/post/6970632333397327909
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值