Linux网络子系统

Linux强大的网络功能是如何实现的,让我们一起进入Linux内核的网络系统了解一下吧。 

7.1. sk_buff结构 
在Linux内核的网络实现中,使用了一个缓存结构(struct sk_buff)来管理网络报文,这个缓存区也叫套接字缓存。sk_buff 是内核网络子系统中最重要的一种数据结构,它贯穿网络报文收发的整个周期。该结构在内核源码的include/linux/skbuff.h文件中定义。 我们有必要了解结构中每个字段的意义。 

一个套接字缓存由两部份组成: 

·         报文数据:存储实际需要通过网络发送和接收的数据。 

·         管理数据(struct sk_buff):管理报文所需的数据,在sk_buff结构中有一个head指针指向内存中报文 数据开始的位置,有一个data指针指向报文数据在内存中的具体地址。head和data之间申请有足够多的空间用来存放报文头信息。 

struct sk_buff结构在内存中的结构示意图: 

                 sk_buff 
 -----------------------------------   ------------> skb->head 
|            headroom               | 
|-----------------------------------|  ------------> skb->data 
|              DATA                 |                
|                                   | 
|                                   | 
|                                   | 
|                                   | 
|                                   | 
|                                   | 
|                                   | 
|                                   | 
|                                   | 
|-----------------------------------|  ------------> skb->tail 
|            tailroom               | 
 -----------------------------------   ------------> skb->end 
  
7.2. sk_buff结构操作函数 
内核通过alloc_skb()和dev_alloc_skb()为套接字缓存申请内存空间。这两个函数的定义位于net/core /skbuff.c文件内。通过这alloc_skb()申请的内存空间有两个,一个是存放实际报文数据的内存空间,通过kmalloc()函数申请;一 个是sk_buff数据结构的内存空间,通过 kmem_cache_alloc()函数申请。dev_alloc_skb()的功能与 alloc_skb()类似,它只被驱动程序的中断所调用,与alloc_skb()比较只是申请的内存空间长度多了16个字节。 

内核通过kfree_skb()和dev_kfree_skb()释放为套接字缓存申请的内存空间。dev_kfree_skb()被驱动程序使 用,功能与kfree_skb()一样。当skb->users为1时kfree_skb()才会执行释放内存空间的动作,否则只会减少 skb->users的值。skb->users为1表示已没有其他用户使用该缓存了。 

skb_reserve()函数为skb_buff缓存结构预留足够的空间来存放各层网络协议的头信息。该函数在在skb缓存申请成功后,加载报 文数据前执行。在执行skb_reserve()函数前,skb->head,skb->data和skb->tail指针的位置的一 样的,都位于skb内存空间的开始位置。这部份空间叫做headroom。有效数据后的空间叫tailroom。skb_reserve的操作只是把 skb->data和skb->tail指针向后移,但缓存总长不变。 

运行skb_reserve()前sk_buff的结构 
  
        sk_buff 
 ----------------------   ---------->  skb->head,skb->data,skb->tail 
|                      | 
|                      | 
|                      | 
|                      | 
|                      | 
|                      | 
|                      | 
|                      | 
|                      | 
 ---------------------    ---------->  skb->end 
  
运行skb_reserve()后sk_buff的结构 
  
        sk_buff 
 ----------------------   ---------->  skb->head 
|                      | 
|      headroom        | 
|                      | 
|--------------------- |  ---------->  skb->data,skb->tail 
|                      | 
|                      | 
|                      | 
|                      | 
|                      | 
 ---------------------    ---------->  skb->end 
         
skb_put()向后扩大数据区空间,tailroom空间减少,skb->data指针不变,skb->tail指针下移。 

skb_push()向前扩大数据区空间,headroom空间减少,skb->tail指针不变,skb->data指针上移 

skb_pull()缩小数据区空间,headroom空间增大,skb->data指针下移,skb->tail指针不变。 

skb_shared_info结构位于skb->end后,用skb_shinfo函数申请内存空间。该结构主要用以描述data内存空间的信息。 

 ---------------------  ----------->  skb->head 
|                     | 
|                     | 
|      sk_buff        | 
|                     | 
|                     | 
|                     | 
|---------------------| ----------->  skb->end 
|                     | 
|   skb_share_info    | 
|                     | 
 --------------------- 
skb_clone和skb_copy可拷贝一个sk_buff结构,skb_clone方式是clone,只生成新的sk_buff内存区,不 会生成新的data内存区,新sk_buff的skb->data指向旧data内存区。skb_copy方式是完全拷贝,生成新的sk_buff 内存区和data内存区。。 

7.3. net_device结构 
net_device结构是Linux内核中所有网络设备的基础数据结构。包含网络适配器的硬件信息(中断、端口、驱动程序函数等)和高层网络协议的网络配置信息(IP地址、子网掩码等)。该结构的定义位于include/linux/netdevice.h 

每个net_device结构表示一个网络设备,如eth0、eth1...。这些网络设备通过dev_base线性表链接起来。内核变量 dev_base表示已注册网络设备列表的入口点,它指向列表的第一个元素(eth0)。然后各元素用next字段指向下一个元素(eth1)。使用 ifconfig -a命令可以查看系统中所有已注册的网络设备。 

net_device结构通过alloc_netdev函数分配,alloc_netdev函数位于net/core/dev.c文件中。该函数需要三个参数。 

·         私有数据结构的大小 

·         设备名,如eth0,eth1等。 

·         配置例程,这些例程会初始化部分net_device字段。 

分配成功则返回指向net_device结构的指针,分配失败则返回NULL。 

7.4. 网络设备初始化 
在使用网络设备之前,必须对它进行初始化和向内核注册该设备。网络设备的初始化包括以下步骤: 

·         硬件初始化:分配IRQ和I/O端口等。 

·         软件初始化:分配IP地址等。 

·         功能初始化:QoS等 

7.5. 网络设备与内核的沟通方式 
网络设备(网卡)通过轮询和中断两种方式与内核沟通。 

·         轮询(polling),由内核发起,内核周期性地检查网络设备是否有数据要处理。 

·         中断(interrupt),由设备发起,设备向内核发送一个硬件中断信号。 

Linux网络系统可以结合轮询和中断两方式以提高网络系统的性能。共小节重点介绍中断方式。 

每个中断都会调用一个叫中断处理器的函数。当驱动程序向内核注册一个网卡时,会请求和分配一个IRQ号。接着为分配的这个IRQ注册中断处理器。 注册和释放中断处理器的代码是架构相关的,不同的硬件平台有不同的代码实现。实现代码位于kernel/irq/manage.c和arch/XXX /kernel/irq.c源码文件中。XXX是不同硬件架构的名称,如我们所使用得最多的i386架构。下面是注册和释放中断处理器的函数原型。 

int request_irq(unsigned int irq, irq_handler_t handler, 
                unsigned long irqflags, const char *devname, void *dev_id) 
  
void free_irq(unsigned int irq, void *dev_id)  
内核是通过IRQ号来找到对应的中断处理器并执行它的。为了找到中断处理器,内核把IRQ号和中断处理器函数的关联起来存储在全局表(global table)中。IRQ号和中断处理器的关联性可以是一对一,也可以是一对多。因为IRQ号是可以被多个设备所共享的。 

通过中断,网卡设备可以向驱动程序传送以下信息: 

·         帧的接收,这是最用的中断类型。 

·         传送失败通知,如传送超时。 

·         DMA传送成功。 

·         设备有足够的内存传送数据帧。当外出队列没有足够的内存空间存放一个最大的帧时(对于以太网卡是1535),网卡产生一个中 断要求以后再传送数据,驱动程序会禁止数据的传送,。当有效内存空间多于设备需传送的最大帧(MTU)时,网卡会发送一个中断通知驱动程序重新启用数据传 送。这些逻辑处理在网卡驱动程序中设计。 netif_stop_queue()函数禁止设备传送队列,netif_start_queue()函数重启 设备的传送队列。这些动作一般在驱动程序的xxx_start_xmit()中处理。 

系统的中断资源是有限的,不可能为每种设备提供独立的中断号,多种设备要共享有限的中断号。上面我们提到中断号是和中断处理器关联的。在中断号共 享的情况下内核如何正确找到对应的中断处理器呢?内核采用一种最简单的方法,就是不管三七二一,当同一中断号的中断发生时,与该中断号关联的所有中断处理 器都一起被调用。调用后再靠中断处理器中的过滤程序来筛选执行真正的中断处理。 

对于使用共享中断号的设备,它的驱动程序在注册时必须先指明允许中断共享。 

IRQ与中断处理器的映射关系保存在一个矢量表中。该表保存了每个IRQ的中断处理器。矢量表的大小是平台相关的,从15(i386)到超过 200都有。 irqaction数据结构保存了映射表的信息。上面提到的request_irq()函数创建irqaction数据结构并通过 setup_irq()把它加入到irq_des矢量表中。irq_des在 kernel/irq/handler.c中定义,平台相关的定义在 arch/XXX/kernel/irq.c文件中。setup_irq()在kernel/irq/manage.c,平台相关的定义在arch /XXX/kernel/irq.c中。 

7.6. 网络设备操作层的初始化 
在系统启动阶段,网络设备操作层通过net_dev_init()进行初始化。net_dev_init()的代码在net/core/dev.c文件中。这是一个以__init标识的函数,表示它是一个低层的代码。 

net_dev_init()的主要初始化工作内容包括以下几点: 

·         生成/proc/net目录和目录下相关的文件。 

7.7. 内核模块加载器 
kmod是内核模块加载器。该加载器在系统启动时会触发/sbin/modprobe和/sbin/hotplug自动加载相应的内核模块和运行 设备启动脚本。modprobe使用/etc/modprobe.conf配置文件。当该文件中有"alias eth0 3c59x"配置时就会自动加 3c59x.ko模块。 

7.8. 虚拟设备 
虚拟设备是在真实设备上的虚拟,虚拟设备和真实设备的对应关系可以一对多或多对一。即一个虚拟设备对应多个真实设备或多个真实设备一个虚拟设备。下面介绍网络子系统中虚拟设备的应用情况。 

·         Bonding,把多个真实网卡虚拟成一个虚拟网卡。对于应用来讲就相当于访问一个网络接口。 

·         802.1Q,802.3以太网帧头扩展,添加了VLAN头信息。把多个真实网卡虚拟成一个虚拟网卡。 

·         Bridging,一个虚拟网桥,把多个真实网卡虚拟成一个虚拟网卡。 

·         Tunnel interfaces,实现GRE和IP-over-IP虚拟通道。把一个真实网卡虚拟成多个虚拟网卡。 

·         True equalizer (TEQL),类似于Bonding。 

上面不是一个完整列表,随着内核的不断开发完善,新功能新应用也会不断出现。 

7.9. 8139too.c源码分析 
程序调用流程: 

module_init(rtl8139_init_module) 
  
static int __init rtl8139_init_module (void) 
  
pci_register_driver(&rtl8139_pci_driver)                  #注册驱动程序 
  
static int __devinit rtl8139_init_one (struct pci_dev *pdev, 
                                       const struct pci_device_id *ent) 
  
static int __devinit rtl8139_init_board (struct pci_dev *pdev, 
                                         struct net_device **dev_out) 
  
dev = alloc_etherdev (sizeof (*tp))         #为设备分配net_device数据结构 
  
pci_enable_device (pdev)        #激活PCI设备 
  
pci_resource_start (pdev, 0)    #获取PCI I/O区域1的首地址 
pci_resource_end (pdev, 0)      #获取PCI I/O区域1的尾地址 
pci_resource_flags (pdev, 0)    #获取PCI I/O区域1资源标记 
pci_resource_len (pdev, 0)      #获取区域资源长度 
  
pci_resource_start (pdev, 1)    #获取PCI I/O区域2的首地址 
pci_resource_end (pdev, 1)      #获取PCI I/O区域2的尾地址 
pci_resource_flags (pdev, 1)    #获取PCI I/O区域2资源标记 
pci_resource_len (pdev, 1)      #获取区域资源长度 
  
pci_request_regions(pdev, DRV_NAME)     #检查其它PCI设备是否使用了相同的地址资源 
  
pci_set_master(pdev)      #通过设置PCI设备的命令寄存器允许DMA 
7.10. 内核网络数据流 
网络报文从应用程序产生,通过网卡发送,在另一端的网卡接收数据并传递给应用程序。这个过程网络报文在内核中调用了一系列的函数。下面把这些函数列举出来,方便我们了解网络报文的流程。 

发送流程: 

write 
  | 
sys_write 
  | 
sock_sendmsg 
  | 
inet_sendmsg 
  | 
tcp_sendmsg 
  | 
tcp_push_one 
  | 
tcp_transmit_skb 
  | 
ip_queue_xmit 
  | 
ip_route_output 
  | 
ip_queue_xmit 
  | 
ip_queue_xmit2 
  | 
ip_output 
  | 
ip_finish_output 
  | 
neith_connected_output 
  | 
dev_queue_xmit ----------------| 
  |                            | 
  |                           queue_run 
  |                           queue_restart 
  |                            | 
hard_start_xmit----------------- 
接收流程: 

netif_rx 
  | 
netif_rx_schedule 
  | 
_cpu_raise_softirq 
  | 
net_rx_action 
  | 
ip_rcv 
  | 
ip_rcv_finish 
  | 
ip_route_input 
  | 
ip_local_deliver 
  | 
ip_local_deliver_finish 
  | 
tcp_v4_rcv 
  | 
tcp_v4_do_rcv 
  | 
tcp_rcv_established------------------| 
  |                                  | 
tcp_data_queue                       | 
  |                                  | 
_skb_queue_tail----------------------| 
  | 
data_ready 
  | 
sock_def_readable 
  | 
wake_up_interruptible 
  | 
tcp_data_wait 
  | 
tcp_recvmsg 
  | 
inet_recvmsg 
  | 
sock_recvmsg 
  | 
sock_read 
  |   
read 

   
  
数据包在应用层称为data,在TCP层称为segment,在IP层称为packet,在数据链路层称为frame。

你可能感兴趣的:(Linux网络子系统)