计网OS相关知识

计算机网络

网络模型

OSI参考模型

  • 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

TCP/IP模型

  • 网络接口层、网络层、传输层、应用层

应用层

应用层有哪些协议

  • HTTP、HTTPS、CDN、DNS、FTP

HTTP报文有哪些部分

请求报文

  • 请求行:包含请求方法、请求目标(URL或URI)和HTTP协议版本。
  • 请求头部:包含关于请求的附加信息,如Host、User-Agent、Content-Type等。
  • 空行:请求头部和请求体之间用空行分隔。
  • 请求体:可选,包含请求的数据,通常用于POST请求等需要传输数据的情况。

响应报文

  • 状态行:包含HTTP协议版本、状态码和状态信息。
  • 响应头部:包含关于响应的附加信息,如Content-Type、Content-Length等。
  • 空行:响应头部和响应体之间用空行分隔。
  • 响应体:包含响应的数据,通常是服务器返回的HTML、JSON等内容。

HTTP常用状态码

  • 1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
  • 2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。200:请求成功;
  • 3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。301:永久重定向;302:临时重定向;
  • 4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。404:无法找到此页面;405:请求的方法类型不支持;
  • 5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。500:服务器内部出错。

301、302状态码分别是什么

  • 「301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
  • 「302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。
  • 301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。

502和504区别

  • 502 Bad Gateway:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
  • 504 Gateway Time-out:作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器收到响应。

GET和POST的区别

  • GET 的语义是从服务器获取指定的资源,GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。
  • POST 的语义是根据请求负荷(报文body)对指定的资源做出处理,POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。
  • GET安全幂等,POST不安全不幂等,浏览器会缓存GET请求的数据,不会缓存POST请求的数据;会把GET请求的数据保存为书签,不会把POST请求的数据保存为书签。

HTTP长连接

  • 客户端与服务端通信前,需要建立连接,建立后发送请求,然后释放连接。如果每次请求都要经历这个过程会很浪费时间。
  • HTTP 的 Keep-Alive 实现了不断开TCP连接,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接。
  • HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

HTTP、HTTPS默认的端口

  • HTTP:80、HTTPS:443

HTTP1.1怎么对请求做拆包,具体来说怎么拆的?

  • 在HTTP/1.1中,请求的拆包是通过”Content-Length”头字段来进行的。该字段指示了请求正文的长度,服务器可以根据该长度来正确接收和解析请求。

HTTP断点重传

  • 断点续传是HTTP/1.1协议支持的特性。实现断点续传的功能,需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段。
  • 例:
    • 客户端开始下载一个1024K的文件,服务端发送Accept-Ranges: bytes来告诉客户端,其支持带Range的请求
    • 假如客户端下载了其中512K时候网络突然断开了,过了一会网络可以了,客户端再下载时候,需要在HTTP头中申明本次需要续传的片段:Range:bytes=512000-这个头通知服务端从文件的512K位置开始传输文件,直到文件内容结束
    • 服务端收到断点续传请求,从文件的512K位置开始传输,并且在HTTP头中增加:Content-Range:bytes 512000-/1024000,Content-Length: 512000。并且此时服务端返回的HTTP状态码应该是206 Partial Content。如果客户端传递过来的Range超过资源的大小,则响应416 Requested Range Not Satisfiable
  • 通过上面流程可以看出:断点续传中4个HTTP头不可少的,分别是Range头、Content-Range头、Accept-Ranges头、Content-Length头。其中第一个Range头是客户端发过来的,后面3个头需要服务端发送给客户端。下面是它们的说明:
  • Accept-Ranges: bytes:这个值声明了可被接受的每一个范围请求, 大多数情况下是字节数 bytes
  • Range: bytes=开始位置-结束位置:Range是浏览器告知服务器所需分部分内容范围的消息头。

HTTP为什么不安全

  • HTTP是明文传输,所以安全上会有以下风险:
    • 窃听风险,通信链路上可以获取通信内容
    • 篡改风险,强制植入垃圾广告
    • 冒充风险,例如冒充淘宝
  • HTTPS再HTTP和TCP层之间加入了SSL/TLS协议,解决了上述风险
    • 信息加密,交互信息无法窃取
    • 校验机制,无法篡改通信内容,篡改了就不能正常显示
    • 身份证书:证明淘宝是真的淘宝网

HTTP和HTTPS的区别

  • HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
  • HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
  • 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
  • HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。

TLS握手过程

客户端→服务端:Client Hello

  • 客户端生成一个随机数
  • 将自己支持的加密算法、TLS版本、随机数发送给服务端

服务端→客户端:Server Hello

  • 服务端生成一个随机数
  • 将随机数、确认TLS版本号、使用的密码套件(RSA)、使用的证书发送给客户端

客户端→服务端:Client Key Exchange,Change Cipher Spec,Encrypted Handshake Message

  • 检查证书是否有效,是否由可信CA签发,是否过期
  • 若有效,则从证书中取出服务端公钥,生成一个“预主密钥”(Pre-Master Secret),并用服务器公钥加密后发送给服务器。
  • 使用客户端随机数、服务端随机数、pre-matster算出会话密钥,之后改用会话密钥加密通道

服务端→客户端:Change Cipher Spec,Encrypted Handshake Message

  • 服务端使用私钥解密出预主密钥
  • 使用客户端随机数、服务端随机数、pre-matster算出会话密钥,之后改用会话密钥加密通道
  • 双方各发送一条Finish消息,表示握手完成

HTTP进行TCP连接之后,在什么情况下会中断

  • 当服务端或者客户端执行 close 系统调用的时候,会发送FIN报文,就会进行四次挥手的过程
  • 当发送方发送了数据之后,接收方超过一段时间没有响应ACK报文,发送方重传数据达到最大次数的时候,就会断开TCP连接
  • 当HTTP长时间没有进行请求和响应的时候,超过一定的时间,就会释放连接

HTTP、SOCKET和TCP的区别

  • HTTP是应用层协议,定义了客户端和服务器之间交换的数据格式和规则;Socket是通信的一端,提供了网络通信的接口;TCP是传输层协议,负责在网络中建立可靠的数据传输连接。它们在网络通信中扮演不同的角色和层次。

  • HTTP是一种用于传输超文本数据的应用层协议,用于在客户端和服务器之间传输和显示Web页面。

  • Socket是计算机网络中的一种抽象,用于描述通信链路的一端,提供了底层的通信接口,可实现不同计算机之间的数据交换。

  • TCP是一种面向连接的、可靠的传输层协议,负责在通信的两端之间建立可靠的数据传输连接。

DNS

  • DNS的全称是Domain Name System(域名系统),它是互联网中用于将域名转换为对应IP地址的分布式数据库系统。DNS扮演着重要的角色,使得人们可以通过易记的域名访问互联网资源,而无需记住复杂的IP地址。默认端口53。
  • 在域名中,越靠右的位置表示其层级越高。
  • 域名的层级关系类似一个树状结构:
    • 根 DNS 服务器(.)
    • 顶级域 DNS 服务器(.com)
    • 权威 DNS 服务器(server.com)

DNS 域名解析的工作流程?

  • 客户端向本地DNS服务器发送请求,询问www.server.com 的 IP 是什么
  • 本地域名服务器收到以后,会查找缓存中是否存在这个域名,若有直接返回IP,若没有,本地DNS会向根域名服务器询问
  • 根域名服务器收到后,发现后置是.com,则将.com顶级域名服务器地址返回给本地DNS
  • 本地DNS收到后,向.com顶级域名服务器询问
  • 顶级域名服务器收到后,将server.com权威DNS服务器地址返回
  • 本地DNS向server.com权威DNS服务器询问
  • 权威DNS服务i其将IP返回
  • 本地DNS将IP返回给客户端
    DNS解析

DNS底层使用TCP还是UDP

  • 低延迟: UDP是一种无连接的协议,不需要在数据传输前建立连接,因此可以减少传输时延,适合DNS这种需要快速响应的应用场景。
  • 简单快速: UDP相比于TCP更简单,没有TCP的连接管理和流量控制机制,传输效率更高,适合DNS这种需要快速传输数据的场景。
  • 轻量级:UDP头部较小,占用较少的网络资源,对于小型请求和响应来说更加轻量级,适合DNS这种频繁且短小的数据交换。

HTTP到底是不是无状态的?

  • HTTP是无状态的,这意味着每个请求都是独立的,服务器不会在多个请求之间保留关于客户端状态的信息。在每个HTTP请求中,服务器不会记住之前的请求或会话状态,因此每个请求都是相互独立的。
  • 虽然HTTP本身是无状态的,但可以通过一些机制来实现状态保持,其中最常见的方式是使用Cookie和Session来跟踪用户状态。通过在客户端存储会话信息或状态信息,服务器可以识别和跟踪特定用户的状态,以提供一定程度的状态保持功能。

携带Cookie的HTTP请求是有状态还是无状态的?Cookie是HTTP协议簇的一部分,那为什么还说HTTP是无状态的?

  • 携带Cookie的HTTP请求实际上是可以在一定程度上实现状态保持的,因为Cookie是用来在客户端存储会话信息和状态信息的一种机制。当浏览器发送包含Cookie的HTTP请求时,服务器可以通过读取这些Cookie来识别用户、管理会话状态以及保持特定的用户状态。因此,可以说即使HTTP本身是无状态的协议,但通过Cookie的使用可以实现一定程度的状态保持功能。
  • HTTP被描述为“无状态”的主要原因是每个HTTP请求都是独立的,服务器并不保存关于客户端的状态信息,每个请求都需要提供足够的信息来理解请求的意图。这样的设计使得Web系统更具有规模化和简单性,但也导致了一些挑战,比如需要额外的机制来处理用户状态和会话管理。
  • 虽然Cookie是HTTP协议簇的一部分,但是HTTP协议在设计初衷上仍然保持无状态特性,即每个请求都是相互独立的。使用Cookie只是在无状态协议下的一种补充机制,用于在客户端存储状态信息以实现状态保持。

cookie和session有什么区别?

  • 存储位置:Cookie的数据存储在客户端(通常是浏览器)。当浏览器向服务器发送请求时,会自动附带Cookie中的数据。Session的数据存储在服务器端。服务器为每个用户分配一个唯一的Session ID,这个ID通常通过Cookie或URL重写的方式发送给客户端,客户端后续的请求会带上这个Session ID,服务器根据ID查找对应的Session数据。
  • 数据容量:单个Cookie的大小限制通常在4KB左右,而且大多数浏览器对每个域名的总Cookie数量也有限制。由于Session存储在服务器上,理论上不受数据大小的限制,主要受限于服务器的内存大小。
  • 安全性:Cookie相对不安全,因为数据存储在客户端,容易受到XSS(跨站脚本攻击)的威胁。不过,可以通过设置HttpOnly属性来防止JavaScript访问,减少XSS攻击的风险,但仍然可能受到CSRF(跨站请求伪造)的攻击。Session通常认为比Cookie更安全,因为敏感数据存储在服务器端。但仍然需要防范Session劫持(通过获取他人的Session ID)和会话固定攻击。
  • 生命周期:Cookie可以设置过期时间,过期后自动删除。也可以设置为会话Cookie,即浏览器关闭时自动删除。Session在默认情况下,当用户关闭浏览器时,Session结束。但服务器也可以设置Session的超时时间,超过这个时间未活动,Session也会失效。
  • 性能:使用Cookie时,因为数据随每个请求发送到服务器,可能会影响网络传输效率,尤其是在Cookie数据较大时。使用Session时,因为数据存储在服务器端,每次请求都需要查询服务器上的Session数据,这可能会增加服务器的负载,特别是在高并发场景下。

token,session,cookie的区别?

  • session存储于服务器,可以理解为一个状态列表,拥有一个唯一识别符号sessionId,通常存放于cookie中。服务器收到cookie后解析出sessionId,再去session列表中查找,才能找到相应session,依赖cookie。
  • cookie类似一个令牌,装有sessionId,存储在客户端,浏览器通常会自动添加。
  • token也类似一个令牌,无状态,用户信息都被加密到token中,服务器收到token后解密就可知道是哪个用户,需要开发者手动添加。

如果客户端禁用了cookie,session还能用吗?

  • 默认情况下禁用 Cookie 后,Session 是无法正常使用的,因为大多数 Web 服务器都是依赖于 Cookie 来传递 Session 的会话 ID 的。

  • 客户端浏览器禁用 Cookie 时,服务器将无法把会话 ID 发送给客户端,客户端也无法在后续请求中携带会话 ID 返回给服务器,从而导致服务器无法识别用户会话。

  • 但是,有几种方法可以绕过这个问题,尽管它们可能会引入额外的复杂性和/或降低用户体验:

    • URL重写:每当服务器响应需要保持状态的请求时,将Session ID附加到URL中作为参数。例如,原本的链接http://example.com/page变为http://example.com/page;jsessionid=XXXXXX,服务器端需要相应地解析 URL 来获取 Session ID,并维护用户的会话状态。这种方式的缺点是URL变得不那么整洁,且如果用户通过电子邮件或其他方式分享了这样的链接,可能导致Session ID的意外泄露。
    • 隐藏表单字段:在每个需要Session信息的HTML表单中包含一个隐藏字段,用来存储Session ID。当表单提交时,Session ID随表单数据一起发送回服务器,服务器通过解析表单数据中的 Session ID 来获取用户的会话状态。这种方法仅适用于通过表单提交的交互模式,不适合链接点击或Ajax请求。

如果我把数据存储到 localStorage,和Cookie有什么区别?

  • 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合;
  • 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据;
  • 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除;
  • 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些;

什么数据应该存在到cookie,什么数据存放到 Localstorage

  • Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据。

JWT 令牌和传统方式有什么区别?

  • 无状态性:JWT是无状态的令牌,不需要在服务器端存储会话信息。相反,JWT令牌中包含了所有必要的信息,如用户身份、权限等。这使得JWT在分布式系统中更加适用,可以方便地进行扩展和跨域访问。
  • 安全性:JWT使用密钥对令牌进行签名,确保令牌的完整性和真实性。只有持有正确密钥的服务器才能对令牌进行验证和解析。这种方式比传统的基于会话和Cookie的验证更加安全,有效防止了CSRF(跨站请求伪造)等攻击。
  • 跨域支持:JWT令牌可以在不同域之间传递,适用于跨域访问的场景。通过在请求的头部或参数中携带JWT令牌,可以实现无需Cookie的跨域身份验证。

JWT 令牌都有哪些字段?

头部

  • 头部用于说明令牌的类型和签名算法

负载

  • 载荷是 JWT 的核心部分,包含各种“声明”(Claims),即关于用户和令牌的信息。

签名

  • 签名用于验证令牌是否被篡改。

JWT 令牌为什么能解决集群部署,什么是集群部署?

  • 在传统的基于会话和Cookie的身份验证方式中,会话信息通常存储在服务器的内存或数据库中。但在集群部署中,不同服务器之间没有共享的会话信息,这会导致用户在不同服务器之间切换时需要重新登录,或者需要引入额外的共享机制(如Redis),增加了复杂性和性能开销。
  • 而JWT令牌通过在令牌中包含所有必要的身份验证和会话信息,使得服务器无需存储会话信息,从而解决了集群部署中的身份验证和会话管理问题。当用户进行登录认证后,服务器将生成一个JWT令牌并返回给客户端。客户端在后续的请求中携带该令牌,服务器可以通过对令牌进行验证和解析来获取用户身份和权限信息,而无需访问共享的会话存储。
  • 由于JWT令牌是自包含的,服务器可以独立地对令牌进行验证,而不需要依赖其他服务器或共享存储。这使得集群中的每个服务器都可以独立处理请求,提高了系统的可伸缩性和容错性。

jwt的缺点是什么?

  • JWT 一旦派发出去,在失效之前都是有效的,没办法即使撤销JWT。
  • 要解决这个问题的话,得在业务层增加判断逻辑,比如增加黑名单机制。使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。

JWT 令牌如果泄露了,怎么解决,JWT是怎么做的?

  • 及时失效令牌:当检测到JWT令牌泄露或存在风险时,可以立即将令牌标记为失效状态。服务器在接收到带有失效标记的令牌时,会拒绝对其进行任何操作,从而保护用户的身份和数据安全。
  • 刷新令牌:JWT令牌通常具有一定的有效期,过期后需要重新获取新的令牌。当检测到令牌泄露时,可以主动刷新令牌,即重新生成一个新的令牌,并将旧令牌标记为失效状态。这样,即使泄露的令牌被恶意使用,也会很快失效,减少了被攻击者滥用的风险。
  • 使用黑名单:服务器可以维护一个令牌的黑名单,将泄露的令牌添加到黑名单中。在接收到令牌时,先检查令牌是否在黑名单中,如果在则拒绝操作。这种方法需要服务器维护黑名单的状态,对性能有一定的影响,但可以有效地保护泄露的令牌不被滥用。

使用session登录流程

  • 用户向服务器发送用户名和密码。
  • 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
  • 服务器向用户返回一个 session_id,写入用户的 Cookie。
  • 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
  • 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

Token怎么保存

Local Storage(本地存储)

  • 优点:Local Storage 提供了较大的存储空间(一般为5MB),且不会随着HTTP请求一起发送到服务器,因此不会出现在HTTP缓存或日志中。
  • 缺点:存在XSS(跨站脚本攻击)的风险,恶意脚本可以通过JavaScript访问到存储在Local Storage中的JWT,从而盗取用户凭证。

Session Storage(会话存储)

  • 优点:与Local Storage类似,但仅限于当前浏览器窗口或标签页,当窗口关闭后数据会被清除,这在一定程度上减少了数据泄露的风险。
  • 缺点:用户体验可能受影响,因为刷新页面或在新标签页打开相同应用时需要重新认证。
  • 优点:可以设置HttpOnly标志来防止通过JavaScript访问,减少XSS攻击的风险;可以利用Secure标志确保仅通过HTTPS发送,增加安全性。
  • 缺点:大小限制较小(通常4KB),并且每次HTTP请求都会携带Cookie,可能影响性能;设置不当可能会受到CSRF(跨站请求伪造)攻击。

为什么有HTTP协议了?还要用RPC?

性能差异

  • HTTP 是文本协议,每次请求都要带上大量的头信息(如 Content-Type、User-Agent 等),数据封装臃肿。
  • RPC 通常是二进制协议(如 gRPC 使用 Protobuf),数据体积小,传输快,适合高并发场景。

调用方式

  • RPC 框架(如 Dubbo、gRPC)支持自动生成客户端代码,调用远程服务就像调用本地方法一样。
  • HTTP 调用则需要手动构造请求、处理响应,开发成本更高。

使用场景

  • RPC:微服务之间调用
  • HTTP:开放API或Web应用

HTTP长连接与WebSocket有什么区别?

  • 全双工和半双工:TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支持全双工的 WebSocket 协议。
  • 应用场景区别:在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送(comet)的效果。对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。

Nginx(应用层)有哪些负载均衡算法?

  • 轮询:按照顺序依次将请求分配给后端服务器。这种算法最简单,但是也无法处理某个节点变慢或者客户端操作有连续性的情况。
  • IP哈希:根据客户端IP地址的哈希值来确定分配请求的后端服务器。适用于需要保持同一客户端的请求始终发送到同一台后端服务器的场景,如会话保持。
  • URL哈希:按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,可以进一步提高后端缓存服务器的效率。
  • 最短响应时间:按照后端服务器的响应时间来分配请求,响应时间短的优先分配。适用于后端服务器性能不均的场景,能够将请求发送到响应时间快的服务器,实现负载均衡。
  • 加权轮询:按照权重分配请求给后端服务器,权重越高的服务器获得更多的请求。适用于后端服务器性能不同的场景,可以根据服务器权重分配请求,提高高性能服务器的利用率。

传输层

说一下TCP头部

  • 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
  • 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
  • 控制位:
    • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
    • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
    • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
    • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

TCP三次握手

过程

TCP三次握手过程

  • 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态

SYN报文

  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
    SYN报文

SYN+ACK报文

  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
    SYNACK报文

ACK报文

  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。
    ACK报文
  • ※第三次握手是可以携带数据的,前两次握手是不可以携带数据的

使用三次握手的原因

避免历史链接

  • RFC 793 指出的 TCP 连接使用三次握手的首要原因:

    The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

  • 若只是两步握手:
    • 客户端给服务端发送了一个SYN报文,序号为90,但由于网络拥塞,这个报文传输的慢。接着客户端宕机重启,重新发送了一个100号的SYN报文
    • ※此时90号先到达服务端,服务端回应了一个ACK为91的报文,此时服务端进入ESTABLISHED状态
    • 客户端接收到ACK为91的报文,但实际上客户端想要接收到ACK为101的报文,此时客户端发送RST报文,连接中止
  • 若只是两步握手的话,在※处服务端就进入了半连接状态,浪费服务器资源
  • 三次握手的过程:
    • 客户端给服务端发送了一个SYN报文,序号为90,但由于网络拥塞,这个报文传输的慢。接着客户端宕机重启,重新发送了一个100号的SYN报文
    • ※此时90号先到达服务端,服务端回应了一个ACK为91的报文
    • 客户端接收到ACK为91的报文,但实际上客户端想要接收到ACK为101的报文,此时客户端发送RST报文,连接中止
    • 100号SYN报文到达服务端,服务端回应ACK为101的报文
    • 客户端收到,建立连接,并发送ACK报文
    • 服务端收到,连接建立

同步双方初始序列号

  • 连接时,客户端向服务端发送一个初始序列号(SYN),服务端收到这个序列号后需要确认,同时生成自己的序列号并发送到客户端(SYN+ACK)
  • 客户端收到服务端的序列号,告诉服务端已经收到(ACK)
  • 作用:
    • 接收方可以去除重复的数据;
    • 接收方可以根据数据包的序列号按序接收;
    • 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);

避免资源浪费

  • 如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
  • 如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
  • 即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。

TCP 三次握手,客户端第三次发送的确认包丢失了发生什么?

  • 客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。
  • 因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,每次重传都会等待2倍的超时时间,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
    • 若tcp_synack_retries 为 2,当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
  • 注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。

三次握手和 accept 是什么关系? accept 做了哪些事情?

accept系统调用

  • accept() 是一个 服务器端的系统调用,用于从内核中“已完成连接队列”中取出一个连接,正式建立与客户端的通信通道。返回一个新的套接字,并设置客户端的地址信息

accept()做的事

accept与三次握手

  • 默认阻塞等待连接
    • 如果“已完成连接队列”为空,accept() 会阻塞,直到有客户端完成三次握手。
  • 从队列中取出连接:
    • 内核维护两个队列:
      • 半连接队列(握手未完成)
      • 已连接队列(握手完成)
    • accept() 从已连接队列中取出一个连接。
  • 返回新的 socket 描述符:
    • 这个新 socket 是专门用于与该客户端通信的,和原来的监听 socket 是分开的。
  • 获取客户端地址信息:
    • 如果你传入 addr 参数,accept() 会填充客户端的 IP 和端口信息。

关系

阶段 说明
🔄 三次握手 TCP 协议自动完成:客户端发送 SYN,服务器回应 SYN+ACK,客户端再发 ACK
🎯 accept() 三次握手完成后,服务器调用 accept() 从内核的“已完成连接队列”中取出连接,返回一个新的 socket,用于后续通信

客户端发送的第一个 SYN 报文,服务器没有收到怎么办?

  • 会超时重传,第一次超时时间一般是1秒,写死在内核中,更改的话需要重新编译内核
  • 超时重传次数有上限,SYN报文重传次数由 tcp_syn_retries内核参数控制,Linux中默认为5
  • 每次超时重传都会等待2倍时间,第一次1秒,第二次2秒,第三次4秒…
  • 超时重传最大次数后,客户端从SYN_SENT状态变为CLOSE状态

服务器收到第一个 SYN 报文,回复的 SYN + ACK 报文丢失了怎么办?

  • 因为SYNACK报文是对SYN报文的回复,所以这个报文丢失会导致客户端重传SYN报文
    • SYN报文重传次数由 tcp_syn_retries内核参数控制,Linux中默认为5
  • 因为SYNACK报文是第二次握手,所以丢失以后服务端也会进行重传
    • SYNACK重传次数由tcp_synack_retries内核参数决定,默认5

第一次握手,客户端发送SYN报后,服务端回复ACK报,那这个过程中服务端内部做了哪些工作?

  • 服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
  • 不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。

大量SYN包发送给服务端服务端会发生什么事情?

  • SYN Flood 攻击,有可能会导致TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。

解决方案一:调大netdev_max_backlog

  • 这个参数用来保存内核来不及处理的数据包的队列大小,默认1000。当网卡接收的速度大于内核处理速度,会满。
  • 这一方法可以缓解网卡拥堵,降低丢包率,但不能防止半连接队列打满

解决方案二:增大SYN半连接队列

  • 增大 net.ipv4.tcp_max_syn_backlog,控制半连接队列的最大长度
  • 增大 listen() 函数中的 backlog,这是你在代码中设置的队列长度
  • 增大 net.core.somaxconn,控制全连接队列的最大长度,也影响半连接队列的处理能力
  • 最终队列大小是min(tcp_max_syn_backlog, somaxconn, backlog),所以三个参数都要调大才有效。

解决方案三:开启 net.ipv4.tcp_syncookies——验证是否真心连接

  • 当半连接队列已满,再收到SYN报文时,并不丢弃,但也不分配连接资源
  • 而是先构造一个特殊序列号(SYN Cookie),包含了客户端和服务端的IP、端口、时间戳、MSS(客户端声明的最大报文段长度)等
  • 发送 SYN+ACK 报文,序列号就是 Cookie
  • 客户端回应 ACK 后,服务器从 ACK 中恢复出原始连接信息,如果合法,将连接放入accept队列
  • 有三个值,0表示不开启,1表示SYN队列满再使用,2表示无条件开启

减少SYNACK重传次数

  • 因为被SYN攻击时,会有大量处于SYN_RCVD的连接,这个状态的连接会重传SYNACK报文
  • 只要减少重传次数,就能更快断开连接

TCP四次挥手(FIN-ACK-FIN-ACK)

TCP四次挥手

客户端FIN

  • 客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;

服务端ACK、服务端FIN

  • 服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会在接收缓冲区中插入一个特殊的“文件结束符”(EOF),这个 EOF 会被放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
  • 接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read() 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;

客户端ACK

  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
    客户端经过 2MSL(报文最大生存时间) 时间之后,也进入 CLOSE 状态;

为什么4次握手中间两次不能变成一次?

  • 服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序:
    • 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
    • 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,
  • 从上面过程可知,是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送。

第二次和第三次挥手能合并嘛

  • 没数据发送可以合并

第三次挥手一直没发,会发生什么?

  • 若客户端使用shutdown()关闭,客户端会一直停留在FIN_WAIT_2状态
  • 若使用close(),客户端维持一段FIN_WAIT_2后,再关闭,时间受tcp_fin_timeout控制

第二次和第三次挥手之间,主动断开的那端能干什么

  • 如果主动断开的一方,是调用了 shutdown 函数来关闭连接,并且只选择了关闭发送能力且没有关闭接收能力的话,那么主动断开的一方在第二次和第三次挥手之间还可以接收数据。
  • 也就是说,在二三挥手之间,可以由服务端发送数据,客户端正常发送ACK

断开连接时客户端 FIN 包丢失,服务端的状态是什么?

  • 正常情况下,客户端会进入FIN_WAIT1状态,服务端进入CLOSE_WAIT状态
  • 客户端会超时重传,服务端依旧是ESTASBLISH状态

为什么四次挥手之后要等2MSL?

  • MSL为报文最大生存时间,是任何报文在网络上存在的最长时间,超过此时间报文会被丢弃,TCP基于IP协议,IP协议头部有TTL字段(最大路由跳数),MSL要大于TTL消耗为0的时间
  • 之所以是2倍MSL,是因为网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

服务端出现大量的timewait有哪些原因?

HTTP没有使用长连接

  • 关闭长连接后,每次请求都要握手、传数据、挥手
  • 根据大多数 Web 服务的实现,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

HTTP长连接超时

  • web 服务软件一般都会提供一个参数,用来指定 HTTP 长连接的超时时间,比如 nginx 提供的 keepalive_timeout 参数。
  • 假设设置了 HTTP 长连接的超时时间是 60 秒,nginx 就会启动一个「定时器」,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,nginx 就会触发回调函数来关闭该连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

HTTP长连接请求数量达到上限

  • Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。
  • 比如 nginx 的 keepalive_requests 这个参数,这个参数是指一个 HTTP 长连接建立之后,nginx 就会为这个连接设置一个计数器,记录这个 HTTP 长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。
  • keepalive_requests 参数的默认值是 100 ,意味着每个 HTTP 长连接最多只能跑 100 次请求,这个参数往往被大多数人忽略,因为当 QPS (每秒请求数) 不是很高时,默认值 100 凑合够用。
  • 但是,对于一些 QPS 比较高的场景,比如超过 10000 QPS,甚至达到 30000 , 50000 甚至更高,如果 keepalive_requests 参数值是 100,这时候就 nginx 就会很频繁地关闭连接,那么此时服务端上就会出大量的 TIME_WAIT 状态。

TCP和UDP区别

特性 TCP(传输控制协议) UDP(用户数据报协议)
是否连接 ✅ 面向连接(三次握手) ❌ 无连接
可靠性 ✅ 保证数据可靠传输(有确认、重传) ❌ 不保证可靠性(无确认、无重传)
顺序保证 ✅ 保证数据按顺序到达 ❌ 不保证顺序
流量控制 ✅ 有流量控制机制 ❌ 无流量控制
拥塞控制 ✅ 有拥塞控制机制 ❌ 无拥塞控制
传输效率 ❌ 较低(因控制机制多) ✅ 高效(轻量级)
首部开销 TCP首部较长,不使用选项字段时为20字节 UDP首部只有8字节
传输方式 TCP为流式传输 UDP是一个包一个包的发送
适合场景 文件传输、网页浏览、邮件、远程登录等 视频直播、语音通话、游戏、DNS查询等

TCP为什么可靠传输

  • 连接管理:即三次握手和四次挥手。连接管理机制能够建立起可靠的连接,这是保证传输可靠性的前提。
  • 序列号:TCP将每个字节的数据都进行了编号,这就是序列号。序列号的具体作用如下:能够保证可靠性,既能防止数据丢失,又能避免数据重复。能够保证有序性,按照序列号顺序进行数据包还原。能够提高效率,基于序列号可实现多次发送,一次确认。
  • 确认应答:接收方接收数据之后,会回传ACK报文,报文中带有此次确认的序列号,用于告知发送方此次接收数据的情况。在指定时间后,若发送端仍未收到确认应答,就会启动超时重传。
  • 超时重传:超时重传主要有两种场景:数据包丢失:在指定时间后,若发送端仍未收到确认应答,就会启动超时重传,向接收端重新发送数据包。确认包丢失:当接收端收到重复数据(通过序列号进行识别)时将其丢弃,并重新回传ACK报文。
  • 流量控制:接收端处理数据的速度是有限的,如果发送方发送数据的速度过快,就会导致接收端的缓冲区溢出,进而导致丢包。为了避免上述情况的发生,TCP支持根据接收端的处理能力,来决定发送端的发送速度。这就是流量控制。流量控制是通过在TCP报文段首部维护一个滑动窗口来实现的。
  • 拥塞控制:拥塞控制就是当网络拥堵严重时,发送端减少数据发送。拥塞控制是通过发送端维护一个拥塞窗口来实现的。可以得出,发送端的发送速度,受限于滑动窗口和拥塞窗口中的最小值。拥塞控制方法分为:慢开始,拥塞避免、快重传和快恢复。

如何使用UDP实现HTTP

使用QUIC协议

  • 实现了序列号、ACK、重传机制
  • 实现了拥塞控制
  • 包含TLS1.3

自己实现(应用层)

  • 给每个 UDP 包加上序列号
  • 实现 ACK 和重传机制
  • 加入超时控制
  • 加密传输(如 TLS)
  • 自定义协议头(类似 QUIC)

tcp粘包怎么解决?

  • 粘包的问题出现是因为不知道一个用户消息的边界在哪,如果知道了边界在哪,接收方就可以通过边界来划分出有效的用户消息。

固定消息长度

  • 顾名思义

特殊字符作为边界

  • 在消息之间插入一些特殊字符,当读到这个字符时,就认为已经读完一条消息
  • HTTP设置回车、换行符作为边界
  • 如果消息中刚好存在这个字符,我们需要设置转义字符

自定义消息体结构

  • 我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。
  • 比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。
    1
    2
    3
    4
    struct { 
    u_int32_t message_length;
    char message_data[];
    } message;
  • 从包头中解析到消息的大小,就可以知道消息边界了

TCP的拥塞控制介绍一下?

内容

  • TCP通过降低发送的数据量来进行拥塞控制
  • 使用拥塞窗口cwnd,它会根据网络拥塞程度动态变化。发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
    • 只要网络中没有出现拥塞,cwnd 就会增大;但网络中出现了拥塞,cwnd 就减少;
    • 如果发生了超时重传,说明网络出现了拥塞
  • 拥塞控制主要是四个算法:

慢启动

  • 规则:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
  • 假定拥塞窗口 cwnd 和发送窗口 swnd 相等,连接建立完成后,一开始初始化 cwnd = 1,表示可以传一个 MSS 大小的数据。
  • 当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个
  • 当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个
  • 当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。
  • 直到达到慢启动门限 ssthresh (slow start threshold)一般来说 ssthresh 的大小是 65535 字节。
    • 当 cwnd < ssthresh 时,使用慢启动算法。
    • 当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

拥塞避免算法

  • 当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。
  • 规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。

拥塞发生算法

  • 分为超时重传和快速重传
  • 超时重传:拥塞避免+慢启动
    • 将慢启动阈值ssthresh设为cwnd/2
    • cwnd=1
    • 但是这种方式太激进了,反应也很强烈,会造成网络卡顿。
  • 快速重传:少量数据包丢失
    • 当收到乱序的数据包时,会连续发送三个期待的ACK,告诉发送方重传
    • 发送方收到三个相同ACK的包,立即重传

快速恢复算法

  • 和快速重传搭配使用
  • 将慢启动阈值ssthresh设为cwnd/2
  • 然后将cwnd设置为ssthresh+3MSS(最大段大小)
  • 执行拥塞避免算法,逐渐增加cwnd
  • 这是因为收到三个重复的 ACK,说明网络中还有一定的带宽可用,不需要像传统的慢启动那样将 cwnd 一下子降为 1。

网络场景

浏览器访问 URL 的全过程解析

解析URL

  • 分析 URL 所需要使用的传输协议和请求的资源路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,则对非法字符进行转义后在进行下一过程。

从缓存中查看域名的IP

  • 浏览器尝试从以下缓存中获取域名对应的 IP 地址:
    1
    浏览器 DNS 缓存 → 操作系统 hosts 文件 → 路由器缓存 → ISP DNS 缓存
  • 如果命中缓存,直接返回 IP 地址,跳过 DNS 查询。

DNS 解析阶段(如果缓存未命中)

  • 浏览器向本地 DNS 服务器发起查询请求。
  • 若本地 DNS 无记录,则递归查询:
    • 根域名服务器(.)
    • 顶级域名服务器(如 .com)
    • 权威域名服务器(如 example.com)
  • 最终返回目标主机的 IP 地址。

MAC 地址解析阶段(链路层准备)

  • 网络层将目标 IP 地址下发给数据链路层。
  • 判断目标 IP 是否在同一子网:
    • ✅ 同一子网:使用 ARP 协议获取目标主机的 MAC 地址
    • ❌ 不同子网:通过网关转发,使用 ARP 获取网关的 MAC 地址
  • 构造以太网帧,包含源 MAC 和目标 MAC。

建立 TCP 连接(三次握手)

建立 TLS 连接(HTTPS 的四次握手)

  • 客户端发送 ClientHello(包含加密参数)
  • 服务器回应 ServerHello(返回证书等)
  • 客户端验证证书,生成密钥
  • 双方完成密钥交换,建立加密通道

发送 HTTP 请求

  • 浏览器构造并发送 HTTP 请求报文:
    • 请求方法(GET、POST 等)
    • 请求头(User-Agent、Cookie、Accept 等)
    • 请求体(如表单数据)

服务器处理并返回响应

访问网页开始转圈怎么排查问题

  • 先确定是服务端的问题,还是客户端的问题。先确认浏览器是否可以访问其他网站,如果不可以,说明客户端网络自身的问题,然后检查客户端网络配置(连接wifi正不正常,有没有插网线);如果可以正常其他网页,说明客户端网络是可以正常上网的。
  • 如果客户端网络没问题,就抓包确认 DNS 是否解析出了 IP 地址,如果没有解析出来,说明域名写错了,如果解析出了 IP 地址,抓包确认有没有和服务端建立三次握手,如果能成功建立三次握手,并且发出了 HTTP 请求,但是就是没有显示页面,可以查看服务端返回的响应码:
    • 如果是404错误码,检查输入的url是否正确;
    • 如果是500,说明服务器此时有问题;
    • 如果是200,F12看看前端代码有问题导致浏览器没有渲染出页面。
  • 如果客户端网络是正常的,但是访问速度很慢,导致很久才显示出来。这时候要看客户端的网口流量是否太大的了,导致tcp发生丢包之类的问题。

server a和server b,如何判断两个服务器正常连接?

TCP保活机制

  • 定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
  • Linux默认值:
    1
    2
    3
    net.ipv4.tcp_keepalive_time = 7200     # 空闲多久开始探测(默认2小时)
    net.ipv4.tcp_keepalive_intvl = 75 # 探测间隔时间
    net.ipv4.tcp_keepalive_probes = 9 # 探测次数检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
  • 也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。
  • 正常时,TCP保活的探测报文发送到对端,对端正常回应,此时重置保活时间
  • 当对端主机宕机并重启时,TCP保活的探测报文发送到对端,对端可以响应,但因为没有该连接的有效信息,所以返回一个RST报文,这样就会很快发现TCP连接已重置
  • 对端主机宕机,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

心跳机制

  • web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完成一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。

服务端正常启动了,但是客户端请求不到有哪些原因?如何排查?

排查无响应

  • 检查接口IP地址是否正确,ping一下接口地址。
  • 检查被测接口端口号是否正确,可以在本机Telnet接口的IP和端口号,检查端口号能否连通
  • 检查服务器的防火墙是否关闭,如果是以为安全或者权限问题不能关闭,需要找运维进行策略配置,开放对应的IP和端口。
  • 检查你的客户端(浏览器、测试工具 (opens new window)),是否设置了网络代理,网络代理可以造成请求失败。

排查有响应

  • 400:客户端请求错误,比如请求参数格式错误
  • 401:未授权,比如请求header里,缺乏必要的信息头。(token,auth等)
  • 403:禁止,常见原因是因为用户的账号没有对应的URL权限,还有就是项目中所用的中间件,不允许远程连接(Tomcat)
  • 404:资源未找到,导致这种情况的原因很多,比如URL地址不正确
  • 500:服务器内部错误,出现这种情况,说明服务器内部报错了 ,需要登录服务器,检查错误日志,根具体的提示信息在进行排查
  • 502/503/504(错误的网关、服务器无法获得、网关超时):如果单次调用接口就报该错误,说明后端服务器配置有问题或者服务不可用,挂掉了;如果是并发压测时出现的,说明后端压力太大,出现异常,此问题一般是后端出现了响应时间过长或者是无响应造成的

服务器ping不通但是http能请求成功,会出现这种情况吗?什么原因造成的?

  • ping 走的是 icmp 协议,http 走的是 tcp 协议。
  • 有可能服务器的防火墙禁止 icmp 协议,但是 tcp 协议没有禁止,就会出现服务器 ping 不通,但是 http 能请求成果。

网络攻击

什么是ddos攻击?怎么防范?

ddos攻击

  • 攻击者控制一系列设备,组成僵尸网络,向被攻击服务器发送大量请求

分类

  • 网络层攻击:例如ICMP Flood(Ping Flood)利用 ICMP 协议发送大量 ping 请求,消耗目标的处理能力和带宽。UDP Flood,向目标发送大量无连接的 UDP 数据包,目标主机需处理每个包,造成资源耗尽。
  • 传输层攻击:SYN Flood,利用 TCP 三次握手机制,发送大量 SYN 请求但不完成握手,造成半连接堆积。
  • 应用层攻击:HTTP Flood模拟大量合法的 HTTP 请求(GET/POST),压垮 Web 服务。

防范

  • 增强网络基础设施:提升网络带宽、增加服务器的处理能力和承载能力,通过增强基础设施的能力来抵御攻击。
  • 使用防火墙和入侵检测系统:配置防火墙规则,限制不必要的网络流量,阻止来自可疑IP地址的流量。入侵检测系统可以帮助及时发现并响应DDoS攻击。
  • 流量清洗和负载均衡:使用专业的DDoS防护服务提供商,通过流量清洗技术过滤掉恶意流量,将合法流量转发给目标服务器。负载均衡可以将流量均匀地分发到多台服务器上,减轻单一服务器的压力。
  • 配置访问控制策略:限制特定IP地址或IP段的访问,设置访问频率限制,防止过多请求集中在单个IP上。

SQL注入

  • 攻击者通过在 Web 应用的输入字段中插入恶意的 SQL 语句,从而操控后台数据库执行非预期操作,例如读取敏感数据、绕过身份验证、修改或删除数据,甚至控制整个数据库系统

防范

  • 输入验证和转义:对输入进行验证和转义。确保输入符合预期格式,并防止任何可能导致SQL注入的特殊字符。
  • 使用参数化查询:使用参数化查询可以避免直接将用户输入嵌入到SQL查询中。参数化查询使用预定义的变量来接收用户输入,并将其传递给数据库引擎,而不是直接将其用作查询的一部分。这样可以防止SQL注入攻击。
  • 限制数据库权限:限制数据库用户的权限,只授予他们执行所需操作所需的最低权限。攻击者可能具有比预期更多的权限,这可能会使攻击更加容易。
  • 实施输入过滤:在某些情况下,实施输入过滤可以进一步减少SQL注入的风险。这可能涉及检查和过滤用户输入中的特殊字符和词汇,以排除可能的恶意输入。

CSRF攻击

过程

  • 用户登陆了可信网站A,A给浏览器返回了一个sessionId,保存在浏览器中
  • 用户打开了恶意网站B,B中嵌入了一个恶意请求
  • 浏览器根据B的请求,向A发送请求
  • 由于Cookie是正确的,所以A会正常响应这个请求

防范

  • CSRF Token:每次生成页面或表单时,服务器附加一个随机生成的令牌(Token)该令牌与用户会话绑定,存储在服务器端。用户提交请求时,必须携带该 Token(通常放在表单字段或请求头中)服务器验证 Token 是否匹配,若不匹配则拒绝请求
  • 验证Referer头:服务器检查请求头中的 Referer 或 Origin 字段。若来源不是本站域名,则拒绝请求
  • 双重验证机制(验证码 / 二次确认):在敏感操作前要求用户输入验证码或点击确认按钮

XSS攻击

反射型XSS

  • 恶意脚本通过 URL 参数或表单提交传入,服务端未做过滤,直接“反射”到页面中

存储型XSS

  • 恶意脚本被存储在数据库、文件或缓存中。页面渲染时从后端读取并执行

基于DOM的XSS

  • 通过修改原始的客户端代码,受害者浏览器的 DOM 环境改变,导致有效载荷的执行。也就是说,页面本身并没有变化,但由于 DOM 环境被恶意修改,有客户端代码被包含进了页面,并且意外执行。

防范

  • 输入验证:对所有用户输入的数据进行有效性检验,过滤或转义特殊字符。例如,禁止用户输入HTML标签和JavaScript代码。
  • Content Security Policy(CSP):通过设置CSP策略,限制网页中可执行的脚本源,有效防范XSS攻击。
  • 输出编码:在网页输出用户输入内容时,使用合适的编码方式,如HTML转义、URL编码等,防止恶意脚本注入。
  • 使用HttpOnly标记:在设置Cookie时,设置HttpOnly属性,使得Cookie无法被JavaScript代码读取,减少受到XSS攻击的可能。

DNS劫持

  • 攻击者在用户查询DNS服务器时篡改响应,将用户请求的域名映射到攻击者控制的虚假IP地址上,使用户误以为访问的是正常网站,实际上被重定向到攻击者操控的恶意网站。这种劫持可以通过植入恶意的DNS记录或劫持用户的DNS流量来实现。

操作系统

用户态和内核态

用户态和内核态的区别?

  • 内核态(Kernel Mode):在内核态下,CPU可以执行所有的指令和访问所有的硬件资源。这种模式下的操作具有更高的权限,主要用于操作系统内核的运行。主要操作包括:系统调用、异常处理、硬件中断
  • 用户态(User Mode):在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。这种模式下的操作权限较低,主要用于运行用户程序。
  • 区分的原因:
    • 安全:用户程序无法直接访问硬件资源,避免恶意程序对系统资源的破坏
    • 稳定:用户态程序出现问题时,不会影响到整个系统,避免了程序故障导致系统崩溃风险
    • 隔离:使内核与用户程序之间有明显的边界,有利于系统的模块化和维护

进程管理

线程和进程

本质区别(定义)

  • 进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

切换的开销

  • 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

稳定性

  • 进程中的线程而不能钢盔,可能导致整个进程崩溃,进程中的子进程崩溃,并不会影响其他进程

内存分配方面

  • 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源

包含关系

  • 没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线

进程,线程,协程的区别是什么?

进程

  • 进程是操作系统中进行资源分配和调度的基本单位,它拥有自己的独立内存空间和系统资源。每个进程都有独立的堆和栈,不与其他进程共享。进程间通信需要通过特定的机制,如管道、消息队列、信号量等。由于进程拥有独立的内存空间,因此其稳定性和安全性相对较高,但同时上下文切换的开销也较大,因为需要保存和恢复整个进程的状态。

线程

  • 线程是进程内的一个执行单元,也是CPU调度和分派的基本单位。与进程不同,线程共享进程的内存空间,包括堆和全局变量。线程之间通信更加高效,因为它们可以直接读写共享内存。线程的上下文切换开销较小,因为只需要保存和恢复线程的上下文,而不是整个进程的状态。然而,由于多个线程共享内存空间,因此存在数据竞争和线程安全的问题,需要通过同步和互斥机制来解决。

协程

  • 协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。协程的切换开销非常小,因为只需要保存和恢复协程的上下文,而无需进行内核级的上下文切换。这使得协程在处理大量并发任务时具有非常高的效率。然而,协程需要程序员显式地进行调度和管理,相对于线程和进程来说,其编程模型更为复杂。

为什么进程崩溃不会对其他进程产生很大影响

  • 进程隔离性:每个进程都有自己独立的内存空间,当一个进程崩溃时,其内存空间会被操作系统回收,不会影响其他进程的内存空间。这种进程间的隔离性保证了一个进程崩溃不会直接影响其他进程的执行。
  • 进程独立性:每个进程都是独立运行的,它们之间不会共享资源,如文件、网络连接等。因此,一个进程的崩溃通常不会对其他进程的资源产生影响。

操作系统给进程分配了什么资源

  • 虚拟内存、文件句柄、信号量等资源。

多线程比单线程的优势,劣势?

  • 优势:提高程序的运行效率,可以充分利用多核处理器的资源,同时处理多个任务,加快程序的执行速度。
  • 劣势:存在多线程数据竞争访问的问题,需要通过锁机制来保证线程安全,增加了加锁的开销,并且还会有死锁的风险。多线程会消耗更多系统资源,如CPU和内存,因为每个线程都需要占用一定的内存和处理时间。

多线程是不是越多越好,太多会有什么问题?

  • 切换开销:线程的创建和切换会消耗系统资源,包括内存和CPU。如果创建太多线程,会占用大量的系统资源,导致系统负载过高,某个线程崩溃后,可能会导致进程崩溃。
  • 死锁的问题:过多的线程可能会导致竞争条件和死锁。竞争条件指的是多个线程同时访问和修改共享资源,如果没有合适的同步机制,可能会导致数据不一致或错误的结果。而死锁则是指多个线程相互等待对方释放资源,导致程序无法继续执行。

进程切换和线程切换区别

  • 进程切换:进程切换涉及到更多的内容,包括整个进程的地址空间、全局变量、文件描述符等。因此,进程切换的开销通常比线程切换大。
  • 线程切换:线程切换只涉及到线程的堆栈、寄存器和程序计数器等,不涉及进程级别的资源,因此线程切换的开销较小。

线程切换为什么比进程切换快,节省了什么资源?

  • 线程切换快,因为线程之间共享同一个进程的资源,不需要重新加载或切换这些资源。
  • 进程切换慢,因为每个进程拥有独立资源,切换时必须保存和恢复大量上下文。
  • 节省了地址空间、页表TLB、文件描述符表、内核数据结构中的PCB

线程切换详细过程是怎么样的?上下文保存在哪里?

过程

  • 保存当前线程状态:将当前线程的寄存器、PC、SP 等保存到它的 TCB 中
  • 更新线程状态:把当前线程标记为“就绪”或“阻塞”
  • 选择下一个线程:调度器根据算法(如时间片轮转、优先级)选出下一个线程
  • 恢复新线程状态:从新线程的 TCB 中恢复寄存器、PC、SP 等,设置为“运行”态,CPU开始执行它的指令

保存位置

  • 一般情况下,上下文信息会保存在线程的控制块(Thread Control Block,TCB)中。
  • TCB是操作系统用于管理线程的数据结构,包含了线程的状态、寄存器的值、堆栈信息等。

进程五状态模型

进程五状态模型

  • 创建态 → 就绪态 系统完成创建进程相关的工作
  • 就绪态 → 运行态 进程被调度
  • 运行态 → 就绪态 时间片到,或CPU被其他高优先级的进程抢占
  • 运行态 → 阻塞态 等待系统资源分配,或等待某事件发生(“系统调用”的方式,主动行为)
  • 阻塞态 → 就绪态 资源分配到位,等待的事件发生(不是进程自身能控制的,被动行为)
  • 运行态 → 终止态 进程运行结束,或运行过程中遇到不可修复的错误

进程上下文有哪些?

  • 进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
  • 所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

进程间通讯有哪些方式?

管道

  • 匿名管道:顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
  • 命名管道:突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
    • 读取同一个管道文件的进程会从这个文件的inode中找到同一个pipe_inode_info 内核对象,pipe_inode_info 就是真正存放数据和同步信息的内核对象。从而使用同一个内核缓冲区进行通信。

消息队列

  • 克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。

共享内存

  • 共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

信号量

  • 需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。

信号

  • 信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。

socket

  • 如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

管道有哪几种?

  • 匿名管道:是一种在父子进程或者兄弟进程之间进行通信的机制,只能用于具有亲缘关系的进程间通信,通常通过pipe系统调用创建。
  • 命名管道:是一种允许无关的进程间进行通信的机制,基于文件系统,可以在不相关的进程之间进行通信。

信号和信号量有什么区别?

  • 信号:一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接收进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。
  • 信号量:进程间通信处理同步互斥的机制。是在多线程环境下使用的一种设施,它负责协调各个线程,以保证它们能够正确,合理的使用公共资源。

共享内存怎么实现的?

  • 共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。
    共享内存

线程通信的方式

互斥锁

  • 进入临界区前加锁,退出时解锁。其他线程在锁被占用时会阻塞等待。

条件变量

  • 一个线程等待条件成立(阻塞),另一个线程改变条件并发出信号唤醒它。
  • 常见于生产者-消费者模型:生产者生产数据后通知消费者。

自旋锁

  • 线程使用CAS检查锁的状态,不会主动产生上下文切换,如果没有成功获取锁,会一直占用CPU进行忙等
  • CAS分为两个步骤:第一步查看锁是否为空闲,第二步若为空闲,则让锁被当前线程持有。这两步是原子的

信号量

  • 内部维护一个计数器,控制可同时访问资源的线程数。
  • 计数器为 0 时,新的访问线程会阻塞。

读写锁

  • 多个线程可同时持有读锁,但写锁是独占的。
  • 当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。
  • 适合“读多写少”的场景。

进程调度算法

先来先服务算法

  • 顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
  • 这似乎很公平,但是当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。 FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。

最短作业优先调度算法

  • 它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
  • 这显然对长作业不利,很容易造成一种极端现象。
  • 比如,一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。

高相应比优先调度算法

  • 每次调度时,计算相应比优先级:优先权=(等待时间+要求服务时间)/要求服务时间
  • 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行;
  • 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;

时间片轮转调度算法

  • 每个进程被分配一个时间段,称为时间片,即允许该进程在该时间段中运行。若进程在时间片用完前阻塞或结束,则CPU立即切换
  • 关键是时间片长度,过短导致CPU频繁上下文切换,过长导致短作业进程响应时间变长

最高优先级调度算法

  • 从就绪队列中选择最高优先级的进程进行运行
  • 静态优先级:优先级不会变化,在创建进程时就已确定
  • 动态优先级:进程运行时间增加则降低优先级,等待时间增加则提高优先级
  • 抢占式:若就绪队列出现优先级高的进程,立即挂起当前进程
  • 非抢占式:等运行完当前进程,再调度高优先级进程

多级反馈队列调度算法

  • 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
  • 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;
    多级反馈队列调度算法
  • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
  • 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
  • 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
  • 对于短作业可能可以在第一级队列很快被处理完。
  • 对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。

为什么并发执行线程需要加锁?

  • 为了保护共享数据。确保在任何时刻只有一个线程能够访问共享数据

自旋锁是什么?应用在哪些场景?

  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁。
  • 自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
  • 自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
  • 自旋的时间和被锁住的代码执行的时间是成「正比」的关系
  • 能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。

死锁发生的条件

互斥条件

  • 系统中的资源一次只能被一个进程占用,不能共享使用。

请求并保持条件

  • 一个进程已经持有至少一个资源,同时又提出新的资源请求,并且在等待新资源的同时不释放自己已经占有的资源。

不可剥夺条件

  • 进程已获得的资源,在使用完之前,不能被系统强行剥夺,只能由进程自己主动释放。

循环等待条件

  • 存在一个进程等待环

预防死锁

破坏条件 方法 说明
互斥条件 尽量使用可共享资源 例如只读文件可多个线程同时访问;但很多资源(打印机、写锁)必须互斥,无法彻底破坏
请求并保持条件 一次性申请所有资源 进程启动时就申请全部所需资源,不能边占边等;缺点是资源利用率低
不可剥夺条件 支持资源抢占 如果申请新资源失败,主动释放已占有的资源,稍后重试
循环等待条件 资源有序分配法 给资源编号,进程必须按编号顺序申请资源,释放则反向

银行家算法

  • 一个进程的最大需求量不超过系统拥有的总资源数,才会被接纳执行。一个进程可以分期请求资源,但总请求书不可超过最大需求量。当系统现有资源数小于进程需求时,对进程的需求可以延迟分配,但总让进程在有限时间内获取资源。
  • 维护了几个变量:
    • 当前系统剩余的各个资源的数量
    • 当前系统剩余的被需要的资源数
    • 每个进程所需要的各个资源的最大数量
    • 每个进程已分配的各个资源的数量
  • 银行家算法的核心思想,就是在分配给进程资源前,首先判断这个进程的安全性,也就是预执行,判断分配后是否产生死锁现象。如果系统当前资源能满足其执行,则尝试分配,如果不满足则让该进程等待。
  • 通过不断检查剩余可用资源是否满足某个进程的最大需求,如果可以则加入安全序列,并把该进程当前持有的资源回收;不断重复这个过程,看最后能否实现让所有进程都加入安全序列。安全序列一定不会发生死锁,但没有死锁不一定是安全序列。

乐观锁和悲观锁有什么区别?

乐观锁

  • 乐观锁假设多个事务之间很少发生冲突,因此在读取数据时不会加锁,而是在更新数据时检查数据的版本(如使用版本号或时间戳),如果版本匹配则执行更新操作,否则认为发生了冲突。
  • 用于读多写少,通过版本控制来处理冲突

悲观锁

  • 基本思想:悲观锁假设多个事务之间会频繁发生冲突,因此在读取数据时会加锁,防止其他事务对数据进行修改,直到当前事务完成操作后才释放锁。
  • 使用场景:悲观锁适用于写多的场景,通过加锁保证数据的一致性。例如,数据库中的行级锁机制可以用于处理并发更新同一行数据的情况。

内存管理

介绍一下操作系统内存管理

虚拟内存

  • 操作系统设计了虚拟内存,每个进程都有自己的独立的虚拟内存,我们所写的程序不会直接与物理内打交道。
  • 有了虚拟内存之后,进程的运行内存就可以大于物理内存大小。CPU访问内存有重复访问一定内存的倾向性,对于那些没有经常访问的内存,我们可以将其换到主存中。

页表

  • 每个进程都使用各自页表进行进程的内存管理,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
  • 页表中还有权限等属性,保障了安全性。
  • Linux通过对内存分页来管理内存,分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。每一个小空间称为页(page),Linux一页为4KB
    页表
  • 将虚拟内存映射为物理内存
  • 而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

什么是虚拟内存和物理内存?

  • 虚拟内存是操作系统为每个进程提供的逻辑内存,进程觉得自己有很大的内存,但其实不是,操作系统只拿出一块真实内存分配给进程,只有被用到的页才会被加载,用不到的页会被置换到主存中,并由页表完成对逻辑地址到物理地址的映射
  • 物理内存就是内存条上的内存

页表

  • 分页:将虚拟内存分成大小相等的页,物理内存分成同样大小的页框
  • 页表完成虚拟地址到物理地址的映射,CPU访问内存时,通过MMU(内存管理单元)查页表完成地址转换
  • 在分页机制下,虚拟地址分为两部分,虚拟页号和页内偏移,MMU会把虚拟页号映射为物理页号,物理页号和页内偏移拼起来就能找到真实物理内存中的位置了
    页表映射

讲一下段表

  • 虚拟地址也可以通过段表与物理地址进行映射的,分段机制会把程序的虚拟地址分成 4 个段:代码段、数据段、堆、栈,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址,如下图:
    段表

虚拟地址转化为物理地址过程

  • 将虚拟内存划分为页,物理内存划分为页框,页表记录页框和页的映射
  • 当程序访问一个虚拟地址时,MMU会将虚拟地址分解为页号和页内偏移量。然后,MMU会查找页表,根据页号找到对应的页表项。页表项中包含了物理页的地址或页框号。最后,MMU将物理页的地址与页内偏移量组合,得到对应的物理地址。

程序的内存布局是怎么样的?

程序的内存布局

  • 代码段,包括二进制可执行代码;
  • 数据段,包括已初始化的静态常量和全局变量;
  • BSS 段,包括未初始化的静态变量和全局变量;
  • 堆段,包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段,包括动态库、共享内存等;
  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;
  • 代码段的起始地址不是0,那一灰色区域为保留区,这是因为在大多数的系统里,我们认为比较小数值的地址不是一个合法地址,例如,我们通常在 C 的代码里会将无效的指针赋值为 NULL。
  • 这个过程是虚拟内存

堆和栈的区别?

  • 分配方式:堆是动态分配内存,由程序员手动申请和释放内存,通常用于存储动态数据结构和对象。栈是静态分配内存,由编译器自动分配和释放内存,用于存储函数的局部变量和函数调用信息。
  • 内存管理:堆需要程序员手动管理内存的分配和释放,如果管理不当可能会导致内存泄漏或内存溢出。栈由编译器自动管理内存,遵循后进先出的原则,变量的生命周期由其作用域决定,函数调用时分配内存,函数返回时释放内存。
  • 大小和速度:堆通常比栈大,内存空间较大,动态分配和释放内存需要时间开销。栈大小有限,通常比较小,内存分配和释放速度较快,因为是编译器自动管理。

fork()会复制哪些东西?

  • fork 阶段会复制父进程的页表(虚拟内存)
  • fork 之后,如果有一个进程对共享内存进行修改,发生了写时复制,就会复制物理内存

介绍copy on write写时复制

  • 在fork后页表对应的页表项的属性会标记该物理内存的权限为只读。
  • 当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制(Copy On Write)」。
  • 只复制发生改变的页

copy on write节省了什么资源?

  • 节省了物理内存的资源,因为 fork 的时候,子进程不需要复制父进程的物理内存,避免了不必要的内存复制开销,子进程只需要复制父进程的页表,这时候父子进程的页表指向的都是共享的物理内存。
  • 只有当父子进程任何有一方对这片共享的物理内存发生了修改操作,才会触发写时复制机制,这时候才会复制发生修改操作的物理内存。

malloc 1KB和1MB 有什么区别?

  • 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
    • 在现有堆顶直接向高地址扩展
  • 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
    • 在堆和栈之间的“内存映射区”找一块独立虚拟内存

操作系统内存不足的时候会发生什么?

  • 应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
  • 当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理。
  • 缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
  • 若没有空闲的物理内存,内核就会开始回收内存
    • 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
    • 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
    • 若直接内存回收也不行,就会触发OOM(out of memory)机制
      • OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。

回收内存时,具体哪些内存可以回收?

  • 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
  • 匿名页(Anonymous Page):这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

回收内存时基于什么算法?

  • LRU(least rencently used)维护两个双向链表:
    • active_list 活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页;
    • inactive_list 不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页;

过程

  • 新分配的页 → 放入 active_list,标记 PG_referenced=0
  • 访问页面时:
    • 在 active_list 中:将 PG_referenced 置 1
    • 在 inactive_list 中:
      • 若 PG_referenced=0 → 置 1(但不移动)
      • 若 PG_referenced=1 → 移回 active_list,并清零标志
  • 回收过程:从 inactive_list 尾部取页:
    • 若 PG_referenced=1 → 清零并跳过(说明刚被访问过)
    • 若 PG_referenced=0:
      • 文件映射页:直接丢弃(脏页需先写回磁盘)
      • 匿名页:写入 swap 分区/文件,再释放物理页

页面置换算法

最佳页面置换算法

  • 置换在「未来」最长时间不访问的页面。
  • 需要知道当前物理内存中的页下一次被使用的时间
  • 取最后一个被使用的页面,将其置换出去
  • 无法实现,只是做参考

先进先出

  • 选择在内存驻留时间很长的页面进行中置换

最近最久未使用(LRU)

  • 选择最长时间没有被访问的页面进行置换
  • 虽然 LRU 在理论上是可以实现的,但代价很高。为了完全实现 LRU,需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。
  • 困难的是,在每次访问内存时都必须要更新「整个链表」。在链表中找到一个页面,删除它,然后把它移动到表头是一个非常费时的操作。
  • 所以,LRU 虽然看上去不错,但是由于开销比较大,实际应用中比较少使用。

时钟页面置换算法

  • 把所有的页面都保存在一个类似钟面的「环形链表」中,一个表针指向最老的页面。
  • 当发生缺页中断时,算法首先检查表针指向的页面:
    • 如果它的访问位位是 0 就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置;
    • 如果访问位是 1 就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个访问位为 0 的页面为止;
      时钟算法

最不常用算法(LFU)

  • 当发生缺页中断时,选择「访问次数」最少的那个页面,并将其淘汰。
  • 看起来很简单,每个页面加一个计数器就可以实现了,但是在操作系统中实现的时候,我们需要考虑效率和硬件成本的。
  • 要增加一个计数器来实现,这个硬件成本是比较高的,另外如果要对这个计数器查找哪个页面访问次数最小,查找链表本身,如果链表长度很大,是非常耗时的,效率不高。
  • LFU 算法只考虑了频率问题,没考虑时间的问题,比如有些页面在过去时间里访问的频率很高,但是现在已经没有访问了,而当前频繁访问的页面由于没有这些页面访问的次数高,在发生缺页中断时,就会可能会误伤当前刚开始频繁访问,但访问次数还不高的页面。

中断

什么是中断

  • CPU停下当前的工作任务,去处理其他事情,处理完后回来继续执行刚才的任务

内中断

  • 陷入:由陷入指令引发,是应用程序故意引发的,如如系统调用,程序调试功能等。
  • 故障:由错误条件引起的,可能被内核修复,内核修复故障后会把CPU使用权还给用户程序,让它继续执行,如缺页故障
  • 终止:由致命错误引起,内核无法修复,直接终止该应用程序,如:除以0、非法使用特权指令

外中断

  • 可屏蔽中断:主要来自外部设备如硬盘,打印机,网卡等。此类中断并不会影响系统运行,可随时处理,甚至不处理,所以名为可屏蔽。
  • 不可屏蔽中断:如电源掉电,硬件线路故障等。这里不可屏蔽的意思不是不可以屏蔽,不建议屏蔽,而是问题太大,屏蔽不了,不能屏蔽的意思。

中断的流程

发生中断

  • 可能是外设(键盘、网卡、定时器等)、CPU 内部异常(除零、缺页)或软件指令(int 指令)触发,向CPU发送中断信号

中断响应

  • CPU 检查当前是否允许中断(看中断使能标志 IF 位)。如果允许,会在当前指令执行完后响应中断。

保护现场(Save Context)

  • 硬件自动保存:PC、标志寄存器
  • 软件:将通用寄存器、浮点寄存器等压入栈中,确保中断返回后能恢复原状态。

识别中断源

  • CPU在中断向量表中找到对应的中断服务程序入口地址。

执行中断服务程序

  • 跳转到 中断服务程序 入口执行。

恢复现场

  • 将之前保存的寄存器、标志位等从栈中恢复。
  • 确保 CPU 状态与中断发生前一致。

中断的作用

  • 中断使得计算机系统具备应对对处理突发事件的能力,提高了CPU的工作效率,如果没有中断系统,CPU就只能按照原来的程序编写的先后顺序,对各个外设进行查询和处理,即轮询工作方式,轮询方法貌似公平,但实际工作效率却很低,却不能及时响应紧急事件。

网络IO

了解过哪些IO模型?

  • 阻塞I/O模型:应用程序发起I/O操作后会被阻塞,直到操作完成才返回结果。适用于对实时性要求不高的场景。
  • 非阻塞I/O模型:应用程序发起I/O操作后立即返回,不会被阻塞,但需要不断轮询或者使用select/poll/epoll等系统调用来检查I/O操作是否完成。适合于需要进行多路复用的场景,例如需要同时处理多个socket连接的服务器程序。
  • I/O复用模型:通过select、poll、epoll等系统调用,应用程序可以同时等待多个I/O操作,当其中任何一个I/O操作准备就绪时,应用程序会被通知。适合于需要同时处理多个I/O操作的场景,比如高并发的服务端程序。
  • 信号驱动I/O模型:应用程序发起I/O操作后,可以继续做其他事情,当I/O操作完成时,操作系统会向应用程序发送信号来通知其完成。适合于需要异步I/O通知的场景,可以提高系统的并发能力。
  • 异步I/O模型:应用程序发起I/O操作后可以立即做其他事情,当I/O操作完成时,应用程序会得到通知。异步I/O模型由操作系统内核完成I/O操作,应用程序只需等待通知即可。适合于需要大量并发连接和高性能的场景,能够减少系统调用次数,提高系统效率。
    这个文章写得很好

服务器处理并发请求有哪几种方式?

  • 单线程web服务器方式:web服务器一次处理一个请求,结束后读取并处理下一个请求,性能比较低,一次只能处理一个请求。
  • 多进程/多线程web服务器:web服务器生成多个进程或线程并行处理多个用户请求,进程或线程可以按需或事先生成。有的web服务器应用程序为每个用户请求生成一个单独的进程或线程来进行响应,不过,一旦并发请求数量达到成千上万时,多个同时运行的进程或线程将会消耗大量的系统资源。(即每个进程只能响应一个请求,并且一个进程对应一个线程)
  • I/O多路复用web服务器:web服务器可以I/O多路复用,达到只用一个线程就能监听和处理多个客户端的 i/o 事件。
  • 多路复用多线程web服务器:将多进程和多路复用的功能结合起来形成的web服务器架构,其避免了让一个进程服务于过多的用户请求,并能充分利用多CPU主机所提供的计算能力。(这种架构可以理解为有多个进程,并且一个进程又生成多个线程,每个线程处理一个请求)

讲一下io多路复用

  • 指的是复用一个线程,处理多个socket中的事件。能够资源复用,防止创建过多线程导致的上下文切换的开销。
    sequenceDiagram
        autonumber
        participant App as 应用程序1
        participant Kernel as 内核
        participant Mon1 as 描述符1
        participant Mon2 as 描述符2
    
        Note over App: 应用程序发起 IO 多路复用,阻塞等待任一 fd 就绪
        App->>Kernel: select/poll/epoll(fd1, fd2, ..., fdN)
    
        par 并行监控
            Kernel-->>Mon1: 监控收发/事件订阅
            Mon1->>Kernel: 数据准备好/事件就绪
            Mon1-->>App: 通知/唤醒
        and
            Kernel-->>Mon2: 监控收发/事件订阅
            Mon2->>Kernel: 数据准备好/事件就绪
            Mon2-->>App: 通知/唤醒
        end
    
        Kernel-->>App: 返回就绪的 fd(例如 fd2)
        App->>Kernel: read(fd2)
        Kernel-->>App: 返回数据
        App->>App: 处理数据
        App->>Kernel: 下一轮 select/poll/epoll(fd1, fd2, ..., fdN)
    

select、poll、epoll 的区别是什么?

  • 内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件。

select/poll

  • 将已连接的 Socket 都放到一个文件描述符集合
  • 然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生
  • 通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
    • 一共两次遍历和两次拷贝
    • select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
  • poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
  • 总结:都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合

epoll

  • epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn)。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
  • epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中(不会删除红黑树中的),当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

epoll 的 边缘触发和水平触发有什么区别?

边缘触发

  • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
  • 如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。

水平触发

  • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
  • 如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。
  • 一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。

零拷贝是什么?

  • 传统 IO 的工作方式,从硬盘读取数据,然后再通过网卡向外发送,我们需要进行 4 上下文切换,和 4 次数据拷贝,其中 2 次数据拷贝发生在内存里的缓冲区和对应的硬件设备之间,这个是由 DMA 完成,另外 2 次则发生在内核态和用户态之间,这个数据搬移工作是由 CPU 完成的。
    传统IO
  • 为了提高文件传输的性能,于是就出现了零拷贝技术,它通过一次系统调用(sendfile 方法)合并了磁盘读取与网络发送两个操作,降低了上下文切换次数。另外,拷贝数据都是发生在内核中的,天然就降低了数据拷贝的次数。
    零拷贝
  • 零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。

计网OS相关知识
https://sdueryrg.github.io/2025/08/28/计网OS相关知识/
作者
yrg
发布于
2025年8月28日
许可协议