【八股总结】至今为止遇到的八股(上半)
本文内容均来自于我的个人总结以及开源社区,感谢互联网。
生产日期:2023.12.11
资本家传递寒冬,我传递温暖,祝大家新一年事事顺利
计算机网络原理
七层网络模型
从下到上:
物理层-数据链路层-网络层-传输层-会话层-表示层-应用层
TCP/IP网络模型
其实就是简化的七层模型
数据链路层-网络层-传输层-应用层
- 应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等;
- 传输层,负责端到端的通信,比如 TCP、UDP 等;
- 网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等;
- 网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、MAC 寻址、差错检测,以及通过网卡传输网络帧等;
当键入网址后,到网页显示,其间发生了什么?
首先,浏览器会解析这个网址,确定你要访问的服务器和文件。
然后,浏览器会生成一个HTTP请求消息,里面包含了一些必要的信息,比如请求方法、浏览器的一些相关信息等等。
为了能够发送请求,浏览器需要知道服务器的IP地址。所以它会通过DNS查询来获取服务器的IP地址。
接下来,浏览器会建立一个网络连接,使用TCP/IP协议与服务器进行通信。这个过程涉及到一些细节,比如分配IP地址和端口号,进行三次握手等等。
一旦网络连接建立好了,浏览器就会把HTTP请求发送给服务器。
服务器收到请求后,会根据请求做出相应的处理。可能会查询数据库,执行一些业务逻辑等等。
然后,服务器会生成一个HTTP响应,里面包含了响应状态码、一些头部信息和响应内容。
浏览器会通过网络连接接收到服务器的响应。
接着,浏览器会解析这个响应,根据响应中的HTML、CSS、JavaScript等资源,生成页面的结构、样式和交互效果。
浏览器会下载并执行页面中引用的其他资源,比如CSS样式表和JavaScript脚本。
最后,浏览器会将页面显示在你的屏幕上,你就可以看到网页的内容了。
【防盗链提醒】
HTTP定义概念
HTTP是超文本传输协议。
HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。
HTTP常见状态码
- 1xx,协议处理的中间状态
- 2xx,服务器成功处理了客户端的请求
- 3xx,客户端请求的资源发生了变动,需要重定向
- 4xx,客户端发送的报文有误,服务器无法处理(错误码)
- 5xx,客户端请求报文正确,但是服务器处理时内部发生了错误
HTTP常见字段
- Host字段:客户端发送请求时,用来指定服务器的域名
- Content-Length字段:服务器在返回数据时用来表明本次回应的数据长度
- Connection 字段:与「HTTP 长连接」机制,以便其他请求复用
HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
- Content-Type 字段:用来告诉客户端本次数据是什么格式
- Content-Encoding 字段:说明数据的压缩格式
【dayceng:防盗链提醒】
HTTP与HTTPS的区别
1、HTTP以明文传输数据,而HTTPS协议传输的数据是经过TLS加密后的,HTTPS具有更高的安全性;
2、HTTPS在三次握手之后还要进行SSL的handshake,用来协商加密使用的对称加密秘钥;
3、使用HTTPS协议需要服务端申请证书,浏览器端安装对应的根证书;
4、HTTP协议的端口是80,HTTPS协议的端口是443;
HTTPS的优点:因为其在数据传输过程中使用了TLS加密,所以它更安全。并且由于需要认证服务器和用户,因此可以保证数据传输的正确性
HTTPS的缺点:在握手阶段的延迟较高,因为在三次握手之外还要进行SSL握手。还有就是HTTPS的部署成本较高,需要购买CA证书,且由于涉及到数据的加解密操作,对与服务器配置的要求也会偏高。
GET和POST请求的区别
GET 和 POST 是两种最常用的 HTTP 请求方法,用于与 Web 服务器进行通信。
GET 用于获取数据,而 POST 用于发送数据。(一般只用GET获取数据,因为其是幂等的)
GET 请求更适合用于幂等的、只读操作,而 POST 请求更适合用于对服务器状态进行更改的操作。
如果按照规范来说:
GET 的语义是请求获取指定的资源。GET 方法是安全、幂等、可被缓存的。
POST 的语义是根据请求负荷(报文主体)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 不安全,不幂等,(大部分实现)不可缓存。
HTTP缓存技术
对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,那么下次就直接读取本地的数据。
HTTP缓存有哪些实现方式?
HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存。
强制缓存
强制缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。
强缓存是利用 Cache-Control 和 Expires 字段控制资源在客户端缓存的有效期。
协商缓存
协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存。
通过配置Last-Modified
字段和ETag
字段来实现。
HTTP长连接和短连接的区别
- 短连接在每个请求后关闭连接,适用于单一请求和响应的情况。
- 长连接允许在同一连接上发送多个请求和响应,减少了连接建立和断开的开销,提高了性能,适用于多次请求的情况。
HTTP/1.1及更高版本通常使用长连接以提高性能,但仍然需要根据具体应用和需求来选择连接方式。
TCP协议有了解吗?HTTP和TCP的关系是什么?
TCP协议(Transmission Control Protocol) 是一种面向连接的、可靠的传输层协议。它负责在网络上可靠地传输数据,确保数据的完整性、顺序性和可靠性。TCP使用三次握手建立连接,四次握手关闭连接,并提供流控制、拥塞控制等功能。它是互联网上最常用的传输协议之一,用于确保数据在网络上的可靠传输。
HTTP协议(Hypertext Transfer Protocol) 是一个应用层协议,用于在Web上传输超文本文档,通常用于浏览器和Web服务器之间的通信。HTTP是基于TCP协议的,它使用TCP作为其传输层协议来传输HTTP消息。
说一下TCP和UDP的区别
TCP vs UDP:
- 可靠性:TCP 提供可靠的数据传输,通过确认和重传机制、滑动窗口、超时重传等来确保数据的正确性和完整性。而UDP 则是一种无连接的协议,不提供可靠性保证,数据报文发送后即忘记,不关心是否被接收或顺序是否正确。
- 连接性:TCP 是面向连接的协议,建立连接后进行数据传输,保证了数据的有序性和可靠性。而UDP 是无连接的协议,每个数据包都是独立的,没有建立连接的过程。
- 效率:由于提供了可靠性和连接管理等功能,TCP 的开销较大,适用于对数据完整性要求较高的应用场景。UDP 则没有这些额外的开销,传输效率较高,适用于实时性要求较高的应用场景,如实时音视频传输、游戏等。
WebSocket与HTTP的区别
WebSocket相对于HTTP更适合实时通信和推送场景,能够提供更低的延迟和更高的并发性。而HTTP更适用于请求-响应模式的传统Web应用,对于每次通信都需要重新建立连接的场景较合适。
TCP协议传输的报文字段长度是多少?
TCP协议传输的报文字段长度最小为20字节,并且可以根据数据偏移字段的值来确定实际的首部长度。
其中一个重要的字段是数据偏移(Data Offset)字段。数据偏移字段指示了TCP报文段的首部长度,以字节为单位。
数据偏移字段是一个4位长的字段,它的最小值是5(0101),表示TCP首部的长度为5个32位字(或者5个4字节的字)。
因此,TCP报文段的首部长度最小为5 * 4 = 20字节,而且可以通过数据偏移字段来确定实际的首部长度。如果TCP选项被使用,首部长度将增加。
TCP三次握手
TCP是一种面向连接的协议,其通过三次握手来确保通信双方的一个正常通信。【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
第一次握手,客户端向服务端发送一个SYN报文,该报文中包含一个随机生成的初始序列号(ISN)用于后续的数据传输;
第二次握手,服务器接受到客户端的SYN报文后,会发送一个ACK确认报文作为响应,此时ACK中的确认号为客户端发来的序号加1,然后也发送一个SYN报文给客户端,表明服务器已经收到了客户端的请求,该SYN中的初始序列号也是随机生成的;
第三次握手,客户端收到服务器的SYN+ACK报文后,会发送一个确认报文ACK作为响应。其中的确认号会设置为服务器发来的序号加1,表示客户端已收到服务器的回应。
只进行前两次连接服务器会发生什么?
如果TCP连接只采用两次握手,会引发一些安全和可靠性问题。
两次握手建立的连接是不可靠的。TCP是全双工的,两次握手只能确定单向链路是正常通信的,不能保证反向也是正常的。
两次握手的方式可能导致半开连接问题。比如客户端发送连接请求,但是服务端没有收到,然后客户端重新建立连接。此时服务器可能收到之前的连接请求,从而导致连接混乱
两次握手存在安全性问题。由于没有第三次握手中的随机序列号,攻击者可以更容易地伪造连接。
客户端在第二次握手的基础上,向服务器发送一个确认请求(ACK)。这个ACK中包含了服务器在前两次握手中发送给客户端的随机序列号。在收到客户端的ACK后,服务器会生成一个新的随机序列号,用于之后数据传输的序列号。
TCP四次挥手
首先,客户端打算关闭连接,此时会发送一个 TCP 首部 FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。【第一次挥手】
然后服务端接收该报文后会向客户端发送一个ACK应答报文,进入CLOSE_WAIT
状态。【第二次挥手】
客户端收到服务端的确认报文后进入FIN_WAIT_2
状态,等待服务端将剩余数据处理完。
处理完剩余数据后,服务端再向客户端发送 FIN
报文,服务端进入 LAST_ACK
状态。【第三次挥手】
收到服务端的 FIN
报文后客户端会回一个ACK,之后进入 TIME_WAIT
状态。【第四次挥手】
服务端收到了 ACK
应答报文后,就进入了 CLOSE
状态,完成连接关闭。
客户端在经过 2MSL
一段时间后,自动进入 CLOSE
状态,完成连接关闭。
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
第四次挥手ack丢失怎么办?
在TCP连接的第四次挥手过程中,如果ACK(确认)丢失,这可能会导致连接无法正常关闭,因为双方无法确认对方已经收到了关闭请求。
发生这种情况一般会使用等待超时的方法来处理。
TCP协议有一个连接超时机制,如果一段时间内没有收到对方的确认,发送方会认为数据丢失,并重新发送。等待足够长的时间,通常是几个最大段生存时间(Maximum Segment Lifetime,MSL)的时间(通常是2分钟左右,即2MSL),可能会导致连接最终关闭。
但这会导致连接保持在TIME_WAIT状态,并占用系统资源。
网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
TCP怎么保证传输可靠
确认和重传机制
TCP 采用确认和重传机制来保证数据的可靠传输。当发送方发送数据时,接收方会返回一个 ACK 确认消息,表示已经成功接收到数据。如果发送方在一定时间内没有收到确认消息,就会重新发送该数据。
滑动窗口机制
TCP 中的滑动窗口机制可以有效地控制发送方和接收方之间的数据流量。发送方根据接收方的反馈信息来调整发送窗口大小,从而避免了数据包的拥塞和丢失。
超时重传
如果发送方连续多次发送数据但未收到确认消息,认为该数据包可能已经丢失或损坏,就会触发超时重传机制,重新发送该数据包。
累积确认机制
TCP 采用累积确认机制,即接收方只需要确认已经接收到的数据序列号最大的分组,就可以表明其之前所有的数据都已经正确接收。这样可以减少确认消息的传输量,提高网络吞吐量。
流量控制
TCP 通过窗口控制机制来限制发送方的数据流量,以防止接收方无法处理过多的数据包。每个 TCP 连接都有一个接收窗口和发送窗口,用于控制发送和接收方之间的数据流量。
说一下TCP的拥塞控制(快重传是怎么发生的)
快重传是TCP拥塞控制中的一个重要机制。快重传是指当接收方收到一个已经正确接收但是序号不连续的数据包时,会立即发出重复确认(Duplicate ACK)。发送方在收到连续的3个重复确认之后,会认为这个数据包之后的数据可能丢失,因此会马上重传该数据包,而不必等待超时计时器的触发。通过快速重传机制,TCP可以更迅速地检测到丢失的数据包,并尽早进行重传。这样可以减少等待超时计时器触发的时间延迟,提高网络传输的效率和可靠性。
需要注意的是,快重传只适用于丢失但有顺序的数据包。如果接收方收到乱序的数据包,则会触发超时重传,这是因为乱序的数据包可能是由于网络拥塞造成的,而不仅仅是丢失。
TCP重传、滑动窗口、流量控制、拥塞控制
什么是重传?为什么会发生重传?
tcp实现可靠传输的方式之一就是使用序列号和确认应答。当发送端数据到达接收端主机时会返回一个确认应答消息
但是如果丢包的话某一方可能就收不到应答,因此需要重传机制来解决
常见的重传机制有:超时重传、快速重传、SACK以及D-SACK
超时重传
其实就是在发送数据的时候设置一个定时器,当经过一定的时间后,如果没有收到对方的应答,那么就会重新发送数据
丢包和应答丢失都会触发超时重传
超时时间如何设置?
首先,数据包从发送到收到确认是有时间差的,被称为往返时延(RTT)
然后再说超时重传时间RTO,所谓的超时重传时间就是指超时之后要再等待多久才再次发送数据包
那么这就涉及到RTO的时长设置,如果RTO过长,会导致网络中的空闲的时间间隙变大,降低了网络传输效率;
因此精确测量RTO时间很重要(一般使用计时器和时间戳来计算)
但是在实际中,RTO的计算要复杂一些。往往要对RTT进行采样,然后计算平均值。
TCP的策略是超时间隔加倍
也就是说:每当遇到一次超时重传时,都会将下一次的超时时间间隔设置为先前值的两倍。两次超时就说明网络环境差,不适合频繁反复发送。
超时触发重传的问题是,重传周期相对较长。
快速重传
快重传是TCP的另一种重传机制
举个例子,加入发送方发送了5个数据,编号的顺序是 Seq1 ~ Seq5 ,但是 Seq2 丢失了。
- 第一份 Seq1 先送到了,于是就 Ack 回 2;(此时回Ack2表明接收方想要接受的下一个数据包的序列号为2)
- 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
- 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。通过重传最后收到了Seq2,因为此时3、4、5也都收到了,所以 Ack 回 6.
【dayceng防盗链提醒:爬虫,你毁了中文技术blog环境】
但如果Seq2之后还有一个Seq4也丢失了,此时接收方也只会不断回复Ack2,此时就有两种决策:
- 选择重传Seq2,但是这样效率很低,因为之后丢失的Seq4也要触发三次Ack4才会重传
- 选择重传Seq2之后的所有报文,这样虽然能解决前面的问题,但是又重复接收了一些已经传输到了的数据包,浪费了资源
因此可以在选择第二种处理方式的基础上,使用选择重传(SACK)机制,只重传没接收的数据
滑动窗口
TCP是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了,再发送下一个。这种你一句我一句的信息传输方式效率实际上是比较低的。一个重要的缺点就是:数据包的往返时间越长,通信的效率就越低。为了解决这个问题,TCP就引入了窗口的概念
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
窗口大小由哪一方决定?
TCP头里有一个字段叫 Window
,也就是窗口大小。这个参数通常是由接收方确定的,接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。(滑动窗口并不是一成不变的,会根据双方的传输情况进行变化)
流量控制
TCP正是通过滑动窗口来实现的流量控制机制。发送方和接收方会维护一个滑动窗口,用于控制数据的发送和接收速率。
- 发送方的滑动窗口:发送方维护一个发送窗口,它表示发送方当前允许发送的数据量大小。发送方将数据切分成较小的数据段,并按照窗口大小发送给接收方。发送方根据接收方的反馈信息动态调整发送窗口的大小。
- 接收方的滑动窗口:接收方维护一个接收窗口,它表示接收方当前能够接收的数据量大小。接收方会在 TCP 报文中的确认号字段中告知发送方自己的接收窗口大小。通过发送这个信息,接收方告诉发送方它还能够接收多少数据。
- 动态调整窗口大小:发送方根据接收方的接收窗口大小进行动态调整。如果接收方的接收窗口变大,发送方可以增加发送窗口的大小,以提高传输速率;如果接收窗口变小,发送方需要减小发送窗口的大小,以避免过快发送导致的数据丢失或拥塞。
通过不断地基于接收方的反馈调整发送窗口的大小,TCP实现了流量控制,确保发送方和接收方之间的数据传输速率平衡,避免了数据的丢失和网络拥塞。
需要注意的是,TCP的流量控制是基于接收方的处理能力进行调节的,而不是根据网络的拥塞程度。TCP 的拥塞控制是另一个机制,在面对网络拥塞时对发送方的发送速率进行调整。流量控制和拥塞控制是 TCP 中两个独立但相关的机制。
拥塞控制
流量控制只是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。
如果在网络出现拥堵时还继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大....
于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。
什么是拥塞窗口?和发送窗口有什么关系呢?
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
拥塞窗口 cwnd
变化的规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大; - 但网络中出现了拥塞,
cwnd
就减少;
那么怎么知道当前网络是否出现了拥塞呢?
比如发送方在规定时间内没有收到确认报文,也就是发生了超时重传,那么就可以判定为出现了拥塞
拥塞控制主要是四个算法:
- 慢启动(当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。)
- 拥塞避免(每当收到一个 ACK 时,cwnd 增加 1/cwnd。)
- 拥塞发生(使用超时重传和快速重传进行介入)
- 快速恢复
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
如何处理粘包?
粘包是指发送方连续发送的多个小数据包在接收方处被误认为一个大数据包,或者一个大数据包被分割成多个小数据包接收。这可能会导致数据解析错误或丢失部分数据。
可以使用消息边界来解决这个问题。在应用层协议中定义明确的消息边界标识,接收方根据消息边界来区分不同的消息。例如,在报文中可以使用特殊字符或标签标识消息的开始和结束。
此外,还可以使用定长包(发送方将每个数据包都固定为相同的长度。接收方按照固定长度进行拆包和处理。如果数据不足定长,可以用填充数据补齐。)或者设定时间间隔的方式去处理。
使用wireshark怎么查看滑动窗口的大小(用来分析拥塞发生的原因)
造成丢包(报文丢失)的原因有很多,例如:
- 数据路径上发生了流量突发,导致链路拥塞(突然打了一个大流量就可能导致严重的丢包)
- 特点是突发性强,持续时间短
- 代理节点出现了问题
- 这会影响所有经过该节点的数据
出现丢包现象时,首先可以先抓个包保留一下证据,也方便后续的分析。然后还可以使用nstat命令实时观察系统中每秒tcp重传报文的数量,或者使用ss -tanl来查看整体网络的一个统计情况。
在ss输出的重传统计中会列出每个IP+端口的重传情况,里面有两个参数可以参考
- (1)、recv-Q:网络接收队列,会显示已经到达本地缓存的数据,这些数据已经被接受,但有可能还没来得及被进程取走,如果短暂不为0,可能是处于半连接状态,如果接收队列Recv-Q一直处于阻塞状态,可能是遭受了DOS攻击
- (2)、send-Q:网路发送队列,对应的发送缓存区,这里显示了发送缓存区中还有多少数据。如果发送队列Send-Q不能很快的清零,可能是有应用向外发送数据包过快,或者是对方接收数据包不够快
具体分析抓到的包时,可以用wireshark的过滤器,通过"tcp.analysis.lost_segment"、"tcp.analysis.retransmission"来筛选出丢包数据包,观察这些包在什么地方有问题(比如传输的数据大小等等)
然后wireshark中的会话统计功能还可以统计通信会话之间接收和发送的数据包和字节数,通过这个工具可以找出网络中哪个会话(IP地址或端口号)占用带宽最大,进一步作出网络策略。
Wiresherk的info常见提示
- 1、Packet size limited during capture 说明被标记的那个包没有抓全。一般是由抓包方式引起,有些操作系统中默认只抓每个帧的前96个字节
- 2、TCP Previous segment not captured 如果Wireshark发现后一个包的Seq大于Seq+Len,就知道中间缺失了一段,如果缺失的那段在整个网络包中找不到(排除了乱序),就会提示
- 3、TCP ACKed unseen segment 当Wireshark发现被Ack的那个包没被抓到,就会提示
- 4、TCP Out-of-Order 当Wireshark发现后一个包的Seq号小于前一个包的Seq+Len时,就会认为乱序,发出提示
- 5、TCP Dup ACK 当乱序或丢包发生时,接收方会收到一些Seq号比期望值大的包。没收到一个这种包就会Ack一次期望的Seq值,提现发送方
- 6、TCP Fast Retransmission 当发送方收到3个或以上的【TCP Dup ACK】,就意识到之前发的包可能丢了,于是快速重传它
- 7、TCP Retransmission 如果一个包真的丢了,又没有后续包可以在接收方触发【Dup Ack】就不会快速重传,这种情况下发送方只好等到超时了再重传
- 8、TCP zerowindow 包种的“win”代表接收窗口的大小,当Wireshark在一个包中发现“win=0”时,就会发提示
- 9、TCP window Full 此提示表示这个包的发送方已经把对方所声明的接收窗口耗尽了
- 10、Time-to-live exceeded(Fragment reassembly time exceeded)
TCP序列号和确认号是如何变化的?
- 公式一:序列号 = 上一次发送的序列号 + len(数据长度)。特殊情况,如果上一次发送的报文是 SYN 报文或者 FIN 报文,则改为 上一次发送的序列号 + 1。
- 公式二:确认号 = 上一次收到的报文中的序列号 + len(数据长度)。特殊情况,如果收到的是 SYN 报文或者 FIN 报文,则改为上一次收到的报文中的序列号 + 1。
ICMP协议
主要用于网络设备之间的通信,以执行网络故障排除和管理任务。ICMP 报文是封装在 IP 包里面,它工作在网络层,是 IP 协议的助手。
ICMP协议的主要功能:
- 错误报告:ICMP用于报告与IP数据包相关的错误和异常情况。例如,当一个路由器无法将数据包传递到目标主机时,它可以生成一个ICMP错误消息通知源主机。
- 网络探测:ICMP还用于执行网络探测和诊断任务。例如,Ping工具使用ICMP Echo请求和响应消息来测试主机的可达性和响应时间。
- 路由器通知:路由器可以使用ICMP消息来通知其他路由器或主机有关网络拓扑的变化。这对于路由表的更新和路由器的重新路由非常重要。
- 时间戳和网络延迟:ICMP协议支持时间戳消息,允许计算机测量网络往返时间和延迟。
为什么断网了还能 ping 通 127.0.0.1
当发现目标IP是外网IP时,会从"真网卡"发出。
当发现目标IP是回环地址时,就会选择本地网卡。
本地网卡,其实就是个"假网卡",它不像"真网卡"那样有个ring buffer
什么的,"假网卡"会把数据推到一个叫 input_pkt_queue
的 链表中。这个链表,其实是所有网卡共享的,上面挂着发给本机的各种消息。消息被发送到这个链表后,会再触发一个软中断。
软中断后会立马去链表里把消息取出,然后顺着数据链路层、网络层等层层往上传递最后给到应用程序。相当于自己给自己发数据
ping
回环地址和ping
本机地址,是一样的,走的是lo0 "假网卡",都会经过网络层和数据链路层等逻辑,最后在快要出网卡前狠狠拐了个弯, 将数据插入到一个链表后就软中断通知 ksoftirqd 来进行收数据的逻辑,压根就不出网络。所以断网了也能ping
通回环地址。
127.0.0.1 和 localhost 以及 0.0.0.0 有区别吗
localhost
就不叫 IP
,它是一个域名(与"baidu.com"
是一个形式的东西),默认会把它解析为 127.0.0.1
(可以在hosts文件中设置)
127.0.0.1
是回环地址。localhost
是域名,但默认等于 127.0.0.1
。
0.0.0.0
, 那么它表示本机上的所有IPV4地址。
【watching you!】
操作系统/计组
内存管理相关
什么是虚拟内存,为什么使用虚拟内存
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。
事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局(具体就是初始化进程控制表中内存相关的链表),实际上并没有立即把虚拟内存对应位置的程序数据和代码(比如.text.data段) 拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如 malloc 时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
当 CPU 访问的页面不在物理内存时,便会产生一个缺页中断,请求操作系统将所缺页调入到物理内存。那它与一般中断的主要区别在于:
- 缺页中断在指令执行「期间」产生和处理中断信号,而一般中断在一条指令执行「完成」后检查和处理中断信号。
- 缺页中断返回到该指令的开始重新执行「该指令」,而一般中断返回回到该指令的「下一个指令」执行。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
虚拟内存的好处:
1.扩大了地址空间;
2.内存保护(提供写保护):每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改;
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间;
4.当进程通信时,可采用虚存共享的方式实现(方便实现内存共享机制);
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存;
6.方便了多任务处理:虚拟内存技术可以为每个进程提供独立的虚拟地址空间,使得多个进程之间不会相互干扰,方便了多任务处理。
虚拟内存的代价:
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存;
2.虚拟地址到物理地址的转换,增加了指令的执行时间;
3.页面的换入换出需要磁盘 I/0,这是很耗时的;
4.如果一页中只有一部分数据,会浪费内存;
内存泄漏和内存溢出的关系
- 内存泄漏(Memory Leak)是指程序在分配内存后,无法释放不再使用的内存,导致系统中的可用内存逐渐减少。这最终可能导致程序运行变慢或崩溃。
- 内存溢出(Memory Overflow)是指程序试图访问超出其分配内存范围的内存位置。这通常会导致程序崩溃或不可预测的行为。
内存泄漏和内存溢出都与程序的内存管理有关,但它们发生的情境和后果是不同的。内存泄漏是指程序未能释放不再使用的内存,导致系统中的内存资源浪费。内存溢出是指程序试图访问未分配给它的内存区域,通常会导致程序崩溃
为了避免内存泄漏,我们应该负责显式释放不再使用的内存,例如使用 delete
或 free
来释放动态分配的内存。
为了避免内存溢出,要确保程序访问内存时不会超出分配的范围,例如在数组访问时要确保不越界。
c++中的内存管理(虚拟内存分布)
c++中的内存管理一般来说是用来管理虚拟内存分布的
虚拟内存分为:代码段、数据段、BSS段、堆区、文件映射区以及栈区
代码段有只读存储区和文本区,只读存储区存放字符串常量,文本区存储机器码
数据段用来保存程序中已经初始化的全局变量和静态变量
BSS(Block Started by Symbol)段用来保存未初始化的全局变量和静态变量
堆区负责在调用new/malloc时进行动态的一个内存分配
映射区则负责存储动态链接库以及调用mmap函数进行文件映射
栈使用栈空间存储函数的返回地址、参数、局部变量以及返回值
其中,代码段、数据段和bss是静态区域,堆、映射区、栈是动态区域
如何判断内存泄漏?(先说一下内存泄漏的定义,再说工具)
内存泄漏是指程序在获得内存后,无法释放不使用的内存,导致系统内存不断减少的一种现象。
首先我们可以排查一下程序中每处申请内存的地方是否有与之对应的内存回收操作,另外我们还可以使用以下内存泄漏的检查工具来定位,比如Valgrind,用来统计当前申请和释放的内存是否一致,进而判断内存是否泄漏。
什么时候会发生段错误?
段错误通常发生在访问非法内存地址的时候,比如使用了野指针或者试图修改字符串常量的内容
结构体内存对齐规则?32位?64位?
结构体内存对齐规则中,基本的对齐单位是字节,32位系统是4字节,64位是8字节。
然后结构体的起始地址必须是基本对齐单位的整数倍,结构体成员的偏移量必须是其类型大小或基本对齐单位的整数倍
假如有一个结构体,第一个是char,第二个是一个sizeof为1M的资源,那这个结构体有多大?
这个可能要看编译器的实现以及内存对齐规则。
根据默认的内存对齐规则,结构体的对齐方式通常是按照结构体中最大类型的大小进行对齐。
在这种情况下,char类型的大小通常为1字节,而sizeof为1M的资源会被视为一个较大的类型,在32位系统中通常为4字节对齐,在64位系统中通常为8字节对齐。
因此,在32位系统上,这个结构体的大小将为8字节(char类型的1字节,补齐3字节,1M资源的4字节对齐);在64位系统上,这个结构体的大小将为16字节(char类型的1字节,补齐7字节,1M资源的8字节对齐)。
为什么要有内存对齐的一个操作呢?
内存对齐可以提高计算机的存取效率:
如果某个变量没有按照对齐要求存储,那么在访问该变量时,计算机可能需要进行额外的操作来调整数据的位置,从而影响系统性能。
内存对齐有助于提高缓存的命中率:
现代计算机的多级缓存通常以缓存行(cache line)为单位进行读写。如果一个结构体或数组跨越了多个缓存行,那么在访问的时候就会导致额外的缓存读取,降低缓存的效果。
因此,内存对齐可以使数据按照缓存行对齐,提高缓存命中率,提升系统性能。
还有就是一些硬件平台对于数据的对齐要求很严格,不做内存对齐可能会导致访问异常。
大端(Big Endian)和小端(Little Endian)是两种不同的字节序排列方式,用于存储多字节数据在计算机内存中的顺序。
大端存储是指低字节存储在高地址,小端存储是指低字节存放在低地址
如果有一个数字13,使用大端和小端存储时有什么不同
数字13可以用一个字节来表示,二进制为00001101。现在我们将它存储在内存中:
-
大端存储: 在大端字节序中,最高有效字节(Most Significant Byte,MSB)位于低地址。所以数字13在大端存储中的表示是:
00001101
-
小端存储: 在小端字节序中,最低有效字节(Least Significant Byte,LSB)位于低地址。所以数字13在小端存储中的表示是:
00001101
注意,对于一个只有一个字节的数字13,无论是大端还是小端存储,其表示都是相同的,因为字节的顺序在这种情况下不会有区别。不同字节序的差异通常在多字节数据类型(如整数)的存储时才会显现出来。
为什么不能无限递归?
由于c++的虚拟内存管理机制,函数的调用会使用栈区的空间,每次函数调用都会分配一定的栈空间,栈空间是有限的。如果无法触发终止条件,那么函数就会不断压栈,直到栈溢出。总的来说,过深的递归或者说无限递归会降低性能甚至耗尽资源。
解决办法是什么?
要避免无限递归,需要在设计递归算法的时候确定好递归停止的条件,确保递归调用可以正常结束。还有就是尽量少的在实际开发中使用递归,可以使用迭代的方法代替递归操作。
怎么调整栈大小?
在Linux系统中,可以使用ulimit命令来限制进程的资源使用,包括栈大小。
还可以通过编译器进行调整,在gcc中好像有个参数是stack_size可以控制这个。
Linux中的内存管理(虚拟内存技术)
对于32位的处理器来说,虚拟空间的大小为4g,每个进程认为自己有4g空间。但实际上他们对应的物理内存可能只有很小的容量
进程得到的4个g的虚拟内存在进程的视角中认为是连续的,但实际上通常会被分割成多个物理内存碎片,还有一部分可能在外部存储器上,在需要时进行数据交换
由于存在两个内存地址,因此一个程序从编写到被执行,需要进行两次映射。
第一次映射到虚拟内存空间,第二次映射到物理内存空间。这两部分工作需要软硬件配合实现,硬件部分是存储管理单元MMU,软件部分是操作系统的内存管理模块
Linux中的虚拟内存使用了分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。
分页机制下,虚拟地址和物理地址是如何映射的?
简单来说是:先把虚拟内存地址切分成页号和偏移量,根据页号从页表里查询对应的物理页号,然后直接拿物理页号加上前面的偏移量,就得到了物理内存地址
new和malloc了解吗?在分配内存时两者的区别。
(答了显示指定内存以及释放内存的时候的区别)
当涉及到内存分配时,一般会使用new
或malloc
。
new
是C++中的一个关键字,它用于在堆上动态分配内存以创建对象,并会调用构造函数进行初始化。另一方面,malloc
是C语言中的一个函数,用于分配指定大小的内存块,但它不会调用构造函数。如果内存分配失败,new
会抛出std::bad_alloc
异常,而malloc
会返回一个空指针。
总的来说,new
更适合在C++中处理对象的动态分配,因为它自动处理了对象的构造,而malloc
则是C语言中的标准做法。
Malloc 在申请内存时,一般会通过 brk 或者 mmap 系统调用进行申请。其中当申请内存小于128K 时,会使用系统函数 brk 在堆区中分配;而当申请内存大于 128K 时,会使用系统函数 mmap在映射区分配
如果使用double free会发生什么?
(答了会找不到内存区间然后发生异常行为。追问:举个例子。回答深浅拷贝)
使用double free会导致严重的内存管理错误,可能会导致:
- 未定义行为
- 内存损坏(释放了相同的内存块两次,导致堆管理器无法正确地跟踪哪些内存块是空闲的,因此它们可能永远不会再次分配给程序使用)
- 安全漏洞
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
例子:除了粗心自己写了两个释放语句以外的情况,还有一种比较隐蔽的情况可能会导致双重释放。比如在main函数中定义了一个指针,该指针被传入一个函数中,在函数逻辑处理完成后,该指针可能在函数中被释放,然后main函数中有可能会将其再次释放,这就产生了double free
堆和栈区别
栈和堆是计算机内存中用于存储数据的两个区域,它们有以下几个区别:
- 内存分配方式:栈采用静态内存分配(编译时分配),而堆采用动态内存分配(运行时分配)。
- 空间大小:栈的空间很小,通常只有几兆字节,而堆的空间很大,可以达到几百兆字节或更多。
- 内存管理方式:栈的管理方式由编译器自动完成,不需要程序员手动管理;而堆的管理方式由程序员手动进行内存分配和释放。
- 内存效率:由于栈采用静态内存分配,并且内存分配和释放都是由编译器进行管理的,所以其效率比堆要高。而堆采用动态内存分配,内存分配和释放都需要程序员手动管理,所以其效率相对较低。(栈的效率高于堆)
- 数据存储方式:栈中存储的数据有固定的顺序,按照先进后出的原则进行存储,而堆中的数据没有固定的顺序,可以随时进行访问。
总的来说,栈和堆各自适用于不同的场景。
栈适合存储局部变量、函数调用等数据,而堆适合存储动态分配的数据对象、大型数组等数据。在使用堆时需要注意内存泄漏和野指针等问题,需要程序员手动进行内存管理。
进程管理相关
是否了解进程线程协程
线程进程概述
进程是对运行时程序的封装,是进行系统资源调度和分配的基本单位,实现了操作系统的并发。
而线程是进程的子任务,是CPU调度和分配的基本单位,用于保证程序的实时性,实现进程内部的并发
那先说一下进程。代码写好之后是一个存储在硬盘中的静态文件,编译之后会生成二进制可执行文件,当运行这个可执行文件的时候,它会被装载到内存中,接着cpu会执行程序中的每一条指令。那么这个运行中的程序就被称为进程process。
那么实际上维护多进程的开销其实是比较大的,比如进程创建、切换的时候涉及到上下文状态的保存。此外进程间通信相对来说也比较麻烦,开销也大。于是就有了线程的概念,线程是进程当中的一条执行流程,线程之间可以并发运行且内存是共享的。
线程的优点:
- 一个进程中可以有多个线程;
- 各线程之间可以并发执行;
- 各线程之间共享地址空间和文件等资源
线程的缺点也是由于共享内存导致的,如果某个进程内的某个线程出现问题崩溃了,那么有可能会导致该进程也跟着崩溃(因为系统可能认为后续会发生一些严重错误,所以干脆就使用信号机制kill了进程)
线程和进程的比较:
- 进程是资源分配的单位,线程是cpu调度的单位;
- 进程拥有完整的资源平台,线程只是独享必不可少的资源,比如寄存器和栈;
- 线程同样也具有就绪、阻塞、运行三种状态,也有状态之间相互转换的关系;
- 线程能够减少并发执行的时间空间开销
减小开销具体体现在:
线程的创建和终止时间比进程快,因为线程不用对资源进行管理。还有就是同一个进程内的线程切换比进程切换要快,因为线程间是共享地址空间的,也就是说虚拟内存是共享的,因此一个进程中的所有线程都有一个相同的页表,所以在切换线程时不用切换页表,这就比进程快了,进程切换时是要切换页表的。
对于线程和进程,我们可以这么理解:
- 当进程只有一个线程时,可以认为进程就等于线程;
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
协程概述
而协程(Coroutine)是一种比线程更轻量级的并发编程方式。它可以在单个线程内实现多个协程之间的切换,从而达到并发执行的效果。
与线程不同的是,协程是由程序员显式地控制切换的,而不是由操作系统进行调度。
程序员可以定义多个协程,并在适当的时候手动切换执行。
协程的本质是一种特殊的控制流程,它可以在程序执行过程中暂时挂起,并在需要时继续执行。
C++的协程支持库使用面向对象的方式来实现协程,它通过生成器和状态机的组合来实现协程的挂起和恢复。具体而言,协程的状态信息被封装在一个对象中,协程的执行过程被编码为状态转移的过程。
在编译时,编译器会将协程的代码转换为状态机的形式,并插入相应的挂起和恢复操作。
协程的优点:
- 轻量级:相较于线程,协程的创建和切换开销更小,因为它们不需要操作系统的介入,而是由程序员自行控制。
- 高效利用CPU:协程可以通过在任务之间切换来充分利用CPU资源,减少线程切换的开销。
- 简化编程模型:协程可以将复杂的异步编程任务简化为顺序执行的代码,提高代码的可读性和可维护性。
- 避免竞态条件:由于协程在单线程中执行,不存在多线程并发访问共享数据的竞态条件问题,因此可以避免相关的线程同步和锁机制。
协程的缺点:
- 无法利用多核CPU:由于协程仍然运行在单个线程中,无法充分利用多核CPU的优势。
- 阻塞操作会阻塞整个线程:如果协程中执行了阻塞操作,如IO操作或者等待其他协程的结果,那么当前线程将被阻塞,无法处理其他协程。
总结来说,协程是一种轻量级的并发编程方式,可以在单线程内实现多个协程之间的切换,从而提高程序的并发性和效率。它通过简化编程模型和减少线程切换开销,提供了一种高效、方便的并发编程解决方案。
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
1、进程与线程的区别
- 【关系】一个线程只能属于一个进程,一个进程至少有一个线程,线程需要依赖进程而存在
- 【内存】进程在执行过程中拥有独立的内存单元,多个线程共享进程的内存(同一个进程中的多个线程共享代码段(代码和常量)、数据段(全局变量和静态变量)、以及堆存储)
- 【开销】进程在创建或者撤销时,系统都要为之分配或回收资源,这些操作的开销是显著大于创建线程时的开销的
- 进程间不会互相影响,但是如果一个进程内的一个线程挂了那么整个进程也会挂掉(因为每个线程都是共享内存的,一个出错那么其他的也会出错,因此游戏设计时不要使用多线程)
2、进程有哪些状态?
- 运行状态;(时刻占用cpu)
- 就绪状态;(随时可以变为运行状态)
- 阻塞状态;(正在等待某一事件发生)
3、进程的控制过程有哪些?
- 创建
- 终止(正常结束、异常结束、外界干预kill)
- 阻塞
- 唤醒
4、什么是进程上下文?上下文切换的是什么?会发生在哪些场景
从一个进程切换到另一个进程运行,称为进程的上下文切换。
进程上下文切换中涉及到的资源包括虚拟内存、栈、全局变量等用户空间资源,以及内核态中的堆栈、寄存器等;
典型场景有:发生时间片轮转、系统资源不足、发生硬件中断
线程进程区别,怎么唤醒阻塞线程?
线程是进程中的一条执行路径,每个线程都运行在进程的上下文中,并共享该进程的系统资源,如内存、文件句柄等。线程可以看作是轻量级的进程,相对于进程而言,线程更加轻便和灵活。
进程和线程的区别主要包括以下几个方面:
- 进程和线程的调度:操作系统将CPU时间分配给进程和线程,但是进程和线程是不同的调度单位,它们的调度方式也不同。操作系统采用多任务调度技术来进行进程的调度,而线程的调度则由进程的线程调度管理器负责。
- 进程和线程的内存管理:每个进程都有自己独立的地址空间,而线程共享其所属进程的地址空间。因此进程之间的内存空间是隔离的,而线程之间可以直接访问共享内存。
- 进程和线程间的通信:进程间通信(IPC)需要使用特殊的机制,如管道、消息队列、共享内存等;而线程间通信只需要共享同一块内存即可。
- 进程和线程的创建与销毁:进程需要复制父进程的地址空间,创建新的进程实体,而线程则可以通过调用库函数创建,线程的销毁也相对比较容易。
唤醒阻塞线程的方法有多种,其中比较常见的方法包括:条件变量、信号量、事件等。以条件变量为例,当一个或多个线程需要等待某个特定条件时,可以调用条件变量机制提供的函数(如pthread_cond_wait()
)将其阻塞,那么其他线程可以修改条件并调用相应的通知函数(如pthread_cond_signal()
或pthread_cond_broadcast()
)来唤醒被阻塞的线程,使它们可以继续执行。
无论是条件变量还是信号量,它们都提供了一种机制,用于在特定条件满足时唤醒阻塞的线程。具体选择哪种机制取决于问题的需求和实际情况。
进程间通信方式
报菜名:进程间通信方式有管道、消息队列、共享内存、信号量、信号以及socket
管道
管道又分为匿名和命名管道,匿名管道中通信的数据是无格式的流且大小收到限制,只能在一个方向流动。匿名管道只能用于存在父子关系的进程间通信。(例如shell中的竖线就是匿名管道)
命名管道则可以在无关系的两个进程间进行使用
另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
消息队列
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。由于写入和读取时都要在用户态和内核态之间进行拷贝,所以消息队列的通信不是最及时的。
共享内存
共享内存则解决了消息队列中通信不及时的问题,避免了在用户态和内核态之间进行拷贝。
由此也产生了新的问题:多个进程之间争夺同一个共享资源会导致数据混乱
因此引入了信号量来保护共享资源,确保同一时刻只有一个进程访问该资源。
由于信号量本身是一个计数器,因此除了实现访问的互斥外,信号量还可以实现进程间同步。通过P操作和V操作来实现
P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
(P操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。)
信号
信号和信号量区别很大。信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件。
信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。
终止和停止进程的信号是无法被忽略的。
线程间通信方式
线程间的通信主要是为了协调不同线程之间的操作和数据交换。
主要有:互斥锁、条件变量、消息队列、共享内存等
互斥锁
互斥锁是最基本的线程同步机制之一。它允许一个线程独占一个共享资源,其他线程必须等待锁被释放后才能使用该资源。
相对应的还有自旋锁,自旋锁是一种忙等待锁,线程在尝试获得锁时不会被阻塞,而是反复检查锁是否可用,直到获得锁。它不会让线程进入睡眠状态,因此没有上下文切换开销。适用于短时间占用资源的情况
条件变量
条件变量通常与互斥锁一起使用,以解决特定的同步问题,如线程的等待和通知。
一些例子:
- 生产者-消费者问题: 多个生产者线程和消费者线程之间的协作,确保生产者在队列不满时生产,消费者在队列不空时消费。
- 读者-写者问题: 控制多个读者和写者之间的访问,以确保在写者进行写操作时,不允许读者进行读操作,以维护数据的一致性。
- 线程池任务管理: 在线程池中,当没有任务可执行时,线程可能等待条件变量以获取新任务的通知。
条件变量通常与互斥锁一起使用,以确保线程在等待条件时不会出现竞争条件。在使用条件变量时,通常有两个主要操作:等待条件的线程会等待条件的发生,而其他线程会通过通知来通知等待线程。
阻塞和等待分别是什么?有什么区别?
答:阻塞和等待是两个与多线程和并发编程相关的概念。
阻塞是指一个线程在执行过程中因为某些原因停止了继续执行,通常是因为需要等待某个条件的满足或者资源的可用性。阻塞可以发生在多种情况下,例如等待用户输入、等待文件读写完成、等待网络数据到达等。
等待是指一个线程主动地进入等待状态,通常是因为它需要等待其他线程完成某个特定的任务或者通知。线程在等待状态下不会占用 CPU 资源,直到被唤醒。等待通常用于线程之间的协作和同步,一个线程等待另一个线程的信号或通知来继续执行。
所以,主要区别在于阻塞是由于外部条件不满足而导致的线程暂停,而等待是线程主动选择进入等待状态,等待通常用于线程之间的协作和同步。一个示例是,阻塞可以发生在等待用户输入时,而等待可以用于线程等待其他线程完成某项工作。
是否了解死锁?产生死锁的原因
死锁就是两个线程为了维护两个不同的共享资源而使用了两个互斥锁,如果锁使用不当就会造成两个线程都在等待对方释放资源的情况,如果没有外力介入,这种等待回一直持续下去,也就形成了死锁。
死锁要同时满足四个条件才会发生:
- 互斥条件;(多个线程不能同时使用一个共享资源)
- 持有并等待条件;(线程A在等待资源2时不会释放自己已经持有的资源1)
- 不可剥夺条件;(对于已经获得的共享资源,在没有用完之前其他线程不能获取)
- 环路等待条件;(两个线程获取资源的顺序构成了环形链)
互斥锁和自旋锁
互斥锁(Mutex Lock)和自旋锁(Spin Lock)都是在多线程编程中用于管理并发访问共享资源的同步机制,但它们有不同的工作原理和适用场景。
- 互斥锁(Mutex Lock):
- 互斥锁是一种阻塞锁,它允许一个线程在获得锁之前等待其他线程释放锁。当一个线程获得了互斥锁,其他线程就会被阻塞,直到拥有锁的线程释放它。
- 互斥锁提供了一种可靠的同步机制,适用于对共享资源的互斥访问,以防止多个线程同时修改资源,从而确保数据的一致性和完整性。
- 互斥锁通常会消耗一定的系统资源,并且在高度竞争的情况下可能导致线程阻塞和上下文切换开销增加。
- 自旋锁(Spin Lock):
- 自旋锁是一种忙等待锁,线程在尝试获得锁时不会被阻塞,而是反复检查锁是否可用,直到获得锁。它不会让线程进入睡眠状态,因此没有上下文切换开销。
- 自旋锁适用于对共享资源的短暂占用,如果锁被其他线程持有的时间很短,自旋锁可能比互斥锁效率更高。
- 然而,自旋锁在高度竞争或长时间等待锁的情况下可能会导致CPU资源的浪费,因为线程会持续检查锁状态,而不会释放CPU给其他线程。
什么是条件变量?
条件变量(Condition Variable)是多线程编程中的一种同步机制,用于在线程之间实现复杂的协作和通信。条件变量通常与互斥锁一起使用,以解决特定的同步问题,如线程的等待和通知。
条件变量的使用场景包括但不限于以下情况:
- 生产者-消费者问题: 多个生产者线程和消费者线程之间的协作,确保生产者在队列不满时生产,消费者在队列不空时消费。
- 读者-写者问题: 控制多个读者和写者之间的访问,以确保在写者进行写操作时,不允许读者进行读操作,以维护数据的一致性。
- 线程池任务管理: 在线程池中,当没有任务可执行时,线程可能等待条件变量以获取新任务的通知。
条件变量通常与互斥锁一起使用,以确保线程在等待条件时不会出现竞争条件。在使用条件变量时,通常有两个主要操作:等待条件的线程会等待条件的发生,而其他线程会通过通知来通知等待线程。
乐观锁悲观锁
定义
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
乐观锁全程并没有加锁,所以它也叫无锁编程。
适用场景
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
悲观锁适用于写操作多的场景,因为写操作具有排他性,使用悲观锁可以防止读写或者写写冲突,缺点是加锁时间比较长,可能会长时间限制其他用途的访问(即并发访问性能不好)
生产者消费者问题
生产者-消费者问题是经典的多线程同步问题,它涉及到一个共享的缓冲区,其中一个或多个生产者将数据放入缓冲区,而一个或多个消费者从缓冲区中取出数据。
解决生产者-消费者问题的方法主要有两种:信号量和条件变量。其中,使用条件变量是一种比较常见的做法。
在条件变量中解决生产者-消费者问题的过程如下:
首先,需要对共享资源即缓冲区进行加锁,以确保同一时间只有一个线程可以访问该缓冲区。
缓冲区是指在生产者-消费者问题中用来存储数据的一块共享内存空间。通常是在操作系统内核中创建的,它被多个线程或进程所共享
在生产者线程中,如果缓冲区已满,则等待条件变量,即“非满条件”,并释放互斥锁,让其他线程可以访问该缓冲区。
在消费者线程中,如果缓冲区为空,则等待“非空条件”并释放互斥锁。
当缓冲区中有数据时,生产者将数据放入缓冲区,并通过“非空条件”通知消费者缓冲区中有数据可供消费;消费者从缓冲区中取出数据,并通过“非满条件”通知生产者缓冲区中有空间可供生产。
最后,当所有线程都完成任务后,需要将锁和条件变量释放,以便其他线程可以访问缓冲区。
通过使用条件变量,可以实现生产者和消费者之间的有效协调,确保生产者不会向满的缓冲区中添加数据,消费者不会从空的缓冲区中取出数据。这样可以避免竞争和死锁等多线程编程中常见的问题。
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
调度算法
调度算法有很多应用场景,进程调度、页面置换以及磁盘调度时都会涉及到调度算法
进程调度
进程调度算法用于帮助CPU合理分配每个进程应该处理的任务,常见的有先来先服务调度算法、最短作业优先调度算法以及时间片轮转调度等。
先来先服务,顾名思义就是每次让就绪队列中最先入队列的进程运行,直到进程退出或阻塞。(如果长作业先运行了就会增加等待时间)
最短作业调度则是先让运行时间短的进程来运行,这样做可以提高吞吐量。(但是如果有很多短作业,长作业就会一直等着而不被执行)
时间片轮转则是权衡上述调度方法的优缺点之后的一个折中方案,即为每个进程分配时间片,这样在时间片内每个线程都可以公平的执行。
时间片设为
20ms~50ms
通常是一个比较合理的折中值
页面置换
为什么要进行页面置换?
主要是因为缺页异常(或者说缺页中断)
当cpu访问的页面不在物理内存的时候,便会产生一个缺页中断,用来请求操作系统将所缺的页调入物理内存中
如果发生缺页中断后,当前物理内存中没有可供使用的空闲页进行换入,那么说明现在的内存已经满了。这时就需要一个算法去选择一个物理页来用于置换。
页面置换算法的目标是尽量页面减少换入换出的次数,常见的置换算法有:最佳页面置换算法【置换在「未来」最长时间不访问的页面】、先进先出置换算法【选择在内存驻留时间很长的页面进行中置换】、LRU和LFU等
前两种都比较理论,说说后两种
最近最久未使用置换算法的基本思路是,发生缺页时,选择最长时间没有被访问的页面进行置换
最不常用算法会在发生缺页中断时,选择访问次数最少的页面并将其淘汰,实现方式是为每个页面设置一个访问计数器,每次访问加1,中断时就选计数最小的页面淘汰
LFU 算法只考虑了频率问题,没考虑时间的问题,因此有可能会误伤当前刚开始频繁访问,但访问次数还不高的页面。
磁盘调度
磁盘调度算法的目的很简单,就是为了提高磁盘的访问性能,一般是通过优化磁盘的访问请求顺序来做到的。
这里也有先来先服务这样的算法,显然这种方式是暴力的,性能不会太好
比较常用的一个算法是最短寻道时间优先算法,该算法会优先选择从当前磁头位置所需寻道时间最短的请求。
Linux命令相关
linux中find,grep,awk,sed区别
-
find
用于查找文件和目录。-
查找当前目录及其子目录中所有的
.txt
文件:find . -name "*.txt"
-
-
grep
用于搜索文本文件中的模式。-
在文件
example.txt
中查找包含单词 "apple" 的行:grep "apple" example.txt
-
-
awk
用于对文本数据进行逐行处理和操作。-
打印文件
data.txt
中第二列(以空格分隔)的内容:awk '{print $2}' data.txt
-
-
sed
用于对文本进行编辑和替换操作。-
删除文件中包含 "error" 的行:
sed '/error/d' log.txt
-
linux df查看文件大小,效率很快,什么原因
运行 df
命令时,操作系统通常会将文件系统的磁盘使用信息缓存到内存中。这意味着 df
不需要实际上访问磁盘上的数据,而是从内存中快速获取这些信息。此外,df
命令执行的是非常轻量级的操作,它只需要读取文件系统元数据,而不涉及读取文件的实际内容。这就会比直接读取文件要快。还有就是操作系统对df命令做了很多优化,这也是其高效的原因。
软连接和硬链接,是什么?删除源文件后会发生什么?
软链接是指向另一个文件的特殊文件,类似于快捷方式。它包含源文件的路径或位置信息。删除源文件后,软链接仍然存在,但指向的文件不再存在,软链接称为“悬空链接”。【ln -s
命令】
硬链接是指在文件系统中创建一个源文件的副本,新创建的硬链接与源文件在磁盘上的存储位置相同。删除源文件后,硬链接仍然存在,因为它们实际上是对同一物理数据块的引用,只有当所有链接(包括源文件和硬链接)都被删除时,数据块才会被释放。【ln
命令】
Linux下怎么查看磁盘/cpu情况,具体命令有哪些?
查看磁盘情况
df命令:用于显示文件系统的磁盘空间使用情况。
df -h
du命令:用于查看文件或目录的磁盘使用情况。
du -h /path/to/directory
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
查看CPU情况
ps命令:ps
命令用于列出当前用户的进程。您可以使用不同的选项来筛选和显示所需的信息。例如,以下命令将显示所有当前用户的进程以及它们的CPU和内存占用情况
ps aux
top命令:top
命令可以用于查看系统中所有运行进程的实时资源使用情况,包括CPU、内存和其他资源的占用情况。
top
htop命令:类似于top,但提供了更多的交互功能和可视化信息。
htop
Linux下你习惯怎样查看日志?如果要取到某一个输出的日志(控制台)字段要怎么做?(如何查看内核日志)
我一般会将控制台输出的日志信息先通过日志库等方法保存到本地,然后使用vscode或者subline去查看,如果日志量比较大的话,我可能会考虑先将其处理为格式化结构再查看,比如json
如果要过滤日志,可以使用grep命令来进行一个搜索,这样可以得到包含关键字的某一行信息
此外也可以使用awk进行字段提取,配合cut就可以剪切并显示需要的字段,这些指令可以使用shell脚本进行整合
如果要实时监控日志文件的话,可以使用tail命令,这样新的日志内容会被输出到终端上。
如何查看内核日志?
可以使用dmesg命令去查看,该命令会显示内核缓冲区中的日志消息,设计内核启动、硬件检测和驱动加载等信息
或者可以到系统目录var/log下查看kern.log文件
如何在Linux后台运行一个服务?
一般我会使用nohup命令将执行的任务放在后台运行,这样不会影响到当前终端的使用,我一般还会指定一个日志文件用于记录后台程序的输出情况
nohup python3 -u train.py > 2test.log 2>&1 &#后台运行训练程序,log存至2test.log
cpu使用率激增,如何查看线程进行排查?
1、在终端窗口中输入 top
命令,然后按下 "Shift + H" 键,可以将进程列表切换为线程列表,从而查看正在运行的线程。
2、还可以使用ps、htop命令(在终端窗口中输入 htop
命令,然后按下 "H" 键,可以将进程列表切换为线程列表。)
慎用cat
都知道 cat
命令是用来查看文件内容的,但是日志文件数据量有多少,它就读多少,很显然不适用大文件。
Linux网络编程
一个网络数据包由哪些部分组成(例如报文头部之类的,详细的说)
一个网络数据包通常由这几个部分组成:报头、payload负载、校验和以及选项字段
报头一般存放关于数据包的元信息,比如五元组、数据包序号、时间戳等,这些信息用于帮助路由器和交换机将数据包以正确的顺序传输到目标的位置;
报头具体包括:源端口(16位2个字节)、目的端口(16位2个字节)、序号、确认号、保留字段(6位)、窗口、校验和以及一些标志位
payload包含了实际传输的信息或者负载,可以是文本、图像或音频等数据;
校验和用于验证数据包的完整性。数据包在传输时会计算并校验数据,将结果附加到数据包中,接收方会以此判断数据包是否损坏;
选项字段用于传递额外的控制信息,例如最大传输单元(MTU)的大小、时间戳、路由选项等;
Linux接收网络包的流程
数据写内存
当网卡接收到一个网络包后,会通过 DMA 技术,将网络包写入到指定的内存地址,也就是写入到 Ring Buffer ,这个是一个环形缓冲区,接着就会告诉操作系统这个网络包已经到达。
DMA(Direct Memory Access)直接内存访问技术,允许某些硬件绕过CPU直接直接访问系统内存;
Ring Buffer(环形缓冲区)是一种循环数据结构,通常用于在内存中临时存储数据。
当外设使用DMA技术直接访问内存时,它可以与环形缓冲区结合使用,实现高效的数据传输和存储。
一般来说,此时需要触发中断来告诉系统网络包已经到达。
通知
但是如果涉及到高速的网络传输,那么这种方式的效率就很低了。
所以为了解决频繁中断带来的性能开销,Linux 内核在 2.6 版本中引入了 NAPI 机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后 poll
的方法来轮询数据。
硬件中断处理
因此,当有网络包到达时,会通过 DMA 技术,将网络包写入到指定的内存地址,接着网卡向 CPU 发起硬件中断,当 CPU 收到硬件中断请求后,根据中断表,调用已经注册的中断处理函数。
硬件中断处理函数会做如下的事情:
- 先「暂时屏蔽中断」,告诉网卡下次再有数据就直接写内存不用通知CPU
- 触发「软中断」并恢复被屏蔽的中断
软中断处理
内核中的ksoftirqd线程专门负责软中断的处理,该线程会从 Ring Buffer 中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理。
协议栈处理
协议栈会逐层检查报文的合法性,最后获取到数据,接收结束
Linux发送网络包的流程
传输层
首先,应用程序会调用 Socket 发送数据包的接口,此时会从用户态陷入到内核态中的 Socket 层,内核会申请一个内核态的 sk_buff 内存,将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区。
接下来,网络协议栈从 Socket 发送缓冲区中取出 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。
接着,对 sk_buff 填充 TCP 头。这里提一下,sk_buff 可以表示各个层的数据包,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame。
至此,传输层的工作也就都完成了。
网络层
然后交给网络层,在网络层里会做这些工作:选取路由(确认下一跳的 IP)、填充 IP 头、netfilter 过滤、对超过 MTU 大小的数据包进行分片。处理完这些工作后会交给网络接口层处理。
网络接口层会通过 ARP 协议获得下一跳的 MAC 地址,然后对 sk_buff 填充帧头和帧尾,接着将 sk_buff 放到网卡的发送队列中。
软中断
上述工作准备好后,会触发「软中断」告诉网卡驱动程序,这里有新的网络包需要发送,驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,接着将 sk_buff 数据映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。
硬中断
当发送完成的时候,网卡设备会触发一个硬中断来释放内存,主要是释放 sk_buff 内存和清理 RingBuffer 内存。
最后,当收到这个 TCP 报文的 ACK 应答时,传输层就会释放原始的 sk_buff 。
发送网络数据的时候,涉及几次内存拷贝操作?
三次
第一次,将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区;
第二次,如果用的是TCP协议,从传输层进入网络层的时候,每一个 sk_buff 都会被克隆一个新的副本出来。副本 sk_buff 会被送往网络层,等它发送完的时候就会释放掉,然后原始的 sk_buff 还保留在传输层;
第三次,当 IP 层发现 sk_buff 大于 MTU 时会再申请额外的 sk_buff,此时会做拷贝
一个网络数据包由哪些部分组成(例如报文头部之类的,详细的说)
一个网络数据包通常由这几个部分组成:报头、payload负载、校验和以及选项字段
报头一般存放关于数据包的元信息,比如五元组、数据包序号、时间戳等,这些信息用于帮助路由器和交换机将数据包以正确的顺序传输到目标的位置;
报头具体包括:源端口(16位2个字节)、目的端口(16位2个字节)、序号、确认号、保留字段(6位)、窗口、校验和以及一些标志位
payload包含了实际传输的信息或者负载,可以是文本、图像或音频等数据;
校验和用于验证数据包的完整性。数据包在传输时会计算并校验数据,将结果附加到数据包中,接收方会以此判断数据包是否损坏;
选项字段用于传递额外的控制信息,例如最大传输单元(MTU)的大小、时间戳、路由选项等;
了解Linux下的socket吗?使用的流程是什么样的
Socket是一种在网络通信中使用的编程接口,允许不同计算机上的应用程序通过网络进行通信。
大概流程是:创建一个socketfd,然后使用bind()函数为socket绑定IP和端口号,以便客户端可以连接到。对于服务器来说,我们需要调用listen()
函数来监听连接请求,有请求发生时使用accept函数进行接受,然后我们就可以使用自己的逻辑或者系统的函数来发送或接收数据。通信结束就使用close函数关闭socket即可。
Linux IO多路复用
你用了epoll,说一下为什么用epoll,还有其他多路复用方式吗? 区别是什么?
epoll 是一种高效的 I/O 多路复用机制,它具有以下优势:
- 存储位置:epoll 将文件描述符集合维护在内核态,每次添加文件描述符时执行系统调用,避免了 select 和 poll 需要将整个集合拷贝到内核态的开销。
- 文件描述符集合表示方法:select 使用线性表,poll 使用链表,而 epoll 使用红黑树。红黑树的查询和插入操作时间复杂度为O(log n),使得 epoll 在处理大量文件描述符时具有更高的效率。
- 遍历方式:select 和 poll 每次调用需要遍历整个文件描述符集合来判断是否有文件描述符就绪,而 epoll 通过回调函数通知已经就绪的文件描述符,并将其放入就绪列表中等待处理,避免了每次遍历的开销。
- 触发模式:select 和 poll 只支持水平触发模式(LT),而 epoll 同时支持水平触发和边缘触发模式(ET)。ET 模式只在状态发生变化时通知一次,适合高效处理事件,避免了重复通知的开销。
- 适用场景:当需要监测的文件描述符数量较少且每个文件描述符都很活跃时,可以选择使用 select 或 poll。当需要监测的文件描述符数量较多且只有少数文件描述符活跃时,epoll 的性能优势更为明显。
总的来说,epoll 提供了更高效的 I/O 多路复用机制,适用于需要处理大量文件描述符和少量活跃文件描述符的场景,尤其在高并发服务器开发中被广泛应用。
说一下epoll是如何处理到来的连接的,使用了水平触发还是边沿触发?有什么区别?
epoll是一种事件驱动的I/O模型,在Linux系统中用于处理大量并发连接的高效机制。它使用了水平触发(LT)和边沿触发(ET)两种模式。
在水平触发模式下,当文件描述符上有可读或可写事件发生时,epoll_wait()函数会立即返回该事件,然后用户需要手动处理该事件。如果用户没有处理完事件,再次调用epoll_wait()函数时会再次返回该事件。
而在边沿触发模式下,当文件描述符上有可读或可写事件发生时,epoll_wait()函数只会通知一次,并且只有当事件状态发生变化时才会再次通知。用户需要在事件就绪后立即处理,否则可能会错过通知。
区别在于:
- 水平触发模式(LT):在每次epoll_wait()函数返回时,只要该文件描述符上仍然有就绪事件,后续的epoll_wait()函数调用仍然会返回该事件。这意味着用户需要对每个就绪事件进行处理,否则可能导致CPU资源浪费。
- 边沿触发模式(ET):在每次epoll_wait()函数返回时,只通知一次就绪事件,即使后续该事件还未处理完毕。只有当事件状态发生变化时,才会再次通知。用户需要确保在事件就绪后立即处理,否则可能会错过通知。
总的来说,水平触发模式适合使用轮询方式处理事件,而边沿触发模式则需要在事件就绪时立即处理,可以提供更高的性能。但是边沿触发模式的处理要求更高,如果不正确地处理事件,可能会导致丢失事件或漏处理事件的情况发生。
在我的服务器项目中也是用到了epoll
服务器的核心是事件监听+事件循环这么一个框架,使用epoll_wait轮询检测事件。
当一个新的客户端与服务器建立连接时,我们通过事件循环eventLoop()为其创建了一个新的socketfd,然后将其与一个定时器绑定并加入定时器链表进行管理。这期间会初始化一个http_conn对象并将该fd加入其中,该对象负责处理通信过程中http内容解析,该对象在工作线程中创建,由线程池进行管理。与此同时,工作线程会向日志处理模块的阻塞队列请求写入日志。
事件处理完成后会释放上面申请到的资源。
为什么ET模式不可以文件描述符阻塞,而LT模式可以呢?
在ET(边沿触发)模式下,当文件描述符有可读事件时,epoll_wait()函数只会通知一次,并且只有当事件状态发生变化时才会再次通知。因此,在ET模式下,如果将文件描述符设置为阻塞模式,可能会出现以下情况:
- 当有数据可读时,epoll_wait()函数通知一次,然后我们调用read()函数来读取数据。如果read()函数没有将所有数据读取完毕,而又没有再次触发事件,那么read()函数会阻塞等待更多的数据到达。这样就无法进行后续请求的处理,导致程序阻塞。
- 在ET模式下,需要循环读取数据直到将所有数据读取完毕。如果将文件描述符设置为阻塞模式,那么当所有数据读取完毕后,继续调用read()函数会阻塞等待新的数据到达,这样会导致无法进行后续请求的处理。
相比之下,在LT(水平触发)模式下,每当文件描述符上有数据可读时,epoll_wait()函数都会通知一次。这意味着在LT模式下,内核缓冲区肯定有数据可读,不会造成没有数据可读而阻塞的情况。因此,在LT模式下,可以将文件描述符设置为阻塞模式,能够较为方便地进行数据的读取和处理。
IO多路复用是同步还是异步?同步IO和异步IO有什么区别?
(linux本身提供的asio目前只支持文件fd,不支持网络fd,但是可以用一个线程去模拟异步IO的操作)
IO多路复用既可以是同步的,也可以是异步的。
同步IO是指在进行IO操作时,程序会进入阻塞状态,直到IO操作完成才会继续执行后续代码。在使用同步IO时,每次读写操作都需要等待数据准备好或者数据发送完成。
异步IO是指在进行IO操作时,程序不会阻塞等待IO操作完成,而是继续执行后续代码。当IO操作完成后,系统会通知程序,程序再去处理已完成的IO操作。在使用异步IO时,可以并发地执行其他任务,无需等待IO操作完成。
区别如下:
- 阻塞与非阻塞:同步IO是阻塞的,程序会一直等待IO操作完成;异步IO是非阻塞的,程序可以继续执行其他任务。
- IO操作的控制权:同步IO由程序主动调用IO操作,并在IO操作完成后获取结果;异步IO则是将IO操作的控制权交给系统,在IO操作完成后由系统通知程序。
- 并发性:同步IO一般只能处理一个IO操作,需要等待上一个操作完成后才能进行下一个操作;异步IO可以并发地处理多个IO操作,无需等待。
- 编程复杂性:同步IO编程相对简单,流程清晰,但由于IO操作会阻塞程序,效率较低;异步IO编程相对复杂,需要使用回调函数等机制来处理IO完成的通知,但可以提高程序的效率,特别适用于高并发场景。
综上所述,同步IO适合简单的IO操作或者对实时性要求不高的场景,而异步IO适合需要高并发和高性能的场景。IO多路复用既可以同步方式使用,也可以异步方式使用,具体取决于开发者的需求和应用场景。
MySQL
一条查询语句在MySQL中是怎么执行的
MySQL的架构分为两层:server层和存储引擎层
Server层负责建立连接、分析和执行SQL语句,存储引擎层则负责存储和读取数据
SQL语句主要是在server层被执行,主要步骤如下:
(1)与MySQL建立连接
这部分主要由连接器负责,客户端与连接器之间通过TCP建立连接,同时连接器会校验客户端的用户名密码,赋予用户对应的权限
(2)缓存查询
与连接器连接后,客户端就可以向MySQL服务发送SQL语句了,此时会解析第一个字段,看是什么类型的语句
如果是查询语句select,那么就会先去查询缓存(以 key-value 形式保存在内存中)里面找缓存数据,如果命中缓存,那么会直接返回查询值到客户端,否则就继续执行,当查询完成后结果会被缓存。
对于更新频繁的表,查询缓存的命中率很低,因为每次表更新缓存都会被清空。
(3)解析SQL语句
server层中的解析器负责对SQL语句进行词法和语法分析,词法分析就是区分出关键字和非关键字,而语法分析则是判断有无语法上的错误,没有的话就构建SQL语法树
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
(4)执行SQL语句
以select语句为例,其执行流程分为:预处理、优化以及执行三个阶段
预处理阶段中,预处理器会检查SQL查询语句中的表或者字段是否存在,并展开*字符号;
优化阶段中,优化器负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引;【可以使用explain查看使用的索引】
执行阶段中,执行器会和存储引擎交互了,交互是以记录为单位的。根据执行计划执行SQL查询语句,从存储引擎读取记录,返回给客户端;
效率比较低的SQL语句如何排查、优化
如果执行SQL响应比较慢,我觉得可能有以下4个原因:
第1个原因:没有索引或者没有命中导致索引失效。
针对上述情况,先收集一段时间MySQL的慢查询日志(使用SQL Profiler等工具),找出耗时最长的语句,可以利用explain去查看对应语句是否命中索引,如果没有命中,可以通过关键字来强制指定索引或者修改数据库的配置来设置查询优化器的行为。
如果没有索引,可以考虑在表上添加对应的索引
第2个原因:单表数据量数据过多,导致查询瓶颈
单表数据量过多也会出现查询瓶颈,即使使用了索引性能也不会特别好,此时就要考虑对表进行拆分,一般就是垂直拆分或者水平拆分
水平拆分的意思是把一张大表(数据行数达到千万级别的),按照业务主键切分为多张小表,这些小表可能达到100张甚至1000张。
垂直拆分的意思是,将一张单表中的多个列,按照业务逻辑把关联性比较大的列放到同一张表中去。
然后除了分表以外我们还可以考虑分库,也就是将拆分后得到的表在放到不同的数据库实例中。
这样的话,我们就可以根据业务主键把请求路由到不同数据库实例,从而让每一个数据库实例承担的流量比较小,达到提高数据库性能的目的。
第3个原因:网络原因或者机器负载过高。
这种情况可以进行读写分离
MySQL支持一主多从的分布式部署,我们可以将主库只用来处理写数据的操作,而多个从库只用来处理读操作。
在流量比较大的场景中,可以增加从库来提高数据库的负载能力,从而提升数据库的总体性能。
第4个原因:热点数据导致单点负载不均衡。
这种情况下,除了对数据库本身的策略调整以外,还可以增加缓存。
将查询比较频繁的热点数据预存到缓存当中,比如Redis、MongoDB、ES等,以此来缓解数据的压力,从而提高数据库的响应速度。
数据库回滚
回滚是指将未完成的事务的所有更改撤消,使它们不会影响数据库。
ROLLBACK;执行回滚,COMMIT;执行提交
回滚只适用于尚未提交的事务。已经提交的事务无法回滚。
mysql中有哪些锁,作用分别是什么
在MySQL中,主要有以下几种锁:
- 共享锁(Shared Lock):也称为读锁(Read Lock),多个事务可以同时持有共享锁,用于保护读操作,不阻塞其他事务的共享锁。共享锁之间是兼容的,即多个事务可以同时持有共享锁,但不能与排他锁(独占锁)同时存在。
- 排他锁(Exclusive Lock):也称为写锁(Write Lock),只允许一个事务持有排他锁,用于保护写操作,阻塞其他事务的任何锁类型。排他锁会阻止其他事务获取共享锁或排他锁。
- 记录锁(Record Lock):当对表中的某一行进行读或写操作时,MySQL会自动为该行加上记录锁,以保证数据的一致性。记录锁是行级别的锁,即只针对特定行有效,不影响其他行的操作。
- 间隙锁(Gap Lock):在多版本并发控制(MVCC)中使用的锁机制,在索引范围查询时,锁定被查询范围内的间隙,防止其他事务插入数据破坏查询结果。间隙锁是防止幻读的一种机制。
这些锁在MySQL中的作用如下:
- 共享锁和排他锁保证了读写操作的隔离性,避免了不一致的并发读写问题。
- 记录锁和间隙锁保证了多个事务之间对表中行的读写操作的一致性。
- 意向锁用于协调共享锁和排他锁的分配,避免了锁冲突和死锁的产生。
MySQL的事务机制
事务指的是单个逻辑工作单元在执行操作时,要么完全执行完成,要么完全不执行,也就是说不管哪一步出了问题都会直接回滚到初始状态。
一个逻辑工作单元要成为事务需要满足原子性、一致性、隔离性和持久性这几个属性。
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,发生错误会自动回滚到初始状态
- 一致性:就是指事务操作前后,数据库的数据需要与操作对应上(类似同步)
- 隔离性:就是数据库允许多个并发事务同时对数据进行读写和修改,通过一些隔离机制保证多个事务并发执行的时候不会出现数据不一致的情况
- 持久性:指的是事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失(落盘保存)
虽然事务执行完之后,数据无法通过回滚的方式复原,但是要恢复数据还是有办法的
比如可以通过提前备份的数据进行恢复,还可以通过执行逆向的SQL语句进行恢复,或者可以查看日志和历史表来修改变更记录
事务的隔离级别分别解决了哪些并发问题
事务的隔离级别有读未提交、读提交、可重复读、串行化这几种
其中读未提交是允许事务读取其他事务尚未提交的数据的,因此其不会解决任何并发问题
读提交只允许事务读取已提交的数据,可以避免脏读问题,但是不能避免幻读和不可重复问题
可重复读保证同一事务多次读取同一数据时,得到的结果是一致的,能够解决脏读和不可重复读问题
幻读、脏读
当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。
不可重复读通常发生于UPDATE操作,而幻读通常发生于INSERT操作。
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象
假设有两个事务,事务1是A要给B转账100元;事务2是B要查询自己的账户余额
事务1开始执行,A的账户少了100元,在事务1完成前,事务2开始执行,B查询了自己的余额
此时如果事务1由于某种原因执行失败,转账未完成,那么此时A的余额会减少,并且B也查询不到自己的余额增加
这就发生了脏读
什么情况下可以解决幻读
在不同的查询方式下有不同的处理幻读的方法
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。(就是通过不断备份和追踪数据来避免数据不一致的情况)
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。(间隙锁会保证在范围查询期间,其他事务无法插入或删除满足查询条件的数据,从而防止了幻读)
数据库事务隔离级别
事务指的是单个逻辑工作单元在执行操作时,要么完全执行完成,要么完全不执行,也就是说不管哪一步出了问题都会直接回滚到初始状态。一个逻辑工作单元要成为事务需要满足原子性、一致性、隔离性和持久性这几个属性。
事务隔离就是同一时间只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一个账户中取钱,在取钱过程结束前,B不能向该账户转账。
事务隔离级别有以下几种:
- 读未提交(指事务还没提交时,它做的变更就能被其他事务看见)【可能发生脏读、不可重复读和幻读现象】
- 读提交(指事务提交后其变更才能被其他事务看见)【可能发生不可重复读和幻读现象】
- 可重复读(指事务执行过程中看到的数据一直跟刚启动事务时看到的一致,InnoDB的默认隔离级别)【可能发生幻读现象】
- 串行化(对记录加上了读写锁,多个事务对某一条记录进行读写操作的时候需要等待)【均不可能发生】
mysql有哪些存储引擎,有哪些日志
常见的存储引擎有:InnoDB、MyISAM和Memory等
InnoDB是MySQL现在采用的默认引擎,支持事务、行级锁和外键约束,适用于对于数据完整性有要求的场景以及高并发场景。
MyISAM是MySQL中的早期引擎,不支持事务和行级锁等特性,在一些只读或者读写比例低的场景还有一些优势。
Memory则是一种将数据存放在内存中的引擎,适合于临时表和缓存数据,速度非常快,但一旦 MySQL 服务重启,数据即会丢失。
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
MySQL中主要的日志类型包括:错误日志、查询日志、慢查询日志和事务日志等
错误日志记录了MySQL在启动运行过程中的所有错误信息;
查询日志则记录了所有对 MySQL 服务器的连接和执行的查询操作,可以用于调试和分析。
慢查询日志记录了超过阈值的查询语句。
其中事务日志是在Innodb存储引擎层生成的日志,包括回滚日志、重做日志等,用来实现来实现事务的持久性和恢复能力
数据库使用的是什么索引,有哪几种索引
MySQL的默认索引是B树索引,创建主键时都会采用该索引。
但是在MySQL5.5之后,InnoDB成为了默认的 MySQL 存储引擎,主要使用的就是B+Tree索引了。
索引本质上是一种为了增加查询速度而对表字段附加的标识,是对数据库表中一列或者多列数据进行排序的一种结构,使用索引可以快速访问数据库表中的特定信息。
从数据结构的角度来看,MySQL常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引。
按照物理存储可分为:聚簇索引和二级索引
按照字段可分为:主键索引、唯一索引、普通索引以及前缀索引
当你执行一个查询时,如果查询条件涉及到了索引列,数据库可以使用索引来快速定位到满足条件的数据行。这个过程被称为 "索引扫描"。
1、什么时候适用索引?
使用WHERE时适用,能提高整个表的查询速度;
使用GROUP BY、ORDER BY时适用;(省去查询时再做一次排序)
2、什么时候不适用索引?
数据量很少的时候不要创建索引;(因为可能用处并不大)
经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
3、什么是回表查询?
回表查询是指在数据库查询中需要从索引返回到实际数据表以检索完整行数据的过程,通常会增加查询的开销
为什么用B+树?B树和B+树的区别?
b+树是一种多路搜索树,主要是为磁盘或者其他直接存取的辅助设备而设计的一种平衡查找树。
在MySQL中,InnoDB存储引擎使用B+树作为索引的数据结构。因为B+树有以下优点:
- B+树的非叶子节点不存放实际的记录数据,仅存放索引,因此可以存放更多的索引,使得B+树比B树更“矮胖”,查询底层节点的磁盘I/O次数会更少。
- B+树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让B+树在插入、删除的效率都更高。
- B+树叶子节点之间用链表连接了起来,有利于范围查询,而B树要实现范围查询,则只能通过树的遍历来完成,这会涉及多个节点的磁盘I/O操作,范围查询效率不如B+树。
上述特点使得B+树更适合在磁盘等外部存储设备上使用,而B树则更适合在内存等快速存储设备上使用。
MVCC
MVCC是一种多版本并发控制机制,是MySQL中InnoDB引擎实现隔离级别的一种具体的方式,用于实现提交读和可重复读两种隔离级别。
MVCC通过保存数据在某个时间点的快照来实现该机制,就是每行后面有两个隐藏的列,分别保存了创建版本号和删除版本号,这些信息被保存在undo日志中,在回滚的时候会用到。
要修改MySQL的隔离级别,可以使用以下语句:
SET TRANSACTION ISOLATION LEVEL <隔离级别>;
不可重复读是指在一个事务内多次读取同一数据,但每次读取的结果都不一致。这是因为在事务执行期间,其他并发事务修改了被读取的数据,导致读取到的数据发生变化。
举个例子,假设有两个事务T1和T2:
- T1开始一个事务,并读取某个表中的数据。
- T2在T1的事务还未结束时,修改了该表中的数据并提交。
- T1再次读取相同的数据,发现与之前读取的结果不一致。
这就是不可重复读的情况,即同一个事务内多次读取同一数据,但每次读取的结果不一致。
不可重复读通常发生于UPDATE操作,而幻读通常发生于INSERT操作。
CPP基础
C++的虚函数
虚函数是在基类中声明的一类函数,使用 virtual
关键字标记,允许子类选择性地重写这些函数。
使用虚函数的原因是为了实现多态这种特性。多态性指的是同一个接口可以有多种不同的实现方式。多态的实现依赖于虚函数表,每个类对象都有一个vtable,它存储了对应虚函数的指针。当调用虚函数时,会根据对象的实际类型查找正确的 vtable 并调用适当的函数。
虚函数是实现多态性的关键机制之一。在C++中,通过将函数声明为虚函数,可以实现动态绑定(Dynamic Binding)或后期绑定(Late Binding),即在运行时确定调用的具体函数实现。
在C++中,多态性是通过虚函数和重载来实现的。
虚函数要注意的地方有哪些?
-
虚函数的实现必须是动态绑定的。为了实现动态绑定,C++编译器需要为每个对象添加一个虚表指针;
-
使用虚函数那么对应的析构函数就也要是虚函数;
-
如果某个函数不需要被重写,那么就不应该将其声明为虚函数;
C++中的宏和inline的含义以及应用场景
宏在C++中是通过预处理器来实现的,它提供了一种在代码中进行简单替换的方式。宏通常用于定义常量或简单的代码替换(DEBUG_PRINT();
打印调试日志),例如定义一些常用的数值或者简单的函数宏。此外,宏还可以用于条件编译,根据不同的条件来包含或者排除特定的代码段。
宏的实现原理是通过预处理器在编译之前对源代码进行文本替换和展开,使得宏定义的内容在实际编译时能够被正确地插入到代码中
而inline是为了实现内联函数的一个关键字,inline用于对函数进行内联展开,减少函数调用带来的开销。内联函数适合用于函数体较小、调用频繁的场景,能够提高程序的执行效率。但需要注意的是,并非所有的函数都适合声明为内联函数,过度使用内联函数也可能造成代码体积增大,影响缓存性能。内联函数仅仅是对编译器发出的一种“建议”,并不是强制性的要求.
内联函数的实现原理是通过编译器在编译阶段对函数调用处进行直接替换,从而减少函数调用的开销。
应用:
对于函数体比较短的函数(比如里面就一个a+b这种的)可以将其声明为内联函数以减少调用开销;
在头文件中定义和声明模板函数也可以使用inline关键字来声明;
C++从预处理到执行的流程大概是什么,分别讲一下
预处理阶段会对源代码进行处理,包括展开宏定义、包含头文件、条件编译等操作,预处理器会生成一个经过预处理的新的源代码文件。
到了编译阶段,编译器首先对源码进行词法和语法分析,生成对应的中间代码或汇编代码。
在汇编阶段,汇编器把汇编代码转为目标文件,其中包含了一些机器指令和用于链接的元信息。
然后在链接阶段,链接器会将目标文件与库文件进行连接,同时解析外部引用,生成可执行文件。链接过程包括符号解析、地址映射等操作。
执行的时候就将文件载入内存便可以由操作系统进行执行。
C++11新特性
lamdba表达式
C++中的Lambda表达式是一种匿名函数,它允许在需要函数的地方定义一个小型函数,而无需显式地命名它。Lambda表达式的语法相对简单,它可以在需要函数对象的地方替代传统的函数或函数指针。
以下是一个简单的Lambda表达式的示例,它定义了一个用于两个整数相加的Lambda函数,并将其应用于两个整数:
#include <iostream>
int main() {
// 定义一个Lambda表达式,接受两个整数并返回它们的和
auto add = [](int a, int b) -> int {
return a + b;
};
// 使用Lambda函数进行加法操作
int result = add(5, 3);
std::cout << "5 + 3 = " << result << std::endl;
return 0;
}
在上述示例中,Lambda表达式 [](int a, int b) -> int { return a + b; }
定义了一个接受两个整数参数的Lambda函数,并返回它们的和。然后,我们将这个Lambda函数赋值给变量 add
,并使用 add(5, 3)
调用它,得到了结果 8
。
Lambda表达式的语法通常包括方括号 []
,用于捕获外部变量(如果需要)、参数列表、返回类型和函数体。在这个示例中,Lambda没有捕获外部变量,有两个整数参数,返回类型是 int
,函数体是计算它们的和。Lambda表达式的特点之一是它可以轻松地在需要函数对象的地方创建小型、即时的匿名函数。
i++和++i
++i(前置递增)首先增加 i
的值,然后返回增加后的值。这意味着 ++i
先增加 i
,然后返回增加后的值,因此你在表达式中使用 ++i
时,得到的是 i
自增后的值。
i++(后置递增)首先返回 i
的当前值,然后再将 i
的值加1。这意味着 i++
先返回 i
的当前值,然后才增加 i
,所以你在表达式中使用 i++
时,得到的是 i
自增前的值。
智能指针
说一下c++中的智能指针,哪个是可以解引用的?哪个是线程安全的?
在C++中,智能指针是一种用于管理动态分配的对象的指针类。有三种主要的智能指针类型:unique_ptr、shared_ptr和weak_ptr。
unique_ptr是独占所有权的智能指针,提供对动态分配对象的独占访问权限,并在其生命周期结束时自动释放所管理的对象。它不支持普通拷贝和赋值操作,因为其拥有指向的对象,可以通过运算符*进行解引用操作。unique_ptr是线程安全的,因为它仅允许一个智能指针拥有对对象的独占访问权限。
shared_ptr是共享所有权的智能指针,可以被多个shared_ptr对象共享,并且会在最后一个引用被销毁时自动释放所管理的对象。shared_ptr使用引用计数来跟踪当前有多少个指针共享对象,可以通过运算符*进行解引用操作。在引用计数的管理上是线程安全的,即多个线程可以同时访问和操作相同的shared_ptr对象。但是,在多线程环境下对同一对象进行修改,则需要额外的同步机制来确保正确性。
weak_ptr是一种弱引用的智能指针,指向shared_ptr所管理的对象,但不会增加引用计数。它被用于避免shared_ptr之间的循环引用。weak_ptr可以通过lock()函数转换为一个shared_ptr,并且只有在该shared_ptr尚未释放时才是有效的。weak_ptr并不提供直接的线程安全性保证,因此在多线程环境下使用时需要特别小心,以避免悬空指针和竞态条件等问题。
unique_ptr和shared_ptr都可以解引用,但unique_ptr不支持普通拷贝和赋值,而shared_ptr采用引用计数机制来管理对象的生命周期。同一个shared_ptr被多个线程"读"是安全的,但被多个线程"写"是不安全的。
shared ptr的底层实现原理是什么?
shared_ptr的底层实现原理是利用计数器来实现多个智能指针共享同一个对象的内存资源。当一个shared_ptr对象被创建时,会在堆上分配两个内存块,一个用于存储所指向对象的地址,另一个用于存储引用计数。
每当有新的 shared_ptr 对象指向同一个对象时,其对应的引用计数加一;当一个 shared_ptr 对象销毁或者重新分配时,其对应的引用计数就会减一。只有当引用计数变为 0 时,表示当前对象没有任何 shared_ptr 对象指向它,此时才会真正地释放这块内存空间。
cpp有哪些锁?对应的API是什么?
1、mutex
C++标准库提供的基本互斥锁。相关API包括std::mutex
、std::lock_guard
和std::unique_lock
。
2、atomic_flag
用于基于原子操作的简单自旋锁,不同于前面的互斥锁。相关API包括std::atomic_flag
的test_and_set
和clear
成员函数。
平时怎么调试cpp代码的?
如果是单纯的调试代码,在Windows下我会用vs带的debug环境和断点调试工具去做,如果是Linux下一般会用gdb来做断点调试或者是使用打印日志的方式来调试代码。
如果是读源码的话,我会先把项目跑起来,能编译运行之后,然后从入口函数开始配合日志打印去阅读每个功能模块,梳理出数据流。
怎么解决菱形继承,什么作用,有其他方式吗?
菱形继承(Diamond Inheritance)是指一个派生类同时继承了两个直接或间接基类,而这两个基类又共同继承了一个基类。
class A {
public:
int a;
};
class B : public A {
public:
int b;
};
class C : public A {
public:
int c;
};
class D : public B, public C {
public:
int d;
};
菱形继承会导致以下问题:
- 内存浪费:由于派生类包含了多个基类的子对象,每个子对象都有一个副本,会导致内存浪费。
- 命名冲突:如果两个不同的基类具有相同名称的成员函数或成员变量,则在派生类中使用时会发生命名冲突。
- 访问不明确:由于派生类同时继承了多个基类,因此可能会存在多个成员函数重名的情况,这会让代码的访问路径变得不明确,给调试带来困难。
解决办法:使用虚继承(Virtual Inheritance)来改变默认的继承方式。虚继承的作用是让两个或多个派生类共享一个基类子对象,避免了重复继承所带来的问题
例如:class B : virtual public A {...}
怎么对字符串hash?
可以使用直接哈希法,取字符串中每个字符的ASCII码值,相加之后的结果作为哈希值。
除此以外还可以使用秦九韶算法(基于多项式中x的不同取值计算哈希)和Rabin-Karp算法(基于滑动窗口计算哈希)
说两个常见的排序算法
快速排序和堆排序都是常用的排序算法。
快速排序是一种分治、原地、不稳定的排序算法。它通过选择一个基准值,将数组分为两个子数组,使得一个子数组中的所有元素都小于基准值,另一个子数组中的所有元素都大于基准值。接着对这两个子数组递归地进行排序。快速排序的时间复杂度为O(nlogn),最坏情况下为O(n^2),空间复杂度为O(logn)。
堆排序是一种选择排序,利用堆结构实现。它将待排序数组构建成一个二叉堆,每次取出堆顶元素,将其与堆底元素交换并进行下沉操作,直到整个数组有序。堆排序的时间复杂度为O(nlogn),空间复杂度为O(1)。在堆排序过程中,首先构建一个最大堆或最小堆。在调整堆的过程中,交换元素的位置可能会导致相同元素的相对顺序发生改变,从而使得堆排序不稳定。
综上所述,快速排序和堆排序都具有O(nlogn)的时间复杂度,快排和堆排都是不稳定的,因为在排序过程中相同元素的先对顺序会发生改变。
顺便说一下,冒泡排序/插入排序是稳定的
红黑树和AVL
红黑树的性质
1、每个节点非黑即红,但根节点一定是黑的
2、每个叶子节点都是黑的
3、如果一个节点是红的,那么它的子节点一定是黑的
4、对于任意节点而言,其到叶子节点的路径上所包含的黑节点数目相同
为什么红黑树旋转次数少?
因为按照红黑树的着色规则,红黑树中每一条路径都会比其他路径长两倍
这样如果某一边子树的深度大于另一边,但是还没超过两倍,红黑树可以不用旋转调整,这样就减少了旋转次数
而自平衡二叉查找树因为是严格平衡的,所以当出现某一边子树的深度大于另一边的情况会立刻进行旋转来调整树的结构,这样会耗费更多的资源
平衡二叉树AVL
红黑树是在AVL树的基础上提出的,AVL树是最早被提出的一种自平衡二叉搜索树
AVL树的左右子树都是平衡二叉树,左右子树的高度差的绝对值不能超过1
红黑树相较于AVL树的优点是什么?
因为AVL树是高度平衡的,所以频繁的插入删除会伴随着频繁的rebalance,导致效率下降。
而红黑树不是高度平衡的,算是一种折中方案,其在插入时最多旋转2次,删除时最多旋转3次。
所以红黑树在查找、删除和插入时的性能都是O(logn)的。
讲几个常用的STL容器
vector是一个动态数组,适用于经常随机访问且不经常在中间部分做插入/删除操作的场景,其底层实现是数组;
两倍扩容可以提高插入操作的效率并且均摊时间复杂度,减少扩容次数,但是也可能导致一些内存浪费
list是一个双向链表,适用于经常插入删除大量数据的场景。
map是以键值对的形式存储数据的有序容器,底层使用红黑树实现,支持下标操作,适用于有序键值对的不重复映射;
而set是一种集合容器,其中所有元素都是唯一的且按照一定的顺序排列,底层也是使用红黑树实现,不支持下标操作,适用于需要快速查找元素并保证元素唯一性的场景。
List、Map、Set
Vector
简介
动态数组,是一个连续存储的容器,在堆上分配空间
vector的底层实现是数组array,其访问复杂度是O(1)。
在添加新元素时,如果vector中还有剩余空间,那么直接添加到指定位置,然后调整迭代器即可。如果已经没有空间了,那就要进行扩容,要重新开辟一个大小为原来两倍的内存空间,然后将原空间的元素复制到新空间中完成初始化,再向新空间中增加元素,最后析构并释放原空间。过程中原有的迭代器会失效。
为什么是两倍?
翻倍扩容可以提高插入操作的效率并且均摊时间复杂度,减少扩容次数,但是也可能导致一些内存浪费
适用场景
经常随机访问,且不经常在中间部分做插入/删除操作(中间做插入/删除需要内存拷贝)
List
简介
动态链表,也是在堆上分配空间,每插入一个元素都会分配一次空间,每删除一个元素都会释放一次空间
list本身是一个双向链表,底层使用了动态数组的相关特性
list的随机访问性能很差,只能快速访问头尾节点。但是它的插入和删除很快,一般都是常数开销
适用场景
因此list适用于经常插入删除大量数据的场景(其实就和链表的使用场景一样了)
Vector与List的区别
第一,vector的底层实现是数组,而list本质上是一个双向链表;
第二,vector支持随机访问,而list不支持;
第三,vector中的内存存储是顺序的,而list不是,这就导致了在vector中间处进行插入和删除操作会导致内存拷贝,而list不存在这个问题;
第四,vector是一次性分配好内存的,只在扩容时再次进行内存分配,而list每次插入新节点时都会申请内存
Map
map即映射(或者说字典),map中的所有元素都是pair,拥有对应的键和值。
pair的第一元素被视为键的值,第二元素被视为实值。所有的元素都会根据元素对应的键进行排序,键不允许重复。
map的底层实现是红黑树
适用场景
有序键值对的不重复映射
Multimap
与map类似,但是允许键值重复
说一下map,map和unordered_map有什么区别,哪个占用的空间更大?
map和unordered_map都是C++ STL中的关联容器,用于存储键值对,其中键是唯一的。
map采用红黑树实现,可以保证所有元素按照键的大小顺序排序。它的搜索、插入、删除操作的时间复杂度都是O(log n),适用于需要有序存储和查找的场景。但是,因为要维护元素的有序性,所以在插入、删除元素时需要进行平衡调整,会带来一定的性能开销。
unordered_map采用哈希表实现,可以在常数时间内完成搜索、插入、删除等操作。它不保证元素的顺序,适用于对元素顺序没有特别要求的场景。在元素数量较大时,哈希表的性能相比红黑树更优。但是,由于哈希表需要维护哈希函数和哈希冲突等问题,所以其实现较为复杂,占用的空间也较大。
因此,如果需要保证元素的有序性并且对空间占用不敏感,可以选择map;如果需要高效的搜索、插入、删除操作,并且对元素顺序没有特别要求,可以选择unordered_map。
请你来说一下 map 和 set 有什么区别,分别又是怎么实现的?
map 和 set 都是 C++的关联容器,其底层实现都是红黑树 (RB-Tree) 。由于 map 和 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和 set 的操作行为,都只是转调 RB-tree 的操作行为。
map中的元素是键值对;
set中的迭代器是const的,不允许修改元素的值;
map支持下标操作,而set不支持下标操作;
为什么map底层使用红黑树实现?
红黑树是一种二叉查找树,每个节点中会多一位用于存储节点颜色,有红和黑两种颜色。红黑树对于节点的着色方式有限制,所以红黑树中每一一条路径比其他路径长出两倍。
因此,红黑树是一种弱平衡的二叉树,相对于自平衡二叉查找树(AVL树)来说,红黑树在调整数据时旋转更少,所以对于搜索、插入和删除操作较多的情况下,通常使用红黑树
"旋转"是一种操作,用于调整树的结构。它通常用于平衡二叉搜索树(AVL 树、红黑树等)或其他类型的二叉树,以保持树的平衡性质或改变树的形状,从而提高树的性能。
- 左旋(Left Rotation): 左旋操作用于解决右子树比左子树深度过大的情况,从而保持平衡。在左旋中,以某个节点为支点,将其右子节点提升到原节点的位置,然后将原节点作为新节点的左子节点。
- 右旋(Right Rotation): 右旋操作用于解决左子树比右子树深度过大的情况,以保持平衡。在右旋中,以某个节点为支点,将其左子节点提升到原节点的位置,然后将原节点作为新节点的右子节点。
红黑树的性质
1、每个节点非黑即红,但根节点一定是黑的
2、每个叶子节点都是黑的
3、如果一个节点是红的,那么它的子节点一定是黑的
4、对于任意节点而言,其到叶子节点的路径上所包含的黑节点数目相同
【防盗链提醒:爬虫是吧?原贴在:https://www.cnblogs.com/DAYceng】
为什么红黑树旋转次数少?
因为按照红黑树的着色规则,红黑树中每一条路径都会比其他路径长两倍
这样如果某一边子树的深度大于另一边,但是还没超过两倍,红黑树可以不用旋转调整,这样就减少了旋转次数
而自平衡二叉查找树因为是严格平衡的,所以当出现某一边子树的深度大于另一边的情况会立刻进行旋转来调整树的结构,这样会耗费更多的资源
平衡二叉树AVL
红黑树是在AVL树的基础上提出的,AVL树是最早被提出的一种自平衡二叉搜索树
AVL树的左右子树都是平衡二叉树,左右子树的高度差的绝对值不能超过1
红黑树相较于AVL树的优点是什么?
因为AVL树是高度平衡的,所以频繁的插入删除会伴随着频繁的rebalance,导致效率下降。
而红黑树不是高度平衡的,算是一种折中方案,其在插入时最多旋转2次,删除时最多旋转3次。
所以红黑树在查找、删除和插入时的性能都是O(logn)的。
Python基础
对于Python中的内存管理有了解吗?(答了一些cpython的东西)
在python中,内存管理涉及到一个包含所有 Python 对象和数据结构的私有堆(heap),由 Python 内存管理器(Python memory manager)对这个堆进行管理。
这个堆是一个抽象的内存池,用于存储程序中的所有对象。
在底层有个原始内存分配器,与操作系统的内存管理器打交道,确保Python有足够的地方存储数据。此外,还有对象特定的分配器,根据不同对象类型的需求来管理内存。例如,整数和字符串的内存管理方式不同,因此Python内存管理器确保它们在同一堆中,但采用不同策略。这保证了内存高效使用。
Python堆内存的管理完全由解释器掌握,用户无法控制。Python对象和内部缓冲区的堆内存分配是由Python内存管理器按需处理,使用Python/C API函数。
Python解释器在其底层实际上调用了C语言的一些函数来进行内存管理。这包括与操作系统的C库函数交互,以分配和释放内存。但是,Python的内存管理器在这些底层操作之上实现了更高级的内存分配策略,以满足Python对象的需要。
为了避免内存问题,扩展作者绝不应尝试使用C库函数(如
malloc()
、calloc()
、realloc()
和free()
)来操纵Python对象。这会导致C分配器和Python内存管理器的混用,可能引发严重问题,因为它们使用不同的算法并在不同的堆上操作。这是必须谨慎避免的。
大多数情况下,推荐使用Python堆来分配内存,因为它受Python内存管理器控制。
比如,当解释器要扩展新的对象类型时,必须使用Python堆。
另一个使用Python堆的原因是需要告知Python内存管理器关于扩展模块的内存需求。即使请求的内存只用于特定的内部目的,将所有请求都交给Python内存管理器让解释器更准确地了解内存占用情况。所以在特定情况下,Python内存管理器可能会触发或不触发合适的操作,比如垃圾回收、内存压缩或其他预防性操作。
要注意,如果使用C库分配器为I/O缓冲区分配内存,那么这块内存将完全不受Python内存管理器的控制。
Python中的线程使用和cpp中有什么不同?在性能上哪个更好?为什么?
在python和c++中,线程都是用来实现多任务并行执行的工具,但是在他们在很多地方还是有区别的
语法上
python可以使用threading模块来创建和管理线程(或者使用threadpoolexecute),而c++中可以使用<thread>
标准库来创建和管理线程(当然也可以用更底层的POSIX线程库,比如pthread)
全局解释器锁GIL/性能
python中有一个全局解释器锁,它规定在任何给定的时间内只能有一个线程执行python字节码。这意味着如果一些CPU密集型任务使用python的多线程机制进行编写,实际上这是不能充分利用多核处理器的。
而c++没有类似的限制,因此使用c++编写的多线程任务可以在多核处理器上实现更好的并行性能
线程管理
c++中线程的创建、销毁和同步需要手动管理,而python中提供了更易用的管理方式,但会产生一些额外的性能开销
Docker相关
什么Docker?
答:Docker是一个容器化平台,它以容器的形式将您的应用程序及其所有依赖项打包在一起,以确保应用程序在任何环境中无缝运行
Docker与虚拟机的区别
Docker容器是一个独立的运行环境,类似于虚拟机,但比虚拟机更加轻量化和灵活,因为它不需要模拟硬件,而是共享主机操作系统的内核。
Docker容器的原理基于Linux操作系统提供的命名空间和控制组等技术,其中命名空间可以隔离进程、网络、文件系统等资源,而控制组可以限制资源使用。
Docker容器有几种状态?
有四种状态: 运行、已暂停、重新启动、已退出
Dockerfile中最常见的指令是什么?
FROM:指定基础镜像;(镜像名:版本号或者说标签)
WORKDIR:指定一个工作路径,后续的指令都会在这个路径下运行
COPY<本地路径><镜像中的目标路径>:将所有程序拷贝到docker镜像中
例如:COPY. . “.”代表根目录下的所有文件,第二个“.”这代表当前工作路径,也就是WORKDIR指定的
LABEL: 功能是为镜像指定标签;
RUN:创建镜像时要运行的命令;
CMD:容器启动时要运行的命令。
Dockerfile中的命令COPY和ADD命令有什么区别?
一般而言,虽然ADD与COPY在功能上类似,但是首选COPY。
COPY比ADD更易懂。COPY仅支持将本地文件复制到容器中,而ADD具有一些功能(如仅限本地的tar提取和远程URL支持),这些功能并不是很常用。ADD更多的还是用于将本地tar文件自动提取到镜像中。
什么是Docker镜像
docker镜像用于创建容器,换句话说docker镜像是docker容器的源码。使用build命令创建镜像,
常见的docker命令
- docker run:运行容器。例如:
docker run -d -p 8080:80 nginx
,将在后台运行一个Nginx容器并将主机的8080端口映射到容器的80端口。 - docker build:构建Docker镜像。例如:
docker build -t my-image
,从当前目录中的Dockerfile构建一个名为my-image
的镜像。 - docker pull:从Docker镜像仓库拉取镜像。例如:
docker pull ubuntu
,从Docker Hub拉取Ubuntu镜像。 - docker push:将镜像推送到Docker镜像仓库。例如:
docker push my-image
,将名为my-image
的镜像推送到Docker镜像仓库。 - docker ps:列出正在运行的容器。例如:
docker ps
,显示所有正在运行的容器的列表。 - docker images:列出本地的Docker镜像。例如:
docker images
,显示所有本地镜像的列表。 - docker stop:停止运行中的容器。例如:
docker stop container_id
,停止ID为container_id
的容器。 - docker start:启动已停止的容器。例如:
docker start container_id
,启动ID为container_id
的容器。 - docker restart:重启容器。例如:
docker restart container_id
,重启ID为container_id
的容器。 - docker exec:在运行中的容器内部执行命令。例如:
docker exec -it container_id bash
,在容器内部启动一个交互式的bash shell。 - docker logs:查看容器的日志。例如:
docker logs container_id
,查看ID为container_id
的容器的日志。 - docker rm:删除容器。例如:
docker rm container_id
,删除ID为container_id
的容器。 - docker rmi:删除镜像。例如:
docker rmi image_id
,删除ID为image_id
的镜像。 - docker network:管理Docker网络。例如:
docker network create my-network
,创建名为my-network
的Docker网络。