您的位置:首页 > 理论基础 > 计算机网络

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

2012-08-07 14:56 1026 查看
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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐