网站集约化建设方案,wordpress仿站软件,推广策划公司,wordpress模板结构本文将介绍Linux系统中#xff0c;基于RTL8139网卡驱动程序#xff0c;是如何一步一步将接收到的数据包传送到内核的网络协议栈的。
下图展示了数据包#xff08;packet#xff09;如何进入内存#xff0c;并被内核的网络模块开始处理#xff1a; -----| | …本文将介绍Linux系统中基于RTL8139网卡驱动程序是如何一步一步将接收到的数据包传送到内核的网络协议栈的。
下图展示了数据包packet如何进入内存并被内核的网络模块开始处理 -----| | Memroy
-------- 1 | | 2 DMA --------------------------------
| Packet |--------| NIC |------------| Packet | Packet | Packet | ...... |
-------- | | --------------------------------| |------------- || ---------------| |3 | Raise IRQ | Disable IRQ| 5 || |↓ |----- ------------| | Run IRQ handler | || CPU |------------------| NIC Driver || | 4 | |----- ------------|6 | Raise soft IRQ|↓1.数据包从外部网络传送到物理网卡。如果目的地址不是该网卡且该网卡没有开启混杂模式该包会被网卡丢弃。 2.网卡将数据包以DMA的方式写进指定的内存地址。该地址是由网卡驱动程序分配并初始化的。 3.数据来了之后网卡产生一个硬件中断IRQ告诉CPU。 4.CPU会根据中断向量表调用中断处理函数这个中断处理函数会调用网卡驱动程序中的中断函数。 5.驱动先禁用网卡的中断因为驱动程序已经知道内存中有数据了告诉网卡驱动程序下次再有数据直接写进内存中不用再通知CPU了这样可以提高效率。避免CPU被不停的中断打扰。 6.启用软中断。这步结束后硬件中断就结束返回了。由于硬中断在执行程序的过程中不能被中断所以如果执行时间过长会导致CPU没法响应其他硬件的中断于是引入了软中断这样可以将硬中断处理函数中比较耗时部分交给软中断处理函数里慢慢处理。
软中断会触发内核网络模块中的软中断处理函数流程如下 -----17 | |-----------| NIC || | ||Enable IRQ -----||------------ Memroy| | Read -----------------------------------------------| NIC Driver |--------------------- | Packet | Packet | Packet | ...... || | | 9 --------------------------------| ------------| | | skbPoll | 8 Raise softIRQ | 6 -----------------| | 10 || ↓ ↓--------------- Call ----------- ------------------ -------------------- 12 ---------------------| net_rx_action |-------| ksoftirqd | | napi_gro_receive |-------| enqueue_to_backlog |-----| CPU input_pkt_queue |--------------- 7 ----------- ------------------ 11 -------------------- ---------------------| | 1314 | - - - - - - - - - - - - - - - - - - - - - - ↓ ↓-------------------------- 15 ------------------------| __netif_receive_skb_core |-----------| packet taps(AF_PACKET) |-------------------------- ------------------------|| 16↓-----------------| protocol layers |-----------------7 内核中的ksoftirqd进程专门负责软中断的处理当它收到软中断后就会调用相应软中断所对应的处理函数对于上面第6步中是网卡驱动模块抛出的软中断ksoftirqd会调用网络模块的net_rx_action函数 8 net_rx_action函数会调用网卡驱动程序里的poll函数来一个一个处理数据包。 9 在poll函数中网卡驱动程序一个接一个读取网卡写到内存中的数据包内存中的数据包的格式只有驱动程序知道。 10 驱动程序将从内存中读取到的数据包转换成内核网络模块能识别的格式skb格式然后调用napi_gro_receive函数 11 napi_gro_receive会处理GRO相关的内容也就是将可以合并的数据包进行合并这样就只需要调用一次协议栈。然后判断是否开启了RPS如果开启了将会调用enqueue_to_backlog. 12 在enqueue_to_backlog函数中会将数据包放入CPU的softnet_data结构体的input_pkt_queue中然后返回如果input_pkt_queue满了的话该数据包将会被丢弃queue的大小可以通过net.core.netdev_max_backlog来配置。 13 CPU会接着在自己的软中断上下文中调用__netif_receive_skb_core函数处理自己input_pkt_queue里的网络数据。 14 如果没开启RPSnapi_gro_receive会直接调用__netif_receive_skb_core 15 看是不是有AF_PACKET类型的socket也就是我们常说的原始套接字如果有的话拷贝一份数据给它。tcpdump抓包就是抓的这里的包。 16 调用协议栈相应的函数将数据包交给协议栈处理。 17 待内存中的所有数据包被处理完成后即poll函数执行完成启用网卡的硬中断这样下次网卡再收到数据的时候就会通知CPU。
enqueue_to_backlog函数也会被netif_rx函数调用而netif_rx正是网络堆栈接收从lo设备发送过来的数据包时所要调用的函数
以上分析参考地址 点击查看原文
以上是大致分析了一下数据包是如何从网卡传送到内核网络协议栈的。那么下面我们就分析网卡驱动程序里面的相关函数
一.中断函数 首先是中断函数rtl8139_interrupt当硬件中断后CPU会调用网卡驱动程序的该函数rtl8139_interrupt函数是在rtl8139_open函数中注册的
static int rtl8139_open (struct net_device *dev)
{
。。。/* 注册中断处理函数 中断号为共享 */retval request_irq (dev-irq, rtl8139_interrupt, IRQF_SHARED, dev-name, dev);
。。。
}
中断函数处理的中断事件可以大致分为几类 A 数据包到达产生的中断(RxAckBits RxFIFOOver | RxOverflow | RxOK) B 异常事件通常都是出错的情况(RxAckBits RxFIFOOver | RxOverflow | RxOK) C发送完成事件(TxOK | TxErr)
我们先来看中断函数 /* The interrupt handler does all of the Rx thread work and cleans upafter the Tx thread. */
static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance)
{/* 参数dev_instance是在上面注册中断处理函数的时候传入的 */struct net_device *dev (struct net_device *) dev_instance;/* tp 为网卡驱动自定义的驱动特有的数据和dev一起分配的 */struct rtl8139_private *tp netdev_priv(dev);void __iomem *ioaddr tp-mmio_addr;u16 status, ackstat;int link_changed 0; /* avoid bogus uninit warning */int handled 0;/* 对驱动数据加锁*/spin_lock (tp-lock);/*读中断状态寄存器获取中断状态*/status RTL_R16 (IntrStatus);/* shared irq? *//* 这时由共享此中断号的其它设备产生的中断 */if (unlikely((status rtl8139_intr_mask) 0))goto out;handled 1;/* h/w no longer present (hotplug?) or major error, bail *//* 硬件错误 */if (unlikely(status 0xFFFF))goto out;/* close possible races with dev_close *//* 设备已关闭*/if (unlikely(!netif_running(dev))) {/* 屏蔽所有中断*/RTL_W16 (IntrMask, 0);goto out;}/* Acknowledge all of the current interrupt sources ASAP, butan first get an additional status bit from CSCR. */if (unlikely(status RxUnderrun))link_changed RTL_R16 (CSCR) CSCR_LinkChangeBit;ackstat status ~(RxAckBits | TxErr);if (ackstat)RTL_W16 (IntrStatus, ackstat);/* Receive packets are processed by poll routine.If not running start it now. *//* 下一步处理数据包到达事件 */if (status RxAckBits){if (napi_schedule_prep(tp-napi)) {RTL_W16_F (IntrMask, rtl8139_norx_intr_mask);/* 这个函数的分析在下面 */__napi_schedule(tp-napi); //这里采用了NAPI新机制暂时不详细说明这个机制会新开一篇文章详细讲解一下}}/* 以下都是一些检查在此不做分析 *//* Check uncommon events with one test. */if (unlikely(status (PCIErr | PCSTimeout | RxUnderrun | RxErr)))rtl8139_weird_interrupt (dev, tp, ioaddr,status, link_changed);if (status (TxOK | TxErr)) {/* 发送完成事件处理 下面会分析*/rtl8139_tx_interrupt (dev, tp, ioaddr);if (status TxErr)RTL_W16 (IntrStatus, TxErr);}out:spin_unlock (tp-lock);netdev_dbg(dev, exiting interrupt, intr_status%#4.4x\n,RTL_R16(IntrStatus));return IRQ_RETVAL(handled);
}__napi_schedule函数的分析
void __napi_schedule(struct napi_struct *n)
{unsigned long flags;local_irq_save(flags); //这里应该是保存中断标志位____napi_schedule(__get_cpu_var(softnet_data), n); //去这个函数看看local_irq_restore(flags); //回复中断标志位
}
____napi_schedule看看这个函数
static inline void ____napi_schedule(struct softnet_data *sd,struct napi_struct *napi)
{list_add_tail(napi-poll_list, sd-poll_list); //将当前设备加入CPU相关全局队列softnet_data的轮询设备列表中/* 调用函数产生网络接收软中断。 */__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
二. 发送完成事件处理 下面我们分析rtl8139_tx_interrupt函数
static void rtl8139_tx_interrupt (struct net_device *dev,struct rtl8139_private *tp,void __iomem *ioaddr)
{unsigned long dirty_tx, tx_left;assert (dev ! NULL);assert (ioaddr ! NULL);/*dirty_tx是最近发送数据包时没有经中断处理的最早数据包所对应的发送描述符*/dirty_tx tp-dirty_tx;/* cur_tx是最近发送完成的最后一个数据包对应的发送描述符所以在此次中断中要处理的就是和dirty_tx之间的发送描述符*/tx_left tp-cur_tx - dirty_tx;while (tx_left 0) {/* 环形缓冲区最大为NUM_TX_DESC取模得到真实值*/int entry dirty_tx % NUM_TX_DESC;int txstatus;/*当前发送描述符的发送状态(一个寄存器为32bit)*/ txstatus RTL_R32 (TxStatus0 (entry * sizeof (u32)));/*还没有发送*/if (!(txstatus (TxStatOK | TxUnderrun | TxAborted)))break; /* It still hasnt been Txed *//* Note: TxCarrierLost is always asserted at 100mbps. */if (txstatus (TxOutOfWindow | TxAborted)) {/* There was an major error, log it. */netif_dbg(tp, tx_err, dev, Transmit error, Tx status %08x\n,txstatus);dev-stats.tx_errors;if (txstatus TxAborted) {dev-stats.tx_aborted_errors;RTL_W32 (TxConfig, TxClearAbt);RTL_W16 (IntrStatus, TxErr);wmb();}if (txstatus TxCarrierLost)dev-stats.tx_carrier_errors;if (txstatus TxOutOfWindow)dev-stats.tx_window_errors;} else {if (txstatus TxUnderrun) {/* Add 64 to the Tx FIFO threshold. */if (tp-tx_flag 0x00300000)tp-tx_flag 0x00020000;dev-stats.tx_fifo_errors;}dev-stats.collisions (txstatus 24) 15;dev-stats.tx_bytes txstatus 0x7ff;dev-stats.tx_packets;}dirty_tx;tx_left--;}#ifndef RTL8139_NDEBUGif (tp-cur_tx - dirty_tx NUM_TX_DESC) {netdev_err(dev, Out-of-sync dirty pointer, %ld vs. %ld\n,dirty_tx, tp-cur_tx);dirty_tx NUM_TX_DESC;}
#endif /* RTL8139_NDEBUG *//* only wake the queue if we did work, and the queue is stopped */if (tp-dirty_tx ! dirty_tx) {tp-dirty_tx dirty_tx;mb();netif_wake_queue (dev);}
}
三.软中断处理函数
由于在前面的中断处理程序中调用了__raise_softirq_irqoff(NET_RX_SOFTIRQ)CPU会在中断处理完成后的适当的时候调用软中断处理函数也就是我们在系统初始化的过程中注册的net_rx_action函数。
static void net_rx_action(struct softirq_action *h)
{/*获取每个CPU的softnet_data结构然后取得其poll_list */struct softnet_data *sd __get_cpu_var(softnet_data);unsigned long time_limit jiffies 2;int budget netdev_budget;void *have;local_irq_disable();/* 处理poll_list上关联的每一个设备*/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.*/if (unlikely(budget 0 || time_after(jiffies, time_limit))) //如果收到数据的总数到了300个goto softnet_break;local_irq_enable();/* Even though interrupts have been re-enabled, this* access is safe because interrupts can only add new* entries to the tail of this list, and only -poll()* calls can remove this head entry from the list.*/n list_first_entry(sd-poll_list, struct napi_struct, poll_list);have netpoll_poll_lock(n);weight n-weight;/* This NAPI_STATE_SCHED test is for avoiding a race* with netpolls poll_napi(). Only the entity which* obtains the lock and sees NAPI_STATE_SCHED set will* actually make the -poll() call. Therefore we avoid* accidentally calling -poll() when NAPI is not scheduled.*/work 0;/* 调用每个设备的pool方法接收数据*/if (test_bit(NAPI_STATE_SCHED, n-state)) {work n-poll(n, weight);trace_napi_poll(n);}WARN_ON_ONCE(work weight);budget - work; //更新budget这样能控制总收到的数据local_irq_disable();/* Drivers must not modify the NAPI state if they* consume the entire weight. In such cases this code* still owns the NAPI instance and therefore can* move the instance around on the list at-will.*/if (unlikely(work weight)) {/* 设备运行出错或自己退出poll_list就删除它*/if (unlikely(napi_disable_pending(n))) {local_irq_enable();napi_complete(n);local_irq_disable();} else/* 该设备还有要接收的数据没被处理,因为轮询算法被移动到poll_llst尾部等待处理*/list_move_tail(n-poll_list, sd-poll_list);}netpoll_poll_unlock(have);}
out:net_rps_action_and_irq_enable(sd);#ifdef CONFIG_NET_DMA/** There may not be any more sk_buffs coming right now, so push* any pending DMA copies to hardware*/dma_issue_pending_all();
#endifreturn;softnet_break:sd-time_squeeze;__raise_softirq_irqoff(NET_RX_SOFTIRQ);goto out;
}
通常在网卡收发数据的时候需要维护一个缓冲区队列来缓存可能存在的突发数据类似于前面的DMA环形缓冲区。队列层中包含了一个叫做struct 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_RPSstruct 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;
#endifunsigned dropped;struct sk_buff_head input_pkt_queue;struct napi_struct backlog;
};下一步进入设备的poll函数。需要注意的是如果是NAPI的网卡驱动的话poll函数是在驱动中注册的驱动实现的如果是非 NAPI的话就是内核定义的process_backlog函数至于process_backlog是如何添加到poll_list中的这里暂时不 管先看看8139驱动的poll 函数是如何实现的。
四.8139 poll函数实现
static int rtl8139_poll(struct napi_struct *napi, int budget)
{struct rtl8139_private *tp container_of(napi, struct rtl8139_private, napi);struct net_device *dev tp-dev;void __iomem *ioaddr tp-mmio_addr;int work_done;spin_lock(tp-rx_lock);work_done 0;/* 在 rtl8139_rx中将接送到的数据拷贝出来并传递给上层协议驱动。*/if (likely(RTL_R16(IntrStatus) RxAckBits))work_done rtl8139_rx(dev, tp, budget);/*说明没有多余的数据到达则恢复接收中断并把此设备从poll_list中清除*/if (work_done budget) {unsigned long flags;/** Order is important since data can get interrupted* again when we think we are done.先关中断在写中断屏蔽位*/spin_lock_irqsave(tp-lock, flags);__napi_complete(napi);RTL_W16_F(IntrMask, rtl8139_intr_mask);spin_unlock_irqrestore(tp-lock, flags);}spin_unlock(tp-rx_lock);return work_done;
}从rtl8139_rx的代码也可以看出当数据包接收出错或者是没有更多的数据包可以接收时work_done才不会达到budget这时应该让网卡重新回到中断的状态以等待数据包的到来。另外一种情况就是work_done等于budget很可能是因为还有数据包要接收所以在net_rx_action函数中只是把该网卡设备移到队列的尾部以期待在下次循环中再次调用其poll函数。
下面看rtl8139_rx的实现 五.rtl8139_rx的实现
static int rtl8139_rx(struct net_device *dev, struct rtl8139_private *tp,int budget)
{void __iomem *ioaddr tp-mmio_addr;int received 0;/* 网卡不断的把数据放进环形接收缓冲区 CPU读出来的时候读到哪里的顺序需要自己维护tp-cur_rx记录上次读到哪里这里将接着从上次的地方拷贝。*/unsigned char *rx_ring tp-rx_ring;unsigned int cur_rx tp-cur_rx;unsigned int rx_size 0;netdev_dbg(dev, In %s(), current %04x BufAddr %04x, free to %04x, Cmd %02x\n,__func__, (u16)cur_rx,RTL_R16(RxBufAddr), RTL_R16(RxBufPtr), RTL_R8(ChipCmd));/*轮询寄存器当ChipCmd RxBufEmpty 位没被网卡设置的时候则说明环形缓冲区中有接收到的数据等待处理*/while (netif_running(dev) received budget (RTL_R8 (ChipCmd) RxBufEmpty) 0) {u32 ring_offset cur_rx % RX_BUF_LEN;u32 rx_status;unsigned int pkt_size;struct sk_buff *skb;rmb();/* 获取接收状态以及接收数据的长度*//* read sizestatus of next frame from DMA ring buffer */rx_status le32_to_cpu (*(__le32 *) (rx_ring ring_offset));rx_size rx_status 16;/* 实际数据包的长度减去4个字节的CRC*/pkt_size rx_size - 4;netif_dbg(tp, rx_status, dev, %s() status %04x, size %04x, cur %04x\n,__func__, rx_status, rx_size, cur_rx);
#if RTL8139_DEBUG 2print_hex_dump(KERN_DEBUG, Frame contents: ,DUMP_PREFIX_OFFSET, 16, 1,rx_ring[ring_offset], 70, true);
#endif/*当EarlyRX 允许的时候可能会发生这种情况一个完整的数据包的一部分已经通过DMA 传送到了内存中而另外一部分还在网卡内部FIFO 中网卡的DMA 操作还在进行中*//* Packet copy from FIFO still in progress.* Theoretically, this should never happen* since EarlyRx is disabled.*/if (unlikely(rx_size 0xfff0)) {if (!tp-fifo_copy_timeout)tp-fifo_copy_timeout jiffies 2;else if (time_after(jiffies, tp-fifo_copy_timeout)) {netdev_dbg(dev, hung FIFO. Reset\n);rx_size 0;goto no_early_rx;}netif_dbg(tp, intr, dev, fifo copy in progress\n);tp-xstats.early_rx;break;}no_early_rx:tp-fifo_copy_timeout 0;/* If Rx err or invalid rx_size/rx_status received* (which happens if we get lost in the ring),* Rx process gets reset, so we abort any further* Rx processing.*/if (unlikely((rx_size (MAX_ETH_FRAME_SIZE4)) ||(rx_size 8) ||(!(rx_status RxStatusOK)))) {rtl8139_rx_err (rx_status, dev, tp, ioaddr);received -1;goto out;}/* Malloc up new buffer, compatible with net-2e. *//* Omit the four octet CRC from the length. */skb netdev_alloc_skb_ip_align(dev, pkt_size);if (likely(skb)) {
#if RX_BUF_IDX 3wrap_copy(skb, rx_ring, ring_offset4, pkt_size);
#elseskb_copy_to_linear_data (skb, rx_ring[ring_offset 4], pkt_size);
#endifskb_put (skb, pkt_size);skb-protocol eth_type_trans (skb, dev);dev-stats.rx_bytes pkt_size;dev-stats.rx_packets;//数据包从这里进入上层netif_receive_skb (skb); } else {if (net_ratelimit())netdev_warn(dev, Memory squeeze, dropping packet\n);dev-stats.rx_dropped;}received;/* 前一个4是头部的状态和长度的4个字节后面的3是为了对齐*/cur_rx (cur_rx rx_size 4 3) ~3;RTL_W16 (RxBufPtr, (u16) (cur_rx - 16));/*清除中断状态位*/rtl8139_isr_ack(tp);}if (unlikely(!received || rx_size 0xfff0))rtl8139_isr_ack(tp);netdev_dbg(dev, Done %s(), current %04x BufAddr %04x, free to %04x, Cmd %02x\n,__func__, cur_rx,RTL_R16(RxBufAddr), RTL_R16(RxBufPtr), RTL_R8(ChipCmd));tp-cur_rx cur_rx;/** The receive buffer should be mostly empty.* Tell NAPI to reenable the Rx irq.*/if (tp-fifo_copy_timeout)received budget;out:return received;
}最后就是netif_receive_skb了数据包从此离开链路层提交给上层。 六.netif_receive_skb
int netif_receive_skb(struct sk_buff *skb)
{if (netdev_tstamp_prequeue)net_timestamp_check(skb);if (skb_defer_rx_timestamp(skb))return NET_RX_SUCCESS;#ifdef CONFIG_RPS{struct rps_dev_flow voidflow, *rflow voidflow;int cpu, ret;rcu_read_lock();cpu get_rps_cpu(skb-dev, skb, rflow);if (cpu 0) {ret enqueue_to_backlog(skb, cpu, rflow-last_qtail);rcu_read_unlock();} else {rcu_read_unlock();ret __netif_receive_skb(skb);}return ret;}
#elsereturn __netif_receive_skb(skb);
#endif
}
netif_receive_skb(skb) 这是一个辅助函数用于在poll中处理接收到的帧。它主要是向各个已注册的协议处理例程发送一个SKB。
总结一下 netif_rx是旧的收包函数 比如在某个网卡收到一个包后首先就是调用这个函数
netif_rx把包放入一个每CPU队列 __skb_queue_tail(queue-input_pkt_queue, skb); 并且raise软中断NET_RX_SOFTIRQ让它进一步处理包因为收包是在网卡驱动的中断中
最后软中断处理函数 net_rx_action会得到运行这个函数会对每个收到包的设备调用其设备的出队列函数 把包从上面的队列中拿出来process_backlog函数拿出来之后就会调用netif_receive_skb开始靠近协议栈有很多人可能要处理它比如PF_PACKET(tcpdump),bridge,等如果最后包还在那么就会进入协议栈的3层对ipv4的包调用了ipv4的包接收函数ip_rcv,这个函数在简单的校验之后会到netfilter,如果还幸存那就复杂了 比如典型的最后就到tcp或者udp的收包程序它们检查有没有socket需要不需要就扔掉等。
通俗的讲 在netif_rx函数中会调用netif_rx_schedule, 然后该函数又会去调用__netif_rx_schedule 在函数__netif_rx_schedule中会去触发软中断NET_RX_SOFTIRQ, 也即是去调用net_rx_action. 然后在net_rx_action函数中会去调用设备的poll函数, 它是设备自己注册的. 在设备的poll函数中, 会去调用neif_receive_skb函数, 在该函数中有下面一条语句 pt_prev-func, 此处的func为一个函数指针, 在之前的注册中设置为ip_rcv. 因此, 就完成了数据包从链路层上传到网络层的这一个过程了.
我们就分析到这里就行了已经知道驱动程序是如何把数据传送给内核了至于再往后的操作这里暂时不管了以后有机会再分析。
想一起探讨以及获得各种学习资源加我有我博客中写的代码的原稿 qq1126137994 微信liu1126137994 可以共同交流关于嵌入式操作系统C语言C语言数据结构等技术问题。