网络设备驱动和数据链路层的分析

来源:互联网 发布:碳晶地暖垫 知乎 编辑:程序博客网 时间:2024/04/30 15:15

当物理网络设备接收到数据时,系统是如何知道并读取数据的呢?当前可通过两种途径解决这个问题。一种方法是轮询方式,系统每隔一定的时间间隔就去检查一次物理设备,若设备“报告”说有数据到达,就调用读取数据的程序。在Linux中,轮询方式可通过定时器实现,但该方法存在一个明显的缺点:不管设备是否有数据,系统总是要固定地花CPU时间去查看设备,且可能延迟对一些紧急数据的处理,因为网络设备有数据时可能不能马上得到CPU的响应。在这种方式下,设备完全处于一种被动的状态,而CPU又负担过重。无论从资源的利用率上还是从效率上看,这种方法都不是最优的。另一种方法是中断方式,中断方式利用硬件体系结构的中断机制实现设备和系统的应答对话,即当物理设备需要CPU处理数据时,设备就发一个中断信号给系统,系统则在收到信号后调用相应的中断服务程序响应对设备中断的处理。中断方式有效地解决了设备与CPU的对话交流问题,并将CPU从繁重的设备轮询中解脱出来,大大提高了CPU的利用率。当前不管是Linux平台还是Windows平台,它们的网络设备驱动程序几乎都是使用中断方式的。故在此我们主要讨论基于中断方式的网络设备驱动程序。

网络分层引起的一个问题是,每层的协议在发送数据包时要加协议头和协议尾到原数据中,在收到数据包时则要将本层的协议头和协议尾从数据包中去掉。这使得在不同层协议间传输时,每层都需要知道自己这一层的协议头和协议尾在数据包的哪里。一种解决方法是在每层都复制缓冲区,但显然效率太低。Linux的做法是用一种数据结构sk_buff在不同协议层及网络设备驱动程序之间传送数据。sk_buff 包括指针和长度域段,允许每个协议层通过标准的函数操作传送的数据包。该数据结构在整个Linux的网络子系统包括网络设备中扮演了一个十分重要的角色,故我们在分析数据包的传输和接收之前,首先来看看sk_buff这个数据结构的内容及系统提供的相关操作。因为对该数据结构的了解将大大有助于对Linux整个网络子系统的理解。Sk_buff.doc

 

由于使用了硬件中断请求机制,当物理网络设备接收到新数据时,它将发送一个硬件中断请求给系统。系统在侦察到有物理设备发出中断请求,就会调用相应的中断服务程序来处理中断请求。在这里,系统首先要知道哪个中断对应哪个中断服务程序。为了让系统知道网络设备的中断服务程序,一般在网络设备初始化的时候或设备被打开时,要向系统登记中断号及相应的中断服务程序(用request_irq这个函数登记)。基于中断方式的设备驱动程序若在设备初始化和设备打开时都没向系统登记中断服务程序,则该设备肯定不能正常工作。由上几节的NE2000代码分析知,NE2000的中断号登记是在设备初始化的时候,并登记中断服务程序为ei_interrupt

 

一个网络接口的中断服务程序的工作步骤一般有以下几步:

 

1.确定发生中断的具体网络接口,是rq2dev_map[irq]还是 (struct device*) dev_id

2.打开标志位dev->interrupt,表示本服务程序正在被使用;

3.读取中断状态寄存器,根据寄存器判断中断发生的原因。有两种可能:一种是有新数据包到达;另一种是上次的数据传输已完成。

4.若是因为有新数据包到达,则调用接收数据包的子函数;

5.若中断由上次传输引起,则通知协议的上一层、修改接口的统计信息、关闭标志位tbusy为下次传输做准备;

6.关闭标志位interrupt

 

当中断服务程序明确物理网络设备有数据包收到时,将调用数据接收子程序来完成实际的依赖于硬件的数据接收工作,并在接收完成后通过函数netif_rx()[net/core/dev.c]将收到的数据包往上层传。数据接收子程序的内容可以由以下四点来概括:

 

1.申请skb缓冲区给新的数据包存储;

2.从硬件中读取新到达的数据;

3.调用netif_rx(),将新的数据包往网络协议的上一层传送;

4.修改接口的统计数据。

 

有上几节的网络设备的初始化分析可知,NE2000中断服务程序ei_interrupt(),同时我们还将发现NE2000的数据接收子程序是ei_receive()。下面我们将继续以NE2000为例,对网络接口的接收过程进行分析。

 

Linux的网络设备是如何置入内核并初始化的 :一系列device数据结构在dev_base 表中相互连接起来,每个device 结构描述了它的 设备,并提供回调例程,当需要网络驱动来执行工作时,网络协议层调用这些例程。这些函数与传输的数据及网络设备地址紧密相关.当一个网络设备从网上接收包时它 必须将接收的数据转换成sk_buff 结构,这些sk_buff 则被网络驱动加入到了backlog 队列中。如果backlog 队列太长,则丢弃接收的sk_buff 。准备好要运行时,网络底 层将被设置标志。当网络底层按计划开始运行后,处理backlog 队列之前任何等待着被 传输的网络包都由它来处理sk_buff 决定哪些层处理被接收的包

 

网卡设备驱动程序将硬件中断中接收到数据帧存入sk_buff结构然后检查硬件帧头, 识别帧类型, 放入接收队列backlog(net_rx完成), 激活接收软中
断作进一步处理.接收软中断(net_bh)提取接收包(中断的底半操作, 根据它所在

的设备和协议类型传递给各自的包处理器. 包处理器用dev_add_pack()注册,如果注册的

设备号是零则表明它接收所有设备的包, 如果注册的包类型是(ETH_P_ALL),则表示它接

收所有类型的包. 如果系统注册有(ETH_P_ALL)类型的包处理器,系统会通过

dev_queue_xmit_nit()
将每一发送包复制一份副本传递给它们
.

驱动程序 中一般都有以下的几个函数(名字不一定相同)

net_open(struct device *dev)

net_rx(struct device *dev)

net_send_packet(struct sk_buff *skb, struct device *dev)

net_close(struct device *dev)

static void net_interrupt(int irq, void *dev_id, struct pt_regs * regs)

int init_module(void)

 

其实驱动程序并不存在一个接收方法(后面做解释,问题 2)。有数据收到应该是驱动
程序通知系统的。一般设备收到数据后都会产生一个中断,在中断处理程序中驱动程序

申请一个sk_buff(skb),从硬件读出数据放置到申请好的缓冲区里。接下来填充
sk_buff
中的一些信息。skb->dev = dev,判断收到帧的协议类型,填入skb->protocol(多协议

的支持)。把指针skb->mac.raw指向硬件数据然后丢弃硬件帧头(skb_pull)。还要设置

skb->pkt_type
,标明第二层(链路层)数据类型。可以是以下类型:

PACKET_BROADCAST :
链路层广播

PACKET_MULTICAST :
链路层组播

PACKET_SELF :
发给自己的帧

PACKET_OTHERHOST :
发给别人的帧(监听模式时会有这种帧)

 

 

函数分析:

1         网卡接收数据处理

网卡驱动程序以3c501.c(3com网卡比较通用)为例:

static void el_receive(struct net_device *dev) //此设备的包接收主处理函数,前缀el3c501的标志

{

       struct net_local *lp = (struct net_local *)dev->priv;//网卡的私有数据存储网卡的状态信息

       int ioaddr = dev->base_addr;//网卡的内存影像区基地址

       int pkt_len;

       struct sk_buff *skb;

 

       pkt_len = inw(RX_LOW);//提取包的长度

       skb = dev_alloc_skb(pkt_len+2);//分配sk_buff结构

                  skb_reserve(skb,2);       /* Force 16 byte alignment */

              skb->dev = dev;

             

              insb(DATAPORT, skb_put(skb,pkt_len), pkt_len);//把数据从内存影像区读到skb

              skb->protocol=eth_type_trans(skb,dev);//去掉帧头,识别帧类型

              Netif_rx(skb); //放入接收队列, 激活接收软中断作进一步处理

              dev->last_rx = jiffies;

              lp->stats.rx_packets++;

              lp->stats.rx_bytes+=pkt_len;//设置网卡统计状态

       }

2         eth_type_trans

       unsigned short eth_type_trans(struct sk_buff *skb, struct net_device *dev)

{

       struct ethhdr *eth;

       unsigned char *rawp;

       skb->mac.raw=skb->data;//指向所接收数据帧

       skb_pull(skb,dev->hard_header_len);//退掉硬件头地址区域,此时data指向上层协议数据

       eth= skb->mac.ethernet;

       if(*eth->h_dest&1) //??

       {//测试目标地址的最高位(接网络比特顺序,最高有效位先传送

              if(memcmp(eth->h_dest,dev->broadcast, ETH_ALEN)==0)

                     skb->pkt_type=PACKET_BROADCAST;//接收到广播包

              else

                     skb->pkt_type=PACKET_MULTICAST;//接收到同播包

       }

              else if(1 /*dev->flags&IFF_PROMISC*/)//假设所有网卡都设置了混杂模式

       {

              if(memcmp(eth->h_dest,dev->dev_addr, ETH_ALEN))

                     skb->pkt_type=PACKET_OTHERHOST;//接收去往其它主机的包硬件地址不匹配

       }

              rawp = skb->data;

       }

 

3         Netif_rx

int netif_rx(struct sk_buff *skb)

{//其中在这段代码中加入了拥塞控制代码,不列出

       int this_cpu = smp_processor_id();//取得当前cpu

       struct softnet_data *queue;

       unsigned long flags;

 

       if (skb->stamp.tv_sec == 0)

              get_fast_time(&skb->stamp);//盖上时间戳标记到达时间

 

       /* The code is rearranged so that the path is the most

          short when CPU is congested, but is still operating.

        */

       queue = & softnet_data[this_cpu]接收包相关数据.doc;//每个cpu都有一个网络到达包的处理队列

       local_irq_save(flags);//保存现场到flags

                     dev_hold(skb->dev);//上锁

                     __skb_queue_tail(&queue->input_pkt_queue,skb);

/* 把此skb加入到softnet_data[cpu]的接收队列中去 */

                     __cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ);

                 /* 标记软中断位 irq_stat[cpu]. __softirq_active |=1<<2 */接收包相关数据结构.doc

       local_irq_restore(flags);