Linux Tcpip协议栈笔记.doc_第1页
Linux Tcpip协议栈笔记.doc_第2页
Linux Tcpip协议栈笔记.doc_第3页
Linux Tcpip协议栈笔记.doc_第4页
Linux Tcpip协议栈笔记.doc_第5页
已阅读5页,还剩95页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Linux TCP/IP协议栈笔记(一)网卡驱动和队列层中的数据包接收tcp/ip 2008-05-16 12:05:00 阅读1 评论0 字号:大中小 数据包的接收作者:kendo/viewthread.php?tid=14&extra=page%3D1Kernel:2.6.12一、从网卡说起这并非是一个网卡驱动分析的专门文档,只是对网卡处理数据包的流程进行一个重点的分析。这里以Intel的e100驱动为例进行分析。大多数网卡都是一个PCI设备,PCI设备都包含了一个标准的配置寄存器,寄存器中,包含了PCI设备的厂商ID、设备ID等等信息,驱动程序使用来描述这些寄存器的标识符。如下:Copy to clipboard - CODE:struct pci_device_id _u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/ _u32 subvendor, subdevice; /* Subsystem IDs or PCI_ANY_ID */ _u32 class, class_mask; /* (class,subclass,prog-if) triplet */ kernel_ulong_t driver_data; /* Data private to the driver */;这样,在驱动程序中,常常就可以看到定义一个struct pci_device_id 类型的数组,告诉内核支持不同类型的PCI设备的列表,以e100驱动为例:#define INTEL_8255X_ETHERNET_DEVICE(device_id, ich) PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID, PCI_CLASS_NETWORK_ETHERNET 8, 0xFFFF00, ich static struct pci_device_id e100_id_table = INTEL_8255X_ETHERNET_DEVICE(0x1029, 0), INTEL_8255X_ETHERNET_DEVICE(0x1030, 0), INTEL_8255X_ETHERNET_DEVICE(0x1031, 3),/*略过一大堆支持的设备*/ 0, ;在内核中,一个PCI设备,使用struct pci_driver结构来描述,struct pci_driver struct list_head node; char *name; struct module *owner; const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */ int(*probe)(struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ int(*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */ int(*resume) (struct pci_dev *dev); /* Device woken up */ int(*enable_wake) (struct pci_dev *dev, pci_power_t state, int enable); /* Enable wake event */ void (*shutdown) (struct pci_dev *dev); struct device_driver driver; struct pci_dynids dynids;因为在系统引导的时候,PCI设备已经被识别,当内核发现一个已经检测到的设备同驱动注册的id_table中的信息相匹配时,它就会触发驱动的probe函数,以e100为例:/* 定义一个名为e100_driver的PCI设备* 1、设备的探测函数为e100_probe;* 2、设备的id_table表为e100_id_table*/static struct pci_driver e100_driver = .name = DRV_NAME, .id_table = e100_id_table, .probe = e100_probe, .remove = _devexit_p(e100_remove),#ifdef CONFIG_PM .suspend = e100_suspend, .resume = e100_resume,#endif .driver = .shutdown = e100_shutdown, ;这样,如果系统检测到有与id_table中对应的设备时,就调用驱动的probe函数。驱动设备在init函数中,调用pci_module_init函数初始化PCI设备e100_driver:static int _init e100_init_module(void) if(1 debug) - 1) & NETIF_MSG_DRV) printk(KERN_INFO PFX %s, %sn, DRV_DESCRIPTION, DRV_VERSION); printk(KERN_INFO PFX %sn, DRV_COPYRIGHT); return pci_module_init(&e100_driver);一切顺利的话,注册的e100_probe函数将被内核调用,这个函数完成两个重要的工作:1、分配/初始化/注册网络设备;2、完成PCI设备的I/O区域的分配和映射,以及完成硬件的其它初始化工作;网络设备使用struct net_device结构来描述,这个结构非常之大,许多重要的参考书籍对它都有较为深入的描述,可以参考Linux设备驱动程序中网卡驱动设计的相关章节。我会在后面的内容中,对其重要的成员进行注释;当probe函数被调用,证明已经发现了我们所支持的网卡,这样,就可以调用register_netdev函数向内核注册网络设备了,注册之前,一般会调用alloc_etherdev为以太网分析一个net_device,然后初始化它的重要成员。除了向内核注册网络设备之外,探测函数另一项重要的工作就是需要对硬件进行初始化,比如,要访问其I/O区域,需要为I/O区域分配内存区域,然后进行映射,这一步一般的流程是:1、request_mem_region()2、ioremap()对于一般的PCI设备而言,可以调用:1、pci_request_regions()2、ioremap()pci_request_regions函数对PCI的6个寄存器都会调用资源分配函数进行申请(需要判断是I/O端口还是I/O内存),例如:Copy to clipboard - CODE:int pci_request_regions(struct pci_dev *pdev, char *res_name) int i; for (i = 0; i 6; i+) if(pci_request_region(pdev, i, res_name) goto err_out; return 0;Copy to clipboard - CODE:int pci_request_region(struct pci_dev *pdev, int bar, char *res_name) if (pci_resource_len(pdev, bar) = 0) return 0; if (pci_resource_flags(pdev, bar) & IORESOURCE_IO) if (!request_region(pci_resource_start(pdev, bar), pci_resource_len(pdev, bar), res_name) goto err_out; else if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) if (!request_mem_region(pci_resource_start(pdev, bar), pci_resource_len(pdev, bar), res_name) goto err_out; return 0;有了这些基础,我们来看设备的探测函数:static int _devinit e100_probe(struct pci_dev *pdev, const struct pci_device_id *ent) struct net_device *netdev; struct nic *nic; int err; /*分配网络设备*/ if(!(netdev = alloc_etherdev(sizeof(struct nic) if(1 open = e100_open; netdev-stop = e100_close; netdev-hard_start_xmit = e100_xmit_frame; netdev-get_stats = e100_get_stats; netdev-set_multicast_list = e100_set_multicast_list; netdev-set_mac_address = e100_set_mac_address; netdev-change_mtu = e100_change_mtu; netdev-do_ioctl = e100_do_ioctl; SET_ETHTOOL_OPS(netdev, &e100_ethtool_ops); netdev-tx_timeout = e100_tx_timeout; netdev-watchdog_timeo = E100_WATCHDOG_PERIOD; netdev-poll = e100_poll; netdev-weight = E100_NAPI_WEIGHT;#ifdef CONFIG_NET_POLL_CONTROLLER netdev-poll_controller = e100_netpoll;#endif /*设置网络设备名称*/ strcpy(netdev-name, pci_name(pdev); /*取得设备私有数据结构*/ nic = netdev_priv(netdev); /*网络设备指针,指向自己*/ nic-netdev = netdev; /*PCIy设备指针,指向自己*/ nic-pdev = pdev; nic-msg_enable = (1 dev); /*分配完成后,映射I/O内存*/ nic-csr = ioremap(pci_resource_start(pdev, 0), sizeof(struct csr); if(!nic-csr) DPRINTK(PROBE, ERR, Cannot map device registers, aborting.n); err = -ENOMEM; goto err_out_free_res; if(ent-driver_data) nic-flags |= ich; else nic-flags &= ich; /*设置设备私有数据结构的大部份默认参数*/ e100_get_defaults(nic); /* 初始化自旋锁,锅的初始化必须在调用 hw_reset 之前执行*/ spin_lock_init(&nic-cb_lock); spin_lock_init(&nic-cmd_lock); /* 硬件复位,通过向指定I/O端口设置复位指令实现. */ e100_hw_reset(nic); /* * PCI网卡被BIOS配置后,某些特性可能会被屏蔽掉。比如,多数BIOS都会清掉“master”位, * 这导致板卡不能随意向主存中拷贝数据。pci_set_master函数数会检查是否需要设置标志位, * 如果需要,则会将“master”位置位。 * PS:什么是PCI master? * 不同于ISA总线,PCI总线的地址总线与数据总线是分时复用的。这样做的好处是,一方面 * 可以节省接插件的管脚数,另一方面便于实现突发数据传输。在做数据传输时,由一个PCI * 设备做发起者(主控,Initiator或Master),而另一个PCI设备做目标(从设备,Target或Slave)。 * 总线上的所有时序的产生与控制,都由Master来发起。PCI总线在同一时刻只能供一对设备完成传输。 */ pci_set_master(pdev); /*添加两个内核定时器,watchdog和blink_timer*/ init_timer(&nic-watchdog); nic-watchdog.function = e100_watchdog; nic-watchdog.data = (unsigned long)nic; init_timer(&nic-blink_timer); nic-blink_timer.function = e100_blink_led; nic-blink_timer.data = (unsigned long)nic; INIT_WORK(&nic-tx_timeout_task, (void (*)(void *)e100_tx_timeout_task, netdev); if(err = e100_alloc(nic) DPRINTK(PROBE, ERR, Cannot alloc driver memory, aborting.n); goto err_out_iounmap; /*phy寄存器初始化*/ e100_phy_init(nic); if(err = e100_eeprom_load(nic) goto err_out_free; memcpy(netdev-dev_addr, nic-eeprom, ETH_ALEN); if(!is_valid_ether_addr(netdev-dev_addr) DPRINTK(PROBE, ERR, Invalid MAC address from EEPROM, aborting.n); err = -EAGAIN; goto err_out_free; /* Wol magic packet can be enabled from eeprom */ if(nic-mac = mac_82558_D101_A4) & (nic-eepromeeprom_id & eeprom_id_wol) nic-flags |= wol_magic; /* ack any pending wake events, disable PME */ pci_enable_wake(pdev, 0, 0); /*注册网络设备*/ strcpy(netdev-name, eth%d); if(err = register_netdev(netdev) DPRINTK(PROBE, ERR, Cannot register net device, aborting.n); goto err_out_free; DPRINTK(PROBE, INFO, addr 0x%lx, irq %d, MAC addr %02X:%02X:%02X:%02X:%02X:%02Xn, pci_resource_start(pdev, 0), pdev-irq, netdev-dev_addr0, netdev-dev_addr1, netdev-dev_addr2, netdev-dev_addr3, netdev-dev_addr4, netdev-dev_addr5); return 0;err_out_free: e100_free(nic);err_out_iounmap: iounmap(nic-csr);err_out_free_res: pci_release_regions(pdev);err_out_disable_pdev: pci_disable_device(pdev);err_out_free_dev: pci_set_drvdata(pdev, NULL); free_netdev(netdev); return err;执行到这里,探测函数的使命就完成了,在对网络设备重要成员初始化时,有:netdev-open = e100_open;指定了设备的open函数为e100_open,这样,当第一次使用设备,比如使用ifconfig工具的时候,open函数将被调用。二、打开设备在探测函数中,设置了netdev-open = e100_open; 指定了设备的open函数为e100_open:Copy to clipboard - CODE:static int e100_open(struct net_device *netdev) struct nic *nic = netdev_priv(netdev); int err = 0; netif_carrier_off(netdev); if(err = e100_up(nic) DPRINTK(IFUP, ERR, Cannot open interface, aborting.n); return err;大多数涉及物理设备可以感知信号载波(carrier)的存在,载波的存在意味着设备可以工作据个例子来讲:当一个用户拔掉了网线,也就意味着信号载波的消失。netif_carrier_off:关闭载波信号;netif_carrier_on:打开载波信号;netif_carrier_ok:检测载波信号;对于探测网卡网线是否连接,这一组函数被使用得较多;接着,调用e100_up函数启动网卡,这个“启动”的过程,最重要的步骤有:1、调用request_irq向内核注册中断;2、调用netif_wake_queue函数来重新启动传输队例;Copy to clipboard - CODE:static int e100_up(struct nic *nic) int err; if(err = e100_rx_alloc_list(nic) return err; if(err = e100_alloc_cbs(nic) goto err_rx_clean_list; if(err = e100_hw_init(nic) goto err_clean_cbs; e100_set_multicast_list(nic-netdev); e100_start_receiver(nic, 0); mod_timer(&nic-watchdog, jiffies); if(err = request_irq(nic-pdev-irq, e100_intr, SA_SHIRQ, nic-netdev-name, nic-netdev) goto err_no_irq; netif_wake_queue(nic-netdev); netif_poll_enable(nic-netdev); /* enable ints _after_ enabling poll, preventing a race between * disable ints+schedule */ e100_enable_irq(nic); return 0;err_no_irq: del_timer_sync(&nic-watchdog);err_clean_cbs: e100_clean_cbs(nic);err_rx_clean_list: e100_rx_clean_list(nic); return err;这样,中断函数e100_intr将被调用;三、网卡中断从本质上来讲,中断,是一种电信号,当设备有某种事件发生的时候,它就会产生中断,通过总线把电信号发送给中断控制器,如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到内存中内核设置的中断处理程序的入口点,进行中断处理。在内核中断处理中,会检测中断与我们刚才注册的中断号匹配,于是,注册的中断处理函数就被调用了。当需要发/收数据,出现错误,连接状态变化等,网卡的中断信号会被触发。当接收到中断后,中断函数读取中断状态位,进行合法性判断,如判断中断信号是否是自己的等,然后,应答设备中断OK,我已经知道了,你回去继续工作吧接着,它就屏蔽此中断,然后netif_rx_schedule函数接收,接收函数 会在未来某一时刻调用设备的poll函数(对这里而言,注册的是e100_poll)实现设备的轮询:Copy to clipboard - CODE:static irqreturn_t e100_intr(int irq, void *dev_id, struct pt_regs *regs) struct net_device *netdev = dev_id; struct nic *nic = netdev_priv(netdev); u8 stat_ack = readb(&nic-csr-scb.stat_ack); DPRINTK(INTR, DEBUG, stat_ack = 0x%02Xn, stat_ack); if(stat_ack = stat_ack_not_ours | /* Not our interrupt */ stat_ack = stat_ack_not_present) /* Hardware is ejected */ return IRQ_NONE; /* Ack interrupt(s) */ writeb(stat_ack, &nic-csr-scb.stat_ack); /* We hit Receive No Resource (RNR); restart RU after cleaning */ if(stat_ack & stat_ack_rnr) nic-ru_running = RU_SUSPENDED; e100_disable_irq(nic); netif_rx_schedule(netdev); return IRQ_HANDLED;对于数据包的接收而言,我们关注的是poll函数中,调用e100_rx_clean进行数据的接收:Copy to clipboard - CODE:static int e100_poll(struct net_device *netdev, int *budget) struct nic *nic = netdev_priv(netdev); /* * netdev-quota是当前CPU能够从所有接口中接收数据包的最大数目,budget是在 * 初始化阶段分配给接口的weight值,轮询函数必须接受二者之间的最小值。表示 * 轮询函数本次要处理的数据包个数。 */ unsigned int work_to_do = min(netdev-quota, *budget); unsigned int work_done = 0; int tx_cleaned; /*进行数据包的接收和传输*/ e100_rx_clean(nic, &work_done, work_to_do); tx_cleaned = e100_tx_clean(nic); /*接收和传输完成后,就退出poll模块,重启中断*/ /* If no Rx and Tx cleanup work was done, exit polling mode. */ if(!tx_cleaned & (work_done = 0) | !netif_running(netdev) netif_rx_complete(netdev); e100_enable_irq(nic); return 0; *budget -= work_done; netdev-quota -= work_done; return 1;static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done, unsigned int work_to_do) struct rx *rx; int restart_required = 0; struct rx *rx_to_start = NULL; /* are we already rnr? then pay attention! this ensures that * the state machine progression never allows a start with a * partially cleaned list, avoiding a race between hardware * and rx_to_clean when in NAPI mode */ if(RU_SUSPENDED = nic-ru_running) restart_required = 1; /* Indicate newly arrived packets */ for(rx = nic-rx_to_clean; rx-skb; rx = nic-rx_to_clean = rx-next) int err = e100_rx_indicate(nic, rx, work_done, work_to_do); if(-EAGAIN = err) /* hit quota so have more work to do, restart once * cleanup is complete */ restart_required = 0; break; else if(-ENODATA = err) break; /* No more to clean */ /* save our starting point as the place well restart the receiver */ if(restart_required) rx_to_start = nic-rx_to_clean; /* Alloc new skbs to refill list */ for(rx = nic-rx_to_use; !rx-skb; rx = nic-rx_to_use = rx-next) if(unlikely(e100_rx_alloc_skb(nic, rx) break; /* Better luck next time (see watchdog) */ if(restart_required) / ack the rnr? writeb(stat_ack_rnr, &nic-csr-scb.stat_ack); e100_start_receiver(nic, rx_to_start); if(work_done) (*work_done)+; 四、网卡的数据接收内核如何从网卡接受数据,传统的经典过程: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_s

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

最新文档

评论

0/150

提交评论