深入理解Linux网络——TCP连接建立过程(三次握手源码详解)-1

简介: 一、相关实际问题为什么服务端程序都需要先listen一下半连接队列和全连接队列长度如何确定“Cannot assign requested address”这个报错是怎么回事

一、相关实际问题

1.为什么服务端程序都需要先listen一下

2.半连接队列和全连接队列长度如何确定

3.“Cannot assign requested address”这个报错是怎么回事

4.一个客户端端口可以同时用在两条连接上吗

5.服务端半/全连接队列满了会怎么样

6.新连接的soket内核对象是什么时候建立的

7.建立一条TCP连接需要消耗多长时间

8.服务器负载很正常,但是CPU被打到底了时怎么回事

二、深入理解listen

1)listen系统调用

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    // 根据fd查找socket内核对象
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if(sock) {
  // 获取内核参数net.core.somaxconn
    somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
  if((unsigned int)backlog > somaxconn)
      backlog = somaxconn;
  // 调用协议栈注册的listen函数
  err = sock->ops->listen(sock, backlog);
}

用户态的socket文件描述符只是一个整数而已,内核是没有办法直接使用的,所以首先就是先根据用户传入的文件描述符来查找对应的socket内核对象。


再接着获取了系统里的net.core.somaxconn内核参数的值,和用户传入的backlog作比较后取一个最小值传入下一步。


所以虽然listen允许我们传入backlog(该值和半连接队列、全连接队列都有关系),但是会受到内核参数的限制。


接着通过调用sock->ops->listen进入协议栈的listen函数。


1.文件描述表:进程级别。一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表,记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。

2.打开文件表:系统级别。内核对所有打开文件维护的一个描述表格,将表格中的每一项称为打开文件句柄。它存储了一个打开文件的所有相关信息,例如当前文件的偏移量,访问模式,状态等等。

3.inode:系统级别。文件系统中的每个文件都有自己的i-node信息,它包含文件类型,访问权限,文件属性等等。065d37f6d7d44a75b2424affa394ae17.png

9b38462ae8ce460cb84a1706ea9e63f5.png

fdtable对应用户已打开文件表,或者说文件描述符表,是进程私有的。它的成员fd是file指针数组的指针,其中数组的索引就是文件描述符,而数组元素就是file指针,或者说已打开文件句柄。一个struct file的实例代表一个打开的文件,当一个用户进程成功打开文件时,会创建次结构体,并包含调用者应用程序的文件访问属性,例如文件数据的偏移量、访问模式和特殊标志等。此对象映射到调用者的文件描述符表,作为调用者应用程序对文件的句柄。



通常数组的第一个元素(索引为0)是进程的标准输入文件,数组的第二个元素(索引为1)是进程的标准输出文件,数组的第三个元素(索引为2)是进程的标准错误文件。查看进程允许打开的最大文件句柄数:ulimit -n;设置进程能打开的最大文件句柄数:ulimit -n xxx。

 72723c4182ef4e628557ad49db706a3b.png

以上说法是在linux中的概念,而在windows中句柄的概念对应的是linux中文件描述符的概念,都是一个非负的整数。

2)协议栈listen

上文提到系统调用最后会通过sock->ops->listen进入协议栈的listen函数,对于AF_INET而言,指向的是inet_listen

int inet_listen(struct socket *sock, int backlog)
{
    // 还不是listen状态(尚未listen过)
    if(old_state != TCP_LISTEN) {
  // 开始监听
  err = inet_csk_listen_start(sk, backlog);
    // 设置全连接队列长度
    sk->sk_max_ack_backlog = backlog;
}

可以看到,全连接队列的长度就是执行listen调用时传入的backlog和系统参数之间较小的那个值。所以如果再线上遇到了全连接队列溢出的问题,想加大该队列的长度,那么可能需要将它们都设置得更大。

回过头来看inet_csk_listen_start函数

int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    // icsk->icsk_accept_queue时接收队列
    // 接收队列内核对象的申请和初始化
    int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
    ......
}

函数再一开始就将struct sock对象强制转换成了inet_connection_sock,名叫icsk。之所以可以强制转换是因为inet_connection_sock是包含sock的。tcp_sock、inet_connection_sock、inet_sock、sock是逐层嵌套的关系,类似面向对象里继承的概念。而对于TCP的socket来说,sock对象实际上是一个tcp_sock。因此TCP的sock对象可以强制类型转换为tcp_sock、inet_connection_sock、inet_sock来使用即子类转换为父类。

struct tcp_sock {
    /* inet_connection_sock has to be the first member of tcp_sock */
    struct inet_connection_sock inet_conn;
    u16 tcp_header_len; /* Bytes of tcp header to send      */
    u16 xmit_size_goal_segs; /* Goal for segmenting output packets */
...
};
struct inet_connection_sock {
    /* inet_sock has to be the first member! */
    struct inet_sock      icsk_inet;
    struct request_sock_queue icsk_accept_queue;
    struct inet_bind_bucket   *icsk_bind_hash;
...
};
struct inet_sock {
    /* sk and pinet6 has to be the first two members of inet_sock */
    struct sock     sk;
#if IS_ENABLED(CONFIG_IPV6)
    struct ipv6_pinfo   *pinet6;
#endif
...
};
struct socket {
    socket_state        state;
...
    struct sock     *sk;
    const struct proto_ops  *ops;
};

也可以由sock强制转换为tcp_sock,因为在套接字创建的时候,就是以struct tcp_sock作为大小进行分配的。也就是内核中的每个sock都是tcp_sock类型,而struct tcp_sock正好是最大的那个结构体,不会出现越界访问的情况。


在接下来的一行reqsk_queue_alloc中实际上包含了两件重要的事情。一是接收队列数据结构的定义,二是接收队列的申请和初始化。


3)接收队列定义

icsk->icsk_accept_queue定义在inet_connection_sock下,是一个request_sock_queue类型的对象,是内核用来接收客户端请求的主要数据结构。我们平时说的全连接队列、半连接队列全都是在这个数据结构里实现的。

729eba138dc942e58206eaeee3a97113.png

我们来看具体的代码。

struct inet_connection_sock {
    struct inet_sock icsk_inet;
    struct request_sock_queue icsk_accept_queue;
    ......
}
struct request_sock_queue {
    // 全连接队列
    struct request_sock *rskq_accept_head;
    struct request_sock *rskq_accept_tail;
    // 半连接队列
    struct listen_sock *listen_opt;
    ......
}
struct listen_sock {
    u8 max_qlen_log;
    u32 nr_table_entires;
    ......
    struct request_sock *syn_table[0];
}


对于全连接队列来说,在它上面不需要进行复杂的查找工作,accept处理的时候只是先进先出地接受就好了。所以全连接队列通过rskq_accept_head和rskq_accept_tail以链表的形式来管理。


和半连接队列相关联的数据对象是listen_opt,它是listen_sock类型的。因为服务端需要在第三次握手时快速地查找出来第一次握手时留存的request_sock对象,所以其实是用了一个哈希表来管理,就是struct request_sock *syn_table[0]。max_qlen_log和nr_table_entries都和半连接队列的长度有关。

4)接收队列申请和初始化

了解了全/半连接队列数据结构后,再回到inet_csk_listen_start函数中。它调用了reqsk_queue_alloc来申请和初始化icsk_accept_queue这个接收队列。

在reqsk_queue_alloc这个函数中完成了接收队列request_sock_queue内核对象的创建和初始化。其中包括内存申请、半连接队列长度的计算、全连接队列头的初始化等等。

int reqsk_queue_alloc(struct request_sock_queue *queue, unsigend int nr_table_entries)
{
    size_t lopt_size - sizeof(struct listen_sock);
    struct listen_sock *lopt;
    // 计算半连接队列的长度
    nt_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
    nr_table_entries = ......
    // 为listen神奇对象申请内存,这里包括了半连接队列
    lopt_size += nr_table_entries * sizeof(sturct request_sock *);
    if(lopt_size > PAGE_SIZE)
      lopt = vzalloc(lopt_size);
    else
  lopt = kzalloc(lopt_size, GFP_KERNEL);
    // 全连接队列头初始化
    queue->rskq_accept_head = NULL;
    // 半连接队列设置
    lopt->nr_table_entries = nr_table_entries;
    queue->listen_opt = lopt;
}

开头定义了一个struct listen_sock的指针,这个listen_sock就是我们平时经常说的半连接队列。接下来计算半连接队列的长度,计算出来实际大小后进行内存的申请。最后将全连接队列呕吐设置成了NULL,将半连接队列挂到了接收队列queue上。


半连接队列上每个元素分配的是一个指针大小,实际指向的request_sock的内存还未分配。这其实是一个哈希表,真正的半连接用的request_sock对象是在握手的过程中分配的,计算完哈希值后挂到这个哈希表上。

5)半连接队列长度计算

reqsk_queue_alloc函数中计算了半连接队列的长度,因为有些复杂所以没有在前面展开,这里深入一下。

int reqsk_queue_alloc(struct request_sock_queue *queue, unsigend int nr_table_entries)
{
    // 计算半连接队列的长度
    nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
    nr_table_entries = max_t(u32, nr_table_entries, 8);
    nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
    // 为了效率,不记录nr_table_entries而是记录2的N次幂等于nr_table_entries
    for(kopt->max_qlen_log = 3; (1 << lopt->max_qlen_log) < nr_table_entries; lopt->max_qlen_log++);
    ......
}

传进来的nr_table_entries在最初是用户传入的backlog和内核参数net.core.somaxconn二者之间的较小值。而在这个reqsk_queue_alloc函数里又将完成三次的对比和计算。


min_t(u32, nr_table_entries, sysctl_max_syn_backlog):和sysctl_max_syn_backlog内核对象比较,取较小值

max_t(u32, nr_table_entries, 8):用来保证nr_table_entries不能比8小,避免传入太小的值导致无法建立连接

roundup_pow_of_two(nr_table_entries + 1):用于上对齐到2的整数次幂

总的来说半连接队列的长度是min(backlog, somaxconn, tcp_max_syn_backlog)+1再向上取整到2的N次幂,但最小不能小于16。


最后为了提升比较性能,内核并没有直接记录半连接队列的长度,而是采用了一种晦涩的方法,只记录其N次幂。即如果队列长度为16,则记录max_qlen_log为4,只需要直到它是为了提升性能的即可。

6)小结

listen的主要工作其实就是申请和初始化接收队列,包括全连接队列和半连接队列。其中全连接队列是一个链表,而半连接队列由于需要快速地查找,所以使用的是一个哈希表。这两个队列是三次握手中很重要的两个数据结构,有了它们服务端才能正常相应来自客户端的三次握手。所以服务端都需要先调用listen才行。

同时我们也知道了去内核时如何确定全连接队列和半连接队列的长度。


1.全连接队列:min(backlog, net.core.somaxconn)

2.半连接队列:max(min(backlog, net.core.somaxconn, tcp_max_syn_backlog) + 1向上取整到2的幂次, 16)

相关文章
|
安全 Linux 网络安全
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
349 0
Nipper 3.9.0 for Windows & Linux - 网络设备漏洞评估
|
网络协议 Unix Linux
深入解析:Linux网络配置工具ifconfig与ip命令的全面对比
虽然 `ifconfig`作为一个经典的网络配置工具,简单易用,但其功能已经不能满足现代网络配置的需求。相比之下,`ip`命令不仅功能全面,而且提供了一致且简洁的语法,适用于各种网络配置场景。因此,在实际使用中,推荐逐步过渡到 `ip`命令,以更好地适应现代网络管理需求。
1197 113
|
安全 网络协议 Linux
Linux网络应用层协议展示:HTTP与HTTPS
此外,必须注意,从HTTP迁移到HTTPS是一项重要且必要的任务,因为这不仅关乎用户信息的安全,也有利于你的网站评级和粉丝的信心。在网络世界中,信息的安全就是一切,选择HTTPS,让您的网站更加安全,使您的用户满意,也使您感到满意。
574 19
|
Linux 数据安全/隐私保护
使用Linux命令行接入无线网络Wi-Fi的示例。
现在,你已经使用命令行成功地连接到 Wi-Fi 网络了。这两个示例涵盖了用 `nmcli` 和 `wpa_supplicant` 连接无线网络的常见场景,让你能够不依赖图形化界面来完成这个任务。在日常使用中熟练掌握这些基本操作能增强你对 Linux 系统的理解,帮助你更有效地处理各种问题。
1385 12
|
Ubuntu Linux
Linux系统管理:服务器时间与网络时间同步技巧。
以上就是在Linux服务器上设置时间同步的方式。然而,要正确运用这些知识,需要理解其背后的工作原理:服务器根据网络中的其他机器的时间进行校对,逐步地精确自己的系统时间,就像一只犹豫不决的啮齿动物,通过观察其他啮齿动物的行为,逐渐确定自己的行为逻辑,既简单,又有趣。最后希望这个过程既能给你带来乐趣,也能提高你作为系统管理员的专业素养。
2590 20
|
安全 Ubuntu Linux
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
811 0
Nipper 3.8.0 for Windows & Linux - 网络设备漏洞评估
|
网络协议 物联网
VB6网络通信软件上位机开发,TCP网络通信,读写数据并处理,完整源码下载
本文介绍使用VB6开发网络通信上位机客户端程序,涵盖Winsock控件的引入与使用,包括连接服务端、发送数据(如通过`Winsock1.SendData`方法)及接收数据(利用`Winsock1_DataArrival`事件)。代码实现TCP网络通信,可读写并处理16进制数据,适用于自动化和工业控制领域。提供完整源码下载,适合学习VB6网络程序开发。 下载链接:[完整源码](http://xzios.cn:86/WJGL/DownLoadDetial?Id=20)
696 12
|
JSON 运维 Ubuntu
Linux下如何使用Curl进行网络请求
希望这篇文章能帮助您在Linux下更好地使用Curl进行网络请求。如有疑问,请随时提问!
1123 10
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
11613 2
|
Linux
SecureCRT连接Linux时乱码问题
本文详细介绍了在使用SecureCRT连接Linux服务器时出现乱码问题的解决方法,包括设置SecureCRT字符编码、检查和配置Linux服务器字符编码、调整终端设置等。通过这些方法,您可以有效解决SecureCRT连接Linux时的乱码问题,确保正常的终端显示和操作。希望本文能帮助您在实际操作中更好地解决类似问题,提高工作效率。
2249 17