🚙

💨 💨 💨

×

  • Categories

  • Archives

  • Tags

  • About

QUIC原理与KCP会话握手借鉴

Posted on 02-26-2021 | In NP

参考:

  • https://zhuanlan.zhihu.com/p/142794794
  • https://zhuanlan.zhihu.com/p/32553477

本文主要介绍 QUIC 协议产生的背景和核心特性。

写在前面

如果你的 App,在不需要任何修改的情况下就能提升 15% 以上的访问速度。特别是弱网络的时候能够提升 20% 以上的访问速度。

如果你的 App,在频繁切换 4G 和 WIFI 网络的情况下,不会断线,不需要重连,用户无任何感知。如果你的 App,既需要 TLS 的安全,也想实现 HTTP2 多路复用的强大。

如果你刚刚才听说 HTTP2 是下一代互联网协议,如果你刚刚才关注到 TLS1.3 是一个革命性具有里程碑意义的协议,但是这两个协议却一直在被另一个更新兴的协议所影响和挑战。

如果这个新兴的协议,它的名字就叫做 “快”,并且正在标准化为新一代的互联网传输协议。

你愿意花一点点时间了解这个协议吗?你愿意投入精力去研究这个协议吗?你愿意全力推动业务来使用这个协议吗?

QUIC 概述

Quic 全称 quick udp internet connection [1],“快速 UDP 互联网连接”,(和英文 quick 谐音,简称 “快”)是由 google 提出的使用 udp 进行多路并发传输的协议。

Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势 [2]:

  1. 减少了 TCP 三次握手及 TLS 握手时间。
  2. 改进的拥塞控制。
  3. 避免队头阻塞的多路复用。
  4. 连接迁移。
  5. 前向冗余纠错。

TCP的缺陷

从上个世纪 90 年代互联网开始兴起一直到现在,大部分的互联网流量传输只使用了几个网络协议。使用 IPv4 进行路由,使用 TCP 进行连接层面的流量控制,使用 SSL/TLS 协议实现传输安全,使用 DNS 进行域名解析,使用 HTTP 进行应用数据的传输。

而且近三十年来,这几个协议的发展都非常缓慢。TCP 主要是拥塞控制算法的改进,SSL/TLS 基本上停留在原地,几个小版本的改动主要是密码套件的升级,TLS1.3[3] 是一个飞跃式的变化,但截止到今天,还没有正式发布。IPv4 虽然有一个大的进步,实现了 IPv6,DNS 也增加了一个安全的 DNSSEC,但和 IPv6 一样,部署进度较慢。

随着移动互联网快速发展以及物联网的逐步兴起,网络交互的场景越来越丰富,网络传输的内容也越来越庞大,用户对网络传输效率和 WEB 响应速度的要求也越来越高。

一方面是历史悠久使用广泛的古老协议,另外一方面用户的使用场景对传输性能的要求又越来越高。如下几个由来已久的问题和矛盾就变得越来越突出。

  1. 协议历史悠久导致中间设备僵化。
  2. 依赖于操作系统的实现导致协议本身僵化。
  3. 建立连接的握手延迟大。
  4. 队头阻塞。

这里分小节简单说明一下:

中间设备的僵化

可能是 TCP 协议使用得太久,也非常可靠。所以我们很多中间设备,包括防火墙、NAT 网关,整流器等出现了一些约定俗成的动作。

比如有些防火墙只允许通过 80 和 443,不放通其他端口。NAT 网关在转换网络地址时重写传输层的头部,有可能导致双方无法使用新的传输格式。整流器和中间代理有时候出于安全的需要,会删除一些它们不认识的选项字段。

TCP 协议本来是支持端口、选项及特性的增加和修改。但是由于 TCP 协议和知名端口及选项使用的历史太悠久,中间设备已经依赖于这些潜规则,所以对这些内容的修改很容易遭到中间环节的干扰而失败。

而这些干扰,也导致很多在 TCP 协议上的优化变得小心谨慎,步履维艰。

依赖于操作系统的实现导致协议僵化

TCP 是由操作系统在内核西方栈层面实现的,应用程序只能使用,不能直接修改。虽然应用程序的更新迭代非常快速和简单。但是 TCP 的迭代却非常缓慢,原因就是操作系统升级很麻烦。

现在移动终端更加流行,但是移动端部分用户的操作系统升级依然可能滞后数年时间。PC 端的系统升级滞后得更加严重,windows xp 现在还有大量用户在使用,尽管它已经存在快 20 年。

服务端系统不依赖用户升级,但是由于操作系统升级涉及到底层软件和运行库的更新,所以也比较保守和缓慢。

这也就意味着即使 TCP 有比较好的特性更新,也很难快速推广。比如 TCP Fast Open。它虽然 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它。

建立连接的握手延迟大

不管是 HTTP1.0/1.1 还是 HTTPS,HTTP2,都使用了 TCP 进行传输。HTTPS 和 HTTP2 还需要使用 TLS 协议来进行安全传输。这就出现了两个握手延迟:

1.TCP 三次握手导致的 TCP 连接建立的延迟。

2.TLS 完全握手需要至少 2 个 RTT 才能建立,简化握手需要 1 个 RTT 的握手延迟。

对于很多短连接场景,这样的握手延迟影响很大,且无法消除。

队头阻塞

队头阻塞主要是 TCP 协议的可靠性机制引入的。TCP 使用序列号来标识数据的顺序,数据必须按照顺序处理,如果前面的数据丢失,后面的数据就算到达了也不会通知应用层来处理。

另外 TLS 协议层面也有一个队头阻塞,因为 TLS 协议都是按照 record 来处理数据的,如果一个 record 中丢失了数据,也会导致整个 record 无法正确处理。

概括来讲,TCP 和 TLS1.2 之前的协议存在着结构性的问题,如果继续在现有的 TCP、TLS 协议之上实现一个全新的应用层协议,依赖于操作系统、中间设备还有用户的支持。部署成本非常高,阻力非常大。

所以 QUIC 协议选择了 UDP,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟,同时在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。

0RTT建立连接

现如今,高速且安全的网络接入服务已经成为人们的必须。传统 TCP+TLS 构建的安全互联服务,升级与补丁更新时有提出(如 TCP Fastopen,新的 TLS 1.3),但是由于基础设施僵化,升级与应用困难。为解决这个问题,Google 另辟蹊径在 UDP 的基础上实现了带加密的更好的 TCP–QUIC(Quick UDP Internet Connection), 一种基于 UDP 的低时延的互联网传输层协议。近期成立了 Working Group 也将 QUIC 作为制定 HTTP 3.0 的标准的基础, 说明 QUIC 的应用前景美好。本文单独就网络传输的建连问题展开了分析, 浅析了建连时间对传输的影响, 以及 QUIC 的 0-RTT 建连是如何解决建连耗时长的问题的。在此基础上, 结合 QUIC 的源码, 浅析了 QUIC 的基本实现, 并描述一种可供参考的分布式环境下的 0-RTT 的落地实践方案。

以一次简单的 HTTPS 请求(example.com)为例(假设访问 example.com 时返回的内容较小,server 端可以在一个数据包里返回响应),为获取请求资源。需要经过以下 4 个步骤:

  1. DNS 查询 example.com 获取 IP。DNS 服务一般默认是由你的 ISP 提供,ISP 通常都会有缓存的,这部分时间能适当减少;
  2. TCP 握手,我们熟悉的 TCP 三次握手需要需要 1 个 RTT;
  3. TLS 握手,以目前应用最广泛的 TLS 1.2 而言,需要 2 个 RTT。对于非首次建连, 可以选择启用会话重用 (Session Resumption),则可缩小握手时间到 1 个 RTT;
  4. HTTP 业务数据交互,假设 example.com 的数据在一次交互就能取回来。那么业务数据的交互需要 1 个 RTT; 经过上面的过程分析可知,要完成一次简短的 HTTPS 业务数据交互,需要经历:
  • 新连接:4RTT + DNS。
  • 会话重用:3RTT + DNS

尤其对于小数据量的交互而言,抛开 DNS 查询时间, 建连时间占剩下的总时间的 2/3 至 3/4 不等,影响不可小觑。加之如果用户网络不好,RTT 延时大的话,建连时间可能耗费数百毫秒至数秒不等,这将极大的影响用户体验。究其原因一方面是 TCP 和 TLS 分层设计导致的:分层的设计需要每个逻辑层次分别建立自己的连接状态。另一方面是 TLS 的握手阶段复杂的密钥协商机制导致的。要降低建连耗时,需要从这两方面着手。

针对 TLS 的握手阶段复杂的密钥协商机问题, TLS 1.3 精简了握手交互过程, 实现了 1-RTT 握手。在会话重用类似的理念的基础上, 对非首次握手会话, 可以进一步实现 0-RTT 握手 (在刚开始 TLS 密钥协商的时候,就能附送一部分经过加密的数据传递给对方)。由于 TLS 是建立在 TCP 之上的, 0-RTT 没有计算 TCP 层的握手开销, 因而对用户来说, 发送数据之前还是要经历 TCP 层的 1RTT 握手, 因而不是真正的 0-RTT 握手。

TLS 1.3 为实现 0-RTT,需要双方在刚开始建立连接的时候就已经持有一个对称密钥,这个密钥在 TLS 1.3 中称为 PSK(Pre-Shared-Key)。PSK 是 TLS 1.2 中的会话重用 (Session Resumption) 机制的一个升级,TLS 1.3 握手结束后,服务器可以发送一个 NST(New-Session-Ticket)的报文给客户端,该报文中记录 PSK 的值、名字和有效期等信息,双方下一次建立连接可以使用该 PSK 值作为初始密钥材料。因为 PSK 是从以前建立的安全信道中获得的,只要证明了双方都持有相同的 PSK,不再需要证书认证,就可以证明双方的身份(因此 PSK 也是一种身份认证机制)。TLS 1.3 新加了 Early Data 类型的报文, 用于在 0-RTT 的握手阶段传递应用层数据, 实现了握手的同时就能附带加密的应用层数据从而实现 0-RTT。

TLS 1.3 的 0-RTT 特性不能防止重放攻击, 需要业务在使用时评估是否有重放攻击风险。如有相关风险的话, 可能需要酌情考虑禁用 0-RTT 特性。

下图对比了 TLS 各版本与场景下的延时对比:

TLS 各版本与场景下的耗时

从对比我们可以看到, 即使用上了 TLS 1.3, 精简了握手过程, 最快能做到 0-RTT 握手 (首次是 1-RTT), 但是对用户感知而言, 还要加上 1RTT 的 TCP 握手开销。 Google 有提出 Fastopen 的方案来使得 TCP 非首次握手就能附带用户数据, 但是由于 TCP 实现僵化, 无法升级应用, 相关 RFC 到现今都是 experimental 状态。这种分层设计带来的延时, 有没有办法进一步降低呢? QUIC 通过合并加密与连接管理解决了这个问题, 我们来看看其是如何实现真正意义上的 0-RTT 的握手, 让与 server 进行第一个数据包的交互就能带上用户数据。

QUIC 为规避 TCP 协议僵化的问题,将 QUIC 协议建立在了 UDP 之上。考虑到安全性是网络的必备选项,加密在 QUIC 里强制的。传输方面参考 TCP 并充分优化了 TCP 多年发现的缺陷和不足, 实现了一套端到端的可靠加密传输。通过将加密和连接管理两层合二为一,消除了当前 TCP+TLS 的分层设计传输引入的时延。

同 TLS 的握手一样, QUIC 的加密握手的核心在于协商出一个加密会话数据的对称密钥。QUIC 的握手使用了 DH 密钥协商算法来协商一个对称密钥。DH 密钥协商算法简单来讲, 需要通信双方各自生成自己的非对称公私钥对, 双发各自保留自己的私钥, 将公钥发给对方, 利用对方的公钥和自己的私钥可以运算出同一个对称密钥。详细的原理这里不展开叙述, 有专业的密码学书籍对其原理有详细的论述, 网上也有很多好的教程对其由深入浅出的总结, 如这一篇。

如上所述, DH 密钥协商需要通行双方各自生成自己的非对称公私钥对。server 端与客户端的关系是 1 对 N 的关系, 明显 server 端生成一份公私钥对, 让 N 个客户端公用, 能明显减少生成开销, 降低管理的成本。server 端的这份公私钥对就是专门用于握手使用的, 客户端一经获取, 就可以缓存下来后续建连时继续使用, 这个就是达成 0-RTT 握手的关键, 因此 server 生成的这份公钥称为 0-RTT 握手公钥。真正的握手过程是这样 (简化了实现细节):

  1. server 端在握手开始前,server 端需要首先生成 (或加载使用上次保存下来的) 握手公私钥对, 该份公私钥对是所有客户端共享的。
  2. client 端首次握手时, client 对 server 一无所知, 需要 1 个 RTT 来询问 server 端的握手公钥 (实际的握手交互还会发送诸如版本等其他数据) 并缓存下来。本步骤只在首次建连时发生(0-RTT 握手公钥的过期也会导致需要重走这一步), 但这种情况很少发生, 影响很小(也没办法避免)。
  3. client 收到 server 端返回这份握手公钥后,生成自己的临时公私钥对后, 计算出共享的对称密钥后, 加密好数据, 并连同 client 的公钥一并发给 server 端。照 DH 密钥协商的原理, 此处已经可以协商出每条会话不一样的会话密钥了 (因为每个 client 生成的公私钥是不同的), 是不是拿这个来加密会话数据就行了呢? 真实的情况不是这样的!
  4. server 端会再次生成一份临时的公私钥对,使用这份临时的私钥与客户端的公钥运算出最终的会话对称密钥。接下来 server 会拿这个最终的会话密钥加密应用层数据, 连同这份临时的 server 端公钥一并发给 client 端, client 端收到后可以按照 DH 的原理依瓢画葫会恢复出最终的会话对称密钥。后续所有的数据都是用最终的会话对称密钥进行加密。server 侧这个动作是不是多此一举呢? 不是的, 这么做的目的是为了获取所谓的前向安全特性: 因为 server 端的后面生成的这份公私钥是临时生成的, 不会保存下来,也就杜绝了密钥泄漏导致会话数据被恶意收集后的被解密掉的风险。

首次握手要多一个 RTT 询问 server 端的 0-RTT 握手公钥, 此询问过程不携带任何应用层数据, 因此是 1-RTT 握手。在首次握手完成后, client 端可以缓存下第一次询问获知的 server 端的公钥信息, 后续的连接过程可以跳过询问,直接使用缓存的 server 端的公钥。公钥信息在 QUIC 的实现里是保存在握手数据包里的 SCFG 中的 (Server Config), 获取 0-RTT 握手公钥就是要获取 SCFG, 后续均以获取 SCFG 代替。详细的握手过程, 可以参见 dog250 博客文章的详细叙述。

从上面的分析可知,0-RTT 握手的首次交互,server 端使用的是保存下来的握手密钥,因而没有无法做到前向安全,不能防止重放攻击, 需要业务在使用时评估是否有重放攻击风险。

QUIC 0-RTT 握手

上文简述了 QUIC 握手的密钥协商过程,首次需要询问一次 Server 以获取 SCFG, 从而获取存储于其中的 0-RTT 握手公钥。这一交互过程中 client 和 server 在 QUIC 的握手协议发送的报文中分别叫 Inchoate Client hello message (inchoate CHLO) 和 Rejection message (REJ)。因而包含 server 端的握手公钥的 SCFG 是在 REJ 报文中发给客户端的。有了 SCFG,接下来就能发起 0-RTT 握手。这一过程中 client 和 server 端在 QUIC 的握手协议发送的报文中分别叫 Full Client Hello message (full CHLO) 和 Server Hello message (SHLO)。下图摘自 Google QUIC 握手协议的官方文档,详细叙述了握手过程 client 侧的处理流程:

QUIC 握手客户端侧处理流程

0RTT的重放攻击风险

0-RTT连接恢复并非那么简单,它会带来许多意外风险及警告,这正是为什么有些默认程序不启用0-RTT连接恢复的原因。用户必须提前考虑所涉及的风险,并做出决定是否使用此功能。可以在应用程序层面使用预防措施来减轻这种情况。

首先,0-RTT连接恢复是不提供forwardsecrecy的,针对连接的secret parameters的妥协将微不足道地允许恢复新连接0-RTT阶段期间,发送applicationdata 进行特定的妥协。

在0-RTT阶段之后、或握手之后发送的数据,仍然是安全的。这是因为TLS1.3 (及QUIC)还是会对handshakecompletion之后发送的数据执行normal key exchange algorithm (这是forwardsecret) 。

值得注意的是,在0-RTT期间发送的applicationdata 可以被路径上的on-path attacker 捕获,然后多次重播到同一个服务器。这个在大部分情况下也许不是问题,因为attacker无法解密数据,0-RTT连接恢复还是非常有用的。在其他的情况下,这可能会引起出乎意料的风险。

举个比例,假设一家银行允许authenticated user (e.g using HTTPcookie, 或其他HTTP身份验证机制)通过specificAPI endpoint发出HTTP请求将资金从其账户发送到另一个用户。

如果attacker能够在使用0-RTT连接恢复时捕获该请求,他们将无法看到plaintext并且获取用户的凭据,原因是因为他们不知道用于加密数据的secretkey;但是他们仍然有可能一直散发重复性地请求,耗尽该用户的银行账户里的钱。

改进的拥塞控制

TCP 的拥塞控制实际上包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复 [22]。

QUIC 协议当前默认使用了 TCP 协议的 Cubic 拥塞控制算法 [6],同时也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等拥塞控制算法。

从拥塞算法本身来看,QUIC 只是按照 TCP 协议重新实现了一遍,那么 QUIC 协议到底改进在哪些方面呢?主要有如下几点:

可插拔

什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。体现在如下方面:

  1. 应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求。
  2. 即使是单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合。
  3. 应用程序不需要停机和升级就能实现拥塞控制的变更,我们在服务端只需要修改一下配置,reload 一下,完全不需要停止服务就能实现拥塞控制的切换。

STGW 在配置层面进行了优化,我们可以针对不同业务,不同网络制式,甚至不同的 RTT,使用不同的拥塞控制算法。

单调递增的 Packet Number

TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。

QUIC 同样是一个可靠的协议,它使用 Packet Number 代替了 TCP 的 sequence number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值。而 TCP 呢,重传 segment 的 sequence number 和原始的 segment 的 Sequence Number 保持不变,也正是由于这个特性,引入了 Tcp 重传的歧义问题。

如上图所示,超时事件 RTO 发生后,客户端发起重传,然后接收到了 Ack 数据。由于序列号一样,这个 Ack 数据到底是原始请求的响应还是重传请求的响应呢?不好判断。

如果算成原始请求的响应,但实际上是重传请求的响应(上图左),会导致采样 RTT 变大。如果算成重传请求的响应,但实际上是原始请求的响应,又很容易导致采样 RTT 过小。

由于 Quic 重传的 Packet 和原始 Packet 的 Pakcet Number 是严格递增的,所以很容易就解决了这个问题。

如上图所示,RTO 发生后,根据重传的 Packet Number 就能确定精确的 RTT 计算。如果 Ack 的 Packet Number 是 N+M,就根据重传请求计算采样 RTT。如果 Ack 的 Pakcet Number 是 N,就根据原始请求的时间计算采样 RTT,没有歧义性。

但是单纯依靠严格递增的 Packet Number 肯定是无法保证数据的顺序性和可靠性。QUIC 又引入了一个 Stream Offset 的概念。

即一个 Stream 可以经过多个 Packet 传输,Packet Number 严格递增,没有依赖。但是 Packet 里的 Payload 如果是 Stream 的话,就需要依靠 Stream 的 Offset 来保证应用数据的顺序。如错误! 未找到引用源。所示,发送端先后发送了 Pakcet N 和 Pakcet N+1,Stream 的 Offset 分别是 x 和 x+y。

假设 Packet N 丢失了,发起重传,重传的 Packet Number 是 N+2,但是它的 Stream 的 Offset 依然是 x,这样就算 Packet N + 2 是后到的,依然可以将 Stream x 和 Stream x+y 按照顺序组织起来,交给应用程序处理。

不允许 Reneging

什么叫 Reneging 呢?就是接收方丢弃已经接收并且上报给 SACK 选项的内容 [8]。TCP 协议不鼓励这种行为,但是协议层面允许这样的行为。主要是考虑到服务器资源有限,比如 Buffer 溢出,内存不够等情况。

Reneging 对数据重传会产生很大的干扰。因为 Sack 都已经表明接收到了,但是接收端事实上丢弃了该数据。

QUIC 在协议层面禁止 Reneging,一个 Packet 只要被 Ack,就认为它一定被正确接收,减少了这种干扰。

更多的 Ack 块

TCP 的 Sack 选项能够告诉发送方已经接收到的连续 Segment 的范围,方便发送方进行选择性重传。

由于 TCP 头部最大只有 60 个字节,标准头部占用了 20 字节,所以 Tcp Option 最大长度只有 40 字节,再加上 Tcp Timestamp option 占用了 10 个字节 [25],所以留给 Sack 选项的只有 30 个字节。

每一个 Sack Block 的长度是 8 个,加上 Sack Option 头部 2 个字节,也就意味着 Tcp Sack Option 最大只能提供 3 个 Block。

但是 Quic Ack Frame 可以同时提供 256 个 Ack Block,在丢包率比较高的网络下,更多的 Sack Block 可以提升网络的恢复速度,减少重传量。

Ack Delay 时间

Tcp 的 Timestamp 选项存在一个问题 [25],它只是回显了发送方的时间戳,但是没有计算接收端接收到 segment 到发送 Ack 该 segment 的时间。这个时间可以简称为 Ack Delay。

这样就会导致 RTT 计算误差。如下图:

  • 可以认为 TCP 的 RTT 计算:RTT = timestamp2 - timestamp1
  • 而 Quic 计算如下:RTT = timestamp2 - timestamp1 - AckDelay

当然 RTT 的具体计算没有这么简单,需要采样,参考历史数值进行平滑计算,参考如下公式

SRTT = SRTT + α(RTT - SRTT)
RTO = μ * SRTT + δ*DevRTT

基于 stream 和 connecton 级别的流量控制

QUIC 的流量控制 [22] 类似 HTTP2,即在 Connection 和 Strea6 级别提供了两种流量控制。为什么需要两类流量控制呢?主要是因为 QUIC 支持多路复用。

  1. Stream 可以认为就是一条 HTTP 请求。
  2. Connection 可以类比一条 TCP 连接。多路复用意味着在一7 Connetion 上会同时存在多条 Stream。既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。

QUIC 实现流量控制的原理比较简单:

通过 window_update 帧告诉对端自己可以接收的字节数,这样发送方就不会发送超过这个数量的数据。

通过 BlockFrame 告诉对端由于流量控制被阻塞了,无法发送数据。

QUIC 的流量控制和 TCP 有点区别,TCP 为了保证可靠性,窗口左边沿向右滑动时的长度取决于已经确认的字节数。如果中间出现丢包,就算接收到了更大序号的 Segment,窗口也无法超过这个序列号。

但 QUIC 不同,就算此前有些 packet 没有接收到,它的滑动只取决于接收到的最大偏移字节数。

  • 针对 Stream:可用窗口 = 最大窗口数 - 接收到的最大偏移数
  • 针对 Connection:可用窗口 = stream1可用窗口 + stream2可用窗口 + ... + streamN可用窗口

同样地,STGW 也在连接和 Stream 级别设置了不同的窗口数。

最重要的是,我们可以在内存不足或者上游处理性能出现问题时,通过流量控制来限制传输速率,保障服务可用性。

没有队头阻塞的多路复用

QUIC 的多路复用和 HTTP2 类似。在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (stream)。但是 QUIC 的多路复用相比 HTTP2 有一个很大的优势。

QUIC 一个连接上的多个 stream 之间没有依赖。这样假如 stream2 丢了一个 udp packet,也只会影响 stream2 的处理。不会影响 stream2 之前及之后的 stream 的处理。

这也就在很大程度上缓解甚至消除了队头阻塞的影响。

多路复用是 HTTP2 最强大的特性 [7],能够将多条请求在一条 TCP 连接上同时发出去。但也恶化了 TCP 的一个问题,队头阻塞 [11],如下图示:

HTTP2 在一个 TCP 连接上同时发送 4 个 Stream。其中 Stream1 已经正确到达,并被应用层读取。但是 Stream2 的第三个 tcp segment 丢失了,TCP 为了保证数据的可靠性,需要发送端重传第 3 个 segment 才能通知应用层读取接下去的数据,虽然这个时候 Stream3 和 Stream4 的全部数据已经到达了接收端,但都被阻塞住了。

不仅如此,由于 HTTP2 强制使用 TLS,还存在一个 TLS 协议层面的队头阻塞 [12]。

Record 是 TLS 协议处理的最小单位,最大不能超过 16K,一些服务器比如 Nginx 默认的大小就是 16K。由于一个 record 必须经过数据一致性校验才能进行加解密,所以一个 16K 的 record,就算丢了一个字节,也会导致已经接收到的 15.99K 数据无法处理,因为它不完整。

那 QUIC 多路复用为什么能避免上述问题呢?

  1. QUIC 最基本的传输单元是 Packet,不会超过 MTU 的大小,整个加密和认证过程都是基于 Packet 的,不会跨越多个 Packet。这样就能避免 TLS 协议存在的队头阻塞。
  2. Stream 之间相互独立,比如 Stream2 丢了一个 Pakcet,不会影响 Stream3 和 Stream4。不存在 TCP 队头阻塞。

当然,并不是所有的 QUIC 数据都不会受到队头阻塞的影响,比如 QUIC 当前也是使用 Hpack 压缩算法 [10],由于算法的限制,丢失一个头部数据时,可能遇到队头阻塞。

总体来说,QUIC 在传输大量数据时,比如视频,受到队头阻塞的影响很小。

加密认证的报文

TCP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击。

但是 QUIC 的 packet 可以说是武装到了牙齿。除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。

这样只要对 QUIC 报文任何修改,接收端都能够及时发现,有效地降低了安全风险。

如下图所示,红色部分是 Stream Frame 的报文头部,有认证。绿色部分是报文内容,全部经过加密。

连接迁移

一条 TCP 连接 [17] 是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。

比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。

又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。

针对 TCP 的连接变化,MPTCP[5] 其实已经有了解决方案,但是由于 MPTCP 需要操作系统及网络协议栈支持,部署阻力非常大,目前并不适用。

所以从 TCP 连接的角度来讲,这个问题是无解的。

那 QUIC 是如何做到连接迁移呢?很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。

由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。

其他亮点

此外,QUIC 还能实现前向冗余纠错,在重要的包比如握手消息发生丢失时,能够根据冗余信息还原出握手消息。

QUIC 还能实现证书压缩,减少证书传输量,针对包头进行验证等。

限于篇幅,本文不再详细介绍,有兴趣的可以参考文档 [23] 和文档 [4] 和文档 [26]。

服务器开发自我修养专栏-TCP详解

Posted on 02-16-2021 | In Self-cultivation

TCP

包头长度 20个字节

三次握手

如果第三次握手的ack丢失了咋办

当客户端收到服务端的SYNACK应答后,其状态变为ESTABLISHED,并会发送ACK包给服务端,准备发送数据了。如果此时ACK在网络中丢失(如上图所示),过了超时计时器后,那么服务端会重新发送SYNACK包,重传次数根据/proc/sys/net/ipv4/tcp_synack_retries来指定,默认是5次。如果重传指定次数到了后,仍然未收到ACK应答,那么一段时间后,Server自动关闭这个连接。

问题就在这里,客户端已经认为连接建立,而服务端则可能处在SYN-RCVD或者CLOSED,接下来我们需要考虑这两种情况下服务端的应答:

  • 服务端处于CLOSED,当接收到连接已经关闭的请求时,服务端会返回RST 报文,客户端接收到后就会关闭连接,如果需要的话则会重连,那么那就是另一个三次握手了。
  • 服务端处于SYN-RCVD,此时如果接收到正常的ACK 报文,那么很好,连接恢复,继续传输数据;如果接收到写入数据等请求呢?注意了,此时写入数据等请求也是带着ACK 报文的,实际上也能恢复连接,使服务器恢复到ESTABLISHED状态,继续传输数据。

SYN-Flood与SYN-Cookie

所谓SYN-Flood(SYN 洪泛攻击),就是利用SYNACK 报文的时候,服务器会为客户端请求分配缓存,那么黑客(攻击者),就可以使用一批虚假的ip向服务器大量地发建立TCP 连接的请求,服务器为这些虚假ip分配了缓存后,处在SYN_RCVD状态,存放在半连接队列中;另外,服务器发送的请求又不可能得到回复(ip都是假的,能回复就有鬼了),只能不断地重发请求,直到达到设定的时间/次数后,才会关闭。

服务器不断为这些半开连接分配资源,导致服务器的连接资源被消耗殆尽,不过所幸,我们可以使用SYN Cookie进行稍微的防御一下。

所谓的SYN Cookie防御系统,与前面接收到SYN 报文就分配缓存不同,此时暂不分配资源;同时利用SYN 报文的源和目的地IP和端口,以及服务器存储的一个秘密数,使用它们进行散列,得到server_isn作为服务端的初始 TCP 序号,也就是所谓的SYN cookie, 然后将SYNACK 报文中发送给客户端,接下来就是对ACK 报文进行判断,如果其返回的ack里的确认号正好等于server_isn + 1,说明这是一个合法的ACK,那么服务器才会为其生成一个具有套接字的全开的连接。(有点类似于JWT那一套机制哈)

缺点:

  • 增加了密码学运算, 增大了cpu消耗
  • 因为没有保存半连接状态, 所以无法存储一些比如大窗口/sack等信息

四次挥手

timewait的意义

  • 2msl之后网络中的数据分节全部消失, 防止影响到复用了原端口ip的新连接
  • 如果b没收到最后一个ack, b就会重发fin, a如果不维护一个timewait却收到了一个fin会感觉莫名其妙然后响应一个rst, 然后b就会解释为一个错误

timewait和closewait太多咋办

  • timewait太多咋办?
    • net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
    • net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
    • net.ipv4.tcp_fin_timeout这个时间可以减少在异常情况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。
  • closewait太多咋办?
    • 解决方案只有: 查代码. 因为如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出fin信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者由于什么逻辑bug导致服务端没有主动发起close, 或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。

tcp拥塞控制

  • 快速重传:
    报文段1成功接收并被确认ACK 2,接收端的期待序号为2,当报文段2丢失,报文段3失序到来,与接收端的期望不匹配,接收端重复发送冗余ACK 2。这样,如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK(其实是收到4个同样的ACK,第一个是正常的,后三个才是冗余的),发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,不需要等待超时重传定时器溢出,大大提高了效率。这便是快速重传机制。
  • 快速恢复
  • 慢启动
  • 拥塞避免

tcp滑动窗口

每个TCP连接的两端都维护一组窗口:发送窗口结构(send window structure)和接收窗口结构(receive window structure)。TCP以字节为单位维护其窗口结构。TCP头部中的窗口大小字段相对ACK号有一个字节的偏移量。发送端计算其可用窗口,即它可以立即发送的数据量。可用窗口(允许发送但还未发送)计算值为提供窗口(即由接收端通告的窗口)大小减去在传(已发送但未得到确认)的数据量。图中P1、P2、P3分别记录了窗口的左边界、下次发送的序列号、右边界。

如上图所示, 随着发送端接收到返回的数据ACK,滑动窗口也随之右移。发送端根据接收端返回的ACK可以得到两个重要的信息:一是接收端期望收到的下一个字节序号;二是当前的窗口大小(再结合发送端已有的其他信息可以得出还能发送多少字节数据)。

需要注意的是:发送窗口的左边界只能右移,因为它控制的是已发送并受到确认的数据,具有累积性,不能返回;右边界可以右移也可以左移(能左移的右边界会带来一些缺陷,下文会讲到)。

接收端也维护一个窗口结构,但比发送窗口简单(只有左边界和右边界)。该窗口结构记录了已接收并确认的数据,以及它能够接收的最大序列号,该窗口能保证接收数据的正确性(避免存储重复的已接收和确认的数据,以及避免存储不应接收的数据)。由于TCP的累积ACK特性,只有当到达数据序列号等于左边界时,窗口才能向前滑动。

零窗口与TCP持续计时器

Zero Window

上图,我们可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP Sliding Window给降成0的。此时,你一定会问,如果Window变成0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成“Window Closed”,那你一定还会问,如果发送端不发数据了,接收方一会儿Window size 可用了,怎么通知发送端呢?

解决这个问题,TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,第次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。

ACK延迟确认机制

接收方在收到数据后,并不会立即回复ACK,而是延迟一定时间。一般ACK延迟发送的时间为200ms,但这个200ms并非收到数据后需要延迟的时间。系统有一个固定的定时器每隔200ms会来检查是否需要发送ACK包。这样做有两个目的。

  • 这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
  • 如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。

服务器开发自我修养专栏-Linux高低精度定时器实现原理

Posted on 02-06-2021 | In Self-cultivation

Linux定时器实现原理

时间轮定时器-低分辨率实现

Linux 2.6.16之前,内核只支持低精度时钟,内核定时器的工作方式:

  1. 系统启动后,会读取时钟源设备(RTC,HPET,PIT…),初始化当前系统时间。
  2. 内核会根据HZ(系统定时器频率,节拍率)参数值,设置时钟事件设备,启动tick(节拍)中断。HZ表示1秒种产生多少个时钟硬件中断,tick就表示连续两个中断的间隔时间。
  3. 设置时钟事件设备后,时钟事件设备会定时产生一个tick中断,触发时钟中断处理函数,更新系统时钟,并检测timer wheel,进行超时事件的处理。

在上面工作方式下,Linux 2.6.16 之前,内核软件定时器采用timer wheel多级时间轮的实现机制,维护操作系统的所有定时事件。timer wheel的触发是基于系统tick周期性中断。所以说这之前,linux只能支持ms级别的时钟,随着时钟源硬件设备的精度提高和软件高精度计时的需求,有了高精度时钟的内核设计。

所谓低分辨率定时器,是指这种定时器的计时单位基于jiffies值的计数,也就是说,它的精度只有1HZ,假如你的内核配置的HZ是1000,那意味着系统中的低分辨率定时器的精度就是1ms。早期的内核版本中,内核并不支持高精度定时器,理所当然只能使用这种低分辨率定时器, 后来随着时钟源硬件设备的精度提高和软件高精度计时的需求,才有了高精度时钟的内核设计

. . .

服务器开发自我修养专栏-MySQL原理

Posted on 01-24-2021 | In Self-cultivation

MySQL

参考网址

  • https://chenjiabing666.github.io/2020/04/20/Mysql%E6%9C%80%E5%85%A8%E9%9D%A2%E8%AF%95%E6%8C%87%E5%8D%97/
  • https://blog.csdn.net/qq_41011723/article/details/105953813
  • https://blog.csdn.net/qq_41011723/article/details/106028153
  • MySQL、MongoDB、Redis 数据库之间的区别

mysql一条语句的执行过程

速记: 连/分/优/执/存

char和varchar的区别是什么

  • char(n) :固定长度类型,比如订阅 char(10),当你输入”abc”三个字符的时候,它们占的空间还是 10 个字节,其他 7 个是空字节。
    • chat 优点:效率高;缺点:占用空间;适用场景:存储密码的 md5 值,固定长度的,使用 char 非常合适。
  • varchar(n) :可变长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度。

所以,从空间上考虑 varcahr 比较合适;从效率上考虑 char 比较合适,二者使用需要权衡。

redo log与binlog与undo log的区别

参考 https://www.cnblogs.com/Java3y/p/12453755.html , 写的非常好
也可参考 https://www.jianshu.com/p/68d5557c65be

redo log

redo log 存在于InnoDB 引擎中,InnoDB引擎是以插件形式引入Mysql的,redo log的引入主要是为了实现Mysql的crash-safe能力。
实际上Mysql的基本存储结构是页(记录都存在页里边),所以MySQL是先把这条记录所在的页找到,然后把该页加载到内存中,将对应记录进行修改。
现在就可能存在一个问题:如果在内存中把数据改了,还没来得及落磁盘,而此时的数据库挂了怎么办?显然这次更改就丢了。

如果每个请求都需要将数据立马落磁盘之后,那速度会很慢,MySQL可能也顶不住。所以MySQL是怎么做的呢?
MySQL引入了redo log,内存写完了,然后会写一份redo log,这份redo log记载着这次在某个页上做了什么修改.其实写redo log的时候,也会有buffer,是先写buffer,再真正落到磁盘中的。至于从buffer什么时候落磁盘,会有配置供我们配置。

写redo log也是需要写磁盘的,但它的好处就是顺序IO(我们都知道顺序IO比随机IO快非常多)。

所以,redo log的存在为了:当我们修改的时候,写完内存了,但数据还没真正写到磁盘的时候。此时我们的数据库挂了,我们可以根据redo log来对数据进行恢复。因为redo log是顺序IO,所以写入的速度很快,并且redo log记载的是物理变化(xxxx页做了xxx修改),文件的体积很小,恢复速度很快

binlog

binlog记录了数据库表结构和表数据变更,比如update/delete/insert/truncate/create。它不会记录select(因为这没有对表没有进行变更)
binlog我们可以简单理解为:存储着每条变更的SQL语句

undo log

undo log主要有两个作用:

  • 回滚
  • 多版本控制(MVCC)

在数据修改的时候,不仅记录了redo log,还记录undo log,如果因为某些原因导致事务失败或回滚了,可以用undo log进行回滚
undo log主要存储的也是逻辑日志,比如我们要insert一条数据了,那undo log会记录的一条对应的delete日志。我们要update一条记录时,它会记录一条对应相反的update记录。

这也应该容易理解,毕竟回滚嘛,跟需要修改的操作相反就好,这样就能达到回滚的目的。因为支持回滚操作,所以我们就能保证:“一个事务包含多个操作,这些操作要么全部执行,要么全都不执行”。【原子性】

因为undo log存储着修改之前的数据,相当于一个前版本,MVCC实现的是读写不阻塞,读的时候只要返回前一个版本的数据就行了。

undolog和binlog和redolog不同之处总结

  • 参考 https://www.jianshu.com/p/68d5557c65be
  • redo log: 只存在于innodb引擎中
    物理格式的日志,记录的是物理数据页面的修改的信息(数据库中每个页的修改),面向的是表空间、数据文件、数据页、偏移量等。
  • undo log
    逻辑格式的日志,在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,与redo log不同。
  • binlog
    • 逻辑格式的日志,可以简单认为就是执行过的事务中的sql语句。
    • 但又不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息。比如delete操作的话,就对应着delete本身和其反向的insert/update操作的话,就对应着update执行前后的版本的信息;insert操作则对应着delete和insert本身的信息。
    • 因此可以基于binlog做到闪回功能。
  • binlog可以作为恢复数据使用,主从复制搭建,redo log作为异常宕机或者介质故障后的数据恢复使用。
  • redo log是在InnoDB存储引擎层产生,而binlog是MySQL数据库的上层产生的,并且binlog日志不仅仅针对INNODB存储引擎,MySQL数据库中的任何存储引擎对于数据库的更改都会产生binlog日志。
  • 两种日志记录的内容形式不同。MySQL的binlog是逻辑日志,可以简单认为记录的就是sql语句。而innodb存储引擎层面的redo日志是物理日志, 是数据页面的修改之后的物理记录。
  • 关于事务提交时,redo log和binlog的写入顺序,为了保证主从复制时候的主从一致(当然也包括使用binlog进行基于时间点还原的情况),是要严格一致的,MySQL通过两阶段提交过程来完成事务的一致性的,也即redo log和binlog的一致性的,理论上是先写redo log,再写binlog,两个日志都提交成功(刷入磁盘),事务才算真正的完成。因此redo日志的写盘,并不一定是随着事务的提交才写入redo日志文件的,而是随着事务的开始,逐步开始的。那么当我执行一条 update 语句时,redo log 和 binlog 是在什么时候被写入的呢?这就有了我们常说的「两阶段提交」:
    • 写入:redo log(prepare)
    • 写入:binlog
    • 写入:redo log(commit)
  • 两种日志与记录写入磁盘的时间点不同,binlog日志只在事务提交完成后进行一次写入。而innodb存储引擎的redo日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的。
  • binlog日志仅在事务提交时记录,并且对于每一个事务,仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志。而对于innodb存储引擎的redo日志,由于其记录是物理操作日志,因此每个事务对应多个日志条目,并且事务的redo日志写入是并发的,并非在事务提交时写入,其在文件中记录的顺序并非是事务开始的顺序。
  • binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redo log是循环使用。
  • binlog 日志是 master 推的还是 salve 来拉的?slave来拉的, 因为每一个slave都是完全独立的个体,所以slave完全依据自己的节奏去处理同步,

二阶段提交

redo log 保证的是数据库的 crash-safe 能力。采用的策略就是常说的“两阶段提交”。

一条update的SQL语句是按照这样的流程来执行的:
将数据页加载到内存 → 修改数据 → 更新数据 → 写redo log(状态为prepare) → 写binlog → 提交事务(数据写入成功后将redo log状态改为commit)

只有当两个日志都提交成功(刷入磁盘),事务才算真正的完成。一旦发生系统故障(不管是宕机、断电、重启等等),都可以配套使用 redo log 与 binlog 做数据修复。

两阶段提交机制的必要性

  • binlog 存在于Mysql Server层中,主要用于数据恢复;当数据被误删时,可以通过上一次的全量备份数据加上某段时间的binlog将数据恢复到指定的某个时间点的数据。
  • redo log 存在于InnoDB 引擎中,InnoDB引擎是以插件形式引入Mysql的,redo log的引入主要是为了实现Mysql的crash-safe能力。

假设redo log和binlog分别提交,可能会造成用日志恢复出来的数据和原来数据不一致的情况。

  • 假设先写redo log再写binlog,即redo log没有prepare阶段,写完直接置为commit状态,然后再写binlog。那么如果写完redo log后Mysql宕机了,重启后系统自动用redo log 恢复出来的数据就会比binlog记录的数据多出一些数据,这就会造成磁盘上数据库数据页和binlog的不一致,下次需要用到binlog恢复误删的数据时,就会发现恢复后的数据和原来的数据不一致。
  • 假设先写binlog再写redolog。如果写完binlog后Mysql宕机了,那么binlog上的记录就会比磁盘上数据页的记录多出一些数据出来,下次用binlog恢复数据,就会发现恢复后的数据和原来的数据不一致。

由此可见,redo log和binlog的两阶段提交是非常必要的。

索引

  • 聚集索引(也叫聚簇索引)是啥
    • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
    • 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。
  • 外键是啥: 比如在students表中,通过class_id的字段,可以把数据与另一张表关联起来,这种列称为外键(一般不用外键, 因为会降低数据库性能)
  • mysql 索引在什么情况下会失效
    • https://database.51cto.com/art/201912/607742.htm
    • 查询条件包含or,可能导致索引失效
    • 如何字段类型是字符串,where时一定用引号括起来,否则索引失效
    • like通配符可能导致索引失效。
    • 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
    • 在索引列上使用mysql的内置函数,索引失效
    • 对索引列运算(如,+、-、*、/),索引失效。
    • 索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。
    • 索引字段上使用is null, is not null,可能导致索引失效。
    • 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
    • mysql估计使用全表扫描要比使用索引快,则不使用索引。
  • mysql 的索引模型:
    在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

为什么说B类树更适合数据库索引

为什么说B类树更适合数据库索引

mysql全文索引 pending_fin

mysql 有那些存储引擎,有哪些区别

  • innodb是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ Next-Key Locking 防止幻影读。主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。
  • MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。
  • MyISAM类型的表强调的是性能,其执行速度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持以及外部键等高级数据库功能。
  • 现在一般都是选用InnoDB了,InnoDB支持行锁, 而MyISAM的全表锁,myisam的读写串行问题,并发效率锁表,效率低,MyISAM对于读写密集型应用一般是不会去选用的
  • memory引擎一般用于临时表, 使用表级锁,没有事务机制, 虽然内存访问快,但如果频繁的读写,表级锁会成为瓶颈, 且内存昂贵..满了就亏了
  • InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

综上所述, 如果表的读操作远远多于写操作时,并且不需要事务的支持的,可以将 MyIASM 作为数据库引擎的首选

mysql 主从同步分哪几个过程


复制的基本过程如下:

  1. 从节点上的I/O 线程连接主节点,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
  2. 主节点接收到来自从节点的I/O请求后,通过负责复制的I/O线程根据请求信息读取指定日志指定位置之后的日志信息,返回给从节点。返回信息中除了日志所包含的信息之外,还包括本次返回的信息的bin-log file 的以及bin-log position;从节点的I/O线程接收到内容后,将接收到的日志内容更新到本机的relay log中,并将读取到的binary log文件名和位置保存到master-info 文件中,以便在下一次读取的时候能够清楚的告诉Master“我需要从某个bin-log 的哪个位置开始往后的日志内容,请发给我”;
  3. Slave 的 SQL线程检测到relay-log 中新增加了内容后,会将relay-log的内容解析成在主节点上实际执行过的操作,并在本数据库中执行。

主从同步延迟与同步数据丢失问题

主库将变更写binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。

这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。
所以mysql实际上在这一块有两个机制:

  • 一个是半同步复制,用来解决主库数据丢失问题
  • 一个是并行复制,用来解决主从同步延时问题(实在解决不了只能强制读主库)。

半同步复制(Semisynchronous replication)

  • 逻辑上: 是介于全同步复制与全异步复制之间的一种,主库只需要等待至少一个从库节点收到并且 Flush Binlog 到 Relay Log 文件即可,主库不需要等待所有从库给主库反馈。同时,这里只是一个收到的反馈,而不是已经完全完成并且提交的反馈,如此,节省了很多时间。
  • 技术上: 介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到relay log中才返回给客户端。相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟最少是一个TCP/IP往返的时间。所以,半同步复制最好在低延时的网络中使用。

库并行复制

所谓并行复制,指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

异步复制(Asynchronous replication)

  • 逻辑上: MySQL默认的复制即是异步的,主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理,这样就会有一个问题,主如果crash掉了,此时主上已经提交的事务可能并没有传到从库上,如果此时,强行将从提升为主,可能导致新主上的数据不完整。
  • 技术上: 主库将事务 Binlog 事件写入到 Binlog 文件中,此时主库只会通知一下 Dump 线程发送这些新的 Binlog,然后主库就会继续处理提交操作,而此时不会保证这些 Binlog 传到任何一个从库节点上。

全同步复制(Fully synchronous replication)

  • 逻辑上: 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。
  • 技术上: 当主库提交事务之后,所有的从库节点必须收到、APPLY并且提交这些事务,然后主库线程才能继续做后续操作。但缺点是,主库完成一个事务的时间会被拉长,性能降低。

乐观锁与悲观锁的区别?

  • 悲观锁:认为数据随时会被修改,因此每次读取数据之前都会上锁,防止其它事务读取或修改数据;应用于数据更新比较频繁的场景;
  • 乐观锁:操作数据时不会上锁,但是更新时会判断在此期间有没有别的事务更新这个数据,若被更新过,则失败重试;适用于读多写少的场景。

乐观锁怎么实现:

  • 加版本号
  • cas

实现事务采取了哪些技术以及思想?

  • ★ a原子性:使用 undo log ,从而达到回滚
  • ★ d持久性:使用 redo log,从而达到故障后恢复
  • ★ i隔离性:使用锁以及MVCC,运用的优化思想有读写分离,读读并行,读写并行
  • ★ c一致性:通过回滚,以及恢复,和在并发环境下的隔离做到一致性。

mysql四个事务隔离级别

四个隔离级别的区别以及每个级别可能产生的问题以及实现原理
MySQL 的事务隔离是在 MySQL. ini 配置文件里添加的,在文件的最后添加:transaction-isolation = REPEATABLE-READ
可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。

MySQL的事务隔离级别一共有四个,分别是

  • 读未提交
  • 读已提交
  • 可重复读
  • 可串行化

MySQL的隔离级别的作用就是让事务之间互相隔离,互不影响,这样可以保证事务的一致性。

  • 隔离级别比较:可串行化>可重复读>读已提交>读未提交
  • 隔离级别对性能的影响比较:可串行化>可重复读>读已提交>读未提交

由此看出,隔离级别越高,所需要消耗的MySQL性能越大(如事务并发严重性),为了平衡二者,一般建议设置的隔离级别为可重复读,MySQL默认的隔离级别也是可重复读。

事务并发可能出现的情况

  • 脏读(Dirty Read)
    • 一个事务读到了另一个未提交事务修改过的数据
    • 会话B开启一个事务,把id=1的name为武汉市修改成温州市,此时另外一个会话A也开启一个事务,读取id=1的name,此时的查询结果为温州市,会话B的事务最后回滚了刚才修改的记录,这样会话A读到的数据是不存在的,这个现象就是脏读。(脏读只在读未提交隔离级别才会出现)
  • 不可重复读(Non-Repeatable Read)
    • 一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。(不可重复读在读未提交和读已提交隔离级别都可能会出现)
    • 会话A开启一个事务,查询id=1的结果,此时查询的结果name为武汉市。接着会话B把id=1的name修改为温州市(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),此时会话A的事务再一次查询id=1的结果,读取的结果name为温州市。会话B再此修改id=1的name为杭州市,会话A的事务再次查询id=1,结果name的值为杭州市,这种现象就是不可重复读。
  • 幻读(Phantom)
    • 一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。(幻读在读未提交、读已提交、可重复读隔离级别都可能会出现)
    • 会话A开启一个事务,查询id>0的记录,此时会查到name=武汉市的记录。接着会话B插入一条name=温州市的数据(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),这时会话A的事务再以刚才的查询条件(id>0)再一次查询,此时会出现两条记录(name为武汉市和温州市的记录),这种现象就是幻读。

各个隔离级别的详细说明

  • 读未提交(READ UNCOMMITTED)
    • 在读未提交隔离级别下,事务A可以读取到事务B修改过但未提交的数据。
    • 可能发生脏读、不可重复读和幻读问题,一般很少使用此隔离级别。
  • 读已提交(READ COMMITTED)
    • 在读已提交隔离级别下,事务B只能在事务A修改过并且已提交后才能读取到事务B修改的数据。
    • 读已提交隔离级别解决了脏读的问题,但可能发生不可重复读和幻读问题,一般很少使用此隔离级别。
  • 可重复读(REPEATABLE READ)
    • 在可重复读隔离级别下,事务B只能在事务A修改过数据并提交后,自己也提交事务后,才能读取到事务B修改的数据。
    • 可重复读隔离级别解决了脏读和不可重复读的问题,但可能发生幻读问题。
    • 提问:为什么上了写锁(写操作),别的事务还可以读操作?因为InnoDB有MVCC机制(多版本并发控制),可以使用快照读,而不会被阻塞。
  • 可串行化(SERIALIZABLE)
    • 各种问题(脏读、不可重复读、幻读)都不会发生,通过加锁实现(读锁和写锁)。

mvcc是啥

mvcc必看文章:

  • mysql mvcc实现原理
  • https://chenjiayang.me/2019/06/22/mysql-innodb-mvcc/

MVCC (Multiversion Concurrency Control) 中文全程叫多版本并发控制,是现代数据库(包括 MySQL、Oracle、PostgreSQL 等)引擎实现中常用的处理读写冲突的手段,目的在于提高数据库高并发场景下的吞吐性能。MVCC 的每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适(要么是最新版本,要么是指定版本)的结果直接返回 。通过这种方式,我们就不需要关注读写操作之间的数据冲突

每条记录在更新的时候都会同时记录一条回滚操作(回滚操作日志undo log)。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。即通过回滚(rollback操作),可以回到前一个状态的值。InnoDB 为了解决这个问题,设计了 ReadView(可读视图)的概念.

如此一来不同的事务在并发过程中,SELECT 操作可以不加锁而是通过 MVCC 机制读取指定的版本历史记录,并通过一些手段保证保证读取的记录值符合事务所处的隔离级别,从而解决并发场景下的读写冲突。

mysql把每个操作都定义成一个事务,每开启一个事务,系统的事务版本号自动递增。每行记录都有两个隐藏列:创建版本号和删除版本号

ReadView

  • 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号 TRX_ID :事务开始时的系统版本号。

MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, …},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。

在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用:

  • TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。
  • TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。
  • TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
    • 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
    • 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。

在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。

mysql在可重复读RR的隔离级别下如何避免幻读的

参考:

  • next-key锁: mysql 排它锁之行锁、间隙锁、后码锁
  • mvcc:
    • mysql mvcc实现原理
    • https://blog.csdn.net/DILIGENT203/article/details/100751755

知识点:

  • Record Lock:行锁, 锁直接加在索引记录上面,锁住的是key。当需要对表中的某条数据进行写操作(insert、update、delete、select for update)时,需要先获取记录的排他锁(X锁),这个就称为行锁。
  • Gap Lock:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而设计的。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。例如当一个事务执行语句SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;,则其它事务就不能在 t.c 中插入 15。
  • Next-Key Lock = Record Lock + Gap Lock , 它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:
    (-∞, 10]
    (10, 11]
    (11, 13]
    (13, 20]
    (20, +∞)

默认情况下,InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上Record Lock,再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。

  • 快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
    select * from table where ?;
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert into table values (…);
    • update table set ? where ?;
    • delete from table where ?;

在MySQL中select称为快照读,不需要锁,而insert、update、delete、select for update则称为当前读,需要给数据加锁,幻读中的“读”即是针对当前读。
RR事务隔离级别允许存在幻读,但InnoDB RR级别却通过Gap锁避免了幻读

mysql如何实现避免幻读:

  1. 在快照读读情况下,mysql通过mvcc来避免幻读。
  2. 在当前读读情况下,mysql通过next-key lock来避免幻读。

正确理解InnoDB引擎RR隔离级别解决了幻读这件事

Mysql官方给出的幻读解释是:只要在一个事务中,第二次select多出了row就算幻读。

先看问题:
a事务先select,b事务insert确实会加一个gap锁,但是如果b事务commit,这个gap锁就会释放(释放后a事务可以随意dml操作),a事务再select出来的结果在MVCC下还和第一次select一样,接着a事务不加条件地update,这个update会作用在所有行上(包括b事务新加的),a事务再次select就会出现b事务中的新行,并且这个新行已经被update修改了,实测在RR级别下确实如此。

如果这样理解的话,Mysql的RR级别确实防不住幻读, 但是我们不能向上面这样理解, 我们得如下理解:

  • select * from t where a=1;属于快照读
  • select * from t where a=1 lock in share mode;属于当前读
  • 不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用。所以mysql的rr级别是解决了幻读的。
  • 如上面问题所说,T1 select 之后 update,会将 T2 中 insert 的数据一起更新,那么认为多出来一行,所以防不住幻读。看着说法无懈可击,但是其实是错误的,InnoDB 中设置了快照读和当前读两种模式,如果只有快照读,那么自然没有幻读问题,但是如果将语句提升到当前读,那么 T1 在 select 的时候需要用如下语法: select * from t for update (lock in share mode) 进入当前读,那么自然没有 T2 可以插入数据这一回事儿了。

服务器开发自我修养专栏-HTTP深入浅出

Posted on 01-12-2021 | In Self-cultivation

HTTP与HTTPS

HTTP 协议考察 HTTP 协议的返回码、HTTP 的方法等。需要特别指出的是 HTTPS 加密的详细过程要非常透彻,不然容易产生一种感觉好像都清楚了,但是一问就有点说不清楚。

下面实例是一点典型的使用GET来传递数据的实例,
客户端请求:

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

服务端响应:

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

输出结果:

Hello World! My payload includes a trailing CRLF.

客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:

  • 请求行(request line)
  • 请求头部(header)
  • 空行
  • 请求数据

由四个部分组成,下图给出了请求报文的一般格式。

服务器响应消息

HTTP响应也由四个部分组成,分别是:

  • 状态行
  • 消息报头
  • 空行
  • 响应正文

https

HTTPS 协议(HyperText Transfer Protocol over Secure Socket Layer):一般理解为HTTP+SSL/TLS,通过 SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密。

那么SSL/TLS又是什么?

  • SSL(Secure Socket Layer,安全套接字层):1994年为 Netscape 所研发,SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。
  • TLS(Transport Layer Security,传输层安全):其前身是 SSL,它最初的几个版本(SSL 1.0、SSL 2.0、SSL 3.0)由网景公司开发,1999年从 3.1 开始被 IETF 标准化并改名,发展至今已经有 TLS 1.0、TLS 1.1、TLS 1.2 三个版本。SSL3.0和TLS1.0由于存在安全漏洞,已经很少被使用到。TLS 1.3 改动会比较大,目前还在草案阶段,目前使用最广泛的是TLS 1.1、TLS 1.2。

https 不是一种新的协议,只是 http 的通信接口部分使用了 ssl 和 tsl 协议替代,加入了加密、证书、完整性保护的功能,下面解释一下加密和证书,如下图所示

对称加密

也叫共享密钥加密, 加密和解密公用一套秘钥,这样就会产生问题,已共享秘钥加密方式必须将秘钥传送给对方,但如果通信被监听,那么秘钥可能会被泄漏产生危险。
常见对称加密算法有des, aes

非对称加密

也叫公开秘钥加密, 使用一种非对称加密的算法,使用一对非对称的秘钥,一把叫做公有秘钥,一把叫做私有秘钥,在加密的时候,通信的一方使用公有秘钥进行加密,通信的另一方使用私有秘钥进行解密,利用这种方式不需要发送私有秘钥,也就不存在泄漏的风险了。
常见非对称加密算法有rsa

https 加密方式

因为公开秘钥加密的方式比共享秘钥加密的方式钥消耗 cpu 资源,https 采取了混合加密的方式,来结合两者的优点。

在秘钥交换阶段使用公开加密的方式,之后建立连接后使用共享秘钥加密方式进行加密,如下图。

为什么要使用证书

因为公开加密还存在一些问题就是无法证明公开秘钥的正确性(有可能被黑客中间替换成了黑客自己的公钥, 然后黑客伪装成服务器/客户端做中间转发),为了解决这个问题,https 采取了有数字证实认证机构和其相关机构颁发的公开秘钥证书,通信过程如下图所示。

解释一下上图的步骤:

  1. 服务器将自己的公开秘钥传到数字证书认证机构
  2. 数字证书认证机构使用自己的秘钥来对传来的服务器公钥进行加密,并颁发数字证书
  3. 服务器将传回的公钥证书发送给客户端,客户端使用数字机构颁发的公开秘钥来验证证书的有效性,以及公开秘钥的真实性
    • 证书签名是先将证书信息(证书机构名称、有效期、拥有者、拥有者公钥)进行hash,再用CA的私有密钥对hash值加密而生成的。
    • 所以拦截者虽然可以拦截并篡改证书信息(主要是拥有者和拥有者的公钥),但是由于拦截者没有CA的私钥,所以无法生成正确的签名,从而导致客户端拿到签名后,用CA公有密钥对证书签名解密后值与用证书计算出来的实际hash值不一样,从而得不到客户端信任。(其实这个ca公钥和私钥也就是非对称加密的思想了)
  4. 客户端使用服务器的公开秘钥进行消息加密,后发送给服务器。
  5. 服务器使用私有秘钥进行解密。

浏览器在安装的时候会内置可信的数字证书机构的公开秘钥,如下图所示。

这就是为什么我们使用自己生成的证书的时候会产生安全警告的原因。

再附一张 https 的具体通信步骤和图解。

cookie

服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。

HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。

GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。

Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F (“/“) 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:

  • /docs
  • /docs/Web/
  • /docs/Web/HTTP

session如何保存较好

一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作..有什么好的解决方案呢?
Session Server使用一个单独的服务器存储 Session 数据,可以使用传统的 MySQL,也使用 Redis 或者 Memcached 这种内存型数据库。

  • 优点:
    为了使得大型网站具有伸缩性,集群中的应用服务器通常需要保持无状态,那么应用服务器不能存储用户的会话信息。Session Server 将用户的会话信息单独进行存储,从而保证了应用服务器的无状态。
  • 缺点:
    需要去实现存取 Session 的代码

cookie和session和token的区别

  • 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
  • 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
  • Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。所以,总结一下:Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
  • 为什么需要token来替代session机制? 因为session的存储对服务器说是一个巨大的开销, 严重的限制了服务器扩展能力, 比如说我用两个机器组成了一个集群, 小 F 通过机器 A 登录了系统, 那 session id 会保存在机器 A 上, 假设小 F 的下一次请求被转发到机器 B 怎么办? 机器 B 可没有小 F 的 session id 啊。有时候会采用一点小伎俩: session sticky , 就是让小 F 的请求一直粘连在机器 A 上, 但是这也不管用, 要是机器 A 挂掉了, 还得转到机器 B 去。

接下来我们介绍事实上的token标准JWT

JWT

sessionId 的方式本质是把用户状态信息维护在 server 端,token 的方式就是把用户的状态信息加密成一串 token 传给前端,然后每次发请求时把 token 带上,传回给服务器端;服务器端收到请求之后,解析 token 并且验证相关信息(用jwt的header里的加密方式然后根据自己的不公开的密钥把jwt中的payload用加密一下得到一个签名 s, 然后用s对比看看是不是跟jwt里的signature相等, 相等则说明token对了);

备注: 对于数据校验,专门的消息认证码生成算法, HMAC - 一种使用单向散列函数构造消息认证码的方法,其过程是不可逆的、唯一确定的,并且使用密钥来生成认证码,其目的是防止数据在传输过程中被篡改或伪造。将原始数据与认证码一起传输,数据接收端将原始数据使用相同密钥和相同算法再次生成认证码,与原有认证码进行比对,校验数据的合法性。

所以跟第一种登录方式最本质的区别是:通过解析 token 的计算时间换取了 session 的存储空间

业界通用的加密方式是 jwt, jwt 的具体格式如图:

简单的介绍一下 jwt,它主要由 3 部分组成:

header 头部
{
"alg": "HS256",
"typ": "JWT"
}
payload 负载
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1555341649998
}
signature 签名
{
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
) secret base64 encoded
}

  • header
    header 里面描述加密算法和 token 的类型,类型一般都是 JWT;
  • payload
    里面放的是用户的信息,也就是第一种登录方式中需要维护在服务器端 session 中的信息;
  • signature
    是对前两部分的签名,也可以理解为加密;实现需要一个密钥(secret),这个 secret 只有服务器才知道,然后使用 header 里面的算法按照如下方法来加密:
    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

总之,最后的 jwt = base64url(header) + "." + base64url(payload) + "." + signature
jwt 可以放在 response 中返回,也可以放在 cookie 中返回,这都是具体的返回方式,并不重要。
客户端发起请求时,官方推荐放在 HTTP header 中:

Authorization: Bearer <token>

这样子确实也可以解决 cookie 跨域(比如移动平台上对cookie支持不好)的问题,不过具体放在哪儿还是根据业务场景来定,并没有一定之规。

jwt过期了如何刷新

前面讲的 Token,都是 Access Token,也就是访问资源接口时所需要的 Token,还有另外一种 Token,Refresh Token,通常情况下,Refresh Token 的有效期会比较长,而 Access Token 的有效期比较短,当 Access Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Access Token,如果 Refresh Token 也失效了,用户就只能重新登录了。

在 JWT 的实践中,引入 Refresh Token,将会话管理流程改进如下:

  1. 客户端使用用户名密码进行认证
  2. 服务端生成有效时间较短的 Access Token(例如 10 分钟),和有效时间较长的 Refresh Token(例如 7 天)
  3. 客户端访问需要认证的接口时,携带 Access Token
  4. 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据
  5. 如果携带 Access Token 访问需要认证的接口时鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token
  6. 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token
  7. 客户端使用新的 Access Token 访问需要认证的接口

常见的HTTP相应状态码

总之:(一般标准用法是这样用哈, 但是真的写代码的时候其实跟get/post/put一样, 想怎么用全看自己, 前后端开发人员协商好就行)

  • 1XX:消息
  • 2XX:成功
  • 3XX:重定向
  • 4XX:请求错误
  • 5XX、6XX:服务器错误

常见状态代码、状态描述的说明如下:

  • 200 OK:
    请求已成功,请求所希望的响应头或数据体将随此响应返回。实际的响应将取决于所使用的请求方法。在GET请求中,响应将包含与请求的资源相对应的实体。在POST请求中,响应将包含描述或操作结果的实体。[7]
  • 301 Moved Permanently:
    被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。[19]除非额外指定,否则这个响应也是可缓存的。
    新的永久性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。
    如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。
    注意:对于某些使用HTTP/1.0协议的浏览器,当它们发送的POST请求得到了一个301响应的话,接下来的重定向请求将会变成GET方式。
  • 400 Bad Request
    由于明显的客户端错误(例如,格式错误的请求语法,太大的大小,无效的请求消息或欺骗性路由请求),服务器不能或不会处理该请求。[31]
  • 401 Unauthorized(RFC 7235)
    参见:HTTP基本认证、HTTP摘要认证
    类似于403 Forbidden,401语义即“未认证”,即用户没有必要的凭据。[32]该状态码表示当前请求需要用户验证。该响应必须包含一个适用于被请求资源的WWW-Authenticate信息头用以询问用户信息。客户端可以重复提交一个包含恰当的Authorization头信息的请求。[33]如果当前请求已经包含了Authorization证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。
    注意:当网站(通常是网站域名)禁止IP地址时,有些网站状态码显示的401,表示该特定地址被拒绝访问网站。
  • 403 Forbidden
    主条目:HTTP 403
    服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息。
  • 404 Not Found
    主条目:HTTP 404
    请求失败,请求所希望得到的资源未被在服务器上发现,但允许用户的后续请求。[35]没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。
  • 500 Internal Server Error
    通用错误消息,服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。没有给出具体错误信息。[59]
  • 503 Service Unavailable
    由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是暂时的,并且将在一段时间以后恢复。[62]如果能够预计延迟时间,那么响应中可以包含一个Retry-After头用以标明这个延迟时间。如果没有给出这个Retry-After信息,那么客户端应当以处理500响应的方式处理它。

get和post的本质区别

从设计初衷上来说,GET 用来实现从服务端取数据,POST 用来实现向服务端提出请求对数据做某些修改,也因此如果你向nginx用post请求静态文件,nginx会直接返回 405 not allowed,但是服务端毕竟是人实现的,你可以让 POST 做 GET 相同的事情

get请求的参数一般放在url中,但是浏览器和服务器程序对url长度还是有限制的。
post请求的参数一般放在body,你硬要放到url中也可以。

在RESTful风格中,get用于从服务器获获取数据,而post用于创建数据

Connection: keep-alive

在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。在后来的HTTP/1.0中以及HTTP/1.1中,引入了重用连接的机制,就是在http请求头中加入Connection: keep-alive来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。协议规定HTTP/1.0如果想要保持长连接,需要在请求头中加上Connection: keep-alive,而HTTP/1.1默认是支持长连接的,有没有这个请求头都行。

要实现长连接很简单,只要客户端和服务端都保持这个http长连接即可。但问题的关键在于保持长连接后,浏览器如何知道服务器已经响应完成?在使用短连接的时候,服务器完成响应后即关闭http连接,这样浏览器就能知道已接收到全部的响应,同时也关闭连接(TCP连接是双向的)。

在使用长连接的时候,响应完成后服务器是不能关闭连接的,那么它就要在响应头中加上特殊标志告诉浏览器已响应完成。一般情况下这个特殊标志就是Content-Length,来指明响应体的数据大小,比如Content-Length: 120表示响应体内容有120个字节,这样浏览器接收到120个字节的响应体后就知道了已经响应完成。

由于Content-Length字段必须真实反映响应体长度,但实际应用中,有些时候响应体长度并没那么好获得,例如响应体来自于网络文件,或者由动态语言生成。这时候要想准确获取长度,只能先开一个足够大的内存空间,等内容全部生成好再计算。但这样做一方面需要更大的内存开销,另一方面也会让客户端等更久。这时候Transfer-Encoding: chunked响应头就派上用场了,该响应头表示响应体内容用的是分块传输,此时服务器可以将数据一块一块地分块响应给浏览器而不必一次性全部响应,待浏览器接收到全部分块后就表示响应结束。

以分块传输一段文本内容:“人的一生总是在追求自由的一生 So easy”来说明分块传输的过程,如下图所示:

url编码urlencode是什么

RFC3986文档规定,Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符。
那如何对Url中的非法字符进行编码呢?
  
Url编码通常也被称为百分号编码(Url Encoding,also known as percent-encoding),是因为它的编码方式非常简单,使用%百分号加上两位的字符——0123456789ABCDEF——代表一个字节的十六进制形式。Url编码默认使用的字符集是US-ASCII。例如a在US-ASCII码中对应的字节是0x61,那么Url编码之后得到的就是%61,我们在地址栏上输入http://g.cn/search?q=%61%62%63,

实际上就等同于在google上搜索abc了。又如@符号在ASCII字符集中对应的字节为0x40,经过Url编码之后得到的是%40。
  
  对于非ASCII字符,需要使用ASCII字符集的超集进行编码得到相应的字节,然后对每个字节执行百分号编码。对于Unicode字符,RFC文档建议使用utf-8对其进行编码得到相应的字节,然后对每个字节执行百分号编码。如”中文”使用UTF-8字符集得到的字节为0xE4 0xB8 0xAD 0xE6 0x96 0x87,经过Url编码之后得到”%E4%B8%AD%E6%96%87”。
  
  如果某个字节对应着ASCII字符集中的某个非保留字符,则此字节无需使用百分号表示。例如”Url编码”,使用UTF-8编码得到的字节是0x55 0x72 0x6C 0xE7 0xBC 0x96 0xE7 0xA0 0x81,由于前三个字节对应着ASCII中的非保留字符”Url”,因此这三个字节可以用非保留字符”Url”表示。最终的Url编码可以简化成”Url%E7%BC%96%E7%A0%81” ,当然,如果你用”%55%72%6C%E7%BC%96%E7%A0%81”也是可以的。

很多HTTP监视工具或者浏览器地址栏等在显示Url的时候会自动将Url进行一次解码(使用UTF-8字符集),这就是为什么当你在Firefox中访问Google搜索中文的时候,地址栏显示的Url包含中文的缘故。但实际上发送给服务端的原始Url还是经过编码的。

服务器开发自我修养专栏-Linux网络编程

Posted on 12-28-2020 | In Self-cultivation

Linux网络编程

I/O模式 水平触发 边缘触发
epoll ✓ ✓
select/poll ✓
信号驱动 ✓

select

一个常见的select例子如下:

#include "unp.h"

void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;

stdineof = 0;
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);

if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
}

Write(fileno(stdout), buf, n);
}

if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}

Writen(sockfd, buf, n);
}
}
}

参考select poll epoll的区别

可以看出select的缺点如下:

  • (遍)select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  • fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译
  • (内)内核/用户空间内存拷贝问题,每次调用select都需要将全部描述符从应用进程缓冲区复制到内核缓冲区
  • (数)单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
  • select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO,那么之后再次select调用还是会将这些文件描述符通知进程。
  • 相比于select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

epoll

一个常见的epoll使用例子:

while (numOpenFds > 0) {
/* Fetch up to MAX_EVENTS items from the ready list of the
epoll instance */

printf("About to epoll_wait()\n");
ready = epoll_wait(epfd, evlist, MAX_EVENTS, -1);
if (ready == -1) {
if (errno == EINTR)
continue; /* Restart if interrupted by signal */
else
errExit("epoll_wait");
}

printf("Ready: %d\n", ready);

/* Deal with returned list of events */

for (j = 0; j < ready; j++) {
printf(" fd=%d; events: %s%s%s\n", evlist[j].data.fd,
(evlist[j].events & EPOLLIN) ? "EPOLLIN " : "",
(evlist[j].events & EPOLLHUP) ? "EPOLLHUP " : "",
(evlist[j].events & EPOLLERR) ? "EPOLLERR " : "");

if (evlist[j].events & EPOLLIN) {
s = read(evlist[j].data.fd, buf, MAX_BUF);
if (s == -1)
errExit("read");
printf(" read %d bytes: %.*s\n", s, s, buf);

} else if (evlist[j].events & (EPOLLHUP | EPOLLERR)) {

/* After the epoll_wait(), EPOLLIN and EPOLLHUP may both have
been set. But we'll only get here, and thus close the file
descriptor, if EPOLLIN was not set. This ensures that all
outstanding input (possibly more than MAX_BUF bytes) is
consumed (by further loop iterations) before the file
descriptor is closed. */

printf(" closing fd %d\n", evlist[j].data.fd);
// 关闭一个文件描述符会自动的将其从所有的 epoll 实例的兴趣列表中移除
if (close(evlist[j].data.fd) == -1)
errExit("close");
numOpenFds--;
}
}
}

epoll的设计和实现select完全不同。epoll把原先的select/poll调用分成了3个部分:

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字
  3. 调用epoll_wait收集发生的事件的连接

总结:

  • epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait 便可以得到事件完成的描述符。
  • 从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。
  • epoll 仅适用于 Linux OS。
  • epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。

水平触发与边缘触发的区别

默认情况下 epoll 提供的是水平触发通知.要使用边缘触发通知,我们在调用epoll_ctl()时在ev.events字段中指定EPOLLET标志.

例如 :

struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
errExit("epoll_ctl");

我们通过一个例子来说明epoll的水平触发和边缘触发通知之间的区别。
假设我们使用epoll来监视一个套接字上的输入(EPOLLIN),接下来会发生如下的事件。

  1. 套接字上有输入到来。
  2. 我们调用一次epoll_wait()。无论我们采用的是水平触发还是边缘触发通知,该调用
    都会告诉我们套接字已经处于就绪态了。
  3. 再次调用epoll_wait()。
    • 如果我们采用的是水平触发通知,那么第二个epoll_wait()调用将告诉我们套接字处于就绪态。
    • 而如果我们采用边缘触发通知,那么第二个epoll_wait()调用将阻塞,因为自从上一次调用epoll_wait()以来并没有新的输入到来。边缘触发通知通常和非阻塞的文件描述符结合使用。因而,采用epoll的边缘触发通知机制的程序基本框架如下:
      1. 让所有待监视的文件描述符都成为非阻塞的。
      2. 通过epoll_ctl()构建epoll的兴趣列表。
      3. 通过epoll_wait()取得处于就绪态的描述符列表。
      4. 针对每一个处于就绪态的文件描述符,不断进行I/O处理直到相关的系统调用( 例如read()、write(),recv()、send()或accept() )返回EAGAIN或EWOULDBLOCK错误。

水平触发需要处理的问题

使用linux epoll模型,水平触发模式(Level-Triggered);当socket可写时,会不停的触发socket可写的事件,如何处理?

  • 第一种最普通的方式:
    当需要向socket写数据时,将该socket加入到epoll模型(epoll_ctl);等待可写事件。
    接收到socket可写事件后,调用write()或send()发送数据。。。
    当数据全部写完后, 将socket描述符移出epoll模型。

    这种方式的缺点是: 即使发送很少的数据,也要将socket加入、移出epoll模型。有一定的操作代价。

  • 第二种方式,(是本人的改进方案, 叫做directly-write)
    向socket写数据时,不将socket加入到epoll模型;而是直接调用send()发送;
    只有当或send()返回错误码EAGAIN(系统缓存满),才将socket加入到epoll模型,等待可写事件后(表明系统缓冲区有空间可以写了),再发送数据。
    全部数据发送完毕,再移出epoll模型。

    这种方案的优点: 当用户数据比较少时,不需要epool的事件处理。
    在高压力的情况下,性能怎么样呢?
    对一次性直接写成功、失败的次数进行统计。如果成功次数远大于失败的次数, 说明性能良好。(如果失败次数远大于成功的次数,则关闭这种直接写的操作,改用第一种方案。同时在日志里记录警告)
    在我自己的应用系统中,实验结果数据证明该方案的性能良好。

    事实上,网络数据可分为两种到达/发送情况:
    一是分散的数据包, 例如每间隔40ms左右,发送/接收3-5个 MTU(或更小,这样就没超过默认的8K系统缓存)。
    二是连续的数据包, 例如每间隔1s左右,连续发送/接收 20个 MTU(或更多)。

  • 第三种方式: 使用Edge-Triggered(边沿触发),这样socket有可写事件,只会触发一次。
    可以在应用层做好标记。以避免频繁的调用 epoll_ctl( EPOLL_CTL_ADD, EPOLL_CTL_MOD)。 这种方式是epoll 的 man 手册里推荐的方式, 性能最高。但如果处理不当容易出错,事件驱动停止。

epoll实现细节

epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!

那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。

这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。

select 和 epoll的区别

select函数,必须得清楚select跟linux特有的epoll的区别, 有三点(遍内数):

  • 遍历 : 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd, 每次只需要简单的从列表里取出就行了
  • 内存拷贝 : select,poll每次调用都要把fd集合从用户态往内核态拷贝一次; epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次
  • 数量限制 : select默认只支持1024个;epoll并没有最大数目限制

非阻塞的connect和accept

  • 非阻塞connect为啥要用?怎么用?
    • 为啥要用: 因为connect是比较耗时的, 所以我们希望可以在connecting的时候并行的做点其他的事
    • 怎么用: 调用非阻塞connect之后会立马返回EINPROCESS错误, 然后我们去epoll注册一个可写事件, 等待此套接字可写我们判断一下如果不是socket发生异常错误则即为connect连上了
  • 非阻塞accept有啥用, 怎么用?为啥要用?
    • 为啥要用: 如果调用阻塞accept,这样如果在select检测到有连接请求,但在调用accept之前,这个请求断开了,然后调用accept的时候就会阻塞在哪里,除非这时有另外一个连接请求,如果没有,则一直被阻塞在accept调用上, 无法处理任何其他已就绪的描述符。
    • 怎么用: 我们去epoll注册一个监听套接字的fd可读事件, 等待此套接字的fd可写我们判断一下如果不是socket发生异常错误则即为准备好了一个新连接
  • 注意 : 当socket异常错误的时候socket是可读并可写的, 所以在非阻塞connect(判断是否可写)/accept(判断是否可读)的时候要特别注意这种情况, 要用getsockopt函数, 使用SO_ERROR选项来检查处理.

阻塞和非阻塞的send和recv和sendto和recvfrom

注意: 首先需要说明的是,不管阻塞还是非阻塞,在发送时都会将数据从应用进程缓冲区拷贝到内核套接字发送缓冲区(UDP并没有实际存在这个内核套接字发送缓冲区, UDP的套接字缓冲区大小仅仅是可写到该套接字UDP数据包的大小上限, TCP/UDP都可以用SO_SNDBUF选项来更改该内核缓冲区大小)。

  • 发送, 我们发送选用send(这里特指TCP)以及sendto(这里特指UDP)来描述
    • 阻塞
      • 在阻塞模式下send操作将会等待所有数据均被拷贝到发送缓冲区后才会返回。阻塞的send操作返回的发送大小,必然是你参数中的发送长度的大小。
      • 在阻塞模式下的sendto操作不会阻塞。
        关于这一点的原因在于:UDP并没有真正的发送缓冲区,它所做的只是将应用缓冲区拷贝给下层协议栈,在此过程中加上UDP头,IP头,所以实际不存在阻塞。
    • 非阻塞
      • 在非阻塞模式下send操作调用会立即返回。
        关于立即返回大家都不会有异议。还是拿阻塞send的那个例子来看,当缓冲区只有192字节,但是却需要发送2000字节时,此时调用立即返回,并得到返回值为192。从中可以看到,非阻塞send仅仅是尽自己的能力向缓冲区拷贝尽可能多的数据,因此在非阻塞下send才有可能返回比你参数中的发送长度小的值。
        如果缓冲区没有任何空间时呢?这时肯定也是立即返回,但是你会得到WSAEWOULDBLOCK/EWOULDBLOCK 的错误,此时表示你无法拷贝任何数据到缓冲区,你最好休息一下再尝试发送。
      • 在非阻塞模式下sendto操作 不会阻塞(与阻塞一致,不作说明)。
  • 接收, 接收选用recv(这里特指TCP)以及recvfrom(这里特指UDP)来描述
    • 阻塞
      • 在阻塞模式下recv,recvfrom操作将会阻塞 到缓冲区里有至少一个字节(TCP)或者一个完整UDP数据报才返回。
      • 在没有数据到来时,对它们的调用都将处于睡眠状态,不会返回。
    • 非阻塞
      • 在非阻塞模式下recv,recvfrom操作将会立即返回。
      • 如果缓冲区有任何一个字节数据(TCP)或者一个完整UDP数据报,它们将会返回接收到的数据大小。而如果没有任何数据则返回错误 WSAEWOULDBLOCK/EWOULDBLOCK。

reuseaddr和reuseport

  • reuseaddr的作用?
    • 参考 https://zhuanlan.zhihu.com/p/35367402
    • 主要是用于绑定TIME_WAIT状态的地址: 一个非常现实的问题是,假如一个systemd托管的service异常退出了,留下了TIME_WAIT状态的socket,那么systemd将会尝试重启这个service。但是因为端口被占用,会导致启动失败,造成两分钟的服务空档期,systemd也可能在这期间放弃重启服务。但是在设置了SO_REUSEADDR以后,处于TIME_WAIT状态的地址也可以被绑定,就杜绝了这个问题。因为TIME_WAIT其实本身就是半死状态,虽然这样重用TIME_WAIT可能会造成不可预料的副作用,但是在现实中问题很少发生,所以也忽略了它的副作用
  • reuseport有啥用?

    • SO_REUSEPORT使用场景:linux kernel 3.9 引入了最新的SO_REUSEPORT选项,使得多进程或者多线程创建多个绑定同一个ip:port的监听socket,提高服务器的接收链接的并发能力,程序的扩展性更好;此时需要设置SO_REUSEPORT(注意所有进程都要设置才生效)。

      setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT,(const void *)&reuse , sizeof(int));

      目的:每一个进程有一个独立的监听socket,并且bind相同的ip:port,独立的listen()和accept();提高接收连接的能力。(例如nginx多进程同时监听同一个ip:port)
      解决的问题:

      • 避免了应用层多线程或者进程监听同一ip:port的“惊群效应”。
      • 内核层面实现负载均衡,保证每个进程或者线程接收均衡的连接数。
      • 只有effective-user-id相同的服务器进程才能监听同一ip:port (安全性考虑)

服务器开发自我修养专栏-Linux文件系统

Posted on 11-06-2020 | In Self-cultivation

Linux文件系统

详细的可以查看本博客的这篇文章哈文件描述符FD与Inode
fd数目大小的限制可以改变, 参考 文件描述符限制

系统目录结构

Linux 系统目录结构 登录系统后,在当前命令窗口下输入命令: ls / 你会看到如下图所示: 树状目录结构: 以下是对这些目录的解释: /bin: bin 是 Binaries (二进制文件) 的缩写, 这个目录存放着最经常使用的命令。

以下是对这些目录的解释:

  • /bin:
    bin 是 Binaries (二进制文件) 的缩写, 这个目录存放着最经常使用的命令。
  • /boot:
    这里存放的是启动 Linux 时使用的一些核心文件,包括一些连接文件以及镜像文件。
  • /dev :
    dev 是 Device(设备) 的缩写, 该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的。
  • /etc:
    etc 是 Etcetera(等等) 的缩写, 这个目录用来存放所有的系统管理所需要的配置文件和子目录。
  • /home:
    用户的主目录,在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,如上图中的 alice、bob 和 eve。
  • /lib:
    lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库。
  • /lost+found:
    这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。
  • /media:
    linux 系统会自动识别一些设备,例如 U 盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下。
  • /mnt:
    系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。
  • /opt:
    opt 是 optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如你安装一个 ORACLE 数据库则就可以放到这个目录下。默认是空的。
  • /proc:
    proc 是 Processes(进程) 的缩写,/proc 是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。
    这个目录的内容不在硬盘上而是在内存里,我们也可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的 ping 命令,使别人无法 ping 你的机器:

    echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
  • /root:
    该目录为系统管理员,也称作超级权限者的用户主目录。

  • /sbin:
    s 就是 Super User 的意思,是 Superuser Binaries (超级用户的二进制文件) 的缩写,这里存放的是系统管理员使用的系统管理程序。
  • /selinux:
    这个目录是 Redhat/CentOS 所特有的目录,Selinux 是一个安全机制,类似于 windows 的防火墙,但是这套机制比较复杂,这个目录就是存放 selinux 相关的文件的。
  • /srv:
    该目录存放一些服务启动之后需要提取的数据。
  • /sys:
    这是 Linux2.6 内核的一个很大的变化。该目录下安装了 2.6 内核中新出现的一个文件系统 sysfs 。
    sysfs 文件系统集成了下面 3 种文件系统的信息:针对进程信息的 proc 文件系统、针对设备的 devfs 文件系统以及针对伪终端的 devpts 文件系统。
    该文件系统是内核设备树的一个直观反映。
    当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。
  • /tmp:
    tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的。
  • /usr:
    usr 是 unix shared resources(共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于 windows 下的 program files 目录。
  • /usr/bin:
    系统用户使用的应用程序。
  • /usr/sbin:
    超级用户使用的比较高级的管理程序和系统守护程序。
  • /usr/src:
    内核源代码默认的放置目录。
  • /var:
    var 是 variable(变量) 的缩写,这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。
  • /run:
    是一个临时文件系统,存储系统启动以来的信息。当系统重启时,这个目录下的文件应该被删掉或清除。如果你的系统上有 /var/run 目录,应该让它指向 run。

在 Linux 系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。
/etc: 上边也提到了,这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。
/bin, /sbin, /usr/bin, /usr/sbin: 这是系统预设的执行文件的放置目录,比如 ls 就是在 /bin/ls 目录下的。
值得提出的是,/bin, /usr/bin 是给系统用户使用的指令(除 root 外的通用户),而 / sbin, /usr/sbin 则是给 root 使用的指令。
/var: 这是一个非常重要的目录,系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在 /var/log 目录下,另外 mail 的预设放置也是在这里。

inode

硬盘的最小存储单位是扇区(Sector),块(block)由多个扇区组成。文件数据存储在块中。块的最常见的大小是 4kb,约为 8 个连续的扇区组成(每个扇区存储 512 字节)。一个文件可能会占用多个 block,但是一个块只能存放一个文件。

虽然,我们将文件存储在了块(block)中,但是我们还需要一个空间来存储文件的 元信息 metadata :如某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等。这种 存储文件元信息的区域就叫 inode,译为索引节点:i(index)+node。 每个文件都有一个 inode,存储文件的元信息。

可以使用 stat 命令可以查看文件的 inode 信息。每个 inode 都有一个号码,Linux/Unix 操作系统不使用文件名来区分文件,而是使用 inode 号码区分不同的文件。

简单来说:inode 就是用来维护某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等信息。
简单总结一下:

  • inode :记录文件的属性信息,可以使用 stat 命令查看 inode 信息。
  • block :实际文件的内容,如果一个文件大于一个块时候,那么将占用多个 block,但是一个块只能存放一个文件。(因为数据是由 inode 指向的,如果有两个文件的数据存放在同一个块中,就会乱套了)

软链接与硬链接

详细的可参考: https://blog.csdn.net/yangxjsun/article/details/79681229

硬链接

普通链接一般就是指硬链接, 硬链接是新的目录条目,其引用系统中的现有文件。文件系统中的每一文件默认具有一个硬链接。为节省空间,可以不复制文件,而创建引用同一文件的新硬链接。新硬链接如果在与现有硬链接相同的目录中创建,则需要有不同的文件名,否则需要在不同的目录中。指向同一文件的所有硬链接具有相同的权限、连接数、用户/组所有权、时间戳以及文件内容。指向同一文件内容的硬链接需要在相同的文件系统中。
简单说,硬链接就是一个 inode 号对应多个文件名。就是同一个文件使用了多个别名(上图中 hard link 就是 file 的一个别名,他们有共同的 inode)。

由于硬链接是有着相同 inode 号仅文件名不同的文件,因此硬链接存在以下几点特性:

  • 文件有相同的 inode 及 data block;
  • 只能对已存在的文件进行创建;
  • 不能交叉文件系统进行硬链接的创建;
  • 不能对目录进行创建,只可对文件创建;
  • 删除一个硬链接文件并不影响其他有相同 inode 号的文件, 只是相应的链接计数器(link count)减1

软链接

(又称符号链接,即 soft link 或 symbolic link) 软链接与硬链接不同,若文件用户数据块中存放的内容是另一文件的路径名的指向,则该文件就是软连接。软链接就是一个普通文件,只是数据块内容有点特殊。软链接有着自己的 inode 号以及用户数据块。(见图2)软连接可以指向目录,而且软连接所指向的目录可以位于不同的文件系统中。

软链接特性:

  • 软链接有自己的文件属性及权限等;
  • 可对不存在的文件或目录创建软链接;
  • 软链接可交叉文件系统;
  • 软链接可对文件或目录创建;
  • 创建软链接时,链接计数 i_nlink 不会增加;
  • 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接或悬挂的软链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。

Linux 为什么多进程能够读写正在删除的文件

参考进程表_文件表_inode_vnode

Linux中多进程环境下,打开同一个文件,当一个进程进行读写操作,如果另外一个进程删除了这个文件,那么读写该文件的进程会发生什么呢?

  • 因为文件被删除了,读写进程发生异常?
  • 正在读写的进程仍然正常读写,好像没有发生什么?

学操作系统原理的时候,我们知道,linux是通过link的数量来控制文件删除,只有当一个文件不存在任何link的时候,这个文件才会被删除。

而每个文件都会有2个link计数器:

  • i_count: i_count的意义是当前使用者的数量,也就是打开文件进程的个数。
  • i_nlink: i_nlink的意义是介质连接的数量.

或者可以理解为 i_count是内存引用计数器,i_nlink是硬盘引用计数器。再换句话说,当文件被某个进程引用时,i_count 就会增加;当创建文件的硬连接的时候,i_nlink 就会增加。

对于 rm 而言,就是减少 i_nlink。这里就出现一个问题,如果一个文件正在被某个进程调用,而用户却执行 rm 操作把文件删除了,会出现什么结果呢?

当用户执行 rm 操作后,ls 或者其他文件管理命令不再能够找到这个文件,但是进程却依然在继续正常执行,依然能够从文件中正确的读取内容。这是因为,rm 操作只是将 i_nlink 置为 0 了;由于文件被进程引用的缘故,i_count 不为 0,所以系统没有真正删除这个文件。i_nlink 是文件删除的充分条件,而 i_count 才是文件删除的必要条件。

基于以上只是,大家猜一下,如果在一个进程在打开文件写日志的时候,手动或者另外一个进程将这个日志删除,会发生什么情况?

是的,数据库并没有停掉。虽然日志文件被删除了,但是有一个进程已经打开了那个文件,所以向那个文件中的写操作仍然会成功,数据仍然会提交。

文件操作偏移lseek

lseek的函数用于设置文件偏移量。

每个打开的文件都有一个与其相关联的“当前文件偏移量”(当前文件偏移量)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非制定O_APPEND选项,否则该偏移量被设置为0。

文件空洞

我们知道lseek()系统调用可以改变文件的偏移量,但如果程序调用使得文件偏移量跨越了文件结尾,然后再执行I/O操作,将会发生什么情况? read()调用将会返回0,表示文件结尾。令人惊讶的是,write()函数可以在文件结尾后的任意位置写入数据。在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞,这一点是允许的。从原来的文件结尾到新写入数据间的这段空间被成为文件空洞。调用write后文件结尾的位置已经发生变化。

文件空洞不占用任何磁盘空间,直到后续某个时点,在文件空洞中写入了数据,文件系统才会为之分配磁盘块。空洞的存在意味着一个文件名义上的大小可能要比其占用的磁盘存储总量要大(有时大出许多)。向文件空洞中写入字节,内核需要为其分配存储单元,即使文件大小不变,系统的可用磁盘空间也将减少。这种情况并不常见,但也需要了解。

实际中的空洞文件会在哪里用到呢?常见的场景有:

  • 一是在下载电影的时候,发现刚开始下载,文件的大小就已经到几百M了.
  • 二是在创建虚拟机的磁盘镜像的时候,你创建了一个100G的磁盘镜像,但是其实装起来系统之后,开始也不过只占用了3,4G的磁盘空间,如果一开始把100G都分配出去的话,无疑是很大的浪费.
  • 空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件(比如视频文件),如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每个线程负责其中一段的写入。(就像修100公里的高速公路,分成20个段来修,每个段就只负责5公里,就可以大大提高效率)。

习题

Linux下两个进程可以同时打开同一个文件,这时如下描述错误的是(答案是4):

  1. 两个进程中分别产生生成两个独立的fd
  2. 两个进程可以任意对文件进行读写操作,操作系统并不保证写的原子性
  3. 进程可以通过系统调用对文件加锁,从而实现对文件内容的保护
  4. 任何一个进程删除该文件时,另外一个进程会立即出现读写失败
  5. 两个进程可以分别读取文件的不同部分而不会相互影响
  6. 一个进程对文件长度和内容的修改另外一个进程可以立即感知

proc文件夹

参考: https://www.cnblogs.com/liushui-sky/p/9354536.html

下面是作者系统(RHEL5.3)上运行的一个PID为2674的进程saslauthd的相关文件,其中有些文件是每个进程都会具有的,后文会对这些常见文件做出说明。

[root@rhel5 ~]# ll /proc/2674
total 0
dr-xr-xr-x 2 root root 0 Feb 8 17:15 attr
-r-------- 1 root root 0 Feb 8 17:14 auxv
-r--r--r-- 1 root root 0 Feb 8 17:09 cmdline
-rw-r--r-- 1 root root 0 Feb 8 17:14 coredump_filter
-r--r--r-- 1 root root 0 Feb 8 17:14 cpuset
lrwxrwxrwx 1 root root 0 Feb 8 17:14 cwd -> /var/run/saslauthd
-r-------- 1 root root 0 Feb 8 17:14 environ
lrwxrwxrwx 1 root root 0 Feb 8 17:09 exe -> /usr/sbin/saslauthd
dr-x------ 2 root root 0 Feb 8 17:15 fd
-r-------- 1 root root 0 Feb 8 17:14 limits
-rw-r--r-- 1 root root 0 Feb 8 17:14 loginuid
-r--r--r-- 1 root root 0 Feb 8 17:14 maps
-rw------- 1 root root 0 Feb 8 17:14 mem
-r--r--r-- 1 root root 0 Feb 8 17:14 mounts
-r-------- 1 root root 0 Feb 8 17:14 mountstats
-rw-r--r-- 1 root root 0 Feb 8 17:14 oom_adj
-r--r--r-- 1 root root 0 Feb 8 17:14 oom_score
lrwxrwxrwx 1 root root 0 Feb 8 17:14 root -> /
-r--r--r-- 1 root root 0 Feb 8 17:14 schedstat
-r-------- 1 root root 0 Feb 8 17:14 smaps
-r--r--r-- 1 root root 0 Feb 8 17:09 stat
-r--r--r-- 1 root root 0 Feb 8 17:14 statm
-r--r--r-- 1 root root 0 Feb 8 17:10 status
dr-xr-xr-x 3 root root 0 Feb 8 17:15 task
-r--r--r-- 1 root root 0 Feb 8 17:14 wchan

  • cmdline — 启动当前进程的完整命令,但僵尸进程目录中的此文件不包含任何信息;

    [root@rhel5 ~]# more /proc/2674/cmdline 
    /usr/sbin/saslauthd
  • cwd — 指向当前进程运行目录的一个符号链接;

  • environ — 当前进程的环境变量列表,彼此间用空字符(NULL)隔开;变量用大写字母表示,其值用小写字母表示;

    [root@rhel5 ~]# more /proc/2674/environ 
    TERM=linuxauthd
  • exe — 指向启动当前进程的可执行文件(完整路径)的符号链接,通过/proc/N/exe可以启动当前进程的一个拷贝;

  • fd — 这是个目录,包含当前进程打开的每一个文件的文件描述符(file descriptor),这些文件描述符是指向实际文件的一个符号链接;

    [root@rhel5 ~]# ll /proc/2674/fd
    total 0
    lrwx------ 1 root root 64 Feb 8 17:17 0 -> /dev/null
    lrwx------ 1 root root 64 Feb 8 17:17 1 -> /dev/null
    lrwx------ 1 root root 64 Feb 8 17:17 2 -> /dev/null
    lrwx------ 1 root root 64 Feb 8 17:17 3 -> socket:[7990]
    lrwx------ 1 root root 64 Feb 8 17:17 4 -> /var/run/saslauthd/saslauthd.pid
    lrwx------ 1 root root 64 Feb 8 17:17 5 -> socket:[7991]
    lrwx------ 1 root root 64 Feb 8 17:17 6 -> /var/run/saslauthd/mux.accept
  • limits — 当前进程所使用的每一个受限资源的软限制、硬限制和管理单元;此文件仅可由实际启动当前进程的UID用户读取;(2.6.24以后的内核版本支持此功能);

  • maps — 当前进程关联到的每个可执行文件和库文件在内存中的映射区域及其访问权限所组成的列表;

    [root@rhel5 ~]# cat /proc/2674/maps 
    00110000-00239000 r-xp 00000000 08:02 130647 /lib/libcrypto.so.0.9.8e
    00239000-0024c000 rwxp 00129000 08:02 130647 /lib/libcrypto.so.0.9.8e
    0024c000-00250000 rwxp 0024c000 00:00 0
    00250000-00252000 r-xp 00000000 08:02 130462 /lib/libdl-2.5.so
    00252000-00253000 r-xp 00001000 08:02 130462 /lib/libdl-2.5.so
  • mem — 当前进程所占用的内存空间,由open、read和lseek等系统调用使用,不能被用户读取;

  • root — 指向当前进程运行根目录的符号链接;在Unix和Linux系统上,通常采用chroot命令使每个进程运行于独立的根目录;

  • stat — 当前进程的状态信息,包含一系统格式化后的数据列,可读性差,通常由ps命令使用;

  • statm — 当前进程占用内存的状态信息,通常以“页面”(page)表示;

  • status — 与stat所提供信息类似,但可读性较好,如下所示,每行表示一个属性信息;其详细介绍请参见 proc的man手册页;

    [root@rhel5 ~]# more /proc/2674/status 
    Name: saslauthd
    State: S (sleeping)
    SleepAVG: 0%
    Tgid: 2674
    Pid: 2674
    PPid: 1
    TracerPid: 0
    Uid: 0 0 0 0
    Gid: 0 0 0 0
    FDSize: 32
    Groups:
    VmPeak: 5576 kB
    VmSize: 5572 kB
    VmLck: 0 kB
    VmHWM: 696 kB
    VmRSS: 696 kB
    …………
  • task — 目录文件,包含由当前进程所运行的每一个线程的相关信息,每个线程的相关信息文件均保存在一个由线程号(tid)命名的目录中,这类似于其内容类似于每个进程目录中的内容;(内核2.6版本以后支持此功能)

123456789101112131415161718…36
Mike

Mike

🚙 🚗 💨 💨 If you want to create a blog like this, just follow my open-source project, "hexo-theme-neo", click the GitHub button below and check it out ^_^ . It is recommended to use Chrome, Safari, or Edge to read this blog since this blog was developed on Edge (Chromium kernel version) and tested on Safari.

11 categories
287 posts
110 tags
about
GitHub Spotify
© 2013 - 2025 Mike