├── .gitignore
├── README.md
├── arch
├── arch-01.md
├── dist-sys-arch.md
└── soft-skill.md
├── kernel
├── bridging.md
├── data-receive.md
├── dpdk-kni-mcast.md
├── images
│ ├── br-ioctl-create.png
│ ├── br-netlink-create.png
│ ├── br-netlink.png
│ ├── bridge-2.png
│ ├── bridge-port-fdb.png
│ ├── bridge.png
│ ├── brif-add-del.png
│ ├── dev-and-port1.png
│ ├── dev-and-port2.png
│ ├── ethernet-802.3.png
│ ├── napi_nonnapi_drivers.jpg
│ ├── vlan-device.png
│ ├── vlan-real-devices.png
│ └── vlan_devices_array.png
├── ipip.md
├── kernel-qemu-gdb.md
├── vlan.md
└── vxlan.md
├── net
└── quic.md
├── netifd
├── images
│ ├── netifd-config.png
│ ├── netifd-objects.png
│ └── netifd-proto-dhcp.png
└── netifd-objects.md
├── ovs
└── ovs-usage.md
└── web
├── all-layers.png
├── h2-bin-frame.png
├── h2-bin-msg.png
├── h2-header-comp.png
├── h2-push.png
├── h2-stream-message-frame.png
├── latency.png
├── multi-req-res.png
├── request-pipeline.png
├── server-con.png
├── tls-handshake.png
├── tls-session-id.png
├── tls.png
└── web-perf.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .*sw[a-z]
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # notes
2 | Notes for software, network, Linux and so on.
3 |
--------------------------------------------------------------------------------
/arch/arch-01.md:
--------------------------------------------------------------------------------
1 | arch-01
2 | -------
3 |
4 | # 00. XXX
5 |
6 | * 思维模式
7 | - 架构设计:判断与取舍;
8 | - 程序设计:逻辑与实现.
9 | * 内容
10 | - 架构基础
11 | - 高性能
12 | - 高可用
13 | - 高可扩展
14 |
15 | # 01. 架构是什么
16 |
17 | ## 基本概念
18 |
19 | ### 系统与子系统
20 |
21 | `系统`
22 | - *关联* 由一群有*关联*的个体组成.不是任意无关的个体.(发动机,PC无关联)
23 | - *规则* 系统内个体按照一定的规则运作.(分工,发动机,变速箱)
24 | - *能力* 系统能力不是个体能力之和,产生新能力.(发动机,轮子..汽车)
25 |
26 | `子系统`
27 | - 也是一群有关联的个体组成的系统
28 | - 是另一个更大的系统的一部分
29 |
30 | ### 模块与组件
31 |
32 | `模块(Module)` 程序+数据结构,一致而有紧密关联的软件组织;模块接口,协作单位.
33 | 逻辑角度划分系统为模块,目的是职责分工. (Login, user-info, ...)
34 |
35 | `组件(Component)` 自包含,可编程,可重用,语言无关,可组装的软件单元.物理角度划分系统为组件,目的是单元复用. (Nginx, DB, LB).
36 |
37 | ### 框架与架构
38 |
39 | `框架(framework)` 实现某标准或特定基本认为的组件规范,及基础软件.(MVC, J2EE, Sprint)
40 | `架构(Architect)` 基础结构,准则,及结构的描述.
41 |
42 | **软件架构** 软件系统的顶层结构
43 | - 包含哪些个体(子系统,模块,组件)
44 | - 运作规则 个体运作,协作规则
45 |
46 | # 02. 架构历史
47 |
48 | > 控制复杂度: 逻辑复杂度,软件复杂度,系统复杂度
49 |
50 | * 结构化程序设计(解决逻辑过于复杂)
51 | - 软件危机,软件工程,人月神话
52 | - 自顶向下,逐步细化,模块化 (面向过程)
53 | * 面向对象(解决可扩展,软件跟不上业务硬件快速变化)
54 | - 考虑复用,可扩展
55 | * 架构设计(解决大系统的组织问题)
56 | - 系统庞大,内部耦合严重,开发效率低
57 | - 系统间耦合严重,影响修改和可扩展
58 | - 系统逻辑复杂,易出问题,难排查
59 |
60 | # 03. 架构目的
61 |
62 | 误区:
63 |
64 | * 为架构而架构(架构重要是为什么?)
65 | - 没有架构系统也能跑;(事实如此)
66 | - 架构不提高开发效率;(事实如此)
67 | - 不促进业务发展.(事实如此)
68 | - 并非所有系统都需要做架构设计.(事实如此)
69 | - 公司要求必须做
70 | - 为了"高性能,高可用,可扩展" (过度设计)
71 | - XXX公司这么做,我们也要这么做
72 | - XXX技术很流行,我们要用起来
73 |
74 | 架构的目的:**解决软件 *系统* 的 *复杂度* 带来的问题**.
75 |
76 | 做好架构,需要:
77 |
78 | * 熟悉和理解需求,识别复杂性所在的地方(找到问题点);针对性的做架构设计.
79 | * 架构不需要面面具到,理解复杂点,才能有所取舍,有的放失.
80 |
81 | # 04. 复杂度来源:追求高性能
82 |
83 | * 单机为提高性能,会带来复杂度:
84 | - 人们对性能,功能,体验的追求;硬件的发展;
85 | - 如何充分利用CPU,内存,网卡,存储,...
86 | - 批处理,多线程/进程,事件驱动/异步编程,SMP,NUMA MPP , Cache
87 | * 多机集群为提高性能,会带来复杂度.
88 | - 业务的发展,移动互联网爆发,双11,红包
89 | - 单纯增加机器不能解决问题
90 | - 任务分配;调度器(LB),
91 | - 任务分解:注册,消息,
92 |
93 | # 05. 复杂度来源:追求高可用
94 |
95 | `高可用` 系统**无中断**执行其功能的能力.
96 |
97 | 单个硬件,单个软件无法做到无中断.通过"冗余"的方法,冗余增加了高可用也带来了复杂性.
98 |
99 | * 计算高可用(无状态)
100 |
101 | 任务分配器+多个无状态服务器模型:
102 |
103 | - 任务分配器
104 | - 连接管理,异常处理(分配器和业务服务器间)
105 | - 分配算法(调度)
106 | - 服务发现管理(ZooKeeper, Memcached)
107 |
108 | * 存储高可用()
109 | - 数据一致性问题(数据+逻辑=业务)
110 | - 数据传输延时问题
111 | - 传输线路的可靠性
112 |
113 | CAP理论,存储不可能同时满足"一致性,可用性,分区容错性",最多只能满足两个,需要取舍.
114 |
115 | * 高可以状态决策 正常,异常状态判断(健康检查), 状态检查不可能完全准确
116 |
117 | `独裁式`
118 |
119 | - 信息搜集者,裁决
120 | - 冗余个体上报状态
121 | 优点:决策不会发生混乱,缺点:决策者是单点,如果对决策者做决策,无限递归.
122 |
123 | `协商式`
124 |
125 | 即主备协商模式:
126 |
127 | 1. 架构简单,不会存在决策者的单点
128 | 2. 连接中断时,可能出现双主,或者无法成为主的情况
129 |
130 | `民主式`
131 |
132 | 多个独立个体投票进行决策,选举leader(ZooKeeper集群,选举算法Paxos)
133 |
134 | 优点:
135 | - 没有独裁式的单点问题,
136 | - 没有协商式的双主问题
137 | 缺点
138 | - 但算法,实现复杂
139 | - 有脑裂问题,集群连接中断,形成两个分隔的集群,各自进行决策导致行为不一致.出现两个主节点.
140 | - 解决脑裂问题可能导致系统可用性下降
141 |
142 | 没有完美的方案.
143 |
144 | # 06. 复杂度来源:追求可扩展性
145 |
146 | 新需求出现时,系统不需要或者仅需要少量修改,无需重构重建。做到 **正确预测变化,完美封装变化**.
147 |
148 | * 软件开发:面向对象(代码复用),设计模式
149 |
150 | ## 预测变化
151 |
152 | * 不能每个点都考虑可扩展(有所取舍,不能过度设计)
153 | * 不能完全不考虑可扩展性
154 | * 要尽量准确预测,但任何预测都可能出错
155 |
156 | ## 应对变化
157 |
158 | > 没有什么事情加一层封装不能解决的。HAL, 中间件, net_device, vfs, ...
159 |
160 | * 将“变化”封装在一个“变化层”
161 | - 将系统拆分成*变化层*和*稳定层*
162 | - 合理设计层与层的*接口*
163 | * 提炼出一个“抽象层”和一个"实现层" (HAL,vfs, 设计模式, 抽象出类,...)
164 |
--------------------------------------------------------------------------------
/arch/dist-sys-arch.md:
--------------------------------------------------------------------------------
1 | 分布式系统架构 (Distributed System Architect)
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 | * SOA面向服务架构:SOAP,WSDL,XML,Bus
34 | - 可重用,模块化,可组合,互操作(component + bus)
35 | - 符合开放标准
36 | - 服务识别,分类,提供,发布,监控,跟踪
37 |
38 | 使用标准协议或中间件联动其他关联的服务(控制反转,依赖倒置)
39 |
40 | * 微服务架构: Docker, k8s, ...
41 | - 每个服务自包含
42 | - 服务编排,整合引擎
43 |
44 | # 解决分布式系统性能问题(高性能,高吞吐量)
45 |
46 | * 缓存系统
47 | - 浏览器缓存,服务器缓存,数据库,文件系统,CPU
48 | - 分布式缓存集群(Proxy负责缓存分片,路由)
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 | - CI/CD:流畅的发布管线,足够的自动化测试,灰度发布。
77 |
78 | # 分布式系统关键技术
79 |
--------------------------------------------------------------------------------
/arch/soft-skill.md:
--------------------------------------------------------------------------------
1 |
2 | 技术领导力(Leadership)
3 | --------
4 |
5 | > 技术领导力(lead) != 管理能力
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 | - C语言、编程范式(面向过程,面向对象,范型编程,函数式)
33 | - 操作系统
34 | - 数据结构算法
35 | * 计算机系统
36 | - 计算机系统原理: 体系结构,总线,内存,缓存,中断,多任务,虚拟化,磁盘
37 | - 操作系统原理和基础:进程,线程,多核,缓存一致性,IPC,内存,文件系统,..
38 | * 网络基础: TCP/IP, HTTP, TLS/SSL, ...
39 | * 数据库原理: SQL, NoSQL
40 | * 分布式技术架构:负载均衡,DNS,多子域名,无状态应用层,缓存,数据库分片,容错,恢复。
41 | * 非同一般的学习能力
42 | - 学习的信息源:完整的学习(书本),Google,论文,开源,文档,开源社区。
43 | - 与高手交流:技术社区,开源项目
44 | - 举一反三的思考:技术类比
45 | - 不怕困难:没有解决不了的技术问题,不要放弃
46 | - 开放的心态:不要待在舒适区,学习新事物,领域
47 | * 坚持做正确的事
48 | - 做正确的事比以正确的方式做事重要(后者方向不对,效率越高离目标越远)
49 | - 做提高效率的事
50 | - 自动化的事
51 | - 学习前沿技术
52 | - 知识密集型的事
53 | - 技术驱动的事
54 | * 不断提高对自己的要求
55 | - 敏锐的技术嗅觉
56 | - 强调实践,学以致用
57 | - Lead by Example:永远在编程,细节
58 |
59 | # 如何成为Leader
60 |
61 | > 技术领导者 != 管理者,经理,而是领头人
62 |
63 | Leader(领头人)被大家追随,通过影响力让大家跟随。而Boss(管理者)的领导力来自职位和威慑。Leader是大家跟我上,Boss是大家给我上。
64 |
65 | * Boss驱动员工,Leader指导员工
66 | - Boss制定时间计划,Push员工完成任务
67 | - Leader:讨论工作细节,规划方向、计划,和员工一起解决难题,完成工作。
68 | * 帮人解决问题
69 | * 被人所依赖
70 | * 保持热情
71 |
--------------------------------------------------------------------------------
/kernel/bridging.md:
--------------------------------------------------------------------------------
1 | Kernel虚拟设备 之 *网桥(Bridging)*
2 | ==================================
3 |
4 | > referencing ULNI, updated with Kernel 4.x
5 |
6 | * [网桥虚拟设备](#br-dev)
7 | * [数据结构](#data-struct)
8 | - [网桥、网桥端口及FDB关系](#br-port-fdb)
9 | - [转发数据库条目:net_bridge_fdb_entry{}](#fdb-entry)
10 | - [网桥端口:net_bridge_port{}](#br-port)
11 | - [网桥私有数据:net_bridge{}](#net_bridge)
12 | * [桥接初始化](#br-init)
13 | * [网桥设备操作](#br-oper)
14 | - [创建网桥设备](#br-create)
15 | - [添加、删除网桥端口](#br-port-add)
16 | - [打开、关闭网桥端口](#br-open-close)
17 | * [入口流量:本地递交与转发](#ingress-forwarding)
18 | - [rx_handler的处理](#rx_handler)
19 |
20 |
21 | #### 网桥虚拟设备
22 |
23 | Linux的网桥是虚拟设备,需要将其他设备(实际或虚拟设备皆可)作为其网桥端口才能工作。将这些设备“绑定”到网桥上,或者说为网桥指定Interface的过程也称为enslave。
24 |
25 |

26 |
27 | 网桥设备的创建使用`brctl`(或者`ip link add type bridge ...`)工具,创建后它并没有任何*网桥端口*(Interface)。
28 |
29 | ```bash
30 | # brctl addbr br0
31 | # brctl show
32 | bridge name bridge id STP enabled interfaces
33 | br0 8000.000000000000 no
34 | ```
35 |
36 | 再为它加上Interface,*网桥ID*通常会根据其第一个接口的MAC地址和默认优先级0x8生成。
37 |
38 | ```bash
39 | # brctl addif br0 eth0
40 | # brctl addif br0 eth1
41 | # brctl show
42 | bridge name bridge id STP enabled interfaces
43 | br0 8000.08002747113f no eth0
44 | eth1
45 | ```
46 |
47 | 如果要让br0开始工作,需要将它打开,可能还需要为它设置个IP地址,作为网桥接口的底层设备(eth0, eth1)不需要IP地址,因为它们在L3是不可见的。设置br0 IP的方式和普通接口是一样的,可以使用`ifconfig`或者`ip`命令。
48 |
49 | ```bash
50 | # ip link set br0 up
51 | # ip addr add 10.0.0.1/24 dev br0 broadcast +
52 | ```
53 |
54 | 我们要注意的是网桥ID的定义:“优先级”+某个端口的MAC地址。而后来的标准中“优先级”其实只用了4位,后面12位作为系统ID扩展。
55 |
56 | ```bash
57 | 4 bits 12 Bits 48 Bits
58 | +------+----------+----------------------------------+
59 | | Prio | SysID Ex | MAC Address |
60 | +------+----------+----------------------------------+
61 | ```
62 |
63 | Prio使用4Bits,那么优先级的增量(每个Prio对应的Extension数目)是4096(2^12)。这样使用一个Prio和MAC标识的网桥表示4096个网桥(实例),它们可以出现在不同的网桥ID空间中。这么做并非随意为之,802.1Q允许的最大VLAN个数就是4096。那么就是说,每个VLAN所在的网桥ID空间,可以使用Prio+MAC标识一个网桥。事实上Cisco私有STP实现对SysID Ex的解释正是VLAN ID。
64 |
65 | 一旦形成了上图所示的网桥,eth0, eth1所连接的Ethernet LAN被连接成一个逻辑LAN。而Eth0,Eth1不需要分配IP地址(它们不需要在L3上可见),只需要为br0分配一个IP地址。当然不是说不能为eth0, eth1设置IP。 但如果果真的为eth0设置了IP(如下图所示,有两个eth0逻辑实例),那其NIC Driver收到包后到底直接交由IP层进行路由,还是交给br0处理,就需要使用ebtables进行流量划分了。
66 |
67 | 
68 |
69 |
70 |
71 | #### 数据结构
72 |
73 | Bridge的实现大多位于net/bridge目录,其中桥接代码涉及的重要数据结构可以在br_private.h中找到。目录中各文件大体完成的功能总结如下。
74 |
75 | ```bash
76 | $ tree -L 1 net/bridge/
77 | net/bridge/
78 | |-- br.c # bridging初始化、退出,和事件处理
79 | |-- br_device.c # 网桥设备操作函数,包括Setup/Free网桥,以及网桥设备的net_device_ops实现。
80 | |-- br_fdb.c # FDB相关函数
81 | |-- br_forward.c # 帧的转发、泛洪
82 | |-- br_if.c # 添加、删除网桥、网桥端口操作“接口”实现
83 | |-- br_input.c # 入口流量的处理,包括控制帧和数据流量。rx_hanlder和两个入口Netfilter HOOK函数。
84 | |-- br_ioctl.c # ioctl操作相关实现,brctl(8)使用ioctl和SYSFS
85 | |-- br_mdb.c
86 | |-- br_multicast.c
87 | |-- br_netfilter.c # Bridge Netfilter(ebtables)的注册和HOOK点回调函数的实现
88 | |-- br_netlink.c # Netlink操作相关实现,ip(8)使用Netlink
89 | |-- br_notify.c # 处理各种netdev_chain的事件(通告),例如端口UP/DOWN/MTU Change等。
90 | |-- br_private.h # Bridging代码相关的 数据结构
91 | |-- br_private_stp.h
92 | |-- br_stp_bpdu.c
93 | |-- br_stp.c
94 | |-- br_stp_if.c # STP “接口”函数实现,也就是API实现。
95 | |-- br_stp_timer.c
96 | |-- br_sysfs_br.c
97 | |-- br_sysfs_if.c
98 | |-- br_vlan.c
99 | |-- Kconfig
100 | |-- Makefile
101 | `-- netfilter # Bridging相关ebtables实现。
102 | ```
103 |
104 | 先看看简单易懂的Bridge ID,Port ID和MAC地址的结构,之前已经提过Bridge ID。Port ID的情况类似,
105 |
106 | ```bash
107 | 4 bits 12 Bits
108 | +------+----------+
109 | | Prio | Port ID |
110 | +------+----------+
111 | ```
112 |
113 | ```C++
114 | typedef struct bridge_id bridge_id;
115 | typedef struct mac_addr mac_addr;
116 | typedef __u16 port_id;
117 |
118 | struct bridge_id
119 | {
120 | unsigned char prio[2];
121 | unsigned char addr[6];
122 | };
123 |
124 | struct mac_addr
125 | {
126 | unsigned char addr[6];
127 | };
128 | ```
129 |
130 |
131 | ##### 网桥、网桥端口及FDB关系
132 |
133 | 下面这张图摘自《深入理解Linux网络技术内幕》(简称ULNI),它很好的解释了网桥相关各个数据结构的关系。虽然因为Kernel的代码自该书出版以来(2.6.12)某些字段发生了变化,但是基本的关系依旧如此。
134 |
135 | 
136 |
137 | 字段变化对`net_device{}`而言,已经没有了字段`br_port`,现在用来标识一个`net_device{}`为网桥端口的字段是`priv_flags`和 `rx_handler_data`。
138 |
139 | ```c++
140 | // net/bridge/br_if.c:
141 | // br_add_if()/netdev_rx_handler_register()
142 | net_device->priv_flag |= IFF_BRIDGE_PORT
143 | net_device->rx_handler = br_handle_frame()
144 | net_device->rx_handler_data = net_bridge_port{}
145 | ```
146 |
147 |
148 | ##### 转发数据库条目:net_bridge_fdb_entry{}
149 |
150 | Bridge维护一个转发数据库(Forwarding Data Base),包含了端口号,在此端口上学习到的MAC地址等信息,用于数据转发(Forwarding)。整个数据库使用Hash表组织,便于快速查找。每个网桥设备的FDB保存在其 `net_bridge->hash`中。
151 |
152 | >`net_bridge{}`位于和`net_device`一同分配的“私有字段”中。
153 |
154 | 每个学到(或静态配置)的MAC由一个数据库的条目,即`net_bridge_fdb_entry{}`结构表示。FDB是*per-vlan*的,因为不同的VLAN的数据转发路径(可由STP生成)可能是不一样的,FDB需要记录VLAN信息。`is_local`表示MAC地址来自本地某个端口,`is_static`表示MAC地址是静态的(由用户配置或来自本地端口),这些地址不会老化。且所有本地的MAC(is_local为1)的MAC总是“静态的”。
155 |
156 | ```c++
157 | struct net_bridge_fdb_entry
158 | {
159 | struct hlist_node hlist; // 哈希表冲突链表节点,头是&net_bridge.hash[i]
160 | struct net_bridge_port *dst; // 条目对应的网桥端口
161 |
162 | unsigned long updated;
163 | unsigned long used; // 引用计数
164 | mac_addr addr;
165 | __u16 vlan_id; // MAC属于哪个VLAN
166 | unsigned char is_local:1, // 是否是来自某个本地端口的MAC,本地端口总是is_static
167 | is_static:1, // 是否是静态配置的MAC
168 | added_by_user:1, // 用户配置
169 | added_by_external_learn:1; // 外部学习
170 | struct rcu_head rcu;
171 | };
172 | ```
173 |
174 | >FDB Per-VLAN的支持是近期(Feb 2013)才加入的,而整个Bridge的打算开始支持VLAN其实也没多久 :)。http://lwn.net/Articles/529743/ ,http://lwn.net/Articles/538877/ 。
175 |
176 | >Briding的VLAN和Kernel由来已久的对于"虚拟VLAN设备"支持(例如eth0.100)不是一个概念。后者只需要在某个现有设备上配置VLAN虚拟设备,并且只进行Tagging/Untagging操作。但如果要在Bridging层面上支持VLAN,以为着要为每个VLAN维护STP实例(不同VLAN稳定后的转发Tree可以是不同的)和FDB,Bridge各端口见的数据转发要遵循VLAN划分,这在之前都是没有实现的。
177 |
178 |
179 | ##### 网桥端口:net_bridge_port{}
180 |
181 | ```c++
182 | struct net_bridge_port
183 | {
184 | ```
185 |
186 | 首先是Layout信息,
187 |
188 | ```c++
189 | struct net_bridge *br; // 所属网桥(反向引用)
190 | struct net_device *dev; // 网桥端口自己的net_device{}结构。
191 | struct list_head list; // 同一个Bridge的各个Port组织在链表dev.port_list中。
192 | ```
193 |
194 | STP相关信息。STP中定义了端口的优先级,STP的各个状态(Disabled,Blocking,Learning,Forwarding)。还有“指定端口”,“根端口”,“指定网桥”的概念。同时还定义了几个定时器。这里保存了这写信息。这里不再复述STP。
195 |
196 | ```c++
197 | /* STP */
198 | u8 priority;// 端口优先级
199 | u8 state; // 端口STP状态:Disabled,Blocking,Learning,Forwarding
200 | u16 port_no; // 端口号,每个Bridge上各个端口的端口号不能改变(不能配置)
201 | unsigned char topology_change_ack;// TCA ?
202 | unsigned char config_pending;
203 | port_id port_id; // 端口ID:Prio+端口号
204 | port_id designated_port;
205 | bridge_id designated_root;
206 | bridge_id designated_bridge;
207 | u32 path_cost;
208 | u32 designated_cost;
209 | unsigned long designated_age;
210 |
211 | struct timer_list forward_delay_timer;// 转发延迟定时器,默认15s
212 | struct timer_list hold_timer; // 控制BPDU发送最大速率的定时器
213 | struct timer_list message_age_timer;// BPDU老化定时器
214 | ```
215 |
216 | Kernel通用信息
217 |
218 | ```c++
219 | struct kobject kobj; // Kernel为了方便一些常用对象操作(添加删除等)建立的基本对象
220 | struct rcu_head rcu;
221 |
222 | unsigned long flags; // 是否处于Flooding,是否Learning,是否被管理员设置了cost等
223 |
224 | ... IGMP Snooping & Netpoll ...
225 |
226 | struct net_port_vlans __rcu *vlan_info;// 在此端口上配置的VLAN信息,例如PVID,VLAN Bitmap, Tag/Untag Map
227 | };
228 | ```
229 |
230 | 网桥端口设备本身对应的net_device{}结构中有一些字段会指示此设备为网桥端口,原先是br_port(v2.6.11)指针,新版的内核则看priv_flag是否设置 IFF_BRIDGE_PORT。如果是网桥端口的话,rx_handler_data指向net_bridge_port{}。这么做的原因自然是尽量让net_device不要放入功能特定的字段。
231 |
232 | ```c++
233 | struct net_device {
234 | ... ...
235 | // 如果是网桥端口IFF_BRIDGE_PORT会被设置。
236 | unsigned int priv_flags; /* Like 'flags' but invisible to userspace.
237 | * See if.h for definitions. */
238 |
239 | ... ...
240 | rx_handler_func_t __rcu *rx_handler;// 创建网桥设备的时候注册为br_handle_frame()
241 | void __rcu *rx_handler_data; // 如果是网桥端口,指向net_bridge_port{}
242 | ... ...
243 | };
244 | ```
245 |
246 | rx_handler是各个per-net_device的入口帧特殊处理的hook点,dev向协议栈(L3)递交skb过程,即netif_receive_skb()的处理过程中,在查询ptype_base完成L2/L3递交前,先检查各个net_device的rx_handler是不是被设置,设置的话会先调用rx_handler。而**网桥端口**设备的rx_handler是被设置的。这个是虚拟网桥如何通过端口设备收包的方式,后面会详细的讨论。
247 |
248 |
249 | ##### 网桥私有数据:net_bridge{}
250 |
251 | 虚拟的网桥本身对于Kernel也是一个网络设备,自然拥有`net_device{}`,而网桥操作相关的信息保存在`net_bridge{}`中。`net_bridge{}`作为(对dev而言)私有信息附属在`net_device{}`之后。创建网桥类型设备的时候`net_bridge{}`作为附属信息由`alloc_netdev()`一起分配。
252 |
253 | ```c++
254 | struct net_bridge
255 | {
256 | spinlock_t lock;
257 | struct list_head port_list; // net_bridge_port{}链表
258 | struct net_device *dev; // 指向网桥设备的net_device{}
259 |
260 | struct pcpu_sw_netstats __percpu *stats; // 统计值,TX/Rx Packet Byte之类
261 | spinlock_t hash_lock;
262 | struct hlist_head hash[BR_HASH_SIZE]; // 转发数据库(FDB)哈希表
263 | ```
264 |
265 | 其中端口设备由port_list连接,FDB是per-bridge的数据库(而且per-vlan),而非Per-port的,故保存在br结构中。考虑到FDB条目数量会比较多,查询频繁,使用Hash表保存。
266 |
267 | > IGMP Snooping和Netfilter相关的不关注。
268 |
269 | ```c++
270 | ... Netfilter 相关...
271 | u16 group_fwd_mask;
272 |
273 | /* STP */
274 | bridge_id designated_root;
275 | bridge_id bridge_id;
276 | u32 root_path_cost;
277 | unsigned long max_age;
278 | unsigned long hello_time;
279 | unsigned long forward_delay;
280 | unsigned long bridge_max_age;
281 | unsigned long ageing_time;
282 | unsigned long bridge_hello_time;
283 | unsigned long bridge_forward_delay;
284 |
285 | u8 group_addr[ETH_ALEN];
286 | u16 root_port;
287 |
288 | enum {
289 | BR_NO_STP, /* no spanning tree */
290 | BR_KERNEL_STP, /* old STP in kernel */
291 | BR_USER_STP, /* new RSTP in userspace */
292 | } stp_enabled;
293 |
294 | unsigned char topology_change;
295 | unsigned char topology_change_detected;
296 |
297 | ... IGMP Snooping ...
298 |
299 | struct timer_list hello_timer;
300 | struct timer_list tcn_timer;
301 | struct timer_list topology_change_timer;
302 | struct timer_list gc_timer;
303 | ```
304 |
305 | 指定端口、网桥ID,路径成本,之类都能在STP协议中找到。我们从stp_enabled标识中看到STP(802.1D)的实现仍然放在Kernel中,而RSTP(Rapid STP)的实现被放在了UserSpace(Kernel以前也没有RSTP的实现)。RSTP的实现可以在这里找到:git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/rstp.git。事实上把某些数据量不大但逻辑相对复杂的控制协议放到应用层的例子还是比较多的,例如IPv6的ND,DHCPv4/DHCPv6,以及未来某些nftables的某些部分。RSTP需要Kernel和Userspace“合作”完成。
306 |
307 | ```c++
308 | struct kobject *ifobj;
309 | u32 auto_cnt;
310 | #ifdef CONFIG_BRIDGE_VLAN_FILTERING
311 | u8 vlan_enabled;
312 | __be16 vlan_proto;
313 | u16 default_pvid;
314 | struct net_port_vlans __rcu *vlan_info; // 网桥设备和网桥端口设备一样,也可视为一个(对L3的)端口,也需要VLAN信息
315 | #endif
316 | };
317 | ```
318 |
319 |
320 | #### 桥接初始化
321 |
322 | 桥接部分初始化和退出的代码定义在`net/bridge/br.c`中,这还有一些事件处理函数。Bridging作为一个内核模块进行初始化。
323 |
324 | ```c++
325 | module_init(br_init)
326 |
327 | static int __init br_init(void)
328 | {
329 | ... ...
330 | err = stp_proto_register(&br_stp_proto);
331 | ... ...
332 | err = br_fdb_init();
333 | ... ...
334 | err = register_pernet_subsys(&br_net_ops);
335 | ... ...
336 | err = br_netfilter_init();
337 | ... ...
338 | err = register_netdevice_notifier(&br_device_notifier);
339 | ... ...
340 | err = br_netlink_init();
341 | ... ...
342 | brioctl_set(br_ioctl_deviceless_stub);
343 |
344 | ... ATM 相关 ...
345 |
346 | return 0;
347 |
348 | ... 出错处理 ...
349 | }
350 | ```
351 |
352 | `br_init()`函数完成的工作有,
353 |
354 | * **注册STP协议处理函数br_stp_rcv**
355 |
356 | 在net/802/stp.c中实现了个通用的STP框架,这个框架又是建立在llc之上(net/llc/),LLC显然是用来处理802.2 LLC层的,我们知道Ethernet II Packet常用于数据传输(尤其是PC端)而802.3 with 802.2 LLC协议通常用来承载STP等控制协议。LLC本身的处理和其他Ethernet PacketType(ARP, IP, IPv6..)没有不同,都是通过dev_add_pack()向netdev的ptype_base注册rcv函数。
357 |
358 | ```bash
359 | netif_receive_skb
360 | +
361 | |- llc_rcv <= ptype_base[ETH_P_802_2]
362 | +
363 | |- br_stp_rcv <= llc_sap->rcv_func
364 | ```
365 |
366 | * **转发数据库初始化**
367 |
368 | 为了效率的考虑`net_bridge_fdb_entry{}`的分配会在kernel cache中进行。这里使用`kmem_cache_create()`初始化一个br_fdb_cache。另外,之前提到FDB Etnry保存在`net_bridge.hash`,为了防止DoS攻击,计算Hash的时候引入一个随机因子让其计算不可预测。该因子也在此处初始化。
369 |
370 | * **注册pernet_operations**
371 |
372 | pernet_operation只注册了.exit函数,作用是在某个网络实例清理的时候,将所有"net"内的的bridge设备、相关Port结构、VLAN结构、Timer和FDB等清理干净。
373 |
374 | * **初始化桥接Netfilter**
375 |
376 | 略。
377 |
378 | * **注册通告链netdev_chain**
379 |
380 | 网桥设备是建立其他网络设备之上的,那些设备的状态(UP/DOWN),地址改变等消息会影响网桥设备(内部数据结构,如端口表,FBD等)。因此需要关注`netdev_chain`。对这些Event的处理由`br_device_event()`完成。
381 |
382 | * **netlink操作初始化**
383 |
384 | Bridging注册了两组Netlink的Operations,分别是AF(AF_BRIDGE)和Link级别的ops。
385 |
386 |
387 | #### 网桥设备操作
388 |
389 | 本节描述网络设备的创建、删除,打开、关闭以及网桥端口的添加、删除。
390 |
391 |
392 | ##### 创建网桥设备
393 |
394 | 一般创建一个新的网络设备分成2个基本步骤:
395 |
396 | * **分配net_device{}并setup**
397 |
398 | 也就是调用`alloc_netdev_mqs(SIZE, NAME, xxx_setup)`。其中 SIZE 是附着在`net_device{}`内存后面的特定数据,对于网桥设备而言就是`net_bridge{}`的大小。`xxx_setup`则是特有设备的初始化过程。`NAME`作为创建接口名的模板,如"eth%d"、"br%d"等,稍后由`register_netdevice()`生成eth1, br0等设备名,也可直接指定。`alloc_netdev()`是alloc_netdev_mqs的wrapper,创建TX/RX队列各一个。分配时注册的`xxx_setup`会在`alloc_netdev_mqs`中被立即调用,用来初始化设备特定数据,我们之前见过`ether_setup`。
399 |
400 | 网桥对应的setup函数为`br_dev_setup()`。和ether_setup简单设置一些ethernet参数不同,`br_dev_setup`完成了许多对网桥设备至关重要的工作,例如为设备指定netdev_ops(即"dev->ndo_xxx",用于后续的open/close/xmit)等。稍后会详细介绍。
401 |
402 | * **注册网络设备**
403 |
404 | 函数`register_netdevice()`生成dev->name、dev->ifindex, 调用`dev.netdev_ops.ndo_init()`初始化设备,初始化输入输出队列,将设备添加到全局(net{})设备列表,一个name为key的Hash `net.dev_name_head`,一个ifindex为key的Hash `net.dev_index_head`和,全局链表`net.dev_base_head`。
405 |
406 | 而创建网桥设备同样遵循上面的步骤。
407 |
408 | 网桥设备的操作方式有多种途径:
409 |
410 | * ioctl,
411 | * sysfs与
412 | * netlink。
413 |
414 | brctl(8)使用了ioctl/sysfs结合的方式,而ip(8)使用了较为新和通用的netlink的方式。而ioctl命令又有新旧之分,例如SIOCGIFBR/SIOCSIFBR和SIOCBRADDBR/SIOCBRDELBR。
415 |
416 | >brctl的代码:
417 | git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/bridge-utils.git
418 | iproute2的代码:
419 | git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/iproute2.git
420 |
421 | 不论使用ioctl,ioctl+sysfs,或者netlink,最终都会使用相同的一组底层Bridge操作接口。
422 |
423 | ###### 使用netlink创建网桥设备
424 |
425 | 初始化网桥模块的时候(`br_init`),调用`br_netlink_init()`向rtnl注册了`br_link_ops`用于后续的netlink操作。
426 |
427 | 
428 |
429 | 当用户使用NETLINK(例如ip命令)创建一个新网桥设备的时候,网桥设备创建过程如下图所示,
430 |
431 | 
432 |
433 |
434 | ###### 使用ioctl创建网桥设备
435 |
436 | 当使用传统的ioctl创建网桥设备,例如brctl命令的时候,则从br_add_bridge()函数开始。可以看到,ioctl和netlink只是call flow略有不同。底层实际上的工作是类似的,区别在于是否利用netlink框架。
437 |
438 | 
439 |
440 |
441 | ###### br_dev_setup()函数
442 |
443 | 不论使用netlink还是传统的ioctl都会调用`alloc_netdev_mqs`,后者会调用setup函数`br_dev_setup`。它的实现在`net/bridge/br_device.c`中。
444 |
445 | ```c++
446 | void br_dev_setup(struct net_device *dev)
447 | {
448 | struct net_bridge *br = netdev_priv(dev);
449 |
450 | eth_hw_addr_random(dev); //生成一个随机的MAC地址
451 | ether_setup(dev);// 虚拟的Bridge是Ethernet类型,进行ethernet初始化(type, MTU,broadcast等)。
452 |
453 | dev->netdev_ops = &br_netdev_ops; // 网桥设备的netdev_ops
454 | dev->destructor = br_dev_free;
455 | dev->ethtool_ops = &br_ethtool_ops;
456 | SET_NETDEV_DEVTYPE(dev, &br_type);// br_type.name = "bridge"
457 | dev->tx_queue_len = 0;
458 | dev->priv_flags = IFF_EBRIDGE;// 标识此设备为Bridge
459 |
460 | dev->features = COMMON_FEATURES | NETIF_F_LLTX | NETIF_F_NETNS_LOCAL |
461 | NETIF_F_HW_VLAN_CTAG_TX | NETIF_F_HW_VLAN_STAG_TX;
462 | dev->hw_features = COMMON_FEATURES | NETIF_F_HW_VLAN_CTAG_TX |
463 | NETIF_F_HW_VLAN_STAG_TX;
464 | dev->vlan_features = COMMON_FEATURES;
465 |
466 | br->dev = dev;
467 | spin_lock_init(&br->lock);
468 | INIT_LIST_HEAD(&br->port_list);//初始化网桥端口链表和锁
469 | spin_lock_init(&br->hash_lock);
470 |
471 | br->bridge_id.prio[0] = 0x80; // 默认优先级
472 | br->bridge_id.prio[1] = 0x00;
473 |
474 | // STP相关初始化
475 | ether_addr_copy(br->group_addr, eth_reserved_addr_base);// 802.1D(STP)组播01:80:C2:00:00:00
476 |
477 | br->stp_enabled = BR_NO_STP;// 默认没有打开STP,不阻塞任何组播包。
478 | br->group_fwd_mask = BR_GROUPFWD_DEFAULT;
479 | br->group_fwd_mask_required = BR_GROUPFWD_DEFAULT;
480 |
481 | br->designated_root = br->bridge_id;
482 | br->bridge_max_age = br->max_age = 20 * HZ; // 20sec BPDU老化时间
483 | br->bridge_hello_time = br->hello_time = 2 * HZ;// 2sec HELLO定时器时间
484 | br->bridge_forward_delay = br->forward_delay = 15 * HZ;// 15sec 转发延时(用于Block->Learning->Forwardnig)
485 | br->ageing_time = 300 * HZ;// FDB 中保存的MAC地址的老化时间(5分钟)
486 |
487 | br_netfilter_rtable_init(br); // Netfilter (ebtables)
488 | br_stp_timer_init(br);
489 | br_multicast_init(br);// 多播转发相关初始化
490 | }
491 | ```
492 |
493 | 先为网桥设备生成一个随机的MAC地址,当bridge的第一个接口被binding的时候,bridge的MAC字段自动转为第一个接口的地址。虚拟网桥设备上ethernet类型,因此会调用`ether_setup()`。
494 |
495 | 每个net_device有一组netdev_ops用来处理设备打开、关闭,传输等,Bridge的net_device_ops内容则更丰富一些,需要ndo_add_save, ndo_fdb_add稍后详细介绍。ethtool可用来查看链接是否UP,以及设备的信息(驱动类型,版本,固件版本,总线等)。
496 |
497 | 开始的时候网桥总是认为自己是根网桥,所有designeated_root设置成自己网桥ID。而一些STP的定时器也需要设置成默认值。有些定时器是双份的,原因是STP的Timer是由Root Bridge通告,而不是使用自己的值。但是自己也可能会成为Root,所以要维护一份自己的定时器值。
498 |
499 |
500 | ##### 添加、删除网桥端口
501 |
502 | 和创建网桥设备一样,为网桥设备添加端口设备,也可以使用ioctl和netlink两种方式。两种方式最终会调用`br_add_if()`。
503 |
504 | 
505 |
506 | ###### br_add_if()函数
507 |
508 | ```C++
509 | int br_add_if(struct net_bridge *br, struct net_device *dev)
510 | ```
511 |
512 | 端口资格检查,有几类设备不能作为网桥端口:
513 | * loopback设备
514 | * 非Ethernet设备
515 | * 网桥设备,即不支持“网桥的网桥”
516 | * 本身是另一个网桥设备端口。每个设备只能有一个Master,否则数据去哪里呢
517 | * 配置为IFF_DONT_BRIDGE的设备
518 |
519 | ```C++
520 | /* Don't allow bridging non-ethernet like devices */
521 | if ((dev->flags & IFF_LOOPBACK) ||
522 | dev->type != ARPHRD_ETHER || dev->addr_len != ETH_ALEN ||
523 | !is_valid_ether_addr(dev->dev_addr))
524 | return -EINVAL;
525 |
526 | /* No bridging of bridges */
527 | if (dev->netdev_ops->ndo_start_xmit == br_dev_xmit)
528 | return -ELOOP;
529 |
530 | /* Device is already being bridged */
531 | if (br_port_exists(dev))
532 | return -EBUSY;
533 |
534 | /* No bridging devices that dislike that (e.g. wireless) */
535 | if (dev->priv_flags & IFF_DONT_BRIDGE)
536 | return -EOPNOTSUPP;
537 | ```
538 |
539 | 如果新的端口设备没有问题,就可以进行分配和初始化`net_bridge_port{}`,这些工作由`new_nbp()`完成。
540 | * 分配一个net_bridge_port{}结构;
541 | * 分配端口ID。
542 | * 初始化端口成本(协议规定万兆、千兆,百兆和十兆的默认成本为2, 4, 19和100),
543 | * 设置端口默认优先级,
544 | * 初始化端口角色(dp)状态(blocking)。
545 | * 启动STP定时器等。
546 |
547 | 网桥设备需要接收所有的组播包,原来此处调用的是 dev_set_promiscuity(dev, 1)让网桥端口(可能是实际设备)工作在混杂模式,这样才能接收目的MAC非此设备的Unicast以及(未join的)所有的Multicast。现在换成了 dev_set_allmulti()。其原因可以在commit 2796d0c648c里找到。
548 |
549 | ```c++
550 | p = new_nbp(br, dev);
551 | if (IS_ERR(p))
552 | return PTR_ERR(p);
553 |
554 | call_netdevice_notifiers(NETDEV_JOIN, dev);
555 |
556 | err = dev_set_allmulti(dev, 1);
557 | if (err)
558 | goto put_back;
559 | ```
560 |
561 | sysfs和kobj
562 |
563 | Kernel为所有的网桥端口建立一个kobj,这样一来可以方便的使用sysfs_ops设置sysfs参数,以及其他对象操作(例如删除对象的时候,release_nbp被调用以删除net_bridge_port结构。通过,kobject_init_and_add/br_sysfs_addif实现p->kobj的初始化和注册等。一旦注册,就可以在/sys/class/net//brif//找到它相应的目录。
564 |
565 | ```c++
566 | err = kobject_init_and_add(&p->kobj, &brport_ktype, &(dev->dev.kobj),
567 | SYSFS_BRIDGE_PORT_ATTR);
568 | if (err)
569 | goto err1;
570 |
571 | err = br_sysfs_addif(p);
572 | if (err)
573 | goto err2;
574 | ```
575 |
576 | 设备从属关系(adjacent)
577 |
578 | ```c++
579 | err = netdev_master_upper_dev_link(dev, br->dev);
580 | if (err)
581 | goto err4;
582 | ```
583 |
584 | 之前介绍各个数据结构关系的时候,提到了端口设备`net_device{}`结构和`net_bridge_port{}`如何关联。即原来通过`net_device->br_port`的联系方式在新的实现中,已经由`priv_flags`和`rx_handler_data`替代。同时也提到了,netif_receive_skb()的L2/L3分用(查询ptype_base)之前,会先调用设备本身的RX处理钩子 dev->rx_handler。
585 |
586 | * 原关系图
587 |
588 | 
589 |
590 | * 新关系图
591 |
592 | 
593 |
594 | `dev->rx_handler`对应网桥端口设备至关重要,它是一个设备从原来普通设备转变为网桥端口后,数据接收路径发生变化的点。设备成为网桥端口后,在L2/L3递交之前就会现有注册的`br_handle_frame`处理。`rx_handler`和`rx_handler_data`的注册由`netdev_rx_handler_register`完成。dev是网桥端口设备的`net_device`。
595 |
596 | > 可以想象网桥设备本身是需要进行L2/L3递交的,故不需要rx_handler!
597 |
598 | ```c++
599 | err = netdev_rx_handler_register(dev, br_handle_frame, p);
600 | if (err)
601 | goto err5;
602 | ```
603 |
604 | 既然是网桥端口那么`dev->priv_flags`被设置上`IFF_BRIDGE_PORT`。同时网桥端口不支持LRO,原因是LRO(Large Receive Offload)适用于目的为Host的Packet,而网桥端口可能会转发数据到其他端口,自然就不能启用这个功能(启用了还会影响GSO)。
605 |
606 | ```c++
607 | dev->priv_flags |= IFF_BRIDGE_PORT;
608 |
609 | dev_disable_lro(dev);
610 | ```
611 |
612 | 添加端口设备到网桥设备端口列表
613 |
614 | 新建完一个新的端口设备,该初始化的也初始化了,现在可以加入到网桥中了。
615 |
616 | ```c++
617 | list_add_rcu(&p->list, &br->port_list);
618 | ```
619 |
620 | 设置驱动(硬件)相关Feature
621 |
622 | 之前对feature标记的设置只在软件的层次上(只是修改了dev->feature等自动),现在需要真正让设备应用这写feature。于是函数 netdev_update_features会调用ndo_set_features设置驱动,并将通告NETDEV_FEAT_CHANGE。
623 |
624 | ```c++
625 | netdev_update_features(br->dev);
626 | ```
627 |
628 | 更新FDB,初始化VLAN
629 |
630 | 网桥设备端口的MAC需要“静态”配置到FDB中,is_local和is_static同时置1。这回答了网桥端口是否有MAC地址的问题。
631 |
632 | ```c++
633 | if (br_fdb_insert(br, p, dev->dev_addr, 0))
634 | netdev_err(dev, "failed insert local address bridge forwarding table\n");
635 | ```
636 |
637 | 初始化网桥端口的VLAN配置,如果Bridge设备有“Default PVID",就将默认PVID设置为端口的PVID并且Untag。
638 |
639 | if (nbp_vlan_init(p))
640 | netdev_err(dev, "failed to initialize vlan filtering on this port\n");
641 |
642 | 重新计算网桥MAC,Bridge ID
643 |
644 | 当一个网桥设备(不是端口设备)刚刚创建的时候,其MAC地址是随机的(见 br_dev_setup,旧实现是空MAC),这也会影响网桥ID(Prio+MAC),没有端口时网桥ID的MAC部分为0。当有个设备作为其端口后,是个合适的机会重新为网桥选一个MAC,并重新计算网桥ID。前提是如果这个端口的MAC合适的话,例如不是0,长度是48Bits,并且值比原来的小(STP中ID小好事,因为其他因素一样的情况下MAC愈小ID愈小,优先级就越高),就用这个端口的MAC。
645 |
646 | ```c++
647 | changed_addr = br_stp_recalculate_bridge_id(br);
648 | ... ...
649 | if (changed_addr)
650 | call_netdevice_notifiers(NETDEV_CHANGEADDR, br->dev);
651 | ```
652 |
653 | 设置设备状态,MTU
654 |
655 | 如果网桥端口设备是UP的,就使能它,设置状态等(如果STP没打开就没有这些步骤了)。
656 | * 状态设置为Blocking,
657 | * 认为自己是Designated Port(暂时)
658 | * 对所有端口重新进行端口角色选择
659 | * 创建端口ID
660 | 这些通过br_stp_enable_port完成,
661 |
662 | ```c++
663 | if (netif_running(dev) && netif_oper_up(dev) &&
664 | (br->dev->flags & IFF_UP))
665 | br_stp_enable_port(p);
666 | ```
667 |
668 | 接下来为新的端口设置MTU,将它设置为整个Bridge设备各个端口的最小MTU;将新端口的MAC地址记录到bridge的FDB中(per VLAN)。通过函数br_fdb_insert插入的fdb表项的is_local和is_static都是1(本地端口嘛)。
669 |
670 | ```c++
671 | dev_set_mtu(br->dev, br_min_mtu(br));
672 |
673 | kobject_uevent(&p->kobj, KOBJ_ADD);
674 |
675 | return 0;
676 |
677 | ... 出错处理,各种rollback ...
678 | }
679 | ```
680 |
681 | br_del_if基本上是br_add_if的逆过程,就不再细说了。注意一下一个端口从Bridge移走的话Bridge的ID也需要重新计算。
682 |
683 |
684 | ##### 打开、关闭网桥设备
685 |
686 | 现在已经知道创建、删除网桥设备以及添加、删除网桥端口时内核都发生了什么。接下来再看看打开关闭网桥设备(例如`ifconfig xxx up`或`ip link set up`)时都有哪些动作发生。
687 |
688 | 网桥设备也是网络设备,也有`dev->ndo_open/close`,所以不管是`ioctl`(brctl)还是`netlink`(ip),最终被调用的是之前在`br_netdev_ops`里面所注册的`br_dev_open`和`br_dev_close`。其实Bridge的`net_device_ops`很多函数都已经看过了。
689 |
690 | ```c++
691 | static const struct net_device_ops br_netdev_ops = {
692 | .ndo_open = br_dev_open, // 本节讲这个
693 | .ndo_stop = br_dev_stop, // 本节讲这个
694 | .ndo_init = br_dev_init, // 本节讲这个
695 | .ndo_start_xmit = br_dev_xmit, // 数据传输
696 | .ndo_get_stats64 = br_get_stats64, // 统计,好理解
697 | .ndo_set_mac_address = br_set_mac_address,// 这个好理解
698 | .ndo_set_rx_mode = br_dev_set_multicast_list,
699 | .ndo_change_mtu = br_change_mtu,
700 | .ndo_do_ioctl = br_dev_ioctl, // 已经提过了
701 | ... netpoll 相关...
702 | .ndo_add_slave = br_add_slave, // 已经提过了
703 | .ndo_del_slave = br_del_slave, // 已经提过了
704 | .ndo_fix_features = br_fix_features, // 已经提过了,见br_add_if
705 | .ndo_fdb_add = br_fdb_add,
706 | .ndo_fdb_del = br_fdb_delete,
707 | .ndo_fdb_dump = br_fdb_dump,
708 | .ndo_bridge_getlink = br_getlink,
709 | .ndo_bridge_setlink = br_setlink,
710 | .ndo_bridge_dellink = br_dellink,
711 | };
712 | ```
713 |
714 | `br_dev_open`自然是用户“up”了这个设备后被调用的。netdev_update_features之前遇到过。`netif_start_queue`打开输出队列,这个和普通设备没有区别(具体参考《UNLI》)。然后是Multicast和STP部分,这就不细说了。`br_dev_close`是`br_dev_open`的反过程。
715 |
716 | ```c++
717 | static int br_dev_open(struct net_device *dev)
718 | {
719 | struct net_bridge *br = netdev_priv(dev);
720 |
721 | netdev_update_features(dev);
722 | netif_start_queue(dev);
723 | br_stp_enable_bridge(br);
724 | br_multicast_open(br);
725 |
726 | return 0;
727 | }
728 | ```
729 |
730 |
731 | #### 入口流量:本地递交与转发
732 |
733 | 和IP协议类似,网桥的数据流有3个方向:
734 |
735 | * 从端口接收,递交到协议栈(ingress)
736 | * 从协议栈发送到端口(egress)
737 | * 端口之间的转发(forwarding)
738 |
739 | 网桥运行STP/RSTP的话,那只有处于`forwarding`状态端口才能转发数据。而只要端口不是`disable`就能进行BPDU的接收、发送。我们假设端口已经进入`Forwarding`,来看看数据接收流程(ingress flow)。
740 |
741 | 虚拟网桥设备的数据接收一定是从其端口设备开始的,不论端口是真是设备或者其他类型的虚拟设备,最后traffic总要到达`netif_receive_skb`。之前提到的添加网桥端口(`br_add_if()`)时会建立 端口结构`net_bridge_port`和端口相关`net_device`的关系,也意味着注册了`dev->rx_handler`(即`br_handle_frame`)和 `rx_handler_data`(即`net_bridge_port{}`)。
742 |
743 | ```c++
744 | br_add_if()
745 | +
746 | \-netdev_rx_handler_register(dev, br_handle_frame, p);
747 | ```
748 |
749 | 我们从网桥端口设备的rx_handler看起。
750 |
751 |
752 | ##### rx_handler的处理
753 |
754 | 之前说到`netif_receive_skb`将数据进行按照`ptype_base`进行L2/L3递交前,会检查dev的`rx_handler`,如果不为空则先调用它,然后根据其的返回决定skb的去向。我们以*网桥端口*设备的视角看看 `__netif_receive_skb_core`的实现。
755 |
756 | > 相关代码片段中的`pt_prev`的作用是什么呢?从`ptype_all/ptype_base`在找到匹配的ptype后(内含递交函数)后先不忙着递交,而是先记录到pt_prev,等到了不得不递交的时候,即skb马上做某些处理了,再递交到上次记录下的pt_prev。那么何苦要那么费劲延迟递交呢?原因是为了减少一次kfree_skb()。http://bbs.chinaunix.net/thread-1933943-1-1.html
757 |
758 | rx_handler的返回值会决定skb的后续去向(从rx_handler_result的注释可以看到清楚的解释),
759 | * RX_HANDLER_CONSUMED
760 | `skb`已经被`rx_handler`消费,不必继续处理,可以释放了。
761 | * RX_HANDLER_ANOTHER
762 | 需要进行另一轮(`another_round`的处理),暗示`skb->dev`被修改,传给了另一个`net_device{}`,(跳转到`__netif_receive_skb_core`较早的位置)再来一次相关流程。。
763 | * RX_HANDLER_EXACT
764 | 强制进行“确定的递交”(Exactly Deliver),不能通配递交。匹配意味着`skb->dev`必须和`ptype->dev`一样。
765 | * RX_HANDLER_PASS
766 | 就像rx_hanlder没有被调用过一样来处理skb。
767 |
768 | 就是说经过`rx_handler`的处理后,`skb`会根据其返回进行后续动作:结束处理,再来一遍,继续ptype_base查询与递交等。。
769 |
770 | ```c++
771 | static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
772 | {
773 | ... ...
774 | another_round:
775 | skb->skb_iif = skb->dev->ifindex;// skb的输入接口是网桥端口的ifindex
776 |
777 | ... VLAN, ptype_all 等 ...
778 |
779 | rx_handler = rcu_dereference(skb->dev->rx_handler);
780 | if (rx_handler) { // 网桥端口设备的rx_handler显然是被注册的
781 | if (pt_prev) {// 不要被pt_prev影响,它是用来优化的,可以忽略这段
782 | ret = deliver_skb(skb, pt_prev, orig_dev);
783 | pt_prev = NULL;
784 | }
785 | switch (rx_handler(&skb)) {// rx_handler处理skb,并通过返回值是后续的处理
786 | case RX_HANDLER_CONSUMED:
787 | ret = NET_RX_SUCCESS; // Bridge的rx_hanlder的数据被修改skb->dev后再次进入netif_receive_skb,原来那个netif_receive_skb返回CONSUMED
788 | goto unlock;
789 | case RX_HANDLER_ANOTHER:// skb->dev已经被修改
790 | goto another_round;
791 | case RX_HANDLER_EXACT:
792 | deliver_exact = true;
793 | case RX_HANDLER_PASS:// 就像没调用过rx_handler一样
794 | break;
795 | default:
796 | BUG();
797 | }
798 | }
799 |
800 | ... VLAN 等 ...
801 |
802 | type = skb->protocol;
803 | list_for_each_entry_rcu(ptype,
804 | &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) { // 协议层递交
805 | if (ptype->type == type &&
806 | (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
807 | ptype->dev == orig_dev)) {
808 | if (pt_prev)
809 | ret = deliver_skb(skb, pt_prev, orig_dev);
810 | pt_prev = ptype;
811 | }
812 | }
813 |
814 | ... ...
815 | }
816 | ```
817 |
818 | 那么问题来了,**网桥端口设备**返回的到底是哪个呢?是不是把skb->dev替换成了**网桥设备**并返回了RX_HANDLER_ANOTHER的方式重走大部分netif_receive_skb呢?我们继续往下看。
819 |
820 | > 其实不是那样 :) 网桥端口返回的是CONSUMED,而网桥端口的处理过程中如果需要向上递交会再次调用netif_receive_skb,而不是返回HANDLER_ANOTHER。
821 |
822 | 5.2 网桥端口设备rx_handler: br_handle_frame
823 |
824 | 好,现在我们看看bridge**端口**的处理函数 br_handle_frame如何处理skb和指示后续操作。该函数位于br_input.c中。
825 |
826 | rx_handler_result_t br_handle_frame(struct sk_buff **pskb)
827 | {
828 | ... ...
829 | if (unlikely(skb->pkt_type == PACKET_LOOPBACK))
830 | return RX_HANDLER_PASS;
831 |
832 | if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
833 | goto drop;
834 |
835 | 网桥端口不打算处理回环数据;源地址必须为合法Ethernet地址:源MAC地址不能是全0,不能是MAC广播和多播,是的话就丢弃。
836 |
837 | skb = skb_share_check(skb, GFP_ATOMIC);
838 | if (!skb)
839 | return RX_HANDLER_CONSUMED;
840 |
841 | 如果skb是共享的,考虑的网桥端口会修改skb,将它clone一份,并将原来那个sk_buff是否(原来的数据不会是否,见sbk如何共享)。
842 |
843 | p = br_port_get_rcu(skb->dev);
844 |
845 | 从net_device{}取出接收skb的网桥端口设备的net_bridge_port{}结构。
846 |
847 | 接下来数据被分为两类:目的地址是Link Local MAC层多播的数据包括了STP的BPDU,和普通数据。
848 |
849 | STP帧(BPDU)和其他保留多播帧
850 |
851 | 首先是Link Local MAC多播的处理。802.1D有组保留的 Link Local 多播MAC地址,他们用于控制协议,如STP。如果接收到了STP但网桥没有开STP协议,就视为普通数据处理;换句话说,就是本网桥当作自己是不认识STP的网桥,例如Hub或不支持STP的Switch。这时需要Flood STP报文到其他端口,而保证那些支持STP网桥则看不到不支持STP设备的存在。对于其他Kernel不支持的管理帧处理方式类似。
852 |
853 | 最后能够在此函数直接递交到Local Host的只能STP功能打开情况下收到的STP帧。递交的时候经过Netfilter的NF_BR_LOCAL_IN的 HOOK点,然后是br_handle_local_finish。br_handle_local_finish的处理实际上不如说是“不处理”,它只是在端口处于Learning的情况下学习个skb的源MAC,并且总是返回0指示包 RX_HANDLER_PASS,由netif_receive_skb继续根据ptype_base处理(STP报文)。
854 |
855 | 所有这段代码对于STP的处理也只是学了个源MAC,然后继续有netif_receive_sbk处理。并没有处理STP帧(BPDU)。
856 |
857 | if (unlikely(is_link_local_ether_addr(dest))) { // MAC Link Local地址通常是管理帧
858 | ... ...
859 | switch (dest[5]) {
860 | case 0x00: /* Bridge Group Address */// 看看STP要怎么弄法,如果真要处理的话不是在这,而是稍后的protocol dipatching(ptype_base)的地方
861 | /* If STP is turned off,
862 | then must forward to keep loop detection */
863 | if (p->br->stp_enabled == BR_NO_STP)
864 | goto forward;// 没开STP,那STP帧就和普通数据帧一样处理
865 | break;
866 |
867 | case 0x01: /* IEEE MAC (Pause) */
868 | goto drop; // MAC Control帧不能通过网桥
869 |
870 | default:// 其他的保留MAC多播和普通数据帧一样处理
871 | /* Allow selective forwarding for most other protocols */
872 | if (p->br->group_fwd_mask & (1u << dest[5]))
873 | goto forward;
874 | }
875 |
876 | //如果能到达这,只有一种情况:STP功能打开的情况下,收到了STP帧
877 | /* Deliver packet to local host only */
878 | if (NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,
879 | NULL, br_handle_local_finish)) { // br_hanle_local_finishq其实只是在Learning状态下学习MAC并返回0
880 | return RX_HANDLER_CONSUMED; /* consumed by filter */
881 | } else { // 通常,NF_HOOK(br_handle_local_finish)返回0,于是STB BPDU到此处“pass”,最后由netif_receive_skb根据ptype_base分发到STP协议层。
882 | *pskb = skb;
883 | return RX_HANDLER_PASS; /* continue processing */
884 | }
885 | }
886 |
887 | 记住,这个函数不会进行STP BPDU的处理!!
888 |
889 | 普通数据帧
890 |
891 | 走到这里的帧要么是普通数据帧,要么是被视为普通数据的控制帧。它们的处理都是一样的,就是当作普通数据处理。
892 | 普通数据帧(非STP帧BPDU),
893 | 没有打开STP功能情况下的STP帧,那么就和普通帧一样处理
894 | 要么就是其他的保留多播(非MAC Control),那么就和普通帧一样处理
895 | ebtable可以改变L2帧在网桥中流向,甚至修改帧,称为ebt_broute,br_should_route_hook就是干这个事情的。如果没有相应的ebtable规则就不会调用rhook。这段代码表明,也只有端口处于“forwarding”状态的时候才能进行数据流向确定与修改。
896 |
897 | forward:
898 | switch (p->state) {
899 | case BR_STATE_FORWARDING:// 这里的重点不是“Forwarding”状态下数据流量如何处理,而是只有“forwarding”状态才能处理ebt_broute,即rhook
900 | rhook = rcu_dereference(br_should_route_hook);
901 | if (rhook) {
902 | if ((*rhook)(skb)) {
903 | *pskb = skb;
904 | return RX_HANDLER_PASS;
905 | }
906 | dest = eth_hdr(skb)->h_dest;
907 | }
908 | /* fall through */
909 | case BR_STATE_LEARNING:
910 | if (ether_addr_equal(p->br->dev->dev_addr, dest))// 如果目的MAC和网桥设备(而不是网桥端口)的MAC相同,标记为PACKET_HOST
911 | skb->pkt_type = PACKET_HOST;
912 |
913 | NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
914 | br_handle_frame_finish);
915 | break;
916 | default:
917 | drop:
918 | kfree_skb(skb);
919 | }
920 | return RX_HANDLER_CONSUMED; // 注意br_handle_frame返回的是consumed
921 | }
922 |
923 | br_handle_frame返回的是consumed,也就是说第一次netif_receive_skb会因为rx_handler返回consumed而结束。
924 |
925 | 不是说非Fowwarding状态不能接受、转发数据吗?怎么就全部到br_handle_frame_finish了呢?先不急,看看br_handle_frame_finish的实现就知道了。
926 |
927 | br_handle_frame分析完了,我们留下两个问题,
928 | STP为什么在该函数中没有被处理,并且还去向了ptype_base的流程。
929 | br_handle_frame_finish是做什么的
930 | 第一个问题其实好理解,STP作为一种特殊类型的Ethernet packet type,注册了自己的packet_type{}。在br_handler_frame的STP处理只是分流一下不该处理的情况(netif_receive_skb的流程做不到这种分流)。正经的STP处理的方法是在稍后查询ptype_base,找到相应的处理函数。
931 |
932 | // net/llc/llc_core.c
933 | static struct packet_type llc_packet_type __read_mostly = {
934 | .type = cpu_to_be16(ETH_P_802_2),
935 | .func = llc_rcv,
936 | };
937 |
938 | 反观普通数据流量,普通NIC收到这些数据时应递交到协议栈,即查询ptype_base然后递交。但设备一旦作为网桥端口,就不能这么处理了,可能需要转发的其他端口什么的,所以才要走br_handler_frame及后续函数。我们看看第二个问题,br_handle_frame_finish接下来是怎么处理普通数据流量(或当作普通数据处理的保留多播流量)的。
939 |
940 | 5.3 数据帧处理:br_handle_frame_finish
941 |
942 | 本节接着br_handle_frame讨论数据帧的处理,这里的数据帧代表非(STP等)控制帧,当然也包括“视为数据帧一同处理”的控制帧(例如STP功能关闭的情况下,BPDU就视为普通数据帧处理)。后面就不再罗嗦了,统一称为“数据帧”或“数据流量”。
943 |
944 | int br_handle_frame_finish(struct sk_buff *skb)
945 | {
946 | const unsigned char *dest = eth_hdr(skb)->h_dest;
947 | struct net_bridge_port *p = br_port_get_rcu(skb->dev);
948 | ... ...
949 | if (!p || p->state == BR_STATE_DISABLED)
950 | goto drop;
951 |
952 | 根据STP,处于Disable状态的端口,认为是“物理”关闭的,什么流量(数据和BPDU)都不接收。
953 |
954 | if (!br_allowed_ingress(p->br, nbp_get_vlan_info(p), skb, &vid)) // br_vlan.c里面的实现是“正体”
955 | goto drop;
956 |
957 | Kernel的Bridge是支持VLAN的,所以在VLAN功能打开的情况下,要看看这个端口设备是不是允许接收skb所在VLAN ID的流量(端口可以配置允许哪些VLAN通过),这点可以通过br_allowed_ingress验证。关于VLAN有一点要注意,如果端口设置了PVID,那么收到的untag的流量属于这个VLAN,并且需要打上tag。
958 |
959 | 另一方面,如果接收到skb已经包含了tag header,而且由没有设置skb->vlan_tci,则需要使用skb_vlan_untag将skb的tag去除,存放到skb->vlan_tci中。
960 |
961 | commit 12464bb8de021a01fa7ec9299c273c247df7f198
962 | Author: Toshiaki Makita
963 | Date: Thu Mar 27 21:46:55 2014 +0900
964 |
965 | bridge: Fix inabillity to retrieve vlan tags when tx offload is disabled
966 |
967 | Bridge vlan code (br_vlan_get_tag()) assumes that all frames have vlan_tci
968 | if they are tagged, but if vlan tx offload is manually disabled on bridge
969 | device and frames are sent from vlan device on the bridge device, the tags
970 | are embedded in skb->data and they break this assumption.
971 | Extract embedded vlan tags and move them to vlan_tci at ingress.
972 |
973 | STP端口如果处于Learning和Forwarding状态,就需要学习新的源MAC(更新FDB)。 net_bridge_port里面有一个 flags,可以设置 BR_LEARNING,不过这个不是端口的状态,只表示端口需要进行学习(处于Learning或者Forwarding这个标记都会被设置,此外端口刚刚建立也会设置它(STP可能把它取消))。表示端口状态的是state字段。FDB是per-VLAN的,因为每个VLAN的STP树的拓扑可能是不同的,所以更新的时候需要VLAN ID。端口如果有PVID,那收到的untag的skb已经被加上了VLAN ID。 br_fdb_update在后面会相信讲。
974 |
975 | 还有一点要注意的是,不光光是MAC需要更新,还需要更新timer;每次收到同MAC的包就重置定时器。
976 |
977 | /* insert into forwarding database after filtering to avoid spoofing */
978 | br = p->br;
979 | if (p->flags & BR_LEARNING)
980 | br_fdb_update(br, p, eth_hdr(skb)->h_source, vid);
981 |
982 | 多播部分暂时先不谈。
983 |
984 | if (!is_broadcast_ether_addr(dest) && is_multicast_ether_addr(dest) &&
985 | br_multicast_rcv(br, p, skb))
986 | goto drop;
987 |
988 | 如果数据来自 br_handle_frame,那么 br_handle_frame_finish被调用的时候端口只能处于两种状态:Learning和Frowarding。所以如果端口是Learning就说明不是Forwarding,学个MAC就行了,不能继续接收数据。
989 |
990 | if (p->state == BR_STATE_LEARNING)
991 | goto drop;
992 |
993 | sbk中有个48Bytes私有字段cb[]供各个Layer自己使用,Bridge代码入口部分用它存放br_input_skb_cb{}。不考虑IGMP Snooping的话,只有一个成员brdev,它实际上保持的是 接收这个数据帧的网桥(不是网桥端口)的net_device{}结构。我们要记住这点,同时再回顾一下另一个标记skb相关设备的地方,skb->skb_iif记录的则是网桥端口的ifindex(见 __netif_receive_skb_core)。
994 |
995 | BR_INPUT_SKB_CB(skb)->brdev = br->dev;
996 |
997 | 接下来是链路层广播、多播和单播的处理,这段代码出现两个skb指针:skb2和原来的skb,他们都可能指向接收的skb,或者为NULL。经处理后,如果skb2不为NULL,则需要递交到本地Host,如果skb不为NULL代表需要转发。
998 | 链路层广播:需要递交到Host,也需要转发(skb和skb2都不是NULL)
999 | 链路层多播:???多播转发、或递交???
1000 | 链路层单播:查询per-VLAN的FDB,
1001 | 如果目的地址是网桥的某个端口MAC之一(在所在VLAN的FDB中可以找到net_bridge_fdb_entry{},且entry->is_local是1)则需要递交到本地(skb2 = skb),此时数据不再转发(skb = NULL)。
1002 | 目的单播不是网桥某个端口的MAC,需要转发,不需要递交
1003 | 除了以上情况 ,还有一种情况必须递交skb到Host:及网桥设置了混杂模式时。这里是网桥,不是网桥端口,网桥端口不用说一定是混杂模式。
1004 |
1005 | /* The packet skb2 goes to the local host (NULL to skip). */
1006 | skb2 = NULL; // 默认不递交(如单播MAC非任何端口的MAC就属于此类)
1007 |
1008 | if (br->dev->flags & IFF_PROMISC)
1009 | skb2 = skb;// 网桥被设置成混杂模式,这必须递交上去
1010 |
1011 | dst = NULL;
1012 |
1013 | if (is_broadcast_ether_addr(dest)) {
1014 | skb2 = skb;// 广播必须递交到Host
1015 | unicast = false;
1016 | } else if (is_multicast_ether_addr(dest)) {
1017 | mdst = br_mdb_get(br, skb, vid);
1018 | if ((mdst || BR_INPUT_SKB_CB_MROUTERS_ONLY(skb)) &&
1019 | br_multicast_querier_exists(br, eth_hdr(skb))) {
1020 | if ((mdst && mdst->mglist) ||
1021 | br_multicast_is_router(br))
1022 | skb2 = skb;
1023 | br_multicast_forward(mdst, skb, skb2);
1024 | skb = NULL;
1025 | if (!skb2)
1026 | goto out;
1027 | } else
1028 | skb2 = skb;
1029 |
1030 | unicast = false;
1031 | br->dev->stats.multicast++;
1032 | } else if ((dst = __br_fdb_get(br, dest, vid)) &&
1033 | dst->is_local) {
1034 | skb2 = skb; // 单播目的是某个端口的MAC(在VLAN内检查),递交到Host
1035 | /* Do not forward the packet since it's local. */
1036 | skb = NULL;
1037 | }
1038 |
1039 | 决定完是不是要转发,是不是要递交到Host,就可以正在的干活了。如果需要转发(skb不为NULL),又在FBI中找到了目的端口,就转发到改端口。否则就flooding。如果需要递交,就调用br_pass_frame_up。
1040 |
1041 | if (skb) {
1042 | if (dst) {
1043 | dst->used = jiffies;
1044 | br_forward(dst->dst, skb, skb2);// 数据转发到FDB查询到的端口
1045 | } else
1046 | br_flood_forward(br, skb, skb2, unicast);// 数据Flood到所有端口
1047 | }
1048 |
1049 | if (skb2)
1050 | return br_pass_frame_up(skb2);// 数据递交到本地Host
1051 |
1052 | ... ...
1053 | }
1054 |
1055 | 顺便提一下,目前为止skb->dev还么有改变,因为不能确定要交换的skb->dev是哪个,如果是本地递交,就会被替换成网桥设备,如果是转发或者flooding则需要换成对应端口设备,而且skb可能还需要再clone。
1056 |
1057 | 5.4 本地递交:br_pass_frame_up
1058 |
1059 | 进入br_pass_frame_up的skb是打算经由Bridge设备,输入到本地Host的。网桥设备本身可以视作一个port,所以离开网桥设备到协议栈的过程就网桥的视角而言属于“egress”。
1060 |
1061 | static int br_pass_frame_up(struct sk_buff *skb)// 通过网桥设备(视为特殊端口),递交到Host协议栈
1062 | {
1063 | ... 统计信息等 ...
1064 |
1065 | 既然数据要离开网桥,就要先看看网桥的VLAN设置只不支持这个VLAN ID。因此,除非被设置成了混杂模式,否则要查看VLAN ID。如果必要skb离开网桥的时候需要加上tag。对比之前的br_handle_frame的时候,那时作为输入Port需要查看br_allowed_ingress,现在skb要通过(网桥设备)离开网桥了,所以要查看br_allowed_egress。稍后转发的时候也是类似的逻辑。
1066 |
1067 | /* Bridge is just like any other port. Make sure the // Bridge设备的处理类似其他网桥端口
1068 | * packet is allowed except in promisc modue when someone
1069 | * may be running packet capture.
1070 | */
1071 | if (!(brdev->flags & IFF_PROMISC) &&
1072 | !br_allowed_egress(br, br_get_vlan_info(br), skb)) {
1073 | kfree_skb(skb);
1074 | return NET_RX_DROP;
1075 | }
1076 |
1077 | 数据包从网桥端口设备进入,经过网桥设备,然后再进入协议栈,其实是“两次经过net_device”,一次是端口设备,另一次是网桥设备。现在数据包离开网桥端口进入网桥设备,需要修改skb->dev字段。
1078 |
1079 | 有3个地方和skb经由的net_device相关
1080 | skb->skb_iif
1081 | 记录是入口网桥端口的ifindex (__netif_receive_skb_core)
1082 | skb->dev
1083 | 起初是网桥端口设备,现在离开网桥端口进入网桥的时候,被替换为网桥设备的net_device。如果设备是TX,或者从一个端口转发的另一个skb->dev也会相应改变。不论数据的流向如何,skb->dev总是指向目前所在的net_device{}。
1084 | BR_INPUT_SKB_CB(skb)->brdev
1085 | 始终是“网桥设备”的net_device。
1086 |
1087 | indev = skb->dev;
1088 | skb->dev = brdev;
1089 |
1090 | 交换dev后重新进行VLAN测试,这次要决定出去的skb是否打tag,如果是untag的,则需要清空skb->vlan_tci。
1091 |
1092 | skb = br_handle_vlan(br, br_get_vlan_info(br), skb);
1093 | if (!skb)
1094 | return NET_RX_DROP;
1095 |
1096 | 递交的最后一步是经过NF_BR_LOCAL_IN钩子点,然后是我们熟悉的netif_receive_skb,只不过这次进入该函数的时候skb->dev已经被换成了Bridge设备。这可以理解为进入了Bridge设备的处理。
1097 |
1098 | return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,
1099 | netif_receive_skb);
1100 | }
1101 |
1102 | 这点是非常重要的,我们之前在查看如何添加网桥端口( br_add_if)的时候,发现创建网桥端口的时候,会调用 netdev_rx_handler_register(dev, br_handle_frame, p);注册rx_handler,但是对网桥设备而言,并没有注册rx_handler,所有网桥设备的数据流量走正常的Protocol Dipatching过程,查询ptype_base并递交到协议层。
1103 |
1104 | 以上过程即刚才所说的两次经过net_device。
1105 |
1106 | Bridge Local In的数据被修改skb->dev后再次进入netif_receive_skb,原来那个netif_receive_skb因为rx_handler返回CONSUMED而结束。
1107 |
1108 | 5.5 数据转发到端口:br_forward
1109 |
1110 | 我们再看看 br_handle_frame_finish的另一个支流,转发支流,首先是转发到单个端口的情况,出现这种精确的转发,意味着FDB里面有目的MAC对应的条目,找到了目的端口。直接转发的某个端口通过函数br_forward。
1111 |
1112 | 先回顾一下br_handle_frame_finish的代码片段,br_forward的第一个参数是刚刚查到的目的网桥端口,第二个参数是要转发的skb,第三个则是要递交的skb,可能为空,当然如果不为NULL,和要转发的是同一个skb。
1113 |
1114 | // br_forward.c: br_handle_frame_finish()
1115 | if (skb) {
1116 | if (dst) {
1117 | dst->used = jiffies;
1118 | br_forward(dst->dst, skb, skb2); // 数据转发到FDB查询到的端口
1119 | } else
1120 | br_flood_forward(br, skb, skb2, unicast); // 数据Flood到所有端口
1121 | }
1122 |
1123 | 注意 br_handle_frame_finish里面的skb,skb2,到了br_forward的参数变成skb, skb0。之所以在转发的时候把“需不需要递交”也作为参数传递进来的原因是,如果skb确实需要同时转发和递交,就需要先clone一份。
1124 |
1125 | 转发前还需要做几个检查,必须同时满足一下条件,
1126 | 不能转发给自己 (ingress/egress端口 不能相同)除非目的端口设置了HAIRPIN模式
1127 | VLAN角度运行数据离开网桥设备
1128 | 端口处于Forwarding状态
1129 | 设置了Hairpin模式的Port可以把frame发给自身。这么做是为了虚拟以太网端口聚集(VEPA)。具体可以参见 http://lwn.net/Articles/347344/ 。
1130 |
1131 | 以上检查由should_deliver()完成。如果确实可以转发,并且同时要递交这个帧(skb0不为NULL),就需要先clone一份,然后转发。deliver_clone是个helper函数,先完成clone,如果成功就调用第二个参数指定的函数来完成转发。最终的递交函数总是__br_forward()。
1132 |
1133 | void br_forward(const struct net_bridge_port *to, struct sk_buff *skb, struct sk_buff *skb0)
1134 | {
1135 | if (should_deliver(to, skb)) {
1136 | if (skb0)
1137 | deliver_clone(to, skb, __br_forward);
1138 | else
1139 | __br_forward(to, skb);
1140 | return;
1141 | }
1142 |
1143 | if (!skb0)
1144 | kfree_skb(skb);
1145 | }
1146 |
1147 | 如果不能转发,同时稍后也不需要递交到本地Host,就把数据帧释放掉。。
1148 |
1149 | static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
1150 | {
1151 | struct net_device *indev;
1152 |
1153 | if (skb_warn_if_lro(skb)) {
1154 | kfree_skb(skb);
1155 | return;
1156 | }
1157 |
1158 | skb = br_handle_vlan(to->br, nbp_get_vlan_info(to), skb);
1159 | if (!skb)
1160 | return;
1161 |
1162 | indev = skb->dev;
1163 | skb->dev = to->dev;
1164 | skb_forward_csum(skb);
1165 |
1166 | NF_HOOK(NFPROTO_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,
1167 | br_forward_finish);
1168 | }
1169 |
1170 | int br_forward_finish(struct sk_buff *skb)
1171 | {
1172 | return NF_HOOK(NFPROTO_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev,
1173 | br_dev_queue_push_xmit);
1174 |
1175 | }
1176 |
1177 | int br_dev_queue_push_xmit(struct sk_buff *skb)
1178 | {
1179 | /* ip_fragment doesn't copy the MAC header */
1180 | if (nf_bridge_maybe_copy_header(skb) ||
1181 | !is_skb_forwardable(skb->dev, skb)) {
1182 | kfree_skb(skb);
1183 | } else {
1184 | skb_push(skb, ETH_HLEN);
1185 | br_drop_fake_rtable(skb);
1186 | dev_queue_xmit(skb);
1187 | }
1188 |
1189 | return 0;
1190 | }
1191 |
1192 | 最终,dev_queue_xmit使得skb通过网桥端口设备出去。
1193 |
1194 | 5.6 Flooding到各个端口:br_flood_forwards
1195 |
1196 | br_flood_forwards只是函数br_flood的包裹函数(据注释说只有在拿到了bridge lock才能调用 - -)。
1197 |
1198 | /* called under bridge lock */
1199 | void br_flood_forward(struct net_bridge *br, struct sk_buff *skb,
1200 | struct sk_buff *skb2, bool unicast)
1201 | {
1202 | br_flood(br, skb, skb2, __br_forward, unicast);
1203 | }
1204 |
1205 | br_flood()遍历每个网桥端口,如果可以的话(满足刚刚说过的should_deliver的要求),就用__packet_hook( __br_forward())转发之。不过函数实现的时候用了一个小技巧,判断为能不能转发后先不急着转发,而是看看下一个端口,如果下一个端口也需要转发,才把数据转发到上次那个要转发到端口。这么做的原因也是减少一次clone。如果没有后续可以转发的端口,就不需要clone了。
1206 |
1207 | static void br_flood(struct net_bridge *br, struct sk_buff *skb,
1208 | struct sk_buff *skb0,
1209 | void (*__packet_hook)(const struct net_bridge_port *p,
1210 | struct sk_buff *skb),
1211 | bool unicast)
1212 | {
1213 | struct net_bridge_port *p;
1214 | struct net_bridge_port *prev;
1215 |
1216 | prev = NULL;
1217 |
1218 | list_for_each_entry_rcu(p, &br->port_list, list) {
1219 | /* Do not flood unicast traffic to ports that turn it off */
1220 | if (unicast && !(p->flags & BR_FLOOD))
1221 | continue;
1222 | prev = maybe_deliver(prev, p, skb, __packet_hook);
1223 | if (IS_ERR(prev))
1224 | goto out;
1225 | }
1226 |
1227 | if (!prev)
1228 | goto out;
1229 |
1230 | if (skb0)
1231 | deliver_clone(prev, skb, __packet_hook);
1232 | else
1233 | __packet_hook(prev, skb);
1234 | return;
1235 |
1236 | out:
1237 | if (!skb0)
1238 | kfree_skb(skb);
1239 | }
1240 |
1241 | skb所在端口是不会被转发的,这个由should_deliver保证。
1242 |
1243 | 此外br_flood也会使用__br_forward最终转发数据帧,和br_forward一样。
1244 |
1245 |
1246 | 6. Egress Traffic: Transmit
1247 |
1248 | 出口流量从Host向 网桥 发送数据开始。数据帧从L3传输到L2(网络设备)的函数为dev_queue_xmit。所以直接调用dev_hard_start_xmit。
1249 |
1250 | 我们先不考虑输出队列、GSO和Bridge Netfilter。Bridge等虚拟设备通常没有输出队列。虽然网桥设备默认是支持GSO的,但是最终GSO将Packet合并后还是会调用br_dev_xmit。Bridge Netfilter我们会在单独章节讨论。
1251 |
1252 | dev_queue_xmit // skb->dev是 网桥设备
1253 | +
1254 | |- dev_hard_start_xmit
1255 | +
1256 | |- netdev_start_xmit
1257 | +
1258 | |- dev->ops.ndo_start_xmit(skb, dev) //br_dev_xmit
1259 |
1260 | 6.1 网桥设备传输
1261 |
1262 | 对于网桥设备而言,ndo_start_xmit就是 br_dev_xmit。从网桥的视角,进入网桥端口(包括从Host进入网桥设备)视为Ingress,离开网桥端口(包括从网桥设备到Host)视为Egress。
1263 |
1264 | netdev_tx_t br_dev_xmit(struct sk_buff *skb, struct net_device *dev)
1265 | {
1266 | ... Netfilter, 统计信息 ...
1267 |
1268 | 设置BR_INPUT_SKB_CB(skb)->brdev为网桥设备,
1269 |
1270 | BR_INPUT_SKB_CB(skb)->brdev = dev;
1271 |
1272 | skb_reset_mac_header(skb);
1273 | skb_pull(skb, ETH_HLEN);
1274 |
1275 | 数据帧要进入网桥设备时,如果帧不带有tag,而网桥设备(网桥设备也可以视作一个端口)有PVID,就需要为数据帧打上PVID所标识的VLAN ID。不论帧原来有tag,还是加上了PVID的tag,都要和网桥的所允许的VLAN,即net_port_vlans.vlan_bitmap,进行比较。只有允许通过才能继续。这些在 br_allow_ingress中完成,注意函数 br_vlan_get_tag返回非0 代表没有tag。 br_allow_ingress就不列了,相对于 br_allow_egress,ingress的时候多一个检查PVID和可能的添加PVID Tag的过程。
1276 |
1277 | 网桥设备net_bridge{}和网桥端口net_bridge_port{}都有各自的net_port_vlans结构,里面标识了各自的PVID,以及允许通过的VLAN ID。
1278 |
1279 | if (!br_allowed_ingress(br, br_get_vlan_info(br), skb, &vid))
1280 | goto out;
1281 |
1282 | 接下来网桥设备要决定将数据帧发往哪些网桥端口了,如果数据帧是
1283 | Ethernet广播,就Flood到所有端口;
1284 | Ethernet多播,如果是IGMP要本地递交一份,否则就查询多播转发数据库,递交到MDB指定的端口,或者如果没有MDB入口,就Flood到所有端口;
1285 | Ethernet单播,查询FDB,如果有Entry就转发到指定端口,否则Flood到所有端口。
1286 | if (is_broadcast_ether_addr(dest)) // 以太网广播
1287 | br_flood_deliver(br, skb, false);
1288 | else if (is_multicast_ether_addr(dest)) { // 以太网多播
1289 | if (unlikely(netpoll_tx_running(dev))) {
1290 | br_flood_deliver(br, skb, false);
1291 | goto out;
1292 | }
1293 | if (br_multicast_rcv(br, NULL, skb)) {
1294 | kfree_skb(skb);
1295 | goto out;
1296 | }
1297 |
1298 | mdst = br_mdb_get(br, skb, vid);
1299 | if ((mdst || BR_INPUT_SKB_CB_MROUTERS_ONLY(skb)) &&
1300 | br_multicast_querier_exists(br, eth_hdr(skb)))
1301 | br_multicast_deliver(mdst, skb);
1302 | else
1303 | br_flood_deliver(br, skb, false);
1304 | } else if ((dst = __br_fdb_get(br, dest, vid)) != NULL) // 以太网单播
1305 | br_deliver(dst->dst, skb);
1306 | else
1307 | br_flood_deliver(br, skb, true);
1308 |
1309 | out:
1310 | ... ...
1311 | }
1312 |
1313 | 至此,网桥设备部分的传输结束,接下来这是网桥端口的传输部分。他们通过br_deliver,br_flood_deliver完成。
1314 |
1315 | 6.2 网桥端口传输
1316 |
1317 | br_deliver是 __br_deliver的包裹函数,它使用 should_deliver查看数据帧能否通过端口外出, should_deliver函数我们之前就见过了,它会检查端口状态是不是处于Forwarding状态,允不许某个VLAN的帧通过。
1318 |
1319 | void br_deliver(const struct net_bridge_port *to, struct sk_buff *skb)
1320 | {
1321 | if (to && should_deliver(to, skb)) {
1322 | __br_deliver(to, skb);
1323 | return;
1324 | }
1325 |
1326 | kfree_skb(skb);
1327 | }
1328 |
1329 | __br_deliver先处理VLAN,如果某个VLAN ID标记为需要Untag,就会将它从数据帧去除。然后将skb->dev设置成网桥端口的 net_device{}。Netpoll我们不讨论。
1330 |
1331 | static void __br_deliver(const struct net_bridge_port *to, struct sk_buff *skb)
1332 | {
1333 | skb = br_handle_vlan(to->br, nbp_get_vlan_info(to), skb);
1334 | if (!skb)
1335 | return;
1336 |
1337 | skb->dev = to->dev;
1338 |
1339 | ... netpoll ...
1340 |
1341 | 然后是BR Netfiter的LOCAL_OUT HOOK点。
1342 |
1343 | NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_OUT, skb, NULL, skb->dev,
1344 | br_forward_finish);
1345 | }
1346 |
1347 | 经过NF_BR_LOCAL_OUT后,数据到达br_forward_finish。br_forward_finish是另一个HOOK点NF_BR_POST_ROUING。
1348 |
1349 | nt br_forward_finish(struct sk_buff *skb)
1350 | {
1351 | return NF_HOOK(NFPROTO_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev,
1352 | br_dev_queue_push_xmit);
1353 |
1354 | }
1355 |
1356 | 经过NF_BR_POST_ROUING,数据到达 br_dev_queue_push_xmit。
1357 |
1358 | 关于Netfilter的HOOK点,我们可以对比一下,
1359 | 对于RX: 到达网桥端口后是PREROUTING,离开网桥端口是LOCAL_IN
1360 | 对于TX: 到达网桥端口后是LOCAL_OUT,离开网桥端口是POSTROUTING
1361 | br_dev_queue_push_xmit时,skb->dev已经替换为了网桥端口的net_device{},而该函数要再次调用 dev_queue_xmit,后者会调用网桥端口“真实”设备的 ndo_start_xmit。
1362 |
1363 | int br_dev_queue_push_xmit(struct sk_buff *skb)
1364 | {
1365 | /* ip_fragment doesn't copy the MAC header */
1366 | if (nf_bridge_maybe_copy_header(skb) ||
1367 | (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb))) {
1368 | kfree_skb(skb);
1369 | } else {
1370 | skb_push(skb, ETH_HLEN);
1371 | br_drop_fake_rtable(skb);
1372 | dev_queue_xmit(skb);
1373 | }
1374 |
1375 | return 0;
1376 | }
1377 |
1378 | 真实设备的 ndo_start_xmit我们就不再累述了。
1379 |
1380 | 6.3 网桥数据流小节
1381 |
1382 | ULNI有个“Big Picture”很好的总结了3个方向的数据流,以及它们经过的Bridge Netfilter HOOK点。其中的RX部分有点小变化,不再有handle_bridge函数,dev->rx_handler直接被注册成br_handle_frame。
1383 |
1384 |
1385 |
1386 | 两次经过net_device{}小节
1387 |
1388 | 再谈谈skb经过两次net_device{}这事。
1389 | 输入路径经过两次net_device{}分别是网桥端口的和网桥设备的,也就是两次调用netif_receive_skb。
1390 | 和输入路径一样,输出的帧同样会经过两次net_device,即先网桥设备后网桥端口,对输出而言的函数是两次调用dev_queue_xmit;
1391 | 如果将这个概念扩展,其实对于转发(forward)的数据帧也是两次经过net_device,两次都是网桥端口的net_device{},函数的话,一次是netif_receive_skb,一次是dev_queue_xmit)。
1392 |
1393 | 7. 转发数据库(FDB)
1394 |
1395 | 转发数据库用于记录MAC地址端口映射。网桥通过地址学习,将学习到的MAC地址和相应端口加入该数据库;网桥端口本身的MAC会被永久的加入到FDB中(br_add_if());用户还可以配置静态的映射。FDB和是否打开STP无关,只不过打开STP后,只有Learning/Forwardnig才会学习。
1396 |
1397 | 记录下的MAC地址(数据库条目)会被更新,并且有老化时间(默认是300秒,也就是5min),如果使用旧STP算法,拓扑变化的时候该老化时间被设置成15秒,如果使用RSTP,FDB中,某端口相关所有条目会被清除。
1398 |
1399 | 虽然之前已经介绍过net_device_fdb_entry{},我们还是罗列一下,
1400 |
1401 | struct net_bridge_fdb_entry
1402 | {
1403 | struct hlist_node hlist; // FDB的各个Entry使用哈希表组织,这个是bucket冲突元素链表节点
1404 | struct net_bridge_port *dst; // 条目对应的网桥端口(没有直接使用端口ID)
1405 |
1406 | struct rcu_head rcu;
1407 | unsigned long updated; // 最后一次更新的时间,会与Bridge的老化定时器比较。
1408 | unsigned long used;
1409 | mac_addr addr; // 条目对应的MAC地址
1410 | unsigned char is_local; // 是否是本地端口的MAC
1411 | unsigned char is_static; // 是否是静态配置的MAC
1412 | __u16 vlan_id; // MAC属于哪个VLAN
1413 | };
1414 |
1415 | 这里重申一下FDB是网桥的属性,因此保存在net_bridge{}中,保存的方式是一个Hash表。
1416 |
1417 | struct net_bridge
1418 | {
1419 | ... ...
1420 | struct hlist_head hash[BR_HASH_SIZE];
1421 | ... ...
1422 | };
1423 |
1424 | FDB条目的添加、删除,查询,更新操作本身想必不会太复杂,无非是哈希表链表操作。关键是搞弄清楚FDB访问和修改的场景。
1425 |
1426 | 7.1 地址老化
1427 |
1428 | 我们知道网桥学到地址都有一个老化的过程。网桥维护了几个超期时间值,包括老化时间br->ageing_time,默认300秒;和转发延迟br->foward_delay,默认15秒。FDB中的每个地址如果自上次跟新(记录于net_bridge_fdb_entry->updated)以来,流逝的时间超过了“保持时间”(由hold_time(),返回可能是老化时间或者短老化时间),地址就需要被删除。hold_time()在正常情况下返回老化时间br->ageing_time,但是如果检测到了拓扑变化,这将老化时间缩短为br->forward_delay,后者也称为“短老化定时器(short aging timer)”。
1429 |
1430 | 7.1.1 注册、打开垃圾收集定时器
1431 |
1432 | 网桥在什么时候检查FDB中的各个地址是否老化、并将老化的地址从FDB中移除呢?Kernel将这个工作交由“垃圾收集定时器”来完成。gc_timer保存在net_bridge{}中。
1433 |
1434 | struct net_bridge
1435 | {
1436 | ... ...
1437 | struct timer_list gc_timer;
1438 | ... ...
1439 | };
1440 |
1441 | 网桥设备被创建并初始化的时候,具体说来是br_dev_setup的时候,通过br_stp_timer_init初始化STP相关的几个定时器,其中包括了垃圾收集定时器。
1442 |
1443 | br_add_bridge
1444 | +
1445 | |- alloc_netdev
1446 | +
1447 | |- br_dev_setup
1448 | +
1449 | |- br_stp_timer_init
1450 | +
1451 | |- ... HELLO Timer ...
1452 | |- ... TCN Timer ...
1453 | |- ... Topology Change Timer ...
1454 | \- setup_timer(&br->gc_timer, br_fdb_cleanup, (unsigned long) br);
1455 |
1456 |
1457 | setup_timer函数将timer->function和timer->data设置为:br_fdb_cleanup和net_bridge{}。要注意的是,不论STP协议是否运行,地址老化(垃圾收集)都是必要的。这里只是设置各个timer的回调函数和私有数据。并没有启动Timer。
1458 |
1459 |
1460 | 在网桥设备打开的时候,br_stp_enable_bridge会把各个timer打开,包括gc_timer,
1461 |
1462 | br_dev_open
1463 | +
1464 | |- br_stp_enable_bridge
1465 | +
1466 | |- ...
1467 | |- mod_timer(&br->gc_timer, jiffies + HZ/10); // gc_timer第一次启动的地方
1468 | |- ...
1469 |
1470 | 第一次打开的时候,在1/10秒后br_fdb_cleanup被调用;此后回调函数br_fdb_cleanup将timer自己设置为每br->aging_time或者“最近的一个条目到期时间”调用。这个timer的实现是值得学习的,因为它不是完全周期性的timer,而是根据条目中需要检查的时间结合一个最大默认周期来进行。
1471 |
1472 | 7.1.2 地址老化处理
1473 |
1474 | 我们看看br_fdb_cleanup()是怎么实现的,顺便也提一下hold_time()。
1475 |
1476 | void br_fdb_cleanup(unsigned long _data)
1477 | {
1478 | struct net_bridge *br = (struct net_bridge *)_data;
1479 | unsigned long delay = hold_time(br);// 地址老化时间,MIN {ageing_time, forward_delay}
1480 | unsigned long next_timer = jiffies + br->ageing_time;// 预设下次收集时间为 ageing_time秒后,稍后可能调整
1481 | int i;
1482 |
1483 | spin_lock(&br->hash_lock);
1484 | for (i = 0; i < BR_HASH_SIZE; i++) {// 遍历所有FDB Hash Bucket
1485 | struct net_bridge_fdb_entry *f;
1486 | struct hlist_node *n;
1487 |
1488 | hlist_for_each_entry_safe(f, n, &br->hash[i], hlist) { // 遍历所有FDB Hash冲突链表
1489 | unsigned long this_timer;
1490 | if (f->is_static)// 静态条目,包括端口地址和用户设置的条目,不会老化、删除。
1491 | continue;
1492 | this_timer = f->updated + delay;// 条目老化到期的时间
1493 | if (time_before_eq(this_timer, jiffies))// 已经到期(到期时间在当前时间之前),就把它删除
1494 | fdb_delete(br, f); // 这就是清除到期FDB Entry的地方
1495 | else if (time_before(this_timer, next_timer))
1496 | next_timer = this_timer;// 如果FDB中的某个条目中默认的下次检查时间之前,就将下次收集时间提前
1497 | }
1498 | }
1499 | spin_unlock(&br->hash_lock);
1500 |
1501 | mod_timer(&br->gc_timer, round_jiffies_up(next_timer));// 设置下次垃圾收集的时间
1502 | }
1503 |
1504 | static inline unsigned long hold_time(const struct net_bridge *br)
1505 | {
1506 | return br->topology_change ? br->forward_delay : br->ageing_time;
1507 | }
1508 |
1509 | 7.2 “本地”FDB条目
1510 |
1511 | 网桥设备、网桥端口设备的MAC地址作为“Local”条目添加到FDB表,其is_local和is_static都需要置1,不会老化。这类FDB Entry通过fdb_insert添加,并且在地址改变的时候,需要做相应的更新。
1512 |
1513 | 从下图我们发现,并没有添加“网桥设备”MAC FDB的地方,这是因为网桥的MAC因默认情况下是其端口之一的地址,因此无需加入FDB。但是如果网桥端口地址改变时则需要更新。
1514 |
1515 |
1516 |
1517 |
1518 | 对于网桥的地址加入,或者不加入FDB对于入口流量的影响,我们应该了解到, 只要帧的目的MAC是网桥或者各个网桥端口的MAC之一,帧就是要被递交到本地Host的。
1519 |
1520 | 了解了何时“插入”本地且静态的网桥端口、网桥的地址后,我们看看fdb_insert的实现,
1521 |
1522 | static int fdb_insert(struct net_bridge *br, struct net_bridge_port *source,
1523 | const unsigned char *addr, u16 vid)
1524 | {
1525 | struct hlist_head *head = &br->hash[br_mac_hash(addr, vid)];// FDB是Per-VLAN的,addr和vid都作为Hash键
1526 | struct net_bridge_fdb_entry *fdb;
1527 |
1528 | if (!is_valid_ether_addr(addr))// 要插入的地址必须是合法的Ethernet地址
1529 | return -EINVAL;
1530 |
1531 | fdb = fdb_find(head, addr, vid);// 在某个VLAN中,地址是否已经存在
1532 | if (fdb) { // 地址已经存在?
1533 | /* it is okay to have multiple ports with same
1534 | * address, just use the first one.
1535 | */
1536 | if (fdb->is_local) // 并且是Local的
1537 | return 0; // 允许多个端口用于同一个地址
1538 | br_warn(br, "adding interface %s with same address "
1539 | "as a received packet\n",
1540 | source ? source->dev->name : br->dev->name);
1541 | fdb_delete(br, fdb);// 但如果地址和分本地地址冲突,就需要将非本地地址的条目删除
1542 | }
1543 |
1544 | fdb = fdb_create(head, source, addr, vid);// 创建新的net_bridge_fdb_entry{},并插入FDB(br->hash)中
1545 | if (!fdb)
1546 | return -ENOMEM;
1547 |
1548 | fdb->is_local = fdb->is_static = 1;// “插入”的地址一定是本地且静态的
1549 | fdb_notify(br, fdb, RTM_NEWNEIGH);
1550 | return 0;
1551 | }
1552 |
1553 | 7.3 地址学习
1554 |
1555 | 除了网桥端口和网桥的MAC地址,用户还能手动添加静态(通过netlink套接字),已经网桥字段学习地址的过程,
1556 |
1557 |
1558 |
1559 |
1560 | fdb_create的实现也不难理解,
1561 |
1562 | static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head,
1563 | struct net_bridge_port *source,
1564 | const unsigned char *addr,
1565 | __u16 vid)
1566 | {
1567 | struct net_bridge_fdb_entry *fdb;
1568 |
1569 | fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC);
1570 | if (fdb) {
1571 | memcpy(fdb->addr.addr, addr, ETH_ALEN);
1572 | fdb->dst = source;
1573 | fdb->vlan_id = vid;
1574 | fdb->is_local = 0;
1575 | fdb->is_static = 0;
1576 | fdb->updated = fdb->used = jiffies;
1577 | hlist_add_head_rcu(&fdb->hlist, head);
1578 | }
1579 | return fdb;
1580 | }
1581 |
1582 | 7.2 FDB初始化,查找
1583 |
1584 | FDB的初始化非常简单,为net_bridge_fdb_entry{}结构初始化一个cache以便快速分配条目。另外还以随机值生成一个salt,这个salt在hash的时候使用,引入随机值可以分散各个Hash键,并且防止DoS攻击。
1585 |
1586 | int __init br_fdb_init(void)
1587 | {
1588 | br_fdb_cache = kmem_cache_create("bridge_fdb_cache",
1589 | sizeof(struct net_bridge_fdb_entry),
1590 | 0,
1591 | SLAB_HWCACHE_ALIGN, NULL);
1592 | if (!br_fdb_cache)
1593 | return -ENOMEM;
1594 |
1595 | get_random_bytes(&fdb_salt, sizeof(fdb_salt));
1596 | return 0;
1597 | }
1598 |
1599 | static inline int br_mac_hash(const unsigned char *mac, __u16 vid)
1600 | {
1601 | /* use 1 byte of OUI and 3 bytes of NIC */
1602 | u32 key = get_unaligned((u32 *)(mac + 2));
1603 | return jhash_2words(key, vid, fdb_salt) & (BR_HASH_SIZE - 1);
1604 | }
1605 |
1606 | FDB的查找有几个函数fdb_find, fdb_find_rcu和__br_fdb_get。前两个用于数据库内部函数的查找,只是为了方便而提炼出来,它们的区别在于有没有RCU保护。__br_fdb_get则用于转发(forwarding)传输(transmit)的时,在Bridge范围内,根据目的地址和所在VLAN,找到外出端口。使用的地方之前已经见过了,实现也并不是很复杂。
1607 |
1608 | struct net_bridge_fdb_entry *__br_fdb_get(struct net_bridge *br,
1609 | const unsigned char *addr,
1610 | __u16 vid)
1611 | {
1612 | struct net_bridge_fdb_entry *fdb;
1613 |
1614 | hlist_for_each_entry_rcu(fdb,
1615 | &br->hash[br_mac_hash(addr, vid)], hlist) {
1616 | if (ether_addr_equal(fdb->addr.addr, addr) &&
1617 | fdb->vlan_id == vid) {
1618 | if (unlikely(has_expired(br, fdb)))
1619 | break;
1620 | return fdb;
1621 | }
1622 | }
1623 |
1624 | return NULL;
1625 | }
1626 |
1627 | 7.3 小节
1628 |
1629 | 虽然br_fdb.c里面的函数起名、和多少有点重复的实现,使得调用关系相对不好理解,但是我们只要脱离细节,抓住一些本质问题,例如地址老化的实现,什么情况下添加、更新和删除FDB条目等,就能较好的理解FDB是如何实现的了。
1630 |
1631 |
1632 |
--------------------------------------------------------------------------------
/kernel/data-receive.md:
--------------------------------------------------------------------------------
1 | Linux Kernel之*帧的接收*
2 | =======================
3 |
4 | * [Kernel和NIC的交互](#irq-poll)
5 | * [中断、下半部和软中断](#IRQ-BH)
6 | - [硬件中断](#hardware-IRQ)
7 | - [下半部](#bottom-Half)
8 | - [网络代码和SoftIRQ](#net-softirq)
9 | * [数据结构](#data-struct)
10 | - [per-CPU结构:softnet_data](#softnet_data)
11 | - [softnet_data初始化](#sd-init)
12 | - [New API结构:napi_struct](#napi_struct)
13 | * [NAPI框架和新、旧驱动程序](#napi-framework)
14 | - [NAPI相关函数](#napi-api)
15 | - [netif_rx函数](#netif_rx)
16 | * [下半部:net_rx_action](#net_rx_action)
17 | * [使用NAPI的设备](#napi-dev)
18 | - [e100初始化和打开](#e100-init-open)
19 | - [e100中断处理函数](#e100-intr)
20 | - [e100的轮询](#e100-poll)
21 | * [使用netif_rx的设备(Non-NAPI)](#non-napi-dev)
22 | * [入口帧的处理:netif_receive_skb](#netif_receive_skb)
23 |
24 | > 通过重读《ULNI》和Kernel实现(目前的版本是4.x)重温网络部分数据帧的接收流程。虽然大部分内容都在《ULNI》中都有,不过2.6.12版本的Kernel已经有些年头了,书中有些描述和现在的实现会有出入(例如`napi_struct{}`的引入,`softnet_data{}`内容的改变等)。借次机会再次复习一下已经遗忘的东西 :-)
25 |
26 |
27 | ### Kernel和NIC的交互
28 |
29 | 帧接收的过程中Kernel和网络设备的交互方式分为*中断(interrupting)*和*轮询(polling)*。
30 |
31 | * **轮询方式**
32 |
33 | 采用Polling的方式,可以定期检查设备寄存器,如果指示有数据,就连续将数据读出。连续读取多个帧的目的在于提高吞吐量(一次轮询只读取一个帧也太低效了)。不过polling会浪费CPU周期,Polling的另一个问题是会造成一定的延迟(从NIC收到数据开始到下次轮询之间会有间隔,最差情况会造成一个轮询间隔(interval)的延迟)。
34 |
35 | * **中断方式**
36 |
37 | 如果要保证低延迟(laytency)则可以考虑中断的方式,保证一有数据就立即通知CPU,然后中断处理函数读取数据帧,存放到输入队列,再通知内核。很显然如果每一帧都触发一个中断,会使得在高负载的情况下CPU忙于中断的处理,无法处理其他任务。考虑到硬件中断的优先级非常高,还会造成输入队列被排满没有机会的情况。
38 |
39 | * **中断中处理多个帧**(旧式网络接收)
40 |
41 | 一种改进的方案是在`一个中断中处理多个帧`。也就是说,当收到一个中断后检查寄存器,如果有数据则持续收取直到收完所有数据,或者达到一个给定的数目(配额)后停止,显然不能因为有数据中断处理就一直不返回。一些旧式的驱动(例如使用`netif_rx()`)的驱动采用了这个模式。
42 |
43 | >一个使用这种(旧式或netif_rx()式)方案的例子是3com的3c59x.c驱动,可以参考《ULNI》或者直接查看源码driver/net/ethernet/3com/3c59x.c。
44 |
45 | 这里只需要理解此类设备如何在一次中断中处理多个帧,先不深入到`vortex_rx()`和`netif_rx()`的实现细节和输入队列等其他话题。
46 |
47 | * **中断与轮询结合(NAPI)**
48 |
49 | 不过更好的方式是中断和轮询的组合(称为New-API,napi方式)。NIC检测到有数据接收通过中断通知Kernel,中断处理函数并不分配skb和读取数据帧,而是将设备(早期是`net_devivce{}`本身现在是相关的napi_struct{})排入队列,然后触发`下半部(bottom-half,BH)`(例如软中断),由下半部异步的方式负责轮询设备一次读取完所有数据或到达配额上限。
50 |
51 |
52 | ### 中断、下半部和软中断
53 |
54 |
55 | #### 硬件中断
56 |
57 | 我们知道中断是为了及时响应外部事件,而一旦处于*中断上下文(interrupt context)*中,
58 |
59 | * 该CPU的其他中断会被关闭(硬中断hardware IRQ不能嵌套)
60 | * CPU也不能执行其他的进程
61 |
62 | 总之,(硬)中断是不能被抢占的(nonpreemptible),也是不可重入的(non-reentrant),这样就能降低竞争条件的发生;这同时也意味着,中断处理函数的工作应该尽快完成。
63 |
64 |
65 | #### 下半部(Bottom-Half,BH)
66 |
67 | 于是中断处理过程被分为“上半部top-half”和“下半部bottom-half”,上半部在中断上下文中执行,下半部则“以异步的方式完成特定的请求”。这样就能把硬中断关闭的时间大幅减少,避免因为上半部执行时间过长而丢失任何数据和信息。
68 |
69 | 下半部的解决方案有,
70 |
71 | * 软中断(SoftIRQ)
72 | * 微任务(tasklet)
73 | * 内核线程(kthread)
74 | * 工作队列(work-queue)
75 |
76 | 前两者不依赖进程环境,可用于执行“不可休眠的任务”;后两个依赖于进程环境,执行期间可以休眠。
77 |
78 | 软中断和微任务都是通过softirq框架实现,它们在并发和上锁上有一些差异,而且软中断的数目(类型)是设计编码时决定的(如`HI_SOFTIRQ`,`NET_RX_SOFTIRQ`等);微任务基于软中断实现,但可以动态添加不同的task。软中断(包括tasklet)所执行的机会有很多,例如,
79 |
80 | 1. 硬中断处理函数(do_IRQ)的末尾,重新打开CPU的中断后,会执行Pending的软中断,这也意味着软中断是可以被硬中断所抢占的。
81 | 2. 从中断和异常事件,系统调用返回
82 | 3. 在CPU上重新打开软中断
83 | 4. 内核线程`run_ksoftirqd()`。如果其他地方都没机会,至少会在这执行一把(也算是兜底?)。命令`ps -e | grep softirq`可以找到per-CPU的softirq线程。
84 |
85 | >考虑到主题是帧的接收以及篇幅限制,软中断和其他下半部的话题可参考《ULNI》《LDD3》等。
86 |
87 |
88 | ### 网络代码和SoftIRQ
89 |
90 | 网络子系统的初始化函数`net_dev_init()`会“注册”两个网络相关的软中断,一个用于发送处理,一个用于接收。
91 |
92 | ```c++
93 | static int __init net_dev_init(void)
94 | {
95 | ... ...
96 | open_softirq(NET_TX_SOFTIRQ, net_tx_action);
97 | open_softirq(NET_RX_SOFTIRQ, net_rx_action);
98 | ... ...
99 | }
100 | ```
101 |
102 | > `net_dev_init()`在系统初始化(boot)期间被调用,系统初始化和宏`subsys_initcall`相关的议题见《ULNI》。
103 |
104 |
105 | ### 数据结构
106 |
107 |
108 | #### per-CPU结构: softnet_data{}
109 |
110 | 网络数据接收中一个非常重要的数据结构是per-CPU变量`softnet_data{}`,简称`sd`。使用Per-CPU变量可以避免上锁提高并发率和吞吐量。新版的`sd`比ULNI中的定义(2.6.12版本内核)复杂了不少,
111 |
112 | ```c++
113 | // net/core/dev.c
114 | DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data);
115 |
116 | // include/linux/netdevice.h
117 | struct softnet_data {
118 | struct list_head poll_list; // 需要ingress轮询的dev列表,“设备”用相关的napi_struct表示
119 | struct sk_buff_head process_queue; // 用于Non-NAPI设备,旧式积压队列,函数process_backlog()把
120 | // skb从sd->input_pkt_queue装异到此处,再进行收取
121 |
122 | ... Stats, RPS, Net FLOW, TX Qdisc ...
123 |
124 | unsigned int dropped; // 丢包统计,例如队列已满
125 | struct sk_buff_head input_pkt_queue; // 用于Non-NAPI设备,驱动分配skb,读取数据,把skb放入该队列
126 | struct napi_struct backlog; // 用于Non-NAPI设备,2.6.12时是 struct net_device backlog_dev;
127 |
128 | };
129 | ```
130 |
131 | > 其中的某些字段和`Receive Packet Steering(RPS)`,RPS通过将接收分组分发到不同的CPU队列进行并发除了而提高PPS,http://lwn.net/Articles/328339/。
132 |
133 | 说明一下旧式的设备驱动指调用`netif_rx()`的设备或者`non-NAPI`设备。先来看看non-napi设备相关的字段,
134 |
135 | * `sd.input_pkt_queue`
136 |
137 | 该队列仅用于non-napi设备。non-API设备会在驱动程序中(往往是中断处理函数)分配skb,从寄存器读取数据到skb,然后调用`netif_rx()`把`skb`排入`sd->input_pkt_queue`。注意,所有non-napi设备共享输入队列,即per-CPU的`sd->input_pkt_queue`。
138 |
139 | * `sd.backlog`
140 |
141 | 用于non-napi设备,为了和NAPI接收架构兼容,所有的non-napi设备使用一个伪`napi_struct`,即这里的`sd.backlog`。NAPI设备把自己的napi_struct{}放入`sd->poll_list`,而所有的non-napi设备在`netif_rx`的时候把`sd->backlog`放入`sd->poll_list`。稍后看到napi架构的下半部(软中断处理)函数`net_rx_action`依次取出排列到`sd->poll_list`的`napi_struct{}`结构,并使用`napi_poll`轮询配额的数据(或不足配额的所有数据)。
142 |
143 | `sd.backlog`是一个内嵌的结构,所有non-napi设备共享它。而它的`sd.backlog.poll`(即`napi.poll`)被初始化为`process_backlog()`,通过这种“共享的伪`napi_struct{}`”方式使得non-napi设备很好的融入了NAPI框架,使得*non-napi*和*NAPI设备*对下半部(`net_rx_action()`)是透明的。不得不说,这是一个值得学习的精巧设计,让我们知道如何向前兼容旧的机制,如何让下层的变化对上层透明。
144 |
145 | * `sd.process_queue`
146 |
147 | 刚才已经提到,non-napi设备把skb放入`sd.input_pkt_queue`,然后在下半部处理中使用`sd.backlog.poll`即`proces_backlog()`函数来处理skb。改函数模拟了NAPI驱动的poll函数行为。作为对比,NAPI设备的poll从设备私有队列读取。Non-NAPI的process_backlog()函数则从non-napi设备共享的输入队列input_pkt_queue中读取配额的数据。然后放入sd.process_queue。
148 |
149 | 然后是一些通用的字段,
150 |
151 | * `sd.poll_list`
152 |
153 | NAPI设备在中断处理函数中将其`napi_struct{}`放入该队列,调度软中断soft-IRQ,稍后有下半部`net_rx_action()`负责轮询该设备。
154 |
155 | > 原本可以忽略本段注释,不过考虑到ULNI使用了旧的内嵌结构`sd.backlog_dev`为了避免阅读时的误解,需要在此说明一下,该字段在2.6.12中的定义是`struct net_device backlog_dev;`。而随着`napi_struct{}`的引入而修改为此。就是说`sd.backlog.poll`原来是`sd.backlog_dev.poll`。
156 |
157 |
158 | #### softnet_data初始化
159 |
160 | per-CPU数据`softnet_data`也在`net_dev_init()`中初始化,注意non-napi使用的`sd.backlog`的初始化设置了统一的poll函数`process_backlog`和配额。
161 |
162 | ```c++
163 | static int __init net_dev_init(void)
164 | {
165 | ... ...
166 | for_each_possible_cpu(i) {
167 | struct softnet_data *sd = &per_cpu(softnet_data, i);
168 |
169 | skb_queue_head_init(&sd->input_pkt_queue);
170 | skb_queue_head_init(&sd->process_queue);
171 | INIT_LIST_HEAD(&sd->poll_list);
172 | ... RPS, TX queue ...
173 | sd->backlog.poll = process_backlog;
174 | sd->backlog.weight = weight_p;
175 | }
176 | ... ...
177 | }
178 | ```
179 |
180 |
181 | #### New API结构: napi_struct{}
182 |
183 | 虽然NAPI很早就已经引入了Kernel,近来kernel将NAPI部分又进行了一定的抽象,定义了`napi_struct{}`。net_device中原来的`dev->poll_list`, `dev->quota`, `dev->weight`也剥离到了`napi_struct{}`中集中处理。
184 |
185 | ```C++
186 | // include/linux/netdevice.h
187 | /*
188 | * Structure for NAPI scheduling similar to tasklet but with weighting
189 | */
190 | struct napi_struct {
191 | struct list_head poll_list; // sd->poll_list元素
192 |
193 | unsigned long state; // NAPI_STATE_XXX
194 | int weight; // 每个NAPI实体试用的配额
195 | int (*poll)(struct napi_struct *, int);
196 | ... GRO, netpoll,timer ...
197 | struct net_device *dev;
198 | struct sk_buff *skb;
199 | struct list_head dev_list;
200 | struct hlist_node napi_hash_node;
201 | unsigned int napi_id;
202 | };
203 | ```
204 |
205 | > NETPOLL则提供了紧急情况下使用通过硬件polling的方式使用网络设备,可以使用它实现netconsole, kgdb-over-ethernet等。
206 |
207 | 两个概念要提一下:`预算(buget)`和`权重(weight)`。`预算`代表了所有要轮询的设备,在一次软件中断中,总共能够处理的帧的输入;限制预算的原因是放在软中断占用CPU时间过长而影响其他进程和软中断(硬件中断不受影响)。除了现在总预算外,为了公平起见,对每个设备也需要分配一定的`权重(weight)`,限制每个设备最大的帧读取量。之前版本的Kernel还有个`配额(quota)`的概念,那时配额代表了设备能够一次性读取的最多帧,而权重(weight)只是quota的增幅。新版的Kernel则直接使用weight。
208 |
209 | > 之前已经提到过,最早引入NAPI(Kernel 2.5)的时候,在`net_device{}`加入了`poll`, `poll_list`, `quota`, `weight`几个字段,随着后来`napi_struct{}`的引入,现在它们都被移动到了`napi_struct{}`中(quota被去除了)。
210 |
211 |
212 | ### NAPI框架和新、旧驱动程序
213 |
214 | ULNI的一张图很好的诠释了NAPI框架和新、旧驱动程序直接的关系。不过新版的Kernel更新了一些napi相关函数,netif_rx()实现也和以前有很大不同。
215 |
216 | 
217 |
218 |
219 | #### NAPI的API
220 |
221 | 先来看看NAPI系列API,
222 |
223 | * `napi_schedule/____napi_schedule`
224 |
225 | `napi_schedule`完成RX的调度工作,它原来叫做`netif_rx_schedule`,而它是____napi_schedule()的包裹函数。函数`____napi_schedule()`(原来叫做`__netif_rx_schedule()`)。它的作用是,
226 |
227 | 1. 把`napi设备`的`napi`结构(对于non-napi设备则是`sd->backlog`)放入设备轮询队列:`sd->poll_list`。
228 | 2. 调度软中断NET_RX_SOFTIRQ
229 |
230 | 如图上所示,NAPI设备驱动一般会直接调用该函数(或变体),而non-napi设备则是通过netif_rx()来调用它。
231 |
232 | * `napi_complete/__napi_complete`
233 |
234 | 当设备驱动(sd->poll,例如e100_poll)完成了对所有接收数据的轮询,就需要把设备(的napi)从sd->poll_list移除,移除过程通过napi_complete或其变体。
235 |
236 | * `netif_napi_add`
237 |
238 | NAPI驱动程序调用该函数设置自己的poll,和权重,此外该函数会初始化设备的napi_struct包括state等。NIC驱动调用该函数的时候,它会把NIC的napi结构放入dev.napi_list以备使用,注意不是放入sd.poll_list那是有数据要轮询才使用的设备列表。
239 |
240 | ```C++
241 | void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
242 | int (*poll)(struct napi_struct *, int), int weight)
243 | {
244 | INIT_LIST_HEAD(&napi->poll_list);
245 | ... ...
246 | napi->timer.function = napi_watchdog;
247 | ... ...
248 | napi->skb = NULL;
249 | napi->poll = poll;
250 | if (weight > NAPI_POLL_WEIGHT) // 权重的设置不能过大
251 | pr_err_once("netif_napi_add() called with weight %d on device %s\n",
252 | weight, dev->name);
253 | napi->weight = weight;
254 | list_add(&napi->dev_list, &dev->napi_list);
255 | napi->dev = dev;
256 | ... ...
257 | set_bit(NAPI_STATE_SCHED, &napi->state);
258 | }
259 | ```
260 |
261 |
262 | #### netif_rx函数
263 |
264 | `netif_rx()`在中断上下文中执行,因此它执行期间关闭CPU的中断,工作完成重新打开。新版的netif_rx实现相对以前简化了许多,尤其是`netif_rx()`不再有拥塞管理部分。
265 | 不考虑RPS,netif_rx函数的工作最终由`enqueue_to_backlog`完成。后者的逻辑非常简单,
266 |
267 | 1. 如果设备的输入队列未被开启(通过测试dev.state是否设置__LINK_STATE_START,输入没有特定的标记)
268 | 1. 如果队列中没有其他数据,先通过`____napi_schedule()`调度NAPI(把`sd.backlog`放入`sd.poll_list`然后调度SoftIRQ)
269 | 2. 把skb放入`sd.input_pkt_queue`尾部(在队列未满或者流控允许的情况下)
270 |
271 | 队列为空要先调度SoftIRQ的原因是,这时non-api共享的`sd.backlog`可能都不在`sd.poll_list`中。因为netif_rx()的执行环境是中断上下文,调度SoftIRQ后它不会被立即执行。
272 |
273 | ```c++
274 | static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
275 | unsigned int *qtail)
276 | {
277 | ... ...
278 | sd = &per_cpu(softnet_data, cpu);
279 |
280 | local_irq_save(flags);
281 |
282 | rps_lock(sd);
283 | if (!netif_running(skb->dev))
284 | goto drop;
285 | qlen = skb_queue_len(&sd->input_pkt_queue);
286 | if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
287 | if (qlen) {
288 | enqueue:
289 | __skb_queue_tail(&sd->input_pkt_queue, skb);
290 | input_queue_tail_incr_save(sd, qtail);
291 | rps_unlock(sd);
292 | local_irq_restore(flags);
293 | return NET_RX_SUCCESS;
294 | }
295 |
296 | /* Schedule NAPI for backlog device
297 | * We can use non atomic operation since we own the queue lock
298 | */
299 | if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
300 | if (!rps_ipi_queued(sd))
301 | ____napi_schedule(sd, &sd->backlog);
302 | }
303 | goto enqueue;
304 | }
305 |
306 | drop:
307 | ... ...
308 | }
309 |
310 | ```
311 |
312 |
313 | ### 下半部:net_rx_action
314 |
315 | `net_rx_action`是软中断`NET_RX_SOFTIRQ`的处理函数。它把`sd->poll_list`中所有设备(对应的napi结构)取出来,依次处理。如果因为总预算超过而未能被轮询的设备,其`napi`结构要重新被加入`sd->poll_list`尾端。此外还有一种情况,就是设备本身的权重被用完了,它们也要再次被加入`sd->poll_list`等待下次`NET_RX_SOFTIRQ`被调度。
316 |
317 | 最后,不论是总预算还是某些设备weight耗尽的关系,本次执行未能完成所有设备及其所有数据的接收,需要重新调度`NET_RX_SOFTIRQ`。
318 |
319 | ```c++
320 | static void net_rx_action(struct softirq_action *h)
321 | {
322 | struct softnet_data *sd = this_cpu_ptr(&softnet_data);
323 | unsigned long time_limit = jiffies + 2;
324 | int budget = netdev_budget;
325 | LIST_HEAD(list); // 待轮询的所有设备,包括了预算用完后未被轮询到的设备
326 | LIST_HEAD(repoll); // 记录那些因为自己的权重配额被而需要下次轮询的设备。
327 |
328 | // 先把所有设备(napi)从poll队列中取出来,如果buget超限,剩下的设备会再次放入尾端。
329 | local_irq_disable();
330 | list_splice_init(&sd->poll_list, &list);
331 | local_irq_enable();
332 |
333 | // 遍历每个设备的napi,直到所有设备都处理完,或者预算用完
334 | for (;;) {
335 | struct napi_struct *n;
336 |
337 | if (list_empty(&list)) {
338 | if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
339 | return;
340 | break;
341 | }
342 |
343 | n = list_first_entry(&list, struct napi_struct, poll_list);
344 | budget -= napi_poll(n, &repoll); // 由设备的->poll自己确认是不是数据已经全部读取或者权重用完了
345 |
346 | // 预算已经用完了
347 | /* If softirq window is exhausted then punt.
348 | * Allow this to run for 2 jiffies since which will allow
349 | * an average latency of 1.5/HZ.
350 | */
351 | if (unlikely(budget <= 0 ||
352 | time_after_eq(jiffies, time_limit))) {
353 | sd->time_squeeze++;
354 | break;
355 | }
356 | }
357 |
358 | local_irq_disable();
359 |
360 | // 以下设备要重新被放入sd->poll_list
361 | // 1. 因为预算用完,list中尚未处理的设备,
362 | // 2. 或者设备本身权重用完而被放入repoll的设备
363 | list_splice_tail_init(&sd->poll_list, &list);
364 | list_splice_tail(&repoll, &list);
365 | list_splice(&list, &sd->poll_list);
366 |
367 | // 不论是总预算还是设备weight的关系,本次执行未能完成所有设备及其所有数据的接收,重新调度NET_RX_SOFTIRQ
368 | if (!list_empty(&sd->poll_list))
369 | __raise_softirq_irqoff(NET_RX_SOFTIRQ);
370 |
371 | net_rps_action_and_irq_enable(sd);
372 | }
373 |
374 | static int napi_poll(struct napi_struct *n, struct list_head *repoll)
375 | {
376 | ... ...
377 | list_del_init(&n->poll_list);
378 | ... ...
379 | weight = n->weight;
380 |
381 | /* This NAPI_STATE_SCHED test is for avoiding a race
382 | * with netpoll's poll_napi(). Only the entity which
383 | * obtains the lock and sees NAPI_STATE_SCHED set will
384 | * actually make the ->poll() call. Therefore we avoid
385 | * accidentally calling ->poll() when NAPI is not scheduled.
386 | */
387 | work = 0;
388 | if (test_bit(NAPI_STATE_SCHED, &n->state)) {
389 | work = n->poll(n, weight);
390 | trace_napi_poll(n);
391 | }
392 | WARN_ON_ONCE(work > weight);
393 |
394 | if (likely(work < weight))
395 | goto out_unlock;
396 |
397 | /* Drivers must not modify the NAPI state if they
398 | * consume the entire weight. In such cases this code
399 | * still "owns" the NAPI instance and therefore can
400 | * move the instance around on the list at-will.
401 | */
402 | if (unlikely(napi_disable_pending(n))) {
403 | napi_complete(n);
404 | goto out_unlock;
405 | }
406 |
407 | ... GRO ...
408 |
409 | /* Some drivers may have called napi_schedule
410 | * prior to exhausting their budget.
411 | */
412 | if (unlikely(!list_empty(&n->poll_list))) {
413 | pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
414 | n->dev ? n->dev->name : "backlog");
415 | goto out_unlock;
416 | }
417 |
418 | list_add_tail(&n->poll_list, repoll);
419 |
420 | out_unlock:
421 | netpoll_poll_unlock(have);
422 |
423 | return work;
424 | }
425 | ```
426 |
427 |
428 | ### 使用NAPI的设备
429 |
430 | ULNI提到那个时代使用NAPI的驱动并不多,但现在情况已经不同了。除了新驱动使用NAPI,原本使用netif_rx的Intel e100也加入了NAPI的阵营。我们以e100为例看看NAPI设备帧的接收过程。帧的接收从NIC的中断开始,中断处理函数在中断上下文运行。e100的中断处理函数为`e100_intr()`。
431 |
432 |
433 | #### e100初始化和打开
434 |
435 | 我们只关注e100初始化过程中和数据接收相关的部分,主要是NAPI和中断处理函数部分。PCI和网络设备初始化和网络设备打开等详细讨论见ULNI。
436 |
437 | e100是PCI 网卡,网络设备包括e100的驱动,以Kernel模块方式编写。模块初始化(链接到Kernel或者动态加载)的时候,注册PCI driver,当PCI自动扫描到设备的时候driver的probe函数,也就是e100_probe就会被调用。e100_probe()分配net_device{},注册网络设备。同时也会注册napi.poll,设置权重,netif_napi_add会初始化相关的napi结构设置相应字段,并将其加入&dev->napi_list待稍后使用,之前已经提到过了。
438 |
439 | ```c++
440 | static int e100_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
441 | {
442 | ... ...
443 | netif_napi_add(netdev, &nic->napi, e100_poll, E100_NAPI_WEIGHT);
444 | }
445 | ```
446 |
447 | 另一方面使用设备前要打开该设备,用户通过ifconfig或ip命令打开一个设备时dev_open()被调用,它会设置IFF_UP和然后通过NotifyChain通告Kernel的其他模块(或者通过netlink通知应用层)。dev_open会调用dev.ndo_open()。对于e100而言则是e100_probe时注册的e100_open()。e100_open进而调用e100_up()。后者会安装中断处理函数e100_intr()。
448 |
449 | ```c++
450 | static int e100_up(struct nic *nic)
451 | {
452 | ... ...
453 | if ((err = e100_hw_init(nic)))
454 | goto err_clean_cbs;
455 | ... ...
456 | if ((err = request_irq(nic->pdev->irq, e100_intr, IRQF_SHARED,
457 | nic->netdev->name, nic->netdev)))
458 | goto err_no_irq;
459 | netif_wake_queue(nic->netdev);
460 | napi_enable(&nic->napi);
461 | /* enable ints _after_ enabling poll, preventing a race between
462 | * disable ints+schedule */
463 | e100_enable_irq(nic);
464 | return 0;
465 | ... ...
466 | }
467 |
468 | ```
469 |
470 |
471 | #### e100中断处理函数
472 |
473 | e100的中断处理函数e100_intr()。
474 |
475 | 1. 现实读取状态寄存器
476 | 2. 因为中断是共享的,所有共享中断的驱动要判断中断是否是自己的,这点通过寄存器判断
477 | 3. 写寄存器应答中断(清除中断)
478 | 4. 如果可以的话,使用__napi_schedule调度NAPI,它把e100的napi结构&nic->napi放入sd.poll_list然后调度软中断
479 | 5. 在下半部处理完本设备的poll前关闭中断。
480 |
481 | NIC中断的关闭不会导致数据丢失,因为数据会被保存到NIC的私有队列中,只是关闭中断不会通知CPU调用处理函数;但是下半部还是会通过轮询的方式把数据从NIC队列中读出。
482 |
483 | > NIC一般会有一个FIFO队列,可通过读寄存器或者DMA方式从该队列把数据取出。
484 |
485 | 稍后NET_RX_SOFTIRQ的处理函数net_rx_action会调用e100的poll函数,
486 |
487 | ```C++
488 | static irqreturn_t e100_intr(int irq, void *dev_id)
489 | {
490 | ... ...
491 | u8 stat_ack = ioread8(&nic->csr->scb.stat_ack);
492 | ... ...
493 | if (stat_ack == stat_ack_not_ours || /* Not our interrupt */
494 | stat_ack == stat_ack_not_present) /* Hardware is ejected */
495 | return IRQ_NONE;
496 |
497 | /* Ack interrupt(s) */
498 | iowrite8(stat_ack, &nic->csr->scb.stat_ack);
499 |
500 | /* We hit Receive No Resource (RNR); restart RU after cleaning */
501 | if (stat_ack & stat_ack_rnr)
502 | nic->ru_running = RU_SUSPENDED;
503 |
504 | if (likely(napi_schedule_prep(&nic->napi))) {
505 | e100_disable_irq(nic);
506 | __napi_schedule(&nic->napi);
507 | }
508 |
509 | return IRQ_HANDLED;
510 | }
511 |
512 | ```
513 |
514 |
515 | #### e100的轮询
516 |
517 | 调用`napi.poll`函数时指定预算buget(在`net_rx_action/napi_poll`阶段指widget),认为完成后函数返实际完成量。如果**没有**完成了所有指定的工作量,意味着NIC私有队列(FIFO)中已经没有数据了。此时就会调用`napi_complete`把自己的napi结构从`sd.poll_list`退出;同时打开NIC的中断,等有新数据后通知Kernel(之前已经提到,关闭中断并不会阻止NIC接收数据到私有的FIFO。
518 |
519 | ```c++
520 | static int e100_poll(struct napi_struct *napi, int budget)
521 | {
522 | struct nic *nic = container_of(napi, struct nic, napi);
523 | unsigned int work_done = 0;
524 |
525 | e100_rx_clean(nic, &work_done, budget);
526 | e100_tx_clean(nic);
527 |
528 | /* If budget not fully consumed, exit the polling mode */
529 | if (work_done < budget) {
530 | napi_complete(napi);
531 | e100_enable_irq(nic);
532 | }
533 |
534 | return work_done;
535 | }
536 | ```
537 |
538 |
539 | ### 使用netif_rx的设备(Non-NAPI)
540 |
541 | 3Com公司的3c59x网卡使用旧式的接口,即`netif_rx`。代码在driver/net/ethernet/3com/3c59x.c。中断处理函数vortex_interrupt()调用vortex_rx,后者会
542 |
543 | 1. 分配skb,
544 | 2. 使用DMA或者读寄存器(内部FIFO)的方式调用把数据复制到skb,
545 | 3. 调用netif_rx()。
546 |
547 | netif_rx()之前已经详细了解过了,它把共享的sd.backlog放入sd.poll_list,然后调度软中断。SoftIRQ处理函数net_rx_action调用的sd.poll是sd.backlog.poll即process_backlog。我们来看看process_backlog的实现。
548 |
549 | 它总把`sd->input_skb_queue`的数据整体挪到`sd->process_queue`然后再处理。每次调用先处理`process_queue`的数据,并计算quota,如果达到了配额就不再进行处理。如果处理完了还没完成配额,就把`input_pkt_queue`的数据挪到`process_queue`尾部。然后继续处理`process_queue`。
550 |
551 | > 用两个queue的原因是和锁有关么?
552 |
553 | ```c++
554 | static int process_backlog(struct napi_struct *napi, int quota)
555 | {
556 | ... ...
557 | napi->weight = weight_p;
558 | local_irq_disable();
559 | while (1) {
560 | struct sk_buff *skb;
561 |
562 | while ((skb = __skb_dequeue(&sd->process_queue))) {
563 | rcu_read_lock();
564 | local_irq_enable();
565 | __netif_receive_skb(skb);
566 | rcu_read_unlock();
567 | local_irq_disable();
568 | input_queue_head_incr(sd);
569 | if (++work >= quota) {
570 | local_irq_enable();
571 | return work;
572 | }
573 | }
574 |
575 | rps_lock(sd);
576 | if (skb_queue_empty(&sd->input_pkt_queue)) {
577 | /*
578 | * Inline a custom version of __napi_complete().
579 | * only current cpu owns and manipulates this napi,
580 | * and NAPI_STATE_SCHED is the only possible flag set
581 | * on backlog.
582 | * We can use a plain write instead of clear_bit(),
583 | * and we dont need an smp_mb() memory barrier.
584 | */
585 | napi->state = 0;
586 | rps_unlock(sd);
587 |
588 | break;
589 | }
590 |
591 | skb_queue_splice_tail_init(&sd->input_pkt_queue,
592 | &sd->process_queue);
593 | rps_unlock(sd);
594 | }
595 | local_irq_enable();
596 |
597 | return work;
598 | }
599 | ```
600 |
601 |
602 | ### Ingress帧的处理:netif_receive_skb
603 |
604 | 不论是NAPI NIC自己实现的poll函数(例如`e100_poll/e100_rx_indicate`)还是non-napi统一使用的`process_backlog`,最终它们都会调用`netif_receive_skb`完成接收过程最后的处理。`netif_receive_skb`只是`__netif_receive_skb`的包裹函数,后者又是`__netif_receive_skb_core`的包裹函数。作为Ingress处理最后的阶段,它实现了,
605 |
606 | 1. 将skb从L2向L3递交;或者,
607 | 2. 将skb虚拟设备递交,如VLAN、Bridge设备。
608 |
609 | #### Ethernet帧类型和eth_type_trans函数
610 |
611 | 有一点要说明,设备驱动分配完skb后都会使用eth_type_trans()设置skb->protocol。例如旧驱动3c59x在调用netif_rx前,或者NAPI设备e100在其poll函数实现的时候。且这一步总是在`netif_receive_skb`前完成。
612 |
613 | ```C++
614 | skb->protocol = eth_type_trans(skb, dev)
615 | ```
616 |
617 | `eth_type_trans`会,
618 |
619 | Ethernet的出现早于IEEE 802.3的标准,并且IEEE 802.3向后兼容。方便描述,我们把802.3之前的Ethernet称为ethernet-II类型帧。Ethernet-II和802.3新定义帧的区别可以通过检查帧的type/length字段,如果字段>1536(0x0600),就是传统ethernet-II帧(虽然802.3也包括它);如果字段<1500,就是802.3新定义的帧;中间值为非法。对应IEEE定义的的802.3帧(>1500部分),又分为无协议类型的RAW 802.3和带逻辑链路控制(802.2 LLC)的802.3,例如STP就使用802.3 with 802.2。带LLC的情况下还能带有802.2 SNAP。RAW 802.3只有IPX使用。
620 |
621 | > Ethernet-II是DIX2.0 (DEC/Intel/Xerox)的说法,Cisco的相应的说法为ARPA。
622 |
623 | 下图总结了上面所说的各个类型的帧。
624 |
625 | 
626 |
627 | 传统Ethernet-II的常见类型有:
628 |
629 | |type|protocol|
630 | |-------|:--------|
631 | |0x0800 | IPv4 |
632 | |0x0806 | ARP |
633 | |0x86DD | IPv6 |
634 | |0x8100 | 802.1q VLAN |
635 |
636 | 因为现实中只有IPX使用RAW的802.3(不带802.2 LLC),因此Kernel使用ETH_P_802_3表示这种IPX使用的RAW 802.3,而ETH_P_802_2表示带有LLC的802.3。
637 |
638 | eth_type_trans函数完成的工作是,
639 |
640 | 1. 设置skb->dev
641 | 2. 设置skb->pkt_type,PACKET_HOST/OTHERHOST/MULTICAST/BROADCAST等
642 | 3. 根据Ethernet的type/length字段(与1536, 0x0600比较)判断它是
643 |
644 | 于是这个函数的返回值是:
645 | * eth->h_proto: 总是大于1536,如0x0800, 0x8100 (VLAN)
646 | * ETH_P_802_3: RAW 802.3 for IPX only
647 | * ETH_P_802_2: 802.3 with 802.2 (LLC)
648 | 这个值作为返回值ETH_P_802_3或ETH_P_802_2,设置到skb->protocol。
649 |
650 | > Kernel中的eth_proto_is_802_3函数其实是指,type/length作为type使用,也就是说帧是ethernet-II类型。
651 |
652 | #### 实际设备和虚拟设备
653 |
654 | 实际设备和虚拟设备,或者虚拟设备和虚拟设备之间总是有上下的串联关系。意味着从RX的角度,skb会先后通过不同设备,每次通过的时候`skb->dev`会设置为当前设备的`net_device`结构。
655 |
656 | #### __netif_receive_skb_core函数
657 |
658 | ```C++
659 | static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
660 | ```
661 |
662 | 函数非常长,因此分段描述,
663 |
664 | ```C++
665 | orig_dev = skb->dev;
666 |
667 | skb_reset_network_header(skb);
668 | if (!skb_transport_header_was_set(skb))
669 | skb_reset_transport_header(skb);
670 | skb_reset_mac_len(skb);
671 |
672 | pt_prev = NULL;
673 |
674 | another_round:
675 | skb->skb_iif = skb->dev->ifindex;
676 | ... ...
677 | ```
678 |
679 | 首先记录下本次调用时skb所在的dev,如果只是从物理网卡接收,稍后递交到L3的,那情况情况比较简单就是NIC的net_dev。但如果skb要经过虚拟设备,例如VLAN和Bridge,skb->dev会在虚拟设备的驱动中改为虚拟设备的dev。
680 |
681 | 设置L2,L3 header的位置(偏移)和MAC地址长度以备用,同时记录当前设备的ifindex。
682 |
683 | ```C++
684 | if (skb->protocol == cpu_to_be16(ETH_P_8021Q) ||
685 | skb->protocol == cpu_to_be16(ETH_P_8021AD)) {
686 | skb = skb_vlan_untag(skb);
687 | ... ...
688 | }
689 | ```
690 |
691 | 这里保存并剥去一个VLAN tag。也就是帧类型为802.1q VLAN (0x8100)或者802.1ad QinQ (0x88A8)的情况,如果是QinQ只处理前一个VLAN tag。802.1q VLAN有4个Bype,前2 Byte称为Tag Protocol Identifier(TPI)它和Ethernet-II的type字段复用;且已经保存到了skb->protocol。后2 Byte为Tag Control Info(TCI),包含了Priority和VID。`skb_vlan_untag`把VLAN首部的TCI的2B保存到skb->vlan_tci(TPI已经在skb->protocol)。然后就能安全的执行untag操作,剥去(跳过)skb的VLAN tag,重新设置各层Header的偏移。
692 |
693 | ```C++
694 | list_for_each_entry_rcu(ptype, &ptype_all, list) {
695 | if (pt_prev)
696 | ret = deliver_skb(skb, pt_prev, orig_dev);
697 | pt_prev = ptype;
698 | }
699 |
700 | list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
701 | if (pt_prev)
702 | ret = deliver_skb(skb, pt_prev, orig_dev);
703 | pt_prev = ptype;
704 | }
705 | ```
706 |
707 | 这部分是嗅探器(sniffer)的处理,将sbk递叫给所有嗅探器。`ptype_all`用来保存那些对“所有类型帧”感兴趣的Sniffer所注册的递交函数。在应用层可以通过`socket(AF_PACKET, SOCK_XXX, ETH_P_ALL)`接收所有类型的Ethernet数据。除了全局的`ptype_all`列表,每个设备也有一个列表,用来接收本设备所有类型的帧。`deliver_skb`会调用注册`packet_type`时所指定的回调函数。注意的是ptype的处理,是“向后延缓一次”的。即遍历ptype_all的时候先之前一个ptype,然后记录下本次ptype待下次处理。
708 |
709 | > Kernel中的taps只“分接器”,而Sniffer就是一种tap。
710 |
711 | ```c++
712 | if (skb_vlan_tag_present(skb)) {
713 | if (pt_prev) {
714 | ret = deliver_skb(skb, pt_prev, orig_dev);
715 | pt_prev = NULL;
716 | }
717 | if (vlan_do_receive(&skb))
718 | goto another_round;
719 | else if (unlikely(!skb))
720 | goto out;
721 | }
722 | ```
723 |
724 | 之前的`skb_vlan_untag`部分VLAN untag处理只是把信息保存到了skb->vlan_tci并移动skb的指针剥去了一个VLAN tag,但那个tag并没有被真正的处理。因为延缓一次ptype_all处理的关系,如果必要进行那次deliver。
725 |
726 | `vlan_do_receive`真正进行VLAN tag的处理(net/8021q/vlan_core.c)。它根据VLAN 协议 VLAN ID等信息在LowerLayer(当前)设备(LowerLayer设备通常是真实设备,但VLAN设备也可以在其他虚拟设备上建立,此时就是指那个虚拟设备)上所添加的VLAN设备。修改skb->dev为VLAN设备,检查MAC目的地址是否是自己的,如果是就修订skb->pkt_type为PACKET_HOST(LowerLayer设备可能认为目的不是自己,因为MAC地址不同)。设置skb->vlan_tci为0。
727 |
728 | 我们看到skb通过VLAN设备的时候,除了判断MAC是否是自己的,没有其他措施,因为VLAN tag之前已经剥离了,只需要将vlan_tci设置为0表示VLAN这层处理过了。
729 |
730 | ```C++
731 | rx_handler = rcu_dereference(skb->dev->rx_handler);
732 | if (rx_handler) {
733 | if (pt_prev) {
734 | ret = deliver_skb(skb, pt_prev, orig_dev);
735 | pt_prev = NULL;
736 | }
737 | switch (rx_handler(&skb)) {
738 | case RX_HANDLER_CONSUMED:
739 | ret = NET_RX_SUCCESS;
740 | goto out;
741 | case RX_HANDLER_ANOTHER:
742 | goto another_round;
743 | case RX_HANDLER_EXACT:
744 | deliver_exact = true;
745 | case RX_HANDLER_PASS:
746 | break;
747 | default:
748 | BUG();
749 | }
750 | }
751 | ```
752 |
753 | Kernel给`net_device`留了个RX Hook,即dev->rx_handler。用来处理设备特殊的逻辑,例如Bridging设备就使用了该钩子。Bridging具体的用法在这就不描述了。
754 |
755 | ```c++
756 | if (unlikely(skb_vlan_tag_present(skb))) {
757 | if (skb_vlan_tag_get_id(skb))
758 | skb->pkt_type = PACKET_OTHERHOST;
759 | /* Note: we might in the future use prio bits
760 | * and set skb->priority like in vlan_do_receive()
761 | * For the time being, just ignore Priority Code Point
762 | */
763 | skb->vlan_tci = 0;
764 | }
765 | ```
766 |
767 | 走到这,如果有vlan tag的话应该已经被处理了,也就是vlan_tci应该被设置为0了,如果不为0,说明那个VLAN tag不是自己应该处理的。也就是没有VID对应的VLAN设备的情况下收到了这个VLAN的包,那么就认为不是自己应该接收的。
768 |
769 | ```C++
770 | type = skb->protocol;
771 | ... ...
772 | deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
773 | &orig_dev->ptype_specific);
774 | ... ...
775 |
776 | ```
777 |
778 | 遍历ptype_base进行L2/L3分用递交。每个L3都会注册自己的ptype_packet到ptype_base。
779 |
780 | END
781 |
--------------------------------------------------------------------------------
/kernel/dpdk-kni-mcast.md:
--------------------------------------------------------------------------------
1 | 最近尝试了`dpdk`的`kni`驱动,使得非高速转发路径的数据能利用Kernel协议栈,这确实方便了非面向性能的应用。不过在`kni`虚拟接口上运行多播应用的时候的时候发现,根本不工作?
2 |
3 | #### 谈谈NIC的多播接收
4 |
5 | 接收多播对于一个网卡来说是必须的,然而初始情况下Ethernet网卡只接收两类数据帧:
6 |
7 | * 目的MAC是网卡物理地址的帧;
8 | * 以及以太网广播帧。
9 |
10 | 除非,
11 |
12 | * 打开了混杂模式,那么所有见到的帧都会接收(例如用`tcpdump`时);
13 | * 或者,明确指示它接收某个以太网多播(或某组多播或其他Unicast)。
14 |
15 | 这也是为什么多播的效率好于广播的原因,本机不关心的多播在硬件层就ignore,不必麻烦协议栈。由于应用程序或协议(`IGMP/MLD`、`IPv6 SLAAC/ND`,`OSPF`等)本身的需求,要加入一个多播组的时候,例如`224.0.0.1`,将其转化为以太网多播地址(`01:00:5e:00:00:01`)然后告知网卡要接受这个多播帧。
16 |
17 | > 网卡一般会有一个表维护要加入哪些多播,因为资源的限制,不可能做到为每个以太网多播维护一个很大的表,会采用哈希的形式多对一映射,或者限制多播表的大小。
18 |
19 | #### 怎么`kni`设备收不到多播?
20 |
21 | 回到`kni`的情况,网卡本身显然是支持多播的,以防万一,查了下DPDK的NIC驱动也是支持的;那么只能怀疑`kni`虚拟设备的驱动了,果然,`kni`驱动“[实现了](http://dpdk.org/dev/patchwork/patch/5074/)”设置多播的函数`ndo_set_rx_mode()`,只不过是个空函数而已。
22 |
23 | > `ndo_xxx`系列函数(包括`ndo_set_rx_mode`)是Kernel `net_device`层和具体设备驱动的一个Callback(虚函数),实现`ndo_set_rx_mode`目的是控制网卡的接收模式。换句话说就是设置NIC地址过滤表,除了自己地址和广播外,额外接收哪些硬件地址,比如,
24 | > * `Secondary Unicast`(自己硬件地址之外的地址)
25 | > * `Promiscuous Mode` (全都给收上来吧)
26 | > * `Multicast Mode` (告诉我哪些多播要收,还是Multicast全收)
27 |
28 | kni是虚拟设备,转交PMD设备的包,不关心是不是多播,本来是“没必要”实现这个逻辑的。不过既然KNI是建立在实际PMD设备之上的虚拟Linux设备,Linux只能操作到`kni`驱动,需要把DPDK PMD控制下的物理网卡的多播打开才行。
29 |
30 | Double check下最新的Release,没有填坑的迹象。
31 |
32 | #### 那么只能自己动手了
33 |
34 | 网卡硬件是OK的,PDM驱动也是支持多播的(虽然只是提供了个API让用户自己设置)。那么目标很明确,通过`kni`驱动把需要加入的MAC多播告知实际控制网卡的`PMD`驱动即可。分成几个步骤,
35 |
36 | * 从Kernel取出MAC多播列表
37 | * 通过某种方式从kni驱动通知到PMD驱动
38 | * 通过PMD驱动提供的API将多播列表设置到实际网卡中。
39 |
40 | ###### Kernel把多播列表存在哪?
41 |
42 | 去设置多播的地方找找(除非你知道它们就在`net_device->mc`里面),
43 |
44 | ```C
45 | ip_mc_join_group()
46 | |- ip_mc_find_dev
47 | |- ip_mc_inc_group
48 | |- IPv4多播被加入了dev.in_device.mc_hash/.mc_list
49 | |- igmp_group_added
50 | |- ip_mc_filter_add
51 | |- dev_mc_add
52 | |- __hw_addr_add_ex
53 | | 分配netdev_hw_addr加入dev->mc列表
54 | |- __dev_set_rx_mode
55 | |- dev.ops.ndo_set_rx_mode
56 | 设置具体驱动的RX Mode
57 | ```
58 |
59 | 硬件多播被作为`netdev_hw_addr`保存在了`dev->mc`列表之中,并通过之前提到的`ndo_set_rx_mode`传递给具体的驱动。“Helper”宏`netdev_for_each_mc_addr`可以用来遍历该链表。
60 |
61 | ###### `kni`怎么通知`PMD`?
62 |
63 | DPDK的PMD是用户态驱动,`kni`驱动是内核态的,他们之间的通过内存映射实现的FIFO来通信,包括数据包和控制消息。具体可以参考[kni的文档](http://dpdk.org/doc/guides/prog_guide/kernel_nic_interface.html)和代码。
64 |
65 | `kni`和`PMD`控制通道是`sync FIFO`,对应了`kni_dev.sync_kva/va`。具体说来是用了上面的那段内存来传输`rte_kni_request`结构的,Kernel可以使用API `kni_net_process_request`发送请求到用户态;用户态由`rte_kni_handle_request`处理请求,返回处理结果。
66 |
67 | 但是Requset结构目前只支持通知 MTU改变和link状态改变;为了传递多播列表需要自定义一个消息类型以及保存多播的结构放到Requset结构。
68 |
69 | 考虑到毕竟MTU和Link UP/Down这种消息相对一个多播列表而言数据量很小,担心一个多播列表保存Request超过`kni_dev.sync_kva/va`的大小(毕竟表面上看不出它到底多大呀!)。追查了分配这个buffer的地方:`rte_kni_init()/memzone "kni_sync_%d"`,差不多有64K大,放心了。
70 |
71 | > 利用`sync FIFO`是不是好?不好说,只是使用别的kernel/user-space通信方式,也有点繁琐。
72 |
73 | 定义数据结构,从`net_dev->mc`提取MAC多播,用Request传递给PMD,记得处理Response。
74 |
75 | ###### PMD驱动多播列表设置到实际网卡中
76 |
77 | 这个简单,在`rte_kni_handle_request`接收到Kernel的消息直接调用API即可。
78 |
79 | 最后还有一点要注意,`ndo_set_rx_mode`是在原子(spinlock)上下文,而`kni_net_process_request`调用了`mutex`,原子上下文不能使用`mutex`这种会引发上下文切换的函数。所以在`ndo_set_rx_mode`里将多播列表暂时保存起来然后利用`kni`的`kthread`线程去检查并发送请求,后者访问`dev->mc`数据的时候记得上锁(`netdev_addr_lock_bh`)并一次性取出。
80 |
81 | 多播在`kni`虚拟接口上正常接收,可以,这很DIY。
--------------------------------------------------------------------------------
/kernel/images/br-ioctl-create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/br-ioctl-create.png
--------------------------------------------------------------------------------
/kernel/images/br-netlink-create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/br-netlink-create.png
--------------------------------------------------------------------------------
/kernel/images/br-netlink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/br-netlink.png
--------------------------------------------------------------------------------
/kernel/images/bridge-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/bridge-2.png
--------------------------------------------------------------------------------
/kernel/images/bridge-port-fdb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/bridge-port-fdb.png
--------------------------------------------------------------------------------
/kernel/images/bridge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/bridge.png
--------------------------------------------------------------------------------
/kernel/images/brif-add-del.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/brif-add-del.png
--------------------------------------------------------------------------------
/kernel/images/dev-and-port1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/dev-and-port1.png
--------------------------------------------------------------------------------
/kernel/images/dev-and-port2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/dev-and-port2.png
--------------------------------------------------------------------------------
/kernel/images/ethernet-802.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/ethernet-802.3.png
--------------------------------------------------------------------------------
/kernel/images/napi_nonnapi_drivers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/napi_nonnapi_drivers.jpg
--------------------------------------------------------------------------------
/kernel/images/vlan-device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/vlan-device.png
--------------------------------------------------------------------------------
/kernel/images/vlan-real-devices.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/vlan-real-devices.png
--------------------------------------------------------------------------------
/kernel/images/vlan_devices_array.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/kernel/images/vlan_devices_array.png
--------------------------------------------------------------------------------
/kernel/ipip.md:
--------------------------------------------------------------------------------
1 | # Linux虚拟设备 之 *隧道 (ipip/gre)*
2 |
3 | Linux的隧道设备`ipip`, `gre/gretap`, `sit`, `6rd`等,都是通过内核模块的形式实现的,同时Linux将基于IPv4的各种隧道的公共代码编写了一个模块`ip_tunnel` (*ip_tunnel.c/ip_tunnels.h*).另一个,内核模块`tunnel4`(及其`xfrm`)则为各个隧道提供了数据接收、传输的框架。
4 |
5 | ## 使用隧道设备
6 |
7 | 加载`ipip`模块后,才能使用IP-in-IP隧道,加载时会自动加载所依赖的内核模块(`tunnel4`和`ip_tunnel`)。同时生成一个默认的`tunl0`。隧道的MTU比普通以太网设备小些,需要额外空间存放隧道header。
8 |
9 | ```
10 | $ modprobe ipip
11 | $ lsmod | grep ipip
12 | ipip 16384 0
13 | tunnel4 16384 1 ipip
14 | ip_tunnel 28672 1 ipip
15 |
16 | $ ip link show
17 | 3: tunl0@NONE: mtu 1480 qdisc noop state DOWN mode DEFAULT group default qlen 1
18 | link/ipip 0.0.0.0 brd 0.0.0.0
19 | ```
20 |
21 | 在Host A `10.3.3.3`和Host B `10.4.4.4`间建立ipip隧道,只需要A、B在Layer 3可达即可。并使用overlay网络`192.168.0.0/24`进行通信。下面是Host A的配置,Host B的配置类似。如果overlay网络要配置非简单的直连(on-link)地址,注意合理配置路由。
22 |
23 | ```
24 | $ sudo ip tunnel add ipip0 mode ipip local 10.3.3.3 remote 10.4.4.4 ttl 64 dev eth0
25 | $ sudo ip addr add 192.168.0.1/24 dev ipip0
26 | $ sudo ip link set ipip0 up
27 | ```
28 |
29 | 查看隧道设备是否工作。
30 |
31 | ```
32 | $ ping 192.168.0.2
33 | PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
34 | 64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=31.4 ms
35 | ```
36 |
37 | ## 数据结构
38 |
39 | Per-net Tunnel结构,保存net中所有同类型的tunnel设备,以及fb设备等。fb即"fallback"设备,是指,`no source, no destination, no key, no options`的设备。数据包中如果有"key"查找tunnel的时候key必须匹配,否则就匹配keyless tunnel。而fb tunnel作为“keyless” tunnel查找失败后,最后的选择。
40 |
41 | ```
42 | ip_tunnel_net {
43 | struct net_device *fb_tunnel_dev;
44 | struct hlist_head tunnels[IP_TNL_HASH_SIZE];
45 | struct ip_tunnel __rcu *collect_md_tun;
46 | };
47 | ```
48 |
49 | Tunnel设备私有数据(附属于`net_device`)
50 |
51 | ```
52 | struct ip_tunnel {
53 | struct ip_tunnel __rcu *next;
54 | struct hlist_node hash_node; // per-net Hash表节点
55 | struct net_device *dev;
56 | struct net *net; /* netns for packet i/o */
57 |
58 | ... ICMP错误统计、GRE, ERSPAN, 相关信息 ...
59 |
60 | struct dst_cache dst_cache; // 路由缓存
61 |
62 | struct ip_tunnel_parm parms; // 配置参数
63 |
64 | int mlink;
65 | int encap_hlen; /* Encap header length (FOU,GUE) */
66 | int hlen; /* tun_hlen + encap_hlen */
67 | struct ip_tunnel_encap encap;
68 |
69 | ... 6rd信息 ...
70 |
71 | struct ip_tunnel_prl_entry __rcu *prl; /* potential router list */
72 | unsigned int prl_count; /* # of entries in PRL */
73 | unsigned int ip_tnl_net_id;
74 | struct gro_cells gro_cells;
75 | __u32 fwmark;
76 | bool collect_md;
77 | bool ignore_df;
78 | };
79 | ```
80 |
81 | `ipip`设备的netlink操作函数,
82 |
83 | ```
84 | static struct rtnl_link_ops ipip_link_ops __read_mostly = {
85 | .kind = "ipip",
86 | .maxtype = IFLA_IPTUN_MAX,
87 | .policy = ipip_policy,
88 | .priv_size = sizeof(struct ip_tunnel), # net_device私有数据
89 | .setup = ipip_tunnel_setup, # alloc_netdev时调用
90 | .validate = ipip_tunnel_validate,
91 | .newlink = ipip_newlink,
92 | .changelink = ipip_changelink,
93 | .dellink = ip_tunnel_dellink,
94 | .get_size = ipip_get_size,
95 | .fill_info = ipip_fill_info, # dump信息
96 | .get_link_net = ip_tunnel_get_link_net,
97 | };
98 | ```
99 |
100 | ## 初始化
101 |
102 | `ipip` 作为一个内核模块实现,初始化流程如下,
103 |
104 | ```
105 | module_init(ipip_init);
106 | |- 注册per-net设备
107 | |- ops->init() 即 ipip_init_net()
108 | |- ip_tunnel_init_net(net, ipip_net_ops, "tunl0")
109 | |- 初始化tunnel设备的per-net Hash: ip_tunnel_net.tunnels
110 | |- 创建fb设备 “tunl0”(需要rtnl_link_ops)
111 | __ip_tunnel_create(net, ops, param)
112 | |- alloc_netdev(): 私有数据为ip_tunnel{}
113 | |- dev->rtnl_link_ops = ops
114 | |- register_netdevice()
115 | |- 注册xfrm4隧道hander用于数据接收
116 | |- xfrm4_tunnel_register(&ipip_handler, AF_INET);
117 | |- 注册netlink操作:
118 | |- rtnl_link_register(&ipip_link_ops);
119 | ```
120 |
121 | ## 添加、删除隧道
122 |
123 | 先考察下如何创建隧道设备。至于删除过程,它基本是创建的逆过程,因此不再深入研究。
124 |
125 | ### 创建隧道接口
126 |
127 | 使用`ip tunnel ...`命令添加隧道之后内部发生了什么?我们先从整体看看,整个隧道设备的创建过程。
128 |
129 | `ip`命令(*iproute2*)会使用*netlink*套接字组装和发送`RTM_NEWLINK`消息,最终会走到注册的*rtnetlink*函数`rtnl_newlink`。该函数完成的工作大致如下,
130 |
131 | 1. 解析netlink套接字传递的参数(也就是ip tunnel 命令跟随的参数 ),做通用参数检查;
132 | 2. 然后根据kind找到`rtnl_link_ops`,此处是`ipip_link_ops`。
133 | 3. 进一步根据kind的`.policy`解析参数,并调用类型相关的`.validate`检查。
134 | 4. 如果设备已存在(修改)则调用`ops->changelink`。
135 | 5. 如果要创建设备,调用`rtnl_create_link`,后者调用`alloc_netdev_mqs`分配`net_device`,分配并部分初始化后会调用`.setup`即`ipip_tunnel_setup`做类型特定的初始化工作。
136 | 6. 设置ifindex
137 | 7. 调用`ops->newlink`即`ipip_newlink`。通常,`->newlink`会调用`register_netdevice`将设备注册到系统中。如果->newlink为空,主动调用`register_netdevice`。
138 | 8. 通告netdev事件、其他内核模块或userspace程序可能监听该事件。
139 |
140 | 整个过程中,牵涉到了许多`rtnl_link_ops`字段,换一个角度,从`rtnl_link_ops`需要实现的Callback("虚函数")本身,重新总结下,
141 |
142 | |rtnl_link_ops字段|`ipip`模块相应值|说明|
143 | |--|--|
144 | |.kind|"ipip"|用来查找`rtnl_link_ops`|
145 | |.policy|ipip_policy|解析kind特定参数,例如`ipip`的*remote*, *local*等|
146 | |.priv_size|sizeof(struct ip_tunnel)|调用`alloc_netdev_mqs`分配`net_device`附属的私有结构空间|
147 | |.setup|ipip_tunnel_setup|调用`alloc_netdev_mqs`分配`net_device`同时调用`.setup`做类型特定初始化。|
148 | |.validate|ipip_tunnel_validate|解析类型特定参数后,进行验证|
149 | |.newlink|ipip_newlink|创建完`net_device`后,进行新接口设置,通常在内部调用`register_netdevice`向系统注册设备。|
150 | |.changelink|ipip_changelink|修改设备参数|
151 |
152 | > 如果用`ifconfig`创建隧道接口,则使用传统的`ioctl`,此处不讨论`ioctl`方式。
153 |
154 | 了解了这些netlink callback的调用背景后,以`ipip`为例,看看部分callback具体实现,
155 |
156 | ### 函数`ipip_tunnel_setup`
157 |
158 | 我们知道,`net_device`的数据收发、ioctl,打开关闭等,是通过其设备相关的虚函数表`net_device_ops`来实现的,分配了`ipip`隧道设备的net_device后,`ipip_tunnel_setup`会设置相关的`net_device_ops`。这些函数,会在设备打开、关闭和数据传输接收时提到。
159 |
160 | 然后是设置设备类型`ARPHRD_TUNNEL`,`IFF_XXX`标记,和features等。最后,会调用`ip_tunnel_setup`进行隧道通用的设置,后者目前只设置了`tunnel->ip_tnl_net_id`。
161 |
162 | ```
163 | static const struct net_device_ops ipip_netdev_ops = {
164 | .ndo_init = ipip_tunnel_init,
165 | .ndo_uninit = ip_tunnel_uninit,
166 | .ndo_start_xmit = ipip_tunnel_xmit,
167 | .ndo_do_ioctl = ipip_tunnel_ioctl,
168 | .ndo_change_mtu = ip_tunnel_change_mtu,
169 | .ndo_get_stats64 = ip_tunnel_get_stats64,
170 | .ndo_get_iflink = ip_tunnel_get_iflink,
171 | };
172 |
173 | static void ipip_tunnel_setup(struct net_device *dev)
174 | {
175 | dev->netdev_ops = &ipip_netdev_ops;
176 |
177 | dev->type = ARPHRD_TUNNEL;
178 | dev->flags = IFF_NOARP;
179 | dev->addr_len = 4;
180 | dev->features |= NETIF_F_LLTX;
181 | netif_keep_dst(dev);
182 |
183 | dev->features |= IPIP_FEATURES;
184 | dev->hw_features |= IPIP_FEATURES;
185 | ip_tunnel_setup(dev, ipip_net_id); // 只是设置了tunnel->ip_tnl_net_id。
186 | }
187 | ```
188 |
189 | ### 函数`ipip_newlink`
190 |
191 | 函数会继续解析尚未解析的参数到内部结构,例如封装参数(type, sport, dport),collect_md和fwmark。然后调用统一库函数`ip_tunnel_newlink`。
192 |
193 | ```
194 | ipip_newlink
195 | +
196 | |- ipip_netlink_encap_parms(data, &ipencap)
197 | |- ipip_netlink_parms(data, &p, &t->collect_md, &fwmark);
198 | |- ip_tunnel_newlink(dev, tb, &p, &fwmark)
199 | ```
200 |
201 | `ip_tunnel_newlink`完成的工作,
202 |
203 | * 确保设备(在per-net的tunnel Hash或colect_md上)不存在,否则返回`-EEXIST`;
204 | * 设置参数:`ip_tunnel`的`.net`,`.parms`, `.fwmark`等;
205 | * 调用`register_netdevice`向系统**注册**网络设备;
206 | * (如果类型是*ARPHDR_ETHER*)未通过参数指定HW地址则生成随机地址。
207 | - `ipip`设备类型是*ARPHRD_TUNNEL*而非*ARPHDR_ETHER*,故没有以太网地址。
208 | * `ip_tunnel_bind_dev`:返回mtu并设置需要的headroom(dev->needed_headroom)。
209 | - 根据参数进行路由查找,并保存到tunnel->dst_cache,可能的话同时确定下层设备。
210 | - 根据下层设备(路由查找得到的外出设备或param->link)的MTU以及需要的封装header设置mtu及需要的headroom。
211 | * `ip_tunnel_add`插入新`ip_tunnel`设备到Per-net结构
212 | - 将设备插入per-net的`ip_tunnel_net->tunnels`哈希表;以及,如果需要`ip_tunnel_net->collect_md_tun`。
213 |
214 | ### 函数`ipip_tunnel_init`
215 |
216 | `register_netdevice`时会调用`dev->ndo_init`,即`ipip_tunnel_init`函数。
217 |
218 | ```
219 | static int ipip_tunnel_init(struct net_device *dev)
220 | {
221 | struct ip_tunnel *tunnel = netdev_priv(dev);
222 |
223 | memcpy(dev->dev_addr, &tunnel->parms.iph.saddr, 4);
224 | memcpy(dev->broadcast, &tunnel->parms.iph.daddr, 4);
225 |
226 | tunnel->tun_hlen = 0;
227 | tunnel->hlen = tunnel->tun_hlen + tunnel->encap_hlen;
228 | return ip_tunnel_init(dev);
229 | }
230 | ```
231 |
232 | `ip_tunnel_init`比较重要,它为tunnel设备,
233 |
234 | * 设置析构函数;
235 | * 分配per-CPU的统计字段;
236 | * 分配初始化per-CPU路由缓存字段(tunnel->dst_cache);
237 | * 创建`gro_cell`用于*NAPI*的数据接收。
238 | * 初始化ip_tunnel相关字段:
239 | - tunnel->dev
240 | - tunnel->net
241 | - tunnel->param.iph/name
242 |
243 | ```
244 | int ip_tunnel_init(struct net_device *dev)
245 | {
246 | ... ...
247 | /* 注册设备的“析构函数”,当设备释放的时候,释放ip_tunnel相关资源 */
248 | dev->needs_free_netdev = true;
249 | dev->priv_destructor = ip_tunnel_dev_free;
250 |
251 | /* 分配 per-CPU的统计字段 */
252 | dev->tstats = netdev_alloc_pcpu_stats(struct pcpu_sw_netstats);
253 |
254 | /* 分配初始化per-CPU路由缓存字段 */
255 | err = dst_cache_init(&tunnel->dst_cache, GFP_KERNEL);
256 |
257 | /* 创建`gro_cell`用于*NAPI*的数据接收。 */
258 | err = gro_cells_init(&tunnel->gro_cells, dev);
259 | if (err) {
260 | dst_cache_destroy(&tunnel->dst_cache);
261 | free_percpu(dev->tstats);
262 | return err;
263 | }
264 |
265 | /* 参数等初始化 */
266 | tunnel->dev = dev;
267 | tunnel->net = dev_net(dev);
268 | strcpy(tunnel->parms.name, dev->name);
269 | iph->version = 4;
270 | iph->ihl = 5;
271 | ... ...
272 | return 0;
273 | }
274 | ```
275 |
276 | ## 数据接收、传输
277 |
278 | ### 数据分用:`tunnel4`模块和`xfrm`
279 |
280 | 接收过程中数据包的分用依赖于`inet_protos[]`表`IPPROTO_IPIP`也不例外,注册`IPPROTO_IPIP`的正是`tunnel4`模块。其注册的处理函数是`tunnel4_rcv`。
281 |
282 | ```
283 | module_init(tunnel4_init)
284 | +
285 | |- inet_add_protocol(&tunnel4_protocol, IPPROTO_IPIP))
286 | ```
287 |
288 | ```
289 | static const struct net_protocol tunnel4_protocol = {
290 | .handler = tunnel4_rcv,
291 | ... ...
292 | };
293 | ```
294 |
295 | 当IP层判断收到的分组是IP-in-IP分组时,交由`tunnel4_rcv`处理。
296 |
297 | 另一方面,`tunnel4`模块维护一个为每个基于ipv4的隧道维护了个全局的隧道handler列表 `tunnel4_handlers`,其元素是`xfrm_tunnel`。
298 |
299 | ```
300 | static struct xfrm_tunnel __rcu *tunnel4_handlers __read_mostly;
301 |
302 | struct xfrm_tunnel {
303 | int (*handler)(struct sk_buff *skb);
304 | int (*err_handler)(struct sk_buff *skb, u32 info);
305 |
306 | struct xfrm_tunnel __rcu *next;
307 | int priority;
308 | };
309 | ```
310 |
311 | `ipip`模块初始化函数`ipip_init`注册了其隧道处理handler `ipip_hander`,其中接收函数是`ipip_rcv`。
312 |
313 | ```
314 | static struct xfrm_tunnel ipip_handler __read_mostly = {
315 | .handler = ipip_rcv,
316 | .err_handler = ipip_err,
317 | .priority = 1,
318 | };
319 | ```
320 |
321 | ### 数据接收
322 |
323 | 当IP层处理完成(此处指outer的IP header处理),进行上层协议分用,如果上层协议类型是`IPPROTO_IPIP`,则交由`tunnel4_rcv`处理。
324 |
325 | ```
326 | ip_rcv
327 | +
328 | |- ip_rcv_finish
329 | +
330 | |- 理由查询
331 | |- rt->dst.input 即 ip_local_deliver # 路由查询结果Local IN
332 | +
333 | |- ip_local_deliver_finish
334 | +
335 | |- inet_protos[]表查询(L4分用)
336 | |- ipprot->handler 即 tunnel4_rcv
337 | ```
338 |
339 | `tunnel4_rcv`按照注册时的优先级,依次遍历并调用`xfrm_tunnel`。
340 |
341 | ```
342 | tunnel4_rcv(skb)
343 | +
344 | |- 按照注册时的优先级,依次遍历并调用`xfrm_tunnel.hander`。
345 | 对于ipip而言,Hanlder是ipip_rcv。
346 |
347 | for_each_tunnel_rcu(tunnel4_handlers, handler)
348 | if (!handler->handler(skb)) # ipip_rcv
349 | return 0;
350 | ```
351 |
352 | `ipip_rcv`是`ipip_tunnel_rcv`的wrapper函数。`ipip_tunnel_rcv`函数,
353 |
354 | * 查询per-net的ipip隧道Hash表(创建时被插入`ip_tunnel_net->tunnels`)。查询条件包括:
355 | - link设备,即下层设备
356 | - 源IP
357 | - 目的IP
358 | 查询会按照不同标准进行多轮尝试,具体会在`ip_tunnel_lookup`详细介绍。
359 | * 进行xfrm安全策略检查
360 | * 调用ip_tunnel_rcv()
361 |
362 | ```
363 | ipip_rcv
364 | +
365 | |- ipip_tunnel_rcv
366 | +
367 | |- ip_tunnel_lookup根据link设备、源、目的IP在per-net Hash中查找隧道设备
368 | |- 进行xfrm安全策略检查
369 | |- ip_tunnel_rcv()
370 | +
371 | |- 检查checksum (ipip无)
372 | |- 记录seqno (ipip无)
373 | |- 重新设置L3 header
374 | |- 收包统计
375 | |- 设置skb->dev为隧道设备的net_device.
376 | |- 从tunnel设备GRO的napi收取数据。
377 | ```
378 |
379 | #### 利用`gro_cell`进行数据接收
380 |
381 | > 如果还记得非napi设备(旧式netif_rx设备)是如何和napi兼容的,设备将skb放入了一个per-CPU的skb队列,napi每次从队列dequeue数据。gro_cell采用类似的方法。
382 |
383 | `gro_cell`并不复杂,一个per-CPU的cells,每个cell包含一个skb队列、一个napi结构。API也只有3个,初始化,数据接收和销毁。
384 |
385 | ```
386 | struct gro_cell {
387 | struct sk_buff_head napi_skbs;
388 | struct napi_struct napi;
389 | };
390 |
391 | struct gro_cells {
392 | struct gro_cell __percpu *cells;
393 | };
394 |
395 | int gro_cells_receive(struct gro_cells *gcells, struct sk_buff *skb);
396 | int gro_cells_init(struct gro_cells *gcells, struct net_device *dev);
397 | void gro_cells_destroy(struct gro_cells *gcells);
398 | ```
399 |
400 | 初始化`gro_cells`好理解,
401 |
402 | ```
403 | gro_cells_init(gcells, dev)
404 | +
405 | |- gcells->cells = alloc_percpu(...)
406 | |- for each cpu's cell
407 | +
408 | |- 初始化cell->napi_skbs
409 | |- netif_napi_add(dev, &cell->napi, gre_cell_poll, weight)
410 | |- napi_enable(&cell->napi)
411 | ```
412 |
413 | 数据接收通过`gro_cells_receive`,它先获取当前CPU的`gro_cell`,如果队列满(超过设备最大挤压值)则丢弃分组,否则将分组skb插入`cell->napi_skbs`队列。调度设备的napi进行数据接收,此处会触发RX软中断。
414 |
415 | ```
416 | int gro_cells_receive(struct gro_cells *gcells, struct sk_buff *skb)
417 | {
418 | /* 获取当前CPU gro_cell */
419 | cell = this_cpu_ptr(gcells->cells);
420 |
421 | /* 队列满则丢弃 */
422 | if (skb_queue_len(&cell->napi_skbs) > netdev_max_backlog) {
423 | atomic_long_inc(&dev->rx_dropped);
424 | kfree_skb(skb);
425 | return NET_RX_DROP;
426 | }
427 |
428 | /* 插入队列 */
429 | __skb_queue_tail(&cell->napi_skbs, skb);
430 | if (skb_queue_len(&cell->napi_skbs) == 1)
431 | napi_schedule(&cell->napi);
432 | return NET_RX_SUCCESS;
433 | }
434 | ```
435 |
436 | RX软中断处理函数`net_rx_action`会按配额调用`napi_pool`,最终调用之前注册的`gro_cell_pool`。它把数据从队列中取出,然后调用`napi_gro_receive`,`napi_skb_finish`,`netif_receive_skb_internel`。GRO的情况比较复杂,不做讨论。
437 |
438 | ```
439 | net_rx_action
440 | +
441 | |- napi_pool
442 | +
443 | |- gro_cell_pool
444 | +
445 | |- napi_gro_receive
446 | +
447 | |- napi_skb_finish
448 | +
449 | |- netif_receive_skb_internel
450 | ```
451 |
452 | #### Tunnel设备查询
453 |
454 | 函数`ip_tunnel_lookup`负责通用ipv4 tunnel的查询,
455 |
456 | ```
457 | struct ip_tunnel *ip_tunnel_lookup(struct ip_tunnel_net *itn,
458 | int link, __be16 flags,
459 | __be32 remote, __be32 local,
460 | __be32 key)
461 | ```
462 |
463 | 第一轮查询按照key进行,key提取自分组,需要和tunnel所设置的key匹配。具体匹配条件如下,
464 |
465 | 1. 源地址匹配
466 | 2. 目的地匹配
467 | 3. 设备处于打开(UP)状态
468 | 4. key匹配
469 | 5. Link设备匹配
470 |
471 | 可见第一轮查询比较严格,尽量精确的找到匹配的tunnel。如果完全匹配就返回。否则需要进行第二轮。此外,第一轮如果只是link设备不匹配,把改tunnel作为候选,再进行第二轮。
472 |
473 | ```
474 | hash = ip_tunnel_hash(key, remote);
475 | head = &itn->tunnels[hash];
476 |
477 | hlist_for_each_entry_rcu(t, head, hash_node) {
478 | if (local != t->parms.iph.saddr ||
479 | remote != t->parms.iph.daddr ||
480 | !(t->dev->flags & IFF_UP))
481 | continue;
482 |
483 | if (!ip_tunnel_key_match(&t->parms, flags, key))
484 | continue;
485 |
486 | if (t->parms.link == link)
487 | return t;
488 | else
489 | cand = t;
490 | }
491 | ```
492 |
493 | 如果第一轮查询失败,则进行第二轮查找。此时可能有的候选(第一轮link不匹配),也可能没有。
494 |
495 | 第二轮条件基本和第一轮相同,区别是允许未设置local IP的tunnel进行匹配,对于未设置local IP的tunnel不检查源地址。同样如果匹配则返回。如果不匹配,仅仅因为link不同,且第一轮没有候选,会在第二轮设置候选。
496 |
497 | 1. 目的地匹配
498 | 2. tunnel未设置local IP
499 | 3. 设备处于打开(UP)状态
500 | 4. key匹配
501 | 5. Link设备匹配
502 |
503 | ```
504 | hlist_for_each_entry_rcu(t, head, hash_node) {
505 | if (remote != t->parms.iph.daddr ||
506 | t->parms.iph.saddr != 0 ||
507 | !(t->dev->flags & IFF_UP))
508 | continue;
509 |
510 | if (!ip_tunnel_key_match(&t->parms, flags, key))
511 | continue;
512 |
513 | if (t->parms.link == link)
514 | return t;
515 | else if (!cand)
516 | cand = t;
517 | }
518 | ```
519 |
520 | 目前为止的两轮查找,key和remote都是必备的。对于,没有设置remote的tunnel,可能在第三轮中找到。对于未设置remote的tunnel,Hash计算不能考虑分组中的remote地址,需要重新计算。第三轮的匹配条件是,
521 |
522 | 1. tunnel未指定remote且源地址等于tunnel的local,或者 源地址是多播且等于tunnel的remote
523 | 2. 设备处于打开(UP)状态,
524 | 3. key匹配
525 | 4. Link设备匹配
526 |
527 | 同样,如果匹配就返回; 如果不匹配,且只是link未匹配,之前又没有合适的备选,就作为备选。
528 |
529 | ```
530 | hash = ip_tunnel_hash(key, 0);
531 | head = &itn->tunnels[hash];
532 |
533 | hlist_for_each_entry_rcu(t, head, hash_node) {
534 | if ((local != t->parms.iph.saddr || t->parms.iph.daddr != 0) &&
535 | (local != t->parms.iph.daddr || !ipv4_is_multicast(local)))
536 | continue;
537 |
538 | if (!(t->dev->flags & IFF_UP))
539 | continue;
540 |
541 | if (!ip_tunnel_key_match(&t->parms, flags, key))
542 | continue;
543 |
544 | if (t->parms.link == link)
545 | return t;
546 | else if (!cand)
547 | cand = t;
548 | }
549 | ```
550 |
551 | 第三轮查找仍未找到的情况下,继续,此时如果,设置了TUNNEL_NO_KEY,则进行第四轮。如果设置了,则直接跳过第四轮,进行最后的collect_md和fb设备选择。
552 |
553 | ```
554 | if (flags & TUNNEL_NO_KEY)
555 | goto skip_key_lookup;
556 | ```
557 |
558 | 注意`ipip`模块设置了`TUNNEL_NO_KEY`,所以不会进行第四论,而是直接进入后面的查找。
559 |
560 | 第四轮查找的条件是,
561 |
562 | 1. key匹配
563 | 2. tunnel的remote和local都设置,
564 | 3. 设备处于打开(UP)状态
565 | 4. link设备匹配,
566 |
567 | 如果匹配则返回;否则如果只是link不匹配,之前未设置后备,则作为后备。
568 |
569 | ```
570 | hlist_for_each_entry_rcu(t, head, hash_node) {
571 | if (t->parms.i_key != key ||
572 | t->parms.iph.saddr != 0 ||
573 | t->parms.iph.daddr != 0 ||
574 | !(t->dev->flags & IFF_UP))
575 | continue;
576 |
577 | if (t->parms.link == link)
578 | return t;
579 | else if (!cand)
580 | cand = t;
581 | }
582 | ```
583 |
584 | 最后的查询依次在3个设备间进行,如果有设置就直接返回。它们分别是
585 |
586 | 1. cand(后备)设备
587 | 2. collect_md设备
588 | 3. fallback设备
589 |
590 | ```
591 | skip_key_lookup:
592 | if (cand)
593 | return cand;
594 |
595 | t = rcu_dereference(itn->collect_md_tun);
596 | if (t && t->dev->flags & IFF_UP)
597 | return t;
598 |
599 | if (itn->fb_tunnel_dev && itn->fb_tunnel_dev->flags & IFF_UP)
600 | return netdev_priv(itn->fb_tunnel_dev);
601 |
602 | return NULL;
603 | }
604 | ```
605 |
606 | 只有,所以轮次查询失败,且没有后备(只是link不匹配)、没有collect_md,和fallback的情况下,才会查询失败。通常,是不会查询失败的。
607 |
608 | ### 数据传输
609 |
610 | 链路层的数据传输从`dev_queue_xmit`开始,抛开队列规则 (`qdisc`)部分,流程简化为如下。最终会调用设备的`ndo_start_xmit`进行传输,对于`ipip`而言就是`ipip_tunnel_xmit`。
611 |
612 | ```
613 | dev_queue_xmit
614 | +
615 | |- 可能的QoS(如队出队等,见`traffic control)
616 | |- validate_xmit_skb # skb处理,vlan插入tag等处理
617 | |- dev_hard_start_xmit(skb, dev, txq ...)
618 | +
619 | |- netdev_start_xmit
620 | +
621 | |- ops.ndo_start_xmit # 即ipip_tunnel_xmit
622 | ```
623 |
624 | #### 函数`ipip_tunnel_xmit`
625 |
626 | ```
627 | ipip_tunnel_xmit
628 | +
629 | |- ip_tunnel_xmit(skb, dev, tiph, ipproto)
630 | ```
631 |
632 | 隧道分为两种,一种是*point-to-point*隧道,也就是设置了非通配remote的隧道;还有一种是*NBMA* (Non-Broadcast Multi-Access)隧道,没有设置remote。fb设备`tunl0`是*NBMA*的。
633 |
634 | NBMA隧道,需要根据inner IP的目的地址,查找next_hop地址(路由器或者直连的host),并设置为destination地址。
635 |
636 | 后续的传输流程一样。
637 |
638 | 首先是路由查找,查找的时候隧道分为 “connected”和“non-connected”。NBMA隧道,或者tos并非来自tunnel固定参数,即tos会变化的时候,则为“non-connected”。
639 |
640 | * 确定tos地址: 或来自tunnel的设置,或继承inner IP的tos
641 | * 进行路由查找:查找的时候,
642 | - 如果"connected",尝试tunnel中缓存的路由`tunnel->dst_cache`
643 | - 否则,查找路由表
644 | - 如果“connected”,则将路由缓存到`tunnel->dst_cache`。
645 |
646 | 路由的外出设备不能是tunnel设备本身。
647 |
648 | 更新路由的路径MTU,PMTU需要考虑:
649 | * 路由的MTU: `dst_mtu(&rt->mtu)`
650 | * 减去tunnel设备的硬件header, outter IP长度, 隧道本身额外header长度(如GRE头,ipip无)
651 | 如果数据超过MTU,返回`-E2BIG`。
652 |
653 | 设置tos,ttl, IP_DF标记(看tunnel设置、或继承)
654 | 最后调用`iptunnel_xmit`进行outter header的封装和数据发送。
655 |
656 | #### 函数`iptunnel_xmit`
657 |
658 | 函数`iptunnel_xmit`完成几件事,
659 |
660 | 1. 清理skb的字段
661 | 2. 设置skb的dst缓存
662 | 3. 封装outer IP首部
663 | 4. 调用`ip_local_out`发送数据
664 |
--------------------------------------------------------------------------------
/kernel/kernel-qemu-gdb.md:
--------------------------------------------------------------------------------
1 | # 通过QEMU+GDB调试Linux内核
2 |
3 | ## 环境
4 |
5 | 宿主机(调试机)环境:
6 |
7 | * Ubuntu 16.04.3 LTS
8 | * Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz
9 | * `4.4.0-112-generic x86_64`
10 |
11 | qemu虚拟机(被调试):
12 |
13 | * kernel: 主线版本 v4.9
14 |
15 | ## 内核编译
16 |
17 | 下载并配置并编译内核,
18 |
19 | ```shell
20 | $ cd linux
21 | $ make defconfig
22 | $ make menuconfig
23 | ```
24 |
25 | 建议的配置如下,注意`[*]`或`<*>`代表选择,`[ ]`代表关闭.
26 |
27 | ```shell
28 | Kernel hacking --->
29 | [*] KGDB: kernel debugger --->
30 | <*> KGDB: use kgdb over the serial console (NEW)
31 | [*] KGDB: Allow debugging with traps in notifiers
32 | Compile-time checks and compiler options --->
33 | [*] Compile the kernel with debug info
34 | [*] Provide GDB scripts for kernel debugging
35 |
36 | Kernel hacking --->
37 | Memory Debugging --->
38 | [ ] Testcase for the marking rodata read-only
39 |
40 | Processor type and features --->
41 | [ ] Randomize the address of the kernel image (KASLR)
42 | ```
43 |
44 | 编译内核,用虚拟机跑调试内核,不需要install.可以使用`-j`选项设置make的cpu数提高编译速度.
45 |
46 | > 新版本Kernel需要关闭KASLR,否则无法设置断点调试.
47 |
48 | ```shell
49 | make # or make -j#cpu
50 | make modules
51 | ```
52 |
53 | 看看是否编译成功,
54 |
55 | ```
56 | $ ls vmlinux ./arch/x86/boot/bzImage
57 | ./arch/x86/boot/bzImage vmlinux
58 | ```
59 |
60 | ## 制作根文件系统
61 |
62 | 下面,我们使用[busybox](https://www.busybox.net/)制作一个简单的根文件系统.
63 |
64 | ### 编译busybox
65 |
66 | 首先需要编译busybox,下载个稳定版本的busybox并编译,
67 |
68 | ```shell
69 | $ wget http://busybox.net/downloads/busybox-1.27.2.tar.bz2
70 | $ tar vjxf busybox-1.27.2.tar.bz2
71 | $ cd busybox-1.27.2/
72 | $ make menuconfig
73 | ```
74 |
75 | 相关的配置如下,同样`[*]`的代表打开,`[ ]`代表关闭.可以适当增加删减一些配置.
76 |
77 | ```shell
78 | Busybox Settings --->
79 | [*] Don't use /usr (NEW)
80 | --- Build Options
81 | [*] Build BusyBox as a static binary (no shared libs)
82 | --- Installation Options ("make install" behavior)
83 | (./_install) BusyBox installation prefix (NEW)
84 |
85 | Miscellaneous Utilities --->
86 | [ ] flash_eraseall
87 | [ ] flash_lock
88 | [ ] flash_unlock
89 | [ ] flashcp
90 | ```
91 |
92 | 编译busybox,会安装到`./_install`目录下
93 |
94 | ```shell
95 | $ make # or make -j#cpu
96 | $ make install
97 |
98 | $ ls _install/
99 | bin linuxrc sbin
100 | ```
101 |
102 | ### 制作根文件系统
103 |
104 | 根文件系统镜像大小256MiB,格式化为ext3文件系统.
105 |
106 | ```shell
107 | # in working-dir
108 | $ dd if=/dev/zero of=rootfs.img bs=1M count=256
109 | $ mkfs.ext3 rootfs.img
110 | ```
111 |
112 | 将文件系统mount到本地路径,复制busybox相关的文件,并生成必要的文件和目录
113 |
114 | ```
115 | # in working-dir
116 | $ mkdir /tmp/rootfs-busybox
117 | $ sudo mount -o loop $PWD/rootfs.img /tmp/rootfs-busybox
118 |
119 | $ sudo cp -a busybox-1.27.2/_install/* /tmp/rootfs-busybox/
120 | $ pushd /tmp/rootfs-busybox/
121 | $ sudo mkdir dev sys proc etc lib mnt
122 | $ popd
123 | ```
124 |
125 | 还需要制作系统初始化文件,
126 |
127 | ```shell
128 | # in working-dir
129 | $ sudo cp -a busybox-1.27.2/examples/bootfloppy/etc/* /tmp/rootfs-busybox/etc/
130 | ```
131 |
132 | Busybox所使用的`rcS`,内容可以写成
133 |
134 | ```shell
135 | $ cat /tmp/rootfs-busybox/etc/init.d/rcS
136 | #! /bin/sh
137 |
138 | /bin/mount -a
139 | /bin/mount -t sysfs sysfs /sys
140 | /bin/mount -t tmpfs tmpfs /dev
141 | /sbin/mdev -s
142 | ```
143 |
144 | 接下来就不需要挂载的虚拟磁盘了
145 |
146 | ```
147 | $ sudo umount /tmp/rootfs-busybox
148 | ```
149 |
150 | ## 安装qemu
151 |
152 | 安装并运行编译的调试内核
153 |
154 | ```shell
155 | $ sudo apt-get install qemu # 或 Fedora/CentOS: yum install qemu
156 | $ sudo qemu-system-x86_64 -kernel linux/arch/x86/boot/bzImage -append 'root=/dev/sda' -boot c -hda rootfs.img -k en-us
157 | ```
158 |
159 | > tips:
160 | > * 使用`ctrl+alt+2`切换qemu控制台,使用`ctrl+alt+1`切换回调试kernel
161 | > * 使用`ctrl+alt`将被qemu VM捕获的鼠标焦点切换回host
162 |
163 |
164 | ## 通过qemu和gdb调试kernel
165 |
166 | 使用qemu运行Kernel,然后切换到qenu控制台,输入
167 |
168 | ```shell
169 | (qemu) gdbserver tcp::1234
170 | Waiting for gdb connection on device 'tcp::1234'
171 | ```
172 |
173 | 打开另一个终端,使用调试工具(gdb/ddd/cgdb)调试vmlinux文件,并连接gdb server.
174 |
175 | ```shell
176 | $ cd linux
177 | $ gdb vmlinux
178 | ... ...
179 | Reading symbols from vmlinux...done.
180 | (gdb) target remote 127.0.0.1:1234
181 | Remote debugging using 127.0.0.1:1234
182 | default_idle () at arch/x86/kernel/process.c:355
183 | (gdb) b ip_rcv
184 | Breakpoint 1 at 0xffffffff817dab80: file net/ipv4/ip_input.c, line 413.
185 | (gdb) c
186 | Continuing.
187 |
188 | Breakpoint 1, ip_rcv (skb=0xffff880007175000, dev=0xffff88000726f000, pt=0xffffffff8233ec20 , orig_
189 | dev=0xffff88000726f000) at net/ipv4/ip_input.c:413
190 | (gdb)
191 | ```
192 |
193 | > 要触发上述断点,可以切换回qemu的调试kernel.为`lo`接口添加`127.0.0.1/8`地址并使能`lo`接口,然后`ping`环回地址.
194 |
195 | ## 调试网络
196 |
197 | ### 网络设备设置
198 |
199 | 我们使用qemu的**bridge模式**设置虚机网络,该模式需要在宿主机配置网桥,并使用该网桥配置地址和默认路由.具体见host的`/etc/qemu-ifup`文件.
200 | 然后使用`-net`参数启动qemu虚机.
201 |
202 | > 如果通过宿主机eth0远程登录,该操作可能导致网络登录中断.
203 |
204 | ```shell
205 | host $ sudo brctl addbr br0
206 | host $ sudo brctl addif br0 eth0
207 | host $ sudo ifconfig eth0 0
208 | host $ sudo dhclient br0
209 | host $ sudo qemu-system-x86_64 -kernel linux/arch/x86/boot/bzImage \
210 | -append 'root=/dev/sda' -boot c -hda rootfs.img -k en-us \
211 | -net nic -net tap,ifname=tap0
212 | ```
213 |
214 | `tap0`是在宿主机中对应的接口名.我们可以在宿主机中看到网桥及其两个端口.tap设备的另一端是VM的eth0.
215 |
216 | ```shell
217 | host $ brctl show
218 | bridge name bridge id STP enabled interfaces
219 | br0 8000.1866da0573d1 no eth0
220 | tap0
221 | ```
222 |
223 | 为简单测试VM和宿主机的网络联通性,我们在宿主机的`br0`和虚机的`eth0`分别配置两个私有地址来测试.
224 |
225 | ```shell
226 | # qemu VM(调试Kernel)
227 | / # ip addr add 192.168.0.2/24 dev eth0
228 | / # ip link set eth0 up
229 | ```
230 |
231 | ```shell
232 | # 宿主机
233 | host $ sudo ip addr add 192.168.0.1/24 dev br0
234 | host $ ping 192.168.0.2
235 | PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
236 | 64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.328 ms
237 | 64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.282 ms
238 | ... ...
239 | ```
240 |
241 | 这样可以基本满足调试Kernel的网络协议栈的环境了.
242 |
243 | > 或者在VM中运行`udhcpc eth0`让VM获取和host相同网络的IP,不过这需要DHCP Server的支持.
244 |
245 | ## 使用nfs挂载rootfs
246 |
247 | 调试内核模块(或其他用户态程序的时候),挂载静态的ext3文件系统并不方便.为此我们可以采用nfs的形式挂载qemu kernel的rootfs,这样就能方便的在host中修改,编译内核模块,并在qemu kernel中配合gdb进行调试.根文件系统制作方法和之前相同,
248 |
249 | ```
250 | # host working dir
251 | host $ mkdir rootfs.nfs
252 | host $ cp -a busybox-1.27.2/_install/* rootfs.nfs/
253 | host $ pushd rootfs.nfs/
254 | host $ mkdir dev sys proc etc lib mnt
255 | host $ popd
256 | host $ cp -a busybox-1.27.2/examples/bootfloppy/etc/* rootfs.nfs/etc/
257 | host $ cat rootfs.nfs/etc/init.d/rcS
258 | #! /bin/sh
259 |
260 | /bin/mount -a
261 | /bin/mount -t sysfs sysfs /sys
262 | /bin/mount -t tmpfs tmpfs /dev
263 | /sbin/mdev -s
264 |
265 | host $ chmod -R 777 rootfs.nfs/
266 | ```
267 |
268 | 配置host的nfs服务并启动,
269 |
270 | ```shell
271 | host $ apt-get install nfs-kernel-server
272 | host $ cat /etc/exports
273 | /path/to/working/dir/rootfs.nfs *(rw,insecure,sync,no_root_squash)
274 |
275 | host $ service nfs-kernel-server restart
276 | ```
277 |
278 | 使用nfs挂载qemu Kernel的根文件系统
279 |
280 | ```
281 | host $ sudo qemu-system-x86_64 -kernel linux/arch/x86/boot/bzImage \
282 | -append 'root=/dev/nfs nfsroot="192.168.1.1:/path/to/working/dir/rootfs.nfs/" rw ip=192.168.1.2' \
283 | -boot c -k en-us -net nic -net tap,ifname=tap0
284 | ```
285 |
286 | 其中`nfsroot`为host的IP及要挂载的根文件系统在host中的路径,`ip`参数填写qemu Kernel将使用的IP地址.
287 |
288 | ### 调试内核模块
289 |
290 | > Note: 编译内核模块的时候,源码树和虚机Kernel编译需要是同一份.不然会出现模块版本不匹配无法运行的情况. 编译内核模块的时候,使用`ccflags-y += -g -O0`保留信息避免优化.
291 |
292 | Kernel模块每次插入后的内存位置不确定,需要将其各个内存段的位置取出才能按源码单步调试. 首先在`do_init_module`设置断点, insmod的时候会触发断点,
293 |
294 | ```gdb
295 | (gdb) b do_init_module
296 | ```
297 |
298 | 模块各内存段信息保存在`mod->sect_attrs->attrs[]`数组中,我们需要以下几个字段信息,
299 |
300 | * `.text`
301 | * `.rodata`
302 | * `.bss`
303 |
304 | 分别打印字段的名字和其地址,
305 |
306 | ```gdb
307 | (gdb) print mod->sect_attrs->attrs[1].name
308 | $82 = 0xffff880006109ad8 <__this_cpu_preempt_check> ".text"
309 | (gdb) print mod->sect_attrs->attrs[5].name
310 | $86 = 0xffff880006109ad0 <__phys_addr_nodebug+10> ".rodata"
311 | (gdb) print mod->sect_attrs->attrs[12].name
312 | $93 = 0xffff880006109ac8 <__phys_addr_nodebug+2> ".bss"
313 |
314 | (gdb) print /x mod->sect_attrs->attrs[1]->address
315 | $96 = 0xffffffffa0005000
316 | (gdb) print /x mod->sect_attrs->attrs[5]->address
317 | $97 = 0xffffffffa0006040
318 | (gdb) print /x mod->sect_attrs->attrs[12]->address
319 | $98 = 0xffffffffa0007380
320 | ```
321 |
322 | 然后为gdb设置模块路径和各内存段地址,
323 |
324 | ```gdb
325 | (gdb) add-symbol-file /path/to/module/xxx.ko 0xffffffffa0005000 \
326 | -s .data 0xffffffffa0006040 \
327 | -s .bss 0xffffffffa0007380
328 | ```
329 |
330 | 接下来,就能为模块的各个函数设置断点进行调试了.
331 |
332 | ## 参考
333 |
334 | * https://www.jianshu.com/p/02557f0d29dc
335 | * http://blog.csdn.net/ganggexiongqi/article/details/5877756
336 | * https://www.binss.me/blog/how-to-debug-linux-kernel/
337 | * https://www.jianshu.com/p/110b60c14a8b
338 | * https://help.ubuntu.com/community/SettingUpNFSHowTo
339 |
--------------------------------------------------------------------------------
/kernel/vlan.md:
--------------------------------------------------------------------------------
1 | # Kernel虚拟设备 之 *VLAN*
2 |
3 | > 最近为项目实现了VLAN的支持,期间把Kernel的VLAN模块(`net/802.1q`)读了遍,留笔记于此。
4 | > 对于VLAN帧各字段意义[WiKi](https://en.wikipedia.org/wiki/IEEE_802.1Q)有比较详细的描述。
5 |
6 | 和Bridge, Tunnel, Bonding等一样,VLAN在Kernel中使用虚拟设备实现,即`net_device`的一种扩展。
7 | 依据《ULNI》对网络栈的描述套路(个人觉得非常有效),本文也分“数据结构”,“初始化、创建、删除”,“数据接收(RX)”,“数据发送(TX)”几个部分进行。
8 |
9 | ### VLAN数据结构
10 |
11 | VLAN作为一种虚拟设备(`vlan-device`),需要构建在“真实(real-device)”设备之上,这里的“真实”设备即可以是物理网卡(例如,`eth0`),
12 | 也可以是其他虚拟设备,如`bonding`设备。同时、一个real-device,可以”挂“多个VLAN设备。 例如下图所描述的情况,
13 |
14 | 
15 |
16 | * `eth0`是物理网卡,`eth0.100`和`eth0.200`分别模拟了两个VLAN的`access port`。
17 | * 带VLAN tag的数据被`eth0`接收后,如果tag是100,则交由`eth0.100`再进入协议栈。
18 | * 数据发送的时候,从`eth0.100`出去的数据会由`eth.100`打上100 tag。
19 |
20 | 当然,tag的strip和insert过程可以通过HW来*offloading*以减少CUP的负担。
21 |
22 | #### VLAN设备: `vlan_dev_priv`
23 |
24 | vlan-device也是一种`net_device`,所以只需要用通常的方式对`net_device`扩展,并实现`net_dev_ops`,`ethtool_ops`等。VLAN相关的信息保存在`vlan_dev_priv中`,分配VLAN设备`net_device`的时候一起分配。各字段的意义见注释。
25 |
26 | ``` C
27 | struct vlan_dev_priv {
28 | /*
29 | * 下面几个字段用于将VLAN Header中的优先级映射为Kernel所使用的优先级,即skb->priority。
30 | * 因为不同的协议如IPv4/IPv6和VLAN都定义了自己的“优先级”,同时没有统一的处理方式。
31 | * Kernel的做法是全部映射。只不过这种映射可以配置。
32 | */
33 | unsigned int nr_ingress_mappings;
34 | u32 ingress_priority_map[8];
35 | unsigned int nr_egress_mappings;
36 | struct vlan_priority_tci_mapping *egress_priority_map[16];
37 |
38 | __be16 vlan_proto; // VLAN协议ETH_P_802.1Q等
39 | u16 vlan_id; // VLAN ID
40 | u16 flags;
41 |
42 | struct net_device *real_dev; // VLAN设备依附的“真实设备”
43 | unsigned char real_dev_addr[ETH_ALEN]; // 真实设备Link地址
44 |
45 | struct proc_dir_entry *dent; // /proc文件目录节点
46 | struct vlan_pcpu_stats __percpu *vlan_pcpu_stats; //采用per-CPU的统计(避免并发竞争)
47 |
48 | unsigned int nest_level; // 防止虚拟设备嵌套过多
49 | };
50 | ```
51 |
52 | 使用netlink创建VLAN设备,在alloc_netdev_mqs的时候`priv_size`即`vlan_dev_priv`的大小。
53 |
54 | > `alloc_netdev_mqs`(及其wrapper函数)和`register_netdevice`是Kernel创建、注册网络设备、包括虚拟设备所使用的两个API。
55 |
56 | #### Real和VLAN设备关系
57 |
58 | ###### Real设备维护的VLAN信息:`vlan_info`
59 |
60 | 那么就需要维护”real-device"和"vlan-device"的关系,刚刚提到、不同类型的设备(物理网卡、bonding)都可作为real-device来挂载vlan-device。
61 | 所以,在每个设备要用到的`net_device`中内嵌了VLAN相关信息,即`vlan_info`。
62 |
63 | ``` C
64 | struct vlan_info {
65 | struct net_device *real_dev; /* The ethernet(like) device
66 | * the vlan is attached to.
67 | */
68 | struct vlan_group grp; // 可用查找对应VLAN设备的net_device结构。
69 | struct list_head vid_list; // 存放vlan_vid_info列表,该列表用于其他虚拟设备如bridge/bonding实现VLAN。
70 | unsigned int nr_vids;
71 | struct rcu_head rcu;
72 | };
73 | ```
74 |
75 | > vid_list/nr_vids所管理的vlan_vid_info主要用于其他虚拟设备如bridge/bonding等实现VLAN支持。
76 |
77 | “真实”设备的`net_device`中的`vlan_info`字段,用来管理attach到它的所有VLAN(及VLAN虚拟设备)。
78 |
79 | ``` C
80 | struct net_device {
81 | ...
82 | struct vlan_info __rcu *vlan_info;
83 | ...
84 | }
85 | ```
86 | ###### VLAN设备Hash表:`vlan_group`
87 |
88 | 再看看`vlan_group`的定义,在不考虑GVRP/MVRP协议的情况下,可以认为,它的作用就是从真实设备上,快速查找VLAN设备对应的`net_device`。
89 |
90 | ``` C
91 | struct vlan_group {
92 | unsigned int nr_vlan_devs;
93 | struct hlist_node hlist; /* linked list */
94 | struct net_device **vlan_devices_arrays[VLAN_PROTO_NUM]
95 | [VLAN_GROUP_ARRAY_SPLIT_PARTS];
96 | };
97 |
98 | ```
99 |
100 | 如何理解`vlan_devices_arrays`这个多维数组呢? 其实并不复杂,可以简单认为它是一个多级Hash表。 我们知道802.1q VLAN的VLAN ID字段只有12个bit,也就是说最多支持4096个VLAN。如果以VLAN ID作为Hash Key,那么每种VLAN协议(目前其实只支持802.1q和802.1ad QinQ两种)各需要一个4096大小的`net_device`数组作为*buckets*。只不过Kernel先把4096分成8组,做了个二级Hash,然后为每种VLAN协议分配一个Hash表。 这样一来`vlan_devices_arrays`的维度就上去了。
101 |
102 | 多维Hash表结构如图所示,
103 |
104 | 
105 |
106 | ###### XXX:`vlan_vid_info`
107 |
108 | #### VLAN及Real设备关系Overview
109 |
110 | 综上所述,一个Real设备和挂载(attach)的VLAN虚拟设备的数据结构关系如下,
111 |
112 | 
113 |
--------------------------------------------------------------------------------
/kernel/vxlan.md:
--------------------------------------------------------------------------------
1 |
2 | 数据结构
3 | ========
4 |
5 | #### VXLAN虚拟设备
6 |
7 | VXLAN使用虚拟设备实现,相关信息保存在`vxlan_dev`中,并附属于`net_device`结构。
8 |
9 | ```C
10 | /* Pseudo network device */
11 | struct vxlan_dev {
12 | struct hlist_node hlist; /* vni hash table */
13 | struct list_head next; /* vxlan's per namespace list */
14 | struct vxlan_sock __rcu *vn4_sock; /* listening socket for IPv4 */
15 | #if IS_ENABLED(CONFIG_IPV6)
16 | struct vxlan_sock __rcu *vn6_sock; /* listening socket for IPv6 */
17 | #endif
18 | struct net_device *dev;
19 | struct net *net; /* netns for packet i/o */
20 | struct vxlan_rdst default_dst; /* default destination */
21 | u32 flags; /* VXLAN_F_* in vxlan.h */
22 |
23 | struct timer_list age_timer;
24 | spinlock_t hash_lock;
25 | unsigned int addrcnt;
26 | struct gro_cells gro_cells;
27 |
28 | struct vxlan_config cfg;
29 |
30 | struct hlist_head fdb_head[FDB_HASH_SIZE];
31 | };
32 | ```
33 |
34 | * hlist
35 | * vn4_sock
36 | * vn6_sock
37 |
38 | 每个`vxlan_dev`隶属于一个*VNI*。同时,每个`vxlan_dev`都有对应的UDP socket,即`vxlan_sock`,可理解为RFC中的VTEP 。默认情况下,多个监听相同*vxlan端口*的`vxlan_dev`共享同一个`vxlan_sock`(共享一个VTEP)。所以,`vxlan_sock`维护了一个`vxlan_dev`的Hash表 ,Hash Key为VNI。`vxlan_dev.hlist`即该Hash的节点。
39 |
40 | > 监听的vxlan端口即`vxlan_config.dst_port`字段,它表示本地监听的UDP vxlan端口。
41 |
42 | * default_dst
43 |
44 | 对于目的MAC对端VTEP未知(Unknow MAC Destination)的数据需要使用默认Remote即`vxlan_dev.default_dst`发送组播数据。此外,多播和广播也会映射到underlay的多播,它会保存在`default_dst`中。`vxlan_dev`所在的VNI保存在`default_dst.remove_vni`。
45 |
46 | * hash_lock
47 | * fdb_head
48 | * age_timer
49 |
50 | `vxlan_dev`在发Inner Ethernet包的时候,先根据目的MAC搜索FDB,FDB是一个Hash表。同时为FDB维护了一个老化定时器,定期进行垃圾收集。`hash_lock`用户保护FDB Hash。
51 |
52 | #### UDP Socket
53 |
54 | 每个vxlan设备都有对应的UDP Socket用于发送接收封装VXLAN报文的underlay UDP数据报,同时多个vxlan设备(每个vxlan 设备都有自己的VNI)可能会共享`vxlan_sock`。因此`vxlan_sock`维护一个以VNI为key的哈希表,表的节点是`vxlan_dev`。
55 |
56 | > 除非打开"no_shared"参数,否则只要元组``相同就会共享。
57 |
58 | ```C
59 | /* per UDP socket information */
60 | struct vxlan_sock {
61 | struct hlist_node hlist; // net-namespace sock_list Hash元节点
62 | struct socket *sock; // UDP Socket
63 | struct hlist_head vni_list[VNI_HASH_SIZE]; // 以VNI为Key的vxlan_dev哈希表。
64 | atomic_t refcnt;
65 | u32 flags;
66 | };
67 | ```
68 |
69 | `flags`表示不同的vxlan vtep工作模式,包括,是否打开“data plane learning", 是否支持RSC,是否作为ARP代理实现ARP redution等。具体参考`net/vxlan.h`中`VXLAN_F_XXX`的定义。
70 |
71 | #### VXLAN配置
72 |
73 | Linux下vxlan设备可以通过iproute2套件即*ip(8)*命令进行配置,示例如下,
74 |
75 | ```
76 | ip link add vxlan0 type vxlan id 42 group 239.1.1.1 dev eth1 dstport 4789
77 | ```
78 |
79 | 输入以下命令获取帮助,
80 |
81 | ```
82 | ip link help vxlan
83 | ```
84 |
85 | 可参考ip(8)或者[vxlan配置](https://www.kernel.org/doc/Documentation/networking/vxlan.txt)。`iproute2`工具通过netlink操作vxlan设备,相关netlink参数被转换并保存在`vxlan_config`结构中。
86 |
87 | ```C
88 | struct vxlan_config {
89 | union vxlan_addr remote_ip;
90 | union vxlan_addr saddr;
91 | __be32 vni;
92 | int remote_ifindex;
93 | int mtu;
94 | __be16 dst_port;
95 | u16 port_min;
96 | u16 port_max;
97 | u8 tos;
98 | u8 ttl;
99 | __be32 label;
100 | u32 flags;
101 | unsigned long age_interval;
102 | unsigned int addrmax;
103 | bool no_share;
104 | };
105 | ```
106 |
107 | 其中几个重要的字段描述如下,
108 |
109 | * `remote_ip`
110 |
111 | 当转发表(FDB)中没有目的MAC的entry时,或发送广播、多播时,Outter IP使用该多播地址发送vxlan数据。
112 | 该字段可通过`ip`命令的*group*或*remote*参数配置。
113 |
114 | > 创建设备时会为remote_ip创建一个zero_mac的FDB条目,发包总是查询FDB。
115 |
116 | * `dst_port`
117 |
118 | 运行vxlan协议的outer UDP目的端口,IANA指定的VXLAN Well Known端口是4789,Linux为了兼容旧设备默认
119 | 使用8472,用户可用`ip`命令的*dstport*参数配置。
120 |
121 | * `vni`
122 |
123 | VXLAN设备的VNI。
124 |
125 | * `remote_ifindex`
126 |
127 | VXLAN设备的lower设备的ifindex。
128 |
129 | * `port_min`及`port_max`
130 |
131 | VXLAN UDP源端口选择范围。
132 |
133 | #### per-net数据结构
134 |
135 | ```C
136 | struct vxlan_net {
137 | struct list_head vxlan_list;
138 | struct hlist_head sock_list[PORT_HASH_SIZE];
139 | spinlock_t sock_lock;
140 | };
141 | ```
142 |
143 | * `vxlan_list`: 所在network namespace中所有的vxlan虚拟设备,即`vxlan_dev`。
144 | * `sock_list`: namespace中,维护所有的vxlan的UDP Sockets,即`vxlan_sock`。
145 |
146 |
147 | #### 转发数据库
148 |
149 | `vxlan_dev`维护了`vxlan_fdb`的Hash表,即`vxlan_dev.fdb_head`,Key是Remote MAC和VNI。`hlist`是该Hash的节点。`vxlan_fdb`为FDB表。
150 |
151 |
152 | ```C
153 | /* Forwarding table entry */
154 | struct vxlan_fdb {
155 | struct hlist_node hlist; /* linked list of entries */ // vxlan_dev.fdb_head表节点
156 | struct rcu_head rcu;
157 | unsigned long updated; /* jiffies */ // 最近修改时间
158 | unsigned long used; // 最近使用时间
159 | struct list_head remotes; // vxlan_rdst链表
160 | u8 eth_addr[ETH_ALEN]; // 源MAC
161 | u16 state; /* see ndm_state */
162 | __be32 vni; // 所在VNI
163 | u8 flags; /* see ndm_flags */
164 | };
165 | ```
166 |
167 | * hlist
168 |
169 | Hash表`vxlan_dev.fdb_head`节点。
170 |
171 | * updated
172 | * used
173 | 上次使用、更新的时间,used用于垃圾收集等。
174 |
175 | * eth_addr
176 |
177 | 目的(目标VMMAC地址。
178 |
179 | * remotes
180 | VTEP信息保存于`vxlan_rdst`。`remotes`即`vxlan_dst`链表。对于多播/all_zero MAC,一个源MAC可能有多个remote,发送数据时会向每个remote发送一个skb的拷贝。
181 | 1. all_zero MAC:remotes是多播(vxlan默认多播),或者多个Unicast。
182 | 2. Multicast MAC:有多个VTEP加入该多播组。(可用`bridge fdb add/del/append/replace`配置)
183 |
184 | * vni
185 | 所在VNI。
186 |
187 | 结构`vxlan_rdst`描述remote VTEP。包括对方的IP,UDP端口,VNI和通往对方的本地vxlan接口ifindex以及路由缓存。
188 |
189 | ```C
190 | struct vxlan_rdst {
191 | union vxlan_addr remote_ip;
192 | __be16 remote_port;
193 | __be32 remote_vni;
194 | u32 remote_ifindex;
195 | struct list_head list;
196 | struct rcu_head rcu;
197 | struct dst_cache dst_cache;
198 | };
199 | ```
200 |
201 | > fdb可以用*bridge*命令查看 `bridge fdb show dev vxlan0`
202 |
203 | 初始化,设备创建及打开
204 | =============
205 |
206 | #### 模块初始化
207 |
208 | vxlan是一个内核模块,其初始化函数如下,
209 |
210 | 1. 创建随机数用于`vxlan_sock`的VNI(`vxlan_dev`)hash,引入随机因子防DoS攻击。
211 | 1. *per-net*初始化,主要是初始化`vxlan_net`数据结构。
212 | 1. 注册*netdevice*通告链,用于处理lower-layer设备,及UDP隧道状态变化。
213 | 1. 注册*rtnetlink*回调函数组。
214 |
215 | ```C
216 | static int __init vxlan_init_module(void)
217 | {
218 | int rc;
219 |
220 | get_random_bytes(&vxlan_salt, sizeof(vxlan_salt));
221 |
222 | rc = register_pernet_subsys(&vxlan_net_ops);
223 | ... ...
224 |
225 | rc = register_netdevice_notifier(&vxlan_notifier_block);
226 | ... ...
227 |
228 | rc = rtnl_link_register(&vxlan_link_ops);
229 | ... ...
230 | }
231 | ```
232 |
233 | 我们看下vxlan相关netlink的函数,
234 |
235 | ```C
236 | static struct rtnl_link_ops vxlan_link_ops __read_mostly = {
237 | .kind = "vxlan",
238 | .maxtype = IFLA_VXLAN_MAX,
239 | .policy = vxlan_policy,
240 | .priv_size = sizeof(struct vxlan_dev),
241 | .setup = vxlan_setup,
242 | .validate = vxlan_validate,
243 | .newlink = vxlan_newlink,
244 | .changelink = vxlan_changelink,
245 | .dellink = vxlan_dellink,
246 | .get_size = vxlan_get_size,
247 | .fill_info = vxlan_fill_info,
248 | .get_link_net = vxlan_get_link_net,
249 | };
250 | ```
251 |
252 | #### 设备创建
253 |
254 | ##### 通过netlink创建设备
255 |
256 | 当用户通过*ip(8)*命令创建vxlan设备时,Kernel收到`RTM_NEWLINK`类型的netlink消息(保存了vxlan相关参数)。
257 |
258 | ```bash
259 | $ ip link add vxlan0 type vxlan id 42 group 239.1.1.1 dev eth0 dstport 4789
260 | $ ip link -d show
261 | ... ...
262 | 3: vxlan0: mtu 1450 qdisc noop state DOWN mode DEFAULT group default qlen 1000
263 | link/ether 16:35:89:64:de:21 brd ff:ff:ff:ff:ff:ff promiscuity 0
264 | vxlan id 42 group 239.1.1.1 dev eth0 srcport 0 0 dstport 4789 ageing 300 addrgenmode eui64
265 | ```
266 |
267 | 于是`rtnl_newlink`函数被调用,它完成以下工作,
268 |
269 |
270 | ```C
271 | static int rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh,
272 | struct netlink_ext_ack *extack)
273 | ```
274 |
275 | - 解析netlink消息中的参数,例如ifname="vxlan0",type="vxlan", id=42, ...;
276 | - 根据kind,搜索`rtnl_link_ops`,这里kind是vxlan,所以会找到`vxlan_link_ops`;
277 | - 调用ops->validate(此处是vxlan_validate)进行进一步(虚拟设备相关的)的参数检查;
278 | - 如果设备存在在,改变其参数,会调用`ops->change_link`
279 | - 如果设备不存在,则创建、注册设备`net_device`(alloc_netdev_mqs/register_netdevice)
280 |
281 |
282 | > 创建Linux网络设备的两个核心函数是`alloc_netdev_mqs`和`register_netdevice`。前者负责分配、初始化每个Kenel网络设备(包括所有物理设备、虚拟设备)的“基类” `net_device`。它根据`ops.priv_size`大小分配设备类型相关的私有数据,私有数据附属于`net_device`内存之后(继承),随后可通过`netdev_priv`获取。
283 |
284 | > 对于vxlan而言,私有数据即是`vxlan_dev`;此外,`allo3: vxlan0: mtu 1450 qdisc noop state DOWN mode DEFAULT group default qlen 1000
285 | link/ether 16:35:89:64:de:21 brd ff:ff:ff:ff:ff:ff promiscuity 0
286 | vxlan id 42 group 239.1.1.1 dev eth0 srcport 0 0 dstport 4789 ageing 300 addrgenmode eui64c_netdev_mqs`还会调用设备类型相关的*setup*函数`vxlan_setup`。
287 |
288 | > `register_netdevice`则负责将`net_device`注册到kernel中,随后就能通过统一的ifindex/ifname进行管理;通过注册的`dev->netdev_ops/ethtool_ops`进行收发包、参数设置。
289 |
290 | 整个过程,以及其中`net_device`的私有数据(`vxlan_dev`)分配,`validate`, `setup`回调函数被调用处简述如下,
291 |
292 | ```
293 | rntl_newlink
294 | +
295 | |- 从netlink msg进行参数解析,保存在nlattr *tb[]中。
296 | |- 按kind找到rtnl_link_ops # 即vxlan_link_ops
297 | |- 调用ops->validate # 即vxlan_validate
298 | |- rtnl_create_link # 若设备不存在,创建之
299 | | +
300 | | |- alloc_netdev_mqs(ops->priv_size, ifname, , ops->setup, ...)
301 | | | # 此处,分配vxlan_dev,调用vxlan_setup
302 | | |- dev_set_net(dev, net) # 设置network-namespace
303 | | |- dev->rtnl_link_ops = ops # 设置为vxlan_link_ops
304 | |
305 | |- dev->ifindex = ifm->if_index # 设置设备的ifindex
306 | |- ops->new_link() #此处调用vxlan_newlink,设置netdev_ops,调用register_netdevice
307 | ```
308 |
309 | ##### vxlan_setup
310 |
311 | ```C
312 | /* Initialize the device structure. */
313 | static void vxlan_setup(struct net_device *dev)
314 | {
315 | struct vxlan_dev *vxlan = netdev_priv(dev);
316 | unsigned int h;
317 |
318 | eth_hw_addr_random(dev); # 随机生成vxlan设备的MAC地址,VXLAN设备是独立的L2中的Host
319 | ether_setup(dev); # 设置MTU,以太网广播等ethernet设备common的信息
320 |
321 | dev->destructor = free_netdev;
322 | SET_NETDEV_DEVTYPE(dev, &vxlan_type);
323 |
324 | dev->features |= NETIF_F_LLTX;
325 | dev->features |= NETIF_F_SG | NETIF_F_HW_CSUM;
326 | dev->features |= NETIF_F_RXCSUM;
327 | dev->features |= NETIF_F_GSO_SOFTWARE;
328 |
329 | dev->vlan_features = dev->features;
330 | dev->hw_features |= NETIF_F_SG | NETIF_F_HW_CSUM | NETIF_F_RXCSUM;
331 | dev->hw_features |= NETIF_F_GSO_SOFTWARE;
332 | netif_keep_dst(dev);
333 | dev->priv_flags |= IFF_NO_QUEUE;
334 |
335 | INIT_LIST_HEAD(&vxlan->next);
336 | spin_lock_init(&vxlan->hash_lock); # 用于保护fdb_head[]
337 |
338 | init_timer_deferrable(&vxlan->age_timer); # 初始化timer对FDB进行定期清理
339 | vxlan->age_timer.function = vxlan_cleanup;
340 | vxlan->age_timer.data = (unsigned long) vxlan;
341 |
342 | vxlan->cfg.dst_port = htons(vxlan_port);
343 |
344 | vxlan->dev = dev; # 指向vxlan虚设备的net_device
345 |
346 | gro_cells_init(&vxlan->gro_cells, dev);
347 |
348 | for (h = 0; h < FDB_HASH_SIZE; ++h)
349 | INIT_HLIST_HEAD(&vxlan->fdb_head[h]); # 初始化vxlan设备的转发数据库(FDB)
350 | }
351 |
352 | ```
353 |
354 | ##### vxlan_newlink
355 |
356 | ```C
357 | static int vxlan_newlink(struct net *src_net, struct net_device *dev,
358 | struct nlattr *tb[], struct nlattr *data[])
359 | {
360 | struct vxlan_config conf;
361 | int err;
362 |
363 | err = vxlan_nl2conf(tb, data, dev, &conf, false);
364 | if (err)
365 | return err;
366 |
367 | return __vxlan_dev_create(src_net, dev, &conf);
368 | }
369 |
370 | ```
371 |
372 | `rntl_newlink`将从netlink msg解析出的配置参数保存在`tb[]`中,而这里的`vxlan_nl2conf`将这些参数转换为vxlan内部的`vxlan_config`结构。`__vxlan_dev_create`用于初始化vxlan虚拟设备、并插入per-net结构,并调用`register_netdevice`将其对应的`net_device`注册到系统中。
373 |
374 | ```C
375 | static int __vxlan_dev_create(struct net *net, struct net_device *dev,
376 | struct vxlan_config *conf)
377 | {
378 | struct vxlan_net *vn = net_generic(net, vxlan_net_id);
379 | struct vxlan_dev *vxlan = netdev_priv(dev);
380 | ... ...
381 | err = vxlan_dev_configure(net, dev, conf, false);
382 | ... ...
383 | dev->ethtool_ops = &vxlan_ethtool_ops;
384 |
385 | /* create an fdb entry for a valid default destination */
386 | if (!vxlan_addr_any(&vxlan->default_dst.remote_ip)) {
387 | err = vxlan_fdb_create(vxlan, all_zeros_mac,
388 | &vxlan->default_dst.remote_ip,
389 | ... ...);
390 | ... ...
391 | }
392 |
393 | err = register_netdevice(dev);
394 | ... ...
395 |
396 | list_add(&vxlan->next, &vn->vxlan_list);
397 | return 0;
398 | }
399 | ```
400 |
401 | 1. `vxlan_dev_configure`完成的工作:
402 |
403 | * 设置`dev->netdev_ops`为`vxlan_netdev_ether_ops`
404 | * 设置`vxlan.default_dst`,用于发送“Unknow MAC Destination” Unicast,以及多播、广播数据;
405 | * 从lower-device继承GSO等设置;
406 | * 设置vxlan设备的MTU:大小为Lower设备MTU减去可能的最大vxlan Header大小;
407 | * 保持配置信息到`vxlan->cfg`。
408 |
409 | 2. 设置`dev->ethtool_ops`为`vxlan_ethtool_ops`;
410 | 3. 为default destination创建FDB条目;
411 | 4. 调用`register_netdevice`将vxlan设备的`net_device`注册到系统中;
412 | 5. 将`vxlan_dev`加入到per-net结构`vxlan_net.vxlan_list`中。
413 |
414 | 其中,`vxlan_netdev_ether_ops`需要特别关注,其定义如下,
415 |
416 | ```C
417 | static const struct net_device_ops vxlan_netdev_ether_ops = {
418 | .ndo_init = vxlan_init,
419 | .ndo_uninit = vxlan_uninit,
420 | .ndo_open = vxlan_open, // 设备打开(使能)
421 | .ndo_stop = vxlan_stop,
422 | .ndo_start_xmit = vxlan_xmit, // 数据传输(TX)
423 | .ndo_get_stats64 = ip_tunnel_get_stats64,
424 | .ndo_set_rx_mode = vxlan_set_multicast_list,
425 | .ndo_change_mtu = vxlan_change_mtu,
426 | .ndo_validate_addr = eth_validate_addr,
427 | .ndo_set_mac_address = eth_mac_addr,
428 | .ndo_fdb_add = vxlan_fdb_add,
429 | .ndo_fdb_del = vxlan_fdb_delete,
430 | .ndo_fdb_dump = vxlan_fdb_dump,
431 | .ndo_fill_metadata_dst = vxlan_fill_metadata_dst,
432 | };
433 |
434 | ```
435 |
436 | #### 打开vxlan设备
437 |
438 | 创建完的vxlan设备,还需要使能下才能工作,或者说需要打开设备,
439 |
440 | ```
441 | $ ip link set vxlan0 up
442 | ```
443 |
444 | 之前在创建设备的时候,将vxlan设备的`net_device.netdev_ops`设置为了`vxlan_netdev_ether_ops`。
445 |
446 | ```
447 | vxlan_newlink
448 | +
449 | |- __vxlan_dev_create
450 | +
451 | |- vxlan_dev_configure
452 | +
453 | |- dev->netdev_ops = &vxlan_netdev_ether_ops
454 | ```
455 |
456 | 打开设备(open),意味着其`net_device.netdev_ops.ndo_open`即`vxlan_open`被调用。
457 |
458 | `vxlan_open`完成以下工作
459 |
460 | 1. 设置vxlan设备对应的vxlan_sock,UDP Socket可以尝试和之前创建的复用(共享),如果无法复用这需要创建。
461 | 2. 加入多播组,接收其他VTEP发送的多播地址。要加入的多播组,也是default destition的remote_ip多播。
462 | 3. 启动老化定时器,定期清理FDB。
463 |
464 | 关于VXLAN的多播, VXLAN发送多播的情况有
465 |
466 | * unknow MAC destination,即VTEP未学习到对端的`MAC,VTEP IP`前。
467 | * 发送多波Ethernet帧,映射到outer多播
468 | * 发送广播Ethernet帧,映射到outer多播
469 |
470 | 同时,还需要接收vxlan里面对方发送的多播。发生和监听的多播在vxlan中相同,可配置。具体参考RFC 7348。
471 |
472 | ```C
473 | static int vxlan_open(struct net_device *dev)
474 | {
475 | ... ...
476 | ret = vxlan_sock_add(vxlan); // 查找或创建vxlan_sock
477 | ... ...
478 | if (vxlan_addr_multicast(&vxlan->default_dst.remote_ip)) {
479 | ret = vxlan_igmp_join(vxlan); // 加入多播组,接受其他VTEP发送的多播。
480 | ... ...
481 | }
482 |
483 | if (vxlan->cfg.age_interval) // 启动老化定时器,定期清理FDB。
484 | mod_timer(&vxlan->age_timer, jiffies + FDB_AGE_INTERVAL);
485 |
486 | return ret;
487 | }
488 | ```
489 |
490 | #### 创建vxlan_sock
491 |
492 | 创建UDP Socket时候会向UDP tunnel注册回调函数来接收vxlan数据。
493 |
494 | ```C
495 | static int __vxlan_sock_add(struct vxlan_dev *vxlan, bool ipv6)
496 | {
497 | ... ...
498 | if (!vxlan->cfg.no_share) { // 先尝试复用已有的vxlan_sock,除非设置了no_share
499 | ... ...
500 | vs = vxlan_find_sock(vxlan->net, ipv6 ? AF_INET6 : AF_INET,
501 | vxlan->cfg.dst_port, vxlan->flags);
502 | ... ...
503 | }
504 | if (!vs) // 如果没有vxlan_sock可以复用,则创建之。
505 | vs = vxlan_socket_create(vxlan->net, ipv6,
506 | vxlan->cfg.dst_port, vxlan->flags);
507 | ... ...
508 | rcu_assign_pointer(vxlan->vn4_sock, vs); // 设置vxlan_dev的vxlan_sock
509 | vxlan_vs_add_dev(vs, vxlan); // 将vxlan_dev加入vxlan_sock的VNI哈希表中
510 | return 0;
511 | }
512 |
513 | ```
514 |
515 | 函数`vxlan_socket_create`负责创建`vxlan_sock`。
516 |
517 | ```C
518 | /* Create new listen socket if needed */
519 | static struct vxlan_sock *vxlan_socket_create(struct net *net, bool ipv6,
520 | __be16 port, u32 flags)
521 | {
522 | ... ...
523 | vs = kzalloc(sizeof(*vs), GFP_KERNEL);
524 | ... ...
525 | sock = vxlan_create_sock(net, ipv6, port, flags); // 调用udp_sock_create分配UDP socket{}。
526 | ... ...
527 | vs->sock = sock;
528 | ... ...
529 | hlist_add_head_rcu(&vs->hlist, vs_head(net, port)); // 将新创建的vxlan_sock加入vxlan_net.sock_list[]哈希
530 | ... ...
531 |
532 | /* Mark socket as an encapsulation socket. */
533 | memset(&tunnel_cfg, 0, sizeof(tunnel_cfg));
534 | tunnel_cfg.sk_user_data = vs; // 隧道用户数据为vxlan_sock
535 | tunnel_cfg.encap_type = 1;
536 | tunnel_cfg.encap_rcv = vxlan_rcv; // 设置UDP tunnel的接收函数vxlan_rcv
537 | tunnel_cfg.encap_destroy = NULL;
538 | tunnel_cfg.gro_receive = vxlan_gro_receive;
539 | tunnel_cfg.gro_complete = vxlan_gro_complete;
540 |
541 | setup_udp_tunnel_sock(net, sock, &tunnel_cfg);
542 |
543 | return vs;
544 | }
545 | ```
546 |
547 | 数据接收
548 | ========
549 |
550 | #### RX主要流程
551 |
552 | 打开(enable)VXLAN设备的时候,向UDP tunnel注册了接收数据的callback,即`vxlan_rcv`。其中,`sock.sk_user_data`即对应的`vxlan_sock`(见`vxlan_socket_create`)。
553 |
554 | ```C
555 | vxlan_rcv(struct sock *sk, struct sk_buff *skb)
556 | +
557 | |- sanity check
558 | |- vs = rcu_dereference_sk_user_data(sk) // 取出vxlan_sock
559 | |- vni = vxlan_vni(vxlan_hdr(skb)->vx_vni) // 取出VXLAN首部的VNI字段
560 | |- vxlan = vxlan_vs_find_vni(vs, vni) // 以VNI为Key查找vxlan_sock.vni_list,找到对应的vxlan_dev
561 | | //如果没有对应的vxlan说明该VNI无人接收,则立即丢弃。
562 | |
563 | |- __iptunnel_pull_header(...) // 根据inner Ethernet头设置skb->protocol
564 | |- vxlan_set_mac(vxlan, vs, skb, vni) // 重设skb相关dev/protocol字段,地址学习(data-plane learning)
565 | | +
566 | | |- skb_reset_mac_header(skb)
567 | | |- skb->protocol = eth_type_trans(skb, vxlan->dev); // 设置skb.dev为vxlan_dev.dev,调整skb.protocol
568 | | |- 重新计算checksum
569 | | |- 防止循环:丢弃源MAC是vxlan_dev地址的包
570 | | |- vxlan_snoop(skb->dev, &saddr, eth_hdr(skb)->h_source, vni)
571 | | // control-plane学习,即学习源MAC和源VTEP的IP,记录到FDB,后续发往该MAC使用Unicast UDP。
572 | | // 之前可能已经被学习过而存在于FDB。
573 | |
574 | |- skb_reset_network_header(skb);
575 | |- ENC处理及收包统计(vxlan->dev->tstats)
576 | |- gro_cells_receive(&vxlan->gro_cells, skb) // 收包 netif_rx或者加入GRO NAPI队列后调度napi_schedule
577 | // 此时skb->dev是vxlan->dev,后续进入正常的netif/napi收包流程
578 | }
579 | ```
580 |
581 | #### Data-plane学习
582 |
583 | `vxlan_snoop`完成MAC地址学习。
584 |
585 | ```
586 | vxlan_snoop(skb->dev, &saddr, eth_hdr(skb)->h_source, vni)
587 | +
588 | |- f = vxlan_find_mac(vxlan, src_mac, vni); // 查询FDB看源MAC是否已经存在
589 | |- if (likely(f)) // 已经存在
590 | | +
591 | | |- // 源IP和FDB Entry的相同,则直接返回。否则,如果不是静态配置的,则更新Entry的VTEP源IP。
592 | |- else // 不存在
593 | +
594 | |- vxlan_fdb_create(...) //不存在,则创建FDB条目。
595 | ```
596 |
597 | 数据发送
598 | ========
599 |
600 | #### vxlan_xmit
601 |
602 | 之前提到`vxlan_dev`的`net_device_ops.ndo_start_xmit`是`vxlan_xmit`,该函数用于封装overlay的Ethernet数据为vxlan数据进行发送。
603 |
604 | > 忽略flow-based tunnelling (VXLAN_F_COLLECT_METADATA)和ARP Proxy(VXLAN_F_PROXY)功能。
605 |
606 | ```C
607 | static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev)
608 | ```
609 |
610 | 1. 根据目的MAC查找FDB条目,可以支持“[RSC (route shortcircuit)](http://events.linuxfoundation.org/sites/events/files/slides/2013-linuxcon.pdf)”优化。
611 | 2. 如果查找失败,用全0 MAC进行查找默认的Destination对应的FDB条目。这个条目在`__vxlan_dev_create`的时候被创建,如果默认destination的IP合法的话。通常这个IP是VXLAN多播。如果还没找到,则丢弃。
612 | 3. 遍历FDB条目(`vxlan_fdb`)的每个remotes(`vxlan_rdst`),每个remote都发送一个skb(多个remote需要`skb_clone`)。
613 |
614 |
615 | ```C
616 | static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev)
617 | {
618 | ... ...
619 | eth = eth_hdr(skb);
620 | f = vxlan_find_mac(vxlan, eth->h_dest, vni);
621 | did_rsc = false;
622 |
623 | if (f && (f->flags & NTF_ROUTER) && (vxlan->flags & VXLAN_F_RSC) &&
624 | (ntohs(eth->h_proto) == ETH_P_IP ||
625 | ntohs(eth->h_proto) == ETH_P_IPV6)) {
626 | did_rsc = route_shortcircuit(dev, skb);
627 | if (did_rsc)
628 | f = vxlan_find_mac(vxlan, eth->h_dest, vni);
629 | }
630 |
631 | if (f == NULL) {
632 | f = vxlan_find_mac(vxlan, all_zeros_mac, vni);
633 | if (f == NULL) {
634 | if ((vxlan->flags & VXLAN_F_L2MISS) &&
635 | !is_multicast_ether_addr(eth->h_dest))
636 | vxlan_fdb_miss(vxlan, eth->h_dest);
637 |
638 | dev->stats.tx_dropped++;
639 | kfree_skb(skb);
640 | return NETDEV_TX_OK;
641 | }
642 | }
643 |
644 | list_for_each_entry_rcu(rdst, &f->remotes, list) {
645 | struct sk_buff *skb1;
646 |
647 | if (!fdst) {
648 | fdst = rdst;
649 | continue;
650 | }
651 | skb1 = skb_clone(skb, GFP_ATOMIC);
652 | if (skb1)
653 | vxlan_xmit_one(skb1, dev, vni, rdst, did_rsc);
654 | }
655 |
656 | if (fdst)
657 | vxlan_xmit_one(skb, dev, vni, fdst, did_rsc);
658 | else
659 | kfree_skb(skb);
660 | return NETDEV_TX_OK;
661 | }
662 | ```
663 |
664 | 参考资料
665 | ======
666 |
667 | 1. [Software Defined Networking using VXLAN](http://events.linuxfoundation.org/sites/events/files/slides/2013-linuxcon.pdf)
668 | 2. [vxlan.txt](https://www.kernel.org/doc/Documentation/networking/vxlan.txt)
669 | 3. [RFC7348](https://tools.ietf.org/html/rfc7348)
670 |
--------------------------------------------------------------------------------
/net/quic.md:
--------------------------------------------------------------------------------
1 | QUIC学习笔记
2 | ===========
3 |
4 | # QUIC概览
5 |
6 | > Quick UDP Internet Connections
7 |
8 | QUIC通过*UDP*在两端支持一组*复用的连接*, 提供等同于*TLS/SSL的安全*,同时减少连接和传输*延时*,以及通过双向带宽评估*避免拥塞*。
9 |
10 | * 基于UDP(兼容legacy设备)
11 | * 连接复用 (Multiplexed Connections,彻底解决TCP/HTTP的头端阻塞问题,支持roaming)
12 | * 安全(SSL/TLS)
13 | * 减少连接和传输Latency (0-RTT)
14 | * 流量和拥塞控制(双向带宽评估, 连接和流级别的控制,丰富的语义)
15 | * 应用层实现便于更快的迭代。
16 |
17 | > wikipedia: QUIC supports a set of multiplexed connections between two endpoints over User Datagram Protocol (UDP), and was designed to provide security protection equivalent to TLS/SSL, along with reduced connection and transport latency, and bandwidth estimation in each direction to avoid congestion.
18 |
19 | 时间线:
20 |
21 | | Year | Event |
22 | |------|-------|
23 | | 2012 | designed by Jim Roskind at Google
24 | | 2013 | announced publicly in 2013
25 | | 2015 | an Internet Draft submitted to the IETF.
26 | | 2016 | QUIC working group was established.
27 | | 2018 | supported by approximately 0.8 percent of web servers.
28 |
29 | ### 目标与特性
30 |
31 | * C/S建立端对端QUIC连接(connection),连接由复用的流(stream)组成, 每个流相当于独立的TCP连接,
32 | * 降低延迟(0-RTT)
33 |
34 | 0-RTT和TLS的session复用,无状态恢复(ticket机制)很像。优化包括了不同的阶段:握手(handshake)阶段,加密设置(encryption setup), 初始化数据请求(initial data requests)
35 |
36 | * 类似SPDY(HTTP/2.0)的流复用
37 |
38 | 设计QUIC的主要动机之一是解决TCP头端阻塞.复用流还能支持移动roaming(IP地址变化).
39 |
40 | * 更好的处理丢包
41 |
42 | 加密块(cryptographic block)边界与分组(packet)边界对齐,减少丢包的影响。
43 | TCP使用拥塞窗口(congestion window)避免拥塞,影响到多个复用的连接(单个init-cwnd)。QUIC使用更现代的技术,例如分组测距(packet pacing)不间断带宽预测;主动推测(proactive speculative)重传(重要的分组重复发送)。
44 |
45 | > 用户态开发周期短,有效的优化可以方向移植到TCP和TLS(开发周期较长),例如BBR算法。
46 |
47 | ### 支持情况
48 |
49 | * Client支持
50 | - Google Chrome `chrome://net-internals/#quic`
51 | - Opera
52 | * Server支持
53 | - Google的[prototype-server](https://cs.chromium.org/chromium/src/net/tools/quic/quic_server.cc)
54 | - [Caddy](https://github.com/beacer/caddy)项目及使用的库[quic-go](https://github.com/lucas-clemente/quic-go)
55 | - LiteSpeed Technologies的[负载均衡器](https://www.litespeedtech.com/products/litespeed-web-adc)及WebServer。
56 |
57 | ### 源代码
58 |
59 | * [Chromium](http://www.chromium.org/developers/how-tos/get-the-code),Google Chrome web browser, C++
60 | * [quic-go](https://github.com/lucas-clemente/quic-go) , Go, Caddy项目使用。
61 | * [LSQUIC Client Library](https://github.com/litespeedtech/lsquic-client), C, LiteSpeed Web Server的实现。
62 | * [Quicr](https://github.com/Ralith/quicr)及[Quinn](https://github.com/djc/quinn), Rust语言.
63 |
64 |
65 | # QUIC设计文档
66 |
67 | Jim Roskind的[QUIC设计文档](https://docs.google.com/document/d/1RNHkx_VvKWyWg6Lr8SZ-saqsQx7rFV-ev2jRFUoVD34/edit), 解释了发明QUIC的历史,背景,中心设计哲学。
68 |
69 | ### 设计动机
70 |
71 | ##### 降低时延
72 |
73 | 减低Internet时延,提供更好的交互式服务。带宽在不断增长,而时延受制于光速等因素无法减少。我们需要更少的时延以及更少费时的重传((less time-consuming retransmits)。
74 |
75 | > 时延包括:传播时延,传输时延,处理时延,排队时延 - 《Web性能权威指南》
76 |
77 | ##### 连接复用
78 |
79 | “IP和socket对”的资源有限,现有的优化使用多个连接,只是传输冗余的信息。
80 |
81 | * 复用的传输可以降低端口的使用数,
82 | * 统一报告及响应通道的特性(如丢包);
83 | * 允许更高层的协议压缩冗余数据(例如SPDY/H2的headers)。
84 | * 让3层负载均衡器将同一个client的连接流量让同一个Server处理。
85 |
86 | > 此外,还能解决TCP的头端阻塞问题;以及支持roaming。
87 | > 比如浏览器为实现HTTP/1.0 pipeline、避免头端阻塞,使用多个TCP连接 - 《Web性能权威指南》
88 |
89 | ##### 支持SPDY
90 |
91 | QUIC其中一个主要的动机是更好的支持SPDY。SPDY可以工作在TCP/SSL上,通过请求pipeline降低延迟(发送多个请求,不需要等之前的请求完成),是一样压缩降低带宽(二进制协议,首部压缩).但SPDY遇到了几个问题,
92 |
93 | * 单个分组的延迟会引入头端阻塞(TCP有序交付模型的头端阻塞问题),需要更好的传输复用技术.
94 | * 不适宜的TCP拥塞避免导致额外的带宽减少,也串行化延时开销.单个SPDY连接代替K个独立的连接.一个包丢失,整个TCP连接(所有SPDY连接)的拥塞窗口减半(CUBIC是30%).复用同一个TCP连接,无法做到按stream流控.
95 | * TLS/SSL会话恢复延迟(resumption delay).在传数据前,TLS握手至少需要一个额外的RTT(来恢复session),比完全握手快.但是这是TLS实现的问题,而非功能或安全上的必须.
96 | * TLS引入了解密依赖,前面的分组必须先解密,才能解密后续分组.
97 |
98 | > TLS 1.3的完全握手只需要1个额外的RTT.
99 |
100 | ### 设计目标
101 |
102 | QUIC设计目标,
103 |
104 | * 广泛的部署与当今的Internet
105 |
106 | 要能通过middle-boxes,无需client的内核升级,或提升至特权(elevated privileges)等.这个目标可以使用UDP-based用户态协议解决.middle-boxex和防火墙通常阻挡或者大幅降级除了TCP/UDP之外的传输,因此不考虑重新发明(revolution)新的传输层。
107 |
108 | * 减少丢包引起的头端阻塞(head-of-line blocking)
109 |
110 | 即一个连接分多个stream,一个stream的丢包不影响其他复用的stream.可以发展TCP解决服用流相互阻塞,例如MPTCP,但是依然解决不了TCP Socket接口的in-order交付模型,而更改内核会违反目标1。依然使用流复用和UDP解决,UDP的API没有in-order交付模型。
111 |
112 | * 低延时
113 |
114 | 最小化RTT开销,包括连接建立(setup),恢复(resumption)以及应对丢包。
115 | - 明显减少连接建立延迟(0-RTT,加密hello, initial-request)
116 | - 尝试使用FEC减少丢包的重传。
117 |
118 | > FEC已经从最新的QUIC版本中移除,原因是在许多网络环境中效果不如预期。
119 |
120 | * 改善移动支持
121 |
122 | 包括延迟和效率(radio关闭的时候TCP连接也随之关闭),关闭连接后重新建立或恢复非常耗时。QUIC支持0-RTT会话恢复。
123 |
124 | * 相对TCP更有好的拥塞避免支持
125 |
126 | 独立的stream流量控制,而非整个连接。防止快发生端造成慢接收端缓冲溢出。提供TCP友好、兼容的协议。
127 |
128 | * 相当于TLS的私密性(不需要按序传输按序解密)
129 |
130 | 防止middle-boxes修改新协议的唯一方法是加密。同时要提供等同于TLS的 *抗干扰*,*私密性*, *重放包含*, *认证* 特性。
131 |
132 | * 可靠且安全的资源需求可伸缩性。
133 | * 降低带宽消耗,增加通道状态响应(多个复用的stream采取统一的channel状态信号)
134 | * 在多个复用stream中实现可靠传输
135 | * 对于代理(proxy)高效的复用和解复用属性。
136 | * 重用,发展现有的协议
137 |
138 | ### API元素
139 |
140 | 建立复用流的API有几个复杂性问题待消除:
141 |
142 | * 添加新的stream到connection中的API
143 | * 从不同的stream分别read/write的API
144 | * 设置每个不同的stream包括:可靠性,性能配置(根据不同网络环境tradeoff)
145 |
146 | ##### Stream特性
147 |
148 | 不同的stream有不同的特性,可以被app单独修改。
149 |
150 | * 可调整的冗余(redundancy)级别(针对不同的带宽、延时)
151 | * 可调整的优先级级别(模仿SPDY所发展的优先级语义)
152 | * 控制通道,被视为带外流(out-of-band stream),
153 | - 用于对其他stream状态改变发送信号;
154 | - 由不同目的的控制帧组成;
155 | - 用于加密协商。
156 |
157 | > FEC不再支持。
158 |
159 | ##### 有序数据交付
160 |
161 | 需要提供类似TCP的可靠、有序交付(in-order-delivery),需要针对stream的API最大程度模拟TCP Socket。以便服务大部分现有应用,且只需要最小改动。需要API最大程度模拟SPDY。
162 |
163 | ##### 连接状态
164 |
165 | 历史上,应用和连接(TCP连接)是分离的。应用完成发送后,可以选择关闭连接,但数据可能还在本地队列中没有发送或没被应答。这样会造成关闭链接,终止应用直接的竞争。为更好对应用进行有效而紧密的绑定(tight binding),以下统计对应用可见:
166 |
167 | 1. RTT (平滑预测)
168 | 2. 分组大小(包括所有的overhead, payload)
169 | 3. 带宽(整个连接,平滑预测)
170 | 4. 峰值持续带宽(Peak Sustained Bandwidth,整个连接)
171 | 5. 拥塞窗口大小
172 | 6. 队列大小
173 | 7. 队列中的字节数
174 | 8. Per-stream队列大小
175 |
176 | 通告(Notification)或事件(Event)访问,
177 |
178 | 1. 队列大小下降到0
179 | 2. 所有需要的ACK已经接收(连接可以无状态损失的关闭)
180 |
181 | ### 协议哲学
182 |
183 | 协议有4个阶段需要考虑性能效率:
184 |
185 | * Startup
186 |
187 | 在连接建立阶段降低延迟,包括恢复(Resumption)连接的情况。
188 |
189 | * Steady State
190 |
191 | 确保进入稳定状态后的高效率和低延时,
192 |
193 | * Idle Entry
194 |
195 | 有效快速(低延时)的切换到idle状态。包括应对TCP尾部丢包(tail-drop)引起的严重延迟。
196 |
197 | * Idle Departure
198 |
199 | ##### 通过无状态UDP进行连接:克服NATs
200 |
201 | 最基本的问题:如何让UDP *数据报* 适应 *基于连接*(connection based)的协议,尤其是在有middle-boxex和firewall NAT服务的情况下。后者不但无法提供帮助,还会成为阻碍。
202 |
203 | Middle-box要断开某个TCP连接,可以向两端发起`RST`。对于UDP而已,没有类似的通告方法。当middle-box解绑UDP流量后,endpoint无法发送数据到对方,也没有`NACK`,直接变成黑洞。TCP继续发送至少会引发对端`RST`。更复杂的是,如果client继续发,middle-boxes可能又建立新的绑定,新绑定可能使用了不同的源端口。这种情况下QUIC需要将新流量视为现有连接的后续流量。
204 |
205 | 调查显示NAT boxes通常会在UDP端口映射idle 30-120秒解帮,或者更快。过早的unbinding,也许因为NAT表LRU回收,或者其他脆弱的实现。这些unbinding会对QUIC造成问题。
206 |
207 | > 即引入CID解决。
208 |
209 | ###### CID: 连接标识的关键
210 |
211 | NAT可以导致源端口在连接生成周期内的改变(unbind/rebind),源IP,源端口不足以识别连接。所以引入连接标识符(`CID`),CID是一个伪随机的nonce,64bits。在client发送Server的第一个分组携带,后续分组可显式或隐式的存在CID。
212 |
213 | Server使用CID,
214 |
215 | * 标识不同的连接,
216 | * 为连接”恢复(resurrect)” 会话加密上下文(参考TLS的session-ID/ticket-ID),包括加密密钥和认证密钥。
217 |
218 | > 这里指的是Client LAN侧的NAPT,实际上LB这种甚至会导致4元组改变,还有一种情况是client切换链路(WiFi,LTE)的时候导致源IP的变化。总之,连接的四元组都可能变化。
219 |
220 | ###### NAT绑定的keep-alive
221 |
222 | 一种补救措施是使用keepalive,但是对于移动设备而言会有问题,因为它关闭radio电源省电。
223 |
224 | ###### UDP分组分片
225 |
226 | 是否需要禁止分片?好处与坏处。今后的趋势是分片会逐步减少。
227 |
228 | > IPv6不提倡分片,分片信息不在基础header中,而且中间设备不允许分片。而是需要加强端进行PMTU发现。
229 |
230 | 分片后,最初的分组含有UDP和CID,但后续的分片缺乏这些信息。而重装需要用到IP首部的"identification",共16 bit,一旦同时有2^16个分组在传输,就可能有冲突。NAT会加剧冲突。
231 |
232 | 所以接受方要么放弃重装努力,要么可能提供一个错误的重装分组。未能重装的分组,会被鉴权hash识别为垃圾数据,而导致协议错误,比丢弃分片更严重。
233 |
234 | 分片还影响带宽利用率,浪费目标server的时间。
235 |
236 | 现有的API无法让app了解到分片的存在。如果有这样的API,就可以避免易出错(error-prone)的“路径MTU发现,PMTU exploration”,而是采用观察分片发生和回应的方法来应对。
237 |
238 | ##### 连接建立(establishment)与恢复(resumption)
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 | # QUIC官方网站
260 |
261 | Google的QUIC[官方网站](https://www.chromium.org/quic),提供了google版QUIC的文档。
262 |
263 | # IETF文档
264 |
265 |
266 | References
267 | ----------
268 |
269 | * [WiKi-QUIC](https://en.wikipedia.org/wiki/QUIC)
270 | * Jim Roskind的[QUIC设计文档](https://docs.google.com/document/d/1RNHkx_VvKWyWg6Lr8SZ-saqsQx7rFV-ev2jRFUoVD34/edit), 解释了发明QUIC的历史,背景,中心设计哲学。
271 | * [官方网站](https://www.chromium.org/quic)
272 |
--------------------------------------------------------------------------------
/netifd/images/netifd-config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/netifd/images/netifd-config.png
--------------------------------------------------------------------------------
/netifd/images/netifd-objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/netifd/images/netifd-objects.png
--------------------------------------------------------------------------------
/netifd/images/netifd-proto-dhcp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/netifd/images/netifd-proto-dhcp.png
--------------------------------------------------------------------------------
/netifd/netifd-objects.md:
--------------------------------------------------------------------------------
1 | OpenWRT之 *netifd* - 概述
2 | ========================
3 |
4 | * [netifd对象概述](#object)
5 | - [设备对象 device{}](#device)
6 | - [接口对象 interface{}](#interface)
7 | - [协议处理对象 proto_handler{}](#protocol)
8 | - [对象关系图](#objects-pic)
9 | * [netifd配置架构](#config-arch)
10 | - [UCI和配置文件](#uci)
11 | - [修改配置步骤](#config-modify)
12 | - [netifd ubus接口](#ubus)
13 | - [初始化脚本](#init-script)
14 | - [网络协议脚本](#network-script)
15 | * [参考文档](#config-doc)
16 |
17 |
18 | netifd对象概述
19 | ------------
20 |
21 |
22 | #### 设备对象 `device{}`
23 |
24 | netifd内部对象分为3个层次: `device{}`, `interface{}` 和 `protocol{}`。`device{}`是最底层,可以表示物理网络接口(例如 *eth0*),也可以是一些虚拟设备(bridge, vlan, tunnel, binding等)。Netifd各`device{}`类型的可以和Linux的网络设备和虚拟网络设备模型进行类比。
25 |
26 | 其他任何对象如果对某个`device{}`的状态改变感兴趣,可以注册自己的callback,也就是我们说所的“订阅-发布”机制。从该机制的实现来看,`device{}`对象维护了一个`device_user{}`列表`device.users`,用来记录消息订阅方的cb,以及对哪些event感兴趣。不同的订阅者感兴趣的`device{}`事件不同。同时`device.users`充当`device{}`引用计数的功能,当所有对`device{}`的引用,即`device_user{}`,被删除后`device{}`也会被释放。不存在孤立、没有人引用的`device{}`。
27 |
28 | >*注:订阅者并不能实时订阅,而是将device_user{}硬编码到其结构中。就是说谁对`device{}`感兴趣,是设计时约定的。被没有注册“订阅”的函数。*
29 |
30 | 除了高层对象(如`interface{}`)对`device{}`的引用,`device{}`对象之间也可以相互引用,例如网桥设备(type为'bridge'的`device{}`)可以应用其网桥端口设备。打个比方,网桥设备*br-lan*有两个网桥端口,它们分别是物理接口*eth0*和*wifi0*,那么*br-lan*的`device{}`就会同时引用eth0和wifi0的`device{}`;vlan也有类似的上下级引用。这点和Linux的master/slave设备关系类似。
31 |
32 | 因为`device{}`会被多方引用,它的up/down状态也通过refcount管理。调用`claim_device()`将设备打开(*brough up*);调用`release_device()`将设备关闭(*brough down*),它们都会操作引用计数。而实际的操作,例如关闭,只有当up/down引用计数为0的时候,才会正真的关闭设备。
33 |
34 | 值得注意的是`device{}`对象的存在,并不代表它是可见的(`device.sys_present`标记了是否系统范围内available)。而其他对象可以在`device{}`可见之前就attach它,并且等待其可见的event (`DEV_EVENT_ADD`)。
35 |
36 | 设备的状态变迁会通知所有订阅者(`device_user{}`),设备支持的event如下,
37 |
38 | * DEV_EVENT_ADD
39 |
40 | 设备对系统可见(*available*)。如果已经有人关注过改设备(某个对象持有的`device_user{}`被添加到`device.users`)就会立刻发送该事件。
41 |
42 | * DEV_EVENT_REMOVE
43 |
44 | 设备不再对系统可见(*not available*),收到改事件后,所有的user需要释放释放了设备的引用并完成相关状态的清理。
45 |
46 | * DEV_EVENT_SETUP
47 |
48 | 设备即将被打开(*brought up*),设备的用户可以借此机会对它进行low-level的配置工作。
49 |
50 | * DEV_EVENT_UP
51 |
52 | 设备被成功打开。
53 |
54 | * DEV_EVENT_TEARDOWN
55 |
56 | 设备即将被关闭。
57 |
58 | * DEV_EVENT_DOWN
59 |
60 | 设备被成功关闭。
61 |
62 | * DEV_EVENT_LINK_UP *(optional)*
63 |
64 | 链路建立。
65 |
66 | * DEV_EVENT_LINK_DOWN *(optional)*
67 |
68 | 链路断开。
69 |
70 |
71 | #### 接口对象 `interface{}`
72 |
73 | 接口`interface{}`被用来表示high-level的配置信息(例如IP地址等L3信息),这些配置会被应用到一个或多个`device{}`上。一个`interface{}`必须绑定到一个*主设备*和一个*layer3设备*。通常,如果配置(*setup*)过程比较简单,例如static或者DHCP,那么*主设备*和*layer3设备*是同一个(`device{}`)实例。而复杂协议,诸如ppp/pptp和VPN,则需要将*l3设备*映射成另一个设备实例。
74 |
75 | 和`device{}`类似,`interface{}`提供(*fixed的*)“订阅-发布”机制通告感兴趣的“用户”其状态的变化。`interface{}`支持的状态变化事件较少,
76 |
77 | * IFS_SETUP
78 |
79 | 接口正在被protocol handler(`protocol{}`对象)配置。
80 |
81 | * IFS_UP
82 |
83 | 接口被成功配置。
84 |
85 | * IFS_DOWN
86 |
87 | 接口被关闭。
88 |
89 |
90 | #### 协议处理对象 `proto_handler{}`
91 |
92 | 协议handler可以被attach到任何对象,只要对方提供状态改变的callback。通常`proto_handler{}`被attach到`interface{}`。
93 |
94 | `proto_handler{}`的实例只是静态的一组对象,分别描述了不同的协议,例如static,DHCP,PPP,它们通常是内置的。而通过协议动态配置接口的时候,并不会生成同一协议的多个`proto_handler{}`实例,而是以其为模板,生成运行时协议状态。为此,提供了协议处理运行时对象`interface_proto_state{}`用来保存(某个`interface{}`)相关的协议处理过程和状态。
95 |
96 | >不过`interface_proto_state{}`也只是个基类,一般会使用一个继承自(C的做法是包含)它的类来表示协议配置过程和状态。
97 |
98 | 不论采用何种协议,static也好DHCP也罢,`proto_handler{}`会调用外部脚本,或者程序,通常是调用外部Shell脚本(而Shell脚本再调用其他程序,例如udhcpc),外部脚本、程序,需要响应来着netifd(`proto_handler{}`)的3个命令:
99 |
100 | - PROTO_CMD_SETUP
101 | - PROTO_CMD_RENEW
102 | - PROTO_CMD_TEARDOWN
103 |
104 | 通常命令以异步的方式执行。为此需要,外部脚本、程序回复以下事件作为对命令执行的结果:
105 |
106 | - IFPEV_UP
107 | - IFPEV_DOWN
108 | - IFPEV_LINK_LOST
109 | - IFPEV_LINK_RENEW
110 |
111 | >*注:可能最初受到DHCP协议处理的影响,处理DHCP的外部脚本(及相关程序)异步执行,总是应该响应上面3个命令,以及回复上述的事件。*
112 |
113 | 当然,如果命令的速度足够快,例如静态配置,可以直接在相应函数中返回上述事件。免去命令、事件的交互过程。这时需要设置PROTO_FLAG_IMMEDIATE标记,以免发起IFPEV_UP/DOWN等事件的传输,直接在核心代码中处理配置过程。
114 |
115 |
116 | #### 对象关系图
117 |
118 |
119 | 
120 |
121 |
122 | netifd配置架构
123 | ------------
124 |
125 | 暂时跳出关乎内部实现的各种对象,从外部看看netifd如何与其他模块的关联,主要是如何对它进行配置。这里先给出一张图,好有个基本的概念。
126 |
127 | 
128 |
129 | 遵循Unix的惯例,初始化(启动、停止)脚本放在`/etc/init.d/*`下面,配置文件放在`/etc/config/*`(Unix通常是/etc/xxx.conf或/etc/xxx/xxx.conf)。而一些执行协议任务的网络脚本放在`/lib/netifd/proto/*`下,并引用了`/lib/*`和`/lib/netifd/*`下其他的脚本和库。
130 |
131 |
132 |
133 | #### UCI和配置文件
134 |
135 | OpenWRT有统一的配置接口UCI,UCI提供了库(*libuci*)、工具(*uci*)和Lua语言接口(LuCI),上层用户程序通过UCI以各种方式访问OpenWRT的配置。另外,UCI采用统一的配置文件系统(`/etc/config/*`),以免各个模块使用自定义的配置,方便统一管理。OpenWRT作为一个系统,保持风格一致,便于提供一致的接口和系统性范围的操作(Factory Resetd等)。我们知道,各种OpenSource软件采用不同风格的配置文件,UCI配置文件系统对各个OpenSource配置文件的封装和抽象。
136 |
137 | netifd程序初始化的时候(利用libuci)加载`/etc/config/network`文件,其内容如下所示(*wireless配置略*),
138 |
139 | ```bash
140 | root@OpenWrt:/# cat /etc/config/network
141 |
142 | config interface 'loopback'
143 | option ifname 'lo'
144 | option proto 'static'
145 | option ipaddr '127.0.0.1'
146 | option netmask '255.0.0.0'
147 |
148 | config interface 'lan'
149 | option ifname 'eth0'
150 | option type 'bridge'
151 | option proto 'static'
152 | option ipaddr '192.168.1.1'
153 | option netmask '255.255.255.0'
154 | option ip6assign '60'
155 |
156 | config interface 'wan'
157 | option ifname 'eth1'
158 | option proto 'dhcp'
159 |
160 | ... ...
161 | ```
162 |
163 | 配置文件分成若干section,section以关键字"config"所在行开头,config后面跟section类型和名字,例如上面的配置包含了3个'interface'类型的section,名字分别是'loopback', 'lan'和'wan'。netifd一边解析配置文件,一边构建其内部的数据结构(对象),根据上述配置,就会建立其3个`interface{}`对象和它相关的`device{}`对象。不过并非所有对象都在解析配置文件的时候建立,`proto_handler{}`可能是内置的;而`interface_proto_state{}`则运行时建立。
164 |
165 |
166 | #### 修改配置步骤
167 |
168 | 配置OpenWRT网络部分(不仅仅是负责接口和配置管理的netifd,还包括wireless和switch等部分)的一般步骤为:
169 |
170 | 1. 通过UCI修改配置文件`/etc/config/network`。其实怎么修改都行,手动修改,使用uci工具,通过连接uci系统的WebUI等方法,随意。
171 | 2. 通过初始化脚本(*init-script*) `/etc/init.d/network restart|reload`重启或重新加载netifd。
172 |
173 | restart会导致网络部分包括netifd整个被重启和重新初始化。如果只是reload,那就只需要通知netifd查看和应用修改的参数。命令的传递通过OpenWRT的另一个基础设施,负责RPC的ubus完成。
174 |
175 |
176 | #### netifd ubus接口
177 |
178 | 通过`ubus`命令,可以查看netifd都提供了哪些服务,我们可以看到系统里所有的服务(ubus对象),而network开头的便是由netifd对外提供的。这些服务(ubus对象)的接口(方法)和参数可以通过`-v`选项查看。
179 |
180 | ```bash
181 | root@OpenWrt:/# ubus list
182 | dhcp
183 | log
184 | network
185 | network.device
186 | network.interface
187 | network.interface.lan
188 | network.interface.loopback
189 | network.interface.wan
190 | network.interface.wan6
191 | network.wireless
192 | service
193 | system
194 | ```
195 |
196 | network对象(指network对象本身,不包括其子对象network.interface等,子对象有自己的方法)的方法包括,
197 |
198 | |method|description|
199 | |:------:|-----------|
200 | |restart|重启netifd|
201 | |reload|重新载入配置,并应用修改过的部分|
202 | |add_host_route|添加Host路由,从名字和参数来看,并不能随意添加各种其他类型的路由|
203 | |get_proto_handlers|获取所支持的协议static、DHCP、PPP、DS-Lite、GRE等等和它们接受的参数
204 | |add_dynmic|运行时动态添加接口|
205 | |del_dynmic|运行时动态删除接口|
206 |
207 | ```bash
208 | root@OpenWrt:/# ubus list -v network
209 | 'network' @0747e9c2
210 | "restart":{}
211 | "reload":{}
212 | "add_host_route":{"target":"String","v6":"Boolean","interface":"String"}
213 | "get_proto_handlers":{}
214 | "add_dynamic":{"name":"String"}
215 | "del_dynamic":{"name":"String"}
216 |
217 | ```
218 |
219 | 可见init-script便是通过ubus RPC对netifd network对象的操作,完成网络管理(和其他)部分的restart和reload。
220 |
221 | 而接口对象`network.interface`支持了其他一些method,打开关闭等皆可通过ubus命令查看,可通过命令查看,不在累述。
222 |
223 |
224 | #### 初始化脚本
225 |
226 | 按照Unix惯例放在`/etc/init.d/*`,netifd的初始化脚本是`/etc/init.d/network`。负责重启或重新加载网络管理模块,它不光负责启动netifd,还会操作wifi等部分。
227 |
228 | 对于netifd的“start”,脚本完成的工作是,
229 | * 把netifd放入procd(OpenWRT的进程管理模块)的管理中,
230 | * 告诉procd netifd启动成功的条件是生成了ubus对象network.interface
231 | * 如果netifd异常退出,procd负责把它重新带起来。
232 | * 同时会设置ulimit打开coredump限制,如果netifd异常退出产生coredump文件,会被保存到/tmp目录下。
233 |
234 | 对应的`/etc/init.d/network`代码片段如下,
235 |
236 | ```bash
237 | start_service() {
238 | init_switch
239 |
240 | procd_open_instance
241 | procd_set_param command /sbin/netifd
242 | procd_set_param respawn
243 | procd_set_param watch network.interface
244 | [ -e /proc/sys/kernel/core_pattern ] && {
245 | procd_set_param limits core="unlimited"
246 | echo '/tmp/%e.%p.%s.%t.core' > /proc/sys/kernel/core_pattern
247 | }
248 | procd_close_instance
249 | }
250 | ```
251 |
252 | 而reload相对简单,直接通过ubus接口通知netifd进程重新读取并应用配置文件,
253 |
254 | ```bash
255 | reload_service() {
256 | init_switch
257 | ubus call network reload
258 | /sbin/wifi reload_legacy
259 | }
260 | ```
261 |
262 |
263 | #### 网络协议脚本
264 |
265 | `proto_handler{}`会attach到对协议事件感兴趣的实体,例如`interface{}`。协议状态由`interface_proto_state{}`跟踪。
266 |
267 | Porotocol handler应该响应来自interface的外部命令,
268 |
269 | ```c++
270 | enum interface_proto_cmd {
271 | PROTO_CMD_SETUP,
272 | PROTO_CMD_TEARDOWN,
273 | PROTO_CMD_RENEW, // 这个命令可选
274 | };
275 | ```
276 |
277 | 考虑到协议配置需要一个过程,这些命令通常以的执行为异步方式(因为不能阻塞命令发送方),命令的结果稍后通过事件IFPEV_XXX返回。
278 |
279 | ```c++
280 | enum interface_proto_event {
281 | IFPEV_UP,
282 | IFPEV_DOWN,
283 | IFPEV_LINK_LOST,
284 | IFPEV_RENEW,
285 | };
286 | ```
287 |
288 | proto_handler接收命令,调用外部脚本、进程执行协议配置工作,然后把协议配置的结果通过Event返回interface的流程可以用下图表示。
289 |
290 | 
291 |
292 | 如果命令可以在短时间内完成,命令的callback可以立即发送event。否则就是要uloop调度异步行为。因此那些简单的协议,可以设置PROTO_FLAG_IMMEDIATE标记,那么就不需要自己调度IFPEV_UP/DOWN,而是由核心代码(在cmd callback结束后)自动生成Event。
293 |
294 |
295 | 参考文档
296 | ------------
297 |
298 | 关于UCI和各个配置文件的详细介绍,可参考OpenWRT官方文档,
299 |
300 | - netifd DESIGN文件
301 | - http://wiki.openwrt.org/doc/techref/uci
302 | - http://wiki.openwrt.org/doc/techref/initscripts
303 | - http://wiki.openwrt.org/doc/devel/config-scripting
304 | - http://wiki.openwrt.org/doc/devel/network-scripting
305 |
--------------------------------------------------------------------------------
/ovs/ovs-usage.md:
--------------------------------------------------------------------------------
1 |
2 | **install ovs**
3 | =============
4 |
5 | build ovs
6 | ---------
7 |
8 | if meet "LT_INIT" error, install libtool, note to use `--prefix=/usr` to install it into `/usr/bin` instead of `/usr/local/bin`.
9 |
10 | run these commands to build ovs,
11 |
12 | $ cd ovs
13 | $ ./boot.sh
14 | $ ./configure --with-linux=/lib/modules/`uname -r`/build
15 | $ make
16 | $ sudo make install
17 | $ sudo make modules_install
18 |
19 | setup database
20 | --------------
21 |
22 | $ ovsdb-tool create /usr/local/etc/openvswitch/conf.db vswitchd/vswitch.ovsschema
23 |
24 | startup ovs
25 | -----------
26 |
27 | $ ovsdb-server --remote=punix:/usr/local/var/run/openvswitch/db.sock \
28 | --remote=db:Open_vSwitch,Open_vSwitch,manager_options \
29 | --private-key=db:Open_vSwitch,SSL,private_key \
30 | --certificate=db:Open_vSwitch,SSL,certificate \
31 | --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert \
32 | --pidfile --detach
33 | $ ovs-vsctl --no-wait init
34 | $ ovs-vswitchd --pidfile --detach
35 |
36 | **using ovs**
37 | =============
38 |
39 | adding deleting bridges
40 | -----------------------
41 |
42 | ```bash
43 | $ ovs-vsctl add-br
44 | $ ovs-vsctl list-br
45 | $ ovs-vsctl list-ports
46 |
47 | $ ovs-vsctl # can be any db's table, like root table 'Open_vSwitch'
48 | ```
49 |
50 | `table` can be any db's table, like root table 'Open_vSwitch', 'Bridge', 'Port', 'Interface'. See ovs-vswitchd.conf.db(5) for help.
51 |
52 | run `ovsdb-tool show-log [-mmmm] ` to show the log. `-m` means more, use `-mmm` for more info. see `ovsdb-tool --help` for help.
53 |
54 | show logs
55 | ---------
56 |
57 | ```bash
58 | $ ovsdb-tool show-log -m
59 | record 0: "Open_vSwitch" schema, version="7.12.1", cksum="2211824403 22535"
60 |
61 | record 1: 2015-11-17 14:31:55.892 "ovs-vsctl: ovs-vsctl --no-wait init"
62 | table Open_vSwitch insert row f46de487:
63 |
64 | record 2: 2015-11-17 14:32:13.905
65 | table Open_vSwitch row f46de487 (f46de487):
66 | ```
67 |
68 | > see `install.md` for detail. `cd ovs && find -name '\*.md'` for docs!!
69 |
--------------------------------------------------------------------------------
/web/all-layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/all-layers.png
--------------------------------------------------------------------------------
/web/h2-bin-frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/h2-bin-frame.png
--------------------------------------------------------------------------------
/web/h2-bin-msg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/h2-bin-msg.png
--------------------------------------------------------------------------------
/web/h2-header-comp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/h2-header-comp.png
--------------------------------------------------------------------------------
/web/h2-push.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/h2-push.png
--------------------------------------------------------------------------------
/web/h2-stream-message-frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/h2-stream-message-frame.png
--------------------------------------------------------------------------------
/web/latency.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/latency.png
--------------------------------------------------------------------------------
/web/multi-req-res.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/multi-req-res.png
--------------------------------------------------------------------------------
/web/request-pipeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/request-pipeline.png
--------------------------------------------------------------------------------
/web/server-con.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/server-con.png
--------------------------------------------------------------------------------
/web/tls-handshake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/tls-handshake.png
--------------------------------------------------------------------------------
/web/tls-session-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/tls-session-id.png
--------------------------------------------------------------------------------
/web/tls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beacer/notes/00125737b1005ae5c139b571904b2380462e6ecc/web/tls.png
--------------------------------------------------------------------------------
/web/web-perf.md:
--------------------------------------------------------------------------------
1 | Web性能权威指南 笔记
2 | ==================
3 |
4 | Ch1. 延迟与带宽
5 | -------------
6 |
7 | # **延时** 构成
8 |
9 | * 传播延迟: 信号传播速度,距离的函数,不超过光速.
10 | * 传输延迟: bits转移到链路的时间,*消息长度* 和 *链路速率* 的函数 (Transmit, TX)
11 | * 处理延迟: 处理分组首部,检查位错误,确定分组目标 (processing)
12 | * 排队延迟: 排队等待处理的时间 (queuing),路由器,网卡分组队列等
13 |
14 | # 光速与传播延时
15 |
16 | 光速与介质的折射率(铜线,光纤).纽约到伦敦距离光纤RTT 56ms,纽约到悉尼 160ms.
17 |
18 | > CDN:在不能提高传播速度的情况下,缩短服务器与用户的距离.
19 |
20 | # 延迟的最后一公里
21 |
22 | 延迟相当大一部分在最后几公里,而非跨洋产生.*接入* 互联网的路由器节点,需要数十ms把分组传递到ISP的主路由器.光纤接入18ms,Cable接入26ms,DSL 43ms.`traceroute`可反映没跳的耗时.
23 |
24 | > traceroute通过 TTL和ICMP Time Exceeded消息实现.或者记录路由.
25 |
26 | 多数网站的**性能瓶颈是延迟**,而不是带宽!
27 |
28 | # 带宽
29 |
30 | * 网络核心的带宽
31 | 光纤 一个波长最大容量171G bps,一条光纤400种波长, 共70T bit/s.海底光缆带宽只使用了20%.
32 | * 网络边缘的带宽
33 | 全球 3.1Mbps, 韩国 14.2 Mbps, 美国 8.6Mbps.
34 |
35 | > http://speedtest.net
36 |
37 | Ch2. TCP的构成
38 | -------------
39 |
40 | # TCP三次握手
41 |
42 | TCP三次握手,连接双方都需要经历一次完整的RTT才能开始传输数据.
43 |
44 | > TCP快速打开(TFO): 降低HTTP事务网络延迟15%.Linux 3.7.
45 |
46 | # 拥塞防御与控制
47 |
48 | ## 流量控制
49 |
50 | * 滑动窗口
51 |
52 | - 通告自己的接收窗口`rwnd`
53 |
54 | `优化`:窗口扩大因子选项 `net.ipv4.tcp_window_scaling`
55 |
56 | * 慢启动
57 |
58 | `优化`:提高cwnd初始值为10.以更少的RTT尽快达到带宽的极限,对新建连接,大量短连接的应用帮助很大.Linux 2.6.39(最好是2.6.32).
59 |
60 | - 慢启动重启(Slow-Start Restart, SSR),连接空闲一段时间后,重置cwnd.因为网络情况可能已经发生变化.
61 |
62 | `优化`:关闭`net.ipv4.tcp_window_scaling`
63 |
64 | * 拥塞避免
65 |
66 | 超过慢启动阀值(`ssthresh`)进入拥塞避免(congestion avoidance).发生丢包,则sstresh减半,cwnd设置为新的ssthresh并进入拥塞避免.
67 |
68 | TCP流量曲线呈锯齿状.
69 |
70 | TCP Tahoe/Reno
71 | TCP Vegas/New Reno/BIC/CUBIC/Compound/BBR.
72 |
73 | * 快速重传,快速恢复
74 |
75 | # 带宽时延积
76 |
77 | 窗口大小和RTT决定能否填满带宽时延积BDP,高BDP(长肥管道)情况下,默认的接受窗口可能不足,所以需要窗口扩大因子.
78 |
79 | # 队首阻塞
80 |
81 | 某个分组没收到,后续收到的分组不能交付给应用层.(Head of Line, HOL阻塞).有时不需要按序交付,比如ptp下载,比如每个消息独立(多个HTTP请求),比如每个消息覆盖前一次操作,比如音频,视频.
82 |
83 | # 针对TCP的优化建议
84 |
85 | * TCP三次握手增加整个RTT
86 | * 慢启动并应用到每个连接
87 | * 流量,拥塞控制影响所有连接吞吐量
88 | * 吞吐量由当前拥塞窗口大小控制
89 |
90 | ## 服务器配置调优
91 |
92 | * 增大TCP的初始化拥塞窗口 (升级内核)cwnd-init为10 Linux 3.2+
93 | * 关闭慢启动重启
94 | * 打开窗口扩大因子选项
95 | * TCP快速打开(TFO)
96 |
97 | ## 应用程序行为调优
98 |
99 | * 能少发就少发(减少冗余数据,压缩数据)
100 | * 如果不能让传输更快,就让传输距离缩短(CDN)
101 | * 重用TCP连接是提升性能的关键
102 |
103 | Ch3. UDP的构成
104 | -------------
105 |
106 | 没有周密的计划和规划,一流的构想也会沦为二流的TCP实现.
107 |
108 | # UDP与NAT
109 |
110 | ## 连接状态超时
111 |
112 | TCP有严密的状态机,路由设备可以监控连接状态,根据情况创建删除路由表条目.
113 | UDP没有握手,没有连接终止,没有状态.所以NAT必须保存每个UDP流的信息,只能设置定时器,但是定时多长?可以使用keepalive不断激活NAT设备的计时器.
114 |
115 | ## NAT穿透
116 |
117 | P2P应用需要端到端双向通信
118 |
119 | - STUN
120 | - TURN 中继代理
121 | - ICE
122 |
123 | # 针对UDP的优化建议
124 |
125 | * UDP应用*必须*容忍各种Internet路径条件
126 | * UDP应用*应该*控制传输速度
127 | * UDP应用*应该*对所有流量进行拥塞控制
128 | * UDP应用*应该*使用与TCP相近的带宽
129 | * UDP应用*应该*准备基于丢包的重发机制
130 | * UDP应用*应该*不发送大于PMTU的数据报
131 | * UDP应用*应该*处理报文丢失,重复,乱序
132 | * UDP应用*应该*足够稳定以支持2分钟以上的交付延迟
133 | * UDP应用*应该*支持IPv4 UDP校验和,必须支持IPv6校验和
134 | * UDP应用*可以*在需要时使用keep-alive(最小间隔15秒)
135 |
136 | > WebRTC符合以上要求.
137 |
138 | Ch4. 传输层安全(TLS)
139 | ------------------
140 |
141 | * 1999: TLS 1.0, 对应SSL 3.0,区别不明显,但影响互操作.
142 | * 2006: TLS 1.1
143 | * 2008: TLS 1.2
144 | * 2018: TLS 1.3
145 |
146 | 
147 |
148 | # 加密,身份验证与完整性
149 |
150 | * 加密 (Encrypto)
151 | - 混淆数据的机制.
152 |
153 | * 身份验证(Authentication)
154 | - 验证身份标识有效性的机制
155 |
156 | * 完整性(Integration)
157 | - 检测消息是否被串改或者伪造的机制
158 |
159 | ### 密钥协商
160 |
161 | 连接双方需要就加密数据的*密钥套件*和*密钥*进行协商一致.`非对称密钥加密`,不必通信双方实现"认识",且协商过程通过非加密通道完成.
162 |
163 | ### 身份验证
164 |
165 | 这个验证需要建立"认证机构信任链"(Chain of Trust and Certification Authorities).
166 |
167 | ### 消息封装
168 |
169 | 使用消息验证码(MAC, Message Authentication Code)签署每条消息.MAC是一个单向加密哈希函数,密钥由双方协商确定.发送方发送TLS Record时生成MAC并附加到消息中,接收端通过计算和验证这个MAC值来判断消息的完整性和可靠性.
170 |
171 | HTTP:80被各种设备很好的支持,如果脱离它会造成各种异常(比如私有协议,私有选项无法通过中间设备,防火墙).新的基于HTTP的协议,比如WebSocket, SPDY(HTTP2)基于HTTPS信道,以便绕过中间代理.
172 |
173 | # TLS握手
174 |
175 | 
176 |
177 | 协商:
178 |
179 | * TLS版本
180 | * 加密套件
181 | * 验证证书
182 |
183 | 握手过程,
184 |
185 | - 0ms: 完成三次握手的RTT
186 | - 56ms: Client发送TLS协议版本,所支持的加密套件列表,支持或希望使用的TLS选项
187 | - 84ms: Server选择TLS版本,加密套件; 附上自己的证书.
188 | 可选:发送一个请求,要求Client提供证书及提供TLS扩展参数.
189 | - 112ms: 协商确定版本和加密套件后,客户端生成新的*对称密钥*,用服务器的公钥加密,发送给服务器.
190 | 以上,除了加密的对称密钥,其他全是明文.
191 | - 140ms: 服务器用私钥解密出*对称密钥*后,通过验证MAC检验消息完整性,发送一个加密的"Finish".
192 | - 168ms: 客户端使用之前生成的对称密钥解密Finish,验证MAC.后续开始发送应用数据.
193 |
194 | 相对TCP握手,还多两次RTT,增加延迟.
195 |
196 | # TLS会话恢复
197 |
198 | * TLS额外的握手造成**延时**
199 | * 非对称加密的计算两造成**性能损失**
200 |
201 | Session回复(共享),在多个连接共享协商后的安全密钥.
202 |
203 | ### 会话标识符(Session ID)
204 |
205 | 1. `Session ID`:服务器创建的32 Byte,并作为`ServerHello`的一部分发送.
206 | Server内部,维护Session-ID和协商后的会话参数.包括对称密钥.
207 | 2. Client可以保存Session-ID,并用于随后的`ClientHello`.
208 | 3. Server需要保证Session-ID的缓存及清除.
209 |
210 | 
211 |
212 | 这样,节省一次RTT,以及非对称密钥的加, 解密计算.
213 |
214 | 但是Session-ID对Server内部的维护提出挑战,如何缓存大量的连接信息,如何淘汰不要的连接信息,如何跨服务器共享.
215 |
216 | ### 会话记录单(Session Ticket)
217 |
218 | 为解决Session-ID在服务器端维护的问题,提出了`Session Ticket`机制,不需要Server为每个Client维护会话状态.
219 |
220 | Server在(第一次)完整的TLS握手最后一次报文交换中,添加一条"加密过的" New Session Ticket,只有Server知道如何解密.
221 |
222 | Client收到后保存起来,在后续会话的ClientHello中添加SessionTicket.这样将会话数据保存在客户端.
223 |
224 | 又称"无状态恢复",但Session Ticket仍要解决负载均衡器多Server的问题,比如所有Server共享密钥.并定期轮换密钥.
225 |
226 | # 信任链与证书颁发机构
227 |
228 | 身份验证流程
229 |
230 | # 针对TLS的优化建议
231 |
232 | * 非对称加密计算量大:硬件SSL offload(计算集群) (现在Facebook, Google已经不用专门硬件了)
233 | 硬件的发展使得不需要额外的机器和硬件.
234 | * Session-ID恢复
235 | * Session-Ticket无状态恢复
236 | * 尽量重用TCP连接.
237 | * CDN,代理服务器,缩短距离(一边处理Client的连接,一边和RS保持TLS长连接池)
238 |
239 | Ch9. HTTP简史
240 | ------------
241 |
242 | * HTTP 1.0 1995
243 | - 默认短连接,可通过(`Connection: Keep-Alive`)启用长连接.
244 | * HTTP 1.1 1997 RFC2068, 1999 RFC2616
245 | - 默认使用持久连接(除非`Connection: close`)
246 | - 支持分块编码传输(`Transfer-Encoding: chunked`)在一次message的body返回多个资源.
247 | - 字节范围请求(断点续传)
248 | - 增强的缓存机制(`Cache-Control: max-age=0, no-cache`)
249 | - 传输编码(`Transfer-Encoding`)
250 | - 请求管道
251 | - 内容(MIME),编码,字符集,语言的协商
252 | * HTTP2.0: 2012 (SPDY是HTTP2.0的基础)
253 | - 改善传输性能,实现高吞吐量,低延迟.
254 | - 向前兼容HTTP 1.x
255 |
256 | Ch10. Web性能要点
257 | ---------------
258 |
259 | * 超文本文档
260 | * 富媒体网页
261 | * 交互试Web应用
262 |
263 | > 页面加载时间PLT, Page Load Time.
264 |
265 | # 剖析现代Web应用
266 |
267 | 一个普通的Web应用由下列内容构成(2013)
268 |
269 | * 90 个请求,发送到 15 个主机,总下载量 1311 KB
270 | - HTML:10 个请求,52 KB
271 | - 图片:55 个请求,812 KB
272 | - JavaScript:15 个请求,216 KB
273 | - CSS:5 个请求,36 KB
274 | - 其他资源:5 个请求,195 KB
275 |
276 | 根据用户感觉,需要在250ms内渲染页面.
277 |
278 | * 0~100ms,很快
279 | * 100~300ms, 有点慢
280 | * 300~1000ms,
281 | * > 1000ms, 想离开
282 | * > 10000ms, 不能用
283 |
284 | 分析资源瀑布`http://www.webpagetest.org/`
285 |
286 | # 性能来源:计算,渲染和网络访问
287 |
288 | ## 更多带宽其实不(太)重要
289 |
290 | 带宽很重要.但是对于较小的资源比如网页,RTT就成了瓶颈.
291 |
292 | * 浏览web主页受限于延迟
293 | * 观看视频受限于带宽
294 |
295 | ## 延迟是性能瓶颈
296 |
297 | 
298 |
299 | Ch11. HTTP 1.x
300 | ---------------
301 |
302 | HTTP 1.1引入的重要特性:
303 |
304 | * 连接持久化:以支持连接重用
305 | * 分块传输编码:以支持流式响应
306 | * 请求管道:以支持并行请求处理
307 | * 字节服务:以支持基于范围的资源请求
308 | * 改进的缓存机制
309 |
310 | 性能优化最佳实践:
311 |
312 | * 减少DNS查询
313 | 每次查询都意味着一次RTT,用URL的path而非domain部分来区分资源,以便减少DNS查询.
314 | * 减少HTTP请求
315 | 去掉不必要的HTTP资源请求
316 | * 使用CDN
317 | 地理上减少RTT,增加吞吐量.
318 | * 使用缓存(`Expires`首部,`ETag`标签)
319 | 使用缓存,并制定缓存时间.避免HTTP请求.
320 | * Gzip资源
321 | 压缩资源减少传输.尤其文本的压缩率非常高.
322 | * 避免HTTP重定向
323 | 重定向意味着新的DNC,TCP连接,HTTP请求延迟.
324 |
325 | # 持久连接的优点
326 |
327 | # HTTP管道
328 |
329 | Client同时发起多个请求,不必等服务器完成响应后再发下一个请求.
330 |
331 | 
332 |
333 | 不过HTTP 1.x有个有限制,Response只能严格的串行返回.不影响响应数据交错,所以服务器这边没法并行处理.
334 | 也就是HTTP的**队首阻塞**问题.之前提到过TCP的队首阻塞.
335 |
336 | * 一个慢响应阻塞后续请求(的返回)
337 | * 并行处理时Server需要缓冲响应,占用资源
338 | * 中间代理是否能兼容管道,确保可靠性
339 | * 一个响应失败而终止TCP的话(某个req设置了`Connection: Close`或者关闭连接),会影响其他请求
340 |
341 | 
342 |
343 | # 使用多个TCP连接
344 |
345 | HTTP 1.x不支持多路复用,但是可以打开多个TCP连接,在不同的连接中发送请求.现代浏览器都支持同时打开6个TCP连接.
346 |
347 | 好处不说了,几乎6倍的减少总RTT;坏处是
348 |
349 | * 占用更多Client,Server,及Proxy的资源,包括socket, 内存,CPU.
350 | * TCP之间竞争带宽
351 | * 并发处理,实现复杂性高(开发成本)
352 | * 应用的并行能力
353 |
354 | # 域名区分
355 |
356 | 减少服务器压力,利用并发TCP连接,但增加DNS查询次数.
357 |
358 | Ch12. HTTP 2.0
359 | ---------------
360 |
361 | 更快,更简单,更健壮.将HTTP 1.1提高性能的权益之技一笔购销,把解决问题的方案内置在传输层中.
362 |
363 | * 支持请求,响应多路复用
364 | * 压缩HTTP首部降低开销
365 | * 支持优先级
366 | * 支持服务器推送
367 | * 新的流量控制,错误处理,更新机制
368 | * 向前兼容,不改变HTTP的语义(方法,状态码,URI,headers等)
369 |
370 | # 历史及其与SPDY的渊源
371 |
372 | 2009年Google发布SPDY,解决HTTP 1.1的性能问题,减少网游加载延迟.
373 | 2012年各大厂商(Chrome, Firefox, Opera, 和谷歌,Twitter, Facebook网站支持SPDY.
374 | HTTP-WG开始制定HTTP 2.0标准.
375 |
376 | # 走向HTTP 2.0
377 |
378 | * 改善延迟且可度量
379 | * 解决HTTP头端阻塞
380 | * 并行操作无需多条TCP连接,改进TCP利用率,尤其拥塞控制
381 | * 向前兼容
382 | * 明确和HTTP 1.x的互操作,尤其中间设备
383 | * 新的可扩展机制及策略
384 |
385 | # 设计和技术目标
386 |
387 | ### 二进制分帧层
388 |
389 | HTTP语义(动词,方法,首部,URI)不变,编码方式改成二进制.
390 |
391 | 
392 |
393 | ### 流,消息和帧
394 |
395 | `流` 双向字节流,所有通信在一个TCP连接完成,流是连接中的虚拟通道.有唯一ID.
396 | `消息` 逻辑上的HTTP消息,与HTTP逻辑消息对应的有一系列数据帧(一个或多个)
397 | `帧` HTTP 2.0最小通信单位,包含帧首部,会表述出所在流.
398 |
399 | 
400 |
401 | ### 多向请求与响应
402 |
403 | HTTP 1.x的并行请求必须使用多条TCP连接实现,会导致队首阻塞。HTTP 2.0使用新的二进制分帧突破了这个限制。Client和Server可以吧HTTP消息分解为相互不不依赖的帧,然后乱序发送。
404 |
405 | > 并没有彻底解决队首阻塞,因为使用同一条TCP,依然有TCP层面的队首阻塞问题(见QUIC)。
406 |
407 | 
408 |
409 | ### 请求优先级
410 |
411 | 支持交错传输后,为进一步提升性能,支持31Bit的优先级。 0最高,2^31-1最低。
412 |
413 | ### 每个来源有一个连接
414 |
415 | HTTP 2.0不再依赖多条TCP连接实现多流并行。因此,所有HTTP 2.0连接都是持久化的,且只需要一条长连接。
416 |
417 | ### 流量控制
418 |
419 | 类似滑动窗口的流量控制。
420 |
421 | * 流量控制基于每一跳进行(proxy),而非端到端。
422 | * 流量控制基于*窗口更新*帧,接收方通过自己准备接收某个stream的多少字节,以及整个连接要接收多少字节。
423 | * 流量控制窗口大小使用`WINDOW_UPDATE`帧,指定stream-ID及窗口大小递增值。
424 | * 流量控制有方向性。
425 | * 流量控制可以由接收方禁用。
426 |
427 | ### 服务器推送
428 |
429 | Server可以对一个Client的请求发送多个相应,即处理响应req外,可以额外推送资源。
430 |
431 | 
432 |
433 | ### 首部压缩
434 |
435 | 每次通信携带一组首部,HTTP 1.x中,大约增加500~800字节纯文本,算上cookie,可能有上千字节。
436 |
437 | * HTTP 2.0在Client/Server端使用"Header Table"来跟踪、存储之前发生的`Key-Value`对,对于相同的数据,不再通过Reqeust/Response发送。
438 | * "Header Table"在HTTP 2.0连接周期内始终存在,有Server/Client共同渐进更新。
439 | * 新的Header要么追加到Table,要么替换之前的值。
440 |
441 | 
442 |
443 | # 二进制分帧简介
444 |
445 | 
446 |
447 | * Type
448 | - DATA: 传输HTTP body。
449 | - HEADERS: 传输HTTP Header.
450 | - PRIORITY:指定或重新指定资源的优先级
451 | - RST_STREAM:流的非正常终止。
452 | - SETTINGS:通知两端通信方式的配置数据。
453 | - PUSH_PROMISE:发出创建流和副而且引用资源的要约。
454 | - PING:计算RTT,及Keepalive.
455 | - GOAWAY: 通知对段停止在当前连接中创建流。
456 | - WINDOW_UPDATE:更新流或连接的接收窗口。
457 | - CONTINUATION:继续一系列Header块片段。
458 |
459 | Ch13. 优化应用的交付
460 | ------------------
461 |
462 | 
463 |
464 | 各个层次的优化实践。
465 |
466 | # 经典的性能优化最佳实践
467 |
468 | * 减少DNS的查找
469 |
470 | 查找需要RTT,会阻塞后续请求。可以合并域名减少查找。
471 |
472 | * 重用TCP连接
473 |
474 | 减少3次握手和慢启动延迟。
475 |
476 | * 减少HTTP重定向
477 |
478 | 重定向很浪费时间(DNS/TCP/...),最佳重定向次数为0.
479 |
480 | * 使用CDN
481 |
482 | 不能减少延迟,就缩短RTT。
483 |
484 | * 去掉不必要的资源
485 |
486 | 任何请求都不如没有请求快。
487 |
488 | * 在客户端缓存资源
489 |
490 | `Cache-Control`, `Last-Modified`, `ETag`。
491 |
492 | * 传输压缩过的内容
493 |
494 | 文本的压缩率很高。去掉不必要的元素。选择最优的图片格式和分辨率。
495 |
496 | * 消除不必要的请求开心
497 |
498 | 减少Header数据,cookie。
499 |
500 | * 并行处理请求和响应
501 |
502 | * 针对协议版本采取优化措施
503 |
--------------------------------------------------------------------------------