Chinaunix首页 | 论坛 | 博客
  • 博客访问: 52255
  • 博文数量: 6
  • 博客积分: 73
  • 博客等级: 民兵
  • 技术积分: 50
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-08 16:28
文章分类

全部博文(6)

文章存档

2016年(1)

2011年(5)

我的朋友

分类:

2011-04-05 08:46:44

  |  |   

原文地址:
%3D1
Linux TCP/IP协议栈笔记

网卡驱动和队列层中的数据包接收

作者:kendo

Kernel:2.6.12
文章对于我们理解TCP发送数据包以及收取数据包非常有帮助。

四、网卡的数据接收

内核如何从网卡接受数据,传统的经典过程:
引用
1、数据到达网卡;
2、网卡产生一个中断给内核;
3、内核使用I/O指令,从网卡I/O区域中去读取数据;


我们在许多网卡驱动中,都可以在网卡的中断函数中见到这一过程。

但是,这一种方法,有一种重要的问题,就是大流量的数据来到,网卡会产生大量的中断,内核在中断上下文中,会浪费大量的资源来处理中断本身。所以,一个问题是,“可不可以不使用中断”,这就是轮询技术,所谓NAPI技术,说来也不神秘,就是说,内核屏蔽中断,然后隔一会儿就去问网卡,“你有没有数据啊?”……

从这个描述本身可以看到,哪果数据量少,轮询同样占用大量的不必要的CPU资源,大家各有所长吧,呵呵……

OK,另一个问题,就是从网卡的I/O区域,包括I/O寄存器或I/O内存中去读取数据,这都要CPU去读,也要占用CPU资源,“CPU从I/O区域读,然后把它放到内存(这个内存指的是系统本身的物理内存,跟外设的内存不相干,也叫主内存)中”。于是自然地,就想到了DMA技术——让网卡直接从主内存之间读写它们的I/O数据,CPU,这儿不干你事,自己找乐子去:
引用
1、首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
2、内核将这个缓冲区通过DMA映射,把这个队列交给网卡;
3、网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;
4、内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;


——呵呵,这一个过程比传统的过程少了不少工作,因为设备直接把数据放进了主内存,不需要CPU的干预,效率是不是提高不少?

对应以上4步,来看它的具体实现:
1、分配环形DMA缓冲区
Linux内核中,用skb来描述一个缓存,所谓分配,就是建立一定数量的skb,然后把它们组织成一个双向链表;

2、建立DMA映射
内核通过调用
dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
建立映射关系。
struct device *dev,描述一个设备;
buffer:把哪个地址映射给设备;也就是某一个skb——要映射全部,当然是做一个双向链表的循环即可;
size:缓存大小;
direction:映射方向——谁传给谁:一般来说,是“双向”映射,数据在设备和内存之间双向流动;

对于PCI设备而言(网卡一般是PCI的),通过另一个包裹函数pci_map_single,这样,就把buffer交给设备了!设备可以直接从里边读/取数据。

3、这一步由硬件完成;

4、取消映射
dma_unmap_single,对PCI而言,大多调用它的包裹函数pci_unmap_single,不取消的话,缓存控制权还在设备手里,要调用它,把主动权掌握在CPU手里——因为我们已经接收到数据了,应该由CPU把数据交给上层网络栈;

当然,不取消之前,通常要读一些状态位信息,诸如此类,一般是调用
dma_sync_single_for_cpu()
让CPU在取消映射前,就可以访问DMA缓冲区中的内容。

关于DMA映射的更多内容,可以参考《Linux设备驱动程序》“内存映射和DMA”章节相关内容!

OK,有了这些知识,我们就可以来看e100的代码了,它跟上面讲的步骤基本上一样的——绕了这么多圈子,就是想绕到e100上面了,呵呵!


在e100_open函数中,调用e100_up,我们前面分析它时,略过了一个重要的东东,就是环形缓冲区的建立,这一步,是通过
e100_rx_alloc_list函数调用完成的:

  1. static int e100_rx_alloc_list(struct nic *nic)  
  2. {  
  3.         struct rx *rx;  
  4.         unsigned int i, count = nic->params.rfds.count;  
  5.   
  6.         nic->rx_to_use = nic->rx_to_clean = NULL;  
  7.         nic->ru_running = RU_UNINITIALIZED;  
  8.   
  9.         /*结构struct rx用来描述一个缓冲区节点,这里分配了count个*/  
  10.         if(!(nic->rxs = kmalloc(sizeof(struct rx) * count, GFP_ATOMIC)))  
  11.                 return -ENOMEM;  
  12.         memset(nic->rxs, 0, sizeof(struct rx) * count);  
  13.   
  14.         /*虽然是连续分配的,不过还是遍历它,建立双向链表,然后为每一个rx的skb指针分员分配空间 
  15.         skb用来描述内核中的一个数据包,呵呵,说到重点了*/  
  16.         for(rx = nic->rxs, i = 0; i < count; rx++, i++) {  
  17.                 rx->next = (i + 1 < count) ? rx + 1 : nic->rxs;  
  18.                 rx->prev = (i == 0) ? nic->rxs + count - 1 : rx - 1;  
  19.                 if(e100_rx_alloc_skb(nic, rx)) {                /*分配缓存*/  
  20.                         e100_rx_clean_list(nic);  
  21.                         return -ENOMEM;  
  22.                 }  
  23.         }  
  24.   
  25.         nic->rx_to_use = nic->rx_to_clean = nic->rxs;  
  26.         nic->ru_running = RU_SUSPENDED;  
  27.   
  28.         return 0;  
  29. }  


  1. #define RFD_BUF_LEN (sizeof(struct rfd) + VLAN_ETH_FRAME_LEN)  
  2. static inline int e100_rx_alloc_skb(struct nic *nic, struct rx *rx)  
  3. {  
  4.         /*skb缓存的分配,是通过调用系统函数dev_alloc_skb来完成的,它同内核栈中通常调用alloc_skb的区别在于, 
  5.         它是原子的,所以,通常在中断上下文中使用*/  
  6.         if(!(rx->skb = dev_alloc_skb(RFD_BUF_LEN + NET_IP_ALIGN)))  
  7.                 return -ENOMEM;  
  8.   
  9.         /*初始化必要的成员 */  
  10.         rx->skb->dev = nic->netdev;  
  11.         skb_reserve(rx->skb, NET_IP_ALIGN);  
  12.         /*这里在数据区之前,留了一块sizeof(struct rfd) 这么大的空间,该结构的 
  13.         一个重要作用,用来保存一些状态信息,比如,在接收数据之前,可以先通过 
  14.         它,来判断是否真有数据到达等,诸如此类*/  
  15.         memcpy(rx->skb->data, &nic->blank_rfd, sizeof(struct rfd));  
  16.         /*这是最关键的一步,建立DMA映射,把每一个缓冲区rx->skb->data都映射给了设备,缓存区节点 
  17.         rx利用dma_addr保存了每一次映射的地址,这个地址后面会被用到*/  
  18.         rx->dma_addr = pci_map_single(nic->pdev, rx->skb->data,  
  19.                 RFD_BUF_LEN, PCI_DMA_BIDIRECTIONAL);  
  20.   
  21.         if(pci_dma_mapping_error(rx->dma_addr)) {  
  22.                 dev_kfree_skb_any(rx->skb);  
  23.                 rx->skb = 0;  
  24.                 rx->dma_addr = 0;  
  25.                 return -ENOMEM;  
  26.         }  
  27.   
  28.         /* Link the RFD to end of RFA by linking previous RFD to 
  29.          * this one, and clearing EL bit of previous.  */  
  30.         if(rx->prev->skb) {  
  31.                 struct rfd *prev_rfd = (struct rfd *)rx->prev->skb->data;  
  32.                 /*put_unaligned(val,ptr);用到把var放到ptr指针的地方,它能处理处理内存对齐的问题 
  33.                 prev_rfd是在缓冲区开始处保存的一点空间,它的link成员,也保存了映射后的地址*/  
  34.                 put_unaligned(cpu_to_le32(rx->dma_addr),  
  35.                         (u32 *)&prev_rfd->link);  
  36.                 wmb();  
  37.                 prev_rfd->command &= ~cpu_to_le16(cb_el);  
  38.                 pci_dma_sync_single_for_device(nic->pdev, rx->prev->dma_addr,  
  39.                         sizeof(struct rfd), PCI_DMA_TODEVICE);  
  40.         }  
  41.   
  42.         return 0;  
  43. }  

e100_rx_alloc_list函数在一个循环中,建立了环形缓冲区,并调用e100_rx_alloc_skb为每个缓冲区分配了空间,并做了
DMA映射。这样,我们就可以来看接收数据的过程了。

前面我们讲过,中断函数中,调用netif_rx_schedule,表明使用轮询技术,系统会在未来某一时刻,调用设备的poll函数:

  1. static int e100_poll(struct net_device *netdev, int *budget)  
  2. {  
  3.         struct nic *nic = netdev_priv(netdev);  
  4.         unsigned int work_to_do = min(netdev->quota, *budget);  
  5.         unsigned int work_done = 0;  
  6.         int tx_cleaned;  
  7.   
  8.         e100_rx_clean(nic, &work_done, work_to_do);  
  9.         tx_cleaned = e100_tx_clean(nic);  
  10.   
  11.         /* If no Rx and Tx cleanup work was done, exit polling mode. */  
  12.         if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {  
  13.                 netif_rx_complete(netdev);  
  14.                 e100_enable_irq(nic);  
  15.                 return 0;  
  16.         }  
  17.   
  18.         *budget -= work_done;  
  19.         netdev->quota -= work_done;  
  20.   
  21.         return 1;  
  22. }  

目前,我们只关心rx,所以,e100_rx_clean函数就成了我们关注的对像,它用来从缓冲队列中接收全部数据(这或许是取名为clean的原因吧!):

  1. static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done,  
  2.         unsigned int work_to_do)  
  3. {  
  4.         struct rx *rx;  
  5.         int restart_required = 0;  
  6.         struct rx *rx_to_start = NULL;  
  7.   
  8.         /* are we already rnr? then pay attention!!! this ensures that 
  9.          * the state machine progression never allows a start with a  
  10.          * partially cleaned list, avoiding a race between hardware 
  11.          * and rx_to_clean when in NAPI mode */  
  12.         if(RU_SUSPENDED == nic->ru_running)  
  13.                 restart_required = 1;  
  14.   
  15.         /* 函数最重要的工作,就是遍历环形缓冲区,接收数据*/  
  16.         for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {  
  17.                 int err = e100_rx_indicate(nic, rx, work_done, work_to_do);  
  18.                 if(-EAGAIN == err) {  
  19.                         /* hit quota so have more work to do, restart once 
  20.                          * cleanup is complete */  
  21.                         restart_required = 0;  
  22.                         break;  
  23.                 } else if(-ENODATA == err)  
  24.                         break/* No more to clean */  
  25.         }  
  26.   
  27.         /* save our starting point as the place we'll restart the receiver */  
  28.         if(restart_required)  
  29.                 rx_to_start = nic->rx_to_clean;  
  30.   
  31.         /* Alloc new skbs to refill list */  
  32.         for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {  
  33.                 if(unlikely(e100_rx_alloc_skb(nic, rx)))  
  34.                         break/* Better luck next time (see watchdog) */  
  35.         }  
  36.   
  37.         if(restart_required) {  
  38.                 // ack the rnr?  
  39.                 writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);  
  40.                 e100_start_receiver(nic, rx_to_start);  
  41.                 if(work_done)  
  42.                         (*work_done)++;  
  43.         }  
  44. }  


  1. static inline int e100_rx_indicate(struct nic *nic, struct rx *rx,  
  2.         unsigned int *work_done, unsigned int work_to_do)  
  3. {  
  4.         struct sk_buff *skb = rx->skb;  
  5.         struct rfd *rfd = (struct rfd *)skb->data;  
  6.         u16 rfd_status, actual_size;  
  7.   
  8.         if(unlikely(work_done && *work_done >= work_to_do))  
  9.                 return -EAGAIN;  
  10.   
  11.         /* 读取数据之前,也就是取消DMA映射之前,需要先读取cb_complete 状态位, 
  12.         以确定数据是否真的准备好了,并且,rfd的actual_size中,也包含了真实的数据大小 
  13.         pci_dma_sync_single_for_cpu函数前面已经介绍过,它让CPU在取消DMA映射之前,具备 
  14.         访问DMA缓存的能力*/  
  15.         pci_dma_sync_single_for_cpu(nic->pdev, rx->dma_addr,  
  16.                 sizeof(struct rfd), PCI_DMA_FROMDEVICE);  
  17.         rfd_status = le16_to_cpu(rfd->status);  
  18.   
  19.         DPRINTK(RX_STATUS, DEBUG, "status=0x%04X\n", rfd_status);  
  20.   
  21.         /* If data isn't ready, nothing to indicate */  
  22.         if(unlikely(!(rfd_status & cb_complete)))  
  23.                 return -ENODATA;  
  24.   
  25.         /* Get actual data size */  
  26.         actual_size = le16_to_cpu(rfd->actual_size) & 0x3FFF;  
  27.         if(unlikely(actual_size > RFD_BUF_LEN - sizeof(struct rfd)))  
  28.                 actual_size = RFD_BUF_LEN - sizeof(struct rfd);  
  29.   
  30.         /* 取消映射,因为通过DMA,网卡已经把数据放在了主内存中,这里一取消,也就意味着, 
  31.         CPU可以处理主内存中的数据了 */  
  32.         pci_unmap_single(nic->pdev, rx->dma_addr,  
  33.                 RFD_BUF_LEN, PCI_DMA_FROMDEVICE);  
  34.   
  35.         /* this allows for a fast restart without re-enabling interrupts */  
  36.         if(le16_to_cpu(rfd->command) & cb_el)  
  37.                 nic->ru_running = RU_SUSPENDED;  
  38.           
  39.         /*正确地设置data指针,因为最前面有一个sizeof(struct rfd)大小区域,跳过它*/  
  40.         skb_reserve(skb, sizeof(struct rfd));  
  41.         /*更新skb的tail和len指针,也是就更新接收到这么多数据的长度*/  
  42.         skb_put(skb, actual_size);  
  43.         /*设置协议位*/  
  44.         skb->protocol = eth_type_trans(skb, nic->netdev);  
  45.   
  46.         if(unlikely(!(rfd_status & cb_ok))) {  
  47.                 /* Don't indicate if hardware indicates errors */  
  48.                 nic->net_stats.rx_dropped++;  
  49.                 dev_kfree_skb_any(skb);  
  50.         } else if(actual_size > nic->netdev->mtu + VLAN_ETH_HLEN) {  
  51.                 /* Don't indicate oversized frames */  
  52.                 nic->rx_over_length_errors++;  
  53.                 nic->net_stats.rx_dropped++;  
  54.                 dev_kfree_skb_any(skb);  
  55.         } else {  
  56.                 /*网卡驱动要做的最后一步,就是统计接收计数器,设置接收时间戳,然后调用netif_receive_skb, 
  57.                 把数据包交给上层协议栈,自己的光荣始命也就完成了*/  
  58.                 nic->net_stats.rx_packets++;  
  59.                 nic->net_stats.rx_bytes += actual_size;  
  60.                 nic->netdev->last_rx = jiffies;  
  61.                 netif_receive_skb(skb);  
  62.                 if(work_done)  
  63.                         (*work_done)++;  
  64.         }  
  65.   
  66.         rx->skb = NULL;  
  67.   
  68.         return 0;  
  69. }  

网卡驱动执行到这里,数据接收的工作,也就处理完成了。但是,使用这一种方法的驱动,省去了网络栈中一个重要的内容,就是
“队列层”,让我们来看看,传统中断接收数据包模式下,使用netif_rx函数调用,又会发生什么。

PS:九贱没有去研究过所谓的“零拷贝”技术,不太清楚,它同这种DMA直取方式有何不同?难道是把网卡中的I/O内存直接映射到主内存中,这样CPU就可以像读取主内存一样,读取网卡的内存,但是这要求设备要有好大的I/O内存来做缓冲呀!!^o^,外行了……希望哪位DX提点!

TCP/IP四层模型和OSI七层模型
2008年01月24日 星期四 下午 06:51
表1-1是 TCP/IP四层模型和OSI七层模型对应表。我们把OSI七层网络模型和Linux TCP/IP四层概念模型对应,然后将各种网络协议归类。
表1-1 TCP/IP四层模型和OSI七层模型对应表

OSI七层网络模型

Linux TCP/IP四层概念模型

对应网络协议

应用层(Application

应用层

TFTP, FTP, NFS, WAIS

表示层(Presentation

Telnet, Rlogin, SNMP, Gopher

会话层(Session

SMTP, DNS

传输层(Transport

传输层

TCP, UDP

网络层(Network

网际层

IP, ICMP, ARP, RARP, AKP, UUCP

数据链路层(Data Link

网络接口

FDDI, Ethernet, Arpanet, PDN, SLIP, PPP

物理层(Physical

IEEE 802.1A, IEEE 802.2IEEE 802.11

1.网络接口
网络接口把数据链路层和物理层放在一起,对应TCP/IP概念模型的网络接口。对应的网络协议主要是:Ethernet、FDDI和能传输IP数据包的任何协议。
2.网际层
网络层对应Linux TCP/IP概念模型的网际层,网络层协议管理离散的计算机间的数据传输,如IP协议为用户和远程计算机提供了信息包的传输方法,确保信息包能正确地到达目的机器。这一过程中,IP和其他网络层的协议共同用于数据传输,如果没有使用一些监视系统进程的工具,用户是看不到在系统里的IP的。网络嗅探器Sniffers是能看到这些过程的一个装置(它可以是软件,也可以是硬件),它能读取通过网络发送的每一个包,即能读取发生在网络层协议的任何活动,因此网络嗅探器Sniffers会对安全造成威胁。重要的网络层协议包括ARP(地址解析协议)、ICMP(Internet控制消息协议)和IP协议(网际协议)等。
3.传输层
传输层对应Linux TCP/IP概念模型的传输层。传输层提供应用程序间的通信。其功能包括:格式化信息流;提供可靠传输。为实现后者,传输层协议规定接收端必须发回确认信息,如果分组丢失,必须重新发送。传输层包括TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议),它们是传输层中最主要的协议。TCP建立在IP之上,定义了网络上程序到程序的数据传输格式和规则,提供了IP数据包的传输确认、丢失数据包的重新请求、将收到的数据包按照它们的发送次序重新装配的机制。TCP 协议是面向连接的协议,类似于打电话,在开始传输数据之前,必须先建立明确的连接。UDP也建立在IP之上,但它是一种无连接协议,两台计算机之间的传输类似于传递邮件:消息从一台计算机发送到另一台计算机,两者之间没有明确的连接。UDP不保证数据的传输,也不提供重新排列次序或重新请求的功能,所以说它是不可靠的。虽然UDP的不可靠性限制了它的应用场合,但它比TCP具有更好的传输效率。
4.应用层
应用层、表示层和会话层对应Linux TCP/IP概念模型中的应用层。应用层位于协议栈的顶端,它的主要任务是应用。一般是可见的,如利用FTP(文件传输协议)传输一个文件,请求一个和目标计算机的连接,在传输文件的过程中,用户和远程计算机交换的一部分是能看到的。常见的应用层协议有:HTTP,FTP,Telnet,SMTP和Gopher等。应用层是Linux网络设定最关键的一层。Linux服务器的配置文档主要针对应用层中的协议。TCP/IP模型各个层次的功能和协议如表1-2所示。
表1-2 TCP/IP模型各个层次的功能和协议

层次名称

    

    

网络接口

Host-to-Net Layer

负责实际数据的传输,对应OSI参考模型的下两层

HDLC(高级链路控制协议)

PPP(点对点协议)

SLIP(串行线路接口协议)

网际层

Inter-network Layer

负责网络间的寻址

数据传输,对应OSI参考模型的第三层

IP(网际协议)

ICMP(网际控制消息协议)

ARP(地址解析协议)

RARP(反向地址解析协议)

传输层

Transport Layer

负责提供可靠的传输服务,对应OSI参考模型的第四层

TCP(控制传输协议)

UDP(用户数据报协议)

应用层

Application Layer

负责实现一切与应用程序相关的功能,对应OSI参考模型的上三层

FTP(文件传输协议)

HTTP(超文本传输协议)

DNS(域名服务器协议)

SMTP(简单邮件传输协议)

NFS(网络文件系统协议)

说明TCP/IP与OSI最大的不同在于OSI是一个理论上的网络通信模型,而TCP/IP则是实际运行的网络协议。
阅读(2378) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~