一、环境构建
1.实验环境
- Ubuntu 22.04 LTS
- Linux-5.4.34
- busybox-1.36.0
2.环境配置
2.1 安装相关工具
axel是一款多线程下载工具,用于下载Linux内核源代码及其他大文件;build-essential软件包里面包含了很多开发必要的软件工具,比如make、gcc等;QEMU是一种通用的开源计算机仿真器和虚拟器,为自己编译构建的Linux系统运行提供虚拟硬件平台。
sudo apt update
sudo apt-get install axel
sudo apt-get install build-essential
sudo apt install qemu
sudo apt-get install libncurses5-dev bison flex libssl-dev libelf-dev
2.2 下载Linux内核源代码
使用axel多线程下载linux-5.4.34源代码压缩包并解压
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
xz -d linux-5.4.34.tar.xz
tar -xvf linux-5.4.34.tar
cd linux-5.4.34
2.3 配置Linux内核
配置方法分为更新配置和全新配置两类。更新配置是在原有配置文件的基础上修改一些配置项目,而全新配置则将原有的配置文件全部重新生成覆盖原有的配置文件。
make defconfig # Default configuration is based on 'x86_64_defconfig'
make menuconfig
# 打开 debug 相关选项
Kernel hacking --->
Compile-time checks and compiler options --->
[*] Compile the kernel with debug info
[*] Provide GDB scripts for kernel debugging
[*] Kernel debugging
# 关闭 KASLR,否则会导致打断点失败
Processor type and features ---->
[] Randomize the address of the kernel image (KASLR)
2.4 编译Linux内核
只要在源代码根目录下执行make命令就可以编译Linux内核了,不过为了加速编译的过程我们一般使用如下命令。
make -j$(nproc) # nproc gives the number of CPU cores/threads available
注意!!!!
Ubuntu22.04在编译Linux-5.4.34时会出先报错 missing symbol table
解决方法:
将/home/syl/Desktop/linux-5.4.34/tools/objtool/elf.c 路径下的
if (!symtab) {
WARN("missing symbol table");
return -1;
}
改为
if (!symtab) {
return 0;
}
2.5 制作根文件系统
我们这里为了简化实验环境配置,仅制作内存根文件系统。这里借助 BusyBox 构建极简内存根文件系统,提供基本的用户态可执行程序。
首先从 https://www.busybox.net 下载 busybox 源代码解压,解压完成后,跟Linux内核一样先配置编译,并安装。
需要注意在Ubuntu 22.04及更新的版本中,需要使用1.36.0及以上的版本才能正常运行!
axel -n 20 https://busybox.net/downloads/busybox-1.36.0.tar.bz2
tar -jxvf busybox-1.36.0.tar.bz2
cd busybox-1.36.0
make menuconfig
将编译配置选项中选中构建成静态链接,不用动态链接库。
Settings --->
[*] Build static binary (no shared libs)
然后编译安装,默认会安装到源码目录下的 _install 目录中。
make -j$(nproc) && make install
准备内存根文件系统所需的目录和文件
mkdir rootfs
cd rootfs
cp ../busybox-1.36.0/_install/* ./ -rf
mkdir dev proc sys home
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
准备 init 脚本文件放在根文件系统根目录下(rootfs/init),可以添加如下内容到 init 文件。系统并启动时会自动执行。
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "Wellcome OS!"
echo "--------------------"
echo "--------------------"
echo "--------------------"
cd home
/bin/sh
给 init 脚本添加可执行权限。
chmod +x init
准备网络程序用来触发socket API驱动内核TCP/IP协议栈。注意为了简化起见,我们使用静态编译的程序,并把静态编译的server和client拷贝到rootfs/home/目录下。
$ gcc server.c -o server -static
$ gcc client.c -o client -static
$ cp server client rootfs/home/
client.c
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
int main() {
struct sockaddr_in address;
int sock = 0, valread;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
memset(&serv_addr, '0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// Convert IPv4 and IPv6 addresses from text to binary form
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");
return 0;
}
server.c
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// Creating socket file descriptor
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// Forcefully attaching socket to the port 8080
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// Forcefully attaching socket to the port 8080
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) {
perror("accept");
exit(EXIT_FAILURE);
}
char buffer[1024] = {0};
read(new_socket, buffer, sizeof(buffer));
printf("Received message: %s\n", buffer);
return 0;
}
打包成内存根文件系统镜像。使用如下命令将rootfs目录打包成rootfs.cpio.gz文件。
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
测试挂载根文件系统。测试挂载根文件系统,看内核启动完成后是否执行 init 脚本。这时我们不仅可以用-kernel指定加载的Linux内核文件,还可以使用-initrd来指定内存根文件系统文件。
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
2.6 初始化Linux系统的网络功能
在Linux系统中手工命令一般会通过执行ifconfig来激活网络设备接口,ifconfig内部通过调用socket和ioctl来通知内核给网络设备接口做适当的设置工作,我们使用ifconfig配置IP并激活本地回环lo网络接口。
ifconfig lo 127.0.0.1 up
2.7 跟踪调试Linux内核网络代码
使用 gdb 跟踪调试内核,加两个参数,一个是-s,意思是在 TCP 1234 端口上创建了一个 gdb-server。可以另外打开一个窗口,用 gdb 把带有符号表的内核镜像 vmlinux 加载进来,然后连接 gdb server,设置断点跟踪内核。若不想使用 1234 端口,可以使用-gdb tcp:xxxx 来替代-s 选项),另一个是-S 代表启动时暂停虚拟机,等待 gdb 执行 continue 指令(可以简写为 c)。
# 窗口环境下启动QEMU虚拟机
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s
加-nographic -append "console=ttyS0"参数启动不会弹出 QEMU 虚拟机窗口,可以在纯命令行下启动虚拟机,此时可以通过“killall qemu-system-x86_64”命令强行关闭虚拟机。
# 纯命令行下启动QEMU虚拟机
qemu-system-x86_64 -kernel /home/syl/desktop/linux-5.4.34/arch/x86/boot/bzImage -initrd ../rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
再打开一个窗口,启动 gdb vmlinux,或进入gdb命令行后使用file vmlinux把内核符号表加载进来,然后通过target remote localhost:1234建立连接。
注意:需要加上localhost才能正常建立连接!!!
cd linux-5.4.34/
gdb vmlinux
...
(gdb) target remote localhost:1234
再加上一些断点测试。
b start_kernel
b tcp_v4_connect
b inet_csk_accept
接下来我们测试是否能调试TCP/IP源码。我们运行server程序和client程序,看是否能进入断点。
由于图形化下的qemu只有一个终端,因此可以在./server后面加上&,让其在后台运行,以便再运行client程序。
./server &
./client
通过bt可以查看到进入tcp_v4_connect时的堆栈,因此我们已经可以逐步调试TCP/IP的源码了。
2.8 环境配置总结
通过前述一系列的操作我们就可以像调试普通程序一样调试 Linux 内核了。Linux 的内核入口函数是位于 init/main.c 中的 start_kernel,相当于普通 C 程序的 main 函数,负责完成各种内核模块的初始化。 接下来如果执行网络程序server和client则会驱动Linux内核中的TCP/IP协议栈及相关的部分的执行,可以使用相同的方法跟踪网络部分的代码。
通过调试Linux内核中的TCP/IP协议栈及相关的部分的执行,我们可以对源代码进行深一步的分析。
二、TCP/IP协议栈源代码分析
1. inet_init
通过在启动时打上断点,当运行到inet_init()函数时,使用bt查看调用栈。
(gdb) b start_kernel
(gdb) b inet_init
(gdb) c
(gdb) c
(gdb) bt
通过调用栈我们可以看出inet_init()的调用路径以及如何被调用的。
调用链为:
kernel_init()
|
kernel_init_freeable()
|
do_basic_setup()
|
do_initcalls()
|
do_initcall_level()
|
do_one_initcall()
|
inet_init()
调用路径为:
inet_init 被 do_one_initcall 调用:
- 在
init/main.c
文件中,有一个do_one_initcall
函数,它用于执行一个初始化函数。do_one_initcall
函数接受一个函数指针作为参数,并调用该函数。- 在调用栈信息中,
do_one_initcall
调用了inet_init
函数。do_one_initcall 被 do_initcall_level 调用:
do_one_initcall
函数实际上是由do_initcall_level
函数调用的。do_initcall_level
函数用于执行一个特定级别的初始化函数,该级别由level
参数指定。do_initcall_level 被 do_initcalls 调用:
do_initcall_level
函数是由do_initcalls
函数调用的。do_initcalls
函数负责按照预定义的顺序执行不同级别的初始化函数,其中包括device_initcall
、early_initcall
、core_initcall
等。do_initcalls 被 do_basic_setup 调用:
do_initcalls
函数最终是由do_basic_setup
函数调用的。do_basic_setup
函数是在init/main.c
文件中,它是 Linux 内核启动的一个基本设置阶段。
do_initcalls
负责按照预定的顺序执行各种初始化函数。在 do_initcalls
中,do_one_initcall
函数被用于执行一个初始化函数,而 inet_init
函数就是在 core_initcall
阶段的其中之一。do_initcalls(inet_init)
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
定义了一个静态的initcall_levels数组,这是一个指针数组,数组的每个元素都是指向一个数组的指针。do_initcalls()循环调用do_initcall_level(level),level就是initcall的优先级数字,由for循环的终止条件是小于ARRAY_SIZE(initcall_levels) - 1可知,总共会调用从do_initcall_level(0)到do_initcall_level(7)一共八次,最后的__initcall_end是结束标记不会被执行,同样__initcall_start是开始标记,这些函数指针数组的声明在链接脚本arch/x86/kernel/vmlinux.lds中。vmlinux.lds定义了整个内核编译之后的链接过程,决定内核各个段的存储位置,以及入口地址。vmlinux.lds文件起到链接定位的作用。同样每一个普通的程序也都是有一个链接过程受到链接脚本(linker script, 一般以lds作为文件的后缀名)的控制,只是内核的链接脚本更加特殊一些。
在net/ipv4/af_inet.c中可以找到inet_init函数,它就是TCP/IP协议栈内核模块的初始化函数,我们将inet_init函数摘录如下。
static int __init inet_init(void)
{
...
}
fs_initcall(inet_init);
显然紧随inet_init函数之后的fs_initcall(inet_init);负责将inet_init添加到initcall。fs_initcall(inet_init);在函数体外部,看起来比较特殊,它是include/linux/init.h中的宏定义。
fs_initcall宏定义及其展开的形式,大致是说将内核模块初始化函数按照功能优先级分组放置到.init段,.init段在链接脚本arch/x86/kernel/vmlinux.lds中定义。vmlinux.lds定义了整个内核编译之后的链接过程,决定内核各个段的存储位置,以及入口地址。
2. 跟踪分析TCP/IP协议栈如何将自己与上层套接口与下层数据链路层关联起来的?
CP/IP协议栈作为Linux内核的一个软件模块嵌入到Linux内核中,从网络协议层次的角度看,大致如图,TCP/IP协议栈上面对接Socket套接口,下面对接二层链路层,即LAN或WAN网络协议及网络设备驱动。
我们看到inet_add_protocol和inet_register_protosw,以及不同协议的结构体变量,比如tcp_prot、udp_prot和tcp_protocol、udp_protocol等。
proto_register实际上就是把tcp_prot一类的struct proto结构体变量中的一些成员进行分配内存等初始化操作,并添加到proto_list双向链表中。
sock_register负责将inet_family_ops(其中主要包括inet_create)加入net_families[],即告知Socket套接口层存在TCP/IP协议栈。
inet_add_protocol负责将tcp_protocol、udp_protocol等结构体变量存入inet_protos[],即告知TCP/IP中的其他协议本协议的存在。
然后由inet_register_protosw将inetsw_array[],其中包括tcp_prot、udp_prot等已完成初始化的结构体变量,以及inet_stream_ops、inet_dgram_ops、inet_sockraw_ops等,存入inetsw双向链表,当inet_create执行时会根据创建的socket类型和协议选择inetsw双向链表中元素用来初始化struct socket结构体。
Socket套接口是通过将struct socket结构体在创建Socket描述符fd时加入file->private_data,这样通过Socket描述符fd能够找到对应的struct socket结构体变量。struct socket结构体中的struct proto_ops *ops指向Socket套接口函数对应的TCP/IP接口函数,即inet_stream_ops、inet_dgram_ops、inet_sockraw_ops中的函数指针,最终调用tcp_prot、udp_prot等具体协议的处理函数。
dev_add_pack(&ip_packet_type);则是将是告知链路层将IP数据包交给ip_rcv处理。同样在arp_init中也有类似的方式告知告知链路层将ARP数据包交给arp_rcv处理。
TCP/IP协议栈通过使用套接口(Socket)来与上层应用程序关联,而与下层数据链路层关联则通过网络接口卡(NIC)和驱动程序来实现。我们可以设置一些断点具体查看是如何关联的
#套接层断点
break sock_create
#TCP/IP协议栈断点
break tcp_v4_rcv
#数据链路层断点
break netif_receive_skb
break dev_hard_start_xmit
#网络设备断点
break register_netdevice
break unregister_netdevice
通过下图的堆栈我们可以调用关系,从而看出数据链路层是如何与TCP/IP层发生关联的。
通过跟踪分析TCP/IP协议栈,我们知道了TCP/IP协议栈如何与上层套接口和下层数据链路层关联起来的。在Linux内核中,TCP/IP协议栈通过注册回调函数的方式将自己与上层套接口和下层数据链路层关联起来。当上层套接口需要发送或接收数据时,它会调用协议栈的回调函数。同时,协议栈也会注册自己的回调函数,以便在数据链路层接收到数据时能够正确地处理。这种关联方式使得协议栈能够与上层套接口和下层数据链路层进行交互,完成数据的发送和接收。
3. TCP的三次握手源代码跟踪分析,跟踪找出设置和发送SYN/ACK的位置,以及状态转换的位置
TCP协议相关的代码主要集中在/net/ipv4/目录下,其中/net/ipv4/tcp_ipv4.c文件中的结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数,socket接口层里sock->opt->connect和sock->opt->accept对应的接口函数即是在这里初始化的,sock->opt->connect实际调用的是tcp_v4_connect函数,sock->opt->accept实际调用的是inet_csk_accept函数。
在创建socket套接字描述符时sys_socket内核函数会根据指定的协议(例如socket(PF_INET,SOCK_STREAM,0))挂载上对应的协议处理函数。
TCP的三次握手从用户程序的角度看就是客户端connect和服务端accept建立起连接时背后完成的工作,在内核socket接口层这两个socket API函数对应着sys_connect和sys_accept函数,进一步对应着sock->opt->connect和sock->opt->accept两个函数指针,在TCP协议中这两个函数指针对应着tcp_v4_connect函数和inet_csk_accept函数。
我们可以通过前面构建的内核调试环境设置断点跟踪tcp_v4_connect函数和inet_csk_accept函数来进一步验证三次握手的过程。
b tcp_v4_connect
b inet_csk_accept
tcp_v4_connect函数的主要作用就是发起一个TCP连接,建立TCP连接的过程自然需要底层协议的支持,因此我们从这个函数中可以看到它调用了IP层提供的一些服务,比如ip_route_connect和ip_route_newports从名称就可以简单分辨,这里我们关注在TCP层面的三次握手,不去深究底层协议提供的功能细节。我们可以看到这里设置了 TCP_SYN_SENT并进一步调用了tcp_connect(sk)来实际构造SYN并发送出去。
tcp_connect函数具体负责构造一个携带SYN标志位的TCP头并发送出去,同时还设置了计时器超时重发。
其中tcp_transmit_skb函数负责将tcp数据发送出去,这里调用了icsk->icsk_af_ops->queue_xmit函数指针,实际上就是在TCP/IP协议栈初始化时设定好的IP层向上提供数据发送接口ip_queue_xmit函数,这里TCP协议栈通过调用这个icsk->icsk_af_ops->queue_xmit函数指针来触发IP协议栈代码发送数据。
服务端调用inet_csk_accept函数会从请求队列中取出一个连接请求,如果队列为空则通过inet_csk_wait_for_connect阻塞住等待客户端的连接。inet_csk_wait_for_connect函数就是无限for循环,一旦有连接请求进来则跳出循环。
按以上思路跟踪调试代码,会发现connect之后将连接请求发送出去,accept等待连接请求,connect启动到返回和accept返回之间就是三次握手的时间。
以上的分析我们都是按照用户程序调用socket接口、通过系统调用陷入内核进入内核态的socket接口层代码,然后根据创建socket时指定协议选择适当的函数指针,比如sock->opt->connect和sock->opt->accept两个函数指针,从而进入协议处理代码中,我们以以TCP/IPv4为例(net/ipv4目录下),则是分别进入tcp_v4_connect函数和inet_csk_accept函数中。
以TCP/IPv4为例(net/ipv4目录下),则是分别进入tcp_v4_connect函数和inet_csk_accept函数中。总之,主要的思路是call-in的方式逐级进行函数调用,但是接收数据放入accept队列的代码我们还没有跟踪到,接下来我们需要换一个思路,网卡接收到数据需要通知上层协议来接收并处理数据,那么应该有TCP协议的接收数据的函数被底层网络驱动callback方式进行调用,针对这个思路我们需要回过头来看TCP/IP协议栈的初始化过程中是不是有将recv的函数指针发布给网络底层代码。
TCP/IP协议栈的初始化过程是在inet_init函数,其中有tcp_protocol结构体变量,其中的handler被赋值为tcp_v4_rcv。
1498static const struct net_protocol tcp_protocol = {
1499 .early_demux = tcp_v4_early_demux,
1500 .handler = tcp_v4_rcv,
1501 .err_handler = tcp_v4_err,
1502 .no_policy = 1,
1503 .netns_ok = 1,
1504 .icmp_strict_tag_validation = 1,
1505};
...
1708 /*
1709 * Add all the base protocols.
1710 */
1711
1712 if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
1713 pr_crit("%s: Cannot add ICMP protocol\n", __func__);
1714 if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
1715 pr_crit("%s: Cannot add UDP protocol\n", __func__);
1716 if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
1717 pr_crit("%s: Cannot add TCP protocol\n", __func__);
handler被赋值为tcp_v4_rcv。到这里我们就找到TCP协议中负责接收处理数据的入口,接收TCP连接请求及进行三次握手处理过程应该也是在这里为起点,从tcp_v4_rcv能够找到对SYN/ACK标志的处理(三次握手),连接请求建立后并将连接放入accept的等待队列。
我们先关注tcp_v4_connect:
/* This will initiate an outgoing connection. */
//对于TCP 协议来说,其连接实际上就是发送一个 SYN 报文,在服务器的应答到来时,回答它一个 ack 报文,也就是完成三次握手中的第一和第三次
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;
int err;
struct ip_options_rcu *inet_opt;
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL;
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;
nexthop = daddr = usin->sin_addr.s_addr;
inet_opt = rcu_dereference_protected(inet->inet_opt,
lockdep_sock_is_held(sk));
//将下一跳地址和目的地址的临时变量都暂时设为用户提交的地址。
if (inet_opt && inet_opt->opt.srr) {
if (!daddr)
return -EINVAL;
nexthop = inet_opt->opt.faddr;
}
//源端口
orig_sport = inet->inet_sport;
//目的端口
orig_dport = usin->sin_port;
fl4 = &inet->cork.fl.u.ip4;
//如果使用了来源地址路由,选择一个合适的下一跳地址。
rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
IPPROTO_TCP,
orig_sport, orig_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
return err;
}
if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}
//进行路由查找,并校验返回的路由的类型,TCP是不被允许使用多播和广播的
if (!inet_opt || !inet_opt->opt.srr)
daddr = fl4->daddr;
//更新目的地址临时变量——使用路由查找后返回的值
if (!inet->inet_saddr)
inet->inet_saddr = fl4->saddr;
sk_rcv_saddr_set(sk, inet->inet_saddr);
//如果还没有设置源地址,和本地发送地址,则使用路由中返回的值
if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
/* Reset inherited state */
tp->rx_opt.ts_recent = 0;
tp->rx_opt.ts_recent_stamp = 0;
if (likely(!tp->repair))
tp->write_seq = 0;
}
if (tcp_death_row.sysctl_tw_recycle &&
!tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr)
tcp_fetch_timewait_stamp(sk, &rt->dst);
//保存目的地址及端口
inet->inet_dport = usin->sin_port;
sk_daddr_set(sk, daddr);
inet_csk(sk)->icsk_ext_hdr_len = 0;
if (inet_opt)
inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
//设置最小允许的mss值 536
tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
/* Socket identity is still unknown (sport may be zero).
* However we set state to SYN-SENT and not releasing socket
* lock select source port, enter ourselves into the hash tables and
* complete initialization after this.
*/
//套接字状态被置为 TCP_SYN_SENT,
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(&tcp_death_row, sk);
if (err)
goto failure;
sk_set_txhash(sk);
//动态选择一个本地端口,并加入 hash 表,与bind(2)选择端口类似
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet->inet_sport, inet->inet_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
rt = NULL;
goto failure;
}
/* OK, now commit destination to socket. */
//设置下一跳地址,以及网卡分片相关
sk->sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &rt->dst);
//还未计算初始序号
if (!tp->write_seq && likely(!tp->repair))
//根据双方地址、端口计算初始序号
tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port);
//为 TCP报文计算一个 seq值(实际使用的值是 tp->write_seq+1) --> 根据初始序号和当前时间,随机算一个初始id
inet->inet_id = tp->write_seq ^ jiffies;
//函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去。
err = tcp_connect(sk);
rt = NULL;
if (err)
goto failure;
return 0;
failure:
/*
* This unhashes the socket and releases the local port,
* if necessary.
*/
tcp_set_state(sk, TCP_CLOSE);
ip_rt_put(rt);
sk->sk_route_caps = 0;
inet->inet_dport = 0;
return err;
}
在该函数主要完成:
1. 路由查找,得到下一跳地址,并更新socket对象的下一跳地址
2. 将socket对象的状态设置为TCP_SYN_SENT
3. 如果没设置序号初值,则选定一个随机初值
4. 调用函数tcp_connect完成报文构建和发送
tcp_connect:
/* Build a SYN and send it off. */
//由tcp_v4_connect()->tcp_connect()->tcp_transmit_skb()发送,并置为TCP_SYN_SENT.
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;
//初始化传输控制块中与连接相关的成员
tcp_connect_init(sk);
if (unlikely(tp->repair)) {
tcp_finish_connect(sk, NULL);
return 0;
}
//分配skbuff --> 为SYN段分配报文并进行初始化
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
if (unlikely(!buff))
return -ENOBUFS;
//构建syn报文
//在函数tcp_v4_connect中write_seq已经被初始化随机值
tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
tp->retrans_stamp = tcp_time_stamp;
//将报文添加到发送队列上
tcp_connect_queue_skb(sk, buff);
//显式拥塞通告 --->
//路由器在出现拥塞时通知TCP。当TCP段传递时,路由器使用IP首部中的2位来记录拥塞,当TCP段到达后,
//接收方知道报文段是否在某个位置经历过拥塞。然而,需要了解拥塞发生情况的是发送方,而非接收方。因
//此,接收方使用下一个ACK通知发送方有拥塞发生,然后,发送方做出响应,缩小自己的拥塞窗口。
tcp_ecn_send_syn(sk, buff);
/* Send off SYN; include data in Fast Open. */
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
//构造tcp头和ip头并发送
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
if (err == -ECONNREFUSED)
return err;
/* We change tp->snd_nxt after the tcp_transmit_skb() call
* in order to make this packet get counted in tcpOutSegs.
*/
tp->snd_nxt = tp->write_seq;
tp->pushed_seq = tp->write_seq;
TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);
/* Timer for repeating the SYN until an answer. */
//启动重传定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}
该函数完成:
1.初始化套接字跟连接相关的字段
2. 申请sk_buff空间
3. 将sk_buff初始化为syn报文,实质是操作tcp_skb_cb,在初始化TCP头的时候会用到
4. 调用tcp_connect_queue_skb()函数将报文sk_buff添加到发送队列sk->sk_write_queue
5. 调用tcp_transmit_skb()函数构造tcp头,然后交给网络层。
6. 初始化重传定时器
进入tcp_connect_queue_skb:
/* This routine actually transmits TCP packets queued in by
* tcp_do_sendmsg(). This is used by both the initial
* transmission and possible later retransmissions.
* All SKB's seen here are completely headerless. It is our
* job to build the TCP header, and pass the packet down to
* IP so it can do the same plus pass the packet off to the
* device.
*
* We are working here with either a clone of the original
* SKB, or a fresh unique copy made by the retransmit engine.
*/
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 tcp_out_options opts;
unsigned int tcp_options_size, tcp_header_size;
struct tcp_md5sig_key *md5;
struct tcphdr *th;
int err;
BUG_ON(!skb || !tcp_skb_pcount(skb));
tp = tcp_sk(sk);
//根据传递进来的clone_it参数来确定是否需要克隆待发送的报文
if (clone_it) {
skb_mstamp_get(&skb->skb_mstamp);
TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq
- tp->snd_una;
tcp_rate_skb_sent(sk, skb);
//如果一个SKB会被不同的用户独立操作,而这些用户可能只是修改SKB描述符中的某些字段值,如h、nh,则内核没有必要为每个用户复制一份完整
//的SKB描述及其相应的数据缓存区,而会为了提高性能,只作克隆操作。克隆过程只复制SKB描述符,同时增加数据缓存区的引用计数,以免共享数
//据被提前释放。完成这些功能的是skb_clone()。一个使用包克隆的场景是,一个接收包程序要把该包传递给多个接收者,例如包处理函数或者一
//个或多个网络模块。原始的及克隆的SKB描述符的cloned值都会被设置为1,克隆SKB描述符的users值置为1,这样在第一次释放时就会释放掉。同时
//将数据缓存区引用计数dataref递增1,因为又多了一个克隆SKB描述符指向它
if (unlikely(skb_cloned(skb)))
//如果skb已经被clone,则只能复制该skb的数据到新分配的skb中
skb = pskb_copy(skb, gfp_mask);
else
skb = skb_clone(skb, gfp_mask);
if (unlikely(!skb))
return -ENOBUFS;
}
//获取INET层和TCP层的传输控制块、skb中的TCP私有数据块
inet = inet_sk(sk);
tcb = TCP_SKB_CB(skb);
memset(&opts, 0, sizeof(opts));
//根据TCP选项重新调整TCP首部的长度
//判断当前TCP报文是否是SYN段,因为有些选项只能出现在SYN报文中,需做特别处理
if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
else
tcp_options_size = tcp_established_options(sk, skb, &opts,
&md5);
//tcp首部的总长度等于可选长度加上struct tcphdr
tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
/* if no packet is in qdisc/device queue, then allow XPS to select
* another queue. We can be called from tcp_tsq_handler()
* which holds one reference to sk_wmem_alloc.
*
* TODO: Ideally, in-flight pure ACK packets should not matter here.
* One way to get this would be to set skb->truesize = 2 on them.
*/
skb->ooo_okay = sk_wmem_alloc_get(sk) < SKB_TRUESIZE(1);
//调用skb_push()在数据部分的头部添加TCP首部,长度即为之前计算得到的那个tcp_header_size,实际上是把data指针往上移。
skb_push(skb, tcp_header_size);
skb_reset_transport_header(skb);
skb_orphan(skb);
skb->sk = sk;
skb->destructor = skb_is_tcp_pure_ack(skb) ? __sock_wfree : tcp_wfree;
skb_set_hash_from_sk(skb, sk);
atomic_add(skb->truesize, &sk->sk_wmem_alloc);
/* Build TCP header and checksum it. */
//填充TCP首部中的源端口source、目的端口dest、TCP报文的序号seq、确认序号ack_seq以及各个标志位
th = (struct tcphdr *)skb->data;
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(tp->rcv_nxt);
*(((__be16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) |
tcb->tcp_flags);
th->check = 0;
th->urg_ptr = 0;
/* The urg_mode check is necessary during a below snd_una win probe */
if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
if (before(tp->snd_up, tcb->seq + 0x10000)) {
th->urg_ptr = htons(tp->snd_up - tcb->seq);
th->urg = 1;
} else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {
th->urg_ptr = htons(0xFFFF);
th->urg = 1;
}
}
tcp_options_write((__be32 *)(th + 1), tp, &opts);
skb_shinfo(skb)->gso_type = sk->sk_gso_type;
//分两种情况设置TCP首部的接收窗口的大小
if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
//如果是SYN段,则设置接收窗口初始值为rcv_wnd
th->window = htons(tcp_select_window(sk));
//显式拥塞通告
tcp_ecn_send(sk, skb, th, tcp_header_size);
} else {
/* RFC1323: The window in SYN & SYN/ACK segments
* is never scaled.
*/
th->window = htons(min(tp->rcv_wnd, 65535U));
}
#ifdef CONFIG_TCP_MD5SIG
/* Calculate the MD5 hash, as we have all we need now */
if (md5) {
sk_nocaps_add(sk, NETIF_F_GSO_MASK);
tp->af_specific->calc_md5_hash(opts.hash_location,
md5, sk, skb);
}
#endif
// ipv4_specific --> tcp_v4_send_check
icsk->icsk_af_ops->send_check(sk, skb);
if (likely(tcb->tcp_flags & TCPHDR_ACK))
tcp_event_ack_sent(sk, tcp_skb_pcount(skb));
if (skb->len != tcp_header_size) {
tcp_event_data_sent(tp, sk);
tp->data_segs_out += tcp_skb_pcount(skb);
}
if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
tcp_skb_pcount(skb));
tp->segs_out += tcp_skb_pcount(skb);
/* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */
skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);
/* Our usage of tstamp should remain private */
skb->tstamp.tv64 = 0;
/* Cleanup our debris for IP stacks */
memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
sizeof(struct inet6_skb_parm)));
//构造ip头并发送,queue_xmit注册为 ip_queue_xmit 函数 --> ipv4_specific
//调用发送接口queue_xmit发送报文,进入到ip层,如果失败返回错误码。在TCP中该接口实现函数为 ip_queue_xmit()
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
if (likely(err <= 0))
return err;
tcp_enter_cwr(sk);
return net_xmit_eval(err);
}
主要是移动sk_buff的data指针,然后填充TCP头,接着的事就是交给网络层,将报文发出。这样三次握手中的第一次握手在客户端的层面完成,报文到达服务端,由服务端处理完毕后,第一次握手完成,客户端socket状态变为TCP_SYN_SENT。下面我看服务端的处理。
数据到达网卡的时候,对于TCP协议,将大致要经过这个一个调用链:
网卡驱动 ---> netif_receive_skb() ---> ip_rcv() ---> ip_local_deliver_finish() ---> tcp_v4_rcv()
cp_v4_rcv():
/*
* From tcp_input.c
*/
//网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()
int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
const struct iphdr *iph;
const struct tcphdr *th;
bool refcounted;
struct sock *sk;
int ret;
//如果不是发往本地的数据包,则直接丢弃
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
/* Count it even if it's bad */
__TCP_INC_STATS(net, TCP_MIB_INSEGS);
包长是否大于TCP头的长度
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
//tcp头 --> 不是很懂为何老是获取tcp头
th = (const struct tcphdr *)skb->data;
if (unlikely(th->doff < sizeof(struct tcphdr) / 4))
goto bad_packet;
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;
/* An explanation is required here, I think.
* Packet length and doff are validated by header prediction,
* provided case of th->doff==0 is eliminated.
* So, we defer the checks. */
if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))
goto csum_error;
//得到tcp的头 --> 不是很懂为何老是获取tcp头
th = (const struct tcphdr *)skb->data;
//得到ip报文头
iph = ip_hdr(skb);
/* This is tricky : We move IPCB at its correct location into TCP_SKB_CB()
* barrier() makes sure compiler wont play fool^Waliasing games.
*/
memmove(&TCP_SKB_CB(skb)->header.h4, IPCB(skb),
sizeof(struct inet_skb_parm));
barrier();
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th);
TCP_SKB_CB(skb)->tcp_tw_isn = 0;
TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);
TCP_SKB_CB(skb)->sacked = 0;
lookup:
//根据源端口号,目的端口号和接收的interface查找sock对象------>先在建立连接的哈希表中查找------>如果没找到就从监听哈希表中找
//对于建立过程来讲肯是监听哈希表中才能找到
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, &refcounted);
//如果找不到处理的socket对象,就把数据报丢掉
if (!sk)
goto no_tcp_socket;
process:
//检查sock是否处于半关闭状态
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk);
struct sock *nsk;
sk = req->rsk_listener;
if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
sk_drops_add(sk, skb);
reqsk_put(req);
goto discard_it;
}
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_csk_reqsk_queue_drop_and_put(sk, req);
goto lookup;
}
/* We own a reference on the listener, increase it again
* as we might lose it too soon.
*/
sock_hold(sk);
refcounted = true;
nsk = tcp_check_req(sk, skb, req, false);
if (!nsk) {
reqsk_put(req);
goto discard_and_relse;
}
if (nsk == sk) {
reqsk_put(req);
} else if (tcp_child_process(sk, nsk, skb)) {
tcp_v4_send_reset(nsk, skb);
goto discard_and_relse;
} else {
sock_put(sk);
return 0;
}
}
if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
__NET_INC_STATS(net, LINUX_MIB_TCPMINTTLDROP);
goto discard_and_relse;
}
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;
if (tcp_v4_inbound_md5_hash(sk, skb))
goto discard_and_relse;
nf_reset(skb);
if (tcp_filter(sk, skb))
goto discard_and_relse;
//tcp头部 --> 不是很懂为何老是获取tcp头
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
skb->dev = NULL;
//如果socket处于监听状态 --> 我们重点关注这里
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
sk_incoming_cpu_update(sk);
bh_lock_sock_nested(sk);
tcp_segs_in(tcp_sk(sk), skb);
ret = 0;
//查看是否有用户态进程对该sock进行了锁定
//如果sock_owned_by_user为真,则sock的状态不能进行更改
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;
}
bh_unlock_sock(sk);
put_and_return:
if (refcounted)
sock_put(sk);
return ret;
no_tcp_socket:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;
if (tcp_checksum_complete(skb)) {
csum_error:
__TCP_INC_STATS(net, TCP_MIB_CSUMERRORS);
bad_packet:
__TCP_INC_STATS(net, TCP_MIB_INERRS);
} else {
tcp_v4_send_reset(NULL, skb);
}
discard_it:
/* Discard frame. */
kfree_skb(skb);
return 0;
discard_and_relse:
sk_drops_add(sk, skb);
if (refcounted)
sock_put(sk);
goto discard_it;
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
if (tcp_checksum_complete(skb)) {
inet_twsk_put(inet_twsk(sk));
goto csum_error;
}
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
case TCP_TW_SYN: {
struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
&tcp_hashinfo, skb,
__tcp_hdrlen(th),
iph->saddr, th->source,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {
inet_twsk_deschedule_put(inet_twsk(sk));
sk = sk2;
refcounted = false;
goto process;
}
/* Fall through to ACK */
}
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:
tcp_v4_send_reset(sk, skb);
inet_twsk_deschedule_put(inet_twsk(sk));
goto discard_it;
case TCP_TW_SUCCESS:;
}
goto discard_it;
}
该函数主要工作就是根据tcp头部信息查到处理报文的socket对象,然后检查socket状态做不同处理,监听状态TCP_LISTEN,调用函数tcp_v4_do_rcv():
/* The socket must have it's spinlock held when we get
* here, unless it is a TCP_LISTEN socket.
*
* We have a potential double-lock case here, so even when
* doing backlog processing we use the BH locking scheme.
* This is because we cannot sleep with the original spinlock
* held.
*/
//网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()
//tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack()
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
//如果是连接已建立状态
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
sock_rps_save_rxhash(sk, skb);
sk_mark_napi_id(sk, skb);
if (dst) {
if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
!dst->ops->check(dst, 0)) {
dst_release(dst);
sk->sk_rx_dst = NULL;
}
}
tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
return 0;
}
if (tcp_checksum_complete(skb))
goto csum_err;
//如果是监听状态 --> 我们重点关注
if (sk->sk_state == TCP_LISTEN) {
//SYN Flood相关
struct sock *nsk = tcp_v4_cookie_check(sk, skb);
if (!nsk)
goto discard;
//SYN Flood相关
if (nsk != sk) {
sock_rps_save_rxhash(nsk, skb);
sk_mark_napi_id(nsk, skb);
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
} else
sock_rps_save_rxhash(sk, skb);
//根据不同状态做不同处理
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
reset:
tcp_v4_send_reset(rsk, skb);
discard:
kfree_skb(skb);
/* Be careful here. If this function gets more complicated and
* gcc suffers from register pressure on the x86, sk (in %ebx)
* might be destroyed here. This current version compiles correctly,
* but you have been warned.
*/
return 0;
csum_err:
TCP_INC_STATS(sock_net(sk), TCP_MIB_CSUMERRORS);
TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
调用tcp_rcv_state_process():
/*
* This function implements the receiving procedure of RFC 793 for
* all states except ESTABLISHED and TIME_WAIT.
* It's called from both tcp_v4_rcv and tcp_v6_rcv and should be
* address independent.
*/
//除了ESTABLISHED和TIME_WAIT状态外,其他状态下的TCP段处理都由本函数实现
// tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().
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->sk_state) {
//SYN_RECV状态的处理
case TCP_CLOSE:
goto discard;
//服务端第一次握手处理
case TCP_LISTEN:
if (th->ack)
return 1;
if (th->rst)
goto discard;
if (th->syn) {
if (th->fin)
goto discard;
// tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
consume_skb(skb);
return 0;
}
goto discard;
//客户端第二次握手处理
case TCP_SYN_SENT:
tp->rx_opt.saw_tstamp = 0;
//处理SYN_SENT状态下接收到的TCP段
queued = tcp_rcv_synsent_state_process(sk, skb, th);
if (queued >= 0)
return queued;
/* Do step6 onward by hand. */
//处理完第二次握手后,还需要处理带外数据
tcp_urg(sk, skb, th);
__kfree_skb(skb);
//检测是否有数据需要发送
tcp_data_snd_check(sk);
return 0;
}
tp->rx_opt.saw_tstamp = 0;
req = tp->fastopen_rsk;
if (req) {
WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
sk->sk_state != TCP_FIN_WAIT1);
if (!tcp_check_req(sk, skb, req, true))
goto discard;
}
if (!th->ack && !th->rst && !th->syn)
goto discard;
if (!tcp_validate_incoming(sk, skb, th, 0))
return 0;
/* step 5: check the ACK field */
acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH |
FLAG_UPDATE_TS_RECENT) > 0;
switch (sk->sk_state) {
//服务端第三次握手处理
case TCP_SYN_RECV:
if (!acceptable)
return 1;
if (!tp->srtt_us)
tcp_synack_rtt_meas(sk, req);
/* Once we leave TCP_SYN_RECV, we no longer need req
* so release it.
*/
if (req) {
inet_csk(sk)->icsk_retransmits = 0;
reqsk_fastopen_remove(sk, req, false);
} else {
/* Make sure socket is routed, for correct metrics. */
//建立路由,初始化拥塞控制模块
icsk->icsk_af_ops->rebuild_header(sk);
tcp_init_congestion_control(sk);
tcp_mtup_init(sk);
tp->copied_seq = tp->rcv_nxt;
tcp_init_buffer_space(sk);
}
smp_mb();
//正常的第三次握手,设置连接状态为TCP_ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);
/* Note, that this wakeup is only for marginal crossed SYN case.
* Passively open sockets are not waked up, because
* sk->sk_sleep == NULL and sk->sk_socket == NULL.
*/
//状态已经正常,唤醒那些等待的线程
if (sk->sk_socket)
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
if (tp->rx_opt.tstamp_ok)
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
if (req) {
/* Re-arm the timer because data may have been sent out.
* This is similar to the regular data transmission case
* when new data has just been ack'ed.
*
* (TFO) - we could try to be more aggressive and
* retransmitting any data sooner based on when they
* are sent out.
*/
tcp_rearm_rto(sk);
} else
tcp_init_metrics(sk);
if (!inet_csk(sk)->icsk_ca_ops->cong_control)
tcp_update_pacing_rate(sk);
/* Prevent spurious tcp_cwnd_restart() on first data packet */
//更新最近一次发送数据包的时间
tp->lsndtime = tcp_time_stamp;
tcp_initialize_rcv_mss(sk);
//计算有关TCP首部预测的标志
tcp_fast_path_on(tp);
break;
case TCP_FIN_WAIT1: {
struct dst_entry *dst;
int tmo;
/* If we enter the TCP_FIN_WAIT1 state and we are a
* Fast Open socket and this is the first acceptable
* ACK we have received, this would have acknowledged
* our SYNACK so stop the SYNACK timer.
*/
if (req) {
/* Return RST if ack_seq is invalid.
* Note that RFC793 only says to generate a
* DUPACK for it but for TCP Fast Open it seems
* better to treat this case like TCP_SYN_RECV
* above.
*/
if (!acceptable)
return 1;
/* We no longer need the request sock. */
reqsk_fastopen_remove(sk, req, false);
tcp_rearm_rto(sk);
}
if (tp->snd_una != tp->write_seq)
break;
tcp_set_state(sk, TCP_FIN_WAIT2);
sk->sk_shutdown |= SEND_SHUTDOWN;
dst = __sk_dst_get(sk);
if (dst)
dst_confirm(dst);
if (!sock_flag(sk, SOCK_DEAD)) {
/* Wake up lingering close() */
sk->sk_state_change(sk);
break;
}
if (tp->linger2 < 0 ||
(TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt))) {
tcp_done(sk);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
return 1;
}
tmo = tcp_fin_time(sk);
if (tmo > TCP_TIMEWAIT_LEN) {
inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
} else if (th->fin || sock_owned_by_user(sk)) {
/* Bad case. We could lose such FIN otherwise.
* It is not a big problem, but it looks confusing
* and not so rare event. We still can lose it now,
* if it spins in bh_lock_sock(), but it is really
* marginal case.
*/
inet_csk_reset_keepalive_timer(sk, tmo);
} else {
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
goto discard;
}
break;
}
case TCP_CLOSING:
if (tp->snd_una == tp->write_seq) {
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
goto discard;
}
break;
case TCP_LAST_ACK:
if (tp->snd_una == tp->write_seq) {
tcp_update_metrics(sk);
tcp_done(sk);
goto discard;
}
break;
}
/* step 6: check the URG bit */
tcp_urg(sk, skb, th);
/* step 7: process the segment text */
switch (sk->sk_state) {
case TCP_CLOSE_WAIT:
case TCP_CLOSING:
case TCP_LAST_ACK:
if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt))
break;
case TCP_FIN_WAIT1:
case TCP_FIN_WAIT2:
/* RFC 793 says to queue data in these states,
* RFC 1122 says we MUST send a reset.
* BSD 4.4 also does reset.
*/
if (sk->sk_shutdown & RCV_SHUTDOWN) {
if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
tcp_reset(sk);
return 1;
}
}
/* Fall through */
case TCP_ESTABLISHED:
tcp_data_queue(sk, skb);
queued = 1;
break;
}
/* tcp_data could move socket to TIME-WAIT */
if (sk->sk_state != TCP_CLOSE) {
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
}
if (!queued) {
discard:
tcp_drop(sk, skb);
}
return 0;
}
这是TCP建立连接的核心所在,几乎所有状态的套接字,在收到数据报时都在这里完成处理。对于服务端来说,收到第一次握手报文时的状态为TCP_LISTEN,处理代码为:
//服务端第一次握手处理
case TCP_LISTEN:
if (th->ack)
return 1;
if (th->rst)
goto discard;
if (th->syn) {
if (th->fin)
goto discard;
// tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
consume_skb(skb);
return 0;
}
goto discard;
接下将由tcp_v4_conn_request函数处理,而tcp_v4_conn_request实际上调用tcp_conn_request:
// tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
/* Never answer to SYNs send to broadcast or multicast */
if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
goto drop;
//tcp_request_sock_ops 定义在 tcp_ipv4.c 1256行
//inet_init --> proto_register --> req_prot_init -->初始化cache名
return tcp_conn_request(&tcp_request_sock_ops,
&tcp_request_sock_ipv4_ops, sk, skb);
drop:
tcp_listendrop(sk);
return 0;
}
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)
{
struct tcp_fastopen_cookie foc = { .len = -1 };
__u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
struct tcp_options_received tmp_opt;
struct tcp_sock *tp = tcp_sk(sk);
struct net *net = sock_net(sk);
struct sock *fastopen_sk = NULL;
struct dst_entry *dst = NULL;
struct request_sock *req;
bool want_cookie = false;
struct flowi fl;
/* TW buckets are converted to open requests without
* limitations, they conserve resources and peer is
* evidently real one.
*/
//处理TCP SYN FLOOD攻击相关的东西
//Client发送SYN包给Server后挂了,Server回给Client的SYN-ACK一直没收到Client的ACK确认,这个时候这个连接既没建立起来,
//也不能算失败。这就需要一个超时时间让Server将这个连接断开,否则这个连接就会一直占用Server的SYN连接队列中的一个位置,
//大量这样的连接就会将Server的SYN连接队列耗尽,让正常的连接无法得到处理。
//目前,Linux下默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔
//为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,
//TCP才会把断开这个连接。由于,SYN超时需要63秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的SYN包给Server(俗称 SYN flood 攻击),
//用于耗尽Server的SYN队列。对于应对SYN 过多的问题,linux提供了几个TCP参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。
if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}
/* Accept backlog is full. If we have already queued enough
* of warm entries in syn queue, drop request. It is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
//分配一个request_sock对象来代表这个半连接
//在三次握手协议中,服务器维护一个半连接队列,该队列为每个客户端的SYN包开设一个条目(服务端在接收到SYN包的时候,
//就已经创建了request_sock结构,存储在半连接队列中),该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客
//户的确认包(会进行第二次握手发送SYN+ACK 的包加以确认)。这些条目所标识的连接在服务器处于Syn_RECV状态,当服
//务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。该队列为SYN 队列,长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) ,
//在机器的tcp_max_syn_backlog值在/proc/sys/net/ipv4/tcp_max_syn_backlog下配置。
// tcp_request_sock_ops
//inet_init --> proto_register --> req_prot_init -->初始化cache名
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
if (!req)
goto drop;
//特定协议的request_sock的特殊操作函数集
tcp_rsk(req)->af_specific = af_ops;
tcp_clear_options(&tmp_opt);
tmp_opt.mss_clamp = af_ops->mss_clamp;
tmp_opt.user_mss = tp->rx_opt.user_mss;
tcp_parse_options(skb, &tmp_opt, 0, want_cookie ? NULL : &foc);
if (want_cookie && !tmp_opt.saw_tstamp)
tcp_clear_options(&tmp_opt);
tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
//初始化连接请求块,包括request_sock、inet_request_sock、tcp_request_sock
tcp_openreq_init(req, &tmp_opt, skb, sk);
inet_rsk(req)->no_srccheck = inet_sk(sk)->transparent;
/* Note: tcp_v6_init_req() might override ir_iif for link locals */
inet_rsk(req)->ir_iif = inet_request_bound_dev_if(sk, skb);
//tcp_request_sock_ipv4_ops --> tcp_v4_init_req
af_ops->init_req(req, sk, skb);
if (security_inet_conn_request(sk, skb, req))
goto drop_and_free;
if (!want_cookie && !isn) {
/* VJ's idea. We save last timestamp seen
* from the destination in peer table, when entering
* state TIME-WAIT, and check against it before
* accepting new connection request.
*
* If "isn" is not zero, this request hit alive
* timewait bucket, so that all the necessary checks
* are made in the function processing timewait state.
*/
if (tcp_death_row.sysctl_tw_recycle) {
bool strict;
dst = af_ops->route_req(sk, &fl, req, &strict);
if (dst && strict &&
!tcp_peer_is_proven(req, dst, true,
tmp_opt.saw_tstamp)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
goto drop_and_release;
}
}
/* Kill the following clause, if you dislike this way. */
else if (!net->ipv4.sysctl_tcp_syncookies &&
(sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(sysctl_max_syn_backlog >> 2)) &&
!tcp_peer_is_proven(req, dst, false,
tmp_opt.saw_tstamp)) {
/* Without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* It means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
rsk_ops->family);
goto drop_and_release;
}
isn = af_ops->init_seq(skb);
}
if (!dst) {
dst = af_ops->route_req(sk, &fl, req, NULL);
if (!dst)
goto drop_and_free;
}
//拥塞显式通告的东西
tcp_ecn_create_request(req, skb, sk, dst);
if (want_cookie) {
isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);
req->cookie_ts = tmp_opt.tstamp_ok;
if (!tmp_opt.tstamp_ok)
inet_rsk(req)->ecn_ok = 0;
}
tcp_rsk(req)->snt_isn = isn;
tcp_rsk(req)->txhash = net_tx_rndhash();
//接收窗口初始化
tcp_openreq_init_rwin(req, sk, dst);
if (!want_cookie) {
tcp_reqsk_record_syn(sk, req, skb);
fastopen_sk = tcp_try_fastopen(sk, skb, req, &foc, dst);
}
//握手过程传输数据相关的东西
if (fastopen_sk) {
af_ops->send_synack(fastopen_sk, dst, &fl, req,
&foc, TCP_SYNACK_FASTOPEN);
/* Add the child socket directly into the accept queue */
inet_csk_reqsk_queue_add(sk, req, fastopen_sk);
sk->sk_data_ready(sk);
bh_unlock_sock(fastopen_sk);
sock_put(fastopen_sk);
} else {
//设置TFO选项为false
tcp_rsk(req)->tfo_listener = false;
if (!want_cookie)
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
// tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_v4_conn_request() -> tcp_v4_send_synack().
// tcp_request_sock_ipv4_ops --> tcp_v4_send_synack
af_ops->send_synack(sk, dst, &fl, req, &foc,
!want_cookie ? TCP_SYNACK_NORMAL :
TCP_SYNACK_COOKIE);
if (want_cookie) {
reqsk_free(req);
return 0;
}
}
reqsk_put(req);
return 0;
drop_and_release:
dst_release(dst);
drop_and_free:
reqsk_free(req);
drop:
tcp_listendrop(sk);
return 0;
}
该函数功能有:
1. 分配一个request_sock对象来代表这次连接请求(状态为TCP_NEW_SYN_RECV),如果没有设置防范syn flood相关的选项,则将该request_sock添加到established状态的tcp_sock散列表(如果设置了防范选项,则request_sock对象都没有,只有建立完成时才会分配)
2. 调用tcp_v4_send_synack回复客户端ack,开启第二次握手,下面是tcp_v4_send_synack的代码解析
//向客户端发送SYN+ACK报文
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)
{
const struct inet_request_sock *ireq = inet_rsk(req);
struct flowi4 fl4;
int err = -1;
struct sk_buff *skb;
/* First, grab a route. */
//查找到客户端的路由
if (!dst && (dst = inet_csk_route_req(sk, &fl4, req)) == NULL)
return -1;
//根据路由、传输控制块、连接请求块中的构建SYN+ACK段
skb = tcp_make_synack(sk, dst, req, foc, synack_type);
//生成SYN+ACK段成功
if (skb) {
//生成校验码
__tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr);
//生成IP数据报并发送出去
err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
ireq->ir_rmt_addr,
ireq->opt);
err = net_xmit_eval(err);
}
return err;
}
查找客户端路由,构造syn包,然后调用ip_build_and_send_pkt,依靠网络层将数据报发出去。至此,第一次握手完成,第二次握手服务端层面完成。
数据报到达客户端网卡,同样经过:
网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() --> tcp_v4_do_rcv()
客户端socket的状态为TCP_SYN_SENT,所以直接进入tcp_rcv_state_process,处理该状态的代码为:
//客户端第二次握手处理
case TCP_SYN_SENT:
tp->rx_opt.saw_tstamp = 0;
//处理SYN_SENT状态下接收到的TCP段
queued = tcp_rcv_synsent_state_process(sk, skb, th);
if (queued >= 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:
//在SYN_SENT状态下处理接收到的段,但是不处理带外数据
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_fastopen_cookie foc = { .len = -1 };
int saved_clamp = tp->rx_opt.mss_clamp;
//解析TCP选项并保存到传输控制块中
tcp_parse_options(skb, &tp->rx_opt, 0, &foc);
if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr)
tp->rx_opt.rcv_tsecr -= tp->tsoffset;
if (th->ack) {
//处理ACK标志
/* rfc793:
* "If the state is SYN-SENT then
* first check the ACK bit
* If the ACK bit is set
* If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
* a reset (unless the RST bit is set, if so drop
* the segment and return)"
*/
if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||
after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))
goto reset_and_undo;
if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
!between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,
tcp_time_stamp)) {
NET_INC_STATS(sock_net(sk),
LINUX_MIB_PAWSACTIVEREJECTED);
goto reset_and_undo;
}
/* Now ACK is acceptable.
*
* "If the RST bit is set
* If the ACK was acceptable then signal the user "error:
* connection reset", drop the segment, enter CLOSED state,
* delete TCB, and return."
*/
if (th->rst) {
tcp_reset(sk);
goto discard;
}
/* rfc793:
* "fifth, if neither of the SYN or RST bits is set then
* drop the segment and return."
*
* See note below!
* --ANK(990513)
*/
if (!th->syn)
//在SYN_SENT状态下接收到的段必须存在SYN标志,否则说明接收到的段无效,丢弃该段
goto discard_and_undo;
/* rfc793:
* "If the SYN bit is on ...
* are acceptable then ...
* (our SYN has been ACKed), change the connection
* state to ESTABLISHED..."
*/
//从首部标志中获取显式拥塞通知的特性
tcp_ecn_rcv_synack(tp, th);
//设置与窗口相关的成员变量 --》 根据接收窗口size设置发送窗口size
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
tcp_ack(sk, skb, FLAG_SLOWPATH);
/* Ok.. it's good. Set up sequence numbers and
* move to established.
*/
tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
/* RFC1323: The window in SYN & SYN/ACK segments is
* never scaled.
*/
tp->snd_wnd = ntohs(th->window);
if (!tp->rx_opt.wscale_ok) {
tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0;
tp->window_clamp = min(tp->window_clamp, 65535U);
}
//根据是否支持时间戳选项来设置传输控制块的相关字段
if (tp->rx_opt.saw_tstamp) {
tp->rx_opt.tstamp_ok = 1;
tp->tcp_header_len =
sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
tcp_store_ts_recent(tp);
} else {
tp->tcp_header_len = sizeof(struct tcphdr);
}
if (tcp_is_sack(tp) && sysctl_tcp_fack)
tcp_enable_fack(tp);
//初始化MTU、MSS等成员变量
tcp_mtup_init(sk);
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
tcp_initialize_rcv_mss(sk);
/* Remember, tcp_poll() does not lock socket!
* Change state from SYN-SENT only after copied_seq
* is initialized. */
tp->copied_seq = tp->rcv_nxt;
smp_mb();
//设置一些标志等 --> 将sock状态设置为TCP_ESTABLISHED
tcp_finish_connect(sk, skb);
if ((tp->syn_fastopen || tp->syn_data) &&
tcp_rcv_fastopen_synack(sk, skb, &foc))
return -1;
//连接建立完成,根据情况进入延时确认模式
if (sk->sk_write_pending ||
icsk->icsk_accept_queue.rskq_defer_accept ||
icsk->icsk_ack.pingpong) {
/* Save one ACK. Data will be ready after
* several ticks, if write_pending is set.
*
* It may be deleted, but with this feature tcpdumps
* look so _wonderfully_ clever, that I was not able
* to stand against the temptation 8) --ANK
*/
inet_csk_schedule_ack(sk);
icsk->icsk_ack.lrcvtime = tcp_time_stamp;
tcp_enter_quickack_mode(sk);
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
TCP_DELACK_MAX, TCP_RTO_MAX);
discard:
tcp_drop(sk, skb);
return 0;
} else {
//不需要延时确认,立即发送ACK段
tcp_send_ack(sk);
}
return -1;
}
/* No ACK in the segment */
//收到RST段,则丢弃传输控制块
if (th->rst) {
/* rfc793:
* "If the RST bit is set
*
* Otherwise (no ACK) drop the segment and return."
*/
goto discard_and_undo;
}
/* PAWS check. */
//PAWS检测失效,也丢弃传输控制块
if (tp->rx_opt.ts_recent_stamp && tp->rx_opt.saw_tstamp &&
tcp_paws_reject(&tp->rx_opt, 0))
goto discard_and_undo;
//在SYN_SENT状态下收到了SYN段并且没有ACK,说明是两端同时打开
if (th->syn) {
/* We see SYN without ACK. It is attempt of
* simultaneous connect with crossed SYNs.
* Particularly, it can be connect to self.
*/
//设置状态为TCP_SYN_RECV
tcp_set_state(sk, TCP_SYN_RECV);
//设置时间戳相关的字段
if (tp->rx_opt.saw_tstamp) {
tp->rx_opt.tstamp_ok = 1;
tcp_store_ts_recent(tp);
tp->tcp_header_len =
sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
} else {
tp->tcp_header_len = sizeof(struct tcphdr);
}
//初始化窗口相关的成员变量
tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tp->copied_seq = tp->rcv_nxt;
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
/* RFC1323: The window in SYN & SYN/ACK segments is
* never scaled.
*/
tp->snd_wnd = ntohs(th->window);
tp->snd_wl1 = TCP_SKB_CB(skb)->seq;
tp->max_window = tp->snd_wnd;
//从首部标志中获取显式拥塞通知的特性
tcp_ecn_rcv_syn(tp, th);
//初始化MSS相关的成员变量
tcp_mtup_init(sk);
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
tcp_initialize_rcv_mss(sk);
//向对端发送SYN+ACK段,并丢弃接收到的SYN段
tcp_send_synack(sk);
#if 0
/* Note, we could accept data and URG from this segment.
* There are no obstacles to make this (except that we must
* either change tcp_recvmsg() to prevent it from returning data
* before 3WHS completes per RFC793, or employ TCP Fast Open).
*
* However, if we ignore data in ACKless segments sometimes,
* we have no reasons to accept it sometimes.
* Also, seems the code doing it in step6 of tcp_rcv_state_process
* is not flawless. So, discard packet for sanity.
* Uncomment this return to process the data.
*/
return -1;
#else
goto discard;
#endif
}
/* "fifth, if neither of the SYN or RST bits is set then
* drop the segment and return."
*/
discard_and_undo:
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
goto discard;
reset_and_undo:
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
return 1;
}
首先调用tcp_finish_connect设置sock状态为TCP_ESTABLISHED:
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
//设置sock状态为TCP_ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
if (skb) {
icsk->icsk_af_ops->sk_rx_dst_set(sk, skb);
security_inet_conn_established(sk, skb);
}
/* Make sure socket is routed, for correct metrics. */
icsk->icsk_af_ops->rebuild_header(sk);
tcp_init_metrics(sk);
tcp_init_congestion_control(sk);
/* Prevent spurious tcp_cwnd_restart() on first data
* packet.
*/
tp->lsndtime = tcp_time_stamp;
tcp_init_buffer_space(sk);
//如果启用了连接保活,则启用连接保活定时器
if (sock_flag(sk, SOCK_KEEPOPEN))
inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
//首部预测
if (!tp->rx_opt.snd_wscale)
__tcp_fast_path_on(tp, tp->snd_wnd);
else
tp->pred_flags = 0;
//如果套口不处于SOCK_DEAD状态,则唤醒等待该套接口的进程
if (!sock_flag(sk, SOCK_DEAD)) {
sk->sk_state_change(sk);
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
}
}
接着延时发送或立即发送确认ack,直接发送确认ack,调用tcp_send_ack:
//主动连接时,向服务器端发送ACK完成连接,并更新窗口
void tcp_send_ack(struct sock *sk)
{
struct sk_buff *buff;
/* If we have been reset, we may not send again. */
if (sk->sk_state == TCP_CLOSE)
return;
tcp_ca_event(sk, CA_EVENT_NON_DELAYED_ACK);
/* We are not putting this on the write queue, so
* tcp_transmit_skb() will set the ownership to this
* sock.
*/
//构造ack段
buff = alloc_skb(MAX_TCP_HEADER,
sk_gfp_mask(sk, GFP_ATOMIC | __GFP_NOWARN));
if (unlikely(!buff)) {
inet_csk_schedule_ack(sk);
inet_csk(sk)->icsk_ack.ato = TCP_ATO_MIN;
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
TCP_DELACK_MAX, TCP_RTO_MAX);
return;
}
/* Reserve space for headers and prepare control bits. */
skb_reserve(buff, MAX_TCP_HEADER);
tcp_init_nondata_skb(buff, tcp_acceptable_seq(sk), TCPHDR_ACK);
/* We do not want pure acks influencing TCP Small Queues or fq/pacing
* too much.
* SKB_TRUESIZE(max(1 .. 66, MAX_TCP_HEADER)) is unfortunately ~784
* We also avoid tcp_wfree() overhead (cache line miss accessing
* tp->tsq_flags) by using regular sock_wfree()
*/
skb_set_tcp_pure_ack(buff);
/* Send it off, this clears delayed acks for us. */
skb_mstamp_get(&buff->skb_mstamp);
//将ack段发出
tcp_transmit_skb(sk, buff, 0, (__force gfp_t)0);
}
构造报文,然后交给网络层发送。至此第二次握手完成,客户端sock状态变为TCP_ESTABLISHED,第三次握手开始。之前服务端的sock的状态为TCP_NEW_SYN_RECV,报文到达网卡:
网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv(),报文将被以下代码处理:
//网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()
int tcp_v4_rcv(struct sk_buff *skb)
{
.............
//收到握手最后一个ack后,会找到TCP_NEW_SYN_RECV状态的req,然后创建一个新的sock进入TCP_SYN_RECV状态,最终进入TCP_ESTABLISHED状态. 并放入accept队列通知select/epoll
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk);
struct sock *nsk;
sk = req->rsk_listener;
if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
sk_drops_add(sk, skb);
reqsk_put(req);
goto discard_it;
}
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_csk_reqsk_queue_drop_and_put(sk, req);
goto lookup;
}
/* We own a reference on the listener, increase it again
* as we might lose it too soon.
*/
sock_hold(sk);
refcounted = true;
//创建新的sock进入TCP_SYN_RECV state
nsk = tcp_check_req(sk, skb, req, false);
if (!nsk) {
reqsk_put(req);
goto discard_and_relse;
}
if (nsk == sk) {
reqsk_put(req);
//调用 tcp_rcv_state_process
} else if (tcp_child_process(sk, nsk, skb)) {
tcp_v4_send_reset(nsk, skb);
goto discard_and_relse;
} else {//成功后直接返回
sock_put(sk);
return 0;
}
}
...........
}
创建新sock,进入tcp_check_req():
/*
* Process an incoming packet for SYN_RECV sockets represented as a
* request_sock. Normally sk is the listener socket but for TFO it
* points to the child socket.
*
* XXX (TFO) - The current impl contains a special check for ack
* validation and inside tcp_v4_reqsk_send_ack(). Can we do better?
*
* We don't need to initialize tmp_opt.sack_ok as we don't use the results
*/
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
bool fastopen)
{
struct tcp_options_received tmp_opt;
struct sock *child;
const struct tcphdr *th = tcp_hdr(skb);
__be32 flg = tcp_flag_word(th) & (TCP_FLAG_RST|TCP_FLAG_SYN|TCP_FLAG_ACK);
bool paws_reject = false;
bool own_req;
tmp_opt.saw_tstamp = 0;
if (th->doff > (sizeof(struct tcphdr)>>2)) {
tcp_parse_options(skb, &tmp_opt, 0, NULL);
if (tmp_opt.saw_tstamp) {
tmp_opt.ts_recent = req->ts_recent;
/* We do not store true stamp, but it is not required,
* it can be estimated (approximately)
* from another data.
*/
tmp_opt.ts_recent_stamp = get_seconds() - ((TCP_TIMEOUT_INIT/HZ)<<req->num_timeout);
paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
}
}
/* Check for pure retransmitted SYN. */
if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn &&
flg == TCP_FLAG_SYN &&
!paws_reject) {
/*
* RFC793 draws (Incorrectly! It was fixed in RFC1122)
* this case on figure 6 and figure 8, but formal
* protocol description says NOTHING.
* To be more exact, it says that we should send ACK,
* because this segment (at least, if it has no data)
* is out of window.
*
* CONCLUSION: RFC793 (even with RFC1122) DOES NOT
* describe SYN-RECV state. All the description
* is wrong, we cannot believe to it and should
* rely only on common sense and implementation
* experience.
*
* Enforce "SYN-ACK" according to figure 8, figure 6
* of RFC793, fixed by RFC1122.
*
* Note that even if there is new data in the SYN packet
* they will be thrown away too.
*
* Reset timer after retransmitting SYNACK, similar to
* the idea of fast retransmit in recovery.
*/
if (!tcp_oow_rate_limited(sock_net(sk), skb,
LINUX_MIB_TCPACKSKIPPEDSYNRECV,
&tcp_rsk(req)->last_oow_ack_time) &&
!inet_rtx_syn_ack(sk, req)) {
unsigned long expires = jiffies;
expires += min(TCP_TIMEOUT_INIT << req->num_timeout,
TCP_RTO_MAX);
if (!fastopen)
mod_timer_pending(&req->rsk_timer, expires);
else
req->rsk_timer.expires = expires;
}
return NULL;
}
/* Further reproduces section "SEGMENT ARRIVES"
for state SYN-RECEIVED of RFC793.
It is broken, however, it does not work only
when SYNs are crossed.
You would think that SYN crossing is impossible here, since
we should have a SYN_SENT socket (from connect()) on our end,
but this is not true if the crossed SYNs were sent to both
ends by a malicious third party. We must defend against this,
and to do that we first verify the ACK (as per RFC793, page
36) and reset if it is invalid. Is this a true full defense?
To convince ourselves, let us consider a way in which the ACK
test can still pass in this 'malicious crossed SYNs' case.
Malicious sender sends identical SYNs (and thus identical sequence
numbers) to both A and B:
A: gets SYN, seq=7
B: gets SYN, seq=7
By our good fortune, both A and B select the same initial
send sequence number of seven :-)
A: sends SYN|ACK, seq=7, ack_seq=8
B: sends SYN|ACK, seq=7, ack_seq=8
So we are now A eating this SYN|ACK, ACK test passes. So
does sequence test, SYN is truncated, and thus we consider
it a bare ACK.
If icsk->icsk_accept_queue.rskq_defer_accept, we silently drop this
bare ACK. Otherwise, we create an established connection. Both
ends (listening sockets) accept the new incoming connection and try
to talk to each other. 8-)
Note: This case is both harmless, and rare. Possibility is about the
same as us discovering intelligent life on another plant tomorrow.
But generally, we should (RFC lies!) to accept ACK
from SYNACK both here and in tcp_rcv_state_process().
tcp_rcv_state_process() does not, hence, we do not too.
Note that the case is absolutely generic:
we cannot optimize anything here without
violating protocol. All the checks must be made
before attempt to create socket.
*/
/* RFC793 page 36: "If the connection is in any non-synchronized state ...
* and the incoming segment acknowledges something not yet
* sent (the segment carries an unacceptable ACK) ...
* a reset is sent."
*
* Invalid ACK: reset will be sent by listening socket.
* Note that the ACK validity check for a Fast Open socket is done
* elsewhere and is checked directly against the child socket rather
* than req because user data may have been sent out.
*/
if ((flg & TCP_FLAG_ACK) && !fastopen &&
(TCP_SKB_CB(skb)->ack_seq !=
tcp_rsk(req)->snt_isn + 1))
return sk;
/* Also, it would be not so bad idea to check rcv_tsecr, which
* is essentially ACK extension and too early or too late values
* should cause reset in unsynchronized states.
*/
/* RFC793: "first check sequence number". */
if (paws_reject || !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
tcp_rsk(req)->rcv_nxt, tcp_rsk(req)->rcv_nxt + req->rsk_rcv_wnd)) {
/* Out of window: send ACK and drop. */
if (!(flg & TCP_FLAG_RST) &&
!tcp_oow_rate_limited(sock_net(sk), skb,
LINUX_MIB_TCPACKSKIPPEDSYNRECV,
&tcp_rsk(req)->last_oow_ack_time))
req->rsk_ops->send_ack(sk, skb, req);
if (paws_reject)
__NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
return NULL;
}
/* In sequence, PAWS is OK. */
if (tmp_opt.saw_tstamp && !after(TCP_SKB_CB(skb)->seq, tcp_rsk(req)->rcv_nxt))
req->ts_recent = tmp_opt.rcv_tsval;
if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn) {
/* Truncate SYN, it is out of window starting
at tcp_rsk(req)->rcv_isn + 1. */
flg &= ~TCP_FLAG_SYN;
}
/* RFC793: "second check the RST bit" and
* "fourth, check the SYN bit"
*/
if (flg & (TCP_FLAG_RST|TCP_FLAG_SYN)) {
__TCP_INC_STATS(sock_net(sk), TCP_MIB_ATTEMPTFAILS);
goto embryonic_reset;
}
/* ACK sequence verified above, just make sure ACK is
* set. If ACK not set, just silently drop the packet.
*
* XXX (TFO) - if we ever allow "data after SYN", the
* following check needs to be removed.
*/
if (!(flg & TCP_FLAG_ACK))
return NULL;
/* For Fast Open no more processing is needed (sk is the
* child socket).
*/
if (fastopen)
return sk;
/* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
if (req->num_timeout < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
inet_rsk(req)->acked = 1;
__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
return NULL;
}
/* OK, ACK is valid, create big socket and
* feed this segment to it. It will repeat all
* the tests. THIS SEGMENT MUST MOVE SOCKET TO
* ESTABLISHED STATE. If it will be dropped after
* socket is created, wait for troubles.
*/
// 生成child sk, 从ehash中删除req sock ipv4_specific --> tcp_v4_syn_recv_sock
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL,
req, &own_req);
if (!child)
goto listen_overflow;
//sk->sk_rxhash = skb->hash;
sock_rps_save_rxhash(child, skb);
//更新rtt_min,srtt,rto
tcp_synack_rtt_meas(child, req);
//插入accept队列
return inet_csk_complete_hashdance(sk, child, req, own_req);
listen_overflow:
if (!sysctl_tcp_abort_on_overflow) {
inet_rsk(req)->acked = 1;
return NULL;
}
embryonic_reset:
if (!(flg & TCP_FLAG_RST)) {
/* Received a bad SYN pkt - for TFO We try not to reset
* the local connection unless it's really necessary to
* avoid becoming vulnerable to outside attack aiming at
* resetting legit local connections.
*/
req->rsk_ops->send_reset(sk, skb);
} else if (fastopen) { /* received a valid RST pkt */
reqsk_fastopen_remove(sk, req, true);
tcp_reset(sk);
}
if (!fastopen) {
inet_csk_reqsk_queue_drop(sk, req);
__NET_INC_STATS(sock_net(sk), LINUX_MIB_EMBRYONICRSTS);
}
return NULL;
}
1. 通调用链过tcp_v4_syn_recv_sock --> tcp_create_openreq_child --> inet_csk_clone_lock 生成新sock,状态设置为TCP_SYN_RECV;且tcp_v4_syn_recv_sock通过调用inet_ehash_nolisten将新sock加入ESTABLISHED状态的哈希表中
2. 通过调用inet_csk_complete_hashdance,将新sock插入accept队列至此我们得到一个代表本次连接的新sock,状态为TCP_SYN_RECV,接着调用tcp_child_process,进而调用tcp_rcv_state_process:
/*
* Queue segment on the new socket if the new socket is active,
* otherwise we just shortcircuit this and continue with
* the new socket.
*
* For the vast majority of cases child->sk_state will be TCP_SYN_RECV
* when entering. But other states are possible due to a race condition
* where after __inet_lookup_established() fails but before the listener
* locked is obtained, other packets cause the same connection to
* be created.
*/
int tcp_child_process(struct sock *parent, struct sock *child,
struct sk_buff *skb)
{
int ret = 0;
int state = child->sk_state;
tcp_segs_in(tcp_sk(child), skb);
if (!sock_owned_by_user(child)) {
ret = tcp_rcv_state_process(child, skb);
/* Wakeup parent, send SIGIO */
if (state == TCP_SYN_RECV && child->sk_state != state)
parent->sk_data_ready(parent);
} else {
/* Alas, it is possible again, because we do lookup
* in main socket hash table and lock on listening
* socket does not protect us more.
*/
__sk_add_backlog(child, skb);
}
bh_unlock_sock(child);
sock_put(child);
return ret;
}
回到函数tcp_rcv_state_process,TCP_SYN_RECV状态的套接字将由一下代码处理:
//服务端第三次握手处理
case TCP_SYN_RECV:
if (!acceptable)
return 1;
if (!tp->srtt_us)
tcp_synack_rtt_meas(sk, req);
/* Once we leave TCP_SYN_RECV, we no longer need req
* so release it.
*/
if (req) {
inet_csk(sk)->icsk_retransmits = 0;
reqsk_fastopen_remove(sk, req, false);
} else {
/* Make sure socket is routed, for correct metrics. */
//建立路由,初始化拥塞控制模块
icsk->icsk_af_ops->rebuild_header(sk);
tcp_init_congestion_control(sk);
tcp_mtup_init(sk);
tp->copied_seq = tp->rcv_nxt;
tcp_init_buffer_space(sk);
}
smp_mb();
//正常的第三次握手,设置连接状态为TCP_ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);
/* Note, that this wakeup is only for marginal crossed SYN case.
* Passively open sockets are not waked up, because
* sk->sk_sleep == NULL and sk->sk_socket == NULL.
*/
//状态已经正常,唤醒那些等待的线程
if (sk->sk_socket)
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
if (tp->rx_opt.tstamp_ok)
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
if (req) {
/* Re-arm the timer because data may have been sent out.
* This is similar to the regular data transmission case
* when new data has just been ack'ed.
*
* (TFO) - we could try to be more aggressive and
* retransmitting any data sooner based on when they
* are sent out.
*/
tcp_rearm_rto(sk);
} else
tcp_init_metrics(sk);
if (!inet_csk(sk)->icsk_ca_ops->cong_control)
tcp_update_pacing_rate(sk);
/* Prevent spurious tcp_cwnd_restart() on first data packet */
//更新最近一次发送数据包的时间
tp->lsndtime = tcp_time_stamp;
tcp_initialize_rcv_mss(sk);
//计算有关TCP首部预测的标志
tcp_fast_path_on(tp);
break;
server端处理第一次握手并发出第二次握手:
client端处理第二次握手并发出第三次握手:
serve端处理第三次握手,TCP连接建立:
4. send在TCP/IP协议栈中的执行路径
1. 应用层
- 网络应用调用Socket API socket创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 sock_create() 方法。该方法返回被创建好了的那个 socket 的 描述符。
- 对于 TCP socket ,应用调用 connect()函数,使客户端和服务器端通过该 socket 建立一个连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。
- 建立连接之后然后可以调用send函数发出一个 message 给接收端。sock_sendmsg 被调用,调用相应协议的发送函数。
2. 传输层
- 先调用tcp_sendmsg 函数,把用户层的数据填充到skb中。在tcp_sendmsg_locked中,将数据整理成发送队列,每个队列中的元素就是skb。
- 计算校验和和顺序号,保证数据的可靠传输。
- 数据创建之后调用tcp_push()来发送,tcp_push函数调用tcp_write_xmit()函数,它又将调用发送函数tcp_transmit_skb,所有的SKB都经过该函数进行发送。最后进入到ip_queue_xmit到网络层。
3. 网络层
- ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有则使用ip_route_output()选择一个路由。
- 填充IP包的各个字段,比如版本、包头长度、TOS等。ip_fragment 函数进行分片,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。
- 使用 ip_finish_ouput2 设置链路层报文头。如果链路层报头缓存存在,拷贝到skb里。如果没有,就调用neigh_resolve_output,使用 ARP 获取。
4.数据链路层
- 数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。从dev_queue_xmit函数开始,位于net/core/dev.c文件中。
gdb调试如下:
5.recv在TCP/IP协议栈中的执行路径
1. 应用层
- 应用调用 read 或者 recvfrom 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recgmsg 函数。
- 对于 INET 类型的 socket,/net/ipv4/af_inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。
- TCP 调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到 user buffer。
2. 传输层
- tcp_v4_rcv函数为TCP的总入口,数据包从IP层传递上来,进入该函数,其中handler即为IP层向TCP传递数据包的回调函数,设置为tcp_v4_rcv,tcp_v4_rcv函数只要做以下几个工作:(1) 设置TCP_CB (2) 查找控制块 (3)根据控制块状态做不同处理,包括TCP_TIME_WAIT状态处理,TCP_NEW_SYN_RECV状态处理,TCP_LISTEN状态处理 (4) 接收TCP段
- 调用__sys_recvfrom,整个函数的调用路径与send非常类似。整个函数实际调用的是sock->ops->recvmsg(sock,msg,msg_data_left(msg),flags),同样,根据tcp_port结构的初始化,调用的其实是tcp_rcvmsg
- 维护三个队列,prequeue、backlog、receive_queue,分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter
3. 网络层
- IP层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment,然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
- ip_rcv_finish 函数会调用ip_route_input函数,进入路由处理环节。会调用 ip_route_input 来更新路由,然后查找 route,决定该会被发到本机还是会被转发还是丢弃。
- 如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用 ip_local_deliver 函数。如果需要转发 (forward),则进入转发流程,调用 dst_input 函数。
4. 链路层
- 机器的物理网络适配器接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
- 终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。进入软中断处理流程,调用 net_rx_action 函数。包从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。
- netif_receive_skb根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。
gdb调试如下:
6. 路由表的结构和初始化过程
在linux的路由系统主要保存了三种与路由相关的数据,第一种是在物理上和本机相连接的主机地址信息表,第二种是保存了在网络访问中判断一个网络地址应该走什么路由的数据表;第三种是最新使用过的查询路由地址的缓存地址数据表。
1.neighbour结构 neighbour_table{ }是一个包含和本机所连接的所有邻元素的信息的数据结构。该结构中有个元素是neighbour结构的数组,数组的每一个元素都是一个对应于邻机的neighbour结构,系统中由于协议的不同,会有不同的判断邻居的方式,每种都有neighbour_table{}类型的实例,这些实例是通过neighbour_table{}中的指针next串联起来的。在neighbour结构中,包含有与该邻居相连的网络接口设备net_device的指针,网络接口的硬件地址,邻居的硬件地址,包含有neigh_ops{}指针,这些函数指针是直接用来连接传输数据的,包含有queue_xmit(struct * sk_buff)函数入口地址,这个函数可能会调用硬件驱动程序的发送函数。
2.FIB结构 在FIB中保存的是最重要的路由规则,通过对FIB数据的查找和换算,一定能够获得路由一个地址的方法。系统中路由一般采取的手段是:先到路由缓存中查找表项,如果能够找到,直接对应的一项作为路由的规则;如果不能找到,那么就到FIB中根据规则换算传算出来,并且增加一项新的,在路由缓存中将项目添加进去。
3.route结构(即路由缓存中的结构)
struct rtentry
{
unsigned long int rt_pad1;
struct sockaddr rt_dst; /* Target address. */
struct sockaddr rt_gateway; /* Gateway addr (RTF_GATEWAY). */
struct sockaddr rt_genmask; /* Target network mask (IP). */
unsigned short int rt_flags;
short int rt_pad2;
unsigned long int rt_pad3;
unsigned char rt_tos;
unsigned char rt_class;
#if __WORDSIZE == 64
short int rt_pad4[3];
#else
short int rt_pad4;
#endif
short int rt_metric; /* +1 for binary compatibility! */
char *rt_dev; /* Forcing the device at add. */
unsigned long int rt_mtu; /* Per route MTU/Window. */
unsigned long int rt_window; /* Window clamping. */
unsigned short int rt_irtt; /* Initial RTT. */
};
struct fib_table :
struct fib_table {
struct hlist_node tb_hlist; // 哈希表中的节点,用于连接具有相同哈希值的路由表
u32 tb_id; // 路由表的唯一标识符,通常是一个整数
int tb_num_default; // 默认路由表的数量
struct rcu_head rcu; // RCU(Read-Copy Update)头部,用于进行无锁访问
unsigned long *tb_data; // 路由表的主体,可能是一个指向前缀树等数据结构的指针
unsigned long __data[0]; // 变长数组,用于存储具体的路由表数据,长度为0表示这是一个灵活数组成员
};
ip_fib_init
函数是 Linux 内核中用于初始化 IPv4 路由表的函数。该函数的定义在 net/ipv4/fib_frontend.c
文件中。
/**
* ip_fib_init - 初始化 IPv4 FIB 子系统
*
* 在 IPv4 栈初始化期间调用此函数,设置 FIB(Forwarding Information Base)子系统的必要数据结构。
*/
void __init ip_fib_init(void)
{
// 初始化 FIB trie,用于存储和查找 IPv4 路由表信息
fib_trie_init();
// 注册 IPv4 FIB 子系统的 pernet 操作
register_pernet_subsys(&fib_net_ops);
// 注册网络设备通知回调,用于处理网络设备事件
register_netdevice_notifier(&fib_netdev_notifier);
// 注册 IPv4 地址通知回调,用于处理 IPv4 地址事件
register_inetaddr_notifier(&fib_inetaddr_notifier);
// 在 RTNetLink 中注册 IPv4 路由表的消息处理函数
rtnl_register(PF_INET, RTM_NEWROUTE, inet_rtm_newroute, NULL, 0);
rtnl_register(PF_INET, RTM_DELROUTE, inet_rtm_delroute, NULL, 0);
rtnl_register(PF_INET, RTM_GETROUTE, NULL, inet_dump_fib, 0);
}
一下这些函数与路由表初始化有关位于route.c文件中:
1. ip_route_output
函数: 该函数用于查找目标IP地址对应的路由表项。如果找到,返回指向目标struct rtable
的指针;如果没有找到,将会创建一个新的路由表项。在创建新表项时,通常会调用ip_route_input_noref
和ip_route_input_common
等函数。
2. ip_route_input_noref
函数: 该函数用于在路由表中查找目标IP地址对应的路由表项。如果找到,返回指向目标struct rtable
的指针;如果没有找到,将会创建一个新的路由表项。
3.ip_route_input_common
函数: 该函数用于初始化并插入新创建的路由表项。在ip_route_input_common
中,会调用ip_route_input_slow
函数,该函数实现了路由表项的真正创建和初始化。
4.ip_route_input_slow
函数: 该函数用于创建和初始化新的路由表项,然后插入到路由表中。ip_route_input_slow
函数内部会调用fib_validate_source
函数验证源地址,然后创建和初始化新的路由表项。
7. 通过目的IP查询路由表的到下一跳的IP地址的过程
路由策略处理相关的内核代码集中在的net/ipv4/fib_rules.c和net/core/fib_rules.c两个文件中。
通过目的IP查询路由表的到下一跳的IP地址的过程, fib_lookup为起点。
/**
* fib_lookup - 在 FIB 表中查找给定 IPv4 流的路由信息
* @net: 网络命名空间
* @flp: IPv4 流的流标识符结构体(flowi4)
* @res: 存储查找结果的结构体(fib_result)
* @flags: 查找标志,例如 FIB_LOOKUP_NOREF 表示不增加引用计数
*
* 该函数用于在给定的网络命名空间中,通过 IPv4 流的标识符(flowi4)在 FIB 表中查找路由信息。
* 查找的结果将被存储在提供的 fib_result 结构体中。如果查找成功,返回 0;否则返回错误码。
*
* @return: 查找成功返回 0,否则返回错误码(负数)。
*/
static inline int fib_lookup(struct net *net, const struct flowi4 *flp,
struct fib_result *res, unsigned int flags)
{
struct fib_table *tb;
int err = -ENETUNREACH;
// 读取锁定RCU,开始临界区
rcu_read_lock();
// 获取主 FIB 表
tb = fib_get_table(net, RT_TABLE_MAIN);
// 在 FIB 表中进行查找,结果存储在提供的 fib_result 结构体中
if (tb)
err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
// 处理查找结果为 -EAGAIN 的情况,将其转换为 -ENETUNREACH
if (err == -EAGAIN)
err = -ENETUNREACH;
// 解锁RCU,结束临界区
rcu_read_unlock();
// 返回查找结果
return err;
}
调用过fib_lookup后,函数会根据查找的结构进行不同的处理。一般情况是转发或者本地,这两种的情况都会先分配一个新的路由缓存结点,填充适当的值然后插入到缓存中;两者的不同主要在于,设置dst.input函数分别为ip_forward或ip_local_deliver,转发的情况还要绑定关于下一跳信息的neighbour(这个结构主要用来得到网段上邻居的物理地址)。除了转发或本地还有可能是其它情况,比如有错误,没查到,丢弃,NAT等。
fib_lookup函数是路由策略数据库的查询接口,它首先查找策略表,找到一条匹配的策略,然后再执行该策略所对应的动作,动作一般来说就是要查找对应的一张路由表,所以接下来会调用fn_hash_lookup函数进行处理。
gdb跟踪到fib_lookup函数时的调用栈如下:
8. ARP缓存的数据结构及初始化过程,包括ARP缓存的初始化
ARP(Address Resolution Protocol)缓存的数据结构主要涉及到 struct neighbour
结构和相关函数。ARP缓存用于存储IP地址到MAC地址的映射关系,以提高数据包的转发效率。
struct neighbour:
struct neighbour {
struct neighbour __rcu *next; // 指向下一个邻居项的指针,通过 RCU 机制进行管理
struct neigh_table *tbl; // 指向邻居表的指针,表示该邻居项所属的邻居表
struct neigh_parms *parms; // 指向邻居参数的指针,表示邻居项的参数
unsigned long confirmed; // 上一次确认邻居项的时间戳
unsigned long updated; // 上一次更新邻居项的时间戳
rwlock_t lock; // 读写自旋锁,用于对邻居项的并发访问进行保护
refcount_t refcnt; // 引用计数,用于跟踪邻居项的引用情况
unsigned int arp_queue_len_bytes; // ARP 队列长度的字节数
struct sk_buff_head arp_queue; // 用于存储 ARP 队列的 sk_buff_head 结构
struct timer_list timer; // 定时器,用于邻居项的定时操作,例如垃圾回收
unsigned long used; // 上一次使用邻居项的时间戳
atomic_t probes; // 邻居项的探测次数
__u8 flags; // 一些标志位,用于表示邻居项的状态
__u8 nud_state; // 邻居项的 NUD(Neighbor Unreachability Detection)状态
__u8 type; // 邻居项的类型
__u8 dead; // 用于标记邻居项是否已经过期
u8 protocol; // 表示邻居项所使用的网络协议
seqlock_t ha_lock; // 用于对硬件地址进行加锁的序列化锁
unsigned char ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))] __aligned(8); // 存储硬件地址(MAC 地址)的数组
struct hh_cache hh; // 用于存储硬件地址缓存的 hh_cache 结构
int (*output)(struct neighbour *, struct sk_buff *); // 输出操作的函数指针
const struct neigh_ops *ops; // 指向邻居操作的指针,包含了处理邻居项的函数
struct list_head gc_list; // 用于连接到垃圾回收列表的链表节点
struct rcu_head rcu; // RCU 头,用于进行 RCU 回收
struct net_device *dev; // 指向网络设备的指针,表示邻居项所属的网络设备
u8 primary_key[0]; // 一个零长度的数组,用于存储主键
} __randomize_layout; // 随机化布局,增强安全性
arp_init函数进行ARP缓存的数据结构及初始化过程
void __init arp_init(void)
{
// 初始化 ARP 表
neigh_table_init(NEIGH_ARP_TABLE, &arp_tbl);
// 向网络设备协议栈中添加 ARP 数据包处理函数
dev_add_pack(&arp_packet_type);
// 初始化 ARP 过程中的 proc 文件系统相关内容
arp_proc_init();
#ifdef CONFIG_SYSCTL
// 注册 sysctl 参数,允许用户配置 ARP 相关参数
neigh_sysctl_register(NULL, &arp_tbl.parms, NULL);
#endif
// 注册网络设备事件通知,用于监测网络设备状态的变化
register_netdevice_notifier(&arp_netdev_notifier);
}
neighbour
的初始化过程主要涉及到 neigh_table_init
函数。
void neigh_table_init(int index, struct neigh_table *tbl)
{
unsigned long now = jiffies; // 获取当前的时间戳
// 初始化neighbor table参数链表和垃圾回收列表
INIT_LIST_HEAD(&tbl->parms_list);
INIT_LIST_HEAD(&tbl->gc_list);
// 将neighbor table参数(parms)添加到参数链表中
list_add(&tbl->parms.list, &tbl->parms_list);
// 设置neighbor table参数的网络命名空间、引用计数等信息
write_pnet(&tbl->parms.net, &init_net);
refcount_set(&tbl->parms.refcnt, 1);
// 随机生成邻居可达性时间,用于设置neighbor table参数的可达性时间
tbl->parms.reachable_time =
neigh_rand_reach_time(NEIGH_VAR(&tbl->parms, BASE_REACHABLE_TIME));
// 为neighbor table分配并初始化统计数据结构
tbl->stats = alloc_percpu(struct neigh_statistics);
if (!tbl->stats)
panic("cannot create neighbour cache statistics");
#ifdef CONFIG_PROC_FS
// 在/proc 文件系统中创建neighbor table的统计信息目录
if (!proc_create_seq_data(tbl->id, 0, init_net.proc_net_stat,
&neigh_stat_seq_ops, tbl))
panic("cannot create neighbour proc dir entry");
#endif
// 初始化 RCU 指针,分配邻居哈希表
RCU_INIT_POINTER(tbl->nht, neigh_hash_alloc(3));
// 分配邻居哈希表桶
unsigned long phsize = (PNEIGH_HASHMASK + 1) * sizeof(struct pneigh_entry *);
tbl->phash_buckets = kzalloc(phsize, GFP_KERNEL);
if (!tbl->nht || !tbl->phash_buckets)
panic("cannot allocate neighbour cache hashes");
// 如果未指定neighbor table项的大小,则计算并设置默认大小
if (!tbl->entry_size)
tbl->entry_size = ALIGN(offsetof(struct neighbour, primary_key) + tbl->key_len, NEIGH_PRIV_ALIGN);
else
WARN_ON(tbl->entry_size % NEIGH_PRIV_ALIGN);
// 初始化读写自旋锁、延迟工作队列和定时器
rwlock_init(&tbl->lock);
INIT_DEFERRABLE_WORK(&tbl->gc_work, neigh_periodic_work);
queue_delayed_work(system_power_efficient_wq, &tbl->gc_work, tbl->parms.reachable_time);
timer_setup(&tbl->proxy_timer, neigh_proxy_process, 0);
// 初始化代理队列和相关数据结构
skb_queue_head_init_class(&tbl->proxy_queue, &neigh_table_proxy_queue_class);
// 设置neighbor table的最后刷新时间和最后随机时间
tbl->last_flush = now;
tbl->last_rand = now + tbl->parms.reachable_time * 20;
// 将neighbor table添加到neighbor table数组中
neigh_tables[index] = tbl;
}
EXPORT_SYMBOL(neigh_table_init); // 将该函数导出,使其能够在内核模块中使用
9. 如何将IP地址解析出对应的MAC地址
在net/core/neighbour.c中,neigh_lookup
用于查找给定 IP 地址的邻居项,如果找到,则返回对应的 struct neighbour
结构。
neigh_lookup:
/**
* neigh_lookup - 在邻居表中查找给定 pkey 和 dev 对应的邻居项
* @tbl: 邻居表指针
* @pkey: 目标 IP 地址的指针
* @dev: 目标网络设备指针
*
* 此函数在指定的邻居表中查找与目标 IP 地址(pkey)和网络设备(dev)相匹配的邻居项。
* 如果找到匹配项,则会增加邻居项的引用计数,以确保在使用邻居项时不会被释放。
*
* 返回值:如果找到匹配的邻居项,则返回该邻居项的指针;否则返回 NULL。
*/
struct neighbour *neigh_lookup(struct neigh_table *tbl, const void *pkey,
struct net_device *dev)
{
struct neighbour *n;
// 增加邻居表的查找统计
NEIGH_CACHE_STAT_INC(tbl, lookups);
// 读取锁定RCU,开始临界区
rcu_read_lock_bh();
// 调用内部函数进行邻居查找(不增加引用计数)
n = __neigh_lookup_noref(tbl, pkey, dev);
// 如果找到邻居项,增加引用计数以防止在使用过程中被释放
if (n) {
if (!refcount_inc_not_zero(&n->refcnt))
n = NULL; // 如果引用计数增加失败,表示邻居项已被释放,返回 NULL
// 增加邻居表的命中统计
NEIGH_CACHE_STAT_INC(tbl, hits);
}
// 解锁RCU,结束临界区
rcu_read_unlock_bh();
// 返回找到的邻居项指针(或 NULL)
return n;
}
// 将该函数导出,使其能够在内核模块中使用
EXPORT_SYMBOL(neigh_lookup);
它调用函数__neigh_lookup_noref(tbl, pkey, dev);
static inline struct neighbour *__neigh_lookup_noref(struct neigh_table *tbl,
const void *pkey,
struct net_device *dev)
{
return ___neigh_lookup_noref(tbl, tbl->key_eq, tbl->hash, pkey, dev);
}
而它实际调用neighbour *___neigh_lookup_noref函数:
/**
* ___neigh_lookup_noref - 在邻居表中查找不增加引用计数的邻居项
* @tbl: 邻居表指针
* @key_eq: 比较邻居项关键字是否相等的函数指针
* @hash: 计算哈希值的函数指针
* @pkey: 目标项的关键字指针
* @dev: 目标网络设备指针
*
* 在指定的邻居表中通过关键字查找邻居项,但不增加邻居项的引用计数。
* 查找过程使用哈希表进行加速,通过哈希值在链表上查找匹配项。
*
* 返回值:如果找到匹配的邻居项,则返回该邻居项的指针;否则返回 NULL。
*/
static inline struct neighbour *___neigh_lookup_noref(
struct neigh_table *tbl,
bool (*key_eq)(const struct neighbour *n, const void *pkey),
__u32 (*hash)(const void *pkey,
const struct net_device *dev,
__u32 *hash_rnd),
const void *pkey,
struct net_device *dev)
{
// 通过RCU机制获取邻居哈希表
struct neigh_hash_table *nht = rcu_dereference_bh(tbl->nht);
struct neighbour *n;
u32 hash_val;
// 计算关键字的哈希值,并根据哈希值定位到哈希表中的桶
hash_val = hash(pkey, dev, nht->hash_rnd) >> (32 - nht->hash_shift);
// 在哈希表桶上遍历链表,查找匹配项
for (n = rcu_dereference_bh(nht->hash_buckets[hash_val]);
n != NULL;
n = rcu_dereference_bh(n->next)) {
// 检查设备和关键字是否匹配
if (n->dev == dev && key_eq(n, pkey))
return n; // 如果匹配,返回找到的邻居项指针
}
return NULL; // 没有找到匹配项,返回 NULL
}
在最终找到邻居项之后,可以从邻居项的数据结构中获取对应的 MAC 地址。数据结构是 struct neighbour
,其中包含了目标 IP 地址和 MAC 地址的映射关系。
我们通过gdb查看堆栈:
10. 跟踪TCP send过程中的路由查询和ARP解析的最底层实现
ip_route_output_key
:该函数用于进行输出路由查询。给定目标 IP 地址、输出设备和一些其他信息,它会返回一个 struct rtable
结构体,包含了目标的路由信息。
static inline struct rtable *__ip_route_output_key(struct net *net,
struct flowi4 *flp)
{
return ip_route_output_key_hash(net, flp, NULL);
}
实际调用 ip_route_output_key_hash函数:
/**
* ip_route_output_key_hash - 基于给定 IPv4 流标识符查找路由表项
* @net: 网络命名空间
* @fl4: IPv4 流标识符结构体(flowi4)
* @skb: 与路由查询相关的数据包
*
* 该函数用于基于给定的 IPv4 流标识符(flowi4)在指定网络命名空间的路由表中查找路由表项。
* 这个函数是一个输出路由查询的高层次接口,它调用底层的 ip_route_output_key_hash_rcu 函数执行实际的路由查找操作。
* 返回找到的路由表项(rtable),如果未找到则返回 NULL。
*
* @return: 返回找到的路由表项(rtable),如果未找到则返回 NULL。
*/
struct rtable *ip_route_output_key_hash(struct net *net, struct flowi4 *fl4,
const struct sk_buff *skb)
{
__u8 tos = RT_FL_TOS(fl4);
struct fib_result res = {
.type = RTN_UNSPEC,
.fi = NULL,
.table = NULL,
.tclassid = 0,
};
struct rtable *rth;
// 设置流标识符的输入接口为环回接口,TOS 字段和作用域字段
fl4->flowi4_iif = LOOPBACK_IFINDEX;
fl4->flowi4_tos = tos & IPTOS_RT_MASK;
fl4->flowi4_scope = ((tos & RTO_ONLINK) ?
RT_SCOPE_LINK : RT_SCOPE_UNIVERSE);
// 读取锁定RCU,开始临界区
rcu_read_lock();
// 调用底层的路由查询函数 ip_route_output_key_hash_rcu 进行实际的路由查找
rth = ip_route_output_key_hash_rcu(net, fl4, &res, skb);
// 解锁RCU,结束临界区
rcu_read_unlock();
// 返回找到的路由表项(rtable),如果未找到则返回 NULL
return rth;
}
EXPORT_SYMBOL_GPL(ip_route_output_key_hash);
fib_lookup
:fib_lookup
函数用于在 FIB 表中查找给定 IPv4 流(struct flowi4
)的路由信息。它会调用底层的 fib_table_lookup
函数执行实际的路由查找操作。
/**
* fib_lookup - 在 FIB 表中查找给定 IPv4 流的路由信息
* @net: 网络命名空间
* @flp: IPv4 流的流标识符结构体(flowi4)
* @res: 存储查找结果的结构体(fib_result)
* @flags: 查找标志,例如 FIB_LOOKUP_NOREF 表示不增加引用计数
*
* 该函数用于在给定的网络命名空间中,通过 IPv4 流的标识符(flowi4)在 FIB 表中查找路由信息。
* 查找的结果将被存储在提供的 fib_result 结构体中。如果查找成功,返回 0;否则返回错误码。
*
* @return: 查找成功返回 0,否则返回错误码(负数)。
*/
static inline int fib_lookup(struct net *net, const struct flowi4 *flp,
struct fib_result *res, unsigned int flags)
{
struct fib_table *tb;
int err = -ENETUNREACH;
// 读取锁定RCU,开始临界区
rcu_read_lock();
// 获取主 FIB 表
tb = fib_get_table(net, RT_TABLE_MAIN);
// 在 FIB 表中进行查找,结果存储在提供的 fib_result 结构体中
if (tb)
err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
// 处理查找结果为 -EAGAIN 的情况,将其转换为 -ENETUNREACH
if (err == -EAGAIN)
err = -ENETUNREACH;
// 解锁RCU,结束临界区
rcu_read_unlock();
// 返回查找结果
return err;
}
fib_table_lookup
:该函数是 fib_lookup
的底层实现,用于在指定的 FIB 表中进行路由查询。它负责查找匹配给定流标识符(struct flowi4
)的路由信息,并将结果存储在 struct fib_result
中。
fib_get_table
:用于获取给定网络命名空间下的 FIB 表。在路由查询开始时,通常会先调用这个函数获取主 FIB 表。
/**
* fib_get_table - 获取指定网络命名空间中的 FIB 表
* @net: 网络命名空间
* @id: FIB 表的标识符
*
* 该函数用于在指定的网络命名空间中通过 FIB 表的标识符获取对应的 FIB 表。
* 如果找到匹配的 FIB 表,则返回该 FIB 表的指针;否则返回 NULL。
*
* @return: 如果找到匹配的 FIB 表,则返回该 FIB 表的指针;否则返回 NULL。
*/
struct fib_table *fib_get_table(struct net *net, u32 id)
{
struct fib_table *tb;
struct hlist_head *head;
unsigned int h;
// 如果 FIB 表标识符为 0,则使用默认的主 FIB 表标识符
if (id == 0)
id = RT_TABLE_MAIN;
// 计算 FIB 表标识符的哈希值
h = id & (FIB_TABLE_HASHSZ - 1);
// 获取哈希表头
head = &net->ipv4.fib_table_hash[h];
// 在哈希桶链表上遍历,查找匹配的 FIB 表
hlist_for_each_entry_rcu(tb, head, tb_hlist,
lockdep_rtnl_is_held()) {
if (tb->tb_id == id)
return tb; // 找到匹配的 FIB 表,返回指针
}
// 没有找到匹配的 FIB 表,返回 NULL
return NULL;
}
ARP 解析的最底层实现在 net/ipv4/arp.c
文件中。
arp_create
函数用于创建 ARP 请求帧。
/**
* arp_create - 创建 ARP 数据包
* @type: ARP 操作类型(ARPOP_REQUEST 或 ARPOP_REPLY)
* @ptype: ARP 协议类型(ETH_P_IP)
* @dest_ip: 目标 IP 地址
* @dev: 目标网络设备
* @src_ip: 源 IP 地址
* @dest_hw: 目标硬件地址
* @src_hw: 源硬件地址
* @target_hw: 目标硬件地址(仅在 ARP 操作类型为 ARPOP_REPLY 时有效)
*
* 该函数用于创建 ARP 数据包(skb)。根据传入的参数,设置 ARP 头和数据部分,并填充源、目标 IP 和硬件地址等信息。
* 如果创建成功,则返回指向创建的 sk_buff 结构的指针;否则返回 NULL。
*
* @return: 如果创建成功,则返回指向创建的 sk_buff 结构的指针;否则返回 NULL。
*/
struct sk_buff *arp_create(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 sk_buff *skb;
struct arphdr *arp;
unsigned char *arp_ptr;
int hlen = LL_RESERVED_SPACE(dev);
int tlen = dev->needed_tailroom;
/*
* 分配一个缓冲区
*/
skb = alloc_skb(arp_hdr_len(dev) + hlen + tlen, GFP_ATOMIC);
if (!skb)
return NULL;
skb_reserve(skb, hlen);
skb_reset_network_header(skb);
arp = skb_put(skb, arp_hdr_len(dev));
skb->dev = dev;
skb->protocol = htons(ETH_P_ARP);
if (!src_hw)
src_hw = dev->dev_addr;
if (!dest_hw)
dest_hw = dev->broadcast;
/*
* 填充 ARP 帧的设备头部
*/
if (dev_hard_header(skb, dev, ptype, dest_hw, src_hw, skb->len) < 0)
goto out;
/*
* 填充 ARP 协议部分。
*
* ARP 硬件类型应与设备类型匹配,除了 FDDI,
* 它(根据 RFC 1390)应始终等于 1(Ethernet)。
*/
switch (dev->type) {
default:
arp->ar_hrd = htons(dev->type);
arp->ar_pro = htons(ETH_P_IP);
break;
#if IS_ENABLED(CONFIG_AX25)
case ARPHRD_AX25:
arp->ar_hrd = htons(ARPHRD_AX25);
arp->ar_pro = htons(AX25_P_IP);
break;
#if IS_ENABLED(CONFIG_NETROM)
case ARPHRD_NETROM:
arp->ar_hrd = htons(ARPHRD_NETROM);
arp->ar_pro = htons(AX25_P_IP);
break;
#endif
#endif
#if IS_ENABLED(CONFIG_FDDI)
case ARPHRD_FDDI:
arp->ar_hrd = htons(ARPHRD_ETHER);
arp->ar_pro = htons(ETH_P_IP);
break;
#endif
}
arp->ar_hln = dev->addr_len;
arp->ar_pln = 4;
arp->ar_op = htons(type);
arp_ptr = (unsigned char *)(arp + 1);
memcpy(arp_ptr, src_hw, dev->addr_len);
arp_ptr += dev->addr_len;
memcpy(arp_ptr, &src_ip, 4);
arp_ptr += 4;
switch (dev->type) {
#if IS_ENABLED(CONFIG_FIREWIRE_NET)
case ARPHRD_IEEE1394:
break;
#endif
default:
if (target_hw)
memcpy(arp_ptr, target_hw, dev->addr_len);
else
memset(arp_ptr, 0, dev->addr_len);
arp_ptr += dev->addr_len;
}
memcpy(arp_ptr, &dest_ip, 4);
return skb;
out:
kfree_skb(skb);
return NULL;
}
EXPORT_SYMBOL(arp_create);
arp_send
函数用于发送 ARP 请求帧。 该函数用于发送 ARP 数据包。根据传入的参数,创建 ARP 数据包,并调用 arp_send_dst 函数发送数据包。该函数是 arp_send_dst 函数的一个简化版本,不指定目标网络设备。如果发送成功,则无返回值。
/**
* arp_send - 发送 ARP 数据包
* @type: ARP 操作类型(ARPOP_REQUEST 或 ARPOP_REPLY)
* @ptype: ARP 协议类型(ETH_P_IP)
* @dest_ip: 目标 IP 地址
* @dev: 目标网络设备
* @src_ip: 源 IP 地址
* @dest_hw: 目标硬件地址
* @src_hw: 源硬件地址
* @target_hw: 目标硬件地址(仅在 ARP 操作类型为 ARPOP_REPLY 时有效)
*
* 该函数用于发送 ARP 数据包。根据传入的参数,创建 ARP 数据包,并调用 arp_send_dst 函数发送数据包。
* 该函数是 arp_send_dst 函数的一个简化版本,不指定目标网络设备。如果发送成功,则无返回值。
*
* @return: 无返回值。
*/
void arp_send(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)
{
arp_send_dst(type, ptype, dest_ip, dev, src_ip, dest_hw, src_hw,
target_hw, NULL);
}
EXPORT_SYMBOL(arp_send);
arp_send_dst 函数:该函数用于向指定目标发送 ARP 数据包。根据传入的参数,创建 ARP 数据包,并通过 arp_xmit 函数发送数据包。
/**
* arp_send_dst - 向指定目标发送 ARP 数据包
* @type: ARP 操作类型(ARPOP_REQUEST 或 ARPOP_REPLY)
* @ptype: ARP 协议类型(ETH_P_IP)
* @dest_ip: 目标 IP 地址
* @dev: 目标网络设备
* @src_ip: 源 IP 地址
* @dest_hw: 目标硬件地址
* @src_hw: 源硬件地址
* @target_hw: 目标硬件地址(仅在 ARP 操作类型为 ARPOP_REPLY 时有效)
* @dst: 目标网络设备的目的项(dst_entry)
*
* 该函数用于向指定目标发送 ARP 数据包。根据传入的参数,创建 ARP 数据包,并通过 arp_xmit 函数发送数据包。
* 如果目标网络设备设置了 IFF_NOARP 标志,则不执行 ARP 操作。
*
* @return: 无返回值。
*/
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;
/* 如果目标网络设备设置了 IFF_NOARP 标志,不执行 ARP 操作 */
if (dev->flags & IFF_NOARP)
return;
// 创建 ARP 数据包
skb = arp_create(type, ptype, dest_ip, dev, src_ip,
dest_hw, src_hw, target_hw);
if (!skb)
return;
// 设置数据包的目标项
skb_dst_set(skb, dst_clone(dst));
// 调用 arp_xmit 函数发送数据包
arp_xmit(skb);
}
arp_rcv函数处理接收到的 ARP 数据包 :
/**
* arp_rcv - 处理接收到的 ARP 数据包
* @skb: 接收到的数据包(sk_buff 结构)
* @dev: 接收到数据包的网络设备
* @pt: packet_type 结构,表示数据包类型
* @orig_dev: 原始接收到数据包的网络设备
*
* 该函数用于处理接收到的 ARP 数据包。根据数据包的内容和网络设备的状态,执行相应的处理逻辑。
*
* @return: 如果成功处理数据包,则返回 NET_RX_SUCCESS;否则返回 NET_RX_DROP。
*/
static int arp_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
const struct arphdr *arp;
/* 如果网络设备设置了 IFF_NOARP 标志,或者数据包类型为 PACKET_OTHERHOST 或 PACKET_LOOPBACK,则忽略数据包 */
if (dev->flags & IFF_NOARP ||
skb->pkt_type == PACKET_OTHERHOST ||
skb->pkt_type == PACKET_LOOPBACK)
goto consumeskb;
// 检查并共享数据包
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
goto out_of_mem;
/* 获取 ARP 头部信息,并检查硬件地址长度和协议地址长度是否符合预期 */
if (!pskb_may_pull(skb, arp_hdr_len(dev)))
goto freeskb;
arp = arp_hdr(skb);
if (arp->ar_hln != dev->addr_len || arp->ar_pln != 4)
goto freeskb;
// 初始化网络邻居信息
memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));
// 调用 ARP 处理函数
return NF_HOOK(NFPROTO_ARP, NF_ARP_IN,
dev_net(dev), NULL, skb, dev, NULL,
arp_process);
consumeskb:
// 释放数据包
consume_skb(skb);
return NET_RX_SUCCESS;
freeskb:
// 释放数据包
kfree_skb(skb);
out_of_mem:
// 内存不足,返回丢弃数据包
return NET_RX_DROP;
}
gdb调试查看堆栈如下:
三、总结
通过这次对于TCP/IP协议栈源代码分析,我深入了解了TCP/IP协议栈的底层原理,学会了利用GDB调试工具来调试Linux内核代码的方法,以前只是知道这些协议的原理,但对于底层代码如何实现一知半解,通过这次使用GDB调试网络内核代码,收获颇丰。
在此感谢孟老师的教导,在老师的课堂上我学到了很多有关网络程序设计的知识,老师的课堂有趣能学到很多知识。此外感谢还要感谢Linux开源社区以及其他人对于Linux网络内核的理解的帮助。有些内容难以理解的通过其他博主的解答感悟颇多。同时也推荐学弟学们来选孟老师的这门课。文章来源:https://www.toymoban.com/news/detail-775576.html
非常感谢孟老师的教育和指导!文章来源地址https://www.toymoban.com/news/detail-775576.html
到了这里,关于TCP/IP协议栈源代码分析:GDB调试环境搭建及源码分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!