5.2. 8139CP 的 NIC 中断:
static irqreturn_t cp_interrupt (int irq, void *dev_instance, struct pt_regs *regs) { struct net_device *dev = dev_instance; struct cp_private *cp = dev->priv; u16 status; /*检查rx-ring中是否有中断到达*/ status = cpr16(IntrStatus); if (!status || (status == 0xFFFF)) return IRQ_NONE; if (netif_msg_intr(cp)) printk(KERN_DEBUG "%s: intr, status %04x cmd %02x cpcmd %04x\n", dev->name, status, cpr8(Cmd), cpr16(CpCmd)); /*清除NIC中断控制器的内容*/ cpw16(IntrStatus, status & ~cp_rx_intr_mask); spin_lock(&cp->lock); /*接收状态寄存器表示有数据包到达*/ if (status & (RxOK | RxErr | RxEmpty | RxFIFOOvr)) { /*把当前的产生中断的NIC设备挂在softnet_data中的POLL队列上,等待网络上层上的应用程序处理*/ if (netif_rx_schedule_prep(dev)) { /*关闭接收中断使能*/ cpw16_f(IntrMask, cp_norx_intr_mask); __netif_rx_schedule(dev); } } /*发送中断的处理过程以及8139C+的专门软中断的处理过程,这里我们不关心*/ if (status & (TxOK | TxErr | TxEmpty | SWInt)) cp_tx(cp); /*如果发生链路变化的情况,需要检查介质无关接口(MII)的载波状态同样也发生变化, 否则就要准备重新启动MII接口*/ if (status & LinkChg) mii_check_media(&cp->mii_if, netif_msg_link(cp), FALSE); /*如果PCI总线发生错误,需要对8139C+的设备重新复位*/ if (status & PciErr) { u16 pci_status; pci_read_config_word(cp->pdev, PCI_STATUS, &pci_status); pci_write_config_word(cp->pdev, PCI_STATUS, pci_status); printk(KERN_ERR "%s: PCI bus error, status=%04x, PCI status=%04x\n", dev->name, status, pci_status); /* TODO: reset hardware */ } spin_unlock(&cp->lock); return IRQ_HANDLED; }
|
5.3. 把 NIC 挂在 POLL 队列(poll_list)上
在 8139CP 的中断程序可以看到 __netif_rx_schedule 的调用方式,它把 NIC 设备挂在softnet_data 结构中的 poll_list 队列上,以便及时的返回中断,让专门数据包处理 bottom-half部分来进行处理,我们先来看一下 __netif_rx_schedule 的内部工作流程。
static inline void __netif_rx_schedule(struct net_device *dev) { unsigned long flags; local_irq_save(flags); dev_hold(dev); /*把当前NIC设备挂在POLL(poll_list)队列中,等待唤醒软中断以后进行轮询*/ 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; /*启动软中断,在表示所有中断的状态字irq_cpustat_t中关于软中断字段__softirq_pending中, 把关于网络轮循接收软中断位置1,等待调度时机来临时候运行该中断的句柄net_rx_action。*/ __raise_softirq_irqoff(NET_RX_SOFTIRQ); local_irq_restore(flags); }
|
5.4. 由 __netif_rx_schedule 启动的软中断的处理过程分析
软中断事件触发前已经在此设备子系统初始化时刻调用 subsys_initcall(net_dev_init) 在软中断控制台上被激活,挂在任务队列 tasklet 上准备在任务调度 schedule 的时刻运行它了,这个里面最主要的部分是调用了 8139C+ 网络设备的 POLL 方法(dev->poll),从网络设备的 rx-ring队列中获得数据,本来它应当放在网络设备中断服务程序中执行的,按照我们前面解释的那样,POLL方法以空间换取时间的机制把它放在软中断部分来执行轮循机制(采用类似老的 Bottom-half 机制也可以达到同样效果,而且更加容易理解一些)在每次进行进程调度的时候就会执行网络设备软中断,轮询 rx-ring 对 NIC 进行数据的接收。
软中断的处理过程:
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;/*表示队列的最大长度*/ /*锁定当前线程,多处理器的情况之下不能被其他处理器中断处理*/ preempt_disable(); local_irq_disable(); /*检查POLL队列(poll_list)上是否有设备在准备等待轮询取得数据*/ while (!list_empty(&queue->poll_list)) { struct net_device *dev; /*这里保证执行当前的 POLL 过程的时间不超过一个时间片,这样不至于被软中断占用太多的时间, 这样在一次调度的时间内执行完毕当前的 POLL 过程,budget 表示一个时间片内最大数据传输的"块数", 块的意思为每个 POLL 所完成 sk_buff数量,每块中间的 sk_buff 数量为 dev->quota 决定,在 8139CP 驱动中, budget 为 300,而 quota 为 16 表示每给时间片最多可以接收到 4.8K 的 sk_buff 数量*/ if (budget <= 0 || jiffies - start_time > 1) goto softnet_break; local_irq_enable(); /*从公共的 softnet_data 数据结构中的轮循队列上获得等待轮循的设备结构*/ dev = list_entry(queue->poll_list.next, struct net_device, poll_list); /*调用设备的POLL方法从NIC上的Ring Buffer中读入数据*/ if (dev->quota <= 0 || dev->poll(dev, &budget)) { /*完成一次POLL过程的数据的接收,重新定义设备接收数据的"配额" (事实上就是sk_buff缓冲区的数量,每次调用POLL方法的时候可以创建并且最 多可以向上层提交的sk_buff缓冲区数目,这个参数很重要在高速处理的时候有需要慎重优化这个数值, 在有大量数据接收的情况下,需要增加该数值)*/ 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 { /*发生了错误的数据接收状况,或者没有完成"规定"配额的数据接收,并且没有新的数据进来, 这个也可能表示已经完成了传输的过程,调用__netif_rx_complete把网络设备从POLL队列上清除 (介绍POLL过程的时候详细介绍)*/ dev_put(dev); local_irq_disable(); } } out: local_irq_enable(); preempt_enable(); return; softnet_break: __get_cpu_var(netdev_rx_stat).time_squeeze++; __raise_softirq_irqoff(NET_RX_SOFTIRQ); goto out; }
|
5.1. 8139CP 驱动中的轮询方法
dev->poll 方法:
这个方法通常被网络层在向驱动的接收循环队列获取新的数据包时刻调用,而驱动的接收循环队列中可以向网络层交付的包数量则在 dev->quota 字段中表示,我们来看 8139cp 中 POLL 的原型:
static int cp_rx_poll (struct net_device *dev, int *budget)
|
参数 budget 的上层任务所需要底层传递的数据包的数量,这个数值不能超过netdev_max_backlog 的值。
总而言之,POLL 方法被网络层调用,只负责按照网络层的要求值("预算"值)提交对应数量的数据包。8139CP 的 POLL 方法注册通常在设备驱动程序模块初始化(调用 probe)的时候进行,如下:
static int cp_init_one (struct pci_dev *pdev, const struct pci_device_id *ent){ … … dev->poll = cp_rx_poll; … … }
|
设备的 POLL 方法正如前所说的是被网络层上的软中断 net_rx_action 调用,我们现在来看具体的流程:
static int cp_rx_poll (struct net_device *dev, int *budget) { struct cp_private *cp = netdev_priv(dev); unsigned rx_tail = cp->rx_tail; /*设定每次进行调度的时候从设备发送到网络层次最大的数据包的大小*/ unsigned rx_work = dev->quota; unsigned rx; rx_status_loop: rx = 0; /*重新打开NIC中断,在 cp_interrupt 中断句柄中中断关闭了,现在 POLl 已经开始处理环行缓冲队列中的数据, 所以中断可以打开,准备接收新的数据包*/ cpw16(IntrStatus, cp_rx_intr_mask); while (1) {/*POLL循环的开始*/ u32 status, len; dma_addr_t mapping; struct sk_buff *skb, *new_skb; struct cp_desc *desc; unsigned buflen; /*从下标为rx_tail的内存中的环行缓冲队列接收队列rx_skb上"摘下"套接字缓冲区*/ skb = cp->rx_skb[rx_tail].skb; if (!skb) BUG(); desc = &cp->rx_ring[rx_tail]; /*检查在 NIC 的环形队列(rx_ring)上的最后的数据接收状态,是否有出现接收或者 FIFO 的错误,是否*/ status = le32_to_cpu(desc->opts1); if (status & DescOwn) break; len = (status & 0x1fff) - 4; mapping = cp->rx_skb[rx_tail].mapping; if ((status & (FirstFrag | LastFrag)) != (FirstFrag | LastFrag)) { /* we don't support incoming fragmented frames. * instead, we attempt to ensure that the * pre-allocated RX skbs are properly sized such * that RX fragments are never encountered */ cp_rx_err_acct(cp, rx_tail, status, len); cp->net_stats.rx_dropped++; cp->cp_stats.rx_frags++; goto rx_next; } if (status & (RxError | RxErrFIFO)) { cp_rx_err_acct(cp, rx_tail, status, len); goto rx_next; } if (netif_msg_rx_status(cp)) printk(KERN_DEBUG "%s: rx slot %d status 0x%x len %d\n", cp->dev->name, rx_tail, status, len); buflen = cp->rx_buf_sz + RX_OFFSET; /*创建新的套接字缓冲区*/ new_skb = dev_alloc_skb (buflen); if (!new_skb) { cp->net_stats.rx_dropped++; goto rx_next; } skb_reserve(new_skb, RX_OFFSET); new_skb->dev = cp->dev; /*解除原先映射的环行队列上的映射区域*/ pci_unmap_single(cp->pdev, mapping, buflen, PCI_DMA_FROMDEVICE); /*检查套接字缓冲区(sk_buff)上得到的数据校验和是否正确*/ /* Handle checksum offloading for incoming packets. */ if (cp_rx_csum_ok(status)) skb->ip_summed = CHECKSUM_UNNECESSARY; else skb->ip_summed = CHECKSUM_NONE; /*按照数据的实际大小重新定义套接字缓冲区的大小*/ skb_put(skb, len); mapping = cp->rx_skb[rx_tail].mapping = /*DMA影射在前面新创建的套接字缓冲区虚拟地址new_buf->tail到实际的物理地址上, 并且把这个物理地址挂在接收缓冲区的队列中*/ pci_map_single(cp->pdev, new_skb->tail, buflen, PCI_DMA_FROMDEVICE); /*把新建立的缓冲区的虚拟地址挂在接收缓冲区的队列中,在下一次访问rx_skb数组的这个结构时候, POLL方法会从这个虚拟地址读出接收到的数据包*/ cp->rx_skb[rx_tail].skb = new_skb; /*在cp_rx_skb调用netif_rx_skb,填充接收数据包队列,等待网络层在Bottom half队列中调用ip_rcv接收网络数据, 这个函数替代了以前使用的netif_rx*/ cp_rx_skb(cp, skb, desc); rx++; rx_next: /*把前面映射的物理地址挂在NIC设备的环行队列上(也就是rx_ring上,它是在和NIC中物理存储区进行了DMA映射的, 而不是驱动在内存中动态建立的),准备提交给下层(NIC)进行数据传输*/ cp->rx_ring[rx_tail].opts2 = 0; cp->rx_ring[rx_tail].addr = cpu_to_le64(mapping); /*在相应的传输寄存器中写入控制字,把rx_ring的控制权从驱动程序交还给NIC硬件*/ if (rx_tail == (CP_RX_RING_SIZE - 1)) desc->opts1 = cpu_to_le32(DescOwn | RingEnd | cp->rx_buf_sz); else desc->opts1 = cpu_to_le32(DescOwn | cp->rx_buf_sz); /*步进到下一个接收缓冲队列的下一个单元*/ rx_tail = NEXT_RX(rx_tail); if (!rx_work--) break; } cp->rx_tail = rx_tail; /*递减配额值quota,一旦quota递减到0表示这次的POLL传输已经完成了使命, 就等待有数据到来的时候再次唤醒软中断执行POLL方法*/ dev->quota -= rx; *budget -= rx; /* if we did not reach work limit, then we're done with * this round of polling */ if (rx_work) { /*如果仍然有数据达到,那么返回POLL方法循环的开始,继续接收数据*/ if (cpr16(IntrStatus) & cp_rx_intr_mask) goto rx_status_loop; /*这里表示数据已经接收完毕,而且没有新的接收中断产生了,这个时候使能NIC的接收中断, 并且调用__netif_rx_complete把已经完成POLL的设备从poll_list上摘除,等待下一次中断产生的时候, 再次把设备挂上poll_list队列中。*/ local_irq_disable(); cpw16_f(IntrMask, cp_intr_mask); __netif_rx_complete(dev); local_irq_enable(); return 0; /* done */ } return 1; /* not done */ }
|
其他的使用 NAPI 的驱动程序和 8139CP 大同小异,只是使用了网络层专门提供的 POLL 方法--proecess_backlog(/net/dev.c),在 NIC 中断接收到了数据包后,调用网络层上的 netif_rx(/net/dev.c)将硬件中断中接收到数据帧存入 sk_buff 结构, 然后检查硬件帧头,识别帧类型, 放入接收队列(softnet_data 结构中的 input_pkt_queue 队列上), 激活接收软中断作进一步处理. 软中断函数(net_rx_action)提取接收包,而 process_backlog(也就是 POLL 方法)向上层提交数据。
6. 能让接收速度更快一些吗?
可行方法
1. 完全取消 NIC 中断,使用 RXOK 位置控制接收中断,
2. 采用定时器中断 timer_list 的控制句柄,根据硬件平台设定一个合适的间隔周期(间隔周期依据平台不同而异),对 rx-ring 直接进行 POLL 轮询,我们在 MIPS 和 Xscale 上直接使用了中断向量 0--irq0 作为对 rx-ring 进行轮询的 top-half(注意我们在上述两个平台上选择的 HZ 数值是 1000,而通常这个数值是 100,并且重新编写了 Wall-time 的记数程序,让 Wall-Time 的记数还是以 10MS 为间隔),当然也可以根据自己的平台和应用程序的状况选择合适的定时时间。
3. 借助 softnet_data 中的 input_pkt_queue 队列,在时钟中断 bottom-half 中完成 POLL 方法之后,并不直接把数据传输到网络层进行处理,而是把 sk_buff 挂在 input_pkt_queue队列上,唤醒软中断在过后处理,当然可以想象,这样需要付出一定的内存代价,而且实时性能也要差一些。
4. 使用 dirty_rx 字段和 refill_rx_ring 函数,在调用完 POLL 方法以后,而且网络层程序比较空闲的时候为一些 rx-ring 中的单元建立新缓冲挂在环形缓冲队列上,这样可以在新的数据包达到的时候节省时间,操作系统不必要手忙脚乱地开辟新的空间去应付新来的数据。
5. 最后请注意:我们上层的应用程序是以网络数据转发为主的,并没有在应用层面上有很多后台进程的复杂的应用,上述的 1 到 4 点中所做的显而易见是以牺牲系统效率整体效率而单独改善了网络数据的处理。
阅读(1000) | 评论(0) | 转发(0) |