以太网输入
时间:2007-06-09 来源:souldemo
by souldump
E-mail: [email protected]
DATA: 5th.8.2008
在2.6内核中,采用传统的netif_rx方式和NAPI方式来处理驱动收到的以太网帧,另外,由于代码在2.6的早期版本和新近的版本
有些修改,因次本文以2.6.20版本的内核代码为标准参考,如果想了解每个版本之间的差异可参考内核Changlog和代码。
首先,介绍下netif_rx方式和NAPI方式:
netif_rx方式的处理流程:网卡接收到完整帧后,调用驱动中的中断处理函数,
接收到帧->网卡中断->驱动的中断处理函数->netif_rx(将存放数据的skb题交给CPU的网络输入队列、产生软中断)->内核调
用软中断处理函数net_rx_action函数->net_receive_skb函数提交给对应帧类型的输入函数。
NAPI方式处理过程:
接收到帧->网卡中断->驱动的中断处理函数->调用__netif_rx_schedule将该网卡加入CPU轮询设备链表(softnet_data结构)
产生软中断->net_rx_action函数->设备驱动自己编写的poll函数。
注:正如我们要在后面看到的,在netif_rx中,软中断并不是每次都会产生的。
二者的联系:
在2.6内核中,虽然有两种方式处理,但是内核会统一采用poll方式处理输入的帧。如果使用NAPI,要采用环形缓冲数组存放数据,
并使用dma映射。这样,当接收到帧后,就不必再将数据拷贝到新创建的skb中,增加了效率。而对于那些没使用NAPI的驱动,内核
会采用一个在sotfnet_data结构中嵌套的伪设备,使用一个通用的poll函数process_backlog来处理(后面会详细介绍)。另外
一个非常值得一提的是,对于那些不采用NAPI方式的驱动,如果也使用环形缓冲区映射,处理的效率也会大大提高。一个很好的例
子就是sis900.c
以我的愚见,如果表示特别需要,不必自己费心写poll函数,让内核自己去使用通用的就可以了。如果想写,poll函数所做的工作
就是轮询设备对skb调用net_receive_skb,并且维护环形缓冲区,在所有工作完成之后调用__netif_rx_complete将设备从
CPU轮询列表中删除(对于eth_type_trans的调用也应该在这个函数里)。
本文以sis900网卡驱动代码为例分析netif_rx方式,对于NAPI方式,相差不并不大,可参考使用NAPI的驱动代码,如8139cp.c。
当网卡接收到完整的以太网帧后,产生一个中断,内核调用中断处理函数sis900_interrupt(次中断处理函数由dev_open注册).
sis_interrupt函数使用循环处理检查网卡中断状态寄存器的值,并根据此值处理进入的和外出的数据包(根据注释可以看到,为
什么可以在一个循环中处理完进入的报文而又继续判断以便处理外出报文,是因为网卡工作在全双工模式。)。如果是进入的报文,
则调用sis900_rx(),如果是外出的报文则调用sis900_finish_xmit()(本函数以后在介绍以太网输出的时侯会详细介绍)。
下面让我们来看看sis900_rx()函数都对报文做了哪些工作:
对于驱动处理私有结构的代码我们并不关心,我们只关心在所有驱动中都要实现的任务,为上层协议提交的sk_buff结构的处理过程。
1775 skb = sis_priv->rx_skbuff[entry];
1776 skb_put(skb, rx_size);
1777 skb->protocol = eth_type_trans(skb, net_dev);
1778 netif_rx(skb);
先行设置skb的指针,然后设置skb中的tail和len指针,正如我前面所说的,这是因为sis900的驱动虽然没采用NAPI,但在初始
化时创建一组sk_buff数组用来存放数据。并用dma映射到驱动的私有空间,这样,当网卡接收到以太网帧后数据就可以不必再从新
分配skb,并拷贝数据,而只需将这个结构的映射取消,并重新分配一个skb并将其加入映射的环形输入队列(具体细节请参考驱动
代码)。
从这段代码中我们可以看到驱动除设置存放数据的4个指针,还设置了protocol成员。
读过ULNI的人都知道,不同的网络中调用的*_type_trans也是不一样的,对于以太网,调用的则是eth_type_trans,那么我们
再来看下它的完整实现。
// net/ethernet/eth.c
146 /**
147 * eth_type_trans - determine the packet's protocol ID.
148 * @skb: received socket data
149 * @dev: receiving network device
150 *
151 * The rule here is that we
152 * assume 802.3 if the type field is short enough to be a length.
153 * This is normal practice and works for any 'now in use' protocol.
154 */
155 __be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
156 {
157 struct ethhdr *eth;
158 unsigned char *rawp;
159
160 skb->mac.raw = skb->data;
161 skb_pull(skb, ETH_HLEN);
162 eth = eth_hdr(skb);
163
164 if (is_multicast_ether_addr(eth->h_dest)) {
165 if (!compare_ether_addr(eth->h_dest, dev->broadcast))
166 skb->pkt_type = PACKET_BROADCAST;
167 else
168 skb->pkt_type = PACKET_MULTICAST;
169 }
170
171 /*
172 * This ALLMULTI check should be redundant by 1.4
173 * so don't forget to remove it.
174 *
175 * Seems, you forgot to remove it. All silly devices
176 * seems to set IFF_PROMISC.
177 */
178
179 else if (1 /*dev->flags&IFF_PROMISC */ ) {
180 if (unlikely(compare_ether_addr(eth->h_dest, dev->dev_addr)))
181 skb->pkt_type = PACKET_OTHERHOST;
182 }
183
184 if (ntohs(eth->h_proto) >= 1536)
185 return eth->h_proto;
186
187 rawp = skb->data;
188
189 /*
190 * This is a magic hack to spot IPX packets. Older Novell breaks
191 * the protocol design and runs IPX over 802.3 without an 802.2 LLC
192 * layer. We look for FFFF which isn't a used 802.2 SSAP/DSAP. This
193 * won't work for fault tolerant netware but does for the rest.
194 */
195 if (*(unsigned short *)rawp == 0xFFFF)
196 return htons(ETH_P_802_3);
197
198 /*
199 * Real 802.2 LLC
200 */
201 return htons(ETH_P_802_2);
202 }
linux能够接收IEEE封装和以太网封装两种格式的分组,但是默认发送采用的是以太网封装格式。
160-168行
可以看到,其返回值类型为网络字节顺序,首先,使用skb_pull将data指针后移以太网帧头长度个字节,然后使用eth_hdr()
获得以太网帧结构指针(该函数在include/linux/if_ether.h中定义)。我们都知道,目的硬件地址的第一个字节的最低位为1
时为链路层多播地址,因次根据此值判断是否是以太网多播帧来设置pkt_type成员(通过比较硬件的广播地址来判断是否为广播
类型。
169-182行
我们能够看到,如果不是本机的报文,在链路层能够收到,但是会被网络层协议处理函数丢掉。不过对于packet协议族的插口,你
仍旧能够收到那些发往其它主机的报文。
184-185行
在以太网封装的分组中type字段和IEEE封装的分组中length字段是在同一位置,但是由于值的不同,因次可以通过比较它的值来
确定采用哪种封装格式。长度大于1536时为以太网封装。如果是IEEE封装,可以通过进一步的值比较判断是为802.2还是802.3封装。
关于IEEE封装和LLC格式,本文并不做详细介绍,更多细节可参考ULNI13.5节。
驱动程序将这个skb通过netif_rx接口输入例程传递给上层,netif_rx将其放入CPU输入队列,并返回接口的拥塞程度。
556 /**
1557 * netif_rx - post buffer to the network code
1558 * @skb: buffer to post
1559 *
1560 * This function receives a packet from a device driver and queues it for
1561 * the upper (protocol) levels to process. It always succeeds. The buffer
1562 * may be dropped during processing for congestion control or by the
1563 * protocol layers.
1564 *
1565 * return values:
1566 * NET_RX_SUCCESS (no congestion)
1567 * NET_RX_CN_LOW (low congestion)
1568 * NET_RX_CN_MOD (moderate congestion)
1569 * NET_RX_CN_HIGH (high congestion)
1570 * NET_RX_DROP (packet was dropped)
1571 *
1572 */
1573
1574 int netif_rx(struct sk_buff *skb)
1575 {
1576 struct softnet_data *queue;
1577 unsigned long flags;
1578
1579 /* if netpoll wants it, pretend we never saw it */
1580 if (netpoll_rx(skb))
1581 return NET_RX_DROP;
1582
1583 if (!skb->tstamp.off_sec)
1584 net_timestamp(skb);
1585
1586 /*
1587 * The code is rearranged so that the path is the most
1588 * short when CPU is congested, but is still operating.
1589 */
1590 local_irq_save(flags);
1591 ` ` queue = &__get_cpu_var(softnet_data);
1592
1593 __get_cpu_var(netdev_rx_stat).total++;
1594 if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
1595 if (queue->input_pkt_queue.qlen) {
1596 enqueue:
1597 dev_hold(skb->dev);
1598 __skb_queue_tail(&queue->input_pkt_queue, skb);
1599 local_irq_restore(flags);
1600 return NET_RX_SUCCESS;
1601 }
1602
1603 netif_rx_schedule(&queue->backlog_dev);
1604 goto enqueue;
1605 }
1
1607 __get_cpu_var(netdev_rx_stat).dropped++;
1608 local_irq_restore(flags);
1609
1610 kfree_skb(skb);
1611 return NET_RX_DROP;
1612 }
首先检查netpoll是否会处理此分组,netpoll是一种通过轮询方式、不经过网络子系统、也不用允许中断的分组处理过程,用
于netconsole和kgdb-over-ethernet远程调试内核的方式。详细的实现超出了本文的范围,读者可参考相关的资料。
关于这段代码的详细解释可以在ULNI10.5节见到,但是这里有个问题,就是关于最大队列长度的,在2.6.20.1中,它的值为1000,而
根据1594行,当qlen成员达到1000时,仍旧能够添加该skb,因此,实际能够处理的最大队列长度为应为1001????
如果驱动采用NAPI,那么驱动会自己将设备添加到soft_data的轮询链表,而对于那些没有采用NAPI的驱动,内核使用
softnet_data中的backlog_dev来代替,这是一个伪设备,如果如果qlen值不为0,那么直接添加到CPU输入队列就返回了,因
为这时先前的报文已经产生软中断而中断处理函数正在运行或者用尽时间片后队列还未处理完成,如果为0,则那么应该调度
netif_rx_schedule函数注册backlog_dev伪设备,并将其添加到softnet_data的poll_list,该函数最终导
致__raise_softirq_irqoff产生软中断NET_RX_SOFTIRQ,迫使内核运行对应的中断处理函数net_rx_action(该中断处理函数在系统初始化时在net_dev_init中注册)。这样无论驱动是否采用NAPI方式,内核在处理的时侯就可以统一使用
net_rx_action轮询处理了。
poll虚函数在net_rx_action中被调用。如果对于netif_rx方式,那么则会调用process_backlog函数,这个函数的详细分析
可参考ULNI的10.7.1节。它所做的和我前面所说的poll函数的工作大致相同。从CPU的网络输入队列 input_pkt_queue,将报文卸
下来,然后调用net_receive_skb.将skb传递给上层协议的输入函数。
对于net_receive_skb的分析,我想把它放在网络层的输入中分析,因为它们之间的联系更为紧密。
注释:
1)ULNI 《Understanding Linux Network Internals》By Christian Benvenuti
欢迎对本文进行评论,指出其中的错误和不足,也欢迎和本人进行交流:[email protected]
E-mail: [email protected]
DATA: 5th.8.2008
在2.6内核中,采用传统的netif_rx方式和NAPI方式来处理驱动收到的以太网帧,另外,由于代码在2.6的早期版本和新近的版本
有些修改,因次本文以2.6.20版本的内核代码为标准参考,如果想了解每个版本之间的差异可参考内核Changlog和代码。
首先,介绍下netif_rx方式和NAPI方式:
netif_rx方式的处理流程:网卡接收到完整帧后,调用驱动中的中断处理函数,
接收到帧->网卡中断->驱动的中断处理函数->netif_rx(将存放数据的skb题交给CPU的网络输入队列、产生软中断)->内核调
用软中断处理函数net_rx_action函数->net_receive_skb函数提交给对应帧类型的输入函数。
NAPI方式处理过程:
接收到帧->网卡中断->驱动的中断处理函数->调用__netif_rx_schedule将该网卡加入CPU轮询设备链表(softnet_data结构)
产生软中断->net_rx_action函数->设备驱动自己编写的poll函数。
注:正如我们要在后面看到的,在netif_rx中,软中断并不是每次都会产生的。
二者的联系:
在2.6内核中,虽然有两种方式处理,但是内核会统一采用poll方式处理输入的帧。如果使用NAPI,要采用环形缓冲数组存放数据,
并使用dma映射。这样,当接收到帧后,就不必再将数据拷贝到新创建的skb中,增加了效率。而对于那些没使用NAPI的驱动,内核
会采用一个在sotfnet_data结构中嵌套的伪设备,使用一个通用的poll函数process_backlog来处理(后面会详细介绍)。另外
一个非常值得一提的是,对于那些不采用NAPI方式的驱动,如果也使用环形缓冲区映射,处理的效率也会大大提高。一个很好的例
子就是sis900.c
以我的愚见,如果表示特别需要,不必自己费心写poll函数,让内核自己去使用通用的就可以了。如果想写,poll函数所做的工作
就是轮询设备对skb调用net_receive_skb,并且维护环形缓冲区,在所有工作完成之后调用__netif_rx_complete将设备从
CPU轮询列表中删除(对于eth_type_trans的调用也应该在这个函数里)。
本文以sis900网卡驱动代码为例分析netif_rx方式,对于NAPI方式,相差不并不大,可参考使用NAPI的驱动代码,如8139cp.c。
当网卡接收到完整的以太网帧后,产生一个中断,内核调用中断处理函数sis900_interrupt(次中断处理函数由dev_open注册).
sis_interrupt函数使用循环处理检查网卡中断状态寄存器的值,并根据此值处理进入的和外出的数据包(根据注释可以看到,为
什么可以在一个循环中处理完进入的报文而又继续判断以便处理外出报文,是因为网卡工作在全双工模式。)。如果是进入的报文,
则调用sis900_rx(),如果是外出的报文则调用sis900_finish_xmit()(本函数以后在介绍以太网输出的时侯会详细介绍)。
下面让我们来看看sis900_rx()函数都对报文做了哪些工作:
对于驱动处理私有结构的代码我们并不关心,我们只关心在所有驱动中都要实现的任务,为上层协议提交的sk_buff结构的处理过程。
1775 skb = sis_priv->rx_skbuff[entry];
1776 skb_put(skb, rx_size);
1777 skb->protocol = eth_type_trans(skb, net_dev);
1778 netif_rx(skb);
先行设置skb的指针,然后设置skb中的tail和len指针,正如我前面所说的,这是因为sis900的驱动虽然没采用NAPI,但在初始
化时创建一组sk_buff数组用来存放数据。并用dma映射到驱动的私有空间,这样,当网卡接收到以太网帧后数据就可以不必再从新
分配skb,并拷贝数据,而只需将这个结构的映射取消,并重新分配一个skb并将其加入映射的环形输入队列(具体细节请参考驱动
代码)。
从这段代码中我们可以看到驱动除设置存放数据的4个指针,还设置了protocol成员。
读过ULNI的人都知道,不同的网络中调用的*_type_trans也是不一样的,对于以太网,调用的则是eth_type_trans,那么我们
再来看下它的完整实现。
// net/ethernet/eth.c
146 /**
147 * eth_type_trans - determine the packet's protocol ID.
148 * @skb: received socket data
149 * @dev: receiving network device
150 *
151 * The rule here is that we
152 * assume 802.3 if the type field is short enough to be a length.
153 * This is normal practice and works for any 'now in use' protocol.
154 */
155 __be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
156 {
157 struct ethhdr *eth;
158 unsigned char *rawp;
159
160 skb->mac.raw = skb->data;
161 skb_pull(skb, ETH_HLEN);
162 eth = eth_hdr(skb);
163
164 if (is_multicast_ether_addr(eth->h_dest)) {
165 if (!compare_ether_addr(eth->h_dest, dev->broadcast))
166 skb->pkt_type = PACKET_BROADCAST;
167 else
168 skb->pkt_type = PACKET_MULTICAST;
169 }
170
171 /*
172 * This ALLMULTI check should be redundant by 1.4
173 * so don't forget to remove it.
174 *
175 * Seems, you forgot to remove it. All silly devices
176 * seems to set IFF_PROMISC.
177 */
178
179 else if (1 /*dev->flags&IFF_PROMISC */ ) {
180 if (unlikely(compare_ether_addr(eth->h_dest, dev->dev_addr)))
181 skb->pkt_type = PACKET_OTHERHOST;
182 }
183
184 if (ntohs(eth->h_proto) >= 1536)
185 return eth->h_proto;
186
187 rawp = skb->data;
188
189 /*
190 * This is a magic hack to spot IPX packets. Older Novell breaks
191 * the protocol design and runs IPX over 802.3 without an 802.2 LLC
192 * layer. We look for FFFF which isn't a used 802.2 SSAP/DSAP. This
193 * won't work for fault tolerant netware but does for the rest.
194 */
195 if (*(unsigned short *)rawp == 0xFFFF)
196 return htons(ETH_P_802_3);
197
198 /*
199 * Real 802.2 LLC
200 */
201 return htons(ETH_P_802_2);
202 }
linux能够接收IEEE封装和以太网封装两种格式的分组,但是默认发送采用的是以太网封装格式。
160-168行
可以看到,其返回值类型为网络字节顺序,首先,使用skb_pull将data指针后移以太网帧头长度个字节,然后使用eth_hdr()
获得以太网帧结构指针(该函数在include/linux/if_ether.h中定义)。我们都知道,目的硬件地址的第一个字节的最低位为1
时为链路层多播地址,因次根据此值判断是否是以太网多播帧来设置pkt_type成员(通过比较硬件的广播地址来判断是否为广播
类型。
169-182行
我们能够看到,如果不是本机的报文,在链路层能够收到,但是会被网络层协议处理函数丢掉。不过对于packet协议族的插口,你
仍旧能够收到那些发往其它主机的报文。
184-185行
在以太网封装的分组中type字段和IEEE封装的分组中length字段是在同一位置,但是由于值的不同,因次可以通过比较它的值来
确定采用哪种封装格式。长度大于1536时为以太网封装。如果是IEEE封装,可以通过进一步的值比较判断是为802.2还是802.3封装。
关于IEEE封装和LLC格式,本文并不做详细介绍,更多细节可参考ULNI13.5节。
驱动程序将这个skb通过netif_rx接口输入例程传递给上层,netif_rx将其放入CPU输入队列,并返回接口的拥塞程度。
556 /**
1557 * netif_rx - post buffer to the network code
1558 * @skb: buffer to post
1559 *
1560 * This function receives a packet from a device driver and queues it for
1561 * the upper (protocol) levels to process. It always succeeds. The buffer
1562 * may be dropped during processing for congestion control or by the
1563 * protocol layers.
1564 *
1565 * return values:
1566 * NET_RX_SUCCESS (no congestion)
1567 * NET_RX_CN_LOW (low congestion)
1568 * NET_RX_CN_MOD (moderate congestion)
1569 * NET_RX_CN_HIGH (high congestion)
1570 * NET_RX_DROP (packet was dropped)
1571 *
1572 */
1573
1574 int netif_rx(struct sk_buff *skb)
1575 {
1576 struct softnet_data *queue;
1577 unsigned long flags;
1578
1579 /* if netpoll wants it, pretend we never saw it */
1580 if (netpoll_rx(skb))
1581 return NET_RX_DROP;
1582
1583 if (!skb->tstamp.off_sec)
1584 net_timestamp(skb);
1585
1586 /*
1587 * The code is rearranged so that the path is the most
1588 * short when CPU is congested, but is still operating.
1589 */
1590 local_irq_save(flags);
1591 ` ` queue = &__get_cpu_var(softnet_data);
1592
1593 __get_cpu_var(netdev_rx_stat).total++;
1594 if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
1595 if (queue->input_pkt_queue.qlen) {
1596 enqueue:
1597 dev_hold(skb->dev);
1598 __skb_queue_tail(&queue->input_pkt_queue, skb);
1599 local_irq_restore(flags);
1600 return NET_RX_SUCCESS;
1601 }
1602
1603 netif_rx_schedule(&queue->backlog_dev);
1604 goto enqueue;
1605 }
1
1607 __get_cpu_var(netdev_rx_stat).dropped++;
1608 local_irq_restore(flags);
1609
1610 kfree_skb(skb);
1611 return NET_RX_DROP;
1612 }
首先检查netpoll是否会处理此分组,netpoll是一种通过轮询方式、不经过网络子系统、也不用允许中断的分组处理过程,用
于netconsole和kgdb-over-ethernet远程调试内核的方式。详细的实现超出了本文的范围,读者可参考相关的资料。
关于这段代码的详细解释可以在ULNI10.5节见到,但是这里有个问题,就是关于最大队列长度的,在2.6.20.1中,它的值为1000,而
根据1594行,当qlen成员达到1000时,仍旧能够添加该skb,因此,实际能够处理的最大队列长度为应为1001????
如果驱动采用NAPI,那么驱动会自己将设备添加到soft_data的轮询链表,而对于那些没有采用NAPI的驱动,内核使用
softnet_data中的backlog_dev来代替,这是一个伪设备,如果如果qlen值不为0,那么直接添加到CPU输入队列就返回了,因
为这时先前的报文已经产生软中断而中断处理函数正在运行或者用尽时间片后队列还未处理完成,如果为0,则那么应该调度
netif_rx_schedule函数注册backlog_dev伪设备,并将其添加到softnet_data的poll_list,该函数最终导
致__raise_softirq_irqoff产生软中断NET_RX_SOFTIRQ,迫使内核运行对应的中断处理函数net_rx_action(该中断处理函数在系统初始化时在net_dev_init中注册)。这样无论驱动是否采用NAPI方式,内核在处理的时侯就可以统一使用
net_rx_action轮询处理了。
poll虚函数在net_rx_action中被调用。如果对于netif_rx方式,那么则会调用process_backlog函数,这个函数的详细分析
可参考ULNI的10.7.1节。它所做的和我前面所说的poll函数的工作大致相同。从CPU的网络输入队列 input_pkt_queue,将报文卸
下来,然后调用net_receive_skb.将skb传递给上层协议的输入函数。
对于net_receive_skb的分析,我想把它放在网络层的输入中分析,因为它们之间的联系更为紧密。
注释:
1)ULNI 《Understanding Linux Network Internals》By Christian Benvenuti
欢迎对本文进行评论,指出其中的错误和不足,也欢迎和本人进行交流:[email protected]
相关阅读 更多 +