Linux Kernel 内存管理 - 网络

Chapter 7. 网络子系统
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 Kernel 内存管理 - 网络)