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

449248f18e07636cac4d1f4693d33fcd.gif

作者 | 张彦飞allen

来源 | 开发内功修炼

大家都知道,在创建一个服务器程序的时候,需要先 listen 一下,然后才能接收客户端的请求。例如下面的这段代码我们再熟悉不过了。

int main(int argc, char const *argv[])
{int fd = socket(AF_INET, SOCK_STREAM, 0);bind(fd, ...);listen(fd, 128);accept(fd, ...);

那么我们今天来思考一个问题,为什么需要 listen 一下才能接收连接?或者换句话说,listen 内部执行的时候到底干了啥?

如果你也想搞清楚 listen 内部的这些秘密,那么请跟我来!

一、创建 socket

服务器要做的第一件事就是先创建一个 socket。具体就是通过调用 socket 函数。当 socket 函数执行完毕后,在用户层视角我们是看到返回了一个文件描述符 fd。但在内核中其实是一套内核对象组合,大体结构如下。

14efbc9879dea9a817916d83287493d5.png

这里简单了解这个结构就行,后面我们在源码中看到函数指针调用的时候需要回头再来看它。

二、内核执行 listen

2.1 listen 系统调用

我在 net/socket.c 下找到了 listen 系统调用的源码。

//file: net/socket.c
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{//根据 fd 查找 socket 内核对象sock = sockfd_lookup_light(fd, &err, &fput_needed);if (sock) {//获取内核参数 net.core.somaxconnsomaxconn = 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(该值和半连接队列、全连接队列都有关系)。但是如果用户传入的比 net.core.somaxconn 还大的话是不会起作用的。

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

2.2 协议栈 listen

这里我们需要用到第一节中的 socket 内核对象结构图了,通过它我们可以看出 sock->ops->listen 实际执行的是 inet_listen。

//file: net/ipv4/af_inet.c
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;
}

在这里我们先看一下最底下这行,sk->sk_max_ack_backlog 是全连接队列的最大长度。所以这里我们就知道了一个关键技术点,服务器的全连接队列长度是 listen 时传入的 backlog 和 net.core.somaxconn 之间较小的那个值

如果你在线上遇到了全连接队列溢出的问题,想加大该队列长度,那么可能需要同时考虑 listen 时传入的 backlog 和 net.core.somaxconn。

再回过头看 inet_csk_listen_start 函数。

//file: net/ipv4/inet_connection_sock.c
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 是接收队列,详情见 2.3 节 //接收队列内核对象的申请和初始化,详情见 2.4节 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 是逐层嵌套的关系,类似面向对象里的继承的概念。

7a15539b7b74c2c685544b4aaf6b30bc.png

对于 TCP 的 socket 来说,sock 对象实际上是一个 tcp_sock。因此 TCP 中的 sock 对象随时可以强制类型转化为 tcp_sock、inet_connection_sock、inet_sock 来使用。

在接下来的一行 reqsk_queue_alloc 中实际上包含了两件重要的事情。一是接收队列数据结构的定义。二是接收队列的申请和初始化。这两块都比较重要,我们分别在 2.3 节,和 2.4 节介绍。

2.3 接收队列定义

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

1d82f7f168325cb1a931ef351a7335e4.png

我们来看具体的代码。

//file: include/net/inet_connection_sock.h
struct inet_connection_sock {/* inet_sock has to be the first member! */struct inet_sock   icsk_inet;struct request_sock_queue icsk_accept_queue;......
}

我们再来查找到 request_sock_queue 的定义,如下。

//file: include/net/request_sock.h
struct request_sock_queue {//全连接队列struct request_sock *rskq_accept_head;struct request_sock *rskq_accept_tail;//半连接队列struct listen_sock *listen_opt;......
};

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

和半连接队列相关的数据对象是 listen_opt,它是 listen_sock 类型的。

//file: 
struct listen_sock {u8   max_qlen_log;u32   nr_table_entries;......struct request_sock *syn_table[0];
};

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

2.4 接收队列申请和初始化

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

//file: net/ipv4/inet_connection_sock.c
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{...int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);...
}

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

让我们进入它的源码:

//file: net/core/request_sock.c
int reqsk_queue_alloc(struct request_sock_queue *queue,unsigned int nr_table_entries)
{size_t lopt_size = sizeof(struct listen_sock);struct listen_sock *lopt;//计算半连接队列的长度nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);nr_table_entries = ......//为 listen_sock 对象申请内存,这里包含了半连接队列lopt_size += nr_table_entries * sizeof(struct request_sock *);if (lopt_size > PAGE_SIZE)lopt = vzalloc(lopt_size);elselopt = 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 就是我们平时经常说的半连接队列。

接下来计算半连接队列的长度。计算出来了实际大小以后,开始申请内存。最后将全连接队列头 queue->rskq_accept_head 设置成了 NULL,将半连接队列挂到了接收队列 queue 上。

这里要注意一个细节,半连接队列上每个元素分配的是一个指针大小(sizeof(struct request_sock *))。这其实是一个 Hash 表。真正的半连接用的 request_sock 对象是在握手过程中分配,计算完 Hash 值后挂到这个 Hash 表 上。

2.5 半连接队列长度计算

在上一小节,我们提到 reqsk_queue_alloc 函数中计算了半连接队列的长度,由于这个有点小复杂,所以我们单独拉一个小节讨论这个。

//file: net/core/request_sock.c
int reqsk_queue_alloc(struct request_sock_queue *queue,unsigned 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 的几次幂等于 nr_table_entriesfor (lopt->max_qlen_log = 3;(1 << lopt->max_qlen_log) < nr_table_entries;lopt->max_qlen_log++);......
}

传进来的 nr_table_entries 在最初调用 reqsk_queue_alloc 的地方可以看到,它是内核参数 net.core.somaxconn 和用户调用 listen 时传入的 backlog 二者之间的较小值。

在这个 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 的整数幂次的。

说到这儿,你可能已经开始头疼了。确实这样的描述是有点抽象。咱们换个方法,通过两个实际的 Case 来计算一下。

假设:某服务器上内核参数 net.core.somaxconn 为 128, net.ipv4.tcp_max_syn_backlog 为 8192。那么当用户 backlog 传入 5 时,半连接队列到底是多长呢?

和代码一样,我们还把计算分为四步,最终结果为 16

  1. min (backlog, somaxconn)  = min (5, 128) = 5

  2. min (5, tcp_max_syn_backlog) = min (5, 8192) = 5

  3. max (5, 8) = 8

  4. roundup_pow_of_two (8 + 1) = 16

somaxconn 和 tcp_max_syn_backlog 保持不变,listen 时的 backlog 加大到 512,再算一遍,结果为 256

  1. min (backlog, somaxconn)  = min (512, 128) = 128

  2. min (128, tcp_max_syn_backlog) = min (128, 8192) = 128

  3. max (128, 8) = 128

  4. roundup_pow_of_two (128 + 1) = 256

算到这里,我把半连接队列长度的计算归纳成了一句话,半连接队列的长度是 min(backlog, somaxconn, tcp_max_syn_backlog) + 1 再上取整到 2 的幂次,但最小不能小于16。 我用的内核源码是 3.10, 你手头的内核版本可能和这个稍微有些出入。

如果你在线上遇到了半连接队列溢出的问题,想加大该队列长度,那么就需要同时考虑 somaxconn、backlog、和 tcp_max_syn_backlog 三个内核参数。

最后再说一点,为了提升比较性能,内核并没有直接记录半连接队列的长度。而是采用了一种晦涩的方法,只记录其幂次假设队列长度为 16,则记录 max_qlen_log 为 4 (2 的 4 次方等于 16),假设队列长度为 256,则记录 max_qlen_log 为 8 (2 的 8 次方等于 16)。大家只要知道这个东东就是为了提升性能的就行了。

最后,总结一下

计算机系的学生就像背八股文一样记着服务器端 socket 程序流程:先 bind、再 listen、然后才能 accept。至于为什么需要先 listen 一下才可以 accpet,似乎我们很少去关注。

通过今天对 listen 源码的简单浏览,我们发现 listen 最主要的工作就是申请和初始化接收队列,包括全连接队列和半连接队列。其中全连接队列是一个链表,而半连接队列由于需要快速的查找,所以使用的是一个哈希表(其实半连接队列更准确的的叫法应该叫半连接哈希表)。

2e44b952e25d4d0954dbe57c15277ec2.png

全/半两个队列是三次握手中很重要的两个数据结构,有了它们服务器才能正常响应来自客户端的三次握手。所以服务器端都需要 listen 一下才行。

除此之外我们还有额外收获,我们还知道了内核是如何确定全/半连接队列的长度的。

1.全连接队列的长度
对于全连接队列来说,其最大长度是 listen 时传入的 backlog 和 net.core.somaxconn 之间较小的那个值。如果需要加大全连接队列长度,那么就是调整 backlog 和 somaxconn。

2.半连接队列的长度
在 listen 的过程中,内核我们也看到了对于半连接队列来说,其最大长度是 min(backlog, somaxconn, tcp_max_syn_backlog) + 1 再上取整到 2 的幂次,但最小不能小于16。如果需要加大半连接队列长度,那么需要一并考虑 backlog,somaxconn 和 tcp_max_syn_backlog 这三个参数。网上任何告诉你修改某一个参数就能提高半连接队列长度的文章都是错的。

所以,不放过一个细节,你可能会有意想不到的收获!

fd4024e7beb7fd682421bb0a4875aa4c.gif

往期推荐

如果让你来设计网络

70% 开发者对云原生一知半解,“云深”如何知处?

浅述 Docker 的容器编排

如何在 Kubernetes Pod 内进行网络抓包

6b02d171874d45cfb977cb0bf3bae3a6.gif

点分享

8b63a24ecccf0b7617512002e554b4f1.gif

点收藏

2446eb42e72af65ecbfa38226a802c8c.gif

点点赞

d5f850e8acb508457a888093f0ac4138.gif

点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511948.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

10个Bug环环相扣,你能解开几个?

简介&#xff1a;由阿里云云效主办的2021年第3届83行代码挑战赛已经收官。超2万人围观&#xff0c;近4000人参赛&#xff0c;85个团队组团来战。大赛采用游戏闯关玩儿法&#xff0c;融合元宇宙科幻和剧本杀元素&#xff0c;让一众开发者玩得不亦乐乎。 今天请来决赛赛题设计者…

小小智慧树机器人_国网营业厅“AI新势力”,科沃斯商用机器人解锁智慧服务新模式!...

智慧营业厅新格局&#xff0c;AI机器人成标配&#xff1f;AI加持&#xff0c;万物互联、万物智能。2019年&#xff0c;应用人工智能的门槛下降&#xff0c;大量人工智能催生的新产品、服务和最佳实践轮番出现。人工智能正在重塑各行各业&#xff0c;传统营业厅网点该如何搭上AI…

AIoT时代存储如何升级?长江存储发布高速闪存芯片UFS 3.1

2022年4月19日&#xff0c;长江存储科技有限责任公司&#xff08;简称“长江存储”&#xff09;宣布推出UFS 3.1通用闪存——UC023。这是长江存储为5G时代精心打造的一款高速闪存芯片&#xff0c;可广泛适用于高端旗舰智能手机、平板电脑、AR/VR等智能终端领域&#xff0c;以满…

零信任策略下云上安全信息与事件管理实践

简介&#xff1a;随着企业数字化转型的深入推进&#xff0c;网络安全越来越被企业所重视。为了构建完备的安全防御体系&#xff0c;企业通常会引入了防火墙(Firewall)、防病毒系统(Anti-Virus System&#xff0c;AVS)、入侵防御系统(Intrusion Prevention System&#xff0c;IP…

kl散度度量分布_数据挖掘比赛技巧——确定数据同分布

在数据挖掘比赛中&#xff0c;很重要的一个技巧就是要确定训练集与测试集特征是否同分布&#xff0c;这也是机器学习的一个很重要的假设[1]。但很多时候我们知道这个道理&#xff0c;却很难有方法来保证数据同分布&#xff0c;这篇文章就分享一下我所了解的同分布检验方法。封面…

Inclavare Containers:云原生机密计算的未来

简介&#xff1a;本文为你详细的梳理一次 Inclavare Containers 项目的发展脉络&#xff0c;解读它的核心思想和创新技术。 作为业界首个面向机密计算场景的开源容器运行时&#xff0c;Inclavare Containers 项目于 2020 年 5 月开源&#xff0c;短短一年多时间内发展势头非常迅…

没有操作系统程序可以运行起来吗?

作者 | 陆小风来源 | 码农的荒岛求生现在的程序员对操作系统已经习以为常了&#xff0c;但是你有没有想过&#xff0c;如果没有操作系统的话我们可以让程序运行起来吗&#xff1f;先说答案&#xff0c;当然是可以的&#xff0c;而且必须是可以的。你可以从这个角度来思考&#…

sysAK(青囊)系统运维工具集:如何实现高效自动化运维?| 龙蜥技术

简介&#xff1a;What is sysAK、典型工具介绍、开源 3 方面介绍了 sysAK 系统&#xff0c;目前 sysAK 工具集已经在龙蜥社区开源&#xff0c;并且在系统运维 SIG、跟踪诊断 SIG 一起共建&#xff0c;希望大家后期加入 SIG 一起讨论共建。 编者按&#xff1a;本文整理自「云栖…

quill鼠标悬浮 出现提示_CHERRY MC8.1鼠标评测:超前设计延续军火箱信仰

CHERRY作为机械键盘品牌拥有非常高的知名度&#xff0c;许多朋友的第一把机械键盘就是CHERRY品牌。在CHERRY产品线中&#xff0c;最具信仰的一定是军火箱MX8.0键盘。键盘本身手感颜值俱佳&#xff0c;独特的军火箱包装更是收获了大批粉丝。至于最配这把键盘的鼠标却一直让网友们…

高并发IO的底层原理

作者 | 阿辉来源 | Andy阿辉思考&#xff1a;作为程序员的我们&#xff0c;在编写软件进行文件读取&#xff0c;网络收发数据时&#xff0c;是不关心其具体的内部数据传输的。只关心把数据传输到缓冲区或及时从缓冲区读取数据。那么内部究竟是如何实现的呢&#xff0c;今天这篇…

新能源汽车太猛了,这些卡脖子技术你了解吗?

简介&#xff1a;从汽车行业的变化&#xff0c;我们即可初步看出芯片的重要性&#xff0c;那么&#xff0c;芯片对汽车行业的发展具体有哪些重要影响呢&#xff1f; 根据全球汽车咨询机构Auto Forecast Solutions统计的数据&#xff0c;截至10月10日&#xff0c;由于芯片短缺&…

龙蜥社区成立系统运维SIG,开源sysAK系统运维工具集

简介&#xff1a;系统运维SIG致力于打造一个集主机管理、配置部署、监控报警、异常诊断、安全审计等一系列功能的自动化运维平台。 OpenAnolis 龙蜥社区&#xff08;以下简称“龙蜥社区”&#xff09;正式成立系统运维&#xff08;System Operation&Maintenance, sysOM&…

奔跑吧兄弟变成机器人是哪一期_奔跑吧预告,郑恺郭麒麟回归,而我却被女嘉宾的颜值吸引了...

哈喽小伙伴们&#xff0c;近期大家都看了《奔跑吧黄河篇》吗&#xff1f;现在已经播到第二期了&#xff0c;相信大家依旧是对跑男系列节目非常感兴趣的&#xff0c;播放量非常高&#xff0c;稳稳占据TX和AQY两大视频平台的综艺播放第一名的位置&#xff0c;可见网友们真的是非常…

院士专家热议如何拥抱“东数西算”,第二届中国IDC行业Discovery大会顺利召开

4月21日&#xff0c;一场别开生面的主题为“聚光奔赴”的数据中心行业大会圆满落下帷幕。由中国通信工业协会数据中心委员会指导&#xff0c;中国IDC圈与世纪互联共同主办的“2022年第二届中国IDC行业Discovery大会”在线上召开&#xff0c;会议聚焦国家“双碳”目标、“东数西…

一文理解 K8s 容器网络虚拟化

简介&#xff1a;本文需要读者熟悉 Ethernet&#xff08;以太网&#xff09;的基本原理和 Linux 系统的基本网络命令&#xff0c;以及 TCP/IP 协议族并了解传统的网络模型和协议包的流转原理。文中涉及到 Linux 内核的具体实现时&#xff0c;均以内核 v4.19.215 版本为准。 作者…

应对 Job 场景,Serverless 如何帮助企业便捷上云

简介&#xff1a;函数计算作为事件驱动的全托管计算服务&#xff0c;其执行模式天生就与这类 Job 场景非常契合&#xff0c;对上述痛点进行了全方面的支持&#xff0c;助力“任务”的无服务器上云。 作者&#xff1a;冯一博 任务&#xff08;Jobs&#xff09;&#xff0c;是互…

Gartner发布新兴技术研究:深入洞悉元宇宙

供稿 | Gartner 出品 | CSDN云计算 根据Gartner预测&#xff0c;2026年全球30%的企业机构将拥有元宇宙产品和服务。 元宇宙是一个由独立但相互连接的网络所组成的持久、沉浸式数字环境&#xff0c;但目前尚未确定这些网络将使用的通信协议。元宇宙能够实现持久、去中心化、可…

双11实时物流订单实践

简介&#xff1a;随着双11的开启&#xff0c;物流业也迎来了年度大考。2021年双11期间&#xff0c;递四方作为物流仓储服务方&#xff0c;布局仓库和分拣点超40个&#xff0c;50w平米作业场地&#xff0c;单日订单峰值达千万级别&#xff0c;海量购物订单由递四方配送到家&…

阿里云徐立:面向容器和 Serverless Computing 的存储创新

简介&#xff1a;以上为大家分享了阿里云容器存储的技术创新&#xff0c;包括 DADI 镜像加速技术&#xff0c;为容器规模化启动奠定了很好的基础&#xff0c;ESSD 云盘提供极致性能&#xff0c;CNFS 容器网络文件系统提供极致的用户体验。 作者&#xff1a;徐立 云原生的创新…

鸿蒙2.0beta报名,鸿蒙OS 2.0 Beta版系统在哪报名-报名方法介绍

鸿蒙OS系统一直以来深受大家的关注&#xff0c;最近全新推出了鸿蒙OS 2.0 Beta版&#xff0c;那么鸿蒙OS 2.0 Beta版在哪报名呢&#xff1f;小编为大家分享一下关于鸿蒙OS 2.0 Beta版的报名方法介绍&#xff0c;对鸿蒙OS 2.0 Beta版感兴趣的不要错过了。鸿蒙OS 2.0 Beta版系统报…