计算机基础知识总结:网络、操作系统、数据库、C++ 与算法
总结了计算机基础核心知识,涵盖操作系统(内存管理、进程线程、调度、锁)、计算机网络(OSI 模型、TCP/IP、HTTP/HTTPS、QUIC)、数据库(MySQL 索引事务、Redis)以及 C++ 基础(内存分区、指针、智能指针、STL)。重点解析了虚拟内存、进程通信、零拷贝、TCP 三次握手、B+ 树索引及智能指针管理等关键技术点,适合面试复习。

总结了计算机基础核心知识,涵盖操作系统(内存管理、进程线程、调度、锁)、计算机网络(OSI 模型、TCP/IP、HTTP/HTTPS、QUIC)、数据库(MySQL 索引事务、Redis)以及 C++ 基础(内存分区、指针、智能指针、STL)。重点解析了虚拟内存、进程通信、零拷贝、TCP 三次握手、B+ 树索引及智能指针管理等关键技术点,适合面试复习。

虚拟内存为程序提供比实际物理内存更大的内存空间,同时提高内存管理的灵活性和系统的多任务处理能力。虚拟地址空间就是进程所能看到的内存空间,这段空间是连续的、独立的;实际地址空间则是内存上的空间,是所有进程共享的、有限的空间。虚拟内存就是把实际地址空间映射到虚拟地址空间的技术,实现了内存隔离、内存扩展、物理内存管理、页面交换等技术。
内存分段是将一个程序的内存空间分为不同的逻辑段(segments),每个段代表程序的一个功能模块或数据类型,如代码段、数据段、堆栈段等。每个段都有其自己的大小和权限。
内存分页是把整个虚拟和物理内存空间分成固定大小的页(如 4KB)。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。
内存分段容易产生外部碎片,内存分页容易产生内部碎片。
作用:逻辑隔离、内存保护、虚拟内存、共享内存、内存管理。
操作系统中的两种执行模式,用来控制计算机对硬件资源的访问权限和操作范围。处理器硬件根据状态位决定当前可以执行哪些指令、访问哪些内存区域。操作系统依赖处理器的执行模式来实现安全和稳定性,确保应用程序在用户态执行,而特权操作(系统调用、异常处理、中断处理)以及直接访问操作系统的核心部分只能在内核态执行,占用 CPU 时不能被抢占。
中断和异常是两种不同的事件,二者都会导致 CPU 暂停当前的程序执行,转而去执行特定的处理程序。
数组、链表、栈(stack)、队列、哈希表(hash table)、树、图、集合(set)、字典(map)。每种数据结构有自己适合解决的问题。
四个方面:定义、资源开销、独立安全性、通信方式。
进程是运行中的程序,是系统进行资源分配的基本单位,而线程是进程的子任务,是系统能够进行CPU 调度的最小单位,是进程内的执行单元。一个进程可以有多个线程,这些线程共享同一块内存空间。进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈。
因为进程都有独立的内存空间,所以创建和销毁的开销比较大;当切换进程时,要保存和恢复整个进程的状态,所以上下文切换的花销较大。而线程共享同一块内存空间,所以创建和销毁开销比较小;线程间切换只需要保存和恢复少数的线程上下文,因此线程上下文切换开销较小。由于共享内存空间,所以线程之间可以访问共享数据,可以直接通信;而进程是独立的内存空间,只能通过进程间通信方式来进行通信,比如管道、消息队列、信号、信号量、共享内存、socket。
进程间是相互隔离的,具有独立安全性,一个进程崩溃不会影响其他进程;而线程共享内存空间,一个线程崩溃,整个进程都会受影响。
线程共享内存空间就会导致创建、销毁、切换开销小,直接通信,非独立安全性;进程独立内存空间就会导致创建、销毁、切换开销大,进程间通信方式,独立安全性。
线程之间可以并发运行且共享相同的地址空间。
协程是一种比线程更加轻量级的并发执行单元,通常由程序本身而不是操作系统内核来调度。轻量级、协作式调度、不需要锁机制、适合高并发。
协程与并发问题:协程通常运行在同一个线程内,因此所有协程共享相同的内存空间和资源。由于协程在同一线程中按顺序执行(虽然看起来是并发的),它们不会像线程那样同时访问共享资源,因此减少了许多并发问题,如竞态条件和死锁。
协程的优势:
为了提高 CPU 利用率,在这种发送 I/O 事件致使 CPU 空闲的情况下,调度程序需要从就绪队列中选择一个进程来运行。要提高系统的吞吐率,调度程序要权衡长任务和短任务进程的运行完成数量。如果进程的等待时间很长而运行时间很短,那周转时间就很长,这不是我们所期望的,调度程序应该避免这种情况发生。就绪队列中进程的等待时间也是调度程序所需要考虑的原则。对于交互式比较强的应用,响应时间也是调度程序需要考虑的原则。
CPU 利用率、系统吞吐量、周转时间、等待时间、响应时间。
先来先服务调度算法、最短作业优先调度算法、高响应比优先调度算法、时间片轮转调度算法、最高优先级调度算法、多级反馈队列调度算法。
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区,它是访问共享资源的代码片段,一定不能给多线程同时执行。我们希望这段代码是互斥的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区。
同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。
同步就好比:「操作 A 应在操作 B 之前执行」,「操作 C 必须在操作 A 和操作 B 都完成之后才能执行」等;互斥就好比:「操作 A 和操作 B 不能在同一时刻执行」。
为了实现进程/线程间正确的协作,操作系统必须提供实现进程协作的措施和方法,主要的方法有两种:锁:加锁、解锁操作;信号量:P、V 操作。
假设有两个线程 A 和 B,线程 B 需要在线程 A 执行完某个操作之后再继续执行。我们可以使用信号量来实现这个同步:初始时,信号量的值设为 0。线程 A 在完成操作后,对信号量执行 V 操作(信号量加 1)。线程 B 在执行之前,对信号量执行 P 操作(信号量减 1)。由于初始时信号量为 0,线程 B 会被阻塞,直到线程 A 完成操作并执行 V 操作,释放信号量。
线程同步机制是指在多线程编程中,为了保证线程之间的互不干扰,而采用的一种机制。常见的线程同步机制有以下几种:
锁实现了互斥,信号量实现了同步,生产者消费者模型实现了同步和互斥。生消模型让生产者和消费者解耦,让两个线程独立工作、并行执行,这种并行工作机制充分利用了系统资源,提升了程序的并发效率。生产者 - 消费者模型通过锁、条件变量或信号量来保证同步,避免竞态条件。同步的主要目的是协调多个线程或进程之间的执行顺序,互斥的主要目的是防止多个线程或进程在同一时间访问共享资源。
死锁是指两个或多个进程在争夺系统资源时,由于互相等待对方释放资源而无法继续执行的状态。
死锁只有同时满足以下四个条件才会发生:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。
僵尸进程是指一个子进程已经执行完毕并退出,但其父进程还没有读取该子进程的退出状态信息(即通过调用 wait() 或 waitpid() 系统调用获取子进程的终止状态)。在这种情况下,子进程的进程表项仍然保留在系统中,虽然子进程已经终止,但它的进程号(PID)和退出状态信息还占用着系统资源,这种进程称为僵尸进程。
关键点:
wait() 函数处理其退出状态,可能会导致系统中的僵尸进程过多,耗尽系统的进程号资源。孤儿进程是指一个父进程终止了,但它的子进程还在继续运行。这种情况下,这些子进程将变为孤儿进程。为了防止这些孤儿进程无人管理,操作系统会将它们的父进程重新指定为 init 进程(PID 为 1 的进程,在现代系统中可能是 systemd 进程)。init 进程会自动接管这些孤儿进程,并在它们终止时清理它们的资源。
关键点:
init 进程接管,确保它们的资源能够被正确回收。一种在后台运行的特殊类型的进程,通常不与用户直接交互,而是执行系统级的任务或服务,如网络服务、打印服务、日志记录等。守护进程在系统启动时启动,并在后台持续运行,直到系统关闭。它们主要负责系统的维护和服务,保证系统的某些功能或服务持续可用。
要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。要想减少上下文切换到次数,就要减少系统调用的次数。
DMA 技术,也就是直接内存访问。
没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。零拷贝技术可以把文件传输的性能提高至少一倍以上。
在传统的文件传输过程中(如通过网络发送文件),数据的拷贝过程大致如下:
零拷贝的目标是减少或消除步骤 2 和 3 中的数据拷贝。零拷贝就是在以上的流动途径中,避免一些用户空间和内核缓冲区的拷贝。
一个单一的线程或进程中同时监视多个文件描述符(如套接字、文件、管道等),以确定这些文件描述符中的哪些已经准备好进行 I/O 操作(如读、写、或发生异常)。比如一个服务器程序,它需要处理多个客户端的网络请求。每个客户端连接到服务器的一个套接字。可以用多路复用技术(如 epoll)来'监视'所有这些套接字。为每个请求分配一个进程/线程的方式不合适,所以使用一个进程来维护多个 Socket。进程可以通过一个系统调用函数 (select、poll、epoll) 从内核中获取多个事件。
select 和 poll 都是内核通过轮询(遍历)的方式来检查多个文件描述符是否有 I/O 操作就绪(如可读、可写、异常情况等)。poll 没有文件描述符数量的限制,因为它采用的是动态数组的方式来存储文件描述符和事件。epoll 使用事件通知机制,一旦某个文件描述符的状态发生变化,内核会将这个事件通知给 epoll,使得程序不需要反复轮询所有文件描述符。
epoll 实现的数据结构是红黑树和双向链表,红黑树用于存储和管理所有被监视的文件描述符(文件描述符通常是 socket)。当你向 epoll 实例添加、删除或修改一个文件描述符时,epoll 会将这个文件描述符插入或移除红黑树中。红黑树是一种自平衡的二叉搜索树,确保了插入、删除和查找操作的时间复杂度为 O(log n),这使得 epoll 能够高效地管理大量的文件描述符。双向链表用于存储就绪事件(即有 I/O 操作准备好的文件描述符)。当某个文件描述符就绪时,它会被加入到这个双向链表中。双向链表的插入和删除操作非常高效,时间复杂度为 O(1),这对于 epoll 处理大量并发连接时的性能至关重要。
边缘触发(ET)和水平触发(LT)。
你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。
「对事件反应」,也就是来了一个事件,Reactor 就有相对应的反应/响应。收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。
自己的理解:
另外,数据链路层也会和传输层一样进行校验、流量控制,但是针对的对象、作用是不一样的。传输层实现的是端到端的通信,数据链路层实现的则是点对点的通信。点对点一般指设备相连,是局部的,而端到端则可能是跨越了全局网络,中间会经历多次路由器和交换机。
另外,物理层是用什么传输介质、什么物理接口、什么信号表示 0 和 1,解决了两台计算机可以通过信号来传输比特 0 或 1。数据链路层是如何标识 MAC 地址,如何从比特流中区分地址、数据,如何协调主机争用主线,实现了分组在一个网络上传输。网络层如何标识 IP 地址,如何路由选择,解决了在多个网络上传输的问题。传输层确定哪个进程通信,解决传输错误问题。应用层制定网络协议,编写网络应用,来实现特定的网络功能。
通过 IP 地址确定目标主机所在的局域网,然后通过 ARP 协议把 IP 地址对应的 MAC 地址找到,然后在同一片网络中找到目标主机。IP 地址的前三个数标识网络,后一个数标识主机。
假设你在计算机上使用浏览器访问一个网站(比如打开一个网页),从输入网址到网页显示在屏幕上的过程,可以说明 OSI 模型各层的作用:
目标服务器接收到这些信号后,会逆向处理这些层,从物理层到应用层,将最终的 HTTP 响应发送回你的浏览器,浏览器再将网页显示在屏幕上。

UDP 的头部比较简单,只有 8 个字节,这也是为什么 UDP 不能像 TCP 那样实现可靠传输的原因。源端口和目标端口表示数据传输的来源和去向,包长度表示数据报文的总长度(包含了头部和数据部分),方便接收方正确地处理数据。检验和则是用于验证 UDP 数据报在传输过程中是否发生了错误。它帮助确保数据的完整性。

TCP 的头部一般是 20 个字节,复杂的头部决定了它的可靠传输。校验和保证了数据的完整性和错误检测;序列号进行了数据的顺序控制;ACK 实现了重传机制,确认和重传;滑动窗口进行流量控制;拥塞控制;三次挥手和四次握手进行连接管理。
序列号解决网络乱序问题,确认应答号解决丢包问题,窗口大小用来进行流量控制,控制位 (ACK 确认和重传、FIN 断开连接、SYN 请求连接)。


IPV4 地址,四位,主机号全为 0 的是网络地址,全为 1 的是广播地址。A 类,第一位范围 1-126,127 是本地环回测试地址。后三位范围是 1-2^24-1,全 1 表示广播地址。
网络号是 IP 地址中的一部分,用于标识某个特定的网络。相同网络号的设备处于同一网络中。主机号是 IP 地址中的另一部分,用于标识网络内的具体设备(主机)。网络地址是指 IP 地址中主机号部分全为 0 的地址,它标识的是整个网络,而不是网络中的具体设备。广播地址是指 IP 地址中主机号部分全为 1 的地址,用于向网络中的所有设备发送信息。
滑动窗口机制中的流量控制主要依赖接收方的接收窗口大小来调节数据传输速率。
慢开始、拥塞避免、快重传、快恢复。
TCP 发送方一开始使用慢开始算法,让拥塞窗口值从 1 开始按指数规律增大,当拥塞窗口值增大到慢开始门限值时,执行拥塞避免算法,让拥塞窗口值按线性加 1 的规律增大,当发生超时重传时,就判断网络很可能出现了拥塞,这时将慢开始门限值更新为发生拥塞时拥塞窗口值的一半,并将拥塞窗口值减少为 1,并重新执行慢开始算法,拥塞窗口值又从 1 开始按指数规律增大,当增大到了新的慢开始门限值时,停止使用慢开始算法,执行拥塞避免算法。
后来又加入了快重传和快恢复,快重传是指收到三个重复确认。快恢复是指快重传之后门限值更新发生拥塞时拥塞窗口值的一半,并将拥塞窗口值也更新为发生拥塞时拥塞窗口值的一半,而不是更新为 1。
第一次握手发送端向接收端发送请求连接 SYN,接收端收到,并且得知发送端具有发送能力。第二次握手接收端向发送端发送确认连接 ACK,发送端得知接收端具有接受能力,并且向发送端发送请求连接,发送端收到,得知接收端具有发送能力。第三次握手,发送端向接收端发送确认连接,接收端得知发送端具有接收能力。这样一来,两端都知道对方具有发送和接收能力,两端建立连接可以正常的传送数据。三次握手才能确认双方的接收和发送能力是否正常。
如果是两次握手,如果某次发送的报文因为网络延误了,又重新发送了一次连接,延误的发送在重新的连接释放之后才到达服务端,这时候服务端会认为发送端又发送了一次连接请求,于是向发送端发送确认请求,同意建立连接,但客户端实际并未发送,所以忽略请求,于是服务端一直等待,浪费资源。
不采用两次或者四次:避免历史连接、避免资源浪费、确认对方收发能力。
通过四次挥手的过程,TCP 协议确保了双方都能够正常地关闭连接,并且所有未完成的数据传输都能顺利结束。第一次挥手:客户端发送 FIN,表示没有数据要发送了。第二次挥手:服务器发送 ACK,确认收到 FIN 请求。第三次挥手:服务器发送 FIN,表示没有数据要发送了。第四次挥手:客户端发送 ACK,确认收到服务器的 FIN 请求,连接关闭。
应用层产生的请求报文里面会含有请求行、请求头、请求体(post 会有,get 没有),
POST 和 PUT。GET POST PUT PATCH TRACE OPTIONS CONNECT HEAD DELETE
Expires 头部字段,绝对时间,HTTP1.1 Cache-Control 头部字段,更灵活)
优点是在缓存有效期内,浏览器无需与服务器通信,直接使用本地缓存资源,提升加载速度。缺点是如果资源在缓存期间发生了变化,用户可能无法及时获取到最新的内容。区别:安全、连接、端口、证书
信息加密解决安全机密,校验机制解决篡改信息,身份证书解决不可信服务器。
主要原因有两个方面:
客户端和服务端建立一个 TCP 连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文。紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接;在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。
tcp_syn_retries 内核参数决定;服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。半连接队列:服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立连接,服务器会把这种状态下的请求连接放在一个队列里,称为半连接队列。完成三次握手建立起连接的就会放入全连接队列中。
SYN 攻击是客户端短时间内伪造大量不存在的 IP 地址,并向服务器不断发送 SYN 包,服务器回复确认包,并且等待客户端确认,由于源地址不存在,因此服务器不断重发直至超时,这些伪造的 SYN 包将长时间占用半连接队列,导致正常的 SYN 请求因队列满而丢弃,从而引起网络拥塞甚至瘫痪。
防御方法:缩短超时时间,增大半连接队列,防火墙过滤,SYN Cookies(是一种在服务器接收到 SYN 请求时,不立即为其分配资源,而是将请求信息编码到 TCP 序列号中的技术。只有当客户端发送 ACK 确认时,服务器才会验证这个序列号并为连接分配资源。),SYN 速率限制。
tcp_orphan_retries 参数控制。如果还是没能收到服务端的第二次挥手(ACK 报文),那么客户端就会断开连接。tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。tcp_orphan_retries 参数控制。也就是说,四次丢失导致的结果都是 FIN 重传。
四次挥手开始时,客户端和服务器都是连接已建立状态,客户端向服务器发送一个终止信号 FIN,进入终止等待 1,服务器收到后发送一个确认信号 ACK,进入关闭等待状态,客户端收到后进入终止等待 2,整个系统进入半连接状态,服务器向客户端发送 FIN 和 ACK,ACK 是对第一次挥手的重复确认,进入最后确认状态,客户端收到后发送 ACK,进入时间等待状态 (2MSL),服务器收到后进入关闭状态,2MSL 之后客户端也进入关闭状态。至于为什么是 2MSL,2 倍的最大报文生存时间,是为了确保 ACK 能够到达服务器,以使其进入关闭状态,如果丢失能够重传,时间重新计时。
需要 TIME-WAIT 状态,主要是两个原因:
**服务器出现大量 TIME_WAIT 状态的原因有哪些?**说明服务器主动断开了很多 TCP 连接,**什么场景下服务端会主动断开连接呢?**第一个场景:HTTP 没有使用长连接;第二个场景:HTTP 长连接超时;第三个场景:HTTP 长连接的请求数量达到上限。
服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
**如果已经建立了连接,但是客户端突然出现故障了怎么办?保活机制:**定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
**如果已经建立了连接,但是服务端的进程崩溃会发生什么?**TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
**保活机制根本上是断开空闲的 TCP 连接的。**如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
**TCP 连接,一端断电和进程崩溃有什么区别?**一端断电触发保活机制,进程崩溃不同。TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?如果有数据传输,只要及时就会赶上超时重传。如果没有就触发保活机制,没有及时的话会断开连接。
HTTPS 中 TLS 和 TCP 能同时握手吗?不管 TLS 握手次数如何,都得先经过 TCP 三次握手后才能进行,因为 HTTPS 都是基于 TCP 传输协议实现的,得先建立完可靠的 TCP 连接才能做 TLS 握手的事情。前两次握手并不能携带数据,第三次可以。
TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?
**TCP 协议有哪些缺陷?**主要有四个方面:升级 TCP 的工作很困难;TCP 建立连接的延迟;TCP 存在队头阻塞问题;网络迁移需要重新建立 TCP 连接。
**服务端没有 listen,客户端发起连接建立,会发生什么?**服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了连接建立,服务端会返回 RST 报文。
**不使用 listen,可以建立 TCP 连接吗?**可以的,客户端是可以自己连自己的形成连接(TCP 自连接),也可以两个客户端同时向对方发出请求建立连接(TCP 同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能建立连接。
没有 accept,能建立 TCP 连接吗?就算不执行 accept() 方法,三次握手照常进行,并顺利建立连接。半连接队列(SYN 队列),服务端收到第一次握手后,会将 sock 加入到这个队列中,队列内的 sock 都处于 SYN_RECV 状态。全连接队列(ACCEPT 队列),在服务端收到第三次握手后,会将半连接队列的 sock 取出,放到全连接队列中。队列里的 sock 都处于 ESTABLISHED 状态。这里面的连接,就等着服务端执行 accept() 后被取出了。建立连接的过程中根本不需要 accept() 参与,执行 accept() 只是为了从全连接队列里取出一条连接。全连接队列(icsk_accept_queue)是个链表,而半连接队列(syn_table)是个哈希表。出于效率考虑设计的,复杂度尽量低。全连接队列:服务端取走连接的过程中,并不关心具体是哪个连接,只要是个连接就行,所以直接从队列头取就行了。这个过程算法复杂度为 O(1)。半连接队列:有一个第三次握手来了,则需要从队列里把相应 IP 端口的连接取出,哈希表,那么查找半连接的算法复杂度就回到 O(1) 了。
总结:
socket 执行 listen 时,内核都会自动创建一个半连接队列和全连接队列。accept 方法 只是为了从全连接队列中拿出一条连接,本身跟三次握手几乎毫无关系。tcp_abort_on_overflow=1,还会直接发 RST 给客户端。SYN Flood 攻击,可以设置 tcp_syncookies,绕开半连接队列。**TCP 是一个可靠的传输协议,那它一定能保证数据不丢失吗?**数据从发送端到接收端,链路很长,任何一个地方都可能发生丢包,几乎可以说丢包不可避免。TCP 只保证传输层的消息可靠性,并不保证应用层的消息可靠性。

没有 accept,能建立 TCP 连接吗?accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。
超时重传时间 RTO 要略微大于报文往返时间 RTT.
三次同样的 ACK 就会触发快速重传。
窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
SYN 报文什么时候情况下会被丢弃?
在 TCP 正常挥手过程中,处于 TIME_WAIT 状态的连接,收到相同四元组的 SYN 后会发生什么?如果处于 TIME_WAIT 状态的连接收到「合法的 SYN」后,就会重用此四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。如果处于 TIME_WAIT 状态的连接收到「非法的 SYN」后,就会再回复一个第四次挥手的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号,就回 RST 报文给服务端。RST 标志用于强制关闭一个 TCP 连接。
TCP 是面向字节流的协议,UDP 是面向报文的协议。是因为操作系统对 TCP 和 UDP 协议的**发送方的机制不同。**当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,**每个 UDP 报文就是一个用户消息的边界。**当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文。因此,我们不能认为一个用户消息对应一个 TCP 报文,正因为这样,所以 TCP 是面向字节流的协议。当两个消息的某个部分内容被分到同一个 TCP 报文时,就是我们常说的 TCP 粘包问题,这时接收方不知道消息的边界的话,是无法读出有效的消息。粘包的问题出现是因为不知道一个用户消息的边界在哪,所以解决办法是:固定长度的消息、特殊字符作为边界、长度前缀、自定义协议来明确数据包的结构和边界。
TCP 可靠传输的特性(序列号、确认应答、超时重传、流量控制、拥塞控制)
QUIC 通过单向递增的 Packet Number,配合 Stream ID 与 Offset 字段信息,可以支持乱序确认而不影响数据包的正确组装,摆脱了 TCP 必须按顺序确认应答 ACK 的限制,解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。
QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。
Stream 级别的流量控制:Stream 可以认为就是一条 HTTP 请求,每个 Stream 都有独立的滑动窗口,所以每个 Stream 都可以做流量控制,防止单个 Stream 消耗连接(Connection)的全部接收缓冲。Connection 流量控制:限制连接中所有 Stream 相加起来的总字节数,防止发送方超过连接的缓冲容量。
QUIC 是处于应用层的,应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,所以 TCP 拥塞控制算法迭代速度是很慢的。而 QUIC 可以随浏览器更新,QUIC 的拥塞控制算法就可以有较快的迭代速度。TCP 更改拥塞控制算法是对系统中所有应用都生效,无法根据不同应用设定不同的拥塞控制策略。但是因为 QUIC 处于应用层,所以就可以针对不同的应用设置不同的拥塞控制算法,这样灵活性就很高了。
HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的'记录',再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接。QUIC 协议没有用四元组的方式来'绑定'连接,而是通过连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以'无缝'地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
关系型数据库一般将数据存储在表格中,核心思想是数据之间的关系可以通过表格中的键(如主键和外键)来表示和管理。如:MySQL。
非关系型数据库通常不使用表格结构,而是采用其他模型,如键值对、文档、图、列族等。如:Redis。NoSQL 数据库在处理海量、快速变化的数据时表现出色。
列出数据库:show databases;创建数据库:create database mysql_test;切换数据库:use mysql_test;列出表:show tables;创建表:create table student(s_id int, s_name varchar(8), s_birth date);表的列名在前,数据类型在后。在表中插入数据:insert into student values (1,'赵雷','1990-01-01'), (8,'王菊','1990-01-20');
select * 中的 * 符号扩展为表上的所有列。索引是数据的目录,就是帮助存储引擎快速获取数据的一种数据结构。存储引擎,说白了就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。MySQL 存储引擎有 MyISAM、InnoDB、Memory,其中 InnoDB 是在 MySQL 5.5 之后成为默认的存储引擎。索引和数据就是位于存储引擎中。

为什么 MySQL InnoDB 选择 B+tree 作为索引的数据结构?这是因为:B+ 树和 B 树相比,B+Tree 只在叶子节点存储数据,而 B 树的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。B+ 树和二叉树相比,对于有 N 个叶子节点的 B+Tree,其搜索复杂度为 O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。B+ 树和 Hash 表相比,Hash 表不适合做范围查询,它更适合做等值的查询。
从物理存储的角度来看,索引分为聚簇索引(主键索引)、二级索引(辅助索引)。
主键索引就是去用一个 B+ 树去查主键字段,叶子节点存储的是整行数据,通过主键就可以获知全部信息,而二级索引则是去查其他字段,比如学生表中的姓名、成绩等列的字段,叶子节点存储的是该二级字段以及主键,通过二级字段可以获知主键,然后通过回表去查该主键对应的整行数据,所以这种方式可能需要两个 B+ 树才能查到数据。
要设计一个适合 MySQL 索引的数据结构,至少满足以下要求:能在尽可能少的磁盘的 I/O 操作中完成查询工作;要能高效地查询某一个记录,也要能高效地执行范围查找。
索引数据最好能按顺序排列,这样可以使用「二分查找法」高效定位数据。
用数组来实现线性排序的数据虽然简单好用,但是插入新元素的时候性能太低。二叉查找树的特点是一个节点的左子树的所有节点都小于这个节点,右子树的所有节点都大于这个节点。当每次插入的元素都是二叉查找树中最大的元素,二叉查找树就会退化成了一条链表,查找数据的时间复杂度变成了 O(n)。由于树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作(假设一个节点的大小「小于」操作系统的最小读写单位块的大小),也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。
主要是在二叉查找树的基础上增加了一些条件约束:每个节点的左子树和右子树的高度差不能超过 1。不管平衡二叉查找树还是红黑树,都会随着插入的元素增多,而导致树的高度变高,这就意味着磁盘 I/O 操作次数多,会影响整体数据查询的效率。当树的节点越多的时候,并且树的分叉数 M 越大的时候,M 叉树的高度会远小于二叉树的高度。
为了解决降低树的高度的问题,后面就出来了 B 树,它不再限制一个节点就只能有 2 个子节点,而是允许 M 个子节点 (M>2),从而降低树的高度。但是 B 树的每个节点都包含数据(索引 + 记录),而用户的记录数据的大小很有可能远远超过了索引数据,这就需要花费更多的磁盘 I/O 操作次数来读到「有用的索引数据」。
B+ 树与 B 树差异的点,主要是以下这几点:
所以,性能差距如下:
B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O 次数会更少。
B+ 树的插入和删除效率更高。
B+ 树所有叶子节点间还有一个链表进行连接,这种设计对范围查找非常有帮助。
事务是数据库操作的基本单元,事务内的语句具有一致性,要么全部执行成功,要么全部执行失败。事务的四个特性 ACID:原子性,一致性,隔离性,持久性。原子性要么全成功,要么全失败。一致性数据库状态要保持一致性。隔离性并发进程不会互相影响。持久性事务结束操作之后数据库的修改永久保存。
在同时处理多个事务的时候,就可能出现脏读、不可重复读、幻读的问题。
如果在上面这种情况事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
脏读:读到其他事务未提交的数据;不可重复读:前后读取的数据不一致;幻读:前后读取的记录数量不一致。
SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
全局锁、表级锁(表锁、元数据锁、意向锁、AUTO-INC 锁)和行锁(Record Lock,记录锁、Gap Lock,间隙锁、Next-Key Lock,两锁组合)
代码区:存储可执行代码(程序指令)。
全局区:存储全局变量和静态变量(已初始化和未初始化)。
堆区:用于动态内存分配,由程序员管理。
栈区:存储函数调用中的局部数据(局部变量、参数、返回地址)。
堆和栈是用来内存分配和释放的两种不同的内存区域,内存管理就是内存分配和释放。栈的存储空间比较小,用于存储局部变量、函数参数、返回值,适合小数据结构的存储,内存分配和释放速度快,自动管理(自动分配、自动销毁)。堆的存储空间较大,用于动态内存分配,适合大型数据结构存储,内存分配和释放速度较慢,手动管理(malloc、new、free、delete)。
智能指针是 C++ 标准库中的一种工具,用于自动管理动态内存,避免内存泄漏和指针悬挂问题。智能指针主要有以下几种类型:
std::unique_ptrunique_ptr 表示独占的所有权,即一个指针对象在任何时刻只有一个所有者。unique_ptr 不能被复制,只有通过 std::move 转移所有权。std::shared_ptrshared_ptr 允许多个智能指针共享同一块内存。当最后一个 shared_ptr 释放时,内存才会被释放。shared_ptr 都有一个引用计数,用于记录有多少 shared_ptr 共享该内存块。std::weak_ptrweak_ptr 不会影响引用计数,不能直接访问资源,需要先提升为 shared_ptr。shared_ptr 之间的循环引用,避免内存泄漏。看 const 后面跟的什么,跟的 int 就是修饰数据,数据不可变,指针指向可变,称为指针变量 const int *p。跟的 p 就是修饰指针,指针指向不可变,数据可变,称为变量指针 int * const p。
作用:用于记录程序中不可更改的数据
C++ 定义常量两种方式
静态成员就是在成员变量和成员函数前加上关键字 static,称为静态成员,可以通过对象和类名两种方式访问。静态成员分为:
静态成员变量:1.所有对象共享同一份数据;2.在编译阶段分配内存;3.类内声明,类外初始化; 静态成员函数:1.所有对象共享同一个函数;2.静态成员函数只能访问静态成员变量。(因为静态属于类,非静态属于对象,静态成员变量是由类调用的,不用区分是类中的哪个对象调用,但是非静态成员变量是由对象调用的,这时候用静态成员函数去调用非静态成员变量,无法区分到底是哪个对象调用的变量)
静态成员是指属于类本身而不是类的实例的成员。静态成员有以下意义:
析构函数可以是虚函数吗?
std::vector 是 C++ 标准库中的动态数组,它会根据需要自动调整其容量以容纳更多的元素。std::vector 会在内部维护一个存储空间的容量(capacity),这个容量通常会大于或等于当前元素的数量(size)。当向 std::vector 中添加元素并且元素的数量超过当前容量时,vector 会重新分配更大的内存空间来容纳这些元素。这个过程包括:
vector 会分配一个更大的内存块以容纳更多的元素。std::vector 的初始容量和扩容策略取决于具体的实现,但一般来说,std::vector 的初始容量为 0。也就是说,当你创建一个空的 vector 对象时,它通常没有分配任何内存用于存储元素。
std::vector 的初次扩容通常发生在以下几种情况:
vector 中添加第一个元素时,vector 会进行初次扩容。此时,它会分配一个足够容纳至少一个元素的内存块。许多实现将初始容量设置为较小的值,比如 1 或 4,以提高性能。vector,比如 vector(size_type count, const T& value) 或 vector(std::initializer_list<T> init),vector 会根据提供的初始元素数量预分配内存,以减少后续的扩容次数。在添加更多元素并超过当前容量时,std::vector 会进行扩容。扩容通常遵循以下策略:
vector 的容量通常会增加到当前容量的两倍,这样可以减少扩容的次数和内存分配的开销。(2 倍策略通常在性能和内存利用之间提供了一个好的折衷)扩容过程中,std::vector 需要重新分配内存,复制现有元素到新的内存块中,并释放旧的内存块。这个过程是自动进行的,用户不需要手动处理。
冒泡排序、选择排序、**插入排序、**快速排序、归并排序、堆排序

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online