kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

bonding的源代码分析

https://github.com/matrix207/bonding

1. 目的

本文档结合相关内核代码和对Linux 2.6.9内核中Bonding模块的三种主要工作模式的工作
原理和流程。在配置Bond模块时,除了资料[2],本文档也有一定的参考价值。

2. 内容

本文档包含下列内容:

  • Bonding模块工作流程综述。(第3节)
  • Bonding链路状态监控机制(mii模式、arp模式)描述。(第4节)
  • Bonding模块三种主要工作模式:balance-rr、active- backup和broadcast相关代码分析。(第5节)
  • Bonding模块关键数据结构和函数的代码分析。(第5节)

如果想了解bonding模块的原理和工作流程,请阅读3、4节,如果想进一步分析bonding模块的代码,请阅读5节。

3. Bonding模块工作流程综述

Bonding模块本质上是一个虚拟的网卡驱动(network device driver),只不过并没有
真实的物理网卡与之对应,而是由这个虚拟网卡去“管辖”一系列的真实的物理网卡,所以
它的代码结构和一般网卡驱动的代码结构非常类似,这是共性;除此之外,它还有自己的
一些特性功能,例如特别的链路状态监控机制,绑定/解除绑定等。

3.1 物理网卡的活动状态和链路状态:

在bonding模块中为每一个被绑定的物理网卡定义了两种活动状态和四种链路状态:注意,
这里的链路状态和实际网卡真实的链路状态(是否故障、是否有网线连接)没有直接的关系,
虽然bonding模块通过MII或者ARP侦测到实际网卡故障时也会改变自定义的链路状态值
(例如从BOND_LINK_UP切换到BOND_LINK_FAIL随后切换到 BOND_LINK_DOWN状态),但是
概念上应该把这两类链路状态区分开。在本文档随后的内容中,除非特别指出,“链路状态”
都指bonding模块自定义的链路状态。

活动状态:

1
2
BOND_STATE_ACTIVE:处于该状态的网卡是潜在的发送数据包的候选者
BOND_STATE_BACKUP:处于该状态的网卡在选择发送数据的网卡时被排除

链路状态:

1
2
3
4
BOND_LINK_UP:  上线状态(处于该状态的网卡是是潜在的发送数据包的候选者)
BOND_LINK_DOWN:故障状态
BOND_LINK_FAIL:网卡出现故障,向状态BOND_LINK_DOWN 切换中
BOND_LINK_BACK:网卡恢复,向状态BOND_LINK_UP切换中

一个网卡必须活动状态为BOND_STATE_ACTIVE并且链路状态为 BOND_LINK_UP,才有可能作
为发送数据包的候选者,注意,这里所说的数据包并不包含ARP请求,在使用ARP链路状态
监控时,一个处于BOND_LINK_BACK状态的网卡也可能发送ARP请求。

bonding模块的所有工作模式可以分为两类:多主型工作模式和主备型工作模式,balance-rr
和broadcast属于多主型工作模式而active-backup属于主备型工作模式。(balance-xor、
自适应传输负载均衡模式(balance-tlb)和自适应负载均衡模式(balance-alb)也属于
多主型工作模式,IEEE 802.3ad动态链路聚合模式(802.3ad)属于主备型工作模式,在本文档中不加以讨论)

在多主型工作模式中,如果物理网卡不出现故障,所有的物理网卡都处于 BOND_STATE_ACTIVE
和BOND_LINK_UP的状态下,参与数据的收发,此时:如果工作在balance-rr 模式中轮流
向各个网卡发送数据,curr_active_slave字段(见5.1.3)指向前次发送数据(不包含
ARP请求)的物理网卡,该指针每次发送过数据后都会切换到下一个物理网卡;在broadcast
模式中向所有网卡发送数据,curr_active_slave字段除非网卡有故障发生不会切换。

在主备型工作模式中,如果物理网卡不出现故障,只有一块网卡(活动网卡)处于
BOND_STATE_ACTIVE和BOND_LINK_UP的状态下,负责数据的收发,而其他网卡(后备网卡)
处于BOND_STATE_BACKUP 和BOND_LINK_DOWN状态下,此时curr_active_slave字段指向当前的活动网卡。

如果工作在active-backup模式下,可以指定一个物理网卡作为主网卡(primitive interface)
,如果主网卡可用,就把主网卡作为当前活动网卡,否则在其他的可用网卡中选取一块网
卡作为当前活动网卡,而一旦主网卡从故障中恢复,不管当前活动网卡是否故障都切换到
主网卡。在balance-tlb和balance-alb模式中也可以指定主网卡,但是其意义和active-backup模式中并不相同。

3.2 数据收发流程

如果一个物理网卡被虚拟网卡绑定,则代表该网卡的数据结构struct net_device中下列字段将发生变化:

1
2
flags字段(unsigned short)将包含IFF_SLAVE标志。
master字段(struct net_device *)将指向虚拟网卡。

在主备型工作模式下,所有的非活动物理网卡的flags字段还将设置IFF_NOARP标志位表示对ARP请求不做出回应。

而代表虚拟网卡的struct net_device数据结构的flags字段将包含IFF_MASTER标志。

所有被绑定的物理网卡都将被设置相同的MAC地址和MTU值(和虚拟网卡也相同),这些值
将和第一块被绑定的物理网卡保持一致(“第一块网卡”并不是一个强制条件,这是由bonding
模块的启动流程造成的,我们也可以手工设置虚拟网卡的MAC地址和MTU值,这个设定同时
也将用于所有被绑定的物理网卡)。另外,所有被绑定的物理网卡没有IP地址,所以不参
与发送IP数据包的路由选择。

在下面的三节中,只描述数据发送和接收过程中和bonding相关的一些特殊处理,关于Linux
内核的一般数据包收发流程请参考资料[3][4],本文档不再赘述。

3.2.1 接收数据

无论在何种模式下,只要物理网卡的实际链路状态正常,任何被绑定的物理网卡都可以接
收数据(虽然没有IP地址,但是仍然有MAC地址),即使是处于BOND_STATE_BACKUP和BOND_LINK_DOWN
状态时,这是由于BOND_STATE_BACKUP和BOND_LINK_DOWN是bonding模块自己定义的管理物
理网卡所用的状态,和内核的TCP/IP栈没有任何关系,bonding模块最多在主备模式下给
备用物理网卡设置IFF_NOARP标志,使它对ARP数据包不做出回应,仅此而已。

收取数据包时,物理网卡驱动的中断处理函数把数据包放入接收队列中,随后软中断
NET_RX_SOFTIRQ的处理函数net_rx_action被调用,该函数将调用接收数据包的物理网卡网卡的poll函数。

无论一个物理网卡是否支持NAPI,函数netif_receive_skb都将在某个阶段被调用。
(如果物理网卡不支持NAPI,内核使用函数process_backlog代替真实的poll调用,
而process_backlog调用netif_receive_skb)。

在netif_receive_skb函数中将调用函数skb_bond,该函数本质上作如下操作:

1
if(dev->master) skb->dev = dev->master;

即把数据包skb的物理网卡字段替换为虚拟网卡,使得该数据包看起来像是从虚拟网卡接
收的一样,随后的处理和其他数据包没有任何差别,不再赘述。

3.2.2 发送数据

发送数据包时,内核根据路由选择某一个虚拟网卡作为发送接口(注意被绑定的物理网卡
没有IP地址),最后调用该虚拟网卡的数据包传输接口net_device-> hard_start_xmit,
注意此时该数据包中的dev字段指向虚拟网卡。net_device-> hard_start_xmit接口根据
不同的工作模式指向不同的传输函数,但是无论是何种工作模式,最后bond_dev_queue_xmit
函数都将被调用(以一个选定的物理网卡作为参数调用)。

bond_dev_queue_xmit函数将作如下操作:

1
skb->dev = slave_dev;

即替换skb的dev字段为选定的物理网卡。

随后,bond_dev_queue_xmit将调用标准的内核接口dev_queue_xmit把数据包放入选定物理网卡的发送队列中。

3.2.3 ARP请求和回应

既然被绑定物理网卡没有IP地址,那么如果接收到ARP请求之后,根据何IP地址决定是否产生应答?
如果产生应答,在应答中,源IP地址应该是什么?

答案是:被绑定物理网卡接收到ARP请求后,由于函数arp_rcv在netif_receive_skb之后被调用,
而skb->dev经过netif_receive_skb的处理将指向虚拟网卡,所以是否产生应答由该物理网卡所
属的虚拟网卡的IP地址决定(当然前提是物理网卡没有设置IFF_NOARP标志),并且最终的ARP
回应将包含虚拟网卡的IP地址(细节请参考arp_rcv和arp_process函数)。

4. 链路状态监控

链路状态监控有如下两个作用:

  • 根据被绑定物理网卡的实际链路状态(是否故障、网线是否连接)更新bonding模块自定义
    的物理网卡链路状态和活动状态。
  • 在主备模式下,根据自定义的物理网卡链路状态切换活动状态和当前的活动网卡。

Bonging模块支持两种模式的链路状态监控:通过MII ioctl调用直接进行或是通过发送ARP
请求间接进行。前者通过调用物理网卡驱动程序的MII ioctl接口直接获得物理网卡是否
故障的信息,后者通过向预定义的一组IP地址发送ARP请求,并观察被监控物理网卡是否
有收到数据包(可能是ARP回答或者是一般数据包)间接进行。

这两种链路状态监控模式不能并存。参数arp_interval和miimon表示以毫秒为单位的检测
间隔,在加载bonding模块时如果指定了非0的arp_interval参数并且miimon参数等于0,
则使用ARP链路状态监控;如果指定了非0 miimon参数则使用MII链路状态监控
(强制arp_interval = 0从而忽略arp_interval参数)。如果arp_interval和miimon都
等于0则使用参数值为100的MII链路状态监控(强制miimon等于100 ms)。

如果使用ARP链路状态监控,还必须指定 arp_ip_target参数,该参数设定ARP监控时发送ARP
请求的目标IP列表,各个IP之间使用逗号分隔。

如果使用MII链路状态监控,还可以指定参数updelay和downdelay作为从BOND_LINK_DOWN到
BOND_LINK_UP或者从BOND_LINK_UP 到BOND_LINK_DOWN切换的时间间隔,这两个参数默认是0值。
(在ARP链路状态监控中这两个参数没有用)

4.1 MII链路状态监控

MII链路状态监控可以用下列流程图表示

您的浏览器可能不支持显示此图像。

初始时,如果虚拟网卡工作在多主型工作模式下,则所有物理网卡的链路状态为BOND_LINK_UP,
并且活动状态处于BOND_STATE_ACTIVE,IFF_NOARP标志都没有设置;否则所有物理网卡的链路
状态为 BOND_LINK_UP,但是只有当前活动网卡的活动状态处于BOND_STATE_ACTIVE并且没有
设置IFF_NOARP 标志,而其余网卡的活动状态为BOND_STATE_BACKUP并且IFF_NOARP标志被设置。

MII检测机制每miimon毫秒检测一遍所有被绑定物理网卡的状态。

  1. 在某时刻,如果通过MII调用侦测到某一个物理网卡发生故障,则该物理网卡的链路状态立
    即被设置为BOND_LINK_FAILED。
  2. 如果在downdelay毫秒内物理网卡恢复正常,则重新把网卡的链路状态设置为BOND_LINK_UP。
  3. 如果在downdelay毫秒内物理网卡始终没有恢复正常,则该物理网卡的链路状态被设置为
    BOND_LINK_DOWN。如果虚拟网卡工作于主备型工作模式下,则活动状态被设置为BOND_STATE_BACKUP
    同时设置物理网卡的IFF_NOARP标志,并且如果出故障的是当前活动网卡,则通过一个重
    选择过程选择新的活动网卡(一般是第一块可用物理网卡)。
  4. 如果一个链路状态为BOND_LINK_DOWN的物理网卡在MII检测过程中恢复正常,则立即把网卡
    的链路状态设置为BOND_LINK_BACK。
  5. 如果在updelay毫秒内物理网卡又发生故障,就把链路状态重新设置为BOND_LINK_DOWN。
  6. 如果ådelay毫秒内物理网卡始终保持可用状态,就把链路状态重新设置为BOND_LINK_UP。
    如果虚拟网卡工作于主备型工作模式下,则同时设置活动状态为BOND_STATE_ACTIVE并且
    清除物理网卡的IFF_NOARP标志,并且如果是主网卡恢复到BOND_STATE_ACTIVE状态,则会
    把当前活动网卡切换到主网卡。

4.2 ARP链路状态监控

4.2.1 active-backup工作模式下的ARP链路状态监控

该模式下的ARP链路状态监控可以分为两个阶段:

  1. 如果当前活动网卡(curr_active_slave不为NULL)存在,则以间隔 arp_interval毫秒从当
    前活动网卡向arp_targets表示的各个IP地址发送ARP请求,如果当前活动网卡在过去的
    2arp_interval毫秒内没有数据包发送接收并且已经作为活动网卡至少 2arp_interval
    毫秒,则把当前活动网卡的链路状态设置为BOND_LINK_DOWN并且试图在链路状态为BOND_LINK_UP
    或者BOND_LINK_BACK的网卡中选取一个网卡作为当前活动网卡。如果有这样一个网卡存在,
    则原来的活动网卡的活动状态被设置为BOND_STATE_BACKUP,并且IFF_NOARP标志被设置,
    新的活动网卡链路状态被设置为BOND_STATE_UP, 活动状态被设置为BOND_STATE_ACTIVE,
    IFF_NOARP标志被清除。

  2. 如果上述过程没有选出新的活动网卡(正常情况下active-backup 模式下除当前活动网卡
    外所有网卡的链路状态都是BOND_LINK_DOWN,所以可能没有链路状态为BOND_LINK_UP或者
    BOND_LINK_BACK的后备网卡),则开始一个下述的选取活动网卡的过程:

从第一块可用(即IFF_UP标志被设置,netif_running(dev)和netif_carrier_ok(dev)都返回非0值)
物理网卡开始,向arp_targets表示的各个IP地址发送ARP请求,然后观察所有的物理网卡,
如果有物理网卡在arp_interval毫秒内有数据发送接收,就把它设置为当前活动网卡,结束这个选取过程。否则换下一个可用物理网卡,重复这个过程。 注意即使物理网卡被设置IFF_NOARP标志,仍旧可以收到ARP应答数据包。

4.2.2 其他工作模式下的ARP链路状态监控

虚拟网卡每arp_interval遍历一遍所有被绑定物理网卡,如果在网卡在过去的2 * arp_interval毫秒内没有任何数据的发送或者接收,就把网卡的链路状态设置为 BOND_LINK_DOWN,活动状态设置为BOND_STATE_BACKUP,如果在过去的arp_interval毫秒有数据包发送接收,则把网卡的链路状态设置为BOND_LINK_UP,活动状态设置为 BOND_STATE_ACTIVE。

在遍历过程中,对每一个可用的物理网卡(IFF_UP标志被设置,netif_running(dev)和netif_carrier_ok(dev)都返回非0值),都试图从该网卡向arp_targets表示的各个IP地址发送ARP请求,保证其他的被绑定的物理网卡可以收到ARP应答包。

5. 代码分析

5.1 关键数据结构

  • 1.struct bond_params

文件:driver/net/bonding/bonding.h

该结构是全局结构(每系统一个),对应于加载bonding模块时传入的各个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
主要成员:
名称  类型  含义
mode  int  Bonding模块工作模式
	BOND_MODE_ROUNDROBIN     balance-rr模式
	BOND_MODE_ACTIVEBACKUP   active-backup模式
	BOND_MODE_XOR            balance-xor模式
	BOND_MODE_BROADCAST      broadcast模式
	BOND_MODE_8023AD         IEEE 802.3ad动态链路聚合模式
	BOND_MODE_TLB            自适应传输负载均衡模式
	BOND_MODE_ALB            自适应负载均衡模式
miimon  int  使用MII链路状态监控时的时间间隔(ms)
arp_interval  int  使用arp链路状态监控时的时间间隔(ms)
use_carrier  int  使用MII链路状态监控时是否使用更新的carrier调用
updelay  int  使用MII链路状态监控时从BACK状态切换到UP状态的时延(ms)
downdelay  int  使用MII链路状态监控时从FAIL状态切换到DOWN状态的时延(ms)
primary  char[]  用来在active-backup、balance-tlb和balance-alb模式中指定主网卡
arp_targets  u32[]  在ARP链路状态监控中将向这些IP地址发送ARP请求。
  • 2.struct slave

文件:driver/net/bonding/Bonding.h

每一个被管辖的物理网卡对应一个该数据结构的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
主要成员:
名称  类型  含义
dev  struct net_device*  指向被绑定的物理网卡
next,prev  struct slave *  所有的slave数据结构通过这两个指针双向链接到一起形成*循环*链表
delay  s16  用于保存MII链路状态监控和ARP链路状态监控的时延值。
jiffies  u32  用于active-backup模式下的ARP状态监控
link  s8  表示对应网卡的链路状态,取下列四个值之一:
	BOND_LINK_UP     上线状态
	BOND_LINK_DOWN   故障状态
	BOND_LINK_FAIL   网卡出现故障,状态BOND_LINK_DOWN切换中
	BOND_LINK_BACK   网卡恢复,状态BOND_LINK_UP切换中
state  s8  表示对应网卡活动状态,取下列两个值之一:
	BOND_STATE_ACTIVE            活动状态
	BOND_STATE_BACKUP            后备状态
original_flags  u32  保存被绑定物理网卡原来的flags
perm_hwaddr  u8[]  保存被绑定物理网卡原来的MAC地址
ad_info  struct ad_slave_info  记录IEEE 802.3ad动态链路聚合模式下的“每网卡”相关状态信息
tlb_info  struct tlb_slave_info  记录自适应传输负载均衡模式下的“每网卡”相关状态信息
link_failure_count  u32  网卡从BOND_LINK_FAIL切换到BOND_LINK_DOWN的次数
speed  u16  记录网卡的传输速度(10M/100M/1000G)
duplex  u8  网卡工作模式(全双工?)
  • 3.struct bonding

文件:driver/net/bonding/Bonding.h

每一个虚拟网卡对应一个该数据结构的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
主要成员:
名称  类型  含义
dev  struct net_device*  指向虚拟网卡(例如bond0、bond1等等)
first_slave  struct   slave *  指向被绑定的第一个物理网卡对应的slave结构。
curr_active_slave  struct   slave *  指向当前活动的网卡对应的slave结构,在不同的工作模式下有不同的含义。
current_arp_slave  struct   slave *  用于ARP状态监控(只用于bond_activebackup_arp_mon)
primary_slave  struct   slave *  如果使用BOND_MODE_ACTIVEBACKUP、BOND_MODE_TLB或者BOND_MODE_ALB模式,则指向主物理网卡对应的slave结构(primary_slave)
slave_cnt  s32  虚拟网卡所管辖的物理网络接口的个数
lock  rwlock_t  每一个虚拟网卡管辖一系列物理网卡,每一个物理网卡对应一个slave结构,所有的slave被放在一个链表中,这个读写锁用来保护该链表。
curr_slave_lock  rwlock_t  用来保护curr_active_slave的读写锁。
mii_timer  struct   timer_list  用于MII链路状态监控的定时器
arp_timer  struct   timer_list  用于ARP链路状态监控的定时器
kill_timers  s8  如果该标志置位,所有的计时器超时后就不再重新设置,从而可以被安全删除
bond_list  struct   list_head  通过该结构,所有的bonding数据结构被连接为双向链表,链表头是全局变量bond_dev_list。

5.2 关键函数

本节描述了bonding模块关键函数的操作流程,这些函数是基本的原子模块,其他没有被列举的函数仅仅是对这些函数的简单包装。

5.2.1 模块初始化/释放
  • 1.初始化
bonding_init

原型:

1
static int __init bonding_init(void)

bonding_init作为bonging模块的初始化函数,在模块被加载时被调用。它主要做如下工作:

  1. 调用函数bond_check_params解析传入模块的参数并检查其合法性,结果放入数据结构params中。其中params是一个类型为bond_params的全局变量。
  2. 如果内核支持proc文件系统,调用bond_create_proc_dir在/proc/net下创建目录/proc/net/bonding。
  3. 如果传入参数指定了bond设备的个数(通过参数max_bonds),则通过下列步骤创建max_bonds个bond设备(从bond0到bondN)
    1. 调用alloc_netdev和dev_alloc_name创建网络设备,指定每一个设嘯一个bonding结构(dev->priv)。
    2. 为每一个新创建的虚拟网络设备调用bond_init函数。
    3. 调用register_netdevice注册这个新创建的网络设备。
  4. 调用register_netdevice_notifier,注册函数bond_netdev_notifier为网络事件处理函数。
bond_init

原型:

1
static int __init bond_init(struct net_device *bond_dev, struct bond_params *params)

该函数对每一个新创建的虚拟网络设备调用一次。它主要做下列工作。

  1. 取出虚拟网络设备bond_dev的私有数据块,用bond指向它(struct bonding *bond)。
  2. 初始化两个读写锁bond->lock和bond->curr_slave_lock。
  3. 把bond->first_slave、bond->curr_active_slave、bond->current_arp_slave、bond->primary_slave全部置为NULL。bond->dev指向属主网络设备bond_dev。
  4. 设置bond_dev的一系列通用接口函数,例如open、close、get_stats、do_ioctl、set_multicast_list、change_mtu和set_mac_address。
  5. 根据不同的工作模式,设置bond_dev的通用接口函数hard_start_xmit 指向不同的目的函数。例如如果工作模式是BOND_MODE_ROUNDROBIN,则 hard_start_xmit指向函数bond_xmit_roundrobin。
  6. 设置bond_dev->tx_queue_len为0,表示发送队列大小没有限制。
  7. 设置bond_dev->flags为IFF_MASTER|IFF_MULTICAST表示该设备支持Muticase并且是一个流量均衡组中的master(被它管辖的其他物理网卡将被设置IFF_SLAVE标志)。
  8. 其他和VLAN相关的标志设置和初始化。
  9. 如果内核支持proc文件系统,调用bond_create_proc_entry在目录/proc/net/bonding下创建对应的proc文件。
  10. 调用list_add_tail把该bonding数据结构添加到bond_dev_list中。
2. 释放 bonding_exit

原型:

1
static void __exit bonding_exit(void)

该函数在模块被卸载的时候被调用,它主要做如下工作:

  1. 调用unregister_netdevice_notifier注销网络事件处理函数。
  2. 调用bond_free_all注销所有形如bondN的虚拟网络接口。bond_free_all遍历bond_dev_list链表,并且对其中的每一个类型为 struct bonding*的数据结构bond做如下操作:
  3. 调用unregister_netdevice注销bond->dev
  4. 调用bond_deinit(bond->dev)
  5. 调用bond_destroy_proc_dir删除/proc/net/bonding目录
bond_deinit

原型:

1
static inline void bond_deinit(struct net_device *bond_dev)

该函数对每一个虚拟网卡的实例调用一次,它主要做如下操作:

  1. 调用list_del把虚拟网卡对应的bonding数据结构从bond_dev_list链表中摘除。
  2. 调用bond_remove_proc_entry删除/proc/net/bonding目录中的对应文件。
5.2.2 物理网卡的绑定/解除绑定

在下面的讨论中,假定ifenslave使用新的ABI接口,即:

  • 在绑定物理网卡时,如果虚拟的网卡还没有MAC地址,则ifenslave通过IOCTL把该虚拟网卡的MAC地址设置为该物理网卡的MAC地址(保证bond_enslave被调用时虚拟网卡已经有了MAC地址)。
  • 如果被绑定网卡处于UP状态,则ifenslave首先把它设置为DOWN状态(保证bond_enslave被调用时被绑定物理网卡处于DOWN状态)。

如果使用旧版本的ABI接口,则虚拟的网卡的MAC地址由bonding模块在bond_enslave函数中自行设置,并且被绑定网卡在bond_enslave被调用时可能处于UP状态,需要由bond_enslave函数自行处理。

1. 绑定 bond_enslave

原型:

1
static int bond_enslave(struct net_device *bond_dev, struct net_device *slave_dev)

该函数在试图把一个物理网卡绑定到一个虚拟的网卡时被调用,其中bond_dev表示虚拟的网卡,slave_dev表示真实的物理网卡。该函数主要做如下操作:

  1. 取出bond_dev的私有数据,用bond指向它(struct bonding *)。
  2. 一系列的合法性检查,包括:
    1. bond_dev的flags是否已经设置了IFF_UP(虚拟的网卡必须已经处于UP状态)
    2. slave_dev的flags是否没有设置IFF_SLAVE(防止同一个物理网卡被绑定两次)
    3. bond_dev的flags如果设置了NETIF_F_VLAN_CHALLENGED,则bond->vlan_list不能为空。
    4. slave_dev->flags是否没有设置IFF_UP(物理网卡应该处于DOWN状态)
    5. slave_dev->set_mac_address不能为NULL(物理网卡应该支持设置MAC地址)
  3. 调用kmalloc分配一个新的slave结构。
  4. 把slave_dev->flags保存在slave->original_flags中。
  5. 把slave_dev原有的MAC地址保存在slave-> perm_hwaddr中。
  6. 设置slave_dev新的MAC地址为虚拟网卡的MAC地址。
  7. 调用netdev_set_master设置slave_dev,该函数主要作如下操作:
    1. 设置slave_dev->flags的IFF_SLAVE标志。
    2. 设置slave_dev->master指向虚拟网卡bond_dev。
  8. 设置slave->dev指向slave_dev。
  9. 如果bond_dev工作在模式BOND_MODE_TLB或者BOND_MODE_ALB,对slave调用bond_alb_init_slave函数。
  10. 维护和Multicast以及VLAN相关的一系列数据结构。
  11. 调用bond_attach_slave把slave加入bond的链表(通过维护bond-> first_slave和slave结构的next,prev指针)
  12. 把slave的delay和link_failure_count都清零。
  13. 监测slave_dev的链路状态:
    1.如果使用MII链路,并且bond_check_dev_link返回BMSR_LSTATUS(表示链路正常),或者不使用MII链路监控,则根据updelay是否为0把slave->link设置为BOND_LINK_BACK或者BOND_LINK_UP。
    2.如果使用MII链路,并且bond_check_dev_link返回非BMSR_LSTATUS值,则设置slave->link为BOND_LINK_DOWN。
  14. 调用bond_update_speed_duplex更新slave_dev的链路速率,如果失败则设置slave_dev的链路速率为100M全双工。
  15. 如果虚拟网卡工作在BOND_MODE_ACTIVEBACKUP、BOND_MODE_TLB或者BOND_MODE_ALB模式下,并且slave_dev是用户指定的主网卡,则设置bond->primary_slave为slave_dev。
  16. 设置bond->curr_active_slave和slave的活动状态,维护VLAN和Multicast相关数据结构:
    1.如果虚拟网卡工作在BOND_MODE_ACTIVEBACKUP:如果bond->curr_active_slave没有被设置或者bond->curr_active_slave不响应ARP(设置了IFF_NOARP标志),并且slave_dev不处于BOND_LINK_DOWN状态,则设置slave_dev为活动网卡(设置BOND_STATE_ACTIVE标志,清除IFF_NOARP标志),否则设置slave_dev为后备网卡(设置BOND_STATE_BACKUP标志,设置IFF_NOARP标志)。
    2.如果虚拟网卡工作在BOND_MODE_ROUNDROBIN或者 BOND_MODE_BROADCAST:直接设置slave_dev的活动状态为BOND_STATE_ACTIVE(在BOND_MODE_ROUNDROBIN和BOND_MODE_BROADCAST 模式下,IFF_NOARP标志始终被清除),如果没有设置bond->curr_active_slave,则设置bond->curr_active_slave指向slave。
    3.其他工作模式:(还没有加以分析)
2. 解除绑定 bond_release

原型

1
static int bond_release(struct net_device *bond_dev, struct net_device *slave_dev)

该函数在试图解除一个物理网卡的绑定状态时被调用,其中bond_dev表示虚拟的网卡,slave_dev表示真实的物理网卡。该函数主要做如下操作:

  1. 取出bond_dev的私有数据,用bond指向它(struct bonding *)。
  2. 寻找对应的slave结构。
  3. 一系列的合法性检查,包括:
    1. slave_dev->flags是否设置了IFF_SLAVE。
    2. slave结构是否存在。
    3. slave_dev原来的MAC地址是否和bond_dev相同,如果相同给出警告(防止MAC冲突)
  4. 如果虚拟网卡工作在BOND_MODE_8023AD模式,调用bond_3ad_unbind_slave
  5. 调用bond_detach_slave把slave从bond的链表中摘除(通过维护bond-> first_slave和slave结构的next,prev指针)。
  6. 如果slave_dev是虚拟网卡以前的主物理网卡,则设置bond->primary_slave为NULL。
  7. 如果slave_dev是虚拟网卡以前的活动网卡,则设置bond->active_slave为NULL(通过调用bond_change_active_slave函数)。
  8. 如果虚拟网卡工作在模式BOND_MODE_TLB或者BOND_MODE_ALB则调用bond_alb_deinit_slave。
  9. 如果slave_dev是虚拟网卡以前的活动网卡,则调用bond_select_active_slave寻找一个新的活动网卡。
  10. 如果虚拟网卡再也没有管辖的物理网卡,清除虚拟网卡的MAC地址(如果新调用ifenslave绑定物理网卡,则重新设置这个MAC地址)。
  11. 维护VLAN和Multicast相关的数据结构。
  12. 调用netdev_set_master解除master和slave的绑定关系并且调用dev_close关闭slave_dev。
  13. 恢复slave_dev的MAC地址(根据slave->perm_hwaddr)和flags(根据slave->original_flags)。
  14. 调用kfree释放slave结构。
5.2.3 网卡驱动通用接口(interface service routines)

既然bonding模块本质上是一个虚拟网卡的驱动模块,所以必须提供一组所有网卡驱动模块都遵守的通用接口函数。

1. open/close

bond_open(net_device->open接口)

原型:

1
static int bond_open(struct net_device *bond_dev)

该函数在对应的虚拟网卡被打开时调用(即使用ifup/ifconfig工具启动网卡的时候),主要做如下操作(只分析三种主要模式):

  1. 设置bond->kill_timers为0。
  2. 如果使用MII链路状态监控:
    1. 初始化mii_timer。
    2. 设置超时时间mii_timer->expires为当前jiffies+1(立即调用bond_mii_monitor函数)
    3. 设置bond_mii_monitor为定时器的超时处理函数。
  3. 如果使用ARP链路状态监控:
    1. 初始化arp_timer。
    2. 设置超时时间arp_timer->expires为当前jiffies+1(立即调用定时器的超时处理函数)
    3. 如果工作在BOND_MODE_ACTIVEBACKUP,设置bond_activebackup_arp_mon为超时处理函数。
    4. 如果工作在其他模式,设置bond_loadbalance_arp_mon为超时处理函数。
bond_close(net_device->stop接口)

原型:

1
static int bond_close(struct net_device *bond_dev)

该函数在对应的虚拟网卡被关闭时调用(即使用ifdown/ifconfig工具关闭网卡的时候),主要做如下操作(只分析三种主要模式):

  1. 调用bond_mc_list_destroy维护Multicast相关数据结构。
  2. 设置bond->kill_timers为1,所有的计时器超时后就不再重新设置,从而可以被安全删除。
  3. 删除所有的定时器,包括mii_timer和arp_timer。
  4. 调用bond_release_all释放所有被绑定的物理网卡,本质上该函数只是遍历slave链表并且对每一个元素调用bond_release。
  5. 如果虚拟网卡工作在BOND_MODE_TLB或者BOND_MODE_ALB模式下,调用bond_alb_deinitialize。
2. ioctl接口

bond_do_ioctl(net_device->do_ioctl 接口)

原型:

1
static int bond_do_ioctl(struct net_device *bond_dev, struct ifreq *ifr, int cmd)

该函数是虚拟网卡的IOCTRL接口,仅仅根据不同的IOCTRL命令调用其他函数执行相应的功能, 所以不再列出操作流程而仅仅列举出这些被调用的函数和相应的功能:

1.链路状态设置和查询(bond_ethtool_ioctl或者if_mii) 2.Bonding模块状态查询(bond_info_query) 3.被绑定的物理网卡状态查询(bond_slave_info_query) 4.物理网卡的绑定和解除绑定(bond_enslave/bond_release) 5.虚拟网卡的MAC地址设置(bond_sethwaddr) 6.切换当前活动的物理网卡(bond_ioctl_change_active)

3. 统计值查询

bond_get_stats(net_device-> get_stats 接口)

原型:

1
static struct net_device_stats *bond_get_stats(struct net_device *bond_dev)

该函数枚举所有被管辖的物理网卡,并且对每一个物理网卡调用get_stats接口,然后把对应的统计值加起来并作为最终的返回值,这些统计值包括。 名称 含义 rx_packets 接收包总数 rx_bytes 接收字节总数 rx_errors 接收过程中错误数据包数 rx_dropped 接受过程中丢弃包数 tx_packets 发送包总数 tx_bytes 发送字节总数 tx_errors 发送过程中错误数据包数 tx_dropped 发送过程中丢弃包数 multicast Multicast数据包总数 collisions MAC地址冲突次数 rx_length_errors 接收数据包长度错误总数 rx_over_errors ring buff溢出次数 rx_crc_errors 接收数据包CRC校验错误总数 rx_frame_errors 接收数据包frame对齐错误总数 rx_fifo_errors 接收队列溢出次数 rx_missed_errors 接收时丢失的包数(仅仅对某些媒体有效) tx_aborted_errors 发送取消次数(例如发送超时) tx_carrier_errors 链路错误总数 tx_fifo_errors 发送队列溢出次数 tx_heartbeat_errors 心跳信号丢失(仅仅对某些媒体有效) tx_window_errors 接收窗口错误(不明,需要进一步确认)

bond_set_multicast_list(net_device-> set_multicast_list 接口)

原型:

1
static void bond_set_multicast_list(struct net_device *bond_dev)

该函数设置和Multicast和混杂模式相关的一组数据结构,由于三种主要工作模式并不过多地涉及这个函数,所以本文档不给出详细的说明。

bond_change_mtu(net_device-> change_mtu 接口)

原型:

1
static int bond_change_mtu(struct net_device *bond_dev, int new_mtu)

该函数把被虚拟网卡的MTU和被它管辖的所有物理网卡的MTU设置为同一值,主要做如下操作:

  1. 枚举所有被管辖的物理网卡,对每一个物理网卡调用change_mtu设置新的MTU值,如果物理网卡没有change_mtu接口函数,则直接设置slave->dev->mtu等于new_mtu。
  2. 设置bond_dev->mtu的值等于new_mtu。
bond_set_mac_address(net_device-> set_mac_address 接口)

原型:

1
static int bond_set_mac_address(struct net_device *bond_dev, void *addr)

该函数设置虚拟网卡的MAC地址和被管辖的物理网卡的MAC地址为同一值,主要做如下操作:

  1. 枚举所有被管辖的物理网卡,对每一个物理网卡调用set_mac_address设置新的MAC地址,如果物理网卡没有set_mac_address函数,则错误返回。
  2. 设置bond_dev->dev_addr的值等于新的MAC地址。
4. 数据包传输(接收/发送)

Bonding模块仅仅负责把发送数据包按照特定的工作模式转给被管辖的物理网卡发送,而每一个物理网卡负责自己的数据包接收,即虚拟网卡不管理各个物理网卡的数据接收过程,它能做的仅仅是设置它们的IFF_NOARP标志,使某一个物理网卡对ARP请求不做出回应。

在模块初始化时, bond_init函数根据工作模式把net_device-> hard_start_xmit接口设置为不同的函数,对于 BOND_MODE_ROUNDROBIN、BOND_MODE_ACTIVEBACKUP和BOND_MODE_BROADCAST 模式,该接口分别被设置为下列三个函数之一

bond_xmit_roundrobin(net_device-> hard_start_xmit 接口)

原型:

1
static int bond_xmit_roundrobin(struct sk_buff *skb, struct net_device *bond_dev)

该函数用来在BOND_MODE_ROUNDROBIN模式中发送数据包,主要做如下操作:

  1. 合法性检查,包括:

    1. 检查bond_dev ->flags中IFF_UP标志是否设置
    2. netif_running(bond_dev)是否返回非0值
    3. 对应的虚拟网卡是否至少有一个管辖的物理网卡
  2. 从bond->curr_active_slave开始遍历slave链表,找到第一个链路状态为BOND_LINK_UP,活动状态为BOND_STATE_ACTIVE的物理网卡并且调用bond_dev_queue_xmit向这个物理网卡发送数据,然后设置bond->curr_active_slave为slave链表中的下一个物理网卡。

  3. 如果没有找到这样的网卡或者bond_dev_queue_xmit返回非0值,则调用dev_kfree_skb丢弃数据包。

bond_xmit_activebackup(net_device-> hard_start_xmit 接口)

原型:

1
static int bond_xmit_activebackup(struct sk_buff *skb, struct net_device *bond_dev)

该函数用来在BOND_MODE_ACTIVEBACKUP模式中发送数据包,主要做如下操作:

  1. 如果是试图发送ARP请求,则把全局变量my_ip设置为ARP请求中的发送方IP地址(skb->data+以太网头长度+ARP头长度+6),这个全局变量在ARP链路状态监控中被使用。
  2. 合法性检查,包括:

    1. 检查bond_dev ->flags中IFF_UP标志是否设置
    2. netif_running(bond_dev)是否返回非0值
    3. 对应的虚拟网卡是否至少有一个管辖的物理网卡
  3. 如果bond->curr_active_slave不为空,则调用bond_dev_queue_xmit向这个物理网卡发送数据。

  4. 否则,调用dev_kfree_skb丢弃数据包。

bond_xmit_broadcast(net_device-> hard_start_xmit接口)

原型:

1
static int bond_xmit_broadcast(struct sk_buff *skb, struct net_device *bond_dev)

该函数用来在BOND_MODE_BROADCAST模式中发送数据包,主要做如下操作:

  1. 合法性检查,包括:

    1. 检查bond_dev ->flags中IFF_UP标志是否设置
    2. netif_running(bond_dev)是否返回非0值
    3. 对应的虚拟网卡是否至少有一个管辖的物理网卡
  2. 从bond->curr_active_slave开始遍历slave链表,找到所有状态为BOND_LINK_UP,活动状态为BOND_STATE_ACTIVE的物理网卡,包括bond->curr_active_slave,调用bond_dev_queue_xmit向这些物理网卡发送数据(其中需要通过skb_clone复制skb结构)。

  3. 如果发送失败,调用dev_kfree_skb丢弃数据包

# bond_dev_queue_xmit

原型:

1
int bond_dev_queue_xmit(struct bonding *bond, struct sk_buff *skb, struct net_device *slave_dev)

该函数被bond_xmit_roundrobin,bond_xmit_activebackup 和bond_xmit_broadcast 调用,向实际的物理网卡发送数据包,主要做如下操作:

  1. 设置skb->dev为slave_dev(在此之前skb->dev指向虚拟网卡,现在指向真实的物理网卡)
  2. 维护和VLAN相关的数据结构。
  3. 调用dev_queue_xmit发送数据包。
dev_queue_xmit

原型:

1
int dev_queue_xmit(struct sk_buff *skb)

该函数不是bonding模块的一部分而是内核的一个标准接口,为了清楚起见也把它列出来,请参考net/core/dev.c文件。

  1. 如果底层的物理网卡不支持Scatter/Gather IO,而skb包含了分片(注意不是IP分片,而是和DMA相关的一个概念,见skbbuff.h),则调用__skb_linearize合并分片。
  2. 如果底层的设备不支持计算校验和,则计算一系列校验和。
  3. 如果底层的设备有发送队列(qdisc),则把数据包放入发送队列中,退出。
  4. 如果底层的设备没有发送队列(例如loopback或者其他没有真实物理网卡对应的设备,bonding模块自然也算一个),则直接调用底层设备的hard_start_xmit发送数据包。
  5. 如果发送失败,调用dev_kfree_skb丢弃数据包
5.2.4. 链路状态监控
1. MII链路状态监控

bond_mii_monitor

原型:

1
static void bond_mii_monitor(struct net_device *bond_dev)

如果使用MII链路状态监控,则该函数被周期调用以检测每一个被绑定物理网卡的链路状态, 主要做如下操作:

  1. 计算局部变量delta_in_ticks = (bond->params.miimon * HZ) / 1000,即miimon参数的jiffies表示。
  2. 如果kill_timers被设置,直接退出。
  3. 如果没有任何物理网卡被绑定,重新设置定时器,退出。
  4. 根据bond_check_dev_link的结果,按照第5节描述的MII链路状态监控模型设置网卡的链路状态。
  5. 如果原来物理网卡的链路状态为BOND_LINK_FAIL,而 bond_check_dev_link返回非BMSR_LSTATUS值,则除了把链路状态设置为BOND_LINK_DOWN之外,还做如下操作:

    1. 如果虚拟网卡工作在模式BOND_MODE_8023AD,调用bond_3ad_handle_link_change
    2. 如果虚拟网卡工作在模式BOND_MODE_TLB或者BOND_MODE_ALB模式下,调用bond_alb_handle_link_change。
    3. 如果当前被检查的slave不是curr_active_slave,设置标志do_failover表明可能会发生slave切换。
  6. 如果原来物理网卡的链路状态为BOND_LINK_BACK而bond_check_dev_link 返回BMSR_LSTATUS,则除了把链路状态设置为BOND_LINK_UP之外,还做如下操作:

    1. 如果虚拟网卡工作在模式BOND_MODE_8023AD或者被监测网卡不是primary_slave,则设置物理网卡的活动状态为BOND_STATE_BACKUP
    2. 如果虚拟网卡不是工作在模式BOND_MODE_ACTIVEBACKUP,则设置物理网卡的活动状态为BOND_STATE_ACTIVE
    3. 如果虚拟网卡工作在模式BOND_MODE_8023AD,调用bond_3ad_handle_link_change
    4. 如果虚拟网卡工作在模式BOND_MODE_TLB或者BOND_MODE_ALB模式下,调用bond_alb_handle_link_change。
    5. 如果当前被检查的slave不是curr_active_slave,设置标志do_failover表明可能会发生slave切换。
  7. 调用bond_update_speed_duplex更新物理网卡的速率。

  8. 如果do_failover被设置,调用bond_select_active_slave。

  9. 设置定时器的超时值为jiffies+delta_in_ticks。
bond_check_dev_link

原型:

1
static int bond_check_dev_link(struct bonding *bond, struct net_device *slave_dev, int reporting)

该函数调用MII/ETHTOOL IOCTL或者使用netif_carrier_ok()检查链路是否正常工作(如果用
户指定了use_carrier)参数,如果该函数返回BMSR_LSTATUS表明链路是正常的,否则表示链
路故障(例如掉网线等等)。

2. ARP链路状态监控

bond_loadbalance_arp_mon

原型:

1
static void bond_loadbalance_arp_mon(struct net_device *bond_dev)

如果虚拟网卡工作在BOND_MODE_ACTIVEBACKUP 模式下,而用户指定了使用ARP状态监控,
则周期性地对每一个被绑定物理网卡调用该函数,注意该函数不使用 downdelay和updelay参数。

由于BOND_MODE_ACTIVEBACKUP模式下所有的被绑定网卡都是处于活动状态(BOND_STATE_ACTIVE),
所以该函数的功能是轮流从每一个被绑定物理网卡发送ARP请求,并且在一段时间间隔内是否
有数据包接收,如果没有就设置被检查物理网卡的链路状态为BOND_LINK_DOWN,活动状态设置
为BOND_STATE_BACKUP 表示不参与发送数据(但是只要IFF_UP被设置、netif_running和
netif_carrier_ok都返回非0(真)值,即本地网卡检查通过,仍然周期性地发送ARP请求出去),
请参考5.2节中的描述。

该函数主要做如下操作:

  1. 计算局部变量delta_in_ticks = (bond->params.arp_interval * HZ) / 1000,即arp_interval参数的jiffies表示。
  2. 如果kill_timers被设置,直接退出。
  3. 如果没有任何物理网卡被绑定,重新设置定时器,退出。
  4. 枚举所有被绑定的物理网卡,做如下操作:
    1. 假如物理网卡的链路状态不是BOND_LINK_UP并且在delta_in_ticks时间间隔内发送过并且接受过数据包,则把链路状态设置为BOND_LINK_UP,活动状态设置为BOND_STATE_ACTIVE,并且如果curr_active_slave为空则设置do_failover局部变量。
    2. 假如物理网卡的链路状态是BOND_LINK_UP并且在2*delta_in_ticks时间间隔内没有发送过或者没有接受过数据包,则把链路状态设置为BOND_LINK_DOWN,活动状态设置为BOND_STATE_BACKUP,如果当前slave是curr_active_slave则设置do_failover局部变量。
    3. 如果dev->flags中IFF_UP被设置,netif_running和netif_carrier_ok都返回非0(真)值,则尝试调用bond_arp_send_all从该网卡发送ARP请求(参考bond_arp_send_all的描述)。
  5. 如果do_failover被设置,调用bond_select_active_slave。
  6. 设置定时器的超时值为jiffies+delta_in_ticks。
bond_activebackup_arp_mon

原型:

1
static void bond_activebackup_arp_mon(struct net_device *bond_dev)

如果虚拟网卡工作在BOND_MODE_ACTIVEBACKUP模式下,而用户指定了使用ARP状态监控,则周期性地对每一个被绑定物理网卡调用该函数。

该函数主要做如下操作:

  1. 计算局部变量delta_in_ticks = (bond->params.arp_interval * HZ) / 1000,即arp_interval参数的jiffies表示。
  2. 如果kill_timers被设置,直接退出。
  3. 如果没有任何物理网卡被绑定,重新设置定时器,退出。
  4. 枚举所有被绑定的物理网卡,做如下操作:
    1. 如果物理网卡在时间间隔delta_in_ticks内接收过数据包,就把网卡的链路状态设置为BOND_LINK_UP(网卡的活动状态保持不变),设置curr_active_slave为NULL。
    2. 如果物理网卡在时间间隔3*delta_in_ticks内没有接收过数据包并且该网卡不是curr_active_slave,就把网卡的链路状态设置为BOND_LINK_DOWN并且调用bond_set_slave_inactive_flags设置网卡的活动状态为BOND_STATE_BACKUP,并且设置IFF_NOARP标志位,设置curr_active_slave为NULL。
  5. 检查curr_active_slave,如果curr_active_slave不为NULL:
    1. 如果curr_active_slave在2*delta_in_ticks内没有发送也没有接收过数据包,就把curr_active_slave的链路状态设置为BOND_LINK_DOWN并且调用bond_select_active_slave寻找一个新的网卡作为新的curr_active_slave,设置current_arp_slave为curr_active_slave。
    2. 如果使用bond->primary_slave并且bond->primary_slave的链路状态是BOND_LINK_UP且bond->primary_slave不是curr_active_slave,就把bond->primary_slave作为新的curr_active_slave。
    3. 否则设置current_arp_slave为NULL;
    4. 调用bond_arp_send_all通过curr_active_slave发送ARP请求。
  6. 检查curr_active_slave,如果curr_active_slave为NULL,则从current_arp_slave 开始或者从first_slave开始选出一个网卡并且把链路状态设置为BOND_LINK_BACK的作为 curr_active_slave的候选者(包存在current_arp_slave中),在下一次lbond_activebackup_arp_mon 被调用的时候将把这个网卡设置为curr_active_slave。
  7. 设置定时器的超时值为jiffies+delta_in_ticks。
3. slave切换

bond_find_best_slave

原型:

1
static struct slave *bond_find_best_slave(struct bonding *bond)

该函数从被绑定网卡中选出最佳者作为curr_active_slave的候选,主要做如下操作:

  1. 如果没有物理网卡被绑定,返回NULL。
  2. 如果没有设置primary_slave或者primary_slave不可用,从first_slave开始,否则从primary_slave开始遍历被绑定物理网卡列表,如果有网卡的链路状态为BOND_LINK_UP,则返回这个物理网卡。如果没有链路状态为BOND_LINK_UP的网卡,返回处于BOND_LINK_BACK状态最久者(delay值最小)。
bond_change_active_slave

原型:

1
static void bond_change_active_slave(struct bonding *bond, struct slave *new_active)

该函数切换new_active为新的curr_active_slave,主要做如下操作:

  1. 如果curr_active_slave和new_active相同,不做任何操作。
  2. 如果new_active的链路状态是BOND_LINK_BACK,把链路状态设为BOND_LINK_UP。
  3. 如果当前工作在BOND_MODE_ACTIVEBACKUP状态,把curr_active_slave的活动状态设置为BOND_STATE_BACKUP,并且设置IFF_NOARP标志位;把new_active的活动状态设置为BOND_STATE_ACTIVE,清除IFF_NOARP标志位。
  4. 设置curr_active_slave为new_active。

6. 参考

  • [1]《Linux 多网卡绑定/负载均衡调研报告》
  • [2]《Linux Ethernet Bonding Driver mini-howto》/src/net/Documentation/networking/bonding.txt
  • [3]《The Linux® Networking Architecture: Design and Implementation of Network Protocols in the Linux Kernel》Klaus Wehrle
  • [4]《Understanding Linux Network_Internals》Christian Benvenuti

七种网卡绑定模式详解

按ip+port哈希可能会比较好

1
mode=2 miimon=100 xmit_hash_policy=1

http://blog.csdn.net/wuweilong/article/details/39720571

概览:

目前网卡绑定mode共有七种(0~6)mode=0、mode=1、mode=2、mode=3、mode=4、mode=5、mode=6

说明:

需要说明的是如果想做成mode 0的负载均衡,仅仅设置这里optionsbond0 miimon=100 mode=0是不够的,与网卡相连的交换机必须做特殊配置(这两个端口应该采取聚合方式),因为做bonding的这两块网卡是使用同一个MAC地址.从原理分析一下(bond运行在mode0下):

mode 0下bond所绑定的网卡的IP都被修改成相同的mac地址,如果这些网卡都被接在同一个交换机,那么交换机的arp表里这个mac地址对应的端口就有多 个,那么交换机接受到发往这个mac地址的包应该往哪个端口转发呢?正常情况下mac地址是全球唯一的,一个mac地址对应多个端口肯定使交换机迷惑了。所以 mode0下的bond如果连接到交换机,交换机这几个端口应该采取聚合方式(cisco称为 ethernetchannel,foundry称为portgroup),因为交换机做了聚合后,聚合下的几个端口也被捆绑成一个mac地址.我们的解 决办法是,两个网卡接入不同的交换机即可。

mode6模式下无需配置交换机,因为做bonding的这两块网卡是使用不同的MAC地址。

七种bond模式说明:

第一种模式:mode=0 ,即:(balance-rr)Round-robin policy(平衡抡循环策略)

特点:传输数据包顺序是依次传输(即:第1个包走eth0,下一个包就走eth1….一直循环下去,直到最后一个传输完毕),此模式提供负载平衡和容错能力;但是我们知道如果一个连接或者会话的数据包从不同的接口发出的话,中途再经过不同的链路,在客户端很有可能会出现数据包无序到达的问题,而无序到达的数据包需要重新要求被发送,这样网络的吞吐量就会下降

第二种模式:mode=1,即: (active-backup)Active-backup policy(主-备份策略)

特点:只有一个设备处于活动状态,当一个宕掉另一个马上由备份转换为主设备。mac地址是外部可见得,从外面看来,bond的MAC地址是唯一的,以避免switch(交换机)发生混乱。此模式只提供了容错能力;由此可见此算法的优点是可以提供高网络连接的可用性,但是它的资源利用率较低,只有一个接口处于工作状态,在有 N 个网络接口的情况下,资源利用率为1/N

第三种模式:mode=2,即:(balance-xor)XOR policy(平衡策略)

特点:基于指定的传输HASH策略传输数据包。缺省的策略是:(源MAC地址 XOR 目标MAC地址)% slave数量。其他的传输策略可以通过xmit_hash_policy选项指定,此模式提供负载平衡和容错能力

第四种模式:mode=3,即:broadcast(广播策略)

特点:在每个slave接口上传输每个数据包,此模式提供了容错能力

第五种模式:mode=4,即:(802.3ad)IEEE 802.3ad Dynamic link aggregation(IEEE802.3ad 动态链接聚合)

特点:创建一个聚合组,它们共享同样的速率和双工设定。根据802.3ad规范将多个slave工作在同一个激活的聚合体下。外出流量的slave选举是基于传输hash策略,该策略可以通过xmit_hash_policy选项从缺省的XOR策略改变到其他策略。需要注意的 是,并不是所有的传输策略都是802.3ad适应的,尤其考虑到在802.3ad标准43.2.4章节提及的包乱序问题。不同的实现可能会有不同的适应 性。

必要条件:

条件1:ethtool支持获取每个slave的速率和双工设定

条件2:switch(交换机)支持IEEE802.3ad Dynamic link aggregation

条件3:大多数switch(交换机)需要经过特定配置才能支持802.3ad模式

第六种模式:mode=5,即:(balance-tlb)Adaptive transmit load balancing(适配器传输负载均衡)

特点:不需要任何特别的switch(交换机)支持的通道bonding。在每个slave上根据当前的负载(根据速度计算)分配外出流量。如果正在接受数据的slave出故障了,另一个slave接管失败的slave的MAC地址。

该模式的必要条件:ethtool支持获取每个slave的速率

第七种模式:mode=6,即:(balance-alb)Adaptive load balancing(适配器适应性负载均衡)

特点:该模式包含了balance-tlb模式,同时加上针对IPV4流量的接收负载均衡(receiveload balance, rlb),而且不需要任何switch(交换机)的支持。接收负载均衡是通过ARP协商实现的。bonding驱动截获本机发送的ARP应答,并把源硬件地址改写为bond中某个slave的唯一硬件地址,从而使得不同的对端使用不同的硬件地址进行通信。

来自服务器端的接收流量也会被均衡。当本机发送ARP请求时,bonding驱动把对端的IP信息从ARP包中复制并保存下来。当ARP应答从对端到达时,bonding驱动把它的硬件地址提取出来,并发起一个ARP应答给bond中的某个slave。使用ARP协商进行负载均衡的一个问题是:每次广播 ARP请求时都会使用bond的硬件地址,因此对端学习到这个硬件地址后,接收流量将会全部流向当前的slave。这个问题可以通过给所有的对端发送更新(ARP应答)来解决,应答中包含他们独一无二的硬件地址,从而导致流量重新分布。当新的slave加入到bond中时,或者某个未激活的slave重新 激活时,接收流量也要重新分布。接收的负载被顺序地分布(roundrobin)在bond中最高速的slave上当某个链路被重新接上,或者一个新的slave加入到bond中,接收流量在所有当前激活的slave中全部重新分配,通过使用指定的MAC地址给每个 client发起ARP应答。下面介绍的updelay参数必须被设置为某个大于等于switch(交换机)转发延时的值,从而保证发往对端的ARP应答 不会被switch(交换机)阻截。

必要条件:

条件1:ethtool支持获取每个slave的速率;

条件2:底层驱动支持设置某个设备的硬件地址,从而使得总是有个slave(curr_active_slave)使用bond的硬件地址,同时保证每个 bond 中的slave都有一个唯一的硬件地址。如果curr_active_slave出故障,它的硬件地址将会被新选出来的 curr_active_slave接管其实mod=6与mod=0的区别:mod=6,先把eth0流量占满,再占eth1,….ethX;而mod=0的话,会发现2个口的流量都很稳定,基本一样的带宽。而mod=6,会发现第一个口流量很高,第2个口只占了小部分流量

Linux网口绑定:

通过网口绑定(bond)技术,可以很容易实现网口冗余,负载均衡,从而达到高可用高可靠的目的。前提约定:

2个物理网口分别是:eth0,eth1

绑定后的虚拟口是:bond0

服务器IP是:10.10.10.1

第一步,配置设定文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@woo ~]# vi  /etc/sysconfig/network-scripts/ifcfg-bond0
DEVICE=bond0
BOOTPROTO=none
ONBOOT=yes
IPADDR=10.10.10.1
NETMASK=255.255.255.0
NETWORK=192.168.0.0

[root@woo ~]# vi  /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
BOOTPROTO=none
MASTER=bond0
SLAVE=yes

[root@woo ~]# vi  /etc/sysconfig/network-scripts/ifcfg-eth1
DEVICE=eth1
BOOTPROTO=none
MASTER=bond0
SLAVE=yes
第二步,修改modprobe相关设定文件,并加载bonding模块:

1.在这里,我们直接创建一个加载bonding的专属设定文件/etc/modprobe.d/bonding.conf

1
2
3
[root@woo ~]# vi /etc/modprobe.d/bonding.conf
alias bond0 bonding
options bonding mode=0 miimon=200

2.加载模块(重启系统后就不用手动再加载了)

1
[root@woo ~]# modprobe bonding

3.确认模块是否加载成功:

1
2
[root@woo ~]# lsmod | grep bonding
bonding 100065 0
第三步,重启一下网络,然后确认一下状况:
1
2
3
4
5
[root@db01 ~]# service network restart
Shutting down interface bond0:  [  OK  ]
Shutting down loopback interface:  [  OK  ]
Bringing up loopback interface:  [  OK  ]
Bringing up interface bond0:  [  OK  ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@db01 ~]#  cat /proc/net/bonding/bond0
Ethernet Channel Bonding Driver: v3.4.0-1 (October 7, 2008)

Bonding Mode: fault-tolerance (active-backup)
Primary Slave: None
Currently Active Slave: eth0
MII Status: up
MII Polling Interval (ms): 100
Up Delay (ms): 0
Down Delay (ms): 0

Slave Interface: eth0
MII Status: up
Speed: 1000 Mbps
Duplex: full
Link Failure Count: 0
Permanent HW addr: 40:f2:e9:db:c9:c2

Slave Interface: eth1
MII Status: up
Speed: 1000 Mbps
Duplex: full
Link Failure Count: 0
Permanent HW addr: 40:f2:e9:db:c9:c3
[root@db01 ~]#  ifconfig | grep HWaddr
bond0     Link encap:Ethernet  HWaddr 40:F2:E9:DB:C9:C2
eth0      Link encap:Ethernet  HWaddr 40:F2:E9:DB:C9:C2
eth1      Link encap:Ethernet  HWaddr 40:F2:E9:DB:C9:C2

从上面的确认信息中,我们可以看到3个重要信息:

1.现在的bonding模式是active-backup

2.现在Active状态的网口是eth0

3.bond0,eth1的物理地址和处于active状态下的eth0的物理地址相同,这样是为了避免上位交换机发生混乱。

任意拔掉一根网线,然后再访问你的服务器,看网络是否还是通的。

第四步(一般不需要),系统启动自动绑定、增加默认网关:
1
2
3
4
5
[root@woo ~]# vi /etc/rc.d/rc.local
#追加
ifenslave bond0 eth0 eth1
route add default gw 10.10.10.1
#如可上网就不用增加路由,0.1地址按环境修改.

留心:前面只是2个网口绑定成一个bond0的情况,如果我们要设置多个bond口,比如物理网口eth0和eth1组成bond0,eth2和eth3组成bond1,

多网口绑定:

那么网口设置文件的设置方法和上面第1步讲的方法相同,只是/etc/modprobe.d/bonding.conf的设定就不能像下面这样简单的叠加了:

1
2
3
4
5
alias bond0 bonding
options bonding mode=1 miimon=200

alias bond1 bonding
options bonding mode=1 miimon=200
正确的设置方法有2种:

第一种,你可以看到,这种方式的话,多个bond口的模式就只能设成相同的了:

1
2
3
alias bond0 bonding
alias bond1 bonding
options bonding max_bonds=2 miimon=200 mode=1

第二种,这种方式,不同的bond口的mode可以设成不一样:

1
2
3
alias bond0 bonding
options bond0 miimon=100 mode=1
install bond1 /sbin/modprobe bonding -o bond1 miimon=200 mode=0

仔细看看上面这2种设置方法,现在如果是要设置3个,4个,甚至更多的bond口,你应该也会了吧!

后记:

简单的介绍一下上面在加载bonding模块的时候,options里的一些参数的含义:

miimon 监视网络链接的频度,单位是毫秒,我们设置的是200毫秒。

max_bonds 配置的bond口个数

mode bond模式,主要有以下几种,在一般的实际应用中,0和1用的比较多。

socket建立连接 sys_connect

http://blog.csdn.net/chensichensi/article/details/5272346

http://blog.csdn.net/qy532846454/article/details/7882819

http://www.2cto.com/kf/201303/198459.html

1
2
3
4
5
6
7
8
connect(fd, servaddr, addrlen);
-> SYSCALL_DEFINE3()
-> sock->ops->connect() == inet_stream_connect (sock->ops即inet_stream_ops)
-> tcp_v4_connect()
	-> inet_hash_connect()
		-> __inet_hash_connect()
			-> check_established()
				-> __inet_check_established()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
		int, addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;
	/* 找到文件描述符对应的BSD socket结构,在前面的socket调用中建立*/
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;
	/* copy对端的地址到内核空间 */
	err = move_addr_to_kernel(uservaddr, addrlen, (struct sockaddr *)&address);
	if (err < 0)
		goto out_put;

	err =
	    security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
	if (err)
		goto out_put;
	/* 调用该BSD socket对应的connect调用 */
	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags);
out_put:
	/* 释放文件的引用 */
	fput_light(sock->file, fput_needed);
out:
	return err;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
 *    Connect to a remote host. There is regrettably still a little
 *    TCP 'magic' in here.
 */
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
			int addr_len, int flags)
{
	struct sock *sk = sock->sk;
	int err;
	long timeo;

	lock_sock(sk);

	if (uaddr->sa_family == AF_UNSPEC) {
		err = sk->sk_prot->disconnect(sk, flags);
		sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
		goto out;
	}

	switch (sock->state) {
	default:
		err = -EINVAL;
		goto out;
	case SS_CONNECTED:     /* 该BSD socket已连接*/
		err = -EISCONN;
		goto out;
	case SS_CONNECTING:   /* 该BSD socket正在连接*/
		err = -EALREADY;
		/* Fall out of switch with err, set for this state */
		break;
	case SS_UNCONNECTED:
		err = -EISCONN;
		if (sk->sk_state != TCP_CLOSE)
			goto out;
	        /* INET SOCKET 调用协议特有connect操作符 */
		err = sk->sk_prot->connect(sk, uaddr, addr_len);
		if (err < 0)
			goto out;
	        /* 上面的调用完成后,连接并没有完成,*/
		sock->state = SS_CONNECTING;

		/* Just entered SS_CONNECTING state; the only
		 * difference is that return value in non-blocking
		 * case is EINPROGRESS, rather than EALREADY.
		 */
		err = -EINPROGRESS;
		break;
	}
	/* 获取连接超时时间*/
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);

	if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
		/* Error code is set above 进入定时等待 */
		if (!timeo || !inet_wait_for_connect(sk, timeo))
			goto out;

		err = sock_intr_errno(timeo);
		if (signal_pending(current))
			goto out;
	}

	/* Connection was closed by RST, timeout, ICMP error
	 * or another process disconnected us.
	 */
	if (sk->sk_state == TCP_CLOSE)
		goto sock_error;

	/* sk->sk_err may be not zero now, if RECVERR was ordered by user
	 * and error was received after socket entered established state.
	 * Hence, it is handled normally after connect() return successfully.
	 */

	sock->state = SS_CONNECTED;
	err = 0;
out:
	release_sock(sk);
	return err;

sock_error:
	err = sock_error(sk) ? : -ECONNABORTED;
	sock->state = SS_UNCONNECTED;
	if (sk->sk_prot->disconnect(sk, flags))
		sock->state = SS_DISCONNECTING;
	goto out;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
	struct rtable *rt;
	__be32 daddr, nexthop;
	int tmp;
	int err;

	if (addr_len < sizeof(struct sockaddr_in))
		return -EINVAL;

	if (usin->sin_family != AF_INET)
		return -EAFNOSUPPORT;
	/* 开始准备路由 */
	nexthop = daddr = usin->sin_addr.s_addr;
	if (inet->opt && inet->opt->srr) {
		if (!daddr)
			return -EINVAL;
		nexthop = inet->opt->faddr;
	}
	/* 调用路由模块获取出口信息,这里不深入 */
	tmp = ip_route_connect(&rt, nexthop, inet->saddr,
			       RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
			       IPPROTO_TCP,
			       inet->sport, usin->sin_port, sk, 1);
	if (tmp < 0) {
		if (tmp == -ENETUNREACH)
			IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
		return tmp;
	}
	/* 如果获取的路由是广播或多播域, 返回网络不可达,tcp不支持多播与广播 */
	if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
		ip_rt_put(rt);
		return -ENETUNREACH;
	}

	if (!inet->opt || !inet->opt->srr)
		daddr = rt->rt_dst;

	if (!inet->saddr)
		inet->saddr = rt->rt_src;
	inet->rcv_saddr = inet->saddr;

	if (tp->rx_opt.ts_recent_stamp && inet->daddr != daddr) {
		/* Reset inherited state */
		tp->rx_opt.ts_recent      = 0;
		tp->rx_opt.ts_recent_stamp = 0;
		tp->write_seq         = 0;
	}

	if (tcp_death_row.sysctl_tw_recycle &&
	    !tp->rx_opt.ts_recent_stamp && rt->rt_dst == daddr) {
		struct inet_peer *peer = rt_get_peer(rt);
		/*
		 * VJ's idea. We save last timestamp seen from
		 * the destination in peer table, when entering state
		 * TIME-WAIT * and initialize rx_opt.ts_recent from it,
		 * when trying new connection.
		 */
		if (peer != NULL &&
		    peer->tcp_ts_stamp + TCP_PAWS_MSL >= get_seconds()) {
			tp->rx_opt.ts_recent_stamp = peer->tcp_ts_stamp;
			tp->rx_opt.ts_recent = peer->tcp_ts;
		}
	}

	inet->dport = usin->sin_port;
	inet->daddr = daddr;

	inet_csk(sk)->icsk_ext_hdr_len = 0;
	if (inet->opt)
		inet_csk(sk)->icsk_ext_hdr_len = inet->opt->optlen;
	/* mss_clamp */
	tp->rx_opt.mss_clamp = 536;

	/* Socket identity is still unknown (sport may be zero).
	 * However we set state to SYN-SENT and not releasing socket
	 * lock select source port, enter ourselves into the hash tables and
	 * complete initialization after this.
	 */
	tcp_set_state(sk, TCP_SYN_SENT);
	err = inet_hash_connect(&tcp_death_row, sk);
	if (err)
		goto failure;

	err = ip_route_newports(&rt, IPPROTO_TCP,
				inet->sport, inet->dport, sk);
	if (err)
		goto failure;

	/* OK, now commit destination to socket.  */
	sk->sk_gso_type = SKB_GSO_TCPV4;
	sk_setup_caps(sk, &rt->u.dst);

	if (!tp->write_seq)
		tp->write_seq = secure_tcp_sequence_number(inet->saddr,
							   inet->daddr,
							   inet->sport,
							   usin->sin_port);
	/* id是IP包头的id域 */
	inet->id = tp->write_seq ^ jiffies;

	err = tcp_connect(sk);
	rt = NULL;
	if (err)
		goto failure;

	return 0;

failure:
	/*
	 * This unhashes the socket and releases the local port,
	 * if necessary.
	 */
	tcp_set_state(sk, TCP_CLOSE);
	ip_rt_put(rt);
	sk->sk_route_caps = 0;
	inet->dport = 0;
	return err;
}

当snum==0时,表明此时源端口没有指定,此时会随机选择一个空闲端口作为此次连接的源端口。low和high分别表示可用端口的下限和上限,remaining表示可用端口的数,注意这里的可用只是指端口可以用作源端口,其中部分端口可能已经作为其它socket的端口号在使用了,所以要循环1~remaining,直到查找到空闲的源端口。

下面来看下对每个端口的检查,即//choose a valid port部分的代码。这里要先了解下tcp的内核表组成,udp的表内核表udptable只是一张hash表,tcp的表则稍复杂,它的名字是tcp_hashinfo,在tcp_init()中被初始化,这个数据结构定义如下(省略了不相关的数据):

1
2
3
4
5
6
7
8
struct inet_hashinfo {
	struct inet_ehash_bucket *ehash;
	……
	struct inet_bind_hashbucket *bhash;
	……
	struct inet_listen_hashbucket  listening_hash[INET_LHTABLE_SIZE]
					____cacheline_aligned_in_smp;
};

从定义可以看出,tcp表又分成了三张表ehash, bhash, listening_hash,其中ehash, listening_hash对应于socket处在TCP的ESTABLISHED, LISTEN状态,bhash对应于socket已绑定了本地地址。三者间并不互斥,如一个socket可同时在bhash和ehash中,由于TIME_WAIT是一个比较特殊的状态,所以ehash又分成了chain和twchain,为TIME_WAIT的socket单独形成一张表。

回到刚才的代码,现在还只是建立socket连接,使用的就应该是tcp表中的bhash。首先取得内核tcp表的bind表 – bhash,查看是否已有socket占用:

如果没有,则调用inet_bind_bucket_create()创建一个bind表项tb,并插入到bind表中,跳转至goto ok代码段; 如果有,则跳转至goto ok代码段。

进入ok代码段表明已找到合适的bind表项(无论是创建的还是查找到的),调用inet_bind_hash()赋值源端口inet_num。

inet_hash_connect()函数只是对__inet_hash_connect()函数进行了简单的封装。在__inet_hash_connect()中如果已绑定了端口号,并且是和其他传输控制块共享绑定的端口号,则会调用check_established参数指向的函数来检查这个绑定的端口号是否可用,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
int __inet_hash_connect(struct inet_timewait_death_row *death_row,
		struct sock *sk, u32 port_offset,
		int (*check_established)(struct inet_timewait_death_row *,
		struct sock *, __u16, struct inet_timewait_sock **),
		void (*hash)(struct sock *sk))
{
	struct inet_hashinfo *hinfo = death_row->hashinfo;
	const unsigned short snum = inet_sk(sk)->num;
	struct inet_bind_hashbucket *head;
	struct inet_bind_bucket *tb;
	int ret;
	struct net *net = sock_net(sk);

	if (!snum) {
		int i, remaining, low, high, port;
		static u32 hint;
		u32 offset = hint + port_offset;
		struct hlist_node *node;
		struct inet_timewait_sock *tw = NULL;

		inet_get_local_port_range(&low, &high);
		remaining = (high - low) + 1;

		local_bh_enable();
		for (i = 1; i <= remaining; i++) {
			port = low + (i + offset) % remaining;
			if (inet_is_reserved_local_port(port)
				continue;
			head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];
			spin_lock(&head->lock);
			inet_bind_bucket_for_each(tb, node, &head->chain) {
				if (net_eq(ib_net(tb), net) && tb->port == port) {
					if (tb->fastreuse >= 0)
						goto next_port;
					WARN_ON(hlist_empty(&tb->owners));
					if (!check_established(death_row, sk, port, &tw))
						goto ok;
					goto next_port;
				}
			}

			tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net, head, port);
			if (!tb) {
				spin_unlock(&head->lock);
				break;
			}
			tb->fastreuse = -1;
			tb->fastreuseport = -1;
			goto ok;
		next_port:
			spin_unlock(&head->lock);
		}
		local_bh_enable();

		return -EADDRNOTAVAIL;

ok:
		hint += i;

		inet_bind_hash(sk, tb, port);
		if (sk_unhashed(sk)) {
			inet_sk(sk)->sport = htons(port);
			hash(sk);
		}
		spin_unlock(&head->lock);
		if (tw) {
			inet_twsk_deschedule(tw, death_row);
			inet_twsk_put(tw);
		}

		ret = 0;
		goto out;
	}

	head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)];
	tb  = inet_csk(sk)->icsk_bind_hash;
	spin_lock_bh(&head->lock);
	if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {
		hash(sk);
		spin_unlock_bh(&head->lock);
		return 0;
	} else {
		spin_unlock(&head->lock);
		/* No definite answer... Walk to established hash table */
		ret = check_established(death_row, sk, snum, NULL);
out:
		local_bh_enable();
		return ret;
	}
}

(sk_head(&tb->owners) == sk && !sk->sk_bind_node.next)这个判断条件就是用来判断是不是只有当前传输控制块在使用已绑定的端口,条件为false时,会执行else分支,检查是否可用。这么看来,调用bind()成功并不意味着这个端口就真的可以用。

check_established参数对应的函数是__inet_check_established(),在inet_hash_connect()中可以看到。在上面的代码中我们还注意到调用check_established()时第三个参数为NULL,这在后面的分析中会用到。

__inet_check_established()函数中,会分别在TIME_WAIT传输控制块和除TIME_WIAT、LISTEN状态外的传输控制块中查找是已绑定的端口是否已经使用,代码片段如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* called with local bh disabled */
static int __inet_check_established(struct inet_timewait_death_row *death_row,
			struct sock *sk, __u16 lport,
			struct inet_timewait_sock **twp)
{
	struct inet_hashinfo *hinfo = death_row->hashinfo;
	struct inet_sock *inet = inet_sk(sk);
	__be32 daddr = inet->rcv_saddr;
	__be32 saddr = inet->daddr;
	int dif = sk->sk_bound_dev_if;
	INET_ADDR_COOKIE(acookie, saddr, daddr)
	const __portpair ports = INET_COMBINED_PORTS(inet->dport, lport);
	struct net *net = sock_net(sk);
	unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->dport);
	struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
	spinlock_t *lock = inet_ehash_lockp(hinfo, hash);
	struct sock *sk2;
	const struct hlist_nulls_node *node;
	struct inet_timewait_sock *tw;

	spin_lock(lock);

	/* Check TIME-WAIT sockets first. */
	sk_nulls_for_each(sk2, node, &head->twchain) {
		tw = inet_twsk(sk2);


		if (INET_TW_MATCH(sk2, net, hash, acookie,
				saddr, daddr, ports, dif)) {
			if (twsk_unique(sk, sk2, twp))
				goto unique;
			else
				goto not_unique;
		}
	}
	tw = NULL;

	/* And established part... */
	sk_nulls_for_each(sk2, node, &head->chain) {
		if (INET_MATCH(sk2, net, hash, acookie,
				saddr, daddr, ports, dif))
			goto not_unique;
	}

unique:
	......
	return 0;

not_unique:
	spin_unlock(lock);
	return -EADDRNOTAVAIL;
}

如果是TCP套接字,twsk_uniqueue()中会调用tcp_twsk_uniqueue()来判断,返回true的条件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
	const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
	struct tcp_sock *tp = tcp_sk(sk);

	if (tcptw->tw_ts_recent_stamp &&
			(twp == NULL || (sysctl_tcp_tw_reuse &&
			get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
		......
		return 1;
	}

	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
 * Build a SYN and send it off.
 */
int tcp_connect(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *buff;
	/* 初始化连接对应的INET socket结构的参数,为连接做准备 */
	tcp_connect_init(sk);
	/* 获取一个skb,由于是syn包,没有数据,所以大小是MAX_TCP_HEADER的16位对齐 */
	buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);
	if (unlikely(buff == NULL))
		return -ENOBUFS;

	/* Reserve space for headers. */
	skb_reserve(buff, MAX_TCP_HEADER);

	tp->snd_nxt = tp->write_seq;
	/* 设置skb相关参数 */
	tcp_init_nondata_skb(buff, tp->write_seq++, TCPCB_FLAG_SYN);
	/* 设置ECN */
	TCP_ECN_send_syn(sk, buff);

	/* Send it off. */
	/* 保存该数据包的发送时间*/
	TCP_SKB_CB(buff)->when = tcp_time_stamp;
	tp->retrans_stamp = TCP_SKB_CB(buff)->when;
	skb_header_release(buff);
	/* 加入发送队列,待确认后在丢弃*/
	__tcp_add_write_queue_tail(sk, buff);
	sk->sk_wmem_queued += buff->truesize;
	sk_mem_charge(sk, buff->truesize);
	tp->packets_out += tcp_skb_pcount(buff);
	tcp_transmit_skb(sk, buff, 1, GFP_KERNEL);

	/* We change tp->snd_nxt after the tcp_transmit_skb() call
	 * in order to make this packet get counted in tcpOutSegs.
	 */
	tp->snd_nxt = tp->write_seq;
	tp->pushed_seq = tp->write_seq;
	TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);

	/* Timer for repeating the SYN until an answer. */
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
 * Do all connect socket setups that can be done AF independent.
 */
static void tcp_connect_init(struct sock *sk)
{
	struct dst_entry *dst = __sk_dst_get(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	__u8 rcv_wscale;

	/* We'll fix this up when we get a response from the other end.
	 * See tcp_input.c:tcp_rcv_state_process case TCP_SYN_SENT.
	 */
	tp->tcp_header_len = sizeof(struct tcphdr) +
		(sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);

#ifdef CONFIG_TCP_MD5SIG
	if (tp->af_specific->md5_lookup(sk, sk) != NULL)
		tp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;
#endif

	/* If user gave his TCP_MAXSEG, record it to clamp */
	if (tp->rx_opt.user_mss)
		tp->rx_opt.mss_clamp = tp->rx_opt.user_mss;
	tp->max_window = 0;
	/* 初始化MTU probe*/
	tcp_mtup_init(sk);
	/* 设置mss */
	tcp_sync_mss(sk, dst_mtu(dst));

	if (!tp->window_clamp)
		tp->window_clamp = dst_metric(dst, RTAX_WINDOW);
	tp->advmss = dst_metric(dst, RTAX_ADVMSS);
	if (tp->rx_opt.user_mss && tp->rx_opt.user_mss < tp->advmss)
		tp->advmss = tp->rx_opt.user_mss;

	tcp_initialize_rcv_mss(sk);
	/* 根据接收空间大小初始化一个通告窗口 */
	tcp_select_initial_window(tcp_full_space(sk),
				  tp->advmss - (tp->rx_opt.ts_recent_stamp ? tp->tcp_header_len - sizeof(struct tcphdr) : 0),
				  &tp->rcv_wnd,
				  &tp->window_clamp,
				  sysctl_tcp_window_scaling,
				  &rcv_wscale);

	tp->rx_opt.rcv_wscale = rcv_wscale;
	tp->rcv_ssthresh = tp->rcv_wnd;

	sk->sk_err = 0;
	sock_reset_flag(sk, SOCK_DONE);
	tp->snd_wnd = 0;
	/* 更新一些滑动窗口的成员*/
	tcp_init_wl(tp, tp->write_seq, 0);
	tp->snd_una = tp->write_seq;
	tp->snd_sml = tp->write_seq;
	tp->snd_up = tp->write_seq;
	tp->rcv_nxt = 0;
	tp->rcv_wup = 0;
	tp->copied_seq = 0;

	inet_csk(sk)->icsk_rto = TCP_TIMEOUT_INIT;
	inet_csk(sk)->icsk_retransmits = 0;
	tcp_clear_retrans(tp);
}

skb发送后,connect并没有返回,因为此时连接还没有建立,tcp进入等待状态,此时回到前面的inet_stream_connect函数

在发送syn后进入等待状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static long inet_wait_for_connect(struct sock *sk, long timeo)
{
	DEFINE_WAIT(wait);
	/* sk_sleep 保存此INET SOCKET的等待队列 */
	prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);

	/* Basic assumption: if someone sets sk->sk_err, he _must_
	 * change state of the socket from TCP_SYN_*.
	 * Connect() does not allow to get error notifications
	 * without closing the socket.
	 */
	/* 定时等待知道状态变化 */
	while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
		release_sock(sk);
		timeo = schedule_timeout(timeo);
		lock_sock(sk);
		if (signal_pending(current) || !timeo)
			break;
		prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);
	}
	finish_wait(sk->sk_sleep, &wait);
	return timeo;
}

udp checksum

http://wenx05124561.blog.163.com/blog/static/124000805201242032041268/

a. 网卡设备属性

1
2
3
4
#define NETIF_F_IP_CSUM     2   /* 基于IPv4的L4层checksum. */  
#define NETIF_F_NO_CSUM     4   /* 设备可靠不需要L4层checksum. loopack. */  
#define NETIF_F_HW_CSUM     8   /* 基于所有协议的L4层checksum*/  
#define NETIF_F_IPV6_CSUM   16  /* 基于IPv6的L4层checksum*/  

通过ethtool -k eth0可以查看网卡是否支持硬件checksum,tx-checksumming: on 表明支持发送hardware checksum。

b. linux UDP checksum数据结构

1
2
3
4
5
6
7
union {
	__wsum    csum;
	struct {
		__u16 csum_start;
		__u16 csum_offset;
	};
};

1) skb->csum和skb->ip_summed这两个域也是与4层校验相关的,这两个域的含义依赖于skb表示的是一个输入包还是一个输出包。

2) 当网卡设备能提供硬件checksum并且作为输出包的时候,表示为skb->csum_start和skb->csum_offset

csum_start: Offset from skb->head where checksumming should start

csum_offset: Offset from csum_start where checksum should be stored

当数据包是一个输入包时

skb->ip_summed表示的是四层校验的状态,下面的几个宏定义表示了设备驱动传递给4层的一些信息。

1
2
3
#define CHECKSUM_NONE 0
#define CHECKSUM_UNNECESSARY 1
#define CHECKSUM_COMPLETE 2

skb->csum:存放硬件或者软件计算的payload的checksum不包括伪头,但是是否有意义由skb->ip_summed的值决定。

CHECKSUM_NONE表示csum域中的校验值是无意义的,需要L4层自己校验payload和伪头。有可能是硬件检验出错或者硬件没有校验功能,协议栈软件更改如pskb_trim_rcsum函数。

CHECKSUM_UNNECESSARY表示网卡或者协议栈已经计算和验证了L4层的头和校验值。也就是计算了tcp udp的伪头。还有一种情况就是回环,因为在回环中错误发生的概率太低了,因此就不需要计算校验来节省cpu事件。

CHECKSUM_COMPLETE表示网卡已经计算了L4层payload的校验,并且csum已经被赋值,此时L4层的接收者只需要加伪头并验证校验结果。

1) 在L4层发现如果udp->check位段被设为0,那么skb->ip_summed直接设为CHECKSUM_UNNECESSARY,放行该报文。

2) 如果skb->ip_summed为CHECKSUM_COMPLETE,则把skb->csum加上伪头进行校验,成功则将skb->ip_summed设为CHECKSUM_UNNECESSARY, 放行该数据包。

3) 通过上述后skb->ip_summed还不是CHECKSUM_UNNECESSARY,那么重新计算伪头赋给skb->csum。

4) 将还不是CHECKSUM_UNNECESSARY的数据报文的payload加上skb->csum进行checksum计算,成功将设为CHECKSUM_UNNECESSARY并放行,失败则丢弃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static inline int udp4_csum_init(struct sk_buff *skb, struct udphdr *uh, 
				int proto)
{
	const struct iphdr *iph;
	int err; 

	UDP_SKB_CB(skb)->partial_cov = 0; 
	UDP_SKB_CB(skb)->cscov = skb->len;

	if (proto == IPPROTO_UDPLITE) {
		err = udplite_checksum_init(skb, uh); 
		if (err)
			return err; 
	}    

	iph = ip_hdr(skb);
	if (uh->check == 0) { 
		skb->ip_summed = CHECKSUM_UNNECESSARY;
	} else if (skb->ip_summed == CHECKSUM_COMPLETE) {
		if (!csum_tcpudp_magic(iph->saddr, iph->daddr, skb->len,
				proto, skb->csum))
			skb->ip_summed = CHECKSUM_UNNECESSARY;
	}    
	if (!skb_csum_unnecessary(skb))
		skb->csum = csum_tcpudp_nofold(iph->saddr, iph->daddr,
							skb->len, proto, 0);
	/* Probably, we should checksum udp header (it should be in cache
	 * in any case) and data in tiny packets (< rx copybreak).
	 */

	return 0;
}
1
2
if (udp_lib_checksum_complete(skb))
	goto csum_error;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static inline int udp_lib_checksum_complete(struct sk_buff *skb)
{
	return !skb_csum_unnecessary(skb) &&
		__udp_lib_checksum_complete(skb);
}

static inline __sum16 __udp_lib_checksum_complete(struct sk_buff *skb)
{
	return __skb_checksum_complete_head(skb, UDP_SKB_CB(skb)->cscov);
}

__sum16 __skb_checksum_complete_head(struct sk_buff *skb, int len)
{
	__sum16 sum;

	sum = csum_fold(skb_checksum(skb, 0, len, skb->csum));
	if (likely(!sum)) {
		if (unlikely(skb->ip_summed == CHECKSUM_COMPLETE))
			netdev_rx_csum_fault(skb->dev);
		skb->ip_summed = CHECKSUM_UNNECESSARY;
	}
	return sum;
}

当数据包是输出包时

skb->csum表示为csum_start和csum_offset,它表示硬件网卡存放将要计算的校验值的地址,和最后填充的便宜。这个域在输出包时使用,只在校验值在硬件计算的情况下才对于网卡真正有意义。硬件checksum功能只能用于非分片报文。 而此时ip_summed可以被设置的值有下面两种:

1
2
#define CHECKSUM_NONE        0
#define CHECKSUM_PARTIAL  3

CHECKSUM_NONE 表示协议栈计算好了校验值,设备不需要做任何事。CHECKSUM_PARTIAL表示协议栈算好了伪头需要硬件计算payload checksum。

1)对于UDP socket开启了UDP_CSUM_NOXMIT / UDP csum disabled /

1
2
uh->check = 0;
skb->ip_summed = CHECKSUM_NONE;

2)软件udp checksum

1
2
3
4
5
6
7
struct iphdr *iph = ip_hdr(skb);
struct udphdr *uh = udp_hdr(skb);
uh->check = 0;
skb->csum = csum_partial(skb_transport_header (skb), skb->len, 0);//skb->data指向传输层头
uh->check = csum_tcpudp_magic(iph->saddr, iph->daddr, skb->len, iph->protocol, skb->csum);
skb->ip_summed = CHECKSUM_NONE;
//Todo: scatter and gather

3) 硬件checksum: 只能是ip报文长度小于mtu的数据报(没有分片的报文)。

CHECKSUM_PARTIAL表示使用硬件checksum ,L4层的伪头的校验已经完毕,并且已经加入uh->check字段中,此时只需要设备计算整个头4层头的校验值。

(对于支持scatter and gather的报文必须要传输层头在线性空间才能使用硬件checksum功能)

1
2
3
4
uh->check = ~csum_tcpudp_magic(iph->saddr, iph->daddr, skb->len, IPPROTO_UDP, 0);
skb->csum_start = skb_transport_header (skb) - skb->head;
skb->csum_offset = offsetof(struct udphdr, check);
skb->ip_summed = CHECKSUM_PARTIAL;

最后在dev_queue_xmit发送的时候发现设备不支持硬件checksum就会进行软件计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
				struct netdev_queue *txq)

{
	.......

			/* If packet is not checksummed and device does not
			 * support checksumming for this protocol, complete
			 * checksumming here.
			 */
			if (skb->ip_summed == CHECKSUM_PARTIAL) {
				skb_set_transport_header(skb, skb->csum_start -
						skb_headroom(skb));
				if (!dev_can_checksum(dev, skb) &&
						skb_checksum_help(skb))
					goto out_kfree_skb;
			}
	........

Linux系统启动过程分析

http://blog.chinaunix.net/uid-23069658-id-3142047.html

BIOS自检

稍有计算机基础的人都应该听过BIOS(Basic Input / Output System),又称基本输入输出系统,可以视为是一个永久地记录在ROM中的一个软件,是操作系统输入输出管理系统的一部分。早期的BIOS芯片确实是"只读"的,里面的内容是用一种烧录器写入的,一旦写入就不能更改,除非更换芯片。现在的主机板都使用一种叫Flash EPROM的芯片来存储系统BIOS,里面的内容可通过使用主板厂商提供的擦写程序擦除后重新写入,这样就给用户升级BIOS提供了极大的方便。

BIOS的功能由两部分组成,分别是POST码和Runtime服务。POST阶段完成后它将从存储器中被清除,而Runtime服务会被一直保留,用于目标操作系统的启动。BIOS两个阶段所做的详细工作如下:

步骤1:上电自检POST(Power-on self test),主要负责检测系统外围关键设备(如:CPU、内存、显卡、I/O、键盘鼠标等)是否正常。例如,最常见的是内存松动的情况,BIOS自检阶段会报错,系统就无法启动起来;

步骤2:步骤1成功后,便会执行一段小程序用来枚举本地设备并对其初始化。这一步主要是根据我们在BIOS中设置的系统启动顺序来搜索用于启动系统的驱动器,如硬盘、光盘、U盘、软盘和网络等。我们以硬盘启动为例,BIOS此时去读取硬盘驱动器的第一个扇区(MBR,512字节),然后执行里面的代码。实际上这里BIOS并不关心启动设备第一个扇区中是什么内容,它只是负责读取该扇区内容、并执行。

至此,BIOS的任务就完成了,此后将系统启动的控制权移交到MBR部分的代码。

PS: 在个人电脑中,Linux的启动是从0xFFFF0地址开始的。

系统引导

我们首先来了解一下MBR,它是Master Boot Record的缩写。硬盘的0柱面、0磁头、1扇区称为主引导扇区。它由三个部分组成,主引导程序(Bootloader)、 硬盘分区表DPT(Disk Partition table)和硬盘有效标志(55AA),其结构图如下所示:

磁盘分区表包含以下三部分:

1)、Partition ID (5:延申 82:Swap 83:Linux 8e:LVM fd:RAID)

2)、Partition起始磁柱

3)、Partition的磁柱数量

通常情况下,诸如lilo、grub这些常见的引导程序都直接安装在MBR中。我们以grub为例来分析这个引导过程。

grub引导也分为两个阶段stage1阶段和stage2阶段(有些较新的grub又定义了stage1.5阶段)。

1)、stage1:stage1是直接被写入到MBR中去的,这样机器一启动检测完硬件后,就将控制权交给了GRUB的代码。也就是上图所看到的前446个字节空间中存放的是stage1的代码。BIOS将stage1载入内存中0x7c00处并跳转执行。stage1(/stage1/start.S)的任务非常单纯,仅仅是将硬盘0头0道2扇区读入内存。而0头0道2扇区内容是源代码中的/stage2/start.S,编译后512字节,它是stage2或者stage1_5的入口。而此时,stage1是没有识别文件系统的能力的。如果感觉脑子有些晕了,那么下面的过程就直接跳过,去看stage2吧!

【外传】定位硬盘的0头0道2扇区的过程:

BIOS将stage1载入内存0x7c00处并执行,然后调用BIOS INIT13中断,将硬盘0头0道2扇区内容载入内存0x7000处,然后调用copy_buffer将其转移到内存0x8000处。在定位0头0道2扇区时通常有两种寻址方式:LBA和CHS。如果你是刨根问底儿型的爱好者,那么此时去找谷哥打听打听这两种方式的来龙去脉吧。

2)、stage2:严格来说这里还应该再区分个stage1.5的,就一并把stage1.5放在这里一起介绍了,免得大家看得心里乱哄哄的。好的,我们继续说0头0到2扇区的/stage2/start.S文件,当它的内容被读入到内存之后,它的主要作用就是负责将stage2或stage1.5从硬盘读到内存中。如果是stage2,它将被载入到0x820处;如果是stage1.5,它将被载入到0x2200处。这里的stage2或者stage1_5不是/boot分区/boot/grub目录下的文件,因为这个时候grub还没有能力识别任何文件系统。

如果start.S加载stage1.5:stage1.5它存放在硬盘0头0道3扇区向后的位置,stage1_5作为stage1和stage2中间的桥梁,stage1_5有识别文件系统的能力,此后grub才有能力去访问/boot分区/boot/grub目录下的 stage2文件,将stage2载入内存并执行。

如果start.S加载stage2:同样,这个stage2也不是/boot分区/boot/grub目录下的stage2,这个时候start.S读取的是存放在/boot分区Boot Sector的stage2。这种情况下就有一个限制:因为start.S通过BIOS中断方式直接对硬盘寻址(而非通过访问具体的文件系统),其寻址范围有限,限制在8GB以内。因此这种情况需要将/boot分区分在硬盘8GB寻址空间之前。

假如是情形2,我们将/boot/grub目录下的内容清空,依然能成功启动grub;假如是情形1,将/boot/grub目录下stage2删除后,则系统启动过程中grub会启动失败。

启动内核

当stage2被载入内存执行时,它首先会去解析grub的配置文件/boot/grub/grub.conf,然后加载内核镜像到内存中,并将控制权转交给内核。而内核会立即初始化系统中各设备并做相关的配置工作,其中包括CPU、I/O、存储设备等。

关于Linux的设备驱动程序的加载,有一部分驱动程序直接被编译进内核镜像中,另一部分驱动程序则是以模块的形式放在initrd(ramdisk)中。

Linux内核需要适应多种不同的硬件架构,但是将所有的硬件驱动编入内核又是不实际的,而且内核也不可能每新出一种硬件结构,就将该硬件的设备驱动写入内核。实际上Linux的内核镜像仅是包含了基本的硬件驱动,在系统安装过程中会检测系统硬件信息,根据安装信息和系统硬件信息将一部分设备驱动写入 initrd 。这样在以后启动系统时,一部分设备驱动就放在initrd中来加载。这里有必要给大家再多介绍一下initrd这个东东:

initrd 的英文含义是 bootloader initialized RAM disk,就是由 boot loader 初始化的内存盘。在 linu2.6内核启动前,boot loader 会将存储介质中的 initrd 文件加载到内存,内核启动时会在访问真正的根文件系统前先访问该内存中的 initrd 文件系统。在 boot loader 配置了 initrd 的情况下,内核启动被分成了两个阶段,第一阶段先执行 initrd 文件系统中的init,完成加载驱动模块等任务,第二阶段才会执行真正的根文件系统中的 /sbin/init 进程。

另外一个概念:initramfs

initramfs 是在 kernel 2.5中引入的技术,实际上它的含义就是:在内核镜像中附加一个cpio包,这个cpio包中包含了一个小型的文件系统,当内核启动时,内核将这个 cpio包解开,并且将其中包含的文件系统释放到rootfs中,内核中的一部分初始化代码会放到这个文件系统中,作为用户层进程来执行。这样带来的明显的好处是精简了内核的初始化代码,而且使得内核的初始化过程更容易定制。 疑惑的是:我的内核是2.6.32-71.el6.i686版本,但在我的/boot分区下面却存在的是/boot/initramfs-2.6.32-71.el6.i686.img类型的文件,没搞明白,还望高人解惑。我只知道在2.6内核中支持两种格式的initrd,一种是2.4内核的文件系统镜像image-initrd,一种是cpio格式。接下来我们就来探究一下initramfs-2.6.32-71.el6.i686.img里到底放了那些东西。

在tmp文件夹中解压initrd.img里的内容:

如果initrd.img文件的格式显示为“initrd.img:ISO 9660 CD-ROM filesystem data”,则可直接输入命令“mount -o loop initrd.img /mnt/test”进行挂载。

通过上的分析和我们的验证,我们确实得到了这样的结论:

grub的stage2将initrd加载到内存里,让后将其中的内容释放到内容中,内核便去执行initrd中的init脚本,这时内核将控制权交给了init文件处理。我们简单浏览一下init脚本的内容,发现它也主要是加载各种存储介质相关的设备驱动程序。当所需的驱动程序加载完后,会创建一个根设备,然后将根文件系统rootfs以只读的方式挂载。这一步结束后,释放未使用的内存,转换到真正的根文件系统上面去,同时运行/sbin/init程序,执行系统的1号进程。此后系统的控制权就全权交给/sbin/init进程了。

初始化系统

经过千辛万苦的跋涉,我们终于接近黎明的曙光了。接下来就是最后一步了:初始化系统。/sbin/init进程是系统其他所有进程的父进程,当它接管了系统的控制权先之后,它首先会去读取/etc/inittab文件来执行相应的脚本进行系统初始化,如设置键盘、字体,装载模块,设置网络等。主要包括以下工作:

1)、执行系统初始化脚本(/etc/rc.d/rc.sysinit),对系统进行基本的配置,以读写方式挂载根文件系统及其它文件系统,到此系统算是基本运行起来了,后面需要进行运行级别的确定及相应服务的启动。rc.sysinit所做的事情(不同的Linux发行版,该文件可能有些差异)如下:

(1)获取网络环境与主机类型。首先会读取网络环境设置文件"/etc/sysconfig/network",获取主机名称与默认网关等网络环境。

(2)测试与载入内存设备/proc及usb设备/sys。除了/proc外,系统会主动检测是否有usb设备,并主动加载usb驱动,尝试载入usb文件系统。

(3)决定是否启动SELinux。

(4)接口设备的检测与即插即用(pnp)参数的测试。

(5)用户自定义模块的加载。用户可以再"/etc/sysconfig/modules/*.modules"加入自定义的模块,此时会加载到系统中。

(6)加载核心的相关设置。按"/etc/sysctl.conf"这个文件的设置值配置功能。

(7)设置系统时间(clock)。

(8)设置终端的控制台的字形。

(9)设置raid及LVM等硬盘功能。

(10)以方式查看检验磁盘文件系统。

(11)进行磁盘配额quota的转换。

(12)重新以读取模式载入系统磁盘。

(13)启动quota功能。

(14)启动系统随机数设备(产生随机数功能)。

(15)清楚启动过程中的临时文件。

(16)将启动信息加载到"/var/log/dmesg"文件中。

当/etc/rc.d/rc.sysinit执行完后,系统就可以顺利工作了,只是还需要启动系统所需要的各种服务,这样主机才可以提供相关的网络和主机功能,因此便会执行下面的脚本。

2)、执行/etc/rc.d/rc脚本。该文件定义了服务启动的顺序是先K后S,而具体的每个运行级别的服务状态是放在/etc/rc.d/rc.d(=0~6)目录下,所有的文件均是指向/etc/init.d下相应文件的符号链接。rc.sysinit通过分析/etc/inittab文件来确定系统的启动级别,然后才去执行/etc/rc.d/rc*.d下的文件。

/etc/init.d-> /etc/rc.d/init.d

/etc/rc ->/etc/rc.d/rc

/etc/rc.d ->/etc/rc.d/rc.d

/etc/rc.local-> /etc/rc.d/rc.local

/etc/rc.sysinit-> /etc/rc.d/rc.sysinit

也就是说,/etc目录下的init.d、rc、rc*.d、rc.local和rc.sysinit均是指向/etc/rc.d目录下相应文件和文件夹的符号链接。我们以启动级别3为例来简要说明一下。

/etc/rc.d/rc3.d目录,该目录下的内容全部都是以 S 或 K 开头的链接文件,都链接到"/etc/rc.d/init.d"目录下的各种shell脚本。S表示的是启动时需要start的服务内容,K表示关机时需要关闭的服务内容。/etc/rc.d/rc.d中的系统服务会在系统后台启动,如果要对某个运行级别中的服务进行更具体的定制,通过chkconfig命令来操作,或者通过setup、ntsys、system-config-services来进行定制。如果我们需要自己增加启动的内容,可以在init.d目录中增加相关的shell脚本,然后在rc.d目录中建立链接文件指向该shell脚本。这些shell脚本的启动或结束顺序是由S或K字母后面的数字决定,数字越小的脚本越先执行。例如,/etc/rc.d/rc3.d /S01sysstat就比/etc/rc.d/rc3.d /S99local先执行。

3)、执行用户自定义引导程序/etc/rc.d/rc.local。其实当执行/etc/rc.d/rc3.d/S99local时,它就是在执行/etc/rc.d/rc.local。S99local是指向rc.local的符号链接。就是一般来说,自定义的程序不需要执行上面所说的繁琐的建立shell增加链接文件的步骤,只需要将命令放在rc.local里面就可以了,这个shell脚本就是保留给用户自定义启动内容的。

4)、完成了系统所有的启动任务后,linux会启动终端或X-Window来等待用户登录。tty1,tty2,tty3…这表示在运行等级1,2,3,4的时候,都会执行"/sbin/mingetty",而且执行了6个,所以linux会有6个纯文本终端,mingetty就是启动终端的命令。

除了这6个之外还会执行"/etc/X11/prefdm-nodaemon"这个主要启动X-Window

至此,系统就启动完毕了。以上分析不到的地方还请各位大虾不吝指正。

关于Linux的其他分析内容下次再继续写。 最后附上一张非常完整的系统启动流程图,适合各个水平阶段的读者。

http://blog.itpub.net/8111049/viewspace-680043

http://bbs.chinaunix.net/thread-2046548-1-1.html

http://blog.chinaunix.net/uid-26495963-id-3066282.html