深入理解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)

相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
相关文章
|
6天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
19 2
|
18天前
|
域名解析 网络协议 安全
|
24天前
|
运维 监控 网络协议
|
18天前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
25天前
|
Web App开发 缓存 网络协议
不为人知的网络编程(十八):UDP比TCP高效?还真不一定!
熟悉网络编程的(尤其搞实时音视频聊天技术的)同学们都有个约定俗成的主观论调,一提起UDP和TCP,马上想到的是UDP没有TCP可靠,但UDP肯定比TCP高效。说到UDP比TCP高效,理由是什么呢?事实真是这样吗?跟着本文咱们一探究竟!
49 10
|
20天前
|
存储 Ubuntu Linux
2024全网最全面及最新且最为详细的网络安全技巧 (三) 之 linux提权各类技巧 上集
在本节实验中,我们学习了 Linux 系统登录认证的过程,文件的意义,并通过做实验的方式对 Linux 系统 passwd 文件提权方法有了深入的理解。祝你在接下来的技巧课程中学习愉快,学有所获~和文件是 Linux 系统登录认证的关键文件,如果系统运维人员对shadow或shadow文件的内容或权限配置有误,则可以被利用来进行系统提权。上一章中,我们已经学习了文件的提权方法, 在本章节中,我们将学习如何利用来完成系统提权。在本节实验中,我们学习了。
|
28天前
|
Ubuntu Linux 虚拟化
Linux虚拟机网络配置
【10月更文挑战第25天】在 Linux 虚拟机中,网络配置是实现虚拟机与外部网络通信的关键步骤。本文介绍了四种常见的网络配置方式:桥接模式、NAT 模式、仅主机模式和自定义网络模式,每种模式都详细说明了其原理和配置步骤。通过这些配置,用户可以根据实际需求选择合适的网络模式,确保虚拟机能够顺利地进行网络通信。
|
1月前
|
关系型数据库 MySQL Linux
Navicat 连接 Windows、Linux系统下的MySQL 各种错误,修改密码。
使用Navicat连接Windows和Linux系统下的MySQL时可能遇到的四种错误及其解决方法,包括错误代码2003、1045和2013,以及如何修改MySQL密码。
205 0
|
6天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
25 3
|
14天前
|
缓存 监控 Linux

热门文章

最新文章