精华内容
下载资源
问答
  • 1,网卡驱动创建rx descriptor ring,将ring的总线地址写入网卡寄存器 2,网卡驱动为每个descriptor分配sk_buff和数据缓存区 数据接收阶段(处理首包和后续包的区别) 3,网卡接收数据包,将数据包写入RX FIFO(如何...

    RX过程
    初始化阶段(配置网卡多队列)
    1,网卡驱动创建rx descriptor ring,将ring的总线地址写入网卡寄存器
    2,网卡驱动为每个descriptor分配sk_buff和数据缓存区
    数据接收阶段(处理首包和后续包的区别)
    3,网卡接收数据包,将数据包写入RX FIFO(如何处理第一个数据包?)
    4,DMA找到rx descriptor ring中下一个需要使用的descriptor
    5,整个数据包写入RX FIFO后,DMA通过PCI总线将数据包复制到descriptor的数据缓存区
    6,复制完后,网卡启动硬件中断通知CPU数据缓存区中已经有新的数据包,CPU执行硬中断函数(硬中断处理了啥?使用NAPI轮询,减少硬件中断,调用软中断)
    7,ksoftirqd执行软中断函数(软中断处理了啥?会执行网卡驱动中的函数吗?)
    8,网卡驱动通过netif_receive_skb将sk_buff上送协议栈
    9,释放ring环中的descriptor和缓存

    注意点:
    1,ring环中存放descriptor,指向数据
    2,DMA到内存中的数据,需要软中断中的驱动函数处理成skb

    TX过程
    初始化阶段(配置网卡多队列)
    1,网卡驱动创建tx descriptor ring,将ring的总线地址写入网卡寄存器
    2,网卡驱动为每个descriptor分配sk_buff和数据缓存区
    数据发送阶段
    3,协议栈通过dev_queue_xmit将sk_buff送至网卡驱动
    4,网卡驱动将sk_buff放至tx descriptor ring,更新TDT(网卡驱动需要处理skb数据)
    5,DMA感知到TDT变化,找到tx descriptor ring中下一个需要使用的descriptor
    6,DMA通过PCI总线将数据包复制到TX FIFO
    7,复制完成后,通过MAC芯片将数据包发送出去
    8,发送完成后,网卡更新TDH,启动硬中断通知cpu释放数据缓存区中的数据包

    展开全文
  • 本文首先从宏观上介绍数据包的接收过程,然后详细介绍了Linux...加载网卡驱动,初始化 数据包从外部网络进入网卡 网卡(通过DMA)将包拷贝到内核内存中的ring buffer 产生硬件中断,通知系统收到了一个包 驱动调用 NAP

    本文首先从宏观上介绍数据包的接收过程,然后详细介绍了Linux网络设备驱动的工作过程,最后介绍网卡监控与调优,包括网络数据包总数、丢包、错包数量的相关统计。

    1. 接收数据包过程概述

    介绍数据包收包过程,有助于我们了解Linux内核网络设备在数据收包过程中的位置,下面从宏观的角度介绍数据包从被网卡接收到进入 socket 接收队列的整个过程:

    • 加载网卡驱动,初始化
    • 数据包从外部网络进入网卡
    • 网卡(通过DMA)将包拷贝到内核内存中的ring buffer
    • 产生硬件中断,通知系统收到了一个包
    • 驱动调用 NAPI ,如果轮询(poll)还没有开始,就开始轮询
    • ksoftirqd软中断调用 NAPI 的poll函数从ring buffer收包(poll 函数是网卡驱动在初始化阶段注册的;每个cpu上都运行着一个ksoftirqd进程,在系统启动期间就注册了)
    • ring buffer里面对应的内存区域解除映射(unmapped)
    • 如果 packet steering 功能打开,或者网卡有多队列,网卡收到的数据包会被分发到多个cpu
    • 数据包从队列进入协议层
    • 协议层处理数据包
    • 数据包从协议层进入相应 socket 的接收队列

    2. 网络设备初始化

    下面以常见的Intel I350 网卡的驱动 ibg 为例介绍它的工作过程:

    2.1 初始化

    驱动会使用module_init向内核注册一个初始化函数,当驱动被加载时,内核会调用这个函数。在drivers/net/ethernet/intel/igb/igb_main.c中初始化函数(igb_init_module):

    /**
     *  igb_init_module - Driver Registration Routine
     *
     *  igb_init_module is the first routine called when the driver is
     *  loaded. All it does is register with the PCI subsystem.
     **/
    static int __init igb_init_module(void)
    {
      int ret;
      pr_info("%s - version %s\n", igb_driver_string, igb_driver_version);
      pr_info("%s\n", igb_copyright);
    
      /* ... */
    
      ret = pci_register_driver(&igb_driver);
      return ret;
    }
    
    module_init(igb_init_module);
    

    初始化的大部分工作在pci_register_driver中完成。

    2.2 PCI初始化

    Intel I350 网卡是 PCI express 设备。 PCI 设备通过PCI Configuration Space 里面的寄存器识别自己。

    PCI express 总线是一种完全不同于过去PCI总线的一种全新总线规范,与PCI总线共享并行架构相比,PCI Express总线是一种点对点串行连接的设备连接方式,点对点意味着每一个PCI Express设备都拥有自己独立的数据连接,各个设备之间并发的数据传输互不影响,而对于过去PCI那种共享总线方式,PCI总线上只能有一个设备进行通信,一旦PCI总线上挂接的设备增多,每个设备的实际传输速率就会下降,性能得不到保证。PCI Express以点对点的方式处理通信,每个设备在要求传输数据的时候各自建立自己的传输通道,对于其他设备这个通道是封闭的,这样的操作保证了通道的专有性,避免其他设备的干扰。

    当设备驱动编译时,MODULE_DEVICE_TABLE 宏(定义在 include/module.h) 会导出一个 PCI 设备 ID 列表(a table of PCI device IDs),驱动据此识别它可以控制的设备,内核也会依据这个列表对不同设备加载相应驱动。

    igb 驱动的设备表和 PCI 设备 ID 分别见: drivers/net/ethernet/intel/igb/igb_main.cdrivers/net/ethernet/intel/igb/e1000_hw.h

    static DEFINE_PCI_DEVICE_TABLE(igb_pci_tbl) = {
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_1GBPS) },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_SGMII) },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_2_5GBPS) },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I211_COPPER), board_82575 },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER), board_82575 },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_FIBER), board_82575 },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES), board_82575 },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SGMII), board_82575 },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER_FLASHLESS), board_82575 },
      { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES_FLASHLESS), board_82575 },
      /* ... */
    };
    MODULE_DEVICE_TABLE(pci, igb_pci_tbl);
    

    前面提到,驱动初始化的时候会调用 pci_register_driver,这个函数会将该驱动的各种回调方法注册到一个 struct pci_driver 变量,drivers/net/ethernet/intel/igb/igb_main.c

    static struct pci_driver igb_driver = {
      .name     = igb_driver_name,
      .id_table = igb_pci_tbl,
      .probe    = igb_probe,
      .remove   = igb_remove,
      /* ... */
    };
    

    2.3 网络设备初始化

    通过 PCI ID 识别设备后,内核就会为它选择合适的驱动。每个 PCI 驱动注册了一个 probe() 方法,内核会对每个设备依次调用其驱动的 probe 方法,一旦找到一个合适的驱动,就不会再为这个设备尝试其他驱动。

    很多驱动都需要大量代码来使得设备 ready,具体做的事情各有差异。典型的过程:

    • 启用 PCI 设备
    • 请求(requesting)内存范围和 IO 端口
    • 设置 DMA 掩码
    • 注册设备驱动支持的 ethtool 方法(后面介绍)
    • 注册所需的 watchdog(例如,e1000e 有一个检测设备是否僵死的 watchdog)
    • 其他和具体设备相关的事情,例如一些 workaround,或者特定硬件的非常规处理
    • 创建、初始化和注册一个 struct net_device_ops 类型变量,这个变量包含了用于设备相关的回调函数,例如打开设备、发送数据到网络、设置 MAC 地址等
    • 创建、初始化和注册一个更高层的 struct net_device 类型变量(一个变量就代表了 一个设备)

    下面来看 igb 驱动的 igb_probe 包含哪些过程(drivers/net/ethernet/intel/igb/igb_main.c):

    err = pci_enable_device_mem(pdev);
    /* ... */
    err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    /* ... */
    err = pci_request_selected_regions(pdev, pci_select_bars(pdev,
               IORESOURCE_MEM),
               igb_driver_name);
    
    pci_enable_pcie_error_reporting(pdev);
    
    pci_set_master(pdev);
    pci_save_state(pdev);
    

    更详细的过程可以查看内核文档:
    https://github.com/torvalds/linux/blob/v3.13/Documentation/PCI/pci.txt

    3. 网络设备启动

    igb_probe 做了很多重要的设备初始化工作。除了 PCI 相关的,还有如下一些通用网络功能和网络设备相关的工作:

    • 注册 struct net_device_ops 变量
    • 注册 ethtool 相关的方法
    • 从网卡获取默认 MAC 地址
    • 设置 net_device 特性标记

    3.1 struct net_device_ops

    网络设备相关的操作函数都注册到struct net_device_ops类型的变量中(drivers/net/ethernet/intel/igb/igb_main.c):

    static const struct net_device_ops igb_netdev_ops = {
      .ndo_open               = igb_open,
      .ndo_stop               = igb_close,
      .ndo_start_xmit         = igb_xmit_frame,
      .ndo_get_stats64        = igb_get_stats64,
      .ndo_set_rx_mode        = igb_set_rx_mode,
      .ndo_set_mac_address    = igb_set_mac,
      .ndo_change_mtu         = igb_change_mtu,
      .ndo_do_ioctl           = igb_ioctl,
      /* ... */
    

    这个变量会在 igb_probe()中赋给 struct net_device 中的 netdev_ops 字段:

    static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
    {
      ...
      netdev->netdev_ops = &igb_netdev_ops;
    }
    

    3.2 ethtool 函数注册

    ethtool 是一个命令行工具,可以查看和修改网络设备的一些配置,常用于收集网卡统计数据。在 Ubuntu 上,可以 通过 apt-get install ethtool 安装,过会演示通过此工具监控网卡数据。

    ethtool 通过 ioctl 和设备驱动通信。内核实现了一个通用 ethtool 接口,网卡驱动实现这些接口,就可以被 ethtool 调用。当 ethtool 发起一个系统调用之后,内核会找到对应操作的回调函数 。回调实现了各种简单或复杂的函数,简单的如改变一个 flag 值,复杂的包括调整网卡硬件如何运行。

    相关实现见drivers/net/ethernet/intel/igb/igb_ethtool.c

    3.3 软中断

    当一个数据帧通过 DMA 写到 RAM(内存)后,网卡是如何通知其他系统这个包可以被处理了呢?

    传统的方式是,网卡会产生一个硬件中断(IRQ),通知数据包到了。有三种常见的硬中断类型:

    • MSI-X
    • MSI
    • legacy IRQ

    如果有大量的数据包到达,就会产生大量的硬件中断。CPU 忙于处理硬件中断的时候,可用于处理其他任务的时间就会减少。

    NAPI(New API)是一种新的机制,可以减少产生的硬件中断的数量(但不能完全消除硬中断 )。

    3.4 NAPI

    NAPI 接收数据包的方式和传统方式不同,它允许设备驱动注册一个 poll 方法,然后调用这个方法完成收包。

    NAPI 的使用方式:

    • 驱动打开 NAPI 功能,默认处于未工作状态(没有在收包)
    • 数据包到达,网卡通过 DMA 写到内存
    • 网卡触发一个硬中断,中断处理函数开始执行
    • 软中断(softirq),唤醒 NAPI 子系统。这会触发在一个单独的线程里, 调用驱动注册的 poll 方法收包
    • 驱动禁止网卡产生新的硬件中断,这样做是为了 NAPI 能够在收包的时候不会被新的中断打扰
    • 一旦没有包需要收了,NAPI 关闭,网卡的硬中断重新开启
    • 转步骤 2

    和传统方式相比,NAPI 一次中断会接收多个包,因此可以减少硬件中断的数量。

    poll 方法是通过调用 netif_napi_add 注册到 NAPI 的,同时还可以指定权重 weight,大部分驱动都 hardcode 为 64。

    通常来说,驱动在初始化的时候注册 NAPI poll 方法。

    3.5 igb 驱动的 NAPI 初始化

    igb 驱动的初始化过程是一个很长的调用链:

    • igb_probe -> igb_sw_init
    • igb_sw_init -> igb_init_interrupt_scheme
    • igb_init_interrupt_scheme -> igb_alloc_q_vectors
    • igb_alloc_q_vectors -> igb_alloc_q_vector
    • igb_alloc_q_vector -> netif_napi_add

    从宏观角度来看,这个调用过程会做以下事情:

    • 如果支持 MSI-X,调用 pci_enable_msix 打开它
    • 计算和初始化一些配置,包括网卡收发队列的数量
    • 调用 igb_alloc_q_vector 创建每个发送和接收队列
    • igb_alloc_q_vector 会进一步调用 netif_napi_add 注册 poll 方法到 NAPI 变量

    下面介绍 igb_alloc_q_vector 是如何注册 poll 方法和私有数据的(drivers/net/ethernet/intel/igb/igb_main.c):

    static int igb_alloc_q_vector(struct igb_adapter *adapter,
                                  int v_count, int v_idx,
                                  int txr_count, int txr_idx,
                                  int rxr_count, int rxr_idx)
    {
      /* ... */
    
      /* allocate q_vector and rings */
      q_vector = kzalloc(size, GFP_KERNEL);
      if (!q_vector)
              return -ENOMEM;
    
      /* initialize NAPI */
      netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
    
      /* ... */
    

    q_vector 是新分配的队列,igb_poll 是 poll 方法,当它收包的时候,会通过这个接收队列找到关联的 NAPI 变量(q_vector->napi)。

    4. 启用网卡(Bring A Network Device Up)

    前面提到structure net_device_ops 变量,它包含网卡启用、发包、设置 mac 地址等回调函数(函数指针)。

    当启用一个网卡时(例如,通过 ifconfig eth0 up),net_device_opsndo_open 方法会被调用。它通常会做以下事情:

    • 分配 RX、TX 队列内存
    • 打开 NAPI 功能
    • 注册中断处理函数
    • 打开(enable)硬中断
    • 其他

    igb 驱动中,这个方法对应的是 igb_open 函数。

    4.1 准备从网络接收数据

    目前大部分网卡都使用 DMA 将数据直接写到内存,接下来操作系统可以直接从里面读取。实现这一目的所使用的数据结构是 ring buffer(环形缓冲区)。

    要实现这一功能,设备驱动必须和操作系统合作,预留(reserve)出一段内存来给网卡使用。预留成功后,网卡知道了这块内存的地址,接下来收到的数据包就会放到这里,进而被操作系统取走。

    由于这块内存区域是有限的,如果数据包的速率非常快,单个 CPU 来不及取走这些包,新来的包就会被丢弃。这时候,Receive Side Scaling(RSS,接收端扩展)或者多队列( multiqueue)一类的技术可能就会排上用场。

    一些网卡有能力将接收到的数据包写到多个不同的内存区域,每个区域都是独立的接收队列。这样操作系统就可以利用多个 CPU(硬件层面)并行处理收到的数据包。只有部分网卡支持这个功能。

    Intel I350 网卡支持多队列,我们可以在 igb 的驱动里看出来。igb 驱动启用的时候 ,最开始做的事情之一就是调用 igb_setup_all_rx_resources 函数。这个函数会对每个 RX 队列调用 igb_setup_rx_resources, 里面会管理 DMA 的内存。

    RX 队列的数量和大小可以通过 ethtool 进行配置,调整这两个参数会对收包或者丢包产生可见影响。

    网卡通过对 packet 头(例如源地址、目的地址、端口等)做哈希来决定将 packet 放到哪个 RX 队列。只有很少的网卡支持调整哈希算法。如果支持的话,可以根据算法将特定 的 flow 发到特定的队列,甚至可以做到在硬件层面直接将某些包丢弃

    一些网卡支持调整 RX 队列的权重,可以有意地将更多的流量发到指定的 queue。

    4.2 Enable NAPI

    前面介绍了驱动如何注册 NAPI poll 方法,但是,一般直到网卡被启用之后,NAPI 才被启用。

    启用 NAPI 很简单,调用 napi_enable 函数就行,这个函数会设置 NAPI 变量(struct napi_struct)中一个表示是否启用的标志位。前面说到,NAPI 启用后并不是立即开始工作(而是等硬中断触发)。

    对于 igb,驱动初始化或者通过 ethtool 修改 queue 数量或大小的时候,会启用每个 q_vector 的 NAPI 变量( drivers/net/ethernet/intel/igb/igb_main.c):

    for (i = 0; i < adapter->num_q_vectors; i++)
    	napi_enable(&(adapter->q_vector[i]->napi));
    

    4.3 注册中断处理函数

    启用 NAPI 之后,下一步就是注册中断处理函数。设备有多种方式触发一个中断:

    • MSI-X
    • MSI
    • legacy interrupts

    设备驱动的实现也因此而异。驱动必须判断出设备支持哪种中断方式,然后注册相应的中断处理函数,这些函数在中断发生的时候会被执行。

    一些驱动,例如 igb,会试图为每种中断类型注册一个中断处理函数,如果注册失败,就尝试下一种类型。

    MSI-X 中断是比较推荐的方式,尤其是对于支持多队列的网卡。因为每个 RX 队列有独立的 MSI-X 中断,因此可以被不同的 CPU 处理(通过 irqbalance 方式,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity)。处理中断的 CPU 也是随后处理这个包的 CPU。这样的话,从网卡硬件中断的层面就可以设置让收到的包被不同的 CPU 处理。

    如果不支持 MSI-X,那 MSI 相比于传统中断方式仍然有一些优势,驱动仍然会优先考虑它。

    在 igb 驱动中,函数 igb_msix_ringigb_intr_msiigb_intr 分别是 MSI-X,MSI 和传统中断方式的中断处理函数。

    驱动是如何尝试各种中断类型的( drivers/net/ethernet/intel/igb/igb_main.c):

    static int igb_request_irq(struct igb_adapter *adapter)
    {
      struct net_device *netdev = adapter->netdev;
      struct pci_dev *pdev = adapter->pdev;
      int err = 0;
    
      if (adapter->msix_entries) {
        err = igb_request_msix(adapter);
        if (!err)
          goto request_done;
        /* fall back to MSI */
        /* ... */
      }
    
      /* ... */
    
      if (adapter->flags & IGB_FLAG_HAS_MSI) {
        err = request_irq(pdev->irq, igb_intr_msi, 0,
              netdev->name, adapter);
        if (!err)
          goto request_done;
    
        /* fall back to legacy interrupts */
        /* ... */
      }
    
      err = request_irq(pdev->irq, igb_intr, IRQF_SHARED,
            netdev->name, adapter);
    
      if (err)
        dev_err(&pdev->dev, "Error %d getting interrupt\n", err);
    
    request_done:
      return err;
    }
    

    这就是 igb 驱动注册中断处理函数的过程,这个函数在一个数据包到达网卡触发一个硬件中断时就会被执行。

    4.4 Enable Interrupts

    到这里,几乎所有的准备工作都就绪了。唯一剩下的就是打开硬中断,等待数据包进来。 打开硬中断的方式因硬件而异,igb 驱动是在 __igb_open 里调用辅助函数 igb_irq_enable 完成的。

    中断通过写寄存器的方式打开:

    static void igb_irq_enable(struct igb_adapter *adapter)
    {
    
      /* ... */
        wr32(E1000_IMS, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
        wr32(E1000_IAM, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
      /* ... */
    }
    
    

    现在,网卡已经启用了。驱动可能还会做一些额外的事情,例如启动定时器,工作队列( work queue),或者其他硬件相关的设置。这些工作做完后,网卡就可以接收数据包了。

    5. 网卡监控

    监控网络设备有几种不同的方式,每种方式的监控粒度(granularity)和复杂度不同。我们先从最粗的粒度开始,逐步细化。

    5.1 ethtool -S

    ethtool -S 可以查看网卡统计信息(例如接收和发送的数据包总数,接收和发送的流量,丢弃的包数量,错误的数据包数量等):
    在这里插入图片描述
    监控这些数据比较困难。因为用命令行获取很容易,但是以上字段并没有一个统一的标准。 不同的驱动,甚至同一驱动的不同版本可能字段都会有差异。

    可以先粗略的查看 “drop”, “buffer”, “miss” 等字样。然后,在驱动的源码里找到对应的更新这些字段的地方,这可能是在软件层面更新的,也有可能是在硬件层面通过寄存器更新的。如果是通过硬件寄存器的方式,就得查看网卡的 data sheet(说明书),搞清楚这个寄存器代表什么。ethtoool 给出的这些字段名,有一些是有误导性的(misleading)。

    5.2 sysfs

    sysfs 也提供了统计信息,但相比于网卡层的统计,要更上层一些。

    例如,可以获取的 ens33 的接收端数据包的类型有这些:
    在这里插入图片描述
    获取接收到的数据包的总数为:
    在这里插入图片描述
    不同类型的统计分别位于 /sys/class/net/<NIC>/statistics/ 下面的不同文件,包括 collisions, rx_dropped,rx_errors, rx_missed_errors 等等。

    要注意的是,每种类型代表什么意思,是由驱动来决定的,因此也是由驱动决定何时以及在哪里更新这些计数的。你可能会发现一些驱动将一些特定类型的错误归类为 drop,而另外一些驱动可能将它们归类为 miss。

    这些值至关重要,因此需要查看对应的网卡驱动,搞清楚它们真正代表什么。

    5.2 /proc/net/dev

    /proc/net/dev 提供了更高一层的网卡统计。
    在这里插入图片描述
    这个文件里显示的统计只是 sysfs 里面的一个子集,但适合作为一个常规的统计参考。

    如果对这些数据准确度要求特别高,那必须查看内核源码 、驱动源码和驱动手册,搞清楚每个字段真正代表什么意思,计数是如何以及何时被更新的。

    Linux内核网络设备驱动先介绍到这里,关于网卡调优,下次再介绍。

    参考链接:
    https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/

    展开全文
  • 功能:通过AD9361芯片实现无线组网,能实现视频、文件、音频等传输,其基本原理是在Linux内核层添加一个网卡设备,进行网络包的传输,其过程和真实的网卡一致,通过MAC层从物理设备收发上次协议栈的数据。...

    声明:文中若有不合理的地方,欢迎讨论学习及指正,本文仅仅涉及软件部分的代码,不阐述逻辑代码的实现。

    功能:通过AD9361芯片实现无线组网,能实现视频、文件、音频等传输(当然承载量不能太大,由于逻辑实现采用时分的方法收发包,故只能实现最大约7Mbit/s的传输速率,此方式的弊端在于参与组网的设备越多,则传输的速率越慢,实际该方式有很多可优化的地方),其基本原理是在Linux内核层添加一个网卡设备,进行网络包的传输,其过程和真实的网卡一致,通过MAC层从物理设备收发上次协议栈的数据。

    整体框架:

    程序分析:

    • 分配接收缓冲区

    缓冲区用于接收数据,防止数据突发量太大无法处理,分配函数实现如下:

    ringBuffer = ringbuffer_malloc();

     

    • 注册一个网络设备

    上边初始化了网口的MAC地址以及操作函数,结构体定义如下:


    接着看一下ad9361_net_dev.net_dev->netdev_ops = &ad9361_netdev_ops;里的各部分操作接口(比较简单,没有实现太多接口),如下:

    实际上在上述网卡注册的过程中还应该包含一项初始化,可以看一下内核里其他网卡设备的初始化过程,如下:

    其中ethtool_ops这一项在这里并没有设置,ethtool_ops的成员函数如下:

    包括对一些信息的获取以及其他参数的设置,这里没有用到,所以程序里没有进行初始化,建议对该结构体进行初始化。回到上面说到的netdev_ops,程序中对该部分实现的比较简单,仅有几个必要的函数,其中的open和close也不一定需要,如果要做的更加规范,建议把通用的接口全部实现,open函数里一般是包含缓冲区的初始化、中断申请、添加NAPI等操作,close则是释放这些资源,看一下上边的传输函数,如下:

    static netdev_tx_t ad9361net_start_xmit(struct sk_buff *skb, struct net_device *dev)
    
    {
    
        /* stop queue */
    
        netif_stop_queue(dev);         
    
        send_eth_frame(&ad9361_net_dev,(u8 *)skb->data,skb->len,cnt,vct);
    
        dev_kfree_skb(skb); 
    
        netif_wake_queue(dev);
    
        dev->stats.tx_packets++;
    
        dev->stats.tx_bytes += skb->len;
    
        return NETDEV_TX_OK;
    
    }

    上边实现得比较简洁。下边看一下MAC地址设置函数,如下:

    网络设备相关的函数就到此结束。

    • Mac list初始化

    该部分由于特殊需要,实现维护一张类ARP的表,功能与ARP类似,只是成员不同,不过多阐述。

    • Netlink初始化

    通过netlink实现应用程序对FPGA寄存器的配置,此部分比较简单,不做描述。

    • DMA初始化

    DMA初始化部分参见本人DMA相关文章,在此不做描述。

    • NAPI初始化

    NAPI是网络部分使用的一种新的机制,以前的接口要么使用轮询要么中断,在处理函数中调用netif_receive_skb等接口向上层协议栈上报,这样就存在一些问题,要么CPU一直不断轮询无论数据有没有到来都耗费CPU的时间,要么中断触发后在中断中处理,这样会导致CPU频繁被中断同样也会耗费CPU时间,且中断触发较为频繁时,效率不如轮询,那么NAPI就是两者的结合,使用中断的同时也使用轮询,数据量低时采用中断,反之采用轮询。下面看一下程序实现,如下:

    首先是初始化:

    netif_napi_add(ad9361_net_dev.net_dev, &ad9361_net_dev.napi, ad9361_rx_poll, 16);
    
    napi_enable(&ad9361_net_dev.napi);

    netif_napi_add函数有四个参数,第一个是net_dev结构体指针,第二个是napi的结构体指针,第三个是poll时使用的函数,第四个每次poll能处理的包个数。

    接着看一下ad9361_rx_poll函数,由于函数里内容比较多,这里精简列出:

    static int ad9361_rx_poll(struct napi_struct *napi, int budget)
    {
        int rx = 0;
        while (rx < budget) {
            rx++;
            /*数据读取*/
            ……………………
            /*上报数据*/
            napi_gro_receive(napi, skb);
        }
        if (rx < budget) {
            napi_gro_flush(napi, false);
            napi_complete_done(napi, rx);
            /*enable interrupt*/
            *(ad9361_net_dev.axi_intc + IER_OFFSET) = 0xff;
        }
        return rx;
    }

             为了代码的整洁方便分析,这里进行了删减,但程序以及经过验证,保证可靠。

             中断处理函数如下:

    static irqreturn_t ad9361_mac_irq_handler(int irq, void *data)
    {
        struct ad9361_net *dev = (struct ad9361_net *)data;
        if(!dev){
              dev = &ad9361_net_dev;
        }
        /*napi*/
        if (napi_schedule_prep(&ad9361_net_dev.napi)){
              /*disable interrupt*/
              *(ad9361_net_dev.axi_intc + IER_OFFSET) = 0x0;
              __napi_schedule(&ad9361_net_dev.napi);
        }
        return IRQ_HANDLED;
    }

    在中断被触发之后,进入中断处理函数,如何调度?首先执行napi_schedule_prep判断能否进行调度,参见函数描述,如下:

    如果能进行调度,则执行__napi_schedule函数,函数如下:

    首先函数会保存当前中断的状态,然后调用____napi_schedule函数,最后恢复中断的状态,____napi_schedule函数如下:

    首先将我们初始化的napi结构体加入当前CPU的poll_lost链表用于轮询,随后设置NET_RX_SOFTIRQ触发软中断。

    上述就是NAPI使用的详细代码,可以根据自己的需求进行更改。

    • 映射寄存器

    这部分代码对FPGA的相关寄存器建立映射,方便用于程序配置相关参数,不做描述。

    • Axi interrupt初始化

    初始化代码如下,比较简单,不做描述,参见xilinx官方手册的描述

    • 中断申请

    err = request_irq(ad9361_net_dev.irq, ad9361_mac_irq_handler,  IRQF_TRIGGER_HIGH, "ad9361_net", &ad9361_net_dev);

    • 配置FPGA参数

    略。

    文中若有地方存在不妥的,欢迎交流,软件实现很简单,分享学习。

    最终实现了最大13.4Mb/s即1.5MB/s左右的速率

    展开全文
  • 本文用ixgbe网卡驱动作为研究对象,linux版本是3.9.4 基础知识: ixgbe_adapter /* board specific private data structure */ struct ixgbe_adapter { //数据量太多,摘录部分看过比较有用的 //发送的rings ...

    本文用ixgbe网卡驱动作为研究对象,linux版本是3.9.4


    基础知识:

    ixgbe_adapter

    /* board specific private data structure */
    struct ixgbe_adapter {

    //数据量太多,摘录部分看过比较有用的

    //发送的rings

    struct ixgbe_ring *tx_ring[MAX_TX_QUEUES] ____cacheline_aligned_in_smp;

    //接收的rings

    struct ixgbe_ring *rx_ring[MAX_RX_QUEUES];

    //这个vector里面包含了napi结构,不知道如何下定义这个q_vector

    //应该是跟下面的entries一一对应起来做为是一个中断向量的东西吧

    struct ixgbe_q_vector *q_vector[MAX_Q_VECTORS];

    //这个里面估计是MSIX的多个中断对应的响应接口

    struct msix_entry *msix_entries;

    }


    ixgbe_q_vector

    struct ixgbe_q_vector {
            struct ixgbe_adapter *adapter;
    ifdef CONFIG_IXGBE_DCA
            int cpu;            /* CPU for DCA */
    #endif
            u16 v_idx;              /* index of q_vector within array, also used for
                                     * finding the bit in EICR and friends that
                                     * represents the vector for this ring */
            u16 itr;                /* Interrupt throttle rate written to EITR */
            struct ixgbe_ring_container rx, tx;

            struct napi_struct napi;//这个poll的接口实现是ixgbe_poll
           cpumask_t affinity_mask;
            int numa_node;
            struct rcu_head rcu;    /* to avoid race with update stats on free */
            char name[IFNAMSIZ + 9];

            /* for dynamic allocation of rings associated with this q_vector */
            struct ixgbe_ring ring[0] ____cacheline_internodealigned_in_smp;
    };


    softnet_data

    /*
     * Incoming packets are placed on per-cpu queues
     */
    struct softnet_data {
        struct Qdisc        *output_queue;
        struct Qdisc        **output_queue_tailp;
        struct list_head    poll_list;
        struct sk_buff      *completion_queue;
        struct sk_buff_head process_queue;

        /* stats */
        unsigned int        processed;
        unsigned int        time_squeeze;
        unsigned int        cpu_collision;
        unsigned int        received_rps;

    #ifdef CONFIG_RPS
        struct softnet_data *rps_ipi_list;

        /* Elements below can be accessed between CPUs for RPS */
        struct call_single_data csd ____cacheline_aligned_in_smp;
        struct softnet_data *rps_ipi_next;
        unsigned int        cpu;    
        unsigned int        input_queue_head;
        unsigned int        input_queue_tail;
    #endif
        unsigned int        dropped;
        struct sk_buff_head input_pkt_queue;
        struct napi_struct  backlog;//cpu softnet_data的poll接口是process_backlog
    };


    napi_struct

    /*
     * Structure for NAPI scheduling similar to tasklet but with weighting
     */
    struct napi_struct {
        /* The poll_list must only be managed by the entity which
         * changes the state of the NAPI_STATE_SCHED bit.  This means
         * whoever atomically sets that bit can add this napi_struct
         * to the per-cpu poll_list, and whoever clears that bit
         * can remove from the list right before clearing the bit.
         */
        struct list_head    poll_list;

        unsigned long       state;
        int         weight;
        unsigned int        gro_count;
        int         (*poll)(struct napi_struct *, int);//poll的接口实现
    #ifdef CONFIG_NETPOLL
        spinlock_t      poll_lock;
        int         poll_owner;
    #endif
        struct net_device   *dev;
        struct sk_buff      *gro_list;
        struct sk_buff      *skb;
        struct list_head    dev_list;
    };

    enum {
        NAPI_STATE_SCHED,   /* Poll is scheduled */
        NAPI_STATE_DISABLE, /* Disable pending */
        NAPI_STATE_NPSVC,   /* Netpoll - don't dequeue from poll_list */
    };



    1.......................................................

    文件 ixgbe_main.c
    ixgbe_init_module注册一个ixgbe_driver的pci结构,其中我们关注ixgbe_probe接口,这个是网卡probe时的实现


    2.......................................................

    文件 ixgbe_main.c
    ixgbe_probe这里关注3个事情

    1.创建ixgbe_adapter的adapter结构,这个结果是网卡的一个实例,包含了网卡的所有数据及接口,包含了下面要

       创建的netdev结构,还包含了每个中断号的响应数据结构接口(网卡是支持MSIX的),里面有一个叫q_vector的

       数组,是ixbge_q_vector数据类型的(这个应该是一个中断向量的数据类型),每个元素是一个ixgbe_q_vector类型,

       这个类似的数据结构包含了几样重要的东西,其中一样是napi_struct类型的成员napi,这个就包含了包含了ixgbe_poll

       接口,主要是上层软中断调用的轮询接口;除此adapter还包含了一个重要的成员变量,struct msix_entry *msix_entries;

       这个应该是MSIX没个通道对应的中断向量结构,下面的步骤4会描述

    2.创建一个netdev结构net_device,net_device在linux里面代表是一个网络设备,然后绑定里面的netdev_ops接口

       对应ixgbe_netdev_ops,这个结构有个响应网卡open的回调实现,ixgbe_open,按照open接口的描述

       "Called when a network interface is made active" 网卡激活的时候被调用的,之前的probe接口是检测时被调用

    3.ixgbe_init_interrupt_scheme这个函数主要是设置网卡的napi_struct结构的poll接口,这个poll接口实现是ixgbe_poll,

      这个接口是面向内核层的设备poll包装,这里我们要区分清楚,cpu的softnet_data的napi结构poll的接口实现是

       process_backlog,然而网卡ixgbe_q_vector里面napi结构poll的接口实现是ixgbe_poll,这个最终会在下面是否是

       NAPI的调用方式中体现出来,参考步骤9

    
    


    3.......................................................

    文件 ixgbe_main.c
    ixgbe_open这里主要有两个任务

    1.根据queue初始化对应的ring buffer

       ixgbe_setup_all_tx_resources(adapter); //设置发送队列的rings等等
       ixgbe_setup_all_rx_resources(adapter); //设置接收队列的rings等等

    2. ixgbe_request_irq根据中断类型设置中断响应机制(MSI/MSIX或者其它)一般好点的网卡都是支持MSIX的,

       所以我们看里面的ixgbe_request_msix_irqs这个函数的实现


    4.......................................................

    文件 ixgbe_main.c
    ixgbe_request_msix_irqs函数,因为MSIX是一个队列对应一个中断号,这里主要是对每个队列设置对应的中断响应接口,

    (这里主要还是对adapter的q_vector进行设置,参考上面步骤2的第一点)

    对应的接口是ixgbe_msix_clean_rings,这个函数会调用request_irq设置每个通道的硬中断实现为ixgbe_msix_clean_rings,

    具体部分代码节选如下

    for (vector = 0; vector < adapter->num_q_vectors; vector++) {

      ...........

      struct ixgbe_q_vector *q_vector = adapter->q_vector[vector];
      struct msix_entry *entry = &adapter->msix_entries[vector];
      err = request_irq(entry->vector, &ixgbe_msix_clean_rings, 0,q_vector->name, q_vector);

      ...........

    }

    主要是把q_vector的中断向量实现接口与MSIX的一一对应起来

    除了设置中断响应接口,这个函数还有设置IRQ的CPU affinity



    5.......................................................

    文件 ixgbe_main.c
    ixgbe_msix_clean_rings这个函数就是网卡接收,发送数据时的中断响应接口,里面没做太多东西调用napi_schedule接口,

    这里面的napi schedule有点折腾,要慢慢追踪下去,

    static irqreturn_t ixgbe_msix_clean_rings(int irq, void *data)

    {

    struct ixgbe_q_vector *q_vector = data;

    if (q_vector->rx.ring || q_vector->tx.ring)

      napi_schedule(&q_vector->napi);//这里要注意,传进去的是qvector的napi结构,到时候调用的poll接口是ixgbe_poll

    }

    static inline void napi_schedule(struct napi_struct *n)
    {
    if (napi_schedule_prep(n))
      __napi_schedule(n);
    }

    void __napi_schedule(struct napi_struct *n)
    {
    unsigned long flags;
    local_irq_save(flags);
    ____napi_schedule(&__get_cpu_var(softnet_data), n);

    //这里我们看到,把adapter的q_vector的napi结构放到当前运行cpu的napi结构的poll list队列去

    //__get_cpu_var这个宏按照网上找到的信息,这个是获取当前运行CPU的softnet_data

    //最终会有下面raise一个NET_RX_SOFTIRQ软中断叫CPU去处理自己的softnet_data里面的napi

    //队列

    local_irq_restore(flags);
    }

    ____napi_schedule(struct softnet_data *sd,struct napi_struct *napi)

    {

    list_add_tail(&napi->poll_list, &sd->poll_list);

    __raise_softirq_irqoff(NET_RX_SOFTIRQ);

    }

    当软中断响应后,数据包就从驱动层上升到内核层面的逻辑了,(注意前面的下划线个数,因为个数都有好几个的)


    6.......................................................

    文件 net/core/dev.c
    最终napi_schedule会调用__raise_softirq_irqoff去触发一个软中断NET_RX_SOFTIRQ,然后又对应的软中断接口去实现往
    上的协议栈逻辑
    NET_RX_SOFTIRQ是收到数据包的软中断信号对应的接口是net_rx_action
    NET_TX_SOFTIRQ是发送完数据包后的软中断信号对应的接口是net_tx_action


    7.......................................................

    文件 net/core/dev.c
    net_rx_action主要是获取到cpu的softnet_data结构,然后开始迭代对softnet_Data里面的queue队列中的网络数据包进行处理,

    这里会调用对应设备的napi_struct数据结构里面的poll接口。这个接口在ixgbe_probe里面设置好了,对应是ixgbe_poll函数。

    net_rx_action函数会对目前设备是否已经处于NAPI_STATE_SCHED才会调用ixgbe_poll接口。

    具体看看net_rx_action的代码节选

    static void net_rx_action(struct softirq_action *h)

    {

    //这个是NAPI的工作方式函数,NAPI工作模式下关闭硬中断,在2个时钟周期内poll收到预期数据包budget个数

    //否则重新开启硬中断,跳出poll轮询

    unsigned long time_limit = jiffies + 2;
    int budget = netdev_budget;


    struct softnet_data *sd = &__get_cpu_var(softnet_data);

    local_irq_disable();

    while (!list_empty(&sd->poll_list)) {
        struct napi_struct *n;
        int work, weight;

        ......

    /* If softirq window is exhuasted then punt.
        * Allow this to run for 2 jiffies since which will allow
        * an average latency of 1.5/HZ.
        */

    //在指定2个时钟周期内收到budget个数据包,否则跳出poll轮询,这就是NAPI的工作方式

    //减少中断调用次数获取数据包

    if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
        goto softnet_break;
    local_irq_enable();

        ......

        //获取当前CPU的softnet_data中napi的poll list列表

        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

        if (test_bit(NAPI_STATE_SCHED, &n->state)) {

            work = n->poll(n, weight);//然后就开始调用napi_struct结构的poll接口,这个接口的实现就是上面步骤2的第三小点描述的

            //其实这里的poll调用就是调用了ixgbe_poll

        }

        ......

    }


    8.......................................................

    文件 ixgbe_main.c
    ixgbe_poll函数主要做3个事情
    1.对tx发送数据包队列进行处理 ixgbe_clean_tx_irq
    2.对rx接收数据包队列进行处理 ixgbe_clean_rx_irq
    3.最后通知napi_complete


    9.......................................................

    文件 ixgbe_main.c
    ixgbe_clean_rx_irq函数把ring buffer的内容取出来转成sk_buff包,然后提交到ixgbe_rx_skb,ixgbe_rx_skb会根据

    当前是否是NETPOLL工作模式来区分调用接口,NETPOLL是一种用于调试的工作模式,我们暂不深入研究

    napi_gro_receive会往上调用napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb),然而这个传进来的napi

    结构正是qvector的结构,poll接口是ixgbe_poll实现的

    部分代码节选如下

    static void ixgbe_rx_skb(struct ixgbe_q_vector *q_vector, struct sk_buff *skb)
    {
            struct ixgbe_adapter *adapter = q_vector->adapter;

            if (!(adapter->flags & IXGBE_FLAG_IN_NETPOLL))
                    napi_gro_receive(&q_vector->napi, skb);
            else
                    netif_rx(skb); //这里是NETPOLL的方式,
    }



    10.......................................................

    文件 net/core/dev.c

    netif_receive_skb接口在新版内核里面做了两件事情
    1.google的RPS机制会在这里调度CPU

    2.往上层调用接口,从__netif_receive_skb_core再调用到deliver_skb接口,这个deliver_skb接口会调用一个叫packet_type数据

    结构里面的func接口,这个是数据包接收下层提交到协议栈的接口,数据也就由此进入了TCP/IP协议栈了



    11.......................................................

    文件 net/ipv4/af_inet.c ip_input.c
    这个是ipv4的AF_INET协议模块,在inet_init初始化时会注册一个ip_packet_type的数据结构,这个结果就是packet_type类型的,

    里面指定了数据接收的函数func回调接口的实现ip_rcv。除此之外还注册了2个重要的net_protocol的协议接口tcp_protocol,udp_protocol,这个后面的ip_local_deliver会用到

    ip_rcv的函数就开始了TCP/IP协议栈的工作流程,这里主要是做一些IP包的分析,最后调用netfilter的PRE_ROUTING hook,

    通知完netfilter的hook之后,假如这个包ACCEPT的话会调用ip_rcv_finish


    12.......................................................

    文件 net/ipv4/ip_input.c route.c
    这个ip_rcv_finish函数它会调用ip_route_input_noref,最后就会调用一个dst_input,然后这个dst_input其实是调用一个input的

    函数指针,这个数据包的input函数指针则是接下来往上层的路径入口,这个input函数指针在那里设置,就是在之前

    ip_route_input_noref调用里面设置好了。如果是本机数据则input接口为ip_local_deliver


    13.......................................................

    文件 net/ipv4/ip_input.c af_inet.c
    ip_local_deliver首先会调用netfilter的NF_INET_LOCAL_IN, 如果没问题则调用下一个 ip_local_deliver_finish,这里会获取该IP

    数据包的协议结构net_protocol,然后调用net_protocol里面的handler接口

    net_protocol在之前的af_inet.c里面的inet_init已经注册好了

    tcp协议对应的handler为tcp_v4_rcv
    udp协议对应的handler为udp_rcv


    14.......................................................

    文件 net/ipv4/udp.c
    udp_rcv将UDP包放到sock结构sk里面的sk_receive_queue的接收队列里面


    15.......................................................

    文件 net/ipv4/udp.c net/core/datagram.c
    UDP为应用层提供了一个叫udp_prto的数据结构,类型是proto,里面提供了各种接口,如发送,接收等等recvmsg接口对应

    UDP的实现是udp_recvmsg,这个函数会调用__skb_recv_datagram接口,这个函数就是从sock结构sk里面的

    sk->sk_receive_queue取数据



    展开全文
  • PF_RING浅析

    千次阅读 2014-05-19 19:47:42
    PF_RING架构 PF_RING的主要框架包括如下几部分: ...特殊定制的PF_RING相关的网卡驱动网卡驱动不通过linux内核任何的数据结构一大到进一步加强数据包的抓取效率的目的。PF_RING可以与任何NIC驱动程序
  • PF_RING源码安装包

    2014-11-13 16:46:29
    用于加快捕获包的速度,比linux系统自带的驱动好一些,无需相关网卡驱动
  • linux 内核报错解决

    2019-05-10 14:36:16
    insmod: ERROR: could not insert module pf_ring.ko: File exists报该错误...[root@h187 kernel]# rmmod pf_ringrmmod: ERROR: Module pf_ring is in use by: ixgbe说明pf_ring 已经在万兆网卡驱动上调用了。 [root...
  • pf_ring有三种透明模式(transparent_mode),为0时走的是Linux标准的NAPI包处理流程。为1时,包既走Linux标准包处理流程,也copy给pf_...(1)通用网卡驱动对于通用网卡驱动,只有transparent_mode=0有效。从这里...
  • 网卡参数查询及设置工具ethtool

    千次阅读 2017-01-17 10:50:17
    ethtool是用来查询和设置网卡驱动&硬件信息的工具,功能很强大!Linux一般都会默认安装,在进行网络调试和网络性能分析时非常有用。提供统计信息查询、ring buffer设置、协议卸载设置、网卡测试等等…… 自己在...
  • Linux 丢包分析

    2020-12-01 10:08:13
    网络驱动程序会把网络中的报文读出来放到 ring buffer 中,这个过程使用 DMA(Direct Memory Access),不需要 CPU 参与 内核从 ring buffer 中读取报文进行处理,执行 IP 和 TCP/UDP 层的逻辑,最后把报文放到...
  • 1. 传统linux网络协议栈流程和性能分析 Linux网络协议栈是处理网络数据包的典型系统,它包含了从物理层直到应用层的全过程。...驱动软件从ring buffer中读取,填充内核skbuff结构(第2次拷贝:内核网卡缓冲区ring...
  • linux 系统 UDP 丢包

    2018-10-20 17:28:02
    ● 首先网络报文通过物理网线发送到网卡● 网络驱动程序会把网络中的报文读出来放到 ring buffer 中,这个过程使用 DMA(Direct Memory Access),不需要 CPU 参与● 内核从 ring buffer 中读取报文进行处理,执行 ...
  • 我们使用Linux作为服务器操作系统时,为了达到高并发处理能力,充分利用机器性能,经常会进行一些内核参数的调整优化,但不合理的调整... 网络驱动程序会把网络中的报文读出来放到 ring buffer 中,这个过程使用 DMA
  • 最近工作中遇到某个服务器应用程序 UDP 丢包,在排查过程中查阅了很多...网络驱动程序会把网络中的报文读出来放到 ring buffer 中,这个过程使用 DMA(Direct Memory Access),不需要 CPU 参与 内核从 ring buff...
  • Linux 网络性能tuning向导

    千次阅读 2016-12-07 16:31:57
    本文的目的不完全在于提供调优信息,而是在于告诉读者了解Linux kernel如何处理数据包,从而能够在自己的实践中发挥Linux 内核协议栈最大的性能 The NIC ring buffer 接收环缓冲区在设备驱动程序和NIC之间共享。 ...
  • 转自:http://cizixs.com/2018/01/13/linux-udp-packet-drop-debug 最近工作中遇到某个服务器应用程序 UDP 丢包,在排查过程中查阅了很多资料,总结出来这篇... 网络驱动程序会把网络中的报文读出来放到 ring ...
  • 网络驱动程序会把网络中的报文读出来放到 ring buffer 中,这个过程使用 DMA(Direct Memory Access),不需要 CPU 参与 内核从 ring buffer 中读取报文进行处理,执行 IP 和 TCP/UDP 层的逻辑,最后把报文放到应用...
  • 最近工作中遇到某个服务器应用程序 ...●首先网络报文通过物理网线发送到网卡●网络驱动程序会把网络中的报文读出来放到 ring buffer 中,这个过程使用 DMA(Direct Memory Access),不需要 CPU 参与●内核从 ring ...
  • XDP 位于网卡驱动层,当数据包经过 DMA 存放到 ring buffer 之后,分配 skb 之前,即可被 XDP 处理。由于 XDP 位于整个 Linux 内核网络软件栈的底部,能够非常早地识别并丢弃攻击报文,具有很高的性能。这为我们改善...
  • Linux从入门到精通

    2010-04-25 19:58:09
    E.6.11 在安装时, Linux无法工作在我的Ultra DMA IDE驱动器和主机板上, 怎么办? E.6.12 我的机器有一个PCI Ultra DMA 控制器. 我可以安装Linux吗? E.6.13 我有NT, 并想安装Linux, 但我听说启动多操作系统会出现...
  • E.6.11 在安装时, Linux无法工作在我的Ultra DMA IDE驱动器和主机板上, 怎么办? E.6.12 我的机器有一个PCI Ultra DMA 控制器. 我可以安装Linux吗? E.6.13 我有NT, 并想安装Linux, 但我听说启动多操作系统会出现...
  • packet通过DMA被拷贝到内核中的一个ring buffer 产生一个硬件中断,让系统知道已经有个packet到达内存 驱动会调用NAPI启动一个poll loop,如果它还没启动的话 系统的每个CPU上都有一个ksoftirqd进程,它...
  • linux从入门到精通.chm

    2010-05-17 09:11:20
    E.6.11 在安装时, Linux无法工作在我的Ultra DMA IDE驱动器和主机板上, 怎么办? E.6.12 我的机器有一个PCI Ultra DMA 控制器. 我可以安装Linux吗? E.6.13 我有NT, 并想安装Linux, 但我听说启动多操作系统会出现...
  • Linux从入门到精通》

    热门讨论 2008-09-04 17:05:49
    E.6.11 在安装时, Linux无法工作在我的Ultra DMA IDE驱动器和主机板上, 怎么办? E.6.12 我的机器有一个PCI Ultra DMA 控制器. 我可以安装Linux吗? E.6.13 我有NT, 并想安装Linux, 但我听说启动多操作系统会出现...

空空如也

空空如也

1 2
收藏数 35
精华内容 14
关键字:

linuxring网卡驱动

linux 订阅