This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
<audio id="audio" title="43 | Socket通信遇上特大项目要学会和其他公司合作" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1a/07/1a90cc49c2b9f27bc2ed6159f1e6f107.mp3"></audio>
上一篇预习文章说了这么多现在我们终于可以来看一下在应用层我们应该如何使用socket的接口来进行通信。
如果你对socket相关的网络协议原理不是非常了解建议你先去看一看上一篇的预习文章再来看这一篇的内容就会比较轻松。
按照前一篇文章说的分层机制我们可以想到socket接口大多数情况下操作的是传输层更底层的协议不用它来操心这就是分层的好处。
在传输层有两个主流的协议TCP和UDP所以我们的socket程序设计也是主要操作这两个协议。这两个协议的区别是什么呢通常的答案是下面这样的。
- TCP是面向连接的UDP是面向无连接的。
- TCP提供可靠交付无差错、不丢失、不重复、并且按序到达UDP不提供可靠交付不保证不丢失不保证按顺序到达。
- TCP是面向字节流的发送时发的是一个流没头没尾UDP是面向数据报的一个一个地发送。
- TCP是可以提供流量控制和拥塞控制的既防止对端被压垮也防止网络被压垮。
这些答案没有问题,但是没有到达本质,也经常让人产生错觉。例如,下面这些问题,你看看你是否了解?
- 所谓的连接容易让人误以为使用TCP会使得两端之间的通路和使用UDP不一样那我们会在沿途建立一条线表示这个连接吗
- 我从中国访问美国网站,中间这么多环节,我怎么保证连接不断呢?
- 中间有个网络管理员拔了一根网线不就断了吗?我不能控制它,它也不会通知我,我一个个人电脑怎么能够保持连接呢?
- 还让我做流量控制和拥塞控制,我既管不了中间的链路,也管不了对端的服务器呀,我怎么能够做到?
- 按照网络分层TCP和UDP都是基于IP协议的IP都不能保证可靠说丢就丢TCP怎么能够保证呢
- IP层都是一个包一个包地发送TCP怎么就变成流了
从本质上来讲,所谓的**建立连接**其实是为了在客户端和服务端维护连接而建立一定的数据结构来维护双方交互的状态并用这样的数据结构来保证面向连接的特性。TCP无法左右中间的任何通路也没有什么虚拟的连接中间的通路根本意识不到两端使用了TCP还是UDP。
所谓的**连接**就是两端数据结构状态的协同两边的状态能够对得上。符合TCP协议的规则就认为连接存在两面状态对不上连接就算断了。
流量控制和拥塞控制其实就是根据收到的对端的网络包调整两端数据结构的状态。TCP协议的设计理论上认为这样调整了数据结构的状态就能进行流量控制和拥塞控制了其实在通路上是不是真的做到了谁也管不着。
所谓的**可靠**,也是两端的数据结构做的事情。不丢失其实是数据结构在“点名”,顺序到达其实是数据结构在“排序”,面向数据流其实是数据结构将零散的包,按照顺序捏成一个流发给应用层。总而言之,“连接”两个字让人误以为功夫在通路,其实功夫在两端。
当然无论是用socket操作TCP还是UDP我们首先都要调用socket函数。
```
int socket(int domain, int type, int protocol);
```
socket函数用于创建一个socket的文件描述符唯一标识一个socket。我们把它叫作文件描述符因为在内核中我们会创建类似文件系统的数据结构并且后续的操作都有用到它。
socket函数有三个参数。
- domain表示使用什么IP层协议。AF_INET表示IPv4AF_INET6表示IPv6。
- type表示socket类型。SOCK_STREAM顾名思义就是TCP面向流的SOCK_DGRAM就是UDP面向数据报的SOCK_RAW可以直接操作IP层或者非TCP和UDP的协议。例如ICMP。
- protocol表示的协议包括IPPROTO_TCP、IPPTOTO_UDP。
通信结束后我们还要像关闭文件一样关闭socket。
## 针对TCP应该如何编程
接下来我们来看针对TCP我们应该如何编程。
<img src="https://static001.geekbang.org/resource/image/99/da/997e39e5574252ada22220e4b3646dda.png" alt="">
TCP的服务端要先监听一个端口一般是先调用bind函数给这个socket赋予一个端口和IP地址。
```
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
struct in_addr {
__be32 s_addr;
};
```
其中sockfd是上面我们创建的socket文件描述符。在sockaddr_in结构中sin_family设置为AF_INET表示IPv4sin_port是端口号sin_addr是IP地址。
服务端所在的服务器可能有多个网卡、多个地址可以选择监听在一个地址也可以监听0.0.0.0表示所有的地址都监听。服务端一般要监听在一个众所周知的端口上例如Nginx一般是80Tomcat一般是8080。
客户端要访问服务端肯定事先要知道服务端的端口。无论是电商还是游戏还是视频如果你仔细观察会发现都有一个这样的端口。可能你会发现客户端不需要bind因为浏览器嘛随机分配一个端口就可以了只有你主动去连接别人别人不会主动连接你没有人关心客户端监听到了哪里。
如果你看上面代码中的数据结构里面的变量名称都有“be”两个字母代表的意思是“big-endian”。如果在网络上传输超过1 Byte的类型就要区分**大端**Big Endian和**小端**Little Endian
假设我们要在32位4 Bytes的一个空间存放整数1很显然只要1 Byte放1其他3 Bytes放0就可以了。那问题是最后一个Byte放1呢还是第一个Byte放1呢或者说1作为最低位应该放在32位的最后一个位置呢还是放在第一个位置呢
最低位放在最后一个位置我们叫作小端最低位放在第一个位置叫作大端。TCP/IP栈是按照大端来设计的而x86机器多按照小端来设计因而发出去时需要做一个转换。
接下来就要建立TCP的连接了也就是著名的三次握手其实就是将客户端和服务端的状态通过三次网络交互达到初始状态是协同的状态。下图就是三次握手的序列图以及对应的状态转换。
<img src="https://static001.geekbang.org/resource/image/0e/a4/0ef257133471e95bd334383e0155fda4.png" alt="">
接下来服务端要调用listen进入LISTEN状态等待客户端进行连接。
```
int listen(int sockfd, int backlog);
```
连接的建立过程也即三次握手是TCP层的动作是在内核完成的应用层不需要参与。
接着服务端只需要调用accept等待内核完成了至少一个连接的建立才返回。如果没有一个连接完成了三次握手accept就一直等待如果有多个客户端发起连接并且在内核里面完成了多个三次握手建立了多个连接这些连接会被放在一个队列里面。accept会从队列里面取出一个来进行处理。如果想进一步处理其他连接需要调用多次accept所以accept往往在一个循环里面。
```
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
```
接下来客户端可以通过connect函数发起连接。
```
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
```
我们先在参数中指明要连接的IP地址和端口号然后发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功服务端的accept就会返回另一个socket。
这里需要注意的是监听的socket和真正用来传送数据的socket是两个socket一个叫作**监听socket**,一个叫作**已连接socket**。成功连接建立之后双方开始通过read和write函数来读写数据就像往一个文件流里面写东西一样。
## 针对UDP应该如何编程
接下来我们来看针对UDP应该如何编程。
<img src="https://static001.geekbang.org/resource/image/28/b2/283b0e1c21f0277ba5b4b5cbcaca03b2.png" alt="">
UDP是没有连接的所以不需要三次握手也就不需要调用listen和connect但是UDP的交互仍然需要IP地址和端口号因而也需要bind。
对于UDP来讲没有所谓的连接维护也没有所谓的连接的发起方和接收方甚至都不存在客户端和服务端的概念大家就都是客户端也同时都是服务端。只要有一个socket多台机器就可以任意通信不存在哪两台机器是属于一个连接的概念。因此每一个UDP的socket都需要bind。每次通信时调用sendto和recvfrom都要传入IP地址和端口。
```
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
```
## 总结时刻
这一节我们讲了网络协议的基本原理和socket系统调用这里请你重点关注TCP协议的系统调用。
通过学习我们知道socket系统调用是用户态和内核态的接口网络协议的四层以下都是在内核中的。很多的书籍会讲如何开发一个高性能的socket程序但是这不是我们这门课的重点所以我们主要看内核里面的机制就行了。
因此你需要记住TCP协议的socket调用的过程。我们接下来就按照这个顺序依次回忆一下这些系统调用到内核都做了什么
- 服务端和客户端都调用socket得到文件描述符
- 服务端调用listen进行监听
- 服务端调用accept等待客户端连接
- 客户端调用connect连接服务端
- 服务端accept返回用于传输的socket的文件描述符
- 客户端调用write写入数据
- 服务端调用read读取数据。
<img src="https://static001.geekbang.org/resource/image/d3/5c/d34e667d1c3340deb8c82a2d44f2a65c.png" alt="">
## 课堂练习
请你根据今天讲的socket系统调用写一个简单的socket程序来传输一个字符串。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,105 @@
<audio id="audio" title="43 预习 | Socket通信之网络协议基本原理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f7/92/f72ff7d906f3210b035248fd2b899892.mp3"></audio>
上一节我们讲的进程间通信其实是通过内核的数据结构完成的主要用于在一台Linux上两个进程之间的通信。但是一旦超出一台机器的范畴我们就需要一种跨机器的通信机制。
一台机器将自己想要表达的内容,按照某种约定好的格式发送出去,当另外一台机器收到这些信息后,也能够按照约定好的格式解析出来,从而准确、可靠地获得发送方想要表达的内容。这种约定好的格式就是**网络协议**Networking Protocol
我们将要讲的Socket通信以及相关的系统调用、内核机制都是基于网络协议的如果不了解网络协议的机制解析Socket的过程中你就会迷失方向因此这一节我们有必要做一个预习先来大致讲一下网络协议的基本原理。
## 网络为什么要分层?
我们这里先构建一个相对简单的场景,之后几节内容,我们都要基于这个场景进行讲解。
我们假设这里就涉及三台机器。Linux服务器A和Linux服务器B处于不同的网段通过中间的Linux服务器作为路由器进行转发。
<img src="https://static001.geekbang.org/resource/image/f6/0e/f6982eb85dc66bd04200474efb3a050e.png" alt="">
说到网络协议,我们还需要简要介绍一下两种网络协议模型,一种是**OSI的标准七层模型**,一种是**业界标准的TCP/IP模型**。它们的对应关系如下图所示:
<img src="https://static001.geekbang.org/resource/image/92/0e/92f8e85f7b9a9f764c71081b56286e0e.png" alt="">
为什么网络要分层呢?因为网络环境过于复杂,不是一个能够集中控制的体系。全球数以亿记的服务器和设备各有各的体系,但是都可以通过同一套网络协议栈通过切分成多个层次和组合,来满足不同服务器和设备的通信需求。
我们这里简单介绍一下网络协议的几个层次。
我们从哪一个层次开始呢从第三层网络层开始因为这一层有我们熟悉的IP地址。也因此这一层我们也叫IP层。
我们通常看到的IP地址都是这个样子的192.168.1.100/24。斜杠前面是IP地址这个地址被点分隔为四个部分每个部分8位总共是32位。斜线后面24的意思是32位中前24位是网络号后8位是主机号。
为什么要这样分呢?我们可以想象,虽然全世界组成一张大的互联网,美国的网站你也能够访问的,但是这个网络不是一整个的。你们小区有一个网络,你们公司也有一个网络,联通、移动、电信运营商也各有各的网络,所以一个大网络是被分成个小的网络。
那如何区分这些网络呢?这就是网络号的概念。一个网络里面会有多个设备,这些设备的网络号一样,主机号不一样。不信你可以观察一下你家里的手机、电视、电脑。
连接到网络上的每一个设备都至少有一个IP地址用于定位这个设备。无论是近在咫尺的你旁边同学的电脑还是远在天边的电商网站都可以通过IP地址进行定位。因此**IP地址类似互联网上的邮寄地址是有全局定位功能的**。
就算你要访问美国的一个地址也可以从你身边的网络出发通过不断的打听道儿经过多个网络最终到达目的地址和快递员送包裹的过程差不多。打听道儿的协议也在第三层称为路由协议Routing protocol将网络包从一个网络转发给另一个网络的设备称为路由器。
路由器和路由协议十分复杂,我们这里就不详细讲解了,感兴趣可以去看我写的另一个专栏“趣谈网络协议”里的[相关文章](https://time.geekbang.org/column/article/8729)。
总而言之第三层干的事情就是网络包从一个起始的IP地址沿着路由协议指的道儿经过多个网络通过多次路由器转发到达目标IP地址。
从第三层我们往下看第二层是数据链路层。有时候我们简称为二层或者MAC层。所谓MAC就是每个网卡都有的唯一的硬件地址不绝对唯一相对大概率唯一即可类比[UUID](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81))。这虽然也是一个地址,但是这个地址是没有全局定位功能的。
就像给你送外卖的小哥不可能根据手机尾号找到你家但是手机尾号有本地定位功能的只不过这个定位主要靠“吼”。外卖小哥到了你的楼层就开始大喊“尾号xxxx的你外卖到了
MAC地址的定位功能局限在一个网络里面也即同一个网络号下的IP地址之间可以通过MAC进行定位和通信。从IP地址获取MAC地址要通过ARP协议是通过在本地发送广播包也就是“吼”获得的MAC地址。
由于同一个网络内的机器数量有限通过MAC地址的好处就是简单。匹配上MAC地址就接收匹配不上就不接收没有什么所谓路由协议这样复杂的协议。当然坏处就是MAC地址的作用范围不能出本地网络所以一旦跨网络通信虽然IP地址保持不变但是MAC地址每经过一个路由器就要换一次。
我们看前面的图。服务器A发送网络包给服务器B原IP地址始终是192.168.1.100目标IP地址始终是192.168.2.100但是在网络1里面原MAC地址是MAC1目标MAC地址是路由器的MAC2路由器转发之后原MAC地址是路由器的MAC3目标MAC地址是MAC4。
所以第二层干的事情,就是网络包在本地网络中的服务器之间定位及通信的机制。
我们再往下看第一层物理层这一层就是物理设备。例如连着电脑的网线我们能连上的WiFi这一层我们不打算进行分析。
从第三层往上看第四层是传输层这里面有两个著名的协议TCP和UDP。尤其是TCP更是广泛使用在IP层的代码逻辑中仅仅负责数据从一个IP地址发送给另一个IP地址丢包、乱序、重传、拥塞这些IP层都不管。处理这些问题的代码逻辑写在了传输层的TCP协议里面。
我们常称TCP是可靠传输协议也是难为它了。因为从第一层到第三层都不可靠网络包说丢就丢是TCP这一层通过各种编号、重传等机制让本来不可靠的网络对于更上层来讲变得“看起来”可靠。哪有什么应用层岁月静好只不过TCP层帮你负重前行。
传输层再往上就是应用层例如咱们在浏览器里面输入的HTTPJava服务端写的Servlet都是这一层的。
二层到四层都是在Linux内核里面处理的应用层例如浏览器、Nginx、Tomcat都是用户态的。内核里面对于网络包的处理是不区分应用的。
从四层再往上就需要区分网络包发给哪个应用。在传输层的TCP和UDP协议里面都有端口的概念不同的应用监听不同的端口。例如服务端Nginx监听80、Tomcat监听8080再如客户端浏览器监听一个随机端口FTP客户端监听另外一个随机端口。
应用层和内核互通的机制就是通过Socket系统调用。所以经常有人会问Socket属于哪一层其实它哪一层都不属于它属于操作系统的概念而非网络协议分层的概念。只不过操作系统选择对于网络协议的实现模式是二到四层的处理代码在内核里面七层的处理代码让应用自己去做两者需要跨内核态和用户态通信就需要一个系统调用完成这个衔接这就是Socket。
## 发送数据包
网络分完层之后,对于数据包的发送,就是层层封装的过程。
就像下面的图中展示的一样在Linux服务器B上部署的服务端Nginx和Tomcat都是通过Socket监听80和8080端口。这个时候内核的数据结构就知道了。如果遇到发送到这两个端口的就发送给这两个进程。
在Linux服务器A上的客户端打开一个Firefox连接Ngnix。也是通过Socket客户端会被分配一个随机端口12345。同理打开一个Chrome连接Tomcat同样通过Socket分配随机端口12346。
<img src="https://static001.geekbang.org/resource/image/98/28/98a4496fff94eb02d1b1b8ae88f8dc28.jpeg" alt="">
在客户端浏览器我们将请求封装为HTTP协议通过Socket发送到内核。内核的网络协议栈里面在TCP层创建用于维护连接、序列号、重传、拥塞控制的数据结构将HTTP包加上TCP头发送给IP层IP层加上IP头发送给MAC层MAC层加上MAC头从硬件网卡发出去。
网络包会先到达网络1的交换机。我们常称交换机为二层设备这是因为交换机只会处理到第二层然后它会将网络包的MAC头拿下来发现目标MAC是在自己右面的网口于是就从这个网口发出去。
网络包会到达中间的Linux路由器它左面的网卡会收到网络包发现MAC地址匹配就交给IP层在IP层根据IP头中的信息在路由表中查找。下一跳在哪里应该从哪个网口发出去在这个例子中最终会从右面的网口发出去。我们常把路由器称为三层设备因为它只会处理到第三层。
从路由器右面的网口发出去的包会到网络2的交换机还是会经历一次二层的处理转发到交换机右面的网口。
最终网络包会被转发到Linux服务器B它发现MAC地址匹配就将MAC头取下来交给上一层。IP层发现IP地址匹配将IP头取下来交给上一层。TCP层会根据TCP头中的序列号等信息发现它是一个正确的网络包就会将网络包缓存起来等待应用层的读取。
应用层通过Socket监听某个端口因而读取的时候内核会根据TCP头中的端口号将网络包发给相应的应用。
HTTP层的头和正文是应用层来解析的。通过解析应用层知道了客户端的请求例如购买一个商品还是请求一个网页。当应用层处理完HTTP的请求会将结果仍然封装为HTTP的网络包通过Socket接口发送给内核。
内核会经过层层封装从物理网口发送出去经过网络2的交换机Linux路由器到达网络1经过网络1的交换机到达Linux服务器A。在Linux服务器A上经过层层解封装通过socket接口根据客户端的随机端口号发送给客户端的应用程序浏览器。于是浏览器就能够显示出一个绚丽多彩的页面了。
即便在如此简单的一个环境中,网络包的发送过程,竟然如此的复杂。不过这一章后面,我们还是会层层剖析每一层做的事情。
## 总结时刻
网络协议是一个大话题,如果你想了解网络协议的方方面面,欢迎你订阅我写的另一个专栏“趣谈网络协议”。这个专栏重点解析在这个网络通信过程中,发送端和接收端的操作系统都做了哪些事情,对于中间通路上的复杂的网络通信逻辑没有做深入解析。
如果只是为了掌握这一章的内容这一节我们讲的网络协议的七个层次你不必每一层的每一个协议都很清楚只要记住TCP/UDP-&gt;IPv4-&gt;ARP这一条链就可以了因为后面我们的分析都是重点分析这条链。
另外,前面那个简单的拓扑图中,网络包的封装、转发、解封装的过程,建议你多看几遍,了熟于心,因为接下来,我们就能从代码层面,看到这个过程。到时候,对应起来,你就比较容易理解。
了解了Socket的基本原理下一篇文章我们就来看一看在Linux操作系统里面Socket系统调用的接口是什么样的。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,902 @@
<audio id="audio" title="44 | Socket内核数据结构如何成立特大项目合作部" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8d/d2/8d261d757e85d32fa96139356c61a3d2.mp3"></audio>
上一节我们讲了Socket在TCP和UDP场景下的调用流程。这一节我们就沿着这个流程到内核里面一探究竟看看在内核里面都创建了哪些数据结构做了哪些事情。
## 解析socket函数
我们从Socket系统调用开始。
```
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
int flags;
......
if (SOCK_NONBLOCK != O_NONBLOCK &amp;&amp; (flags &amp; SOCK_NONBLOCK))
flags = (flags &amp; ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &amp;sock);
......
retval = sock_map_fd(sock, flags &amp; (O_CLOEXEC | O_NONBLOCK));
......
return retval;
}
```
这里面的代码比较容易看懂Socket系统调用会调用sock_create创建一个struct socket结构然后通过sock_map_fd和文件描述符对应起来。
在创建Socket的时候有三个参数。
一个是**family**表示地址族。不是所有的Socket都要通过IP进行通信还有其他的通信方式。例如下面的定义中domain sockets就是通过本地文件进行通信的不需要IP地址。只不过通过IP地址只是最常用的模式所以我们这里着重分析这种模式。
```
#define AF_UNIX 1/* Unix domain sockets */
#define AF_INET 2/* Internet IP Protocol */
```
第二个参数是**type**也即Socket的类型。类型是比较少的。
第三个参数是**protocol**,是协议。协议数目是比较多的,也就是说,多个协议会属于同一种类型。
常用的Socket类型有三种分别是SOCK_STREAM、SOCK_DGRAM和SOCK_RAW。
```
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
......
}
```
SOCK_STREAM是面向数据流的协议IPPROTO_TCP属于这种类型。SOCK_DGRAM是面向数据报的协议IPPROTO_UDP属于这种类型。如果在内核里面看的话IPPROTO_ICMP也属于这种类型。SOCK_RAW是原始的IP包IPPROTO_IP属于这种类型。
**这一节我们重点看SOCK_STREAM类型和IPPROTO_TCP协议。**
为了管理family、type、protocol这三个分类层次内核会创建对应的数据结构。
接下来我们打开sock_create函数看一下。它会调用__sock_create。
```
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
......
sock = sock_alloc();
......
sock-&gt;type = type;
......
pf = rcu_dereference(net_families[family]);
......
err = pf-&gt;create(net, sock, protocol, kern);
......
*res = sock;
return 0;
}
```
这里先是分配了一个struct socket结构。接下来我们要用到family参数。这里有一个net_families数组我们可以以family参数为下标找到对应的struct net_proto_family。
```
/* Supported address families. */
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
......
#define AF_INET6 10 /* IP version 6 */
......
#define AF_MPLS 28 /* MPLS */
......
#define AF_MAX 44 /* For now.. */
#define NPROTO AF_MAX
struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
```
我们可以找到net_families的定义。每一个地址族在这个数组里面都有一项里面的内容是net_proto_family。每一种地址族都有自己的net_proto_familyIP地址族的net_proto_family定义如下里面最重要的就是create函数指向inet_create。
```
//net/ipv4/af_inet.c
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,//这个用于socket系统调用创建
......
}
```
我们回到函数__sock_create。接下来在这里面这个inet_create会被调用。
```
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct sock *sk;
struct inet_protosw *answer;
struct inet_sock *inet;
struct proto *answer_prot;
unsigned char answer_flags;
int try_loading_module = 0;
int err;
/* Look for the requested type/protocol pair. */
lookup_protocol:
list_for_each_entry_rcu(answer, &amp;inetsw[sock-&gt;type], list) {
err = 0;
/* Check the non-wild match. */
if (protocol == answer-&gt;protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer-&gt;protocol;
break;
}
if (IPPROTO_IP == answer-&gt;protocol)
break;
}
err = -EPROTONOSUPPORT;
}
......
sock-&gt;ops = answer-&gt;ops;
answer_prot = answer-&gt;prot;
answer_flags = answer-&gt;flags;
......
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
......
inet = inet_sk(sk);
inet-&gt;nodefrag = 0;
if (SOCK_RAW == sock-&gt;type) {
inet-&gt;inet_num = protocol;
if (IPPROTO_RAW == protocol)
inet-&gt;hdrincl = 1;
}
inet-&gt;inet_id = 0;
sock_init_data(sock, sk);
sk-&gt;sk_destruct = inet_sock_destruct;
sk-&gt;sk_protocol = protocol;
sk-&gt;sk_backlog_rcv = sk-&gt;sk_prot-&gt;backlog_rcv;
inet-&gt;uc_ttl = -1;
inet-&gt;mc_loop = 1;
inet-&gt;mc_ttl = 1;
inet-&gt;mc_all = 1;
inet-&gt;mc_index = 0;
inet-&gt;mc_list = NULL;
inet-&gt;rcv_tos = 0;
if (inet-&gt;inet_num) {
inet-&gt;inet_sport = htons(inet-&gt;inet_num);
/* Add to protocol hash chains. */
err = sk-&gt;sk_prot-&gt;hash(sk);
}
if (sk-&gt;sk_prot-&gt;init) {
err = sk-&gt;sk_prot-&gt;init(sk);
}
......
}
```
在inet_create中我们先会看到一个循环list_for_each_entry_rcu。在这里第二个参数type开始起作用。因为循环查看的是inetsw[sock-&gt;type]。
这里的inetsw也是一个数组type作为下标里面的内容是struct inet_protosw是协议也即inetsw数组对于每个类型有一项这一项里面是属于这个类型的协议。
```
static struct list_head inetsw[SOCK_MAX];
static int __init inet_init(void)
{
......
/* Register the socket-side information for inet_create. */
for (r = &amp;inetsw[0]; r &lt; &amp;inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);
for (q = inetsw_array; q &lt; &amp;inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
......
}
```
inetsw数组是在系统初始化的时候初始化的就像下面代码里面实现的一样。
首先一个循环会将inetsw数组的每一项都初始化为一个链表。咱们前面说了一个type类型会包含多个protocol因而我们需要一个链表。接下来一个循环是将inetsw_array注册到inetsw数组里面去。inetsw_array的定义如下这个数组里面的内容很重要后面会用到它们。
```
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &amp;tcp_prot,
.ops = &amp;inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &amp;udp_prot,
.ops = &amp;inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &amp;ping_prot,
.ops = &amp;inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &amp;raw_prot,
.ops = &amp;inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
}
```
我们回到inet_create的list_for_each_entry_rcu循环中。到这里就好理解了这是在inetsw数组中根据type找到属于这个类型的列表然后依次比较列表中的struct inet_protosw的protocol是不是用户指定的protocol如果是就得到了符合用户指定的family-&gt;type-&gt;protocol的struct inet_protosw *answer对象。
接下来struct socket *sock的ops成员变量被赋值为answer的ops。对于TCP来讲就是inet_stream_ops。后面任何用户对于这个socket的操作都是通过inet_stream_ops进行的。
接下来我们创建一个struct sock *sk对象。这里比较让人困惑。socket和sock看起来几乎一样容易让人混淆这里需要说明一下socket是用于负责对上给用户提供接口并且和文件系统关联。而sock负责向下对接内核网络协议栈。
在sk_alloc函数中struct inet_protosw *answer结构的tcp_prot赋值给了struct sock *sk的sk_prot成员。tcp_prot的定义如下里面定义了很多的函数都是sock之下内核协议栈的动作。
```
struct proto tcp_prot = {
.name = &quot;TCP&quot;,
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
.hash = inet_hash,
.get_port = inet_csk_get_port,
......
}
```
在inet_create函数中接下来创建一个struct inet_sock结构这个结构一开始就是struct sock然后扩展了一些其他的信息剩下的代码就填充这些信息。这一幕我们会经常看到将一个结构放在另一个结构的开始位置然后扩展一些成员通过对于指针的强制类型转换来访问这些成员。
socket的创建至此结束。
## 解析bind函数
接下来我们来看bind。
```
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
sock = sockfd_lookup_light(fd, &amp;err, &amp;fput_needed);
if (sock) {
err = move_addr_to_kernel(umyaddr, addrlen, &amp;address);
if (err &gt;= 0) {
err = sock-&gt;ops-&gt;bind(sock,
(struct sockaddr *)
&amp;address, addrlen);
}
fput_light(sock-&gt;file, fput_needed);
}
return err;
}
```
在bind中sockfd_lookup_light会根据fd文件描述符找到struct socket结构。然后将sockaddr从用户态拷贝到内核态然后调用struct socket结构里面ops的bind函数。根据前面创建socket的时候的设定调用的是inet_stream_ops的bind函数也即调用inet_bind。
```
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct sock *sk = sock-&gt;sk;
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
unsigned short snum;
......
snum = ntohs(addr-&gt;sin_port);
......
inet-&gt;inet_rcv_saddr = inet-&gt;inet_saddr = addr-&gt;sin_addr.s_addr;
/* Make sure we are allowed to bind here. */
if ((snum || !inet-&gt;bind_address_no_port) &amp;&amp;
sk-&gt;sk_prot-&gt;get_port(sk, snum)) {
......
}
inet-&gt;inet_sport = htons(inet-&gt;inet_num);
inet-&gt;inet_daddr = 0;
inet-&gt;inet_dport = 0;
sk_dst_reset(sk);
}
```
bind里面会调用sk_prot的get_port函数也即inet_csk_get_port来检查端口是否冲突是否可以绑定。如果允许则会设置struct inet_sock的本方的地址inet_saddr和本方的端口inet_sport对方的地址inet_daddr和对方的端口inet_dport都初始化为0。
bind的逻辑相对比较简单就到这里了。
## 解析listen函数
接下来我们来看listen。
```
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &amp;err, &amp;fput_needed);
if (sock) {
somaxconn = sock_net(sock-&gt;sk)-&gt;core.sysctl_somaxconn;
if ((unsigned int)backlog &gt; somaxconn)
backlog = somaxconn;
err = sock-&gt;ops-&gt;listen(sock, backlog);
fput_light(sock-&gt;file, fput_needed);
}
return err;
}
```
在listen中我们还是通过sockfd_lookup_light根据fd文件描述符找到struct socket结构。接着我们调用struct socket结构里面ops的listen函数。根据前面创建socket的时候的设定调用的是inet_stream_ops的listen函数也即调用inet_listen。
```
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock-&gt;sk;
unsigned char old_state;
int err;
old_state = sk-&gt;sk_state;
/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
err = inet_csk_listen_start(sk, backlog);
}
sk-&gt;sk_max_ack_backlog = backlog;
}
```
如果这个socket还不在TCP_LISTEN状态会调用inet_csk_listen_start进入监听状态。
```
int inet_csk_listen_start(struct sock *sk, int backlog)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet = inet_sk(sk);
int err = -EADDRINUSE;
reqsk_queue_alloc(&amp;icsk-&gt;icsk_accept_queue);
sk-&gt;sk_max_ack_backlog = backlog;
sk-&gt;sk_ack_backlog = 0;
inet_csk_delack_init(sk);
sk_state_store(sk, TCP_LISTEN);
if (!sk-&gt;sk_prot-&gt;get_port(sk, inet-&gt;inet_num)) {
......
}
......
}
```
这里面建立了一个新的结构inet_connection_sock这个结构一开始是struct inet_sockinet_csk其实做了一次强制类型转换扩大了结构看到了吧又是这个套路。
struct inet_connection_sock结构比较复杂。如果打开它你能看到处于各种状态的队列各种超时时间、拥塞控制等字眼。我们说TCP是面向连接的就是客户端和服务端都是有一个结构维护连接的状态就是指这个结构。我们这里先不详细分析里面的变量因为太多了后面我们遇到一个分析一个。
首先我们遇到的是icsk_accept_queue。它是干什么的呢
在TCP的状态里面有一个listen状态当调用listen函数之后就会进入这个状态虽然我们写程序的时候一般要等待服务端调用accept后等待在哪里的时候让客户端就发起连接。其实服务端一旦处于listen状态不用accept客户端也能发起连接。其实TCP的状态中没有一个是否被accept的状态那accept函数的作用是什么呢
在内核中为每个Socket维护两个队列。一个是已经建立了连接的队列这时候连接三次握手已经完毕处于established状态一个是还没有完全建立连接的队列这个时候三次握手还没完成处于syn_rcvd的状态。
服务端调用accept函数其实是在第一个队列中拿出一个已经完成的连接进行处理。如果还没有完成就阻塞等待。这里的icsk_accept_queue就是第一个队列。
初始化完之后将TCP的状态设置为TCP_LISTEN再次调用get_port判断端口是否冲突。
至此listen的逻辑就结束了。
## 解析accept函数
接下来我们解析服务端调用accept。
```
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen)
{
return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen, int, flags)
{
struct socket *sock, *newsock;
struct file *newfile;
int err, len, newfd, fput_needed;
struct sockaddr_storage address;
......
sock = sockfd_lookup_light(fd, &amp;err, &amp;fput_needed);
newsock = sock_alloc();
newsock-&gt;type = sock-&gt;type;
newsock-&gt;ops = sock-&gt;ops;
newfd = get_unused_fd_flags(flags);
newfile = sock_alloc_file(newsock, flags, sock-&gt;sk-&gt;sk_prot_creator-&gt;name);
err = sock-&gt;ops-&gt;accept(sock, newsock, sock-&gt;file-&gt;f_flags, false);
if (upeer_sockaddr) {
if (newsock-&gt;ops-&gt;getname(newsock, (struct sockaddr *)&amp;address, &amp;len, 2) &lt; 0) {
}
err = move_addr_to_user(&amp;address,
len, upeer_sockaddr, upeer_addrlen);
}
fd_install(newfd, newfile);
......
}
```
accept函数的实现印证了socket的原理中说的那样原来的socket是监听socket这里我们会找到原来的struct socket并基于它去创建一个新的newsock。这才是连接socket。除此之外我们还会创建一个新的struct file和fd并关联到socket。
这里面还会调用struct socket的sock-&gt;ops-&gt;accept也即会调用inet_stream_ops的accept函数也即inet_accept。
```
int inet_accept(struct socket *sock, struct socket *newsock, int flags, bool kern)
{
struct sock *sk1 = sock-&gt;sk;
int err = -EINVAL;
struct sock *sk2 = sk1-&gt;sk_prot-&gt;accept(sk1, flags, &amp;err, kern);
sock_rps_record_flow(sk2);
sock_graft(sk2, newsock);
newsock-&gt;state = SS_CONNECTED;
}
```
inet_accept会调用struct sock的sk1-&gt;sk_prot-&gt;accept也即tcp_prot的accept函数inet_csk_accept函数。
```
/*
* This will accept the next outstanding connection.
*/
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &amp;icsk-&gt;icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
int error;
if (sk-&gt;sk_state != TCP_LISTEN)
goto out_err;
/* Find already established connection */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags &amp; O_NONBLOCK);
error = inet_csk_wait_for_connect(sk, timeo);
}
req = reqsk_queue_remove(queue, sk);
newsk = req-&gt;sk;
......
}
/*
* Wait for an incoming connection, avoid race conditions. This must be called
* with the socket locked.
*/
static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
struct inet_connection_sock *icsk = inet_csk(sk);
DEFINE_WAIT(wait);
int err;
for (;;) {
prepare_to_wait_exclusive(sk_sleep(sk), &amp;wait,
TASK_INTERRUPTIBLE);
release_sock(sk);
if (reqsk_queue_empty(&amp;icsk-&gt;icsk_accept_queue))
timeo = schedule_timeout(timeo);
sched_annotate_sleep();
lock_sock(sk);
err = 0;
if (!reqsk_queue_empty(&amp;icsk-&gt;icsk_accept_queue))
break;
err = -EINVAL;
if (sk-&gt;sk_state != TCP_LISTEN)
break;
err = sock_intr_errno(timeo);
if (signal_pending(current))
break;
err = -EAGAIN;
if (!timeo)
break;
}
finish_wait(sk_sleep(sk), &amp;wait);
return err;
}
```
inet_csk_accept的实现印证了上面我们讲的两个队列的逻辑。如果icsk_accept_queue为空则调用inet_csk_wait_for_connect进行等待等待的时候调用schedule_timeout让出CPU并且将进程状态设置为TASK_INTERRUPTIBLE。
如果再次CPU醒来我们会接着判断icsk_accept_queue是否为空同时也会调用signal_pending看有没有信号可以处理。一旦icsk_accept_queue不为空就从inet_csk_wait_for_connect中返回在队列中取出一个struct sock对象赋值给newsk。
## 解析connect函数
什么情况下icsk_accept_queue才不为空呢当然是三次握手结束才可以。接下来我们来分析三次握手的过程。
<img src="https://static001.geekbang.org/resource/image/ab/df/ab92c2afb4aafb53143c471293ccb2df.png" alt="">
三次握手一般是由客户端调用connect发起。
```
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
sock = sockfd_lookup_light(fd, &amp;err, &amp;fput_needed);
err = move_addr_to_kernel(uservaddr, addrlen, &amp;address);
err = sock-&gt;ops-&gt;connect(sock, (struct sockaddr *)&amp;address, addrlen, sock-&gt;file-&gt;f_flags);
}
```
connect函数的实现一开始你应该很眼熟还是通过sockfd_lookup_light根据fd文件描述符找到struct socket结构。接着我们会调用struct socket结构里面ops的connect函数根据前面创建socket的时候的设定调用inet_stream_ops的connect函数也即调用inet_stream_connect。
```
/*
* Connect to a remote host. There is regrettably still a little
* TCP 'magic' in here.
*/
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags, int is_sendmsg)
{
struct sock *sk = sock-&gt;sk;
int err;
long timeo;
switch (sock-&gt;state) {
......
case SS_UNCONNECTED:
err = -EISCONN;
if (sk-&gt;sk_state != TCP_CLOSE)
goto out;
err = sk-&gt;sk_prot-&gt;connect(sk, uaddr, addr_len);
sock-&gt;state = SS_CONNECTING;
break;
}
timeo = sock_sndtimeo(sk, flags &amp; O_NONBLOCK);
if ((1 &lt;&lt; sk-&gt;sk_state) &amp; (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
......
if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
goto out;
err = sock_intr_errno(timeo);
if (signal_pending(current))
goto out;
}
sock-&gt;state = SS_CONNECTED;
}
```
在__inet_stream_connect里面我们发现如果socket处于SS_UNCONNECTED状态那就调用struct sock的sk-&gt;sk_prot-&gt;connect也即tcp_prot的connect函数——tcp_v4_connect函数。
```
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
struct flowi4 *fl4;
struct rtable *rt;
......
orig_sport = inet-&gt;inet_sport;
orig_dport = usin-&gt;sin_port;
rt = ip_route_connect(fl4, nexthop, inet-&gt;inet_saddr,
RT_CONN_FLAGS(sk), sk-&gt;sk_bound_dev_if,
IPPROTO_TCP,
orig_sport, orig_dport, sk);
......
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
sk_set_txhash(sk);
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet-&gt;inet_sport, inet-&gt;inet_dport, sk);
/* OK, now commit destination to socket. */
sk-&gt;sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &amp;rt-&gt;dst);
if (likely(!tp-&gt;repair)) {
if (!tp-&gt;write_seq)
tp-&gt;write_seq = secure_tcp_seq(inet-&gt;inet_saddr,
inet-&gt;inet_daddr,
inet-&gt;inet_sport,
usin-&gt;sin_port);
tp-&gt;tsoffset = secure_tcp_ts_off(sock_net(sk),
inet-&gt;inet_saddr,
inet-&gt;inet_daddr);
}
rt = NULL;
......
err = tcp_connect(sk);
......
}
```
在tcp_v4_connect函数中ip_route_connect其实是做一个路由的选择。为什么呢因为三次握手马上就要发送一个SYN包了这就要凑齐源地址、源端口、目标地址、目标端口。目标地址和目标端口是服务端的已经知道源端口是客户端随机分配的源地址应该用哪一个呢这时候要选择一条路由看从哪个网卡出去就应该填写哪个网卡的IP地址。
接下来在发送SYN之前我们先将客户端socket的状态设置为TCP_SYN_SENT。然后初始化TCP的seq num也即write_seq然后调用tcp_connect进行发送。
```
/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;
......
tcp_connect_init(sk);
......
buff = sk_stream_alloc_skb(sk, 0, sk-&gt;sk_allocation, true);
......
tcp_init_nondata_skb(buff, tp-&gt;write_seq++, TCPHDR_SYN);
tcp_mstamp_refresh(tp);
tp-&gt;retrans_stamp = tcp_time_stamp(tp);
tcp_connect_queue_skb(sk, buff);
tcp_ecn_send_syn(sk, buff);
/* Send off SYN; include data in Fast Open. */
err = tp-&gt;fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk-&gt;sk_allocation);
......
tp-&gt;snd_nxt = tp-&gt;write_seq;
tp-&gt;pushed_seq = tp-&gt;write_seq;
buff = tcp_send_head(sk);
if (unlikely(buff)) {
tp-&gt;snd_nxt = TCP_SKB_CB(buff)-&gt;seq;
tp-&gt;pushed_seq = TCP_SKB_CB(buff)-&gt;seq;
}
......
/* Timer for repeating the SYN until an answer. */
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)-&gt;icsk_rto, TCP_RTO_MAX);
return 0;
}
```
在tcp_connect中有一个新的结构struct tcp_sock如果打开他你会发现他是struct inet_connection_sock的一个扩展struct inet_connection_sock在struct tcp_sock开头的位置通过强制类型转换访问故伎重演又一次。
struct tcp_sock里面维护了更多的TCP的状态咱们同样是遇到了再分析。
接下来tcp_init_nondata_skb初始化一个SYN包tcp_transmit_skb将SYN包发送出去inet_csk_reset_xmit_timer设置了一个timer如果SYN发送不成功则再次发送。
发送网络包的过程我们放到下一节讲解。这里我们姑且认为SYN已经发送出去了。
我们回到__inet_stream_connect函数在调用sk-&gt;sk_prot-&gt;connect之后inet_wait_for_connect会一直等待客户端收到服务端的ACK。而我们知道服务端在accept之后也是在等待中。
网络包是如何接收的呢对于解析的详细过程我们会在下下节讲解这里为了解析三次握手我们简单的看网络包接收到TCP层做的部分事情。
```
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
}
```
我们通过struct net_protocol结构中的handler进行接收调用的函数是tcp_v4_rcv。接下来的调用链为tcp_v4_rcv-&gt;tcp_v4_do_rcv-&gt;tcp_rcv_state_process。tcp_rcv_state_process顾名思义是用来处理接收一个网络包后引起状态变化的。
```
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
switch (sk-&gt;sk_state) {
......
case TCP_LISTEN:
......
if (th-&gt;syn) {
acceptable = icsk-&gt;icsk_af_ops-&gt;conn_request(sk, skb) &gt;= 0;
if (!acceptable)
return 1;
consume_skb(skb);
return 0;
}
......
}
```
目前服务端是处于TCP_LISTEN状态的而且发过来的包是SYN因而就有了上面的代码调用icsk-&gt;icsk_af_ops-&gt;conn_request函数。struct inet_connection_sock对应的操作是inet_connection_sock_af_ops按照下面的定义其实调用的是tcp_v4_conn_request。
```
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.mtu_reduced = tcp_v4_mtu_reduced,
};
```
tcp_v4_conn_request会调用tcp_conn_request这个函数也比较长里面调用了send_synack但实际调用的是tcp_v4_send_synack。具体发送的过程我们不去管它看注释我们能知道这是收到了SYN后回复一个SYN-ACK回复完毕后服务端处于TCP_SYN_RECV。
```
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
......
af_ops-&gt;send_synack(sk, dst, &amp;fl, req, &amp;foc,
!want_cookie ? TCP_SYNACK_NORMAL :
TCP_SYNACK_COOKIE);
......
}
/*
* Send a SYN-ACK after having received a SYN.
*/
static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
struct flowi *fl,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
enum tcp_synack_type synack_type)
{......}
```
这个时候轮到客户端接收网络包了。都是TCP协议栈所以过程和服务端没有太多区别还是会走到tcp_rcv_state_process函数的只不过由于客户端目前处于TCP_SYN_SENT状态就进入了下面的代码分支。
```
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
switch (sk-&gt;sk_state) {
......
case TCP_SYN_SENT:
tp-&gt;rx_opt.saw_tstamp = 0;
tcp_mstamp_refresh(tp);
queued = tcp_rcv_synsent_state_process(sk, skb, th);
if (queued &gt;= 0)
return queued;
/* Do step6 onward by hand. */
tcp_urg(sk, skb, th);
__kfree_skb(skb);
tcp_data_snd_check(sk);
return 0;
}
......
}
```
tcp_rcv_synsent_state_process会调用tcp_send_ack发送一个ACK-ACK发送后客户端处于TCP_ESTABLISHED状态。
又轮到服务端接收网络包了我们还是归tcp_rcv_state_process函数处理。由于服务端目前处于状态TCP_SYN_RECV状态因而又走了另外的分支。当收到这个网络包的时候服务端也处于TCP_ESTABLISHED状态三次握手结束。
```
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
......
switch (sk-&gt;sk_state) {
case TCP_SYN_RECV:
if (req) {
inet_csk(sk)-&gt;icsk_retransmits = 0;
reqsk_fastopen_remove(sk, req, false);
} else {
/* Make sure socket is routed, for correct metrics. */
icsk-&gt;icsk_af_ops-&gt;rebuild_header(sk);
tcp_call_bpf(sk, BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB);
tcp_init_congestion_control(sk);
tcp_mtup_init(sk);
tp-&gt;copied_seq = tp-&gt;rcv_nxt;
tcp_init_buffer_space(sk);
}
smp_mb();
tcp_set_state(sk, TCP_ESTABLISHED);
sk-&gt;sk_state_change(sk);
if (sk-&gt;sk_socket)
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
tp-&gt;snd_una = TCP_SKB_CB(skb)-&gt;ack_seq;
tp-&gt;snd_wnd = ntohs(th-&gt;window) &lt;&lt; tp-&gt;rx_opt.snd_wscale;
tcp_init_wl(tp, TCP_SKB_CB(skb)-&gt;seq);
break;
......
}
```
## 总结时刻
这一节除了网络包的接收和发送,其他的系统调用我们都分析到了。可以看出来,它们有一个统一的数据结构和流程。具体如下图所示:
<img src="https://static001.geekbang.org/resource/image/c0/d8/c028381cf45d65d3f148e57408d26bd8.png" alt="">
首先Socket系统调用会有三级参数family、type、protocal通过这三级参数分别在net_proto_family表中找到type链表在type链表中找到protocal对应的操作。这个操作分为两层对于TCP协议来讲第一层是inet_stream_ops层第二层是tcp_prot层。
于是,接下来的系统调用规律就都一样了:
- bind第一层调用inet_stream_ops的inet_bind函数第二层调用tcp_prot的inet_csk_get_port函数
- listen第一层调用inet_stream_ops的inet_listen函数第二层调用tcp_prot的inet_csk_get_port函数
- accept第一层调用inet_stream_ops的inet_accept函数第二层调用tcp_prot的inet_csk_accept函数
- connect第一层调用inet_stream_ops的inet_stream_connect函数第二层调用tcp_prot的tcp_v4_connect函数。
## 课堂练习
TCP的三次握手协议非常重要请你务必跟着代码走读一遍。另外我们这里重点关注了TCP的场景请走读代码的时候也看一下UDP是如何实现各层的函数的。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。

View File

@@ -0,0 +1,498 @@
<audio id="audio" title="45 | 发送网络包(上):如何表达我们想让合作伙伴做什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d7/c6/d7a052488b1ddd0e4e4a70f6832f33c6.mp3"></audio>
上一节我们通过socket函数、bind函数、listen函数、accept函数以及connect函数在内核建立好了数据结构并完成了TCP连接建立的三次握手过程。
这一节,我们接着来分析,发送一个网络包的过程。
## 解析socket的Write操作
socket对于用户来讲是一个文件一样的存在拥有一个文件描述符。因而对于网络包的发送我们可以使用对于socket文件的写入系统调用也就是write系统调用。
write系统调用对于一个文件描述符的操作大致过程都是类似的。在文件系统那一节我们已经详细解析过这里不再多说。对于每一个打开的文件都有一个struct file结构write系统调用会最终调用stuct file结构指向的file_operations操作。
对于socket来讲它的file_operations定义如下
```
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
```
按照文件系统的写入流程调用的是sock_write_iter。
```
static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *file = iocb-&gt;ki_filp;
struct socket *sock = file-&gt;private_data;
struct msghdr msg = {.msg_iter = *from,
.msg_iocb = iocb};
ssize_t res;
......
res = sock_sendmsg(sock, &amp;msg);
*from = msg.msg_iter;
return res;
}
```
在sock_write_iter中我们通过VFS中的struct file将创建好的socket结构拿出来然后调用sock_sendmsg。而sock_sendmsg会调用sock_sendmsg_nosec。
```
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = sock-&gt;ops-&gt;sendmsg(sock, msg, msg_data_left(msg));
......
}
```
这里调用了socket的ops的sendmsg我们在上一节已经遇到它好几次了。根据inet_stream_ops的定义我们这里调用的是inet_sendmsg。
```
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock-&gt;sk;
......
return sk-&gt;sk_prot-&gt;sendmsg(sk, msg, size);
}
```
这里面从socket结构中我们可以得到更底层的sock结构然后调用sk_prot的sendmsg方法。这个我们同样在上一节遇到好几次了。
## 解析tcp_sendmsg函数
根据tcp_prot的定义我们调用的是tcp_sendmsg。
```
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
int flags, err, copied = 0;
int mss_now = 0, size_goal, copied_syn = 0;
long timeo;
......
/* Ok commence sending. */
copied = 0;
restart:
mss_now = tcp_send_mss(sk, &amp;size_goal, flags);
while (msg_data_left(msg)) {
int copy = 0;
int max = size_goal;
skb = tcp_write_queue_tail(sk);
if (tcp_send_head(sk)) {
if (skb-&gt;ip_summed == CHECKSUM_NONE)
max = mss_now;
copy = max - skb-&gt;len;
}
if (copy &lt;= 0 || !tcp_skb_can_collapse_to(skb)) {
bool first_skb;
new_segment:
/* Allocate new segment. If the interface is SG,
* allocate skb fitting to single page.
*/
if (!sk_stream_memory_free(sk))
goto wait_for_sndbuf;
......
first_skb = skb_queue_empty(&amp;sk-&gt;sk_write_queue);
skb = sk_stream_alloc_skb(sk,
select_size(sk, sg, first_skb),
sk-&gt;sk_allocation,
first_skb);
......
skb_entail(sk, skb);
copy = size_goal;
max = size_goal;
......
}
/* Try to append data to the end of skb. */
if (copy &gt; msg_data_left(msg))
copy = msg_data_left(msg);
/* Where to copy to? */
if (skb_availroom(skb) &gt; 0) {
/* We have some space in skb head. Superb! */
copy = min_t(int, copy, skb_availroom(skb));
err = skb_add_data_nocache(sk, skb, &amp;msg-&gt;msg_iter, copy);
......
} else {
bool merge = true;
int i = skb_shinfo(skb)-&gt;nr_frags;
struct page_frag *pfrag = sk_page_frag(sk);
......
copy = min_t(int, copy, pfrag-&gt;size - pfrag-&gt;offset);
......
err = skb_copy_to_page_nocache(sk, &amp;msg-&gt;msg_iter, skb,
pfrag-&gt;page,
pfrag-&gt;offset,
copy);
......
pfrag-&gt;offset += copy;
}
......
tp-&gt;write_seq += copy;
TCP_SKB_CB(skb)-&gt;end_seq += copy;
tcp_skb_pcount_set(skb, 0);
copied += copy;
if (!msg_data_left(msg)) {
if (unlikely(flags &amp; MSG_EOR))
TCP_SKB_CB(skb)-&gt;eor = 1;
goto out;
}
if (skb-&gt;len &lt; max || (flags &amp; MSG_OOB) || unlikely(tp-&gt;repair))
continue;
if (forced_push(tp)) {
tcp_mark_push(tp, skb);
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
} else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now);
continue;
......
}
......
}
```
tcp_sendmsg的实现还是很复杂的这里面做了这样几件事情。
msg是用户要写入的数据这个数据要拷贝到内核协议栈里面去发送在内核协议栈里面网络包的数据都是由struct sk_buff维护的因而第一件事情就是找到一个空闲的内存空间将用户要写入的数据拷贝到struct sk_buff的管辖范围内。而第二件事情就是发送struct sk_buff。
在tcp_sendmsg中我们首先通过强制类型转换将sock结构转换为struct tcp_sock这个是维护TCP连接状态的重要数据结构。
接下来是tcp_sendmsg的第一件事情把数据拷贝到struct sk_buff。
我们先声明一个变量copied初始化为0这表示拷贝了多少数据。紧接着是一个循环while (msg_data_left(msg))也即如果用户的数据没有发送完毕就一直循环。循环里声明了一个copy变量表示这次拷贝的数值在循环的最后有copied += copy将每次拷贝的数量都加起来。
我们这里只需要看一次循环做了哪些事情。
**第一步**tcp_write_queue_tail从TCP写入队列sk_write_queue中拿出最后一个struct sk_buff在这个写入队列中排满了要发送的struct sk_buff为什么要拿最后一个呢这里面只有最后一个可能会因为上次用户给的数据太少而没有填满。
**第二步**tcp_send_mss会计算MSS也即Max Segment Size。这是什么呢这个意思是说我们在网络上传输的网络包的大小是有限制的而这个限制在最底层开始就有。
**MTU**Maximum Transmission Unit最大传输单元是二层的一个定义。以以太网为例MTU为1500个Byte前面有6个Byte的目标MAC地址6个Byte的源MAC地址2个Byte的类型后面有4个Byte的CRC校验共1518个Byte。
在IP层一个IP数据报在以太网中传输如果它的长度大于该MTU值就要进行分片传输。
在TCP层有个**MSS**Maximum Segment Size最大分段大小等于MTU减去IP头再减去TCP头。也就是在不分片的情况下TCP里面放的最大内容。
在这里max是struct sk_buff的最大数据长度skb-&gt;len是当前已经占用的skb的数据长度相减得到当前skb的剩余数据空间。
**第三步**如果copy小于0说明最后一个struct sk_buff已经没地方存放了需要调用sk_stream_alloc_skb重新分配struct sk_buff然后调用skb_entail将新分配的sk_buff放到队列尾部。
struct sk_buff是存储网络包的重要的数据结构在应用层数据包叫data在TCP层我们称为segment在IP层我们叫packet在数据链路层称为frame。在struct sk_buff首先是一个链表将struct sk_buff结构串起来。
接下来我们从headers_start开始到headers_end结束里面都是各层次的头的位置。这里面有二层的mac_header、三层的network_header和四层的transport_header。
```
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
......
};
struct rb_node rbnode; /* used in netem &amp; tcp stack */
};
......
/* private: */
__u32 headers_start[0];
/* public: */
......
__u32 priority;
int skb_iif;
__u32 hash;
__be16 vlan_proto;
__u16 vlan_tci;
......
union {
__u32 mark;
__u32 reserved_tailroom;
};
union {
__be16 inner_protocol;
__u8 inner_ipproto;
};
__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
__be16 protocol;
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
/* private: */
__u32 headers_end[0];
/* public: */
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
unsigned int truesize;
refcount_t users;
};
```
最后几项, head指向分配的内存块起始地址。data这个指针指向的位置是可变的。它有可能随着报文所处的层次而变动。当接收报文时从网卡驱动开始通过协议栈层层往上传送数据报通过增加 skb-&gt;data 的值,来逐步剥离协议首部。而要发送报文时,各协议会创建 sk_buff{},在经过各下层协议时,通过减少 skb-&gt;data的值来增加协议首部。tail指向数据的结尾end指向分配的内存块的结束地址。
要分配这样一个结构sk_stream_alloc_skb会最终调用到__alloc_skb。在这个函数里面除了分配一个sk_buff结构之外还要分配sk_buff指向的数据区域。这段数据区域分为下面这几个部分。
第一部分是连续的数据区域。紧接着是第二部分一个struct skb_shared_info结构。这个结构是对于网络包发送过程的一个优化因为传输层之上就是应用层了。按照TCP的定义应用层感受不到下面的网络层的IP包是一个个独立的包的存在的。反正就是一个流往里写就是了可能一下子写多了超过了一个IP包的承载能力就会出现上面MSS的定义拆分成一个个的Segment放在一个个的IP包里面也可能一次写一点一次写一点这样数据是分散的在IP层还要通过内存拷贝合成一个IP包。
为了减少内存拷贝的代价,有的网络设备支持**分散聚合**Scatter/GatherI/O顾名思义就是IP层没必要通过内存拷贝进行聚合让散的数据零散的放在原处在设备层进行聚合。如果使用这种模式网络包的数据就不会放在连续的数据区域而是放在struct skb_shared_info结构里面指向的离散数据skb_shared_info的成员变量skb_frag_t frags[MAX_SKB_FRAGS],会指向一个数组的页面,就不能保证连续了。
<img src="https://static001.geekbang.org/resource/image/9a/b8/9ad34c3c748978f915027d5085a858b8.png" alt="">
于是我们就有了**第四步**。在注释/* Where to copy to? */后面有个if-else分支。if分支就是skb_add_data_nocache将数据拷贝到连续的数据区域。else分支就是skb_copy_to_page_nocache将数据拷贝到struct skb_shared_info结构指向的不需要连续的页面区域。
**第五步**就是要发生网络包了。第一种情况是积累的数据报数目太多了因而我们需要通过调用__tcp_push_pending_frames发送网络包。第二种情况是这是第一个网络包需要马上发送调用tcp_push_one。无论__tcp_push_pending_frames还是tcp_push_one都会调用tcp_write_xmit发送网络包。
至此tcp_sendmsg解析完了。
## 解析tcp_write_xmit函数
接下来我们来看tcp_write_xmit是如何发送网络包的。
```
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
......
max_segs = tcp_tso_segs(sk, mss_now);
while ((skb = tcp_send_head(sk))) {
unsigned int limit;
......
tso_segs = tcp_init_tso_segs(skb, mss_now);
......
cwnd_quota = tcp_cwnd_test(tp, skb);
......
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
is_rwnd_limited = true;
break;
}
......
limit = mss_now;
if (tso_segs &gt; 1 &amp;&amp; !tcp_urg_mode(tp))
limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);
if (skb-&gt;len &gt; limit &amp;&amp;
unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
break;
......
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
break;
repair:
/* Advance the send_head. This one is sent out.
* This call will increment packets_out.
*/
tcp_event_new_data_sent(sk, skb);
tcp_minshall_update(tp, mss_now, skb);
sent_pkts += tcp_skb_pcount(skb);
if (push_one)
break;
}
......
}
```
这里面主要的逻辑是一个循环,用来处理发送队列,只要队列不空,就会发送。
在一个循环中涉及TCP层的很多传输算法我们来一一解析。
第一个概念是**TSO**TCP Segmentation Offload。如果发送的网络包非常大就像上面说的一样要进行分段。分段这个事情可以由协议栈代码在内核做但是缺点是比较费CPU另一种方式是延迟到硬件网卡去做需要网卡支持对大数据包进行自动分段可以降低CPU负载。
在代码中tcp_init_tso_segs会调用tcp_set_skb_tso_segs。这里面有这样的语句DIV_ROUND_UP(skb-&gt;len, mss_now)。也就是sk_buff的长度除以mss_now应该分成几个段。如果算出来要分成多个段接下来就是要看是在这里协议栈的代码里面分好还是等待到了底层网卡再分。
于是调用函数tcp_mss_split_point开始计算切分的limit。这里面会计算max_len = mss_now * max_segs根据现在不切分来计算limit所以下一步的判断中大部分情况下tso_fragment不会被调用等待到了底层网卡来切分。
第二个概念是**拥塞窗口**的概念cwndcongestion window也就是说为了避免拼命发包把网络塞满了定义一个窗口的概念在这个窗口之内的才能发送超过这个窗口的就不能发送来控制发送的频率。
那窗口大小是多少呢?就是遵循下面这个著名的拥塞窗口变化图。
<img src="https://static001.geekbang.org/resource/image/40/1f/404a6c5041452c0641ae3cba5319dc1f.png" alt="">
一开始的窗口只有一个mss大小叫作slow start慢启动。一开始的增长速度的很快的翻倍增长。一旦到达一个临界值ssthresh就变成线性增长我们就称为**拥塞避免**。什么时候算真正拥塞呢就是出现了丢包。一旦丢包一种方法是马上降回到一个mss然后重复先翻倍再线性对的过程。如果觉得太过激进也可以有第二种方法就是降到当前cwnd的一半然后进行线性增长。
在代码中tcp_cwnd_test会将当前的snd_cwnd减去已经在窗口里面尚未发送完毕的网络包那就是剩下的窗口大小cwnd_quota也即就能发送这么多了。
第三个概念就是**接收窗口**rwnd的概念receive window也叫滑动窗口。如果说拥塞窗口是为了怕把网络塞满在出现丢包的时候减少发送速度那么滑动窗口就是为了怕把接收方塞满而控制发送速度。
<img src="https://static001.geekbang.org/resource/image/97/65/9791e2f9ff63a9d8f849df7cd55fe965.png" alt="">
滑动窗口,其实就是接收方告诉发送方自己的网络包的接收能力,超过这个能力,我就受不了了。因为滑动窗口的存在,将发送方的缓存分成了四个部分。
- 第一部分:发送了并且已经确认的。这部分是已经发送完毕的网络包,这部分没有用了,可以回收。
- 第二部分:发送了但尚未确认的。这部分,发送方要等待,万一发送不成功,还要重新发送,所以不能删除。
- 第三部分:没有发送,但是已经等待发送的。这部分是接收方空闲的能力,可以马上发送,接收方收得了。
- 第四部分:没有发送,并且暂时还不会发送的。这部分已经超过了接收方的接收能力,再发送接收方就收不了了。
<img src="https://static001.geekbang.org/resource/image/b6/31/b62eea403e665bb196dceba571392531.png" alt="">
因为滑动窗口的存在,接收方的缓存也要分成了三个部分。
- 第一部分:接受并且确认过的任务。这部分完全接收成功了,可以交给应用层了。
- 第二部分:还没接收,但是马上就能接收的任务。这部分有的网络包到达了,但是还没确认,不算完全完毕,有的还没有到达,那就是接收方能够接受的最大的网络包数量。
- 第三部分:还没接收,也没法接收的任务。这部分已经超出接收方能力。
在网络包的交互过程中接收方会将第二部分的大小作为AdvertisedWindow发送给发送方发送方就可以根据他来调整发送速度了。
在tcp_snd_wnd_test函数中会判断sk_buff中的end_seq和tcp_wnd_end(tp)之间的关系也即这个sk_buff是否在滑动窗口的允许范围之内。如果不在范围内说明发送要受限制了我们就要把is_rwnd_limited设置为true。
接下来tcp_mss_split_point函数要被调用了。
```
static unsigned int tcp_mss_split_point(const struct sock *sk,
const struct sk_buff *skb,
unsigned int mss_now,
unsigned int max_segs,
int nonagle)
{
const struct tcp_sock *tp = tcp_sk(sk);
u32 partial, needed, window, max_len;
window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)-&gt;seq;
max_len = mss_now * max_segs;
if (likely(max_len &lt;= window &amp;&amp; skb != tcp_write_queue_tail(sk)))
return max_len;
needed = min(skb-&gt;len, window);
if (max_len &lt;= needed)
return max_len;
......
return needed;
}
```
这里面除了会判断上面讲的是否会因为超出mss而分段还会判断另一个条件就是是否在滑动窗口的运行范围之内如果小于窗口的大小也需要分段也即需要调用tso_fragment。
在一个循环的最后是调用tcp_transmit_skb真的去发送一个网络包。
```
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet;
struct tcp_sock *tp;
struct tcp_skb_cb *tcb;
struct tcphdr *th;
int err;
tp = tcp_sk(sk);
skb-&gt;skb_mstamp = tp-&gt;tcp_mstamp;
inet = inet_sk(sk);
tcb = TCP_SKB_CB(skb);
memset(&amp;opts, 0, sizeof(opts));
tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
skb_push(skb, tcp_header_size);
/* Build TCP header and checksum it. */
th = (struct tcphdr *)skb-&gt;data;
th-&gt;source = inet-&gt;inet_sport;
th-&gt;dest = inet-&gt;inet_dport;
th-&gt;seq = htonl(tcb-&gt;seq);
th-&gt;ack_seq = htonl(tp-&gt;rcv_nxt);
*(((__be16 *)th) + 6) = htons(((tcp_header_size &gt;&gt; 2) &lt;&lt; 12) |
tcb-&gt;tcp_flags);
th-&gt;check = 0;
th-&gt;urg_ptr = 0;
......
tcp_options_write((__be32 *)(th + 1), tp, &amp;opts);
th-&gt;window = htons(min(tp-&gt;rcv_wnd, 65535U));
......
err = icsk-&gt;icsk_af_ops-&gt;queue_xmit(sk, skb, &amp;inet-&gt;cork.fl);
......
}
```
tcp_transmit_skb这个函数比较长主要做了两件事情第一件事情就是填充TCP头如果我们对着TCP头的格式。
<img src="https://static001.geekbang.org/resource/image/be/0e/be225a97816a664367f29be9046aa30e.png" alt="">
这里面有源端口设置为inet_sport有目标端口设置为inet_dport有序列号设置为tcb-&gt;seq有确认序列号设置为tp-&gt;rcv_nxt。我们把所有的flags设置为tcb-&gt;tcp_flags。设置选项为opts。设置窗口大小为tp-&gt;rcv_wnd。
全部设置完毕之后就会调用icsk_af_ops的queue_xmit方法icsk_af_ops指向ipv4_specific也即调用的是ip_queue_xmit函数。
```
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.mtu_reduced = tcp_v4_mtu_reduced,
};
```
## 总结时刻
这一节,我们解析了发送一个网络包的一部分过程,如下图所示。
<img src="https://static001.geekbang.org/resource/image/dc/44/dc66535fa7e1a10fd6d728865f6c9344.png" alt="">
这个过程分成几个层次。
- VFS层write系统调用找到struct file根据里面的file_operations的定义调用sock_write_iter函数。sock_write_iter函数调用sock_sendmsg函数。
- Socket层从struct file里面的private_data得到struct socket根据里面ops的定义调用inet_sendmsg函数。
- Sock层从struct socket里面的sk得到struct sock根据里面sk_prot的定义调用tcp_sendmsg函数。
- TCP层tcp_sendmsg函数会调用tcp_write_xmit函数tcp_write_xmit函数会调用tcp_transmit_skb在这里实现了TCP层面向连接的逻辑。
- IP层扩展struct sock得到struct inet_connection_sock根据里面icsk_af_ops的定义调用ip_queue_xmit函数。
## 课堂练习
如果你对TCP协议的结构不太熟悉可以使用tcpdump命令截取一个TCP的包看看里面的结构。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。

View File

@@ -0,0 +1,892 @@
<audio id="audio" title="46 | 发送网络包(下):如何表达我们想让合作伙伴做什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e7/99/e7ee28d7548651dffc3504e3b4e65299.mp3"></audio>
上一节我们讲网络包的发送讲了上半部分也即从VFS层一直到IP层这一节我们接着看下去看IP层和MAC层是如何发送数据的。
## 解析ip_queue_xmit函数
从ip_queue_xmit函数开始我们就要进入IP层的发送逻辑了。
```
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
struct iphdr *iph;
int res;
inet_opt = rcu_dereference(inet-&gt;inet_opt);
fl4 = &amp;fl-&gt;u.ip4;
rt = skb_rtable(skb);
/* Make sure we can route this packet. */
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (!rt) {
__be32 daddr;
/* Use correct destination address if we have options. */
daddr = inet-&gt;inet_daddr;
......
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet-&gt;inet_saddr,
inet-&gt;inet_dport,
inet-&gt;inet_sport,
sk-&gt;sk_protocol,
RT_CONN_FLAGS(sk),
sk-&gt;sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
sk_setup_caps(sk, &amp;rt-&gt;dst);
}
skb_dst_set_noref(skb, &amp;rt-&gt;dst);
packet_routed:
/* OK, we know where to send it, allocate and build IP header. */
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt-&gt;opt.optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 &lt;&lt; 12) | (5 &lt;&lt; 8) | (inet-&gt;tos &amp; 0xff));
if (ip_dont_fragment(sk, &amp;rt-&gt;dst) &amp;&amp; !skb-&gt;ignore_df)
iph-&gt;frag_off = htons(IP_DF);
else
iph-&gt;frag_off = 0;
iph-&gt;ttl = ip_select_ttl(inet, &amp;rt-&gt;dst);
iph-&gt;protocol = sk-&gt;sk_protocol;
ip_copy_addrs(iph, fl4);
/* Transport layer set skb-&gt;h.foo itself. */
if (inet_opt &amp;&amp; inet_opt-&gt;opt.optlen) {
iph-&gt;ihl += inet_opt-&gt;opt.optlen &gt;&gt; 2;
ip_options_build(skb, &amp;inet_opt-&gt;opt, inet-&gt;inet_daddr, rt, 0);
}
ip_select_ident_segs(net, skb, sk,
skb_shinfo(skb)-&gt;gso_segs ?: 1);
/* TODO : should we use skb-&gt;sk here instead of sk ? */
skb-&gt;priority = sk-&gt;sk_priority;
skb-&gt;mark = sk-&gt;sk_mark;
res = ip_local_out(net, sk, skb);
......
}
```
在ip_queue_xmit中也即IP层的发送函数里面有三部分逻辑。
第一部分,选取路由,也即我要发送这个包应该从哪个网卡出去。
这件事情主要由ip_route_output_ports函数完成。接下来的调用链为ip_route_output_ports-&gt;ip_route_output_flow-&gt;__ip_route_output_key-&gt;ip_route_output_key_hash-&gt;ip_route_output_key_hash_rcu。
```
struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
{
struct net_device *dev_out = NULL;
int orig_oif = fl4-&gt;flowi4_oif;
unsigned int flags = 0;
struct rtable *rth;
......
err = fib_lookup(net, fl4, res, 0);
......
make_route:
rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
......
}
```
ip_route_output_key_hash_rcu先会调用fib_lookup。
**FIB**全称是Forwarding Information Base**转发信息表。**其实就是咱们常说的路由表。
```
static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res, unsigned int flags)
{ struct fib_table *tb;
......
tb = fib_get_table(net, RT_TABLE_MAIN);
if (tb)
err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
......
}
```
路由表可以有多个一般会有一个主表RT_TABLE_MAIN。然后fib_table_lookup函数在这个表里面进行查找。
路由表是一个什么样的结构呢?
路由就是在Linux服务器上的路由表里面配置的一条一条规则。这些规则大概是这样的想访问某个网段从某个网卡出去下一跳是某个IP。
之前我们讲过一个简单的拓扑图里面的三台Linux机器的路由表都可以通过ip route命令查看。
<img src="https://static001.geekbang.org/resource/image/f6/0e/f6982eb85dc66bd04200474efb3a050e.png" alt="">
```
# Linux服务器A
default via 192.168.1.1 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 metric 100
# Linux服务器B
default via 192.168.2.1 dev eth0
192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.100 metric 100
# Linux服务器做路由器
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
```
其实对于两端的服务器来讲我们没有太多路由可以选但是对于中间的Linux服务器做路由器来讲这里有两条路可以选一个是往左面转发一个是往右面转发就需要路由表的查找。
fib_table_lookup的代码逻辑比较复杂好在注释比较清楚。因为路由表要按照前缀进行查询希望找到最长匹配的那一个例如192.168.2.0/24和192.168.0.0/16都能匹配192.168.2.100/24。但是我们应该使用192.168.2.0/24的这一条。
为了更方面的做这个事情我们使用了Trie树这种结构。比如我们有一系列的字符串{bcs#, badge#, baby#, back#, badger#, badness#}。之所以每个字符串都加上#是希望不要一个字符串成为另外一个字符串的前缀。然后我们把它们放在Trie树中如下图所示
<img src="https://static001.geekbang.org/resource/image/3f/11/3f0a99cf1c47afcd0bd740c4b7802511.png" alt="">
对于将IP地址转成二进制放入trie树也是同样的道理可以很快进行路由的查询。
找到了路由,就知道了应该从哪个网卡发出去。
然后ip_route_output_key_hash_rcu会调用__mkroute_output创建一个struct rtable表示找到的路由表项。这个结构是由rt_dst_alloc函数分配的。
```
struct rtable *rt_dst_alloc(struct net_device *dev,
unsigned int flags, u16 type,
bool nopolicy, bool noxfrm, bool will_cache)
{
struct rtable *rt;
rt = dst_alloc(&amp;ipv4_dst_ops, dev, 1, DST_OBSOLETE_FORCE_CHK,
(will_cache ? 0 : DST_HOST) |
(nopolicy ? DST_NOPOLICY : 0) |
(noxfrm ? DST_NOXFRM : 0));
if (rt) {
rt-&gt;rt_genid = rt_genid_ipv4(dev_net(dev));
rt-&gt;rt_flags = flags;
rt-&gt;rt_type = type;
rt-&gt;rt_is_input = 0;
rt-&gt;rt_iif = 0;
rt-&gt;rt_pmtu = 0;
rt-&gt;rt_gateway = 0;
rt-&gt;rt_uses_gateway = 0;
rt-&gt;rt_table_id = 0;
INIT_LIST_HEAD(&amp;rt-&gt;rt_uncached);
rt-&gt;dst.output = ip_output;
if (flags &amp; RTCF_LOCAL)
rt-&gt;dst.input = ip_local_deliver;
}
return rt;
}
```
最终返回struct rtable实例第一部分也就完成了。
第二部分就是准备IP层的头往里面填充内容。这就要对着IP层的头的格式进行理解。
<img src="https://static001.geekbang.org/resource/image/6b/2b/6b2ea7148a8e04138a2228c5dbc7182b.png" alt="">
在这里面服务类型设置为tos标识位里面设置是否允许分片frag_off。如果不允许而遇到MTU太小过不去的情况就发送ICMP报错。TTL是这个包的存活时间为了防止一个IP包迷路以后一直存活下去每经过一个路由器TTL都减一减为零则“死去”。设置protocol指的是更上层的协议这里是TCP。源地址和目标地址由ip_copy_addrs设置。最后设置options。
第三部分就是调用ip_local_out发送IP包。
```
int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int err;
err = __ip_local_out(net, sk, skb);
if (likely(err == 1))
err = dst_output(net, sk, skb);
return err;
}
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph-&gt;tot_len = htons(skb-&gt;len);
skb-&gt;protocol = htons(ETH_P_IP);
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)-&gt;dev,
dst_output);
}
```
ip_local_out先是调用__ip_local_out然后里面调用了nf_hook。这是什么呢nf的意思是Netfilter这是Linux内核的一个机制用于在网络发送和转发的关键节点上加上hook函数这些函数可以截获数据包对数据包进行干预。
一个著名的实现就是内核模块ip_tables。在用户态还有一个客户端程序iptables用命令行来干预内核的规则。
<img src="https://static001.geekbang.org/resource/image/75/4d/75c8257049eed99499e802fcc2eacf4d.png" alt="">
iptables有表和链的概念最终要的是两个表。
filter表处理过滤功能主要包含以下三个链。
- INPUT链过滤所有目标地址是本机的数据包
- FORWARD链过滤所有路过本机的数据包
- OUTPUT链过滤所有由本机产生的数据包
nat表主要处理网络地址转换可以进行SNAT改变源地址、DNAT改变目标地址包含以下三个链。
- PREROUTING链可以在数据包到达时改变目标地址
- OUTPUT链可以改变本地产生的数据包的目标地址
- POSTROUTING链在数据包离开时改变数据包的源地址
<img src="https://static001.geekbang.org/resource/image/76/da/765e5431fe4b17f62b1b5712cc82abda.png" alt="">
在这里网络包马上就要发出去了因而是NF_INET_LOCAL_OUT也即ouput链如果用户曾经在iptables里面写过某些规则就会在nf_hook这个函数里面起作用。
ip_local_out再调用dst_output就是真正的发送数据。
```
/* Output packet to network from transport. */
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
return skb_dst(skb)-&gt;output(net, sk, skb);
}
```
这里调用的就是struct rtable成员dst的ouput函数。在rt_dst_alloc中我们可以看到output函数指向的是ip_output。
```
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)-&gt;dev;
skb-&gt;dev = dev;
skb-&gt;protocol = htons(ETH_P_IP);
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
net, sk, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)-&gt;flags &amp; IPSKB_REROUTED));
}
```
在ip_output里面我们又看到了熟悉的NF_HOOK。这一次是NF_INET_POST_ROUTING也即POSTROUTING链处理完之后调用ip_finish_output。
## 解析ip_finish_output函数
从ip_finish_output函数开始发送网络包的逻辑由第三层到达第二层。ip_finish_output最终调用ip_finish_output2。
```
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
struct rtable *rt = (struct rtable *)dst;
struct net_device *dev = dst-&gt;dev;
unsigned int hh_len = LL_RESERVED_SPACE(dev);
struct neighbour *neigh;
u32 nexthop;
......
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)-&gt;daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&amp;arp_tbl, &amp;nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res;
sock_confirm_neigh(skb, neigh);
res = neigh_output(neigh, skb);
return res;
}
......
}
```
在ip_finish_output2中先找到struct rtable路由表里面的下一跳下一跳一定和本机在同一个局域网中可以通过二层进行通信因而通过__ipv4_neigh_lookup_noref查找如何通过二层访问下一跳。
```
static inline struct neighbour *__ipv4_neigh_lookup_noref(struct net_device *dev, u32 key)
{
return ___neigh_lookup_noref(&amp;arp_tbl, neigh_key_eq32, arp_hashfn, &amp;key, dev);
}
```
__ipv4_neigh_lookup_noref是从本地的ARP表中查找下一跳的MAC地址。ARP表的定义如下
```
struct neigh_table arp_tbl = {
.family = AF_INET,
.key_len = 4,
.protocol = cpu_to_be16(ETH_P_IP),
.hash = arp_hash,
.key_eq = arp_key_eq,
.constructor = arp_constructor,
.proxy_redo = parp_redo,
.id = &quot;arp_cache&quot;,
......
.gc_interval = 30 * HZ,
.gc_thresh1 = 128,
.gc_thresh2 = 512,
.gc_thresh3 = 1024,
};
```
如果在ARP表中没有找到相应的项则调用__neigh_create进行创建。
```
struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev, bool want_ref)
{
u32 hash_val;
int key_len = tbl-&gt;key_len;
int error;
struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
struct neigh_hash_table *nht;
memcpy(n-&gt;primary_key, pkey, key_len);
n-&gt;dev = dev;
dev_hold(dev);
/* Protocol specific setup. */
if (tbl-&gt;constructor &amp;&amp; (error = tbl-&gt;constructor(n)) &lt; 0) {
......
}
......
if (atomic_read(&amp;tbl-&gt;entries) &gt; (1 &lt;&lt; nht-&gt;hash_shift))
nht = neigh_hash_grow(tbl, nht-&gt;hash_shift + 1);
hash_val = tbl-&gt;hash(pkey, dev, nht-&gt;hash_rnd) &gt;&gt; (32 - nht-&gt;hash_shift);
for (n1 = rcu_dereference_protected(nht-&gt;hash_buckets[hash_val],
lockdep_is_held(&amp;tbl-&gt;lock));
n1 != NULL;
n1 = rcu_dereference_protected(n1-&gt;next,
lockdep_is_held(&amp;tbl-&gt;lock))) {
if (dev == n1-&gt;dev &amp;&amp; !memcmp(n1-&gt;primary_key, pkey, key_len)) {
if (want_ref)
neigh_hold(n1);
rc = n1;
goto out_tbl_unlock;
}
}
......
rcu_assign_pointer(n-&gt;next,
rcu_dereference_protected(nht-&gt;hash_buckets[hash_val],
lockdep_is_held(&amp;tbl-&gt;lock)));
rcu_assign_pointer(nht-&gt;hash_buckets[hash_val], n);
......
}
```
__neigh_create先调用neigh_alloc创建一个struct neighbour结构用于维护MAC地址和ARP相关的信息。这个名字也很好理解大家都是在一个局域网里面可以通过MAC地址访问到当然是邻居了。
```
static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
{
struct neighbour *n = NULL;
unsigned long now = jiffies;
int entries;
......
n = kzalloc(tbl-&gt;entry_size + dev-&gt;neigh_priv_len, GFP_ATOMIC);
if (!n)
goto out_entries;
__skb_queue_head_init(&amp;n-&gt;arp_queue);
rwlock_init(&amp;n-&gt;lock);
seqlock_init(&amp;n-&gt;ha_lock);
n-&gt;updated = n-&gt;used = now;
n-&gt;nud_state = NUD_NONE;
n-&gt;output = neigh_blackhole;
seqlock_init(&amp;n-&gt;hh.hh_lock);
n-&gt;parms = neigh_parms_clone(&amp;tbl-&gt;parms);
setup_timer(&amp;n-&gt;timer, neigh_timer_handler, (unsigned long)n);
NEIGH_CACHE_STAT_INC(tbl, allocs);
n-&gt;tbl = tbl;
refcount_set(&amp;n-&gt;refcnt, 1);
n-&gt;dead = 1;
......
}
```
在neigh_alloc中我们先分配一个struct neighbour结构并且初始化。这里面比较重要的有两个成员一个是arp_queue所以上层想通过ARP获取MAC地址的任务都放在这个队列里面。另一个是timer定时器我们设置成过一段时间就调用neigh_timer_handler来处理这些ARP任务。
__neigh_create然后调用了arp_tbl的constructor函数也即调用了arp_constructor在这里面定义了ARP的操作arp_hh_ops。
```
static int arp_constructor(struct neighbour *neigh)
{
__be32 addr = *(__be32 *)neigh-&gt;primary_key;
struct net_device *dev = neigh-&gt;dev;
struct in_device *in_dev;
struct neigh_parms *parms;
......
neigh-&gt;type = inet_addr_type_dev_table(dev_net(dev), dev, addr);
parms = in_dev-&gt;arp_parms;
__neigh_parms_put(neigh-&gt;parms);
neigh-&gt;parms = neigh_parms_clone(parms);
......
neigh-&gt;ops = &amp;arp_hh_ops;
......
neigh-&gt;output = neigh-&gt;ops-&gt;output;
......
}
static const struct neigh_ops arp_hh_ops = {
.family = AF_INET,
.solicit = arp_solicit,
.error_report = arp_error_report,
.output = neigh_resolve_output,
.connected_output = neigh_resolve_output,
};
```
__neigh_create最后是将创建的struct neighbour结构放入一个哈希表从里面的代码逻辑比较容易看出这是一个数组加链表的链式哈希表先计算出哈希值hash_val得到相应的链表然后循环这个链表找到对应的项如果找不到就在最后插入一项。
我们回到ip_finish_output2在__neigh_create之后会调用neigh_output发送网络包。
```
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
......
return n-&gt;output(n, skb);
}
```
按照上面对于struct neighbour的操作函数arp_hh_ops 的定义output调用的是neigh_resolve_output。
```
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
if (!neigh_event_send(neigh, skb)) {
......
rc = dev_queue_xmit(skb);
}
......
}
```
在neigh_resolve_output里面首先neigh_event_send触发一个事件看能否激活ARP。
```
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
int rc;
bool immediate_probe = false;
if (!(neigh-&gt;nud_state &amp; (NUD_STALE | NUD_INCOMPLETE))) {
if (NEIGH_VAR(neigh-&gt;parms, MCAST_PROBES) +
NEIGH_VAR(neigh-&gt;parms, APP_PROBES)) {
unsigned long next, now = jiffies;
atomic_set(&amp;neigh-&gt;probes,
NEIGH_VAR(neigh-&gt;parms, UCAST_PROBES));
neigh-&gt;nud_state = NUD_INCOMPLETE;
neigh-&gt;updated = now;
next = now + max(NEIGH_VAR(neigh-&gt;parms, RETRANS_TIME),
HZ/2);
neigh_add_timer(neigh, next);
immediate_probe = true;
}
......
} else if (neigh-&gt;nud_state &amp; NUD_STALE) {
neigh_dbg(2, &quot;neigh %p is delayed\n&quot;, neigh);
neigh-&gt;nud_state = NUD_DELAY;
neigh-&gt;updated = jiffies;
neigh_add_timer(neigh, jiffies +
NEIGH_VAR(neigh-&gt;parms, DELAY_PROBE_TIME));
}
if (neigh-&gt;nud_state == NUD_INCOMPLETE) {
if (skb) {
.......
__skb_queue_tail(&amp;neigh-&gt;arp_queue, skb);
neigh-&gt;arp_queue_len_Bytes += skb-&gt;truesize;
}
rc = 1;
}
out_unlock_bh:
if (immediate_probe)
neigh_probe(neigh);
.......
}
```
在__neigh_event_send中激活ARP分两种情况第一种情况是马上激活也即immediate_probe。另一种情况是延迟激活则仅仅设置一个timer。然后将ARP包放在arp_queue上。如果马上激活就直接调用neigh_probe如果延迟激活则定时器到了就会触发neigh_timer_handler在这里面还是会调用neigh_probe。
我们就来看neigh_probe的实现在这里面会从arp_queue中拿出ARP包来然后调用struct neighbour的solicit操作。
```
static void neigh_probe(struct neighbour *neigh)
__releases(neigh-&gt;lock)
{
struct sk_buff *skb = skb_peek_tail(&amp;neigh-&gt;arp_queue);
......
if (neigh-&gt;ops-&gt;solicit)
neigh-&gt;ops-&gt;solicit(neigh, skb);
......
}
```
按照上面对于struct neighbour的操作函数arp_hh_ops 的定义solicit调用的是arp_solicit在这里我们可以找到对于arp_send_dst的调用创建并发送一个arp包得到结果放在struct dst_entry里面。
```
static void arp_send_dst(int type, int ptype, __be32 dest_ip,
struct net_device *dev, __be32 src_ip,
const unsigned char *dest_hw,
const unsigned char *src_hw,
const unsigned char *target_hw,
struct dst_entry *dst)
{
struct sk_buff *skb;
......
skb = arp_create(type, ptype, dest_ip, dev, src_ip,
dest_hw, src_hw, target_hw);
......
skb_dst_set(skb, dst_clone(dst));
arp_xmit(skb);
}
```
我们回到neigh_resolve_output中当ARP发送完毕就可以调用dev_queue_xmit发送二层网络包了。
```
/**
* __dev_queue_xmit - transmit a buffer
* @skb: buffer to transmit
* @accel_priv: private data used for L2 forwarding offload
*
* Queue a buffer for transmission to a network device.
*/
static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
{
struct net_device *dev = skb-&gt;dev;
struct netdev_queue *txq;
struct Qdisc *q;
......
txq = netdev_pick_tx(dev, skb, accel_priv);
q = rcu_dereference_bh(txq-&gt;qdisc);
if (q-&gt;enqueue) {
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
......
}
```
就像咱们在讲述硬盘块设备的时候讲过,每个块设备都有队列,用于将内核的数据放到队列里面,然后设备驱动从队列里面取出后,将数据根据具体设备的特性发送给设备。
网络设备也是类似的对于发送来说有一个发送队列struct netdev_queue *txq。
这里还有另一个变量叫做struct Qdisc这个是什么呢如果我们在一台Linux机器上运行ip addr我们能看到对于一个网卡都有下面的输出。
```
# ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1400 qdisc pfifo_fast state UP group default qlen 1000
link/ether fa:16:3e:75:99:08 brd ff:ff:ff:ff:ff:ff
inet 10.173.32.47/21 brd 10.173.39.255 scope global noprefixroute dynamic eth0
valid_lft 67104sec preferred_lft 67104sec
inet6 fe80::f816:3eff:fe75:9908/64 scope link
valid_lft forever preferred_lft forever
```
这里面有个关键字qdisc pfifo_fast是什么意思呢qdisc全称是queueing discipline中文叫排队规则。内核如果需要通过某个网络接口发送数据包都需要按照为这个接口配置的qdisc排队规则把数据包加入队列。
最简单的qdisc是pfifo它不对进入的数据包做任何的处理数据包采用先入先出的方式通过队列。pfifo_fast稍微复杂一些它的队列包括三个波段band。在每个波段里面使用先进先出规则。
三个波段的优先级也不相同。band 0的优先级最高band 2的最低。如果band 0里面有数据包系统就不会处理band 1里面的数据包band 1和band 2之间也是一样。
数据包是按照服务类型Type of ServiceTOS被分配到三个波段里面的。TOS是IP头里面的一个字段代表了当前的包是高优先级的还是低优先级的。
pfifo_fast分为三个先入先出的队列我们能称为三个Band。根据网络包里面的TOS看这个包到底应该进入哪个队列。TOS总共四位每一位表示的意思不同总共十六种类型。
<img src="https://static001.geekbang.org/resource/image/ab/d9/ab6af2f9e1a64868636080a05cfde0d9.png" alt="">
通过命令行tc qdisc show dev eth0我们可以输出结果priomap也是十六个数字。在0到2之间和TOS的十六种类型对应起来。不同的TOS对应不同的队列。其中Band 0优先级最高发送完毕后才轮到Band 1发送最后才是Band 2。
```
# tc qdisc show dev eth0
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
```
接下来__dev_xmit_skb开始进行网络包发送。
```
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev,
struct netdev_queue *txq)
{
......
rc = q-&gt;enqueue(skb, q, &amp;to_free) &amp; NET_XMIT_MASK;
if (qdisc_run_begin(q)) {
......
__qdisc_run(q);
}
......
}
void __qdisc_run(struct Qdisc *q)
{
int quota = dev_tx_weight;
int packets;
while (qdisc_restart(q, &amp;packets)) {
/*
* Ordered by possible occurrence: Postpone processing if
* 1. we've exceeded packet quota
* 2. another process needs the CPU;
*/
quota -= packets;
if (quota &lt;= 0 || need_resched()) {
__netif_schedule(q);
break;
}
}
qdisc_run_end(q);
}
```
__dev_xmit_skb会将请求放入队列然后调用__qdisc_run处理队列中的数据。qdisc_restart用于数据的发送。根据注释中的说法qdisc的另一个功能是用于控制网络包的发送速度因而如果超过速度就需要重新调度则会调用__netif_schedule。
```
static void __netif_reschedule(struct Qdisc *q)
{
struct softnet_data *sd;
unsigned long flags;
local_irq_save(flags);
sd = this_cpu_ptr(&amp;softnet_data);
q-&gt;next_sched = NULL;
*sd-&gt;output_queue_tailp = q;
sd-&gt;output_queue_tailp = &amp;q-&gt;next_sched;
raise_softirq_irqoff(NET_TX_SOFTIRQ);
local_irq_restore(flags);
}
```
__netif_schedule会调用__netif_reschedule发起一个软中断NET_TX_SOFTIRQ。咱们讲设备驱动程序的时候讲过设备驱动程序处理中断分两个过程一个是屏蔽中断的关键处理逻辑一个是延迟处理逻辑。当时说工作队列是延迟处理逻辑的处理方案软中断也是一种方案。
在系统初始化的时候我们会定义软中断的处理函数。例如NET_TX_SOFTIRQ的处理函数是net_tx_action用于发送网络包。还有一个NET_RX_SOFTIRQ的处理函数是net_rx_action用于接收网络包。接收网络包的过程咱们下一节解析。
```
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
```
这里我们来解析一下net_tx_action。
```
static __latent_entropy void net_tx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&amp;softnet_data);
......
if (sd-&gt;output_queue) {
struct Qdisc *head;
local_irq_disable();
head = sd-&gt;output_queue;
sd-&gt;output_queue = NULL;
sd-&gt;output_queue_tailp = &amp;sd-&gt;output_queue;
local_irq_enable();
while (head) {
struct Qdisc *q = head;
spinlock_t *root_lock;
head = head-&gt;next_sched;
......
qdisc_run(q);
}
}
}
```
我们会发现net_tx_action还是调用了qdisc_run还是会调用__qdisc_run然后调用qdisc_restart发送网络包。
我们来看一下qdisc_restart的实现。
```
static inline int qdisc_restart(struct Qdisc *q, int *packets)
{
struct netdev_queue *txq;
struct net_device *dev;
spinlock_t *root_lock;
struct sk_buff *skb;
bool validate;
/* Dequeue packet */
skb = dequeue_skb(q, &amp;validate, packets);
if (unlikely(!skb))
return 0;
root_lock = qdisc_lock(q);
dev = qdisc_dev(q);
txq = skb_get_tx_queue(dev, skb);
return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
}
```
qdisc_restart将网络包从Qdisc的队列中拿下来然后调用sch_direct_xmit进行发送。
```
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev, struct netdev_queue *txq,
spinlock_t *root_lock, bool validate)
{
int ret = NETDEV_TX_BUSY;
if (likely(skb)) {
if (!netif_xmit_frozen_or_stopped(txq))
skb = dev_hard_start_xmit(skb, dev, txq, &amp;ret);
}
......
if (dev_xmit_complete(ret)) {
/* Driver sent out skb successfully or skb was consumed */
ret = qdisc_qlen(q);
} else {
/* Driver returned NETDEV_TX_BUSY - requeue skb */
ret = dev_requeue_skb(skb, q);
}
......
}
```
在sch_direct_xmit中调用dev_hard_start_xmit进行发送如果发送不成功会返回NETDEV_TX_BUSY。这说明网络卡很忙于是就调用dev_requeue_skb重新放入队列。
```
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev, struct netdev_queue *txq, int *ret)
{
struct sk_buff *skb = first;
int rc = NETDEV_TX_OK;
while (skb) {
struct sk_buff *next = skb-&gt;next;
rc = xmit_one(skb, dev, txq, next != NULL);
skb = next;
if (netif_xmit_stopped(txq) &amp;&amp; skb) {
rc = NETDEV_TX_BUSY;
break;
}
}
......
}
```
在dev_hard_start_xmit中是一个while循环。每次在队列中取出一个sk_buff调用xmit_one发送。
接下来的调用链为xmit_one-&gt;netdev_start_xmit-&gt;__netdev_start_xmit。
```
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops, struct sk_buff *skb, struct net_device *dev, bool more)
{
skb-&gt;xmit_more = more ? 1 : 0;
return ops-&gt;ndo_start_xmit(skb, dev);
}
```
这个时候已经到了设备驱动层了。我们能看到drivers/net/ethernet/intel/ixgb/ixgb_main.c里面有对于这个网卡的操作的定义。
```
static const struct net_device_ops ixgb_netdev_ops = {
.ndo_open = ixgb_open,
.ndo_stop = ixgb_close,
.ndo_start_xmit = ixgb_xmit_frame,
.ndo_set_rx_mode = ixgb_set_multi,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = ixgb_set_mac,
.ndo_change_mtu = ixgb_change_mtu,
.ndo_tx_timeout = ixgb_tx_timeout,
.ndo_vlan_rx_add_vid = ixgb_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = ixgb_vlan_rx_kill_vid,
.ndo_fix_features = ixgb_fix_features,
.ndo_set_features = ixgb_set_features,
};
```
在这里面我们可以找到对于ndo_start_xmit的定义调用ixgb_xmit_frame。
```
static netdev_tx_t
ixgb_xmit_frame(struct sk_buff *skb, struct net_device *netdev)
{
struct ixgb_adapter *adapter = netdev_priv(netdev);
......
if (count) {
ixgb_tx_queue(adapter, count, vlan_id, tx_flags);
/* Make sure there is space in the ring for the next send. */
ixgb_maybe_stop_tx(netdev, &amp;adapter-&gt;tx_ring, DESC_NEEDED);
}
......
return NETDEV_TX_OK;
}
```
在ixgb_xmit_frame中我们会得到这个网卡对应的适配器然后将其放入硬件网卡的队列中。
至此,整个发送才算结束。
## 总结时刻
这一节,我们继续解析了发送一个网络包的过程,我们整个过程的图画在了下面。
<img src="https://static001.geekbang.org/resource/image/79/6f/79cc42f3163d159a66e163c006d9f36f.png" alt="">
这个过程分成几个层次。
- VFS层write系统调用找到struct file根据里面的file_operations的定义调用sock_write_iter函数。sock_write_iter函数调用sock_sendmsg函数。
- Socket层从struct file里面的private_data得到struct socket根据里面ops的定义调用inet_sendmsg函数。
- Sock层从struct socket里面的sk得到struct sock根据里面sk_prot的定义调用tcp_sendmsg函数。
- TCP层tcp_sendmsg函数会调用tcp_write_xmit函数tcp_write_xmit函数会调用tcp_transmit_skb在这里实现了TCP层面向连接的逻辑。
- IP层扩展struct sock得到struct inet_connection_sock根据里面icsk_af_ops的定义调用ip_queue_xmit函数。
- IP层ip_route_output_ports函数里面会调用fib_lookup查找路由表。FIB全称是Forwarding Information Base转发信息表也就是路由表。
- 在IP层里面要做的另一个事情是填写IP层的头。
- 在IP层还要做的一件事情就是通过iptables规则。
- MAC层IP层调用ip_finish_output进行MAC层。
- MAC层需要ARP获得MAC地址因而要调用___neigh_lookup_noref查找属于同一个网段的邻居他会调用neigh_probe发送ARP。
- 有了MAC地址就可以调用dev_queue_xmit发送二层网络包了它会调用__dev_xmit_skb会将请求放入队列。
- 设备层网络包的发送会触发一个软中断NET_TX_SOFTIRQ来处理队列中的数据。这个软中断的处理函数是net_tx_action。
- 在软中断处理函数中会将网络包从队列上拿下来调用网络设备的传输函数ixgb_xmit_frame将网络包发到设备的队列上去。
## 课堂练习
上一节你应该通过tcpdump看到了TCP包头的格式这一节请你查看一下IP包的格式以及ARP的过程。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,498 @@
<audio id="audio" title="47 | 接收网络包(上):如何搞明白合作伙伴让我们做什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/9d/d400e8c1fb0a87fd4e600ca7d5b39d9d.mp3"></audio>
前面两节,我们分析了发送网络包的整个过程。这一节,我们来解析接收网络包的过程。
如果说网络包的发送是从应用层开始层层调用一直到网卡驱动程序的话网络包的结束过程就是一个反过来的过程我们不能从应用层的读取开始而应该从网卡接收到一个网络包开始。我们用两节来解析这个过程这一节我们从硬件网卡解析到IP层下一节我们从IP层解析到Socket层。
## 设备驱动层
网卡作为一个硬件,接收到网络包,应该怎么通知操作系统,这个网络包到达了呢?咱们学习过输入输出设备和中断。没错,我们可以触发一个中断。但是这里有个问题,就是网络包的到来,往往是很难预期的。网络吞吐量比较大的时候,网络包的到达会十分频繁。这个时候,如果非常频繁地去触发中断,想想就觉得是个灾难。
比如说CPU正在做某个事情一些网络包来了触发了中断CPU停下手里的事情去处理这些网络包处理完毕按照中断处理的逻辑应该回去继续处理其他事情。这个时候另一些网络包又来了又触发了中断CPU手里的事情还没捂热又要停下来去处理网络包。能不能大家要来的一起来把网络包好好处理一把然后再回去集中处理其他事情呢
网络包能不能一起来这个我们没法儿控制但是我们可以有一种机制就是当一些网络包到来触发了中断内核处理完这些网络包之后我们可以先进入主动轮询poll网卡的方式主动去接收到来的网络包。如果一直有就一直处理等处理告一段落就返回干其他的事情。当再有下一批网络包到来的时候再中断再轮询poll。这样就会大大减少中断的数量提升网络处理的效率这种处理方式我们称为**NAPI**。
为了帮你了解设备驱动层的工作机制我们还是以上一节发送网络包时的网卡drivers/net/ethernet/intel/ixgb/ixgb_main.c为例子来进行解析。
```
static struct pci_driver ixgb_driver = {
.name = ixgb_driver_name,
.id_table = ixgb_pci_tbl,
.probe = ixgb_probe,
.remove = ixgb_remove,
.err_handler = &amp;ixgb_err_handler
};
MODULE_AUTHOR(&quot;Intel Corporation, &lt;linux.nics@intel.com&gt;&quot;);
MODULE_DESCRIPTION(&quot;Intel(R) PRO/10GbE Network Driver&quot;);
MODULE_LICENSE(&quot;GPL&quot;);
MODULE_VERSION(DRV_VERSION);
/**
* ixgb_init_module - Driver Registration Routine
*
* ixgb_init_module is the first routine called when the driver is
* loaded. All it does is register with the PCI subsystem.
**/
static int __init
ixgb_init_module(void)
{
pr_info(&quot;%s - version %s\n&quot;, ixgb_driver_string, ixgb_driver_version);
pr_info(&quot;%s\n&quot;, ixgb_copyright);
return pci_register_driver(&amp;ixgb_driver);
}
module_init(ixgb_init_module);
```
在网卡驱动程序初始化的时候我们会调用ixgb_init_module注册一个驱动ixgb_driver并且调用它的probe函数ixgb_probe。
```
static int
ixgb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev = NULL;
struct ixgb_adapter *adapter;
......
netdev = alloc_etherdev(sizeof(struct ixgb_adapter));
SET_NETDEV_DEV(netdev, &amp;pdev-&gt;dev);
pci_set_drvdata(pdev, netdev);
adapter = netdev_priv(netdev);
adapter-&gt;netdev = netdev;
adapter-&gt;pdev = pdev;
adapter-&gt;hw.back = adapter;
adapter-&gt;msg_enable = netif_msg_init(debug, DEFAULT_MSG_ENABLE);
adapter-&gt;hw.hw_addr = pci_ioremap_bar(pdev, BAR_0);
......
netdev-&gt;netdev_ops = &amp;ixgb_netdev_ops;
ixgb_set_ethtool_ops(netdev);
netdev-&gt;watchdog_timeo = 5 * HZ;
netif_napi_add(netdev, &amp;adapter-&gt;napi, ixgb_clean, 64);
strncpy(netdev-&gt;name, pci_name(pdev), sizeof(netdev-&gt;name) - 1);
adapter-&gt;bd_number = cards_found;
adapter-&gt;link_speed = 0;
adapter-&gt;link_duplex = 0;
......
}
```
在ixgb_probe中我们会创建一个struct net_device表示这个网络设备并且netif_napi_add函数为这个网络设备注册一个轮询poll函数ixgb_clean将来一旦出现网络包的时候就是要通过它来轮询了。
当一个网卡被激活的时候我们会调用函数ixgb_open-&gt;ixgb_up在这里面注册一个硬件的中断处理函数。
```
int
ixgb_up(struct ixgb_adapter *adapter)
{
struct net_device *netdev = adapter-&gt;netdev;
......
err = request_irq(adapter-&gt;pdev-&gt;irq, ixgb_intr, irq_flags,
netdev-&gt;name, netdev);
......
}
/**
* ixgb_intr - Interrupt Handler
* @irq: interrupt number
* @data: pointer to a network interface device structure
**/
static irqreturn_t
ixgb_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct ixgb_adapter *adapter = netdev_priv(netdev);
struct ixgb_hw *hw = &amp;adapter-&gt;hw;
......
if (napi_schedule_prep(&amp;adapter-&gt;napi)) {
IXGB_WRITE_REG(&amp;adapter-&gt;hw, IMC, ~0);
__napi_schedule(&amp;adapter-&gt;napi);
}
return IRQ_HANDLED;
}
```
如果一个网络包到来触发了硬件中断就会调用ixgb_intr这里面会调用__napi_schedule。
```
/**
* __napi_schedule - schedule for receive
* @n: entry to schedule
*
* The entry's receive function will be scheduled to run.
* Consider using __napi_schedule_irqoff() if hard irqs are masked.
*/
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&amp;softnet_data), n);
local_irq_restore(flags);
}
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&amp;napi-&gt;poll_list, &amp;sd-&gt;poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
```
__napi_schedule是处于中断处理的关键部分在他被调用的时候中断是暂时关闭的但是处理网络包是个复杂的过程需要到延迟处理部分所以____napi_schedule将当前设备放到struct softnet_data结构的poll_list里面说明在延迟处理部分可以接着处理这个poll_list里面的网络设备。
然后____napi_schedule触发一个软中断NET_RX_SOFTIRQ通过软中断触发中断处理的延迟处理部分也是常用的手段。
上一节我们知道软中断NET_RX_SOFTIRQ对应的中断处理函数是net_rx_action。
```
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&amp;softnet_data);
LIST_HEAD(list);
list_splice_init(&amp;sd-&gt;poll_list, &amp;list);
......
for (;;) {
struct napi_struct *n;
......
n = list_first_entry(&amp;list, struct napi_struct, poll_list);
budget -= napi_poll(n, &amp;repoll);
}
......
}
```
在net_rx_action中会得到struct softnet_data结构这个结构在发送的时候我们也遇到过。当时它的output_queue用于网络包的发送这里的poll_list用于网络包的接收。
```
struct softnet_data {
struct list_head poll_list;
......
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
......
}
```
在net_rx_action中接下来是一个循环在poll_list里面取出网络包到达的设备然后调用napi_poll来轮询这些设备napi_poll会调用最初设备初始化的时候注册的poll函数对于ixgb_driver对应的函数是ixgb_clean。
ixgb_clean会调用ixgb_clean_rx_irq。
```
static bool
ixgb_clean_rx_irq(struct ixgb_adapter *adapter, int *work_done, int work_to_do)
{
struct ixgb_desc_ring *rx_ring = &amp;adapter-&gt;rx_ring;
struct net_device *netdev = adapter-&gt;netdev;
struct pci_dev *pdev = adapter-&gt;pdev;
struct ixgb_rx_desc *rx_desc, *next_rxd;
struct ixgb_buffer *buffer_info, *next_buffer, *next2_buffer;
u32 length;
unsigned int i, j;
int cleaned_count = 0;
bool cleaned = false;
i = rx_ring-&gt;next_to_clean;
rx_desc = IXGB_RX_DESC(*rx_ring, i);
buffer_info = &amp;rx_ring-&gt;buffer_info[i];
while (rx_desc-&gt;status &amp; IXGB_RX_DESC_STATUS_DD) {
struct sk_buff *skb;
u8 status;
status = rx_desc-&gt;status;
skb = buffer_info-&gt;skb;
buffer_info-&gt;skb = NULL;
prefetch(skb-&gt;data - NET_IP_ALIGN);
if (++i == rx_ring-&gt;count)
i = 0;
next_rxd = IXGB_RX_DESC(*rx_ring, i);
prefetch(next_rxd);
j = i + 1;
if (j == rx_ring-&gt;count)
j = 0;
next2_buffer = &amp;rx_ring-&gt;buffer_info[j];
prefetch(next2_buffer);
next_buffer = &amp;rx_ring-&gt;buffer_info[i];
......
length = le16_to_cpu(rx_desc-&gt;length);
rx_desc-&gt;length = 0;
......
ixgb_check_copybreak(&amp;adapter-&gt;napi, buffer_info, length, &amp;skb);
/* Good Receive */
skb_put(skb, length);
/* Receive Checksum Offload */
ixgb_rx_checksum(adapter, rx_desc, skb);
skb-&gt;protocol = eth_type_trans(skb, netdev);
netif_receive_skb(skb);
......
/* use prefetched values */
rx_desc = next_rxd;
buffer_info = next_buffer;
}
rx_ring-&gt;next_to_clean = i;
......
}
```
在网络设备的驱动层有一个用于接收网络包的rx_ring。它是一个环从网卡硬件接收的包会放在这个环里面。这个环里面的buffer_info[]是一个数组存放的是网络包的内容。i和j是这个数组的下标在ixgb_clean_rx_irq里面的while循环中依次处理环里面的数据。在这里面我们看到了i和j加一之后如果超过了数组的大小就跳回下标0就说明这是一个环。
ixgb_check_copybreak函数将buffer_info里面的内容拷贝到struct sk_buff *skb从而可以作为一个网络包进行后续的处理然后调用netif_receive_skb。
## 网络协议栈的二层逻辑
从netif_receive_skb函数开始我们就进入了内核的网络协议栈。
接下来的调用链为netif_receive_skb-&gt;netif_receive_skb_internal-&gt;__netif_receive_skb-&gt;__netif_receive_skb_core。
在__netif_receive_skb_core中我们先是处理了二层的一些逻辑。例如对于VLAN的处理接下来要想办法交给第三层。
```
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
struct packet_type *ptype, *pt_prev;
......
type = skb-&gt;protocol;
......
deliver_ptype_list_skb(skb, &amp;pt_prev, orig_dev, type,
&amp;orig_dev-&gt;ptype_specific);
if (pt_prev) {
ret = pt_prev-&gt;func(skb, skb-&gt;dev, pt_prev, orig_dev);
}
......
}
static inline void deliver_ptype_list_skb(struct sk_buff *skb,
struct packet_type **pt,
struct net_device *orig_dev,
__be16 type,
struct list_head *ptype_list)
{
struct packet_type *ptype, *pt_prev = *pt;
list_for_each_entry_rcu(ptype, ptype_list, list) {
if (ptype-&gt;type != type)
continue;
if (pt_prev)
deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
*pt = pt_prev;
}
```
在网络包struct sk_buff里面二层的头里面有一个protocol表示里面一层也即三层是什么协议。deliver_ptype_list_skb在一个协议列表中逐个匹配。如果能够匹配到就返回。
这些协议的注册在网络协议栈初始化的时候, inet_init函数调用dev_add_pack(&amp;ip_packet_type)添加IP协议。协议被放在一个链表里面。
```
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
list_add_rcu(&amp;pt-&gt;list, head);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt-&gt;type == htons(ETH_P_ALL))
return pt-&gt;dev ? &amp;pt-&gt;dev-&gt;ptype_all : &amp;ptype_all;
else
return pt-&gt;dev ? &amp;pt-&gt;dev-&gt;ptype_specific : &amp;ptype_base[ntohs(pt-&gt;type) &amp; PTYPE_HASH_MASK];
}
```
假设这个时候的网络包是一个IP包则在这个链表里面一定能够找到ip_packet_type在__netif_receive_skb_core中会调用ip_packet_type的func函数。
```
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};
```
从上面的定义我们可以看出接下来ip_rcv会被调用。
## 网络协议栈的IP层
从ip_rcv函数开始我们的处理逻辑就从二层到了三层IP层。
```
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
const struct iphdr *iph;
struct net *net;
u32 len;
......
net = dev_net(dev);
......
iph = ip_hdr(skb);
len = ntohs(iph-&gt;tot_len);
skb-&gt;transport_header = skb-&gt;network_header + iph-&gt;ihl*4;
......
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
......
}
```
在ip_rcv中得到IP头然后又遇到了我们见过多次的NF_HOOK这次因为是接收网络包第一个hook点是NF_INET_PRE_ROUTING也就是iptables的PREROUTING链。如果里面有规则则执行规则然后调用ip_rcv_finish。
```
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
const struct iphdr *iph = ip_hdr(skb);
struct net_device *dev = skb-&gt;dev;
struct rtable *rt;
int err;
......
rt = skb_rtable(skb);
.....
return dst_input(skb);
}
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)-&gt;input(skb);
```
ip_rcv_finish得到网络包对应的路由表然后调用dst_input在dst_input中调用的是struct rtable的成员的dst的input函数。在rt_dst_alloc中我们可以看到input函数指向的是ip_local_deliver。
```
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
struct net *net = dev_net(skb-&gt;dev);
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb-&gt;dev, NULL,
ip_local_deliver_finish);
}
```
在ip_local_deliver函数中如果IP层进行了分段则进行重新的组合。接下来就是我们熟悉的NF_HOOK。hook点在NF_INET_LOCAL_IN对应iptables里面的INPUT链。在经过iptables规则处理完毕后我们调用ip_local_deliver_finish。
```
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
__skb_pull(skb, skb_network_header_len(skb));
int protocol = ip_hdr(skb)-&gt;protocol;
const struct net_protocol *ipprot;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
int ret;
ret = ipprot-&gt;handler(skb);
......
}
......
}
```
在IP头中有一个字段protocol用于指定里面一层的协议在这里应该是TCP协议。于是从inet_protos数组中找出TCP协议对应的处理函数。这个数组的定义如下里面的内容是struct net_protocol。
```
struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly;
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
......
return !cmpxchg((const struct net_protocol **)&amp;inet_protos[protocol],
NULL, prot) ? 0 : -1;
}
static int __init inet_init(void)
{
......
if (inet_add_protocol(&amp;udp_protocol, IPPROTO_UDP) &lt; 0)
pr_crit(&quot;%s: Cannot add UDP protocol\n&quot;, __func__);
if (inet_add_protocol(&amp;tcp_protocol, IPPROTO_TCP) &lt; 0)
pr_crit(&quot;%s: Cannot add TCP protocol\n&quot;, __func__);
......
}
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
static struct net_protocol udp_protocol = {
.early_demux = udp_v4_early_demux,
.early_demux_handler = udp_v4_early_demux,
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
```
在系统初始化的时候网络协议栈的初始化调用的是inet_init它会调用inet_add_protocol将TCP协议对应的处理函数tcp_protocol、UDP协议对应的处理函数udp_protocol放到inet_protos数组中。
在上面的网络包的接收过程中会取出TCP协议对应的处理函数tcp_protocol然后调用handler函数也即tcp_v4_rcv函数。
## 总结时刻
这一节我们讲了接收网络包的上半部分,分以下几个层次。
- 硬件网卡接收到网络包之后通过DMA技术将网络包放入Ring Buffer。
- 硬件网卡通过中断通知CPU新的网络包的到来。
- 网卡驱动程序会注册中断处理函数ixgb_intr。
- 中断处理函数处理完需要暂时屏蔽中断的核心流程之后通过软中断NET_RX_SOFTIRQ触发接下来的处理过程。
- NET_RX_SOFTIRQ软中断处理函数net_rx_actionnet_rx_action会调用napi_poll进而调用ixgb_clean_rx_irq从Ring Buffer中读取数据到内核struct sk_buff。
- 调用netif_receive_skb进入内核网络协议栈进行一些关于VLAN的二层逻辑处理后调用ip_rcv进入三层IP层。
- 在IP层会处理iptables规则然后调用ip_local_deliver交给更上层TCP层。
- 在TCP层调用tcp_v4_rcv。
<img src="https://static001.geekbang.org/resource/image/a5/37/a51af8ada1135101e252271626669337.png" alt="">
## 课堂练习
我们没有仔细分析对于二层VLAN的处理请你研究一下VLAN的原理然后在代码中看一下对于VLAN的处理过程这是一项重要的网络基础知识。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,529 @@
<audio id="audio" title="48 | 接收网络包(下):如何搞明白合作伙伴让我们做什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/46/35/469ce452d6a394e9c4048082bd8e1b35.mp3"></audio>
上一节我们解析了网络包接收的上半部分从硬件网卡到IP层。这一节我们接着来解析TCP层和Socket层都做了哪些事情。
## 网络协议栈的TCP层
从tcp_v4_rcv函数开始我们的处理逻辑就从IP层到了TCP层。
```
int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb-&gt;dev);
const struct iphdr *iph;
const struct tcphdr *th;
bool refcounted;
struct sock *sk;
int ret;
......
th = (const struct tcphdr *)skb-&gt;data;
iph = ip_hdr(skb);
......
TCP_SKB_CB(skb)-&gt;seq = ntohl(th-&gt;seq);
TCP_SKB_CB(skb)-&gt;end_seq = (TCP_SKB_CB(skb)-&gt;seq + th-&gt;syn + th-&gt;fin + skb-&gt;len - th-&gt;doff * 4);
TCP_SKB_CB(skb)-&gt;ack_seq = ntohl(th-&gt;ack_seq);
TCP_SKB_CB(skb)-&gt;tcp_flags = tcp_flag_byte(th);
TCP_SKB_CB(skb)-&gt;tcp_tw_isn = 0;
TCP_SKB_CB(skb)-&gt;ip_dsfield = ipv4_get_dsfield(iph);
TCP_SKB_CB(skb)-&gt;sacked = 0;
lookup:
sk = __inet_lookup_skb(&amp;tcp_hashinfo, skb, __tcp_hdrlen(th), th-&gt;source, th-&gt;dest, &amp;refcounted);
process:
if (sk-&gt;sk_state == TCP_TIME_WAIT)
goto do_time_wait;
if (sk-&gt;sk_state == TCP_NEW_SYN_RECV) {
......
}
......
th = (const struct tcphdr *)skb-&gt;data;
iph = ip_hdr(skb);
skb-&gt;dev = NULL;
if (sk-&gt;sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
......
if (!sock_owned_by_user(sk)) {
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
goto discard_and_relse;
}
......
}
```
在tcp_v4_rcv中得到TCP的头之后我们可以开始处理TCP层的事情。因为TCP层是分状态的状态被维护在数据结构struct sock里面因而我们要根据IP地址以及TCP头里面的内容在tcp_hashinfo中找到这个包对应的struct sock从而得到这个包对应的连接的状态。
接下来我们就根据不同的状态做不同的处理例如上面代码中的TCP_LISTEN、TCP_NEW_SYN_RECV状态属于连接建立过程中。这个我们在讲三次握手的时候讲过了。再如TCP_TIME_WAIT状态是连接结束的时候的状态这个我们暂时可以不用看。
接下来,我们来分析最主流的网络包的接收过程,这里面涉及三个队列:
- backlog队列
- prequeue队列
- sk_receive_queue队列
为什么接收网络包的过程,需要在这三个队列里面倒腾过来、倒腾过去呢?这是因为,同样一个网络包要在三个主体之间交接。
第一个主体是**软中断的处理过程**。如果你没忘记的话我们在执行tcp_v4_rcv函数的时候依然处于软中断的处理逻辑里所以必然会占用这个软中断。
第二个主体就是**用户态进程**。如果用户态触发系统调用read读取网络包也要从队列里面找。
第三个主体就是**内核协议栈**。哪怕用户进程没有调用read读取网络包当网络包来的时候也得有一个地方收着呀。
这时候我们就能够了解上面代码中sock_owned_by_user的意思了其实就是说当前这个sock是不是正有一个用户态进程等着读数据呢如果没有内核协议栈也调用tcp_add_backlog暂存在backlog队列中并且抓紧离开软中断的处理过程。
如果有一个用户态进程等待读取数据呢我们先调用tcp_prequeue也即赶紧放入prequeue队列并且离开软中断的处理过程。在这个函数里面我们会看到对于sysctl_tcp_low_latency的判断也即是不是要低时延地处理网络包。
如果把sysctl_tcp_low_latency设置为0那就要放在prequeue队列中暂存这样不用等待网络包处理完毕就可以离开软中断的处理过程但是会造成比较长的时延。如果把sysctl_tcp_low_latency设置为1我们还是调用tcp_v4_do_rcv。
```
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
if (sk-&gt;sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk-&gt;sk_rx_dst;
......
tcp_rcv_established(sk, skb, tcp_hdr(skb), skb-&gt;len);
return 0;
}
......
if (tcp_rcv_state_process(sk, skb)) {
......
}
return 0;
......
}
```
在tcp_v4_do_rcv中分两种情况一种情况是连接已经建立处于TCP_ESTABLISHED状态调用tcp_rcv_established。另一种情况就是其他的状态调用tcp_rcv_state_process。
```
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcphdr *th = tcp_hdr(skb);
struct request_sock *req;
int queued = 0;
bool acceptable;
switch (sk-&gt;sk_state) {
case TCP_CLOSE:
......
case TCP_LISTEN:
......
case TCP_SYN_SENT:
......
}
......
switch (sk-&gt;sk_state) {
case TCP_SYN_RECV:
......
case TCP_FIN_WAIT1:
......
case TCP_CLOSING:
......
case TCP_LAST_ACK:
......
}
/* step 7: process the segment text */
switch (sk-&gt;sk_state) {
case TCP_CLOSE_WAIT:
case TCP_CLOSING:
case TCP_LAST_ACK:
......
case TCP_FIN_WAIT1:
case TCP_FIN_WAIT2:
......
case TCP_ESTABLISHED:
......
}
}
```
在tcp_rcv_state_process中如果我们对着TCP的状态图进行比对能看到对于TCP所有状态的处理其中和连接建立相关的状态咱们已经分析过所以我们重点关注连接状态下的工作模式。
<img src="https://static001.geekbang.org/resource/image/38/c6/385ff4a348dfd2f64feb0d7ba81e2bc6.png" alt="">
在连接状态下我们会调用tcp_rcv_established。在这个函数里面我们会调用tcp_data_queue将其放入sk_receive_queue队列进行处理。
```
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
bool fragstolen = false;
......
if (TCP_SKB_CB(skb)-&gt;seq == tp-&gt;rcv_nxt) {
if (tcp_receive_window(tp) == 0)
goto out_of_window;
/* Ok. In sequence. In window. */
if (tp-&gt;ucopy.task == current &amp;&amp;
tp-&gt;copied_seq == tp-&gt;rcv_nxt &amp;&amp; tp-&gt;ucopy.len &amp;&amp;
sock_owned_by_user(sk) &amp;&amp; !tp-&gt;urg_data) {
int chunk = min_t(unsigned int, skb-&gt;len,
tp-&gt;ucopy.len);
__set_current_state(TASK_RUNNING);
if (!skb_copy_datagram_msg(skb, 0, tp-&gt;ucopy.msg, chunk)) {
tp-&gt;ucopy.len -= chunk;
tp-&gt;copied_seq += chunk;
eaten = (chunk == skb-&gt;len);
tcp_rcv_space_adjust(sk);
}
}
if (eaten &lt;= 0) {
queue_and_out:
......
eaten = tcp_queue_rcv(sk, skb, 0, &amp;fragstolen);
}
tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)-&gt;end_seq);
......
if (!RB_EMPTY_ROOT(&amp;tp-&gt;out_of_order_queue)) {
tcp_ofo_queue(sk);
......
}
......
return;
}
if (!after(TCP_SKB_CB(skb)-&gt;end_seq, tp-&gt;rcv_nxt)) {
/* A retransmit, 2nd most common case. Force an immediate ack. */
tcp_dsack_set(sk, TCP_SKB_CB(skb)-&gt;seq, TCP_SKB_CB(skb)-&gt;end_seq);
out_of_window:
tcp_enter_quickack_mode(sk);
inet_csk_schedule_ack(sk);
drop:
tcp_drop(sk, skb);
return;
}
/* Out of window. F.e. zero window probe. */
if (!before(TCP_SKB_CB(skb)-&gt;seq, tp-&gt;rcv_nxt + tcp_receive_window(tp)))
goto out_of_window;
tcp_enter_quickack_mode(sk);
if (before(TCP_SKB_CB(skb)-&gt;seq, tp-&gt;rcv_nxt)) {
/* Partial packet, seq &lt; rcv_next &lt; end_seq */
tcp_dsack_set(sk, TCP_SKB_CB(skb)-&gt;seq, tp-&gt;rcv_nxt);
/* If window is closed, drop tail of packet. But after
* remembering D-SACK for its head made in previous line.
*/
if (!tcp_receive_window(tp))
goto out_of_window;
goto queue_and_out;
}
tcp_data_queue_ofo(sk, skb);
}
```
在tcp_data_queue中对于收到的网络包我们要分情况进行处理。
第一种情况seq == tp-&gt;rcv_nxt说明来的网络包正是我服务端期望的下一个网络包。这个时候我们判断sock_owned_by_user也即用户进程也是正在等待读取这种情况下就直接skb_copy_datagram_msg将网络包拷贝给用户进程就可以了。
如果用户进程没有正在等待读取或者因为内存原因没有能够拷贝成功tcp_queue_rcv里面还是将网络包放入sk_receive_queue队列。
接下来tcp_rcv_nxt_update将tp-&gt;rcv_nxt设置为end_seq也即当前的网络包接收成功后更新下一个期待的网络包。
这个时候我们还会判断一下另一个队列out_of_order_queue也看看乱序队列的情况看看乱序队列里面的包会不会因为这个新的网络包的到来也能放入到sk_receive_queue队列中。
例如客户端发送的网络包序号为5、6、7、8、9。在5还没有到达的时候服务端的rcv_nxt应该是5也即期望下一个网络包是5。但是由于中间网络通路的问题5、6还没到达服务端7、8已经到达了服务端了这就出现了乱序。
乱序的包不能进入sk_receive_queue队列。因为一旦进入到这个队列意味着可以发送给用户进程。然而按照TCP的定义用户进程应该是按顺序收到包的没有排好序就不能给用户进程。所以7、8不能进入sk_receive_queue队列只能暂时放在out_of_order_queue乱序队列中。
当5、6到达的时候5、6先进入sk_receive_queue队列。这个时候我们再来看out_of_order_queue乱序队列中的7、8发现能够接上。于是7、8也能进入sk_receive_queue队列了。tcp_ofo_queue函数就是做这个事情的。
至此第一种情况处理完毕。
第二种情况end_seq不大于rcv_nxt也即服务端期望网络包5。但是来了一个网络包3怎样才会出现这种情况呢肯定是服务端早就收到了网络包3但是ACK没有到达客户端中途丢了那客户端就认为网络包3没有发送成功于是又发送了一遍这种情况下要赶紧给客户端再发送一次ACK表示早就收到了。
第三种情况seq不小于rcv_nxt + tcp_receive_window。这说明客户端发送得太猛了。本来seq肯定应该在接收窗口里面的这样服务端才来得及处理结果现在超出了接收窗口说明客户端一下子把服务端给塞满了。
这种情况下服务端不能再接收数据包了只能发送ACK了在ACK中会将接收窗口为0的情况告知客户端客户端就知道不能再发送了。这个时候双方只能交互窗口探测数据包直到服务端因为用户进程把数据读走了空出接收窗口才能在ACK里面再次告诉客户端又有窗口了又能发送数据包了。
第四种情况seq小于rcv_nxt但是end_seq大于rcv_nxt这说明从seq到rcv_nxt这部分网络包原来的ACK客户端没有收到所以重新发送了一次从rcv_nxt到end_seq时新发送的可以放入sk_receive_queue队列。
当前四种情况都排除掉了说明网络包一定是一个乱序包了。这里有点儿难理解我们还是用上面那个乱序的例子仔细分析一下rcv_nxt=5。
我们假设tcp_receive_window也是5也即超过10服务端就接收不了了。当前来的这个网络包既不在rcv_nxt之前不是3这种也不在rcv_nxt + tcp_receive_window之后不是11这种说明这正在我们期望的接收窗口里面但是又不是rcv_nxt不是我们马上期望的网络包5这正是上面的例子中网络包7、8的情况。
对于网络包7、8我们只好调用tcp_data_queue_ofo进入out_of_order_queue乱序队列但是没有关系当网络包5、6到来的时候我们会走第一种情况把7、8拿出来放到sk_receive_queue队列中。
至此,网络协议栈的处理过程就结束了。
## Socket层
当接收的网络包进入各种队列之后,接下来我们就要等待用户进程去读取它们了。
读取一个socket就像读取一个文件一样读取socket的文件描述符通过read系统调用。
read系统调用对于一个文件描述符的操作大致过程都是类似的在文件系统那一节我们已经详细解析过。最终它会调用到用来表示一个打开文件的结构stuct file指向的file_operations操作。
对于socket来讲它的file_operations定义如下
```
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
```
按照文件系统的读取流程调用的是sock_read_iter。
```
static ssize_t sock_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct file *file = iocb-&gt;ki_filp;
struct socket *sock = file-&gt;private_data;
struct msghdr msg = {.msg_iter = *to,
.msg_iocb = iocb};
ssize_t res;
if (file-&gt;f_flags &amp; O_NONBLOCK)
msg.msg_flags = MSG_DONTWAIT;
......
res = sock_recvmsg(sock, &amp;msg, msg.msg_flags);
*to = msg.msg_iter;
return res;
}
```
在sock_read_iter中通过VFS中的struct file将创建好的socket结构拿出来然后调用sock_recvmsgsock_recvmsg会调用sock_recvmsg_nosec。
```
static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg, int flags)
{
return sock-&gt;ops-&gt;recvmsg(sock, msg, msg_data_left(msg), flags);
}
```
这里调用了socket的ops的recvmsg这个我们遇到好几次了。根据inet_stream_ops的定义这里调用的是inet_recvmsg。
```
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
struct sock *sk = sock-&gt;sk;
int addr_len = 0;
int err;
......
err = sk-&gt;sk_prot-&gt;recvmsg(sk, msg, size, flags &amp; MSG_DONTWAIT,
flags &amp; ~MSG_DONTWAIT, &amp;addr_len);
......
}
```
这里面从socket结构我们可以得到更底层的sock结构然后调用sk_prot的recvmsg方法。这个同样遇到好几次了根据tcp_prot的定义调用的是tcp_recvmsg。
```
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
struct tcp_sock *tp = tcp_sk(sk);
int copied = 0;
u32 peek_seq;
u32 *seq;
unsigned long used;
int err;
int target; /* Read at least this many bytes */
long timeo;
struct task_struct *user_recv = NULL;
struct sk_buff *skb, *last;
.....
do {
u32 offset;
......
/* Next get a buffer. */
last = skb_peek_tail(&amp;sk-&gt;sk_receive_queue);
skb_queue_walk(&amp;sk-&gt;sk_receive_queue, skb) {
last = skb;
offset = *seq - TCP_SKB_CB(skb)-&gt;seq;
if (offset &lt; skb-&gt;len)
goto found_ok_skb;
......
}
......
if (!sysctl_tcp_low_latency &amp;&amp; tp-&gt;ucopy.task == user_recv) {
/* Install new reader */
if (!user_recv &amp;&amp; !(flags &amp; (MSG_TRUNC | MSG_PEEK))) {
user_recv = current;
tp-&gt;ucopy.task = user_recv;
tp-&gt;ucopy.msg = msg;
}
tp-&gt;ucopy.len = len;
/* Look: we have the following (pseudo)queues:
*
* 1. packets in flight
* 2. backlog
* 3. prequeue
* 4. receive_queue
*
* Each queue can be processed only if the next ones
* are empty.
*/
if (!skb_queue_empty(&amp;tp-&gt;ucopy.prequeue))
goto do_prequeue;
}
if (copied &gt;= target) {
/* Do not sleep, just process backlog. */
release_sock(sk);
lock_sock(sk);
} else {
sk_wait_data(sk, &amp;timeo, last);
}
if (user_recv) {
int chunk;
chunk = len - tp-&gt;ucopy.len;
if (chunk != 0) {
len -= chunk;
copied += chunk;
}
if (tp-&gt;rcv_nxt == tp-&gt;copied_seq &amp;&amp;
!skb_queue_empty(&amp;tp-&gt;ucopy.prequeue)) {
do_prequeue:
tcp_prequeue_process(sk);
chunk = len - tp-&gt;ucopy.len;
if (chunk != 0) {
len -= chunk;
copied += chunk;
}
}
}
continue;
found_ok_skb:
/* Ok so how much can we use? */
used = skb-&gt;len - offset;
if (len &lt; used)
used = len;
if (!(flags &amp; MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used);
......
}
*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);
......
} while (len &gt; 0);
......
}
```
tcp_recvmsg这个函数比较长里面逻辑也很复杂好在里面有一段注释概括了这里面的逻辑。注释里面提到了三个队列receive_queue队列、prequeue队列和backlog队列。这里面我们需要把前一个队列处理完毕才处理后一个队列。
tcp_recvmsg的整个逻辑也是这样执行的这里面有一个while循环不断地读取网络包。
这里我们会先处理sk_receive_queue队列。如果找到了网络包就跳到found_ok_skb这里。这里会调用skb_copy_datagram_msg将网络包拷贝到用户进程中然后直接进入下一层循环。
直到sk_receive_queue队列处理完毕我们才到了sysctl_tcp_low_latency判断。如果不需要低时延则会有prequeue队列。于是我们能就跳到do_prequeue这里调用tcp_prequeue_process进行处理。
如果sysctl_tcp_low_latency设置为1也即没有prequeue队列或者prequeue队列为空则需要处理backlog队列在release_sock函数中处理。
release_sock会调用__release_sock这里面会依次处理队列中的网络包。
```
void release_sock(struct sock *sk)
{
......
if (sk-&gt;sk_backlog.tail)
__release_sock(sk);
......
}
static void __release_sock(struct sock *sk)
__releases(&amp;sk-&gt;sk_lock.slock)
__acquires(&amp;sk-&gt;sk_lock.slock)
{
struct sk_buff *skb, *next;
while ((skb = sk-&gt;sk_backlog.head) != NULL) {
sk-&gt;sk_backlog.head = sk-&gt;sk_backlog.tail = NULL;
do {
next = skb-&gt;next;
prefetch(next);
skb-&gt;next = NULL;
sk_backlog_rcv(sk, skb);
cond_resched();
skb = next;
} while (skb != NULL);
}
......
}
```
最后哪里都没有网络包我们只好调用sk_wait_data继续等待在哪里等待网络包的到来。
至此,网络包的接收过程到此结束。
## 总结时刻
这一节我们讲完了接收网络包,我们来从头串一下,整个过程可以分成以下几个层次。
- 硬件网卡接收到网络包之后通过DMA技术将网络包放入Ring Buffer
- 硬件网卡通过中断通知CPU新的网络包的到来
- 网卡驱动程序会注册中断处理函数ixgb_intr
- 中断处理函数处理完需要暂时屏蔽中断的核心流程之后通过软中断NET_RX_SOFTIRQ触发接下来的处理过程
- NET_RX_SOFTIRQ软中断处理函数net_rx_actionnet_rx_action会调用napi_poll进而调用ixgb_clean_rx_irq从Ring Buffer中读取数据到内核struct sk_buff
- 调用netif_receive_skb进入内核网络协议栈进行一些关于VLAN的二层逻辑处理后调用ip_rcv进入三层IP层
- 在IP层会处理iptables规则然后调用ip_local_deliver交给更上层TCP层
- 在TCP层调用tcp_v4_rcv这里面有三个队列需要处理如果当前的Socket不是正在被读则放入backlog队列如果正在被读取不需要很实时的话则放入prequeue队列其他情况调用tcp_v4_do_rcv
- 在tcp_v4_do_rcv中如果是处于TCP_ESTABLISHED状态调用tcp_rcv_established其他的状态调用tcp_rcv_state_process
- 在tcp_rcv_established中调用tcp_data_queue如果序列号能够接的上则放入sk_receive_queue队列如果序列号接不上则暂时放入out_of_order_queue队列等序列号能够接上的时候再放入sk_receive_queue队列。
至此内核接收网络包的过程到此结束,接下来就是用户态读取网络包的过程,这个过程分成几个层次。
- VFS层read系统调用找到struct file根据里面的file_operations的定义调用sock_read_iter函数。sock_read_iter函数调用sock_recvmsg函数。
- Socket层从struct file里面的private_data得到struct socket根据里面ops的定义调用inet_recvmsg函数。
- Sock层从struct socket里面的sk得到struct sock根据里面sk_prot的定义调用tcp_recvmsg函数。
- TCP层tcp_recvmsg函数会依次读取receive_queue队列、prequeue队列和backlog队列。
<img src="https://static001.geekbang.org/resource/image/20/52/20df32a842495d0f629ca5da53e47152.png" alt="">
## 课堂练习
对于TCP协议、三次握手、发送和接收的连接维护、拥塞控制、滑动窗口我们都解析过了。唯独四次挥手我们没有解析对应的代码你应该知道在什么地方了你可以自己试着解析一下四次挥手的过程。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">