网卡驱动和队列层中的数据包接收

网卡工作原理
Linux操作系统的功能可以概括为进程管理、内存管理、文件系统管理、设备管理、网络等几部分。所有的系统操作最终都可以映射到对物理设备的操作。除去对CPU、内存以及其他少数几个物理实体的操作之外,系统对其他设备的所有操作都通过专门的称为驱动程序的代码完成。系统中存在的每种外设在内核中都必须有对应的设备驱动程序对其进行处理。所以分析网卡的工作原理即是分析网卡的驱动程序。
网络是独立的一个模块。为了屏蔽网络环境中物理网络设备的多样性,Linux对所有的设备进行抽象并定义了一个统一的概念,称之为接口。所有对网络硬件的访问都是通过接口进行的,接口提供了一个对所有类型的硬件一致化的操作集合来处理基本数据发送和接收。一个网络接口被看作是一个发送和接收数据包的实体。对于每个网络接口,都用一个net_device的数据结构来表示。net_device中有很多提供系统访问和协议层调用的设备方法,包括提供设备初始化和往系统注册用的init函数,打开和关闭网络设备的open和stop函数,处理数据包发送的函数hard_start_xmit,以及中断处理函数。
所有被发送和接收的包都用数据结构sk_buff表示。要发送数据时,网络系统将分局系统路由表选择相应的网络接口进行数据传输;当接收数据包时,通过驱动程序登记的中断服务程序进行数据的接口处理。
Linux网络驱动程序崇尚倒下分为四层:协议接口层、网络设备接口层、设备驱动功能层、网络设备和网络媒介层。如下图所示:

网卡初始化
网络设备初始化主要工作时检测设备的存在、初始化描述设备的net_device结构及在系统中登记该设备。在系统初始化完成以后,系统检测到的网络设备将保存在链表dev_base中,其中每个链表单元net_device对应一个存在的物理网络设备。
初始化过程首先检测网络物理设备是否存在,这是通过检测物理设备的硬件特征来完成;然后对设备进行资源配置,这些完成之后就要构造设备的net_device数据结构,用检测到值对net_device中的变量初始化;最后Linux内核中注册该设备并申请内存空间。

网卡的打开与关闭
为了使用网络设备,需要打开网卡,打开和关闭的一个接口是由shell命令ifconfig调用的,而ifconfig则要调用一个通用的设备打开函数dev_open(net/core/dev.c),相应的还有一个dev_close函数,这两个函数提供独立于设备的操作接口的打开和关闭功能。一般打开函数执行的操作包括注册中断函数,分配并初始化网卡所需要的接收与发送缓冲区,启动硬件检查网络连接线状态等。

数据包的发送与接收
数据包的发送和接收是实现Linux网络驱动程序中两个最关键的过程。
当物理网络设备接收到数据是,系统通过两种途径解决这个问题。一种方法是轮询方式,另一种方式是中断法师。
在轮询方式中,系统每隔一定的时间间隔就去检查一次物理设备,若设备有数据到达,就调用读取数据的程序。Linux中通过定时器实现,但是此法有一个明显的缺点:不管设备是否有数据,系统总是要固定的消耗CPU资源去查看设备,并且可能对一些紧急数据处理予以延迟。从资源的利用率以及工作的效率上看都不是最优的。
中断方式利用硬件体系结构的中断机制实现设备和系统的应答对话,即当物理设备需要CPU处理数据时,就向CPU发送一个终端信号,系统则在收到信号后调用相应的中断服务程序响应对设备中断的处理。因此,基本在所有的网络设备驱动程序中都是用中断方式的。
每一个网卡上都有一块FIFO存储器,对于NIC(Network Interface Controller),FIFO存储器是用来通过系统总线传送数据到系统存储器之前,缓存从LAN上接收到的数据。对与快速以太网还有一个直接内存存取(DMA:Directly Memory Access)控制器,用于提供对系统存储器的可靠访问。
驱动为网卡分配一个环形缓冲区,在一段连续的物理内存中实现。
1、 数据接收
(1) 接收来自MAC的数据包,先暂存于片内FIFO接收队列;
(2) 当接收器达到早期接收上线时就移至环形缓冲区;
(3) 待整个数据包全部从FIFO移至缓存后,将接收状态寄存器和包长度写入接收的数据包头部,并更新CBA(Current Buffer Address)寄存器的值;
(4) CMD(Command)寄存器中的BufferEmpty位和ISR(中断状态寄存器)寄存器的ROK位置1,并发出ROK的中断;
(5) ISR中断调用完成后,清除ISR(ROK)并更新CAPR(Current Address of Packet Read,指向接收缓存的已读取包的地址),完成本次接收。
2、 数据发送
(1) 将待传送的数据写入主存中一段连续的缓存空间,由OS配合驱动程序完成;
(2) 找到一个可用的描述器,并写入内容,包括该数据包的开始物理地址和传输状态字(包的大小、可传送下限、OWN位);
(3) OWN位有效,将数据从缓存移至片内FIFO队列;
(4) 当FIFO队列中的数据达到早期传送下限,NIC的传送单元就会启动,将数据顺序输出至线路;
(5) 当整个数据包都已经传至FIFO,OWN位置1;
(6) 当整个数据包都已经传至线路上, TOK寄存器置1;
(7) 当TOK(IMR)和TOK(ISR)多置1,就发出TOK中断;
(8) TOK中断调用完成以后,清除TSD状态字,完成本次传送。

可以看出,网卡需要发送/接收数据,都必须以中断的方式告诉系统,系统处理中断后做出相应操作。
网卡存在一定大小的FIFO存储器,同时还有缓冲区,缓冲区是由系统以及驱动共同分配一段连续的物理内存,所有的发送/接收的数据,都必须通过FIFO已经缓冲区,只有一包数据都发送成功后,才能继续发送下一包数据。系统维护缓冲区,只有当缓冲区有空间时才会接受上层来的数据,而网卡处理数据的速率远高于接收数据的最大速率,因此在网卡上不会存在堵塞情况。
对编程而言,在应用层调用传输层函数send/sendto,使用套接字传送数据,屏蔽了底层的所有实现。此时,send/sendto函数是没有阻塞的,只要调用,必然有返回值,成功返回发送数据的长度,失败则返回负值(失败的主要原因是网络连接的问题),因此可能存在数据丢失的现象,需要写程序的时候保证数据的传输成功。但是只有send/sendto函数返回后,程序才会执行下一次发送,因此编程时没必要考虑数据会在传输层上出现阻塞。

网卡驱动和队列层中的数据包接收
一、从网卡说起

这并非是一个网卡驱动分析的专门文档,只是对网卡处理数据包的流程进行一个重点的分析。这里以Intel的e100驱动为例进行分析。
大多数网卡都是一个PCI设备,PCI设备都包含了一个标准的配置寄存器,寄存器中,包含了PCI设备的厂商ID、设备ID等等信息,驱动
程序使用来描述这些寄存器的标识符。如下:

struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice; /* Subsystem ID’s or PCI_ANY_ID */
__u32 class, class_mask; /* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data; /* Data private to the driver */
};

这样,在驱动程序中,常常就可以看到定义一个struct pci_device_id 类型的数组,告诉内核支持不同类型的
PCI设备的列表,以e100驱动为例:

define INTEL_8255X_ETHERNET_DEVICE(device_id, ich) {\

    PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID, \
    PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00, ich }

static struct pci_device_id e100_id_table[] = {
INTEL_8255X_ETHERNET_DEVICE(0x1029, 0),
INTEL_8255X_ETHERNET_DEVICE(0x1030, 0),
INTEL_8255X_ETHERNET_DEVICE(0x1031, 3),
……/略过一大堆支持的设备/
{ 0, }
};

在内核中,一个PCI设备,使用struct pci_driver结构来描述,
struct pci_driver {
struct list_head node;
char *name;
struct module *owner;
const struct pci_device_id id_table; / must be non-NULL for probe to be called */
int (probe) (struct pci_dev *dev, const struct pci_device_id *id); / New device inserted */
void (remove) (struct pci_dev *dev); / Device removed (NULL if not a hot-plug capable driver) */
int (suspend) (struct pci_dev *dev, pm_message_t state); / Device suspended */
int (resume) (struct pci_dev *dev); / Device woken up */
int (enable_wake) (struct pci_dev *dev, pci_power_t state, int enable); / Enable wake event */
void (*shutdown) (struct pci_dev *dev);

    struct device_driver        driver;
    struct pci_dynids dynids;

};

因为在系统引导的时候,PCI设备已经被识别,当内核发现一个已经检测到的设备同驱动注册的id_table中的信息相匹配时,
它就会触发驱动的probe函数,以e100为例:
/*
* 定义一个名为e100_driver的PCI设备
* 1、设备的探测函数为e100_probe;
* 2、设备的id_table表为e100_id_table
*/
static struct pci_driver e100_driver = {
.name = DRV_NAME,
.id_table = e100_id_table,
.probe = e100_probe,
.remove = __devexit_p(e100_remove),

ifdef CONFIG_PM

    .suspend =      e100_suspend,
    .resume =       e100_resume,

endif

    .driver = {
            .shutdown = e100_shutdown,
    }

};

这样,如果系统检测到有与id_table中对应的设备时,就调用驱动的probe函数。

驱动设备在init函数中,调用pci_module_init函数初始化PCI设备e100_driver:

static int __init e100_init_module(void)
{
if(((1 << debug) - 1) & NETIF_MSG_DRV) {
printk(KERN_INFO PFX “%s, %s\n”, DRV_DESCRIPTION, DRV_VERSION);
printk(KERN_INFO PFX “%s\n”, DRV_COPYRIGHT);
}
return pci_module_init(&e100_driver);
}

一切顺利的话,注册的e100_probe函数将被内核调用,这个函数完成两个重要的工作:
1、分配/初始化/注册网络设备;
2、完成PCI设备的I/O区域的分配和映射,以及完成硬件的其它初始化工作;

网络设备使用struct net_device结构来描述,这个结构非常之大,许多重要的参考书籍对它都有较为深入的描述,可以参考《Linux设备驱动程序》中网卡驱动设计的相关章节。我会在后面的内容中,对其重要的成员进行注释;

当probe函数被调用,证明已经发现了我们所支持的网卡,这样,就可以调用register_netdev函数向内核注册网络设备了,注册之前,一般会调用alloc_etherdev为以太网分析一个net_device,然后初始化它的重要成员。

除了向内核注册网络设备之外,探测函数另一项重要的工作就是需要对硬件进行初始化,比如,要访问其I/O区域,需要为I/O区域分配内存区域,然后进行映射,这一步一般的流程是:
1、request_mem_region()
2、ioremap()

对于一般的PCI设备而言,可以调用:
1、pci_request_regions()
2、ioremap()

pci_request_regions函数对PCI的6个寄存器都会调用资源分配函数进行申请(需要判断是I/O端口还是I/O内存),例如:

int pci_request_regions(struct pci_dev *pdev, char *res_name)
{
int i;

    for (i = 0; i < 6; i++)
            if(pci_request_region(pdev, i, res_name))
                    goto err_out;
    return 0;

}

int pci_request_region(struct pci_dev *pdev, int bar, char *res_name)
{
if (pci_resource_len(pdev, bar) == 0)
return 0;

    if (pci_resource_flags(pdev, bar) & IORESOURCE_IO) {
            if (!request_region(pci_resource_start(pdev, bar),
                        pci_resource_len(pdev, bar), res_name))
                    goto err_out;
    }
    else if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) {
            if (!request_mem_region(pci_resource_start(pdev, bar),
                                    pci_resource_len(pdev, bar), res_name))
                    goto err_out;
    }

    return 0;

}

有了这些基础,我们来看设备的探测函数:
static int __devinit e100_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
struct net_device *netdev;
struct nic *nic;
int err;

    /*分配网络设备*/
    if(!(netdev = alloc_etherdev(sizeof(struct nic)))) {
            if(((1 << debug) - 1) & NETIF_MSG_PROBE)
                    printk(KERN_ERR PFX "Etherdev alloc failed, abort.\n");
            return -ENOMEM;
    }

    /*设置各成员指针函数*/
    netdev->open = e100_open;
    netdev->stop = e100_close;
    netdev->hard_start_xmit = e100_xmit_frame;
    netdev->get_stats = e100_get_stats;
    netdev->set_multicast_list = e100_set_multicast_list;
    netdev->set_mac_address = e100_set_mac_address;
    netdev->change_mtu = e100_change_mtu;
    netdev->do_ioctl = e100_do_ioctl;
    SET_ETHTOOL_OPS(netdev, &e100_ethtool_ops);
    netdev->tx_timeout = e100_tx_timeout;
    netdev->watchdog_timeo = E100_WATCHDOG_PERIOD;
    netdev->poll = e100_poll;
    netdev->weight = E100_NAPI_WEIGHT;

ifdef CONFIG_NET_POLL_CONTROLLER

    netdev->poll_controller = e100_netpoll;

endif

    /*设置网络设备名称*/
    strcpy(netdev->name, pci_name(pdev));

    /*取得设备私有数据结构*/
    nic = netdev_priv(netdev);
    /*网络设备指针,指向自己*/
    nic->netdev = netdev;
    /*PCIy设备指针,指向自己*/
    nic->pdev = pdev;
    nic->msg_enable = (1 << debug) - 1;

    /*将PCI设备的私有数据区指向网络设备*/
    pci_set_drvdata(pdev, netdev);

    /*激活PCI设备*/
    if((err = pci_enable_device(pdev))) {
            DPRINTK(PROBE, ERR, "Cannot enable PCI device, aborting.\n");
            goto err_out_free_dev;
    }

    /*判断I/O区域是否是I/O内存,如果不是,则报错退出*/
    if(!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM)) {
            DPRINTK(PROBE, ERR, "Cannot find proper PCI device "
                    "base address, aborting.\n");
            err = -ENODEV;
            goto err_out_disable_pdev;
    }

    /*分配I/O内存区域*/
    if((err = pci_request_regions(pdev, DRV_NAME))) {
            DPRINTK(PROBE, ERR, "Cannot obtain PCI resources, aborting.\n");
            goto err_out_disable_pdev;
    }

    /*
     * 告之内核自己的DMA寻址能力,这里不是很明白,因为从0xFFFFFFFF来看,本来就是内核默认的32了
     * 为什么还要调用pci_set_dma_mask来重复设置呢?可能是对ULL而非UL不是很了解吧。
     */
    if((err = pci_set_dma_mask(pdev, 0xFFFFFFFFULL))) {
            DPRINTK(PROBE, ERR, "No usable DMA configuration, aborting.\n");
            goto err_out_free_res;
    }

    SET_MODULE_OWNER(netdev);
    SET_NETDEV_DEV(netdev, &pdev->dev);

    /*分配完成后,映射I/O内存*/
    nic->csr = ioremap(pci_resource_start(pdev, 0), sizeof(struct csr));
    if(!nic->csr) {
            DPRINTK(PROBE, ERR, "Cannot map device registers, aborting.\n");
            err = -ENOMEM;
            goto err_out_free_res;
    }

    if(ent->driver_data)
            nic->flags |= ich;
    else
            nic->flags &= ~ich;

    /*设置设备私有数据结构的大部份默认参数*/
    e100_get_defaults(nic);

    /* 初始化自旋锁,锅的初始化必须在调用 hw_reset 之前执行*/
    spin_lock_init(&nic->cb_lock);
    spin_lock_init(&nic->cmd_lock);

    /* 硬件复位,通过向指定I/O端口设置复位指令实现. */
    e100_hw_reset(nic);

    /*
     * PCI网卡被BIOS配置后,某些特性可能会被屏蔽掉。比如,多数BIOS都会清掉“master”位,
     * 这导致板卡不能随意向主存中拷贝数据。pci_set_master函数数会检查是否需要设置标志位,
     * 如果需要,则会将“master”位置位。
     * PS:什么是PCI master?
     * 不同于ISA总线,PCI总线的地址总线与数据总线是分时复用的。这样做的好处是,一方面
     * 可以节省接插件的管脚数,另一方面便于实现突发数据传输。在做数据传输时,由一个PCI
     * 设备做发起者(主控,Initiator或Master),而另一个PCI设备做目标(从设备,Target或Slave)。
     * 总线上的所有时序的产生与控制,都由Master来发起。PCI总线在同一时刻只能供一对设备完成传输。
     */
    pci_set_master(pdev);

    /*添加两个内核定时器,watchdog和blink_timer*/
    init_timer(&nic->watchdog);
    nic->watchdog.function = e100_watchdog;
    nic->watchdog.data = (unsigned long)nic;
    init_timer(&nic->blink_timer);
    nic->blink_timer.function = e100_blink_led;
    nic->blink_timer.data = (unsigned long)nic;

    INIT_WORK(&nic->tx_timeout_task,
            (void (*)(void *))e100_tx_timeout_task, netdev);

    if((err = e100_alloc(nic))) {
            DPRINTK(PROBE, ERR, "Cannot alloc driver memory, aborting.\n");
            goto err_out_iounmap;
    }

    /*phy寄存器初始化*/
    e100_phy_init(nic);

    if((err = e100_eeprom_load(nic)))
            goto err_out_free;

    memcpy(netdev->dev_addr, nic->eeprom, ETH_ALEN);
    if(!is_valid_ether_addr(netdev->dev_addr)) {
            DPRINTK(PROBE, ERR, "Invalid MAC address from "
                    "EEPROM, aborting.\n");
            err = -EAGAIN;
            goto err_out_free;
    }

    /* Wol magic packet can be enabled from eeprom */
    if((nic->mac >= mac_82558_D101_A4) &&
       (nic->eeprom[eeprom_id] & eeprom_id_wol))
            nic->flags |= wol_magic;

    /* ack any pending wake events, disable PME */
    pci_enable_wake(pdev, 0, 0);

    /*注册网络设备*/
    strcpy(netdev->name, "eth%d");
    if((err = register_netdev(netdev))) {
            DPRINTK(PROBE, ERR, "Cannot register net device, aborting.\n");
            goto err_out_free;
    }

    DPRINTK(PROBE, INFO, "addr 0x%lx, irq %d, "
            "MAC addr %02X:%02X:%02X:%02X:%02X:%02X\n",
            pci_resource_start(pdev, 0), pdev->irq,
            netdev->dev_addr[0], netdev->dev_addr[1], netdev->dev_addr[2],
            netdev->dev_addr[3], netdev->dev_addr[4], netdev->dev_addr[5]);

    return 0;

err_out_free:
e100_free(nic);
err_out_iounmap:
iounmap(nic->csr);
err_out_free_res:
pci_release_regions(pdev);
err_out_disable_pdev:
pci_disable_device(pdev);
err_out_free_dev:
pci_set_drvdata(pdev, NULL);
free_netdev(netdev);
return err;
}
执行到这里,探测函数的使命就完成了,在对网络设备重要成员初始化时,有:
netdev->open = e100_open;
指定了设备的open函数为e100_open,这样,当第一次使用设备,比如使用ifconfig工具的时候,open函数将被调用。

二、打开设备

在探测函数中,设置了netdev->open = e100_open; 指定了设备的open函数为e100_open:

[Copy to clipboard] [ - ] CODE:
static int e100_open(struct net_device *netdev)
{
struct nic *nic = netdev_priv(netdev);
int err = 0;

    netif_carrier_off(netdev);
    if((err = e100_up(nic)))
            DPRINTK(IFUP, ERR, "Cannot open interface, aborting.\n");
    return err;

}

大多数涉及物理设备可以感知信号载波(carrier)的存在,载波的存在意味着设备可以工作
据个例子来讲:当一个用户拔掉了网线,也就意味着信号载波的消失。
netif_carrier_off:关闭载波信号;
netif_carrier_on:打开载波信号;
netif_carrier_ok:检测载波信号;

对于探测网卡网线是否连接,这一组函数被使用得较多;

接着,调用e100_up函数启动网卡,这个“启动”的过程,最重要的步骤有:
1、调用request_irq向内核注册中断;
2、调用netif_wake_queue函数来重新启动传输队例;

[Copy to clipboard] [ - ] CODE:
static int e100_up(struct nic *nic)
{
int err;

    if((err = e100_rx_alloc_list(nic)))
            return err;
    if((err = e100_alloc_cbs(nic)))
            goto err_rx_clean_list;
    if((err = e100_hw_init(nic)))
            goto err_clean_cbs;
    e100_set_multicast_list(nic->netdev);
    e100_start_receiver(nic, 0);
    mod_timer(&nic->watchdog, jiffies);
    if((err = request_irq(nic->pdev->irq, e100_intr, SA_SHIRQ,
            nic->netdev->name, nic->netdev)))
            goto err_no_irq;
    netif_wake_queue(nic->netdev);
    netif_poll_enable(nic->netdev);
    /* enable ints _after_ enabling poll, preventing a race between
     * disable ints+schedule */
    e100_enable_irq(nic);
    return 0;

err_no_irq:
del_timer_sync(&nic->watchdog);
err_clean_cbs:
e100_clean_cbs(nic);
err_rx_clean_list:
e100_rx_clean_list(nic);
return err;

}

这样,中断函数e100_intr将被调用;

三、网卡中断

从本质上来讲,中断,是一种电信号,当设备有某种事件发生的时候,它就会产生中断,通过总线把电信号发送给中断控制器,如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到内存中内核设置的中断处理程序的入口点,进行中断处理。
在内核中断处理中,会检测中断与我们刚才注册的中断号匹配,于是,注册的中断处理函数就被调用了。

当需要发/收数据,出现错误,连接状态变化等,网卡的中断信号会被触发。当接收到中断后,中断函数读取中断状态位,进行合法性判断,如判断中断信号是否是自己的等,然后,应答设备中断——OK,我已经知道了,你回去继续工作吧……
接着,它就屏蔽此中断,然后netif_rx_schedule函数接收,接收函数 会在未来某一时刻调用设备的poll函数(对这里而言,注册的是e100_poll)实现设备的轮询:

[Copy to clipboard] [ - ] CODE:
static irqreturn_t e100_intr(int irq, void *dev_id, struct pt_regs *regs)
{
struct net_device *netdev = dev_id;
struct nic *nic = netdev_priv(netdev);
u8 stat_ack = readb(&nic->csr->scb.stat_ack);

    DPRINTK(INTR, DEBUG, "stat_ack = 0x%02X\n", stat_ack);

    if(stat_ack == stat_ack_not_ours ||        /* Not our interrupt */
       stat_ack == stat_ack_not_present)        /* Hardware is ejected */
            return IRQ_NONE;

    /* Ack interrupt(s) */
    writeb(stat_ack, &nic->csr->scb.stat_ack);

    /* We hit Receive No Resource (RNR); restart RU after cleaning */
    if(stat_ack & stat_ack_rnr)
            nic->ru_running = RU_SUSPENDED;

    e100_disable_irq(nic);
    netif_rx_schedule(netdev);

    return IRQ_HANDLED;

}

对于数据包的接收而言,我们关注的是poll函数中,调用e100_rx_clean进行数据的接收:

[Copy to clipboard] [ - ] CODE:
static int e100_poll(struct net_device *netdev, int *budget)
{
struct nic *nic = netdev_priv(netdev);
/*
* netdev->quota是当前CPU能够从所有接口中接收数据包的最大数目,budget是在
* 初始化阶段分配给接口的weight值,轮询函数必须接受二者之间的最小值。表示
* 轮询函数本次要处理的数据包个数。
*/
unsigned int work_to_do = min(netdev->quota, *budget);
unsigned int work_done = 0;
int tx_cleaned;

      /*进行数据包的接收和传输*/             
    e100_rx_clean(nic, &work_done, work_to_do);
    tx_cleaned = e100_tx_clean(nic);

     /*接收和传输完成后,就退出poll模块,重启中断*/
    /* If no Rx and Tx cleanup work was done, exit polling mode. */
    if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {
            netif_rx_complete(netdev);
            e100_enable_irq(nic);
            return 0;
    }

    *budget -= work_done;
    netdev->quota -= work_done;

    return 1;

}

static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done,
unsigned int work_to_do)
{
struct rx *rx;
int restart_required = 0;
struct rx *rx_to_start = NULL;

    /* are we already rnr? then pay attention!!! this ensures that
     * the state machine progression never allows a start with a 
     * partially cleaned list, avoiding a race between hardware
     * and rx_to_clean when in NAPI mode */
    if(RU_SUSPENDED == nic->ru_running)
            restart_required = 1;

    /* Indicate newly arrived packets */
    for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {
            int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
            if(-EAGAIN == err) {
                    /* hit quota so have more work to do, restart once
                     * cleanup is complete */
                    restart_required = 0;
                    break;
            } else if(-ENODATA == err)
                    break; /* No more to clean */
    }

    /* save our starting point as the place we'll restart the receiver */
    if(restart_required)
            rx_to_start = nic->rx_to_clean;

    /* Alloc new skbs to refill list */
    for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {
            if(unlikely(e100_rx_alloc_skb(nic, rx)))
                    break; /* Better luck next time (see watchdog) */
    }

    if(restart_required) {
            // ack the rnr?
            writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);
            e100_start_receiver(nic, rx_to_start);
            if(work_done)
                    (*work_done)++;
    }

}

四、网卡的数据接收

内核如何从网卡接受数据,传统的经典过程:
1、数据到达网卡;
2、网卡产生一个中断给内核;
3、内核使用I/O指令,从网卡I/O区域中去读取数据;

我们在许多网卡驱动中,都可以在网卡的中断函数中见到这一过程。

但是,这一种方法,有一种重要的问题,就是大流量的数据来到,网卡会产生大量的中断,内核在中断上下文中,会浪费大量的资源来处理中断本身。所以,一个问题是,“可不可以不使用中断”,这就是轮询技术,所谓NAPI技术,说来也不神秘,就是说,内核屏蔽中断,然后隔一会儿就去问网卡,“你有没有数据啊?”……

从这个描述本身可以看到,哪果数据量少,轮询同样占用大量的不必要的CPU资源,大家各有所长吧,呵呵……

OK,另一个问题,就是从网卡的I/O区域,包括I/O寄存器或I/O内存中去读取数据,这都要CPU去读,也要占用CPU资源,“CPU从I/O区域读,然后把它放到内存(这个内存指的是系统本身的物理内存,跟外设的内存不相干,也叫主内存)中”。于是自然地,就想到了DMA技术——让网卡直接从主内存之间读写它们的I/O数据,CPU,这儿不干你事,自己找乐子去:
1、首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
2、内核将这个缓冲区通过DMA映射,把这个队列交给网卡;
3、网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;
4、内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;

——呵呵,这一个过程比传统的过程少了不少工作,因为设备直接把数据放进了主内存,不需要CPU的干预,效率是不是提高不少?

对应以上4步,来看它的具体实现:
1、分配环形DMA缓冲区
Linux内核中,用skb来描述一个缓存,所谓分配,就是建立一定数量的skb,然后把它们组织成一个双向链表;

2、建立DMA映射
内核通过调用
dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
建立映射关系。
struct device *dev,描述一个设备;
buffer:把哪个地址映射给设备;也就是某一个skb——要映射全部,当然是做一个双向链表的循环即可;
size:缓存大小;
direction:映射方向——谁传给谁:一般来说,是“双向”映射,数据在设备和内存之间双向流动;

对于PCI设备而言(网卡一般是PCI的),通过另一个包裹函数pci_map_single,这样,就把buffer交给设备了!设备可以直接从里边读/取数据。

3、这一步由硬件完成;

4、取消映射
dma_unmap_single,对PCI而言,大多调用它的包裹函数pci_unmap_single,不取消的话,缓存控制权还在设备手里,要调用它,把主动权掌握在CPU手里——因为我们已经接收到数据了,应该由CPU把数据交给上层网络栈;

当然,不取消之前,通常要读一些状态位信息,诸如此类,一般是调用
dma_sync_single_for_cpu()
让CPU在取消映射前,就可以访问DMA缓冲区中的内容。

关于DMA映射的更多内容,可以参考《Linux设备驱动程序》“内存映射和DMA”章节相关内容!

OK,有了这些知识,我们就可以来看e100的代码了,它跟上面讲的步骤基本上一样的——绕了这么多圈子,就是想绕到e100上面了,呵呵!

在e100_open函数中,调用e100_up,我们前面分析它时,略过了一个重要的东东,就是环形缓冲区的建立,这一步,是通过
e100_rx_alloc_list函数调用完成的:

[Copy to clipboard] [ - ] CODE:
static int e100_rx_alloc_list(struct nic *nic)
{
struct rx *rx;
unsigned int i, count = nic->params.rfds.count;

    nic->rx_to_use = nic->rx_to_clean = NULL;
    nic->ru_running = RU_UNINITIALIZED;

    /*结构struct rx用来描述一个缓冲区节点,这里分配了count个*/
    if(!(nic->rxs = kmalloc(sizeof(struct rx) * count, GFP_ATOMIC)))
            return -ENOMEM;
    memset(nic->rxs, 0, sizeof(struct rx) * count);

    /*虽然是连续分配的,不过还是遍历它,建立双向链表,然后为每一个rx的skb指针分员分配空间
    skb用来描述内核中的一个数据包,呵呵,说到重点了*/
    for(rx = nic->rxs, i = 0; i < count; rx++, i++) {
            rx->next = (i + 1 < count) ? rx + 1 : nic->rxs;
            rx->prev = (i == 0) ? nic->rxs + count - 1 : rx - 1;
            if(e100_rx_alloc_skb(nic, rx)) {                /*分配缓存*/
                    e100_rx_clean_list(nic);
                    return -ENOMEM;
            }
    }

    nic->rx_to_use = nic->rx_to_clean = nic->rxs;
    nic->ru_running = RU_SUSPENDED;

    return 0;

}

[Copy to clipboard] [ - ] CODE:

define RFD_BUF_LEN (sizeof(struct rfd) + VLAN_ETH_FRAME_LEN)

static inline int e100_rx_alloc_skb(struct nic *nic, struct rx *rx)
{
/*skb缓存的分配,是通过调用系统函数dev_alloc_skb来完成的,它同内核栈中通常调用alloc_skb的区别在于,
它是原子的,所以,通常在中断上下文中使用*/
if(!(rx->skb = dev_alloc_skb(RFD_BUF_LEN + NET_IP_ALIGN)))
return -ENOMEM;

    /*初始化必要的成员 */
    rx->skb->dev = nic->netdev;
    skb_reserve(rx->skb, NET_IP_ALIGN);
    /*这里在数据区之前,留了一块sizeof(struct rfd) 这么大的空间,该结构的
    一个重要作用,用来保存一些状态信息,比如,在接收数据之前,可以先通过
    它,来判断是否真有数据到达等,诸如此类*/
    memcpy(rx->skb->data, &nic->blank_rfd, sizeof(struct rfd));
    /*这是最关键的一步,建立DMA映射,把每一个缓冲区rx->skb->data都映射给了设备,缓存区节点
    rx利用dma_addr保存了每一次映射的地址,这个地址后面会被用到*/
    rx->dma_addr = pci_map_single(nic->pdev, rx->skb->data,
            RFD_BUF_LEN, PCI_DMA_BIDIRECTIONAL);

    if(pci_dma_mapping_error(rx->dma_addr)) {
            dev_kfree_skb_any(rx->skb);
            rx->skb = 0;
            rx->dma_addr = 0;
            return -ENOMEM;
    }

    /* Link the RFD to end of RFA by linking previous RFD to
     * this one, and clearing EL bit of previous.  */
    if(rx->prev->skb) {
            struct rfd *prev_rfd = (struct rfd *)rx->prev->skb->data;
            /*put_unaligned(val,ptr);用到把var放到ptr指针的地方,它能处理处理内存对齐的问题
            prev_rfd是在缓冲区开始处保存的一点空间,它的link成员,也保存了映射后的地址*/
            put_unaligned(cpu_to_le32(rx->dma_addr),
                    (u32 *)&prev_rfd->link);
            wmb();
            prev_rfd->command &= ~cpu_to_le16(cb_el);
            pci_dma_sync_single_for_device(nic->pdev, rx->prev->dma_addr,
                    sizeof(struct rfd), PCI_DMA_TODEVICE);
    }

    return 0;

}

e100_rx_alloc_list函数在一个循环中,建立了环形缓冲区,并调用e100_rx_alloc_skb为每个缓冲区分配了空间,并做了
DMA映射。这样,我们就可以来看接收数据的过程了。

前面我们讲过,中断函数中,调用netif_rx_schedule,表明使用轮询技术,系统会在未来某一时刻,调用设备的poll函数:

[Copy to clipboard] [ - ] CODE:
static int e100_poll(struct net_device *netdev, int *budget)
{
struct nic *nic = netdev_priv(netdev);
unsigned int work_to_do = min(netdev->quota, *budget);
unsigned int work_done = 0;
int tx_cleaned;

    e100_rx_clean(nic, &work_done, work_to_do);
    tx_cleaned = e100_tx_clean(nic);

    /* If no Rx and Tx cleanup work was done, exit polling mode. */
    if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {
            netif_rx_complete(netdev);
            e100_enable_irq(nic);
            return 0;
    }

    *budget -= work_done;
    netdev->quota -= work_done;

    return 1;

}

目前,我们只关心rx,所以,e100_rx_clean函数就成了我们关注的对像,它用来从缓冲队列中接收全部数据(这或许是取名为clean的原因吧!):

[Copy to clipboard] [ - ] CODE:
static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done,
unsigned int work_to_do)
{
struct rx *rx;
int restart_required = 0;
struct rx *rx_to_start = NULL;

    /* are we already rnr? then pay attention!!! this ensures that
     * the state machine progression never allows a start with a 
     * partially cleaned list, avoiding a race between hardware
     * and rx_to_clean when in NAPI mode */
    if(RU_SUSPENDED == nic->ru_running)
            restart_required = 1;

    /* 函数最重要的工作,就是遍历环形缓冲区,接收数据*/
    for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {
            int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
            if(-EAGAIN == err) {
                    /* hit quota so have more work to do, restart once
                     * cleanup is complete */
                    restart_required = 0;
                    break;
            } else if(-ENODATA == err)
                    break; /* No more to clean */
    }

    /* save our starting point as the place we'll restart the receiver */
    if(restart_required)
            rx_to_start = nic->rx_to_clean;

    /* Alloc new skbs to refill list */
    for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {
            if(unlikely(e100_rx_alloc_skb(nic, rx)))
                    break; /* Better luck next time (see watchdog) */
    }

    if(restart_required) {
            // ack the rnr?
            writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);
            e100_start_receiver(nic, rx_to_start);
            if(work_done)
                    (*work_done)++;
    }

}

[Copy to clipboard] [ - ] CODE:
static inline int e100_rx_indicate(struct nic *nic, struct rx *rx,
unsigned int *work_done, unsigned int work_to_do)
{
struct sk_buff *skb = rx->skb;
struct rfd rfd = (struct rfd )skb->data;
u16 rfd_status, actual_size;

    if(unlikely(work_done && *work_done >= work_to_do))
            return -EAGAIN;

    /* 读取数据之前,也就是取消DMA映射之前,需要先读取cb_complete 状态位,
    以确定数据是否真的准备好了,并且,rfd的actual_size中,也包含了真实的数据大小
    pci_dma_sync_single_for_cpu函数前面已经介绍过,它让CPU在取消DMA映射之前,具备
    访问DMA缓存的能力*/
    pci_dma_sync_single_for_cpu(nic->pdev, rx->dma_addr,
            sizeof(struct rfd), PCI_DMA_FROMDEVICE);
    rfd_status = le16_to_cpu(rfd->status);

    DPRINTK(RX_STATUS, DEBUG, "status=0x%04X\n", rfd_status);

    /* If data isn't ready, nothing to indicate */
    if(unlikely(!(rfd_status & cb_complete)))
            return -ENODATA;

    /* Get actual data size */
    actual_size = le16_to_cpu(rfd->actual_size) & 0x3FFF;
    if(unlikely(actual_size > RFD_BUF_LEN - sizeof(struct rfd)))
            actual_size = RFD_BUF_LEN - sizeof(struct rfd);

    /* 取消映射,因为通过DMA,网卡已经把数据放在了主内存中,这里一取消,也就意味着,
    CPU可以处理主内存中的数据了 */
    pci_unmap_single(nic->pdev, rx->dma_addr,
            RFD_BUF_LEN, PCI_DMA_FROMDEVICE);

    /* this allows for a fast restart without re-enabling interrupts */
    if(le16_to_cpu(rfd->command) & cb_el)
            nic->ru_running = RU_SUSPENDED;

    /*正确地设置data指针,因为最前面有一个sizeof(struct rfd)大小区域,跳过它*/
    skb_reserve(skb, sizeof(struct rfd));
    /*更新skb的tail和len指针,也是就更新接收到这么多数据的长度*/
    skb_put(skb, actual_size);
    /*设置协议位*/
    skb->protocol = eth_type_trans(skb, nic->netdev);

    if(unlikely(!(rfd_status & cb_ok))) {
            /* Don't indicate if hardware indicates errors */
            nic->net_stats.rx_dropped++;
            dev_kfree_skb_any(skb);
    } else if(actual_size > nic->netdev->mtu + VLAN_ETH_HLEN) {
            /* Don't indicate oversized frames */
            nic->rx_over_length_errors++;
            nic->net_stats.rx_dropped++;
            dev_kfree_skb_any(skb);
    } else {
            /*网卡驱动要做的最后一步,就是统计接收计数器,设置接收时间戳,然后调用netif_receive_skb,
            把数据包交给上层协议栈,自己的光荣始命也就完成了*/
            nic->net_stats.rx_packets++;
            nic->net_stats.rx_bytes += actual_size;
            nic->netdev->last_rx = jiffies;
            netif_receive_skb(skb);
            if(work_done)
                    (*work_done)++;
    }

    rx->skb = NULL;

    return 0;

}

网卡驱动执行到这里,数据接收的工作,也就处理完成了。但是,使用这一种方法的驱动,省去了网络栈中一个重要的内容,就是
“队列层”,让我们来看看,传统中断接收数据包模式下,使用netif_rx函数调用,又会发生什么。
五、队列层

1、软中断与下半部
当用中断处理的时候,为了减少中断处理的工作量,比如,一般中断处理时,需要屏蔽其它中断,如果中断处理时间过长,那么其它中断
有可能得不到及时处理,也以,有一种机制,就是把“不必马上处理”的工作,推迟一点,让它在中断处理后的某一个时刻得到处理。这就
是下半部。

下半部只是一个机制,它在Linux中,有多种实现方式,其中一种对时间要求最严格的实现方式,叫“软中断”,可以使用:

open_softirq()

来向内核注册一个软中断,
然后,在合适的时候,调用

raise_softirq_irqoff()

触发它。

如果采用中断方式接收数据(这一节就是在说中断方式接收,后面,就不用这种假设了),同样也需要软中断,可以调用

open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);

向内核注册一个名为NET_RX_SOFTIR的软中断,net_rx_action是软中断的处理函数。

然后,在驱动中断处理完后的某一个时刻,调用

raise_softirq_irqoff(NET_RX_SOFTIRQ);

触发它,这样net_rx_action将得到执行。

2、队列层
什么是队列层?通常,在网卡收发数据的时候,需要维护一个缓冲区队列,来缓存可能存在的突发数据,类似于前面的DMA环形缓冲区。
队列层中,包含了一个叫做struct softnet_data:

[Copy to clipboard] [ - ] CODE:
struct softnet_data
{
/throttle 用于拥塞控制,当拥塞发生时,throttle将被设置,后续进入的数据包将被丢弃/
int throttle;
/netif_rx函数返回的拥塞级别/
int cng_level;
int avg_blog;
/*softnet_data 结构包含一个指向接收和传输队列的指针,input_pkt_queue成员指向准备传送
给网络层的sk_buffs包链表的首部的指针,这个队列中的包是由netif_rx函数递交的*/
struct sk_buff_head input_pkt_queue;

    struct list_head        poll_list;
    struct net_device        *output_queue;
    struct sk_buff                *completion_queue;


    struct net_device        backlog_dev;        /* Sorry. 8) */

};

内核使用了一个同名的变量softnet_data,它是一个Per-CPU变量,每个CPU都有一个。

net/core/dev.c

[Copy to clipboard] [ - ] CODE:
DECLARE_PER_CPU(struct softnet_data,softnet_data);

[Copy to clipboard] [ - ] CODE:
/*
* 网络模块的核心处理模块.
*/
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;

    BUG_ON(!dev_boot_phase);

    net_random_init();

    if (dev_proc_init())                /*初始化proc文件系统*/
            goto out;

    if (netdev_sysfs_init())        /*初始化sysfs文件系统*/
            goto out;

    /*ptype_all和ptype_base是重点,后面会详细分析,它们都是
    struct list_head类型变量,这里初始化链表成员*/
    INIT_LIST_HEAD(&ptype_all);
    for (i = 0; i < 16; i++) 
            INIT_LIST_HEAD(&ptype_base[i]);

    for (i = 0; i < ARRAY_SIZE(dev_name_head); i++)
            INIT_HLIST_HEAD(&dev_name_head[i]);

    for (i = 0; i < ARRAY_SIZE(dev_index_head); i++)
            INIT_HLIST_HEAD(&dev_index_head[i]);

    /*
     *        初始化包接收队列,这里我们的重点了.
     */

    /*遍历每一个CPU,取得它的softnet_data,我们说过,它是一个struct softnet_data的Per-CPU变量*/
    for (i = 0; i < NR_CPUS; i++) {
            struct softnet_data *queue;

            /*取得第i个CPU的softnet_data,因为队列是包含在它里边的,所以,我会直接说,“取得队列”*/
            queue = &per_cpu(softnet_data, i);
            /*初始化队列头*/
            skb_queue_head_init(&queue->input_pkt_queue);
            queue->throttle = 0;
            queue->cng_level = 0;
            queue->avg_blog = 10; /* arbitrary non-zero */
            queue->completion_queue = NULL;
            INIT_LIST_HEAD(&queue->poll_list);
            set_bit(__LINK_STATE_START, &queue->backlog_dev.state);
            queue->backlog_dev.weight = weight_p;
            /*这里,队列中backlog_dev设备,它是一个伪网络设备,不对应任何物理设备,它的poll函数,指向了
            process_backlog,后面我们会详细分析*/
            queue->backlog_dev.poll = process_backlog;
            atomic_set(&queue->backlog_dev.refcnt, 1);
    }

ifdef OFFLINE_SAMPLE

    samp_timer.expires = jiffies + (10 * HZ);
    add_timer(&samp_timer);

endif

    dev_boot_phase = 0;

    /*注册收/发软中断*/
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);

    hotcpu_notifier(dev_cpu_callback, 0);
    dst_init();
    dev_mcast_init();
    rc = 0;

out:
return rc;
}

这样,初始化完成后,在驱动程序中,在中断处理函数中,会调用netif_rx将数据交上来,这与采用轮询技术,有本质的不同:

[Copy to clipboard] [ - ] CODE:
int netif_rx(struct sk_buff *skb)
{
int this_cpu;
struct softnet_data *queue;
unsigned long flags;

    /* if netpoll wants it, pretend we never saw it */
    if (netpoll_rx(skb))
            return NET_RX_DROP;

    /*接收时间戳未设置,设置之*/
    if (!skb->stamp.tv_sec)
            net_timestamp(&skb->stamp);

    /*
     * 这里准备将数据包放入接收队列,需要禁止本地中断,在入队操作完成后,再打开中断.
     */
    local_irq_save(flags);
    /*获取当前CPU对应的softnet_data变量*/
    this_cpu = smp_processor_id();
    queue = &__get_cpu_var(softnet_data);

    /*接收计数器累加*/
    __get_cpu_var(netdev_rx_stat).total++;

    /*接收队列是否已满*/
    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); /将数据包加入接收队列/

ifndef OFFLINE_SAMPLE

                    get_sample_stats(this_cpu);

endif

                    local_irq_restore(flags);
                    return queue->cng_level;
            }

            /*
            * 驱动程序不断地调用net_rx函数,实现接收数据包的入队操作,当qlen == 0时,则进入这段代码,这里,如果已经被设置拥塞标志的话,则清除它,因为这里将要调用软中断,开始将数据包交给上层了,即上层协议的接收函数将执行出队操作,拥塞自然而然也就不存在了。 */
            if (queue->throttle)
                    queue->throttle = 0;

            /*
             * netif_rx_schedule函数完成两件重要的工作:
             * 1、将bakclog_dev设备加入“处理数据包的设备”的链表当中;
             * 2、触发软中断函数,进行数据包接收处理;
             */
            netif_rx_schedule(&queue->backlog_dev);
            goto enqueue;
    }

    /*前面判断了队列是否已满,如果已满而标志未设置,设置之,并累加拥塞计数器*/
    if (!queue->throttle) {
            queue->throttle = 1;
            __get_cpu_var(netdev_rx_stat).throttled++;
    }

/拥塞发生,累加丢包计数器,释放数据包/
drop:
__get_cpu_var(netdev_rx_stat).dropped++;
local_irq_restore(flags);

    kfree_skb(skb);
    return NET_RX_DROP;

}

从这段代码的分析中,我们可以看到,当第一个数据包被接收后,因为qlen==0,所以首先会调用netif_rx_schedule触发软中断,然后利用goto跳转至入队。因为软中断被触发后,将执行出队操作,把数据交往上层处理。而当这个时候,又有数据包进入,即网卡中断产生,因为它的优先级高过软中断,这样,出队操作即被中断,网卡中断程序再将被调用,netif_rx函数又再次被执行,如果队列未满,就入队返回。中断完成后,软中断的执行过程被恢复而继续执行出队——如此生产者/消费者循环不止,生生不息……

netif_rx调用netif_rx_schedule进一步处理数据包,我们注意到:
1、前面讨论过,采用轮询技术时,同样地,也是调用netif_rx_schedule,把设备自己传递了过去;
2、这里,采用中断方式,传递的是队列中的一个“伪设备”,并且,这个伪设备的poll函数指针,指向了一个叫做process_backlog的函数;

netif_rx_schedule函数完成两件重要的工作:
1、将bakclog_dev设备加入“处理数据包的设备”的链表当中;
2、触发软中断函数,进行数据包接收处理;

这样,我们可以猜想,在软中断函数中,不论是伪设备bakclog_dev,还是真实的设备(如前面讨论过的e100),都会被软中断函数以:
dev->poll()
的形式调用,对于e100来说,poll函数的接收过程已经分析了,而对于其它所有没有采用轮询技术的网络设备来说,它们将统统调用
process_backlog函数(我觉得把它改名为pseudo-poll是否更合适一些^o^)。

OK,我想分析到这里,关于中断处理与轮询技术的差异,已经基本分析开了……

继续来看,netif_rx_schedule进一步调用__netif_rx_schedule:

[Copy to clipboard] [ - ] CODE:
/* Try to reschedule poll. Called by irq handler. */

static inline void netif_rx_schedule(struct net_device *dev)
{
if (netif_rx_schedule_prep(dev))
__netif_rx_schedule(dev);
}

[Copy to clipboard] [ - ] CODE:
/* Add interface to tail of rx poll list. This assumes that _prep has
* already been called and returned 1.
*/

static inline void __netif_rx_schedule(struct net_device *dev)
{
unsigned long flags;

    local_irq_save(flags);
    dev_hold(dev);
    /*伪设备也好,真实的设备也罢,都被加入了队列层的设备列表*/
    list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
    if (dev->quota < 0)
            dev->quota += dev->weight;
    else
            dev->quota = dev->weight;
    /*触发软中断*/
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    local_irq_restore(flags);

}

软中断被触发,注册的net_rx_action函数将被调用:

[Copy to clipboard] [ - ] CODE:
/接收的软中断处理函数/
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *queue = &__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;
int budget = netdev_max_backlog;

    local_irq_disable();

    /*
     * 遍历队列的设备链表,如前所述,__netif_rx_schedule已经执行了
     * list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
     * 设备bakclog_dev已经被添加进来了
     */
    while (!list_empty(&queue->poll_list)) {
            struct net_device *dev;

            if (budget <= 0 || jiffies - start_time > 1)
                    goto softnet_break;

            local_irq_enable();

            /*取得链表中的设备*/
            dev = list_entry(queue->poll_list.next,
                             struct net_device, poll_list);
            netpoll_poll_lock(dev);

            /*调用设备的poll函数,处理接收数据包,这样,采用轮询技术的网卡,它的真实的poll函数将被调用,
            这就回到我们上一节讨论的e100_poll函数去了,而对于采用传统中断处理的设备,它们调用的,都将是
            bakclog_dev的process_backlog函数*/
            if (dev->quota <= 0 || dev->poll(dev, &budget)) {
                    netpoll_poll_unlock(dev);

                    /*处理完成后,把设备从设备链表中删除,又重置于末尾*/
                    local_irq_disable();
                    list_del(&dev->poll_list);
                    list_add_tail(&dev->poll_list, &queue->poll_list);
                    if (dev->quota < 0)
                            dev->quota += dev->weight;
                    else
                            dev->quota = dev->weight;
            } else {
                    netpoll_poll_unlock(dev);
                    dev_put(dev);
                    local_irq_disable();
            }
    }

out:
local_irq_enable();
return;

softnet_break:
__get_cpu_var(netdev_rx_stat).time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
}

对于dev->poll(dev, &budget)的调用,一个真实的poll函数的例子,我们已经分析过了,现在来看process_backlog,

[Copy to clipboard] [ - ] CODE:
static int process_backlog(struct net_device *backlog_dev, int *budget)
{
int work = 0;
int quota = min(backlog_dev->quota, *budget);
struct softnet_data *queue = &__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;

    backlog_dev->weight = weight_p;

    /*在这个循环中,执行出队操作,把数据从队列中取出来,交给netif_receive_skb,直至队列为空*/
    for (;;) {
            struct sk_buff *skb;
            struct net_device *dev;

            local_irq_disable();
            skb = __skb_dequeue(&queue->input_pkt_queue);
            if (!skb)
                    goto job_done;
            local_irq_enable();

            dev = skb->dev;

            netif_receive_skb(skb);

            dev_put(dev);

            work++;

            if (work >= quota || jiffies - start_time > 1)
                    break;

    }

    backlog_dev->quota -= work;
    *budget -= work;
    return -1;

/当队列中的数据包被全部处理后,将执行到这里/
job_done:
backlog_dev->quota -= work;
*budget -= work;

    list_del(&backlog_dev->poll_list);
    smp_mb__before_clear_bit();
    netif_poll_enable(backlog_dev);

    if (queue->throttle)
            queue->throttle = 0;
    local_irq_enable();
    return 0;

}

这个函数重要的工作,就是出队,然后调用netif_receive_skb()将数据包交给上层,这与上一节讨论的poll是一样的。这也是为什么,
在网卡驱动的编写中,采用中断技术,要调用netif_rx,而采用轮询技术,要调用netif_receive_skb啦!

到了这里,就处理完数据包与设备相关的部分了,数据包将进入上层协议栈……

刚来实验室的时候主要看的就是数据报在协议栈的具体传输过程,当时有过记录,但是很凌乱,最近又回头看了看相关知识和内核源代码,算是理清了思路,特整理 在此.本篇笔记写的是2.4中数据报的接收过程,从网卡到网络层的具体路线,2.4中大部分网卡采用的是中断的方式接收数据(好像是从2.5以后开始支持 NAPI的,不太确定),本篇笔记总结的是非NAPI,即采用中断接受数据的路线.ok,开始进入主题.

当网卡接收到一个数据报之后,产生一个中断通知内核,然后内核会调用相关的中断处理函数.一般,中断处理程序做如下工作:
1,把数据报拷贝到一个sk_buff中.
2,初始化sk_buff中的一些成员变量,为以后传输到上层用,特别是skb->protocol,标示了上层的具体协议,后面会调用相应的接收函数.
3,更新网卡的状态.
4,调用netif_rx将数据报送往上层(采用NAPI时,此处将调用netif_rx_schdule).

看netif_rx源代码之前首先要了解一下softnet_data结构.此结构是基于cpu的而不是device,即每个cpu对应一个softnet_data.
struct softnet_data
{
/throttle用于拥塞控制,当拥塞时被设置,此后来的数据包都被丢弃/
int throttle;
/netif_rx返回的拥塞级别/
int cng_level;
int avg_blog;
/input_pkt_queue是skb的队列,接收到的skb全都进入到此队列等待后续处理/
struct sk_buff_head input_pkt_queue;
/poll_list是一个双向链表,链表的成员是有接收数据等待处理的device/
struct list_head poll_list;
/net_device链表,成员为有数据报要发送的device/
struct net_device *output_queue;
/完成发送的数据包等待释放的队列/
struct sk_buff *completion_queue;
/注意,backlog_dev不是一个指针,而是一个net_device实体,代表了调用net_rx_action时的device/
struct net_device backlog_dev;
};

ok,了解了softnet_data结构体后接下来看netif_rx的源代码.

/*
*主要工作:
*1,初始化sk_buff的一些域值,比如数据报接收的时间截.
*2,把接收到的数据报入input_pkt_queue接收队列,并且通知内核然后触发相应的软中断,即NET_RX_SOFTIRQ.
* 2.1:当队列为空时,调用netif_rx_schedule,触发软中断.
* 2.2:当队列非空时,直接将数据报入队列,因为此时已经调用了软中断,所以无需再调用.
*3,更新相关状态信息.
*执行完后,流程来到net_rx_action
*/
int netif_rx(struct sk_buff *skb)
{
int this_cpu = smp_processor_id();
struct softnet_data *queue;
unsigned long flags;
//如果接收到的数据包时间截未设置,设置时间截
if (skb->stamp.tv_sec == 0)
do_gettimeofday(&skb->stamp);
queue = &softnet_data[this_cpu];
local_irq_save(flags); //disable irqs on local cpu
netdev_rx_stat[this_cpu].total++;
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); //即automic_inc(&(dev)->refcnt)累加设备引入计数器
__skb_queue_tail(&queue->input_pkt_queue,skb); //把skb入input_pkt_queue队列,此处只是指针的指向,而不是数据报的拷贝,目的是节省时间.
local_irq_restore(flags); //eable irqs on local cpu

ifndef OFFLINE_SAMPLE

        get_sample_stats(this_cpu);

endif

        return queue->cng_level;//返回拥塞等级
    }
 //驱动程序不断的调用netif_rx,将数据包入队操作,当qlen==0时,执行下面代码
 //如果设置了拥塞位,将其设为0
    if (queue->throttle) {
        queue->throttle = 0;

ifdef CONFIG_NET_HW_FLOWCONTROL

        if (atomic_dec_and_test(&netdev_dropping))
            netdev_wakeup();

endif

    }
    /*
     netif_rx_schedule主要完成两件事
     1):将接收skb的device加入"处理数据包的设备"的链表当中
     2):触发软中断函数,进行数据包接收处理,接收软中断的处理函数为net_rx_action
    */
    netif_rx_schedule(&queue->backlog_dev); //只有当input_pkt_queue为空时才调用,非空时只是将数据报入队列,因为如果队列非空,则已经调用了NET_RX_SOFTIRQ软中 断,所以没必要在执行netif_rx_schedule去调用软中断了. 
    goto enqueue;
}
/*如果队列无空闲空间,设置拥塞位*/
if (queue->throttle == 0) {
    queue->throttle = 1;
    netdev_rx_stat[this_cpu].throttled++;

ifdef CONFIG_NET_HW_FLOWCONTROL

    atomic_inc(&netdev_dropping);

endif

}

drop:
netdev_rx_stat[this_cpu].dropped++;
local_irq_restore(flags);
kfree_skb(skb);
return NET_RX_DROP;
}

看完netif_rx后有一个问题,当队列为空的时候会调用netif_rx_schedule,此函数将会把接收skb的device连接到poll_list链表,但如果队列非空时,为什么直接把数据放到input_pkt_queue里了?
想了想,应该是因为这样:因为采用NAPI技术的网卡接收到数据报后不会调用netif_rx,而直接调用netif_rx_schedule,所以调用 netif_rx的都是非NAPI的网卡,那么默认的包处理函数都是process_backlog,也就是说,所有数据报的处理函数是一样的,所以当队 列非空时,直接将接收到的skb放入接收队列即可.
以上纯粹是个人的理解,不知道对不对,如果不对,有知道的朋友希望告诉下,谢谢.

netif_rx返回后,由netif_rx_schedule调用软中断处理函数,所以控制权转交到net_rx_action.

/*
*接收软中断NET_RX_SOFTIRQ的处理函数
*当调用poll_list中的device时,如果网卡采用的是NAPI技术,则调用网卡自定义的poll函数,如果是非NAPI(目前大多数网卡都是采用此技术),则调用默认的process_backlog函数.
*由于此笔记研究的是非NAPI,所以控制权转交到process_backlog函数.
*/
static void net_rx_action(struct softirq_action *h)
{
int this_cpu = smp_processor_id();
struct softnet_data *queue = &softnet_data[this_cpu]; //获得与cpu相关的softnet_data,因为每个cpu只有一个softnet_data
unsigned long start_time = jiffies;
int budget = netdev_max_backlog; //默认值为300,系统每次从队列中最多取出300个skb处理
br_read_lock(BR_NETPROTO_LOCK);
local_irq_disable();
while (!list_empty(&queue->poll_list)) {
struct net_device *dev;
//当处理时间持续超过一个时钟滴答时,会再出发一个中断NET_RX_SOFTIRQ
if (budget = 0 || jiffies - start_time > 1)
goto softnet_break;
local_irq_enable();
//取得poll_list链表中的设备
dev = list_entry(queue->poll_list.next, struct net_device, poll_list);
//调用设备的poll函数,处理接收数据包,采用轮寻技术的网卡,将调用它真实的poll函数
//而对于采用传统中断处理的设备,它们调用的都将是backlog_dev的process_backlog函数
//如果一次poll未处理完全部数据报,则将device移至链表尾部,等待下一次调用.
if (dev->quota = 0 || dev->poll(dev, &budget)) {
//由于要对softnet_data进行操作,则必须禁止中断.
local_irq_disable();
//把device从表头移除,移到链表尾
list_del(&dev->poll_list);
list_add_tail(&dev->poll_list, &queue->poll_list);
if (dev->quota 0)
dev->quota += dev->weight;
else
dev->quota = dev->weight;
} else {
dev_put(dev); //当device中的数据报被处理完毕(网卡采用NAPI技术时),递减device的引用计数 dcreases the reference count .
local_irq_disable();
}
}
local_irq_enable();
br_read_unlock(BR_NETPROTO_LOCK);
return;
softnet_break:
netdev_rx_stat[this_cpu].time_squeeze++;
//如果此时又有中断发生,出发NET_RX_SOFTIRQ中断,再此调用net_rx_action函数.
__cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ);
local_irq_enable();
br_read_unlock(BR_NETPROTO_LOCK);
}

软中断处理函数的主体既是调用数据报的处理函数,网卡采用NAPI技术的情况下,则调用device本身定义的poll函数,如果是非NAPI,则调用的 是默认的process_backlog函数,既然本笔记研究的是非NAPI的情况,那么下面来研究下process_backlog函数.

/*
*把数据报从input_pkt_queue中去出来后,交由netif_receive_skb处理,netif_receive_skb为真正的包处理函数.
*注意,无论是基于NAPI还是非NAPI,最后的包处理函数都是netif_receive_skb.
*/
static int process_backlog(struct net_device *blog_dev, int *budget)
{
int work = 0;

//quota为一次处理数据包的数量,blog_dev->quota的值由netif_rx_schedule初始化为全局变量weight_p的值,默认值为64
int quota = min(blog_dev->quota, *budget);
int this_cpu = smp_processor_id();
struct softnet_data *queue = &softnet_data[this_cpu];
unsigned long start_time = jiffies;

//循环取出skb,交由netif_receive_skb处理,直至队列为空
for (;;) {
struct sk_buff *skb;
struct net_device *dev;
local_irq_disable();
skb = __skb_dequeue(&queue->input_pkt_queue);
if (skb == NULL)
goto job_done;
local_irq_enable();
dev = skb->dev;
//注意此处,取出数据报后,直接交由netif_receive_skb处理.
netif_receive_skb(skb);
dev_put(dev); //dev引用计数-1
work++;
if (work >= quota || jiffies - start_time > 1)
break;

ifdef CONFIG_NET_HW_FLOWCONTROL

    if (queue->throttle && queue->input_pkt_queue.qlen  no_cong_thresh ) {
        if (atomic_dec_and_test(&netdev_dropping)) {
            queue->throttle = 0;
            netdev_wakeup();
            break;
        }
    }

endif

}
//更新quota
blog_dev->quota -= work;
*budget -= work;
return -1;

job_done:
blog_dev->quota -= work;
*budget -= work;
list_del(&blog_dev->poll_list);
clear_bit(__LINK_STATE_RX_SCHED, &blog_dev->state);
if (queue->throttle) {
queue->throttle = 0;

ifdef CONFIG_NET_HW_FLOWCONTROL

    if (atomic_dec_and_test(&netdev_dropping))
        netdev_wakeup();

endif

}
local_irq_enable();
return 0;

}
注意,就像注释中所说,无论NAPI还是非NAPI,最后的包处理函数都是netif_receive_skb.所以此函数比较重要.下面来分析下此函数.
/*
netif_receive_skb作用:
对每一个接收到的skb,到已注册的协议类型中去匹配,先是匹配ptype_all链表,ptype_all中注册的struct packet_type表示要接收处理所有协议的数据,对于
匹配到的struct packet_tpye结构(dev为NULL或者dev等于skb的dev),调用其func成员,把skb传递给它处理.
匹配完ptype_all后,再匹配ptype_base数组中注册的协议类型,skb有一个成员protocol,其值即为以太网首部中的帧类型,在ptype_base中匹配到协议相同,并且
dev符合要求的,调用其func成员即可.
此函数中数据报的流向:sniffer(如果有)->Diverter(分流器)->bridge(如果有)->l3协议的处理函数(比如ip协议的ip_rcv)
*/
int netif_receive_skb(struct sk_buff *skb)
{
//packet_type结构体见下面
struct packet_type *ptype, *pt_prev;
int ret = NET_RX_DROP;
unsigned short type = skb->protocol;

//如果数据包没设置时间截,设置之
if (skb->stamp.tv_sec == 0)
    do_gettimeofday(&skb->stamp);
skb_bond(skb); //使skb->dev指向主设备,多个interfaces可以在一起集中管理,这时候要有个头头管理这些接口,如果skb对应的device来自这样一个group,
 //则传递到L3层之前应使skb->dev指向master
netdev_rx_stat[smp_processor_id()].total++;

ifdef CONFIG_NET_FASTROUTE

if (skb->pkt_type == PACKET_FASTROUTE) {
    netdev_rx_stat[smp_processor_id()].fastroute_deferred_out++;
    return dev_queue_xmit(skb);
}

endif

skb->h.raw = skb->nh.raw = skb->data;
pt_prev = NULL;

//ptype_all是双向链表,ptpye_base是一个哈希表
//初始化时协议类型为ETH_P_ALL时,将packet_type结构加入到ptype_all列表中    
//如果不是,模15后加到ptype_base数组中,此数组相当于hash链表

//这里针对协议类型为ETH_P_ALL的情况进行处理,对于ip协议来说
//类型定义为ETH_P_IP,因此不在这里处理

for (ptype = ptype_all; ptype; ptype = ptype->next) {

//处理器处理所有的网络设备接收的包或找到设备匹配的包处理器
if (!ptype->dev || ptype->dev == skb->dev) {
if (pt_prev) {
//老版本内核时
if (!pt_prev->data) {
/* Deliver skb to an old protocol, which is not threaded well
or which do not understand shared skbs.
*/
ret = deliver_to_old_ones(pt_prev, skb, 0);
} else {
//发给相应的处理函数,下面两条相当于deliver_skb
//有协议对skb处理,所以use加1
atomic_inc(&skb->users);
//传递的是skb的指针,所以可以看出是原数据报本身.即如果在此修改skb,则会影响到后面的数据流向.
ret = pt_prev->func(skb, skb->dev, pt_prev);
}
}
pt_prev = ptype;
}
}

ifdef CONFIG_NET_DIVERT

/如果配置有DIVERT(分流器),则交由分流器处理/
if (skb->dev->divert && skb->dev->divert->divert)
ret = handle_diverter(skb);

endif /* CONFIG_NET_DIVERT */

/*如果配置有BRIDGE或者有BRIDGE模块,则交由桥处理*/        

if defined(CONFIG_BRIDGE) || defined(CONFIG_BRIDGE_MODULE)

if (skb->dev->br_port != NULL &&
 br_handle_frame_hook != NULL) {
    return handle_bridge(skb, pt_prev);
}

endif

//这里针对各种协议进行处理,eg:ip包的类型为ETH_P_IP,因此在这里处理
//&15的意思是模15
for (ptype=ptype_base[ntohs(type)&15];ptype;ptype=ptype->next) {
if (ptype->type == type &&
(!ptype->dev || ptype->dev == skb->dev)) {
if (pt_prev) { //pt_prev指向具体的协议类型
if (!pt_prev->data) {
ret = deliver_to_old_ones(pt_prev, skb, 0);
} else {
atomic_inc(&skb->users);
ret = pt_prev->func(skb, skb->dev, pt_prev);
}
}
pt_prev = ptype;
}
}
//1:当上面的两个数组只有一个元素时
//2:访问上面两个数组的最后一个ptype_type,执行下面的语句
if (pt_prev) {
if (!pt_prev->data) {
ret = deliver_to_old_ones(pt_prev, skb, 1);
} else {
//当只有一个协议的时候,user不加1
ret = pt_prev->func(skb, skb->dev, pt_prev);
}
} else { ///表示搜索完ptype_all和ptype_base后没找到匹配的,free掉skb
kfree_skb(skb);
/* Jamal, now you will not able to escape explaining
* me how you were going to use this. :-)
*/
ret = NET_RX_DROP;
}
return ret;
}
其中packet_type的定义如下:
struct packet_type
{
unsigned short type;
//一般的dev设置为NULL,表示将接收从任何设备接收的数据包
struct net_device *dev;
int (func)(struct sk_buff,strcut net_device*,
struct packet_type *); //接收处理函数
void *data;//private to the packet type
struct packet_type *next;
};
注意,在网络初始化代码里有这么一条语句:struct packet_type * ptype_all = NULL.就是说ptype_all在初始化的时候为空,用于指向Eth_P_ALL类型的packet_type结构,注册在这里的函数,可以接收到所 有的数据报.包括输出的数据包,看代码可知,由于传递的是一个指针,所以在此修改skb的一切操作将会影响后面的处理过程.
如果想要在数据包传递到网络层之前对数据报进行处理,则可以考虑的地点可以有如下几个:
sniffer,diverter,bridge. 其中sniffer本人已经亲自实现过,即在ptype_all中注册自己的函数,可以接收到所有出去或者进入的数据包.
最后,数据包会传到l3层,如果是ip协议,则相应的处理函数为ip_rcv,到此数据报从网卡到l3层的接收过程已经完毕.即总的路线 是:netif_rx–>net_rx_action–>process_backlog–>netif_receive_skb–>sniffer(如 果有)–>diverter(如果有)–>bridge(如果有)–>ip_rcv(或者其他的l3层协议处理函数)
ps1:弄了好几天,终于算把这个数据包的接收过程系统的过了一遍,看过源代码,感觉收获不小,但是觉得知道的还不是很透彻,其中的设计理念等以后有机会 一定要好好的研究研究.由于本次涉及到的东西相对来说比较多,而且涉及到内核网络部分,本人很菜,难免有理解错误的地方,如果有错误,请看到的朋友能够及 时给予指正,不胜感激.
ps2:从在网上查找资料,看相关书籍到通读这一过程的源代码,本人花费了很多时间,因此对本篇笔记很是喜爱和重视,如果有需要转载的朋友,请注明来自hmily8023.cublog.cn,谢谢.

一、硬件布局
(转)网络数据包收发流程(三):e1000网卡和DMA
每个网卡(MAC)都有自己的专用DMA Engine,如上图的 TSEC 和 e1000 网卡intel82546。
上图中的红色线就是以太网数据流,DMA与DDR打交道需要其他模块的协助,如TSEC,PCI controller
以太网数据在 TSEC<–>DDR PCI_Controller<–>DDR 之间的流动,CPU的core是不需要介入的
只有在数据流动结束时(接收完、发送完),DMA Engine才会以外部中断的方式告诉CPU的core

二、DMA Engine
(转)网络数据包收发流程(三):e1000网卡和DMA
上面是DMA Engine的框图,以接收为例:
1.在System memory中为DMA开辟一端连续空间,用来BD数组 (一致性dma内存)
BD是给DMA Engine使用的,所以不同的设备,BD结构不同,但是大致都有状态、长度、指针3个成员。

2.初始化BD数组,status为E,length为0
在System memory中再开辟一块一块的内存,可以不连续,用来存放以太网包
将这些内存块的总线地址赋给buf(dma映射)

3.当MAC接收以太网数据流,放在了Rx FIFO中

4.当一个以太网包接收完全后,DMA engine依次做以下事情
fetch bd:开始一个个的遍历BD数组,直到当前BD状态为Empty为止
update bd:更新BD状态为Ready
move data:把数据从Rx FIFO中搬移到System Memory中dma映射的部分
generate interrupt:数据搬移完了,产生外部中断给cpu core

5.cpu core处理外部中断,此时以太网数据已经在System memory中dma映射的部分了
解除dma映射,更新bd状态为Empty
再开辟一端内存,将这块内存的总线地址赋给bd的指针字段

三、内核中DMA相关API

void *dma_alloc_cohrent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
功能:分配一致性dma内存,返回这块内存的虚拟地址EA, 这块内存的物理地址保存在 dma_handle
dev: NULL也行
size: 分配空间的大小
dma_handle: 用来保存内存的总线地址(物理地址)
注意:一致性DMA映射,BD所占内存就是靠dma_alloc_cohrent来分配的。

dma_addr_t *dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction);
功能:将一块连续的内存 buffer 映射为DMA内存来使用。映射后,CPU不能再操作这块 buffer
返回:这块buffer的总线地址(物理地址)
dev: NULL也行
buffer: 一块连续内存的虚拟地址EA
size: 连续内存的大小
dma_data_direction: dma数据流的方向
注意:流式DMA映射,以太网包所占内存先通过kmalloc来分配,然后通过dma_map_single来映射给bd的

四、e1000驱动中的DMA

网卡驱动中使用DMA的套路差不多都一样,以e1000驱动为例讲一下(TSEC驱动的dma见这里)

4.1 加载e1000网卡驱动

e1000_probe(){ //主要是初始化钩子函数
netdev = alloc_etherdev(sizeof(struct e1000_adapter));
netdev->open = &e1000_open; //重要
netdev->stop = &e1000_close;
netdev->hard_start_xmit = &e1000_xmit_frame;
netdev->get_stats = &e1000_get_stats;
netdev->set_multicast_list = &e1000_set_multi;
netdev->set_mac_address = &e1000_set_mac;
netdev->change_mtu = &e1000_change_mtu;
netdev->do_ioctl = &e1000_ioctl;
e1000_set_ethtool_ops(netdev);
netdev->tx_timeout = &e1000_tx_timeout;
netdev->watchdog_timeo = 5 * HZ;

ifdef CONFIG_E1000_NAPI

netif_napi_add(netdev, &adapter->napi, e1000_clean, 64); //重要

endif

}

4.1 启动e1000网卡

e1000_open() //当用户敲ifconfig up命令时,最终调用网卡驱动的open函数
–>e1000_setup_all_rx_resources(adapter)
–>e1000_setup_rx_resources(adapter, &adapter->rx_ring[i])
//给rx bd分配一致性dma内存
rxdr->desc = pci_alloc_consistent(pdev, rxdr->size, &rxdr->dma);
–>e1000_configure(adapter)
–>e1000_configure_rx(adapter)
adapter->clean_rx = e1000_clean_rx_irq;
adapter->alloc_rx_buf = e1000_alloc_rx_buffers;
–>调用 adapter->alloc_rx_buf钩子函数,即 e1000_alloc_rx_buffers
–> skb = netdev_alloc_skb(netdev, bufsz); //调用kmalloc新建一个skb
buffer_info->dma = pci_map_single(pdev,
skb->data,
adapter->rx_buffer_len,
PCI_DMA_FROMDEVICE); //给skb->data建立DMA映射
rx_desc->buffer_addr = cpu_to_le64(buffer_info->dma);//初始化bd的buf指针
–>e1000_request_irq(adapter);
//挂rx 中断ISR函数为 e1000_intr()

最终bd数据结构应该是下面这个样子
(转)网络数据包收发流程(三):e1000网卡和DMA
4.2 e1000的中断

注意:e1000产生rx中断时,以太网数据包已经在系统内存中,即在skb->data里面
下面的中断处理过程就简略了,详细的看这里
do_IRQ()
{
中断上半部
调用e1000网卡的rx中断函数 e1000_intr()
触发软中断 (使用NAPI的话)
中断下半部
依次调用软中断的所有handler
在net_rx_action中最终调用e1000的napi_struct.poll()钩子函数,即e1000_clean
e1000_clean()最终调用 e1000_clean_rx_irq()
}

e1000_clean_rx_irq()
{
rx_desc = E1000_RX_DESC(*rx_ring, i); //获取rx bd
status = rx_desc->status;
skb = buffer_info->skb;
buffer_info->skb = NULL;

 pci_unmap_single(pdev,                //解除skb->data的DMA映射
               buffer_info->dma,
               buffer_info->length,
               PCI_DMA_FROMDEVICE);
 length = le16_to_cpu(rx_desc->length);
 length -= 4;                          //以太网包的FCS校验就不要了
 skb_put(skb, length);
 skb->protocol = eth_type_trans(skb, netdev);
 netif_receive_skb(skb);               //skb进入协议栈

}

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值