Skip to content

字节跳动C++工程师一面

来源:https://www.nowcoder.com/share/jump/1741711354712

1、mmap的细节?

mmap 是 Unix/Linux 系统中用于内存映射的一个系统调用,其主要作用是将一个文件或设备的内容映射到进程的虚拟地址空间中,使得进程可以通过指针直接访问文件数据,就像操作内存一样。

mmap函数原型及参数

c++
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

各参数含义:

addr:建议映射的起始虚拟地址,一般传入 NULL,由内核自动选择合适的地址。如果指定地址,必须是页对齐的。

length:要映射的字节数。映射区域的大小通常需要按页面大小(例如 4KB)对齐。

prot:内存区域的保护属性,可以是以下标志的按位或组合:

  • PROT_READ:可读
  • PROT_WRITE:可写
  • PROT_EXEC:可执行
  • PROT_NONE:无权限(不允许访问)

flags:控制映射的类型及行为,常用的标志包括:

  • MAP_SHARED:建立一个共享映射,写入的数据会写回文件,并且对其他映射该文件的进程可见。
  • MAP_PRIVATE:建立一个私有映射,写入时采用“写时复制”(copy-on-write),修改不会影响原文件,也不会反映给其他进程。
  • MAP_ANONYMOUS(或 MAP_ANON):匿名映射,不与任何文件关联,此时 fd 通常传 -1。
  • 其他如 MAP_FIXEDMAP_POPULATEMAP_LOCKED 等,用于特定场景和优化。

fd:文件描述符,用于指定需要映射的文件。对于匿名映射,此参数无效(需传 -1)。

offset:映射文件的起始偏移量,必须是页面大小的整数倍。即从文件中的哪个位置开始映射。

匿名映射 vs. 文件映射

文件映射:将文件中的内容映射到内存,读写操作直接作用于映射区,同时可以利用内核页缓存提升 I/O 性能。适用于需要频繁随机访问大文件的场景。

匿名映射:不与任何文件关联,通常用于创建共享内存区域或进程私有的内存区域。常见于需要动态分配一块大内存而避免频繁调用 malloc 的场景。

工作原理

懒加载:映射后,并不会立即加载所有数据到内存。内核会在进程首次访问对应虚拟地址时触发页面错误(page fault),再从磁盘读取数据到物理内存。

页对齐与页面保护:映射区域会按照系统的页面大小进行对齐。prot 参数控制每个页面的访问权限,操作系统会在访问违反权限时生成信号(如 SIGSEGV)。

拷贝写:当使用 MAP_PRIVATE 进行映射时,写入操作不会直接修改文件,而是触发写时复制(COW),在第一次写入时,内核为该页面创建一个私有副本,从而保证文件数据的原始性。

同步机制:对于 MAP_SHARED 映射,写入会直接修改内存中的数据,并且最终会同步回文件中。可以使用 msync 显式要求将修改同步到磁盘。

2、epoll底层原理,和select poll的区别?

epoll

基本概念

Linux 特有的 I/O 多路复用机制,通过内核事件表和回调机制实现高效的事件通知。

由以下三个系统调用组成:

epoll_create: 创建一个 epoll 实例,返回一个文件描述符(epfd)。

c++
int epoll_create(int size);

epoll_ctl: 向 epoll 实例注册、修改或删除文件描述符及其关注的事件。

c++
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • op: 操作类型(如 EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL)。
  • event: 指定关注的事件(如 EPOLLIN 表示可读,EPOLLOUT 表示可写)。

epoll_wait: 等待事件发生,返回就绪的事件列表。

c++
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
底层实现
事件表
  • epoll 在内核中维护一个红黑树(struct epoll_file),用于存储所有被监视的文件描述符及其事件。
  • 红黑树的查找、插入和删除时间复杂度为 O(log n),适合管理大量文件描述符。
就绪队列
  • 内核还维护一个双向链表,表示已就绪的文件描述符(struct list_head)。
  • 当文件描述符状态改变时(如 socket 可读),内核通过回调函数将其加入就绪队列。
回调机制
  • 每个被监视的文件描述符关联一个回调函数(ep_poll_callback)。
  • 当 I/O 事件发生时(如数据到达),内核网络子系统调用该回调,将文件描述符加入就绪队列。
用户态交互

epoll_wait 只是简单地将内核就绪队列中的事件拷贝到用户态数组 events 中,时间复杂度为 O(1) 或 O(k),其中 k 是就绪事件数。

事件触发模式

LT(水平触发,Level Triggered):

  • 默认模式。只要文件描述符上有未处理的事件,就会反复通知。
  • 类似于 poll,适合逐步处理数据的场景。

ET(边沿触发,Edge Triggered):

  • 仅在状态变化时通知一次(例如,从不可读变为可读)。
  • 要求用户一次性处理完所有数据,性能更高,但代码复杂度增加。

select

工作机制
  • select 使用一个 fd_set 位图(通常是固定大小,例如 1024 位)表示监视的文件描述符集合。
  • 调用 select 时,用户指定三类事件集合(读、写、异常),内核检查这些文件描述符的状态,并修改 fd_set,标记就绪的描述符。
  • 返回值是就绪的文件描述符总数,用户需要遍历 fd_set 找出具体哪些描述符就绪。
底层实现
  • 内核逐个轮询所有传入的文件描述符,检查其状态。
  • 将结果写回用户态的 fd_set。

poll

工作机制
  • poll 使用一个动态数组 struct pollfd 替代 select 的位图,用户传入文件描述符和关注的事件。
  • 内核检查每个文件描述符的状态,并更新数组中的 revents 字段,表示就绪事件。
  • 用户遍历数组,处理就绪的描述符。
底层实现
  • 类似于 select,内核轮询所有文件描述符。
  • 没有固定大小限制,支持更多文件描述符。

区别

特性selectpollepoll
数据结构位图 (fd_set)动态数组 (pollfd)红黑树 + 就绪链表
最大 fd 限制固定(通常 1024)无明确限制无明确限制(受内存限制)
时间复杂度O(n)(轮询所有 fd)O(n)(轮询所有 fd)O(1)(仅处理就绪事件)
内核态拷贝每次传入整个 fd_set每次传入整个 pollfd 数组只拷贝就绪事件
事件通知水平触发水平触发支持水平触发和边沿触发
用户态处理遍历所有 fd 检查状态遍历所有 fd 检查状态直接获取就绪 fd
适用场景小规模连接中等规模连接大规模高并发连接

3、红黑树性质和相对其他的优缺点?

性质

红黑树通过以下五条性质保证树的平衡,从而确保查找、插入和删除操作的时间复杂度稳定在 O(log n):

性质 1:每个节点要么是红色,要么是黑色。

红黑树是一种着色的二叉树,每个节点都有一个颜色属性(红或黑)。

性质 2:根节点是黑色。

根节点始终是黑色,这避免了某些不平衡情况。

性质 3:所有叶子节点是黑色。

红黑树中的叶子节点通常是空节点,定义为黑色。这些节点不存储数据,仅用于简化实现。

性质 4:红色节点的子节点必须是黑色(即不存在连续的红色节点)。

这条规则防止了树的某一路径上红色节点过多,从而限制了树的高度。

性质 5:从任意节点到其每个叶子节点的所有简单路径上,黑色节点的数量相同。

这是红黑树平衡的核心保证,称为“黑高”。它确保树的高度不会过于失衡。

由于性质 4 和性质 5,红黑树的高度最多是 2 * log(n+1),其中 n 是节点总数。这是因为最长的路径(红黑交替)不会超过最短路径(全黑)的两倍。

工作原理

红黑树通过在插入和删除操作后调整颜色和树结构(旋转操作),来维持上述性质。

插入
  1. 新节点默认是红色(因为红色插入对黑高影响较小)。
  2. 检查是否违反性质(主要是性质 4 和性质 2)。
  3. 通过以下手段修复:
    • 颜色调整:改变节点颜色。
    • 左旋或右旋:调整树结构。
  4. 最终确保根节点为黑色。
删除
  1. 删除节点后,用其后继节点(或前驱节点)替换。
  2. 如果删除的是黑色节点,会破坏性质 5(黑高减少),需要复杂调整:
    • 通过借用兄弟节点的黑色高度或旋转来重新平衡。
  3. 修复过程可能涉及多次旋转和颜色调整。
时间复杂度
  • 查找:O(log n)
  • 插入:O(log n)
  • 删除:O(log n)
  • 空间复杂度:O(n)

优点

相对于普通二叉搜索树 (BST)
  • 平衡性:普通 BST 在最坏情况下(例如有序插入)会退化为链表,时间复杂度变为 O(n)。红黑树通过自平衡保证 O(log n)。
  • 稳定性:无论输入顺序如何,红黑树都能维持高效性能。
相对于 AVL 树
  • 插入和删除效率更高:AVL 树是 garrotte严格平衡,要求每次插入或删除后都保持树的绝对平衡(每个节点的左右子树高度差不超过 1),调整次数多。而红黑树允许一定的不平衡,调整代价较低。
  • 实现简单:红黑树的平衡规则较宽松(5条性质),实现和维护成本低于 AVL 树。
相对于 B 树
  • 内存效率:红黑树每个节点只存储一个键值,适合内存受限的场景(例如内核数据结构)。B 树每个节点存储多个键值,适合磁盘 I/O 优化(例如数据库)。
  • 简单性:红黑树是二叉树,逻辑和实现比多路 B 树更直观。
相对于哈希表
  • 有序性:红黑树保持键的顺序,支持范围查询(如查找某个范围内的所有键),而哈希表无序。
  • 动态性:红黑树支持高效的动态插入和删除,哈希表在扩容时可能需要 O(n) 时间。

缺点

相对于 AVL 树
  • 查询效率稍低:AVL 树更严格平衡,高度更低(约为 1.44 * log(n)),查询略快于红黑树(约为 2 * log(n))。
  • 不完全对称:红黑树允许一定的不平衡,可能导致某些路径比其他路径长。
相对于 B 树
  • 缓存不友好:红黑树是二叉树,节点分散在内存中,访问时缓存命中率低于 B 树的连续多键存储。
  • 不适合大块数据:B 树针对磁盘 I/O 优化,红黑树更适合内存操作。
相对于哈希表
  • 查询速度慢:哈希表的平均时间复杂度为 O(1),红黑树为 O(log n)。
  • 空间开销:红黑树需要额外的指针和颜色位,空间利用率低于哈希表。

对比表格

数据结构查找插入删除空间复杂度有序性平衡性
红黑树O(log n)O(log n)O(log n)O(n)自平衡
AVL 树O(log n)O(log n)O(log n)O(n)严格平衡
B 树O(log n)O(log n)O(log n)O(n)多路平衡
哈希表O(1)O(1)O(1)O(n)无需平衡
普通 BSTO(n)O(n)O(n)O(n)不平衡

4、mutex的底层实现

基本概念

确保在任意时刻只有一个线程可以持有锁,其他线程必须等待锁释放。

操作:

  • lock():获取锁,如果锁被占用则阻塞或自旋。
  • unlock():释放锁,通知等待的线程。

实现层次:

  • 用户态:轻量级实现,尽量减少系统调用。
  • 内核态:涉及操作系统调度和同步原语。
  • 硬件支持:原子指令确保操作的线程安全性。

底层实现

在 POSIX 线程库(pthread)或 C++ 的 std::mutex 中,mutex 的实现通常基于操作系统的同步机制,例如 Linux 上的 futex(Fast Userspace Mutex)

用户态实现(乐观路径)

在无竞争的情况下,mutex 尽量在用户态完成操作,避免昂贵的系统调用。这依赖于原子操作。

核心工具:硬件提供的原子指令,例如:

  • CAS(Compare-And-Swap,比较并交换)。
  • TAS(Test-And-Set,测试并置位)。
  • FAA(Fetch-And-Add,获取并增加)。

实现逻辑:

  • mutex 维护一个状态变量(例如一个整数),0 表示未锁定,1 表示锁定。
  • lock() 使用原子操作检查并尝试将状态从 0 改为 1:
    • 如果成功,线程获得锁。
    • 如果失败,说明锁被占用,进入竞争路径。
  • unlock() 将状态原子性地置为 0。
内核态实现(竞争路径)

当多个线程竞争锁时,用户态自旋效率低下,需要借助内核的同步机制。在 Linux 上,pthread_mutex 通常基于 futex。

什么是 futex?
  • futex(Fast Userspace Mutex)是 Linux 提供的一种轻量级同步原语,允许在用户态处理无竞争情况,在有竞争时切换到内核态。
  • 它是一个内核支持的整数变量(通常 32 位),结合两个系统调用:
    • futex_wait:当值不符合预期时,线程进入等待。
    • futex_wake:唤醒等待的线程。
实现逻辑
  1. 无竞争时:

    • lock() 使用原子操作将状态从 0 改为 1,完成锁获取。
    • unlock() 将状态置为 0,无需系统调用。
  2. 有竞争时:

    • lock() 检测到状态已是 1,调用 futex_wait,将线程挂起并加入等待队列。
    • unlock() 检测到有等待者,调用 futex_wake,唤醒一个或多个等待线程。
  3. 状态管理:

    futex 变量通常用位表示锁状态:

    • 0:未锁定。
    • 1:锁定,无等待者。
    • 2:锁定,有等待者。
内核态细节(Linux futex)

数据结构:

  • 内核维护一个 futex 等待队列(struct futex_q),记录等待该锁的线程。
  • 每个 futex 变量关联一个哈希表,用于快速查找。

流程:

  1. futex_wait:线程加入等待队列,进入睡眠状态(调度器移除运行队列)。
  2. futex_wake:从等待队列中取出一个线程,唤醒并加入运行队列。

优化:

  • 内核使用自旋锁保护 futex 数据结构。
  • 支持批量唤醒(例如唤醒所有等待者,用于条件变量)。

自旋锁 vs 阻塞锁

自旋锁(Spinlock):

  • 在用户态忙等待(自旋),适用于锁持有时间短的场景。
  • 优点:避免上下文切换。
  • 缺点:浪费 CPU 周期。

阻塞锁(Mutex):

  • 在竞争时进入内核态睡眠,适用于锁持有时间长的场景。
  • 优点:节省 CPU 资源。
  • 缺点:上下文切换开销。

5、http各版本区别?

HTTP/0.9(1991)

背景
  • HTTP 的最初版本,由蒂姆·伯纳斯-李(Tim Berners-Lee)设计,用于简单的超文本传输。
  • 仅支持最基本的功能。
特性

单一方法:只支持 GET 请求。

无头部:请求和响应没有头部,只有纯文本(HTML)。

单请求单响应:客户端发送请求,服务器返回响应后关闭连接。

协议格式:

  • 请求:GET /path
  • 响应:纯 HTML 内容,无状态码。
优缺点

优点:极简,适合早期 Web(仅传输静态 HTML)。

缺点:功能极其有限,不支持动态内容、状态管理或复杂交互。

实现细节

基于 TCP,端口 80。

无版本号(后来追溯为 0.9)。

HTTP/1.0(1996,RFC 1945)

背景

随着 Web 的发展,HTTP/0.9 无法满足需求,HTTP/1.0 引入了更多功能。

特性

多方法支持:增加了 POST、HEAD 等方法。

请求/响应头部:引入了元数据(如 Content-Type、Content-Length)。

状态码:定义了状态码(如 200 OK、404 Not Found)。

单连接模式:每个请求仍需建立新的 TCP 连接(无持久连接)。

协议格式

请求:

c++
GET /path HTTP/1.0
Host: example.com

响应:

http
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 123

<html>...</html>
优缺点

优点:支持更丰富的 Web 内容(图片、表单等),奠定了现代 HTTP 基础。

缺点:

  • 每次请求都需要建立和关闭 TCP 连接,开销大(三次握手、四次挥手)。
  • 队头阻塞(Head-of-Line Blocking):请求串行处理。
实现细节

基于 TCP,仍使用端口 80。

头部字段为键值对,文本格式。

HTTP/1.1(1997,RFC 2616,后更新为 RFC 7230-7235)

背景

HTTP/1.0 的性能瓶颈(如频繁建连)促使 HTTP/1.1 的推出,成为长期主流版本。

特性

持久连接(Keep-Alive):

  • 默认启用(Connection: keep-alive),一个 TCP 连接可处理多个请求。
  • 减少了 TCP 连接建立和关闭的开销。

管道化(Pipelining):

  • 允许客户端在收到前一个响应前发送多个请求。
  • 但服务器仍按序响应,存在队头阻塞问题。

更多头部:

  • Host:支持虚拟主机(同一 IP 托管多个域名)。
  • Range:支持断点续传。
  • Cache-Control:增强缓存机制。

新方法:增加了 PUT、DELETE、OPTIONS 等。

分块传输(Chunked Transfer Encoding):允许动态生成内容,分块发送(无需预知 Content-Length)。

协议格式

请求:

GET /path HTTP/1.1
Host: example.com
Connection: keep-alive

响应:

http
HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked

4
Wiki
5
pedia
0
优缺点

优点:

  • 持久连接提高了效率。
  • 支持更复杂的 Web 应用(REST API、缓存等)。

缺点:

  • 队头阻塞:管道化未完全解决响应顺序问题。
  • 文本协议:头部冗长且未压缩,解析效率较低。
  • 性能瓶颈:多资源加载仍需多个连接。
实现细节

基于 TCP,默认端口 80。

头部字段仍为文本,易读但冗余。

HTTP/2(2015,RFC 7540)

背景

HTTP/1.1 在高并发和低延迟场景下性能不足,HTTP/2 引入了二进制协议和多路复用。

特性

二进制协议:

  • 将请求和响应分解为帧(frame),如头部帧、数据帧。
  • 使用 HPACK 压缩头部,减少冗余。

多路复用(Multiplexing):

  • 一个 TCP 连接上并行发送多个请求和响应,帧交错传输。
  • 消除队头阻塞(应用层)。

流(Stream):

每个请求/响应是一个流,带唯一 ID,支持优先级和取消。

服务器推送(Server Push):

服务器可主动推送资源(如 CSS、JS),无需客户端显式请求。

头部压缩:HPACK 算法减少头部大小,支持动态表更新。

协议格式

数据以二进制帧传输:

Frame: [Length | Type | Flags | Stream ID | Payload]

示例帧类型:HEADERS(头部)、DATA(数据)、RST_STREAM(重置流)。

优缺点

优点:

  • 多路复用显著提高并发性能。
  • 头部压缩减少带宽占用。
  • 服务器推送优化页面加载。

缺点:

  • 仍基于 TCP,TCP 层队头阻塞未解决(丢包时需重传整个窗口)。
  • 实现复杂,调试困难(二进制协议不易读)。
  • 对老旧系统兼容性差。
实现细节

基于 TCP,默认端口 443(通常与 TLS 配合)。

需要 ALPN(Application-Layer Protocol Negotiation)协商协议版本。

HTTP/3(2022,RFC 9114)

背景

HTTP/2 的 TCP 队头阻塞问题促使 HTTP/3 使用 UDP + QUIC 协议,进一步提升性能。

特性

基于 QUIC(Quick UDP Internet Connections):

  • 使用 UDP 替代 TCP,自带可靠性(重传、拥塞控制)和加密。
  • 每个流独立传输,解决 TCP 队头阻塞。

0-RTT 连接:首次连接后,后续请求可跳过握手,直接发送数据。

内置 TLS 1.3:加密和认证集成到 QUIC,无需额外 TLS 层。

多路复用:继承 HTTP/2 的特性,但流间完全独立。

头部压缩:使用 QPACK(改进版 HPACK),适应 QUIC 的无序传输。

协议格式

QUIC 数据包:

[Header | Encrypted Payload]

Payload 包含 HTTP/3 帧,类似 HTTP/2。

优缺点

优点:

  • 彻底解决队头阻塞(流独立)。
  • 更快建立连接(0-RTT)。
  • 对丢包和高延迟网络更友好。

缺点:

  • UDP 不被所有网络设备支持(防火墙可能阻拦)。
  • 实现复杂,部署成本高。
  • 兼容性仍在推广中。
实现细节

基于 UDP,默认端口 443。

QUIC 提供传输层功能,HTTP/3 专注于应用层。

对比表

特性HTTP/0.9HTTP/1.0HTTP/1.1HTTP/2HTTP/3
发布年份19911996199720152022
传输协议TCPTCPTCPTCPUDP + QUIC
请求方法GETGET, POST, HEAD多方法多方法多方法
头部文本文本二进制+压缩二进制+压缩
连接管理单连接单连接持久连接+管道化多路复用多路复用
队头阻塞应用层有TCP 层有
服务器推送支持支持
加密可选可选通常 TLS内置 TLS 1.3
性能中等中等更高

6、TCP第三次握手目的?

TCP(传输控制协议)的三次握手是建立可靠连接的关键过程。第三次握手的目的是确保双方都确认对方的初始序列号,并验证双向通信的可靠性。

三次握手的过程如下:

第一次握手(客户端 → 服务器):客户端发送 SYN 包,包含初始序列号 Seq = x,表示客户端的起始点。

第二次握手(服务器 → 客户端):服务器回复 SYN+ACK 包,包含自己的初始序列号 Seq = y 和对客户端序列号的确认 Ack = x + 1。

第三次握手(客户端 → 服务器):客户端发送 ACK 包,确认服务器的序列号 Ack = y + 1,此时连接建立。

三次握手的完整流程:

客户端                  服务器
  SYN (Seq=x)  ------>
             <------  SYN+ACK (Seq=y, Ack=x+1)
  ACK (Ack=y+1) ------>

具体目的

确认服务器的接收能力和客户端的发送能力

第二次握手时,服务器已确认客户端的 SYN(通过 Ack = x + 1),并表明自己可以接收数据。但客户端尚未确认服务器的 SYN 是否正确送达。

第三次握手的角色:

  • 客户端发送 ACK,确认收到服务器的 Seq = y,从而验证服务器的发送能力和客户端的接收能力。
  • 如果没有第三次握手,服务器无法确定客户端是否真的接收到了自己的 SYN+ACK,可能导致单方面认为连接已建立。
防止已失效的连接请求到达服务器

场景:网络中可能存在延迟或重复的 SYN 包(例如旧连接的残留请求)。

问题:

  • 如果只有两次握手,服务器收到一个过期的 SYN,会回复 SYN+ACK 并认为连接已建立。
  • 但客户端并未发起这个请求,不会回应,导致服务器浪费资源等待。

第三次握手的保护:

  • 客户端只有在主动发起连接后才会发送第三次 ACK。
  • 如果服务器收到的 SYN 是过期的,客户端不会回复 ACK,服务器就不会建立连接,从而避免无效连接。
同步双方的初始序列号

TCP 使用序列号(Sequence Number)确保数据的有序性和可靠性,每个方向的数据流都有独立的初始序列号(ISN)。

第三次握手的意义:

  • 第一次握手同步客户端的 ISN(x)。
  • 第二次握手同步服务器的 ISN(y),并确认客户端的 ISN。
  • 第三次握手确认服务器的 ISN 被客户端接收。
  • 只有三次握手完成后,双方的 ISN 才完全同步,确保后续数据传输的正确性。
验证双向通信的可靠性

TCP 是全双工协议,要求双向通信都可靠。

第三次握手完成了一个完整的“请求-响应-确认”循环,确保:

  • 客户端到服务器的路径畅通(第一次)。
  • 服务器到客户端的路径畅通(第二次)。
  • 客户端再次确认服务器的响应(第三次)。

为什么不能用两次握手?

假设 TCP 只进行两次握手:

  1. 客户端发送 SYN。
  2. 服务器回复 SYN+ACK,连接建立。

问题

  • 单向确认:服务器无法确认客户端是否收到 SYN+ACK,可能导致客户端未就绪而服务器已开始发送数据。
  • 历史连接干扰:
    • 如果网络中有一个延迟的旧 SYN 到达服务器,服务器会回复 SYN+ACK 并认为连接建立。
    • 客户端无感知,服务器却浪费资源。
  • 序列号未完全同步:客户端未确认服务器的 ISN,可能导致后续数据混乱。

三次握手的必要性:

第三次握手解决了上述问题,确保连接的可靠性和安全性。

7、SYN洪泛攻击及解决方案?

SYN 洪泛攻击是一种常见的拒绝服务(DoS)攻击,针对 TCP 三次握手的特性,通过发送大量伪造的 SYN 包耗尽目标服务器的资源,使其无法响应合法请求。

攻击机制

核心思想:攻击者发送大量伪造的 SYN 包,但不完成第三次握手,导致服务器的半连接队列(SYN Queue)溢出。

步骤:

  1. 攻击者伪造大量源 IP 地址(通常是随机或不可达的 IP),向目标服务器发送 SYN 包。
  2. 服务器为每个 SYN 分配资源(内存和连接条目),并回复 SYN+ACK。
  3. 由于源 IP 是伪造的,服务器无法收到第三次 ACK,半连接状态(SYN_RCVD)持续占用资源。
  4. 当半连接队列满时,服务器拒绝新的合法连接请求。

攻击影响

资源耗尽:

  • 服务器内存和 CPU 被大量半连接占用。
  • 队列满后,拒绝服务(DoS)。

服务不可用:合法用户无法建立连接(如网站无法访问)。

放大效应:在分布式拒绝服务(DDoS)中,多个攻击源放大效应。

解决方案

系统配置优化
  1. 增大半连接队列大小:

    修改 Linux 参数:

    sysctl -w net.core.somaxconn=1024
    sysctl -w net.ipv4.tcp_max_syn_backlog=2048

    效果:增加服务器容纳半连接的能力。

    局限:仅缓解,无法根治。

  2. 缩短半连接超时时间:

    调整 tcp_synack_retries:

    sysctl -w net.ipv4.tcp_synack_retries=1

    效果:加快释放未响应的半连接。

    局限:可能影响慢速合法客户端。

  3. 启用 SYN Cookies:

    原理:

    • 不为每个 SYN 分配完整资源,而是用加密的 Cookie(基于时间戳、源 IP 等)替代。
    • 收到 ACK 时验证 Cookie,无需维护半连接状态。

    配置:

    sysctl -w net.ipv4.tcp_syncookies=1

    优点:高效,无需队列空间。

    缺点:不支持某些 TCP 选项(如窗口缩放)。

网络层防御
  1. 过滤伪造 IP:

    方法:在路由器或防火墙上启用反向路径验证(Reverse Path Forwarding, RPF)。

    效果:丢弃源 IP 不符合路由表的数据包。

    工具:如 Cisco 的 ip verify unicast source reachable-via。

  2. 限速(Rate Limiting):

    在防火墙(如 iptables)限制 SYN 包速率:

    iptables -A INPUT -p tcp --syn -m limit --limit 10/s -j ACCEPT
    iptables -A INPUT -p tcp --syn -j DROP

    效果:减少攻击流量。

    局限:可能误伤高并发合法请求。

  3. DDoS 防护服务:

    使用 CDN(如 Cloudflare、Akamai)或专属防护设备。

    原理:将流量分散到多个节点,清洗恶意请求。

8、TCP拥塞控制算法(慢启动、拥塞避免、快速恢复、快速重传)?

TCP 拥塞控制基于“加性增大、乘性减小”(AIMD)的思想。当网络拥塞时,TCP 通过减小发送速率来缓解拥塞;而当网络空闲时,则逐步增加发送速率以充分利用带宽。整个过程依赖于一个关键参数——拥塞窗口(cwnd),它决定了在未收到确认(ACK)前最多能发送多少数据。

慢启动

在连接开始或长时间没有数据传输后,TCP 不知道网络的当前可用带宽,故从一个较小的 cwnd 开始,防止一次性发送过多数据引发拥塞。

原理

初始状态:cwnd 从一个小的值开始(通常是 1 MSS,Maximum Segment Size)。

指数增长:每收到一个 ACK,cwnd 加倍。

阈值限制:当 cwnd 达到慢启动阈值(ssthresh)时,进入拥塞避免阶段。

过程
  1. 初始 cwnd = 1 MSS,发送 1 个段。
  2. 收到 1 个 ACK,cwnd = 2 MSS,发送 2 个段。
  3. 收到 2 个 ACK,cwnd = 4 MSS,发送 4 个段。
  4. 以此类推,直到 cwnd >= ssthresh 或检测到丢包。

示例:

MSS = 1460 字节,ssthresh = 16 MSS:

RTT 1: cwnd = 1  → 发送 1 段
RTT 2: cwnd = 2  → 发送 2 段
RTT 3: cwnd = 4  → 发送 4 段
RTT 4: cwnd = 8  → 发送 8 段
RTT 5: cwnd = 16 → 进入拥塞避免

拥塞避免

在慢启动探测到网络容量后,以线性增长方式调整 cwnd,避免过快增加导致拥塞。

原理

初始状态:cwnd >= ssthresh。

线性增长:每经过一个 RTT(Round-Trip Time),cwnd 增加 1 MSS。

调整公式:cwnd = cwnd + (MSS * MSS / cwnd)(实际增量约为 1 MSS)。

过程
  1. cwnd = 16 MSS,发送 16 个段。
  2. 收到 16 个 ACK,cwnd = 17 MSS,发送 17 个段。
  3. 收到 17 个 ACK,cwnd = 18 MSS,以此类推。

示例:

ssthresh = 16 MSS:

RTT 1: cwnd = 16 → 发送 16 段
RTT 2: cwnd = 17 → 发送 17 段
RTT 3: cwnd = 18 → 发送 18 段

快速重传

在不等待超时的情况下,快速检测到数据包的丢失并进行重传,从而提高传输效率,减少恢复时间。

原理

触发条件:收到 3 个重复 ACK。

重复 ACK 表示接收方收到了乱序段,暗示前一个段丢失。

操作:

  • 立即重传丢失的段。
  • 不等待超时(RTO)。
过程
  • 发送方发送段:1, 2, 3, 4, 5。
  • 段 2 丢失,接收方收到 1, 3, 4, 5。
  • 接收方回复:
    • ACK 2(期待 2)
    • ACK 2(收到 3)
    • ACK 2(收到 4)
    • ACK 2(收到 5)
  • 发送方收到 3 个重复 ACK 2,立即重传段 2。

快速恢复

原理

触发条件:快速重传后(3 个重复 ACK)。

操作:

  1. ssthresh = cwnd / 2。
  2. cwnd = ssthresh + 3 MSS(补偿已收到的 3 个重复 ACK)。
  3. 进入拥塞避免模式。
过程
  • cwnd = 20 MSS,发送 20 个段,段 2 丢失。
  • 收到 3 个重复 ACK:
    • ssthresh = 10 MSS。
    • cwnd = 10 + 3 = 13 MSS。
  • 重传段 2,收到新 ACK 后,cwnd = 10 MSS,进入拥塞避免。

示例:

初始: cwnd = 20
丢包后: 3 个重复 ACK
调整: ssthresh = 10, cwnd = 13
重传成功: cwnd = 10,进入拥塞避免

9、TCP零窗口检测?

TCP 零窗口检测是 TCP 协议中用于处理接收窗口变为 0 的机制。当接收方无法继续接收数据时(例如缓冲区满),它会通过 TCP 头部中的窗口字段通告发送方窗口大小为 0。此时,发送方暂停数据发送,并通过零窗口检测机制定期探测接收方的窗口是否恢复,以避免连接长时间停滞甚至死锁。

目的

避免死锁:确保发送方能主动探测接收方的窗口状态,而不是被动等待。

恢复通信:在接收方窗口恢复(大于 0)后,尽快重启数据传输。

维持连接:防止长时间无数据交换导致连接超时

原理

基本机制
  • 当发送方收到 Window = 0 的 ACK 时,暂停发送数据。
  • 启动一个定时器(Persist Timer,持续定时器),定期发送一个探测段(Probe Segment)。
  • 探测段触发接收方回复当前窗口大小,发送方根据回复决定是否恢复发送。
探测段特点
  • 大小:通常是 1 字节的数据(最小有效负载)。
  • 序列号:与发送窗口的下一个未发送字节相同。
  • 目的:不传输实际数据,仅用于询问接收方的窗口状态。
接收方响应

接收方收到探测段后,回复一个 ACK,包含当前的 Window 值:

  • 如果 Window = 0,发送方继续等待。
  • 如果 Window > 0,发送方恢复数据发送。
定时器

Persist Timer:

  • 初始间隔:通常 1 秒(具体实现可能不同)。
  • 指数退避:每次探测失败后,间隔加倍(如 1s, 2s, 4s),上限通常为 60 秒。
  • 重置条件:收到 Window > 0 的 ACK。

详细过程

假设发送方已发送数据 Seq = 1000-1999,接收方缓冲区满:

  1. 接收方通告零窗口:发送 ACK:Ack = 2000, Window = 0。
  2. 发送方暂停:停止发送数据,启动 Persist Timer。
  3. 发送探测段:定时器超时后,发送 1 字节数据:Seq = 2000, Len = 1。
  4. 接收方响应:
    • 如果缓冲区仍满:Ack = 2000, Window = 0。
    • 发送方继续等待,下次探测间隔加倍。
  5. 窗口恢复:
    • 接收方处理完数据,缓冲区有空间。
    • 收到探测段后回复:Ack = 2000, Window = 4096。
    • 发送方恢复发送:Seq = 2000-6095。

10、算法题还是比较简单:从尾到头打印链表

这里使用栈写一个吧。

思路

  • 从头到尾遍历链表,将节点值压入栈。
  • 利用栈的后进先出(LIFO)特性,弹出并打印。

参考代码(C++)

c++
#include <stack>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

void printReverseStack(ListNode* head) {
    std::stack<int> s;
    ListNode* current = head;

    // 将所有节点值压入栈
    while (current != nullptr) {
        s.push(current->val);
        current = current->next;
    }

    // 弹出并打印
    while (!s.empty()) {
        std::cout << s.top() << " ";
        s.pop();
    }
}

int main() {
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(3);
    head->next->next->next = new ListNode(4);
    head->next->next->next->next = new ListNode(5);

    printReverseStack(head);
    std::cout << std::endl; // 输出:5 4 3 2 1

    return 0;
}