Continuous pursuit technical details
分类: 网络与安全
2013-04-24 15:58:14
接收网络数据包的过程,从数据包到达网卡的物理接口开始,然后由网卡的驱动程序交给网络协议栈,最后经过协议栈的一层层处理之后交给应用程序。大致上是这样的过程,但实际上有更多的细节。本文中主要介绍第一个和第二个步骤。
我们本文中依然以一个Realtek 8139网卡为例(驱动程序为/drivers/net/8139too.c)。请注意在内核代码中receive都是用rx简写的。
(1)接收网络数据包过程之注册与激活软中断
在生成net_device对象及初始化的函数rtl8139_init_one中已经初始化dev->open方法为rtl8139_open函数(在本系列文章2:初始化中的net_device对象中已经介绍,点这里查看)。在rtl8139_open函数(这个函数在网卡启动时被调用)中注册了一个中断函数rtl8139_interrupt:
retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);
所以只要当网卡开启后(状态为up),当网络数据包到达时,都会产生一个硬件中断(这不同于后面的软中断)。这个硬件中断由内核调用中断处理程序rtl8139_interrupt函数处理。这个函数比较重要,网卡发送或者接收数据时内核都会调用这个函数处理中断,而中断的类型是根据网卡状态寄存器的不同而确定的。本文中仅涉及接收数据的中断,因此只给出了接收的代码:
static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance, struct pt_regs *regs)
{
if (status & RxAckBits){
if (netif_rx_schedule_prep(dev))
__netif_rx_schedule (dev);
}
}
主要函数为__netif_rx_schedule(函数名意为:network interface receive schedule,即网络接口接收调度),因为当网卡接收到数据包之后,马上告知CPU在合适的时间去启动调度程序,轮询(poll)网卡。
请注意:Linux接收网络数据包实际上有两种方式。
(a)中断。每个数据包到达都会产生一个中断,然后由内核调用中断处理程序处理。
(b)NAPI(New API)。Linux内核2.6版本之后加入的新机制,核心方法是:不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后以POLL的方法来轮询数据。
因此本文中只介绍NAPI的接收方式。我们不再详细介绍这种机制,网上可找到比较多的资料,可以参考IBM的技术文章:NAPI 技术在 Linux 网络驱动上的应用和完善。
__netif_rx_schedule函数的定义如下:
static inline void __netif_rx_schedule(struct net_device *dev)
{
local_irq_save(flags);//disable interrupt
//Add interface to tail of rx poll list
list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
//activate network rx softirq
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
local_irq_restore(flags);
}
这个函数最核心的就是三步:
(a)local_irq_save:禁用中断
(b)list_add_tail:将设备添加到softnet_data的poll_list中。
(c)激活一个软中断NET_RX_SOFTIRQ。
======================================
说到这里我们必须介绍一个关键数据结构softnet_data,每个CPU都拥有一个这样的网络数据队列(所以函数中使用了__get_cpu_var函数取得),定义如下:
struct softnet_data
{
int throttle; /*为 1 表示当前队列的数据包被禁止*/
int cng_level; /*表示当前处理器的数据包处理拥塞程度*/
int avg_blog; /*某个处理器的平均拥塞度*/
struct sk_buff_head input_pkt_queue; /*接收缓冲区的sk_buff队列*/
struct list_head poll_list; /*POLL设备队列头*/
struct net_device output_queue; /*网络设备发送队列的队列头*/
struct sk_buff completion_queue; /*完成发送的数据包等待释放的队列*/
struct net_device backlog_dev; /*表示当前参与POLL处理的网络设备*/
};
大致说明一下这个数据结构的意义。某个网卡产生中断之后,内核就把这个网卡挂载到轮询列表(poll_list)中。一个CPU会轮询自己的列表中的每一个网卡,看看它们是不是有新的数据包可以处理。我们需要先用一个比喻说明这个数据结构与轮询的关系:网卡就是佃户,CPU就是地主。佃户有自己种的粮食(网络数据包),但地主家也有粮仓(softnet_data)。地主要收粮的时候,就会挨家挨户的去催佃户交粮,放到自己的粮仓里。
=======================================
(2)接收网络数据包的软中断处理
我们知道:激活软中断之后,并不是马上会被处理的。只有当遇到软中断的检查点时,系统才会调用相应的软中断处理函数。
所有的网络接收数据包的软中断处理函数都是net_rx_action。这个函数的详细注释可以看IBM的那篇技术文章。其核心语句就是一个轮询的函数:
dev->poll
就调用了相应设备的poll函数。也就是说,当CPU处理软中断时,才去轮询网卡,把数据放入softnet_data中。
下面是整个中断和轮询过程的一个示意图:
下面我们解释一下poll函数具体干了什么事情。
而我们知道,在Realtek 8139网卡的net_device对象中我们已经注册了一个poll函数:
dev->poll = rtl8139_poll
那么一次poll就表示从网卡缓冲区取出一定量的数据。而rtl8139_poll函数中调用的主要函数就是rtl8139_rx函数。这个函数是完成从网卡取数据,分配skb缓冲区的核心函数。其核心代码如下:
static int rtl8139_rx(struct net_device *dev, struct rtl8139_private *tp, int budget)
{
skb = dev_alloc_skb (pkt_size + 2);
eth_copy_and_sum (skb, &rx_ring[ring_offset + 4], pkt_size, 0);//memcpy
skb->protocol = eth_type_trans (skb, dev);
netif_receive_skb (skb);
}
工作主要分为4部分:
(a)给sk_buff数据结构(skb)分配空间。
(b)从网卡的环形缓冲区rx_ring中拷贝出网络数据包放到sk_buff对象skb中。这个函数实质上就是一个memcpy函数。
(c)在skb中标识其协议为以太网帧。
(d)调用netif_receice_skb函数。
netif_receive_skb函数相对比较重要。函数主体是两个循环:
list_for_each_entry_rcu(ptype, &ptype_all, list)
{
if (!ptype->dev || ptype->dev == skb->dev)
{
if (pt_prev)
ret = deliver_skb(skb, pt_prev);
pt_prev = ptype;
}
}
list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
if (ptype->type == type && (!ptype->dev || ptype->dev == skb->dev))
{
if (pt_prev)
ret = deliver_skb(skb, pt_prev);
pt_prev = ptype;
}
}
两个循环分别遍历了两个链表:ptype_all和ptype_base。前者是内核中注册的sniffer,后者则是注册到内核协议栈中的网络协议类型。如果skb中的协议类型type与ptype_base中的类型一致,那么使用deliver_skb函数发送给这个协议一份,定义如下:
static __inline__ int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev)
{
atomic_inc(&skb->users);
return pt_prev->func(skb, skb->dev, pt_prev);
}
这个函数只是一个封装函数,实际上调用了每个packet type结构中注册的处理函数func。
struct packet_type {
unsigned short type;
struct net_device *dev;
int (*func) (struct sk_buff *,
struct net_device *, struct packet_type *);
void *af_packet_priv;
struct list_head list;
};
例如:IP包类型的处理函数就是ip_rcv(定义在/net/ipv4/ip_output.c文件中),定义如下:
static struct packet_type ip_packet_type = {
.type = __constant_htons(ETH_P_IP),
.func = ip_rcv,
};
这个包的类型是在ip_init协议初始化时添加到全局的ptype_base哈希数组中的:
void __init ip_init(void)
{
dev_add_pack(&ip_packet_type);
}
这就是接收网络数据包的全过程。