后端研发面经总结
计算机网络
[讲讲HTTP与HTTPS的区别;TLS是哪一层;HTTPS的连接流程是怎样;HTTPS的加密方式(对称加密和非对称加密都有)]
应用层 传输层 网络层 数据链路层 物理层
http超文本传输协议,信息是明文传输。80端口。http的连接很简单,是无状态的。
https安全超文本传输协议, https是具有安全性的ssl/tls加密传输协议。443端口。HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。位于应用层。
对称加密:
对称加密是指加密,解密使用同一把秘钥。(秘钥容易泄露)
非对称加密:客户端和服务端均拥有一个公有密匙和一个私有密匙。公有密匙可以对外暴露,而私有密匙只有自己可见。
使用公有密匙加密的消息,只有对应的私有密匙才能解开。反过来,使用私有密匙加密的消息,只有公有密匙才能解开。这样客户端在发送消息前,先用服务器的公匙对消息进行加密,服务器收到后再用自己的私匙进行解密。
优点
- 非对称加密采用公有密匙和私有密匙的方式,解决了http中消息保密性问题,而且使得私有密匙泄露的风险降低。
- 因为公匙加密的消息只有对应的私匙才能解开,所以较大程度上保证了消息的来源性以及消息的准确性和完整性。
非对称加密的缺点: - 非对称加密时需要使用到接收方的公匙对消息进行加密,但是公匙不是保密的,任何人都可以拿到,中间人也可以。那么中间人可以做两件事,第一件是中间人可以在客户端与服务器交换公匙的时候,将客户端的公匙替换成自己的。这样服务器拿到的公匙将不是客户端的,而是中间人的。服务器也无法判断公匙来源的正确性。第二件是中间人可以不替换公匙,但是他可以截获客户端发来的消息,然后篡改,然后用服务器的公匙加密再发往服务器,服务器将收到错误的消息。
- 非对称加密的性能相对对称加密来说会慢上几倍甚至几百倍,比较消耗系统资源。
SSL:(Secure Socket Layer,安全套接字层),位于可靠的面向连接的网络层协议和应用层协议之间的一种协议层。SSL通过互相认证、使用数字签名确保完整性、使用加密确保私密性,以实现客户端和服务器之间的安全通讯。该协议由两层组成:SSL记录协议和SSL握手协议。
TLS:(Transport Layer Security,传输层安全协议),用于两个应用程序之间提供保密性和数据完整性。该协议由两层组成:TLS记录协议和TLS握手协议。
位于传输层的协议。
[HTTPS加密流程说一下,证书是怎么工作的]
HTTPS 在内容传输的加密上使用的是对称加密,非对称加密只作用在证书验证阶段。
- 证书验证阶段:
1)浏览器发起 HTTPS 请求;
2)服务端返回 HTTPS 证书;
3)客户端验证证书是否合法,如果不合法则提示告警。 - 数据传输阶段:
1)当证书验证合法后,在本地生成随机数;
2)通过公钥加密随机数,并把加密后的随机数传输到服务端;
3)服务端通过私钥对随机数进行解密;
4)服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输。
常见非对称加密算法:
RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的;
DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准);
ECC(Elliptic Curves Cryptography):椭圆曲线密码编码学。
[http状态码有哪些。]
常见:
200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
302 - 临时重定向,资源已临时分配新URL
401 - 需要通过HTTP认证,或认证失败
403 - 请求资源被拒绝
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误
503:服务器由于临时的服务器过载或者是维护,无法解决当前的请求
[http是有状态还是无状态?是有连接还是无连接?]
无状态、无连接
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送数据过来,但是,发送完,不会记录任何信息。
Cookie 是客户端的存储空间,由浏览器来维持。具体来说 cookie 机制采用的是在客户端保持状态的方案。
Cookie,有时也用其复数形式Cookies,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)。
Session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。
客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 Session。客户端浏览器再次访问时,只需要从该 Session 中查找该客户的状态就可以了。
服务器让浏览器cookie保存sessionid啥的,用于识别,如果cookie被禁用那就是url重写,在url里带上sessionid。
cookie跨域问题(cookie只有同源的才能使用,为了防止这种情况需要使用cookie跨域)
nginx反向代理,解决跨域问题
jsonp 利用src的跨域属性
[TCP连接全过程;三次握手过程;二次握手行不行;TCP四次挥手过程;TCP的重传;](运输层协议)
tcp连接三次握手
- 第一次客户端A向服务端B发送连接请求序号标志SYN = 1,序号seq = x,请求b的连接
- 服务端向A发送同步syn,序号seq=y,确认标志ACK,确认序号ack=x+1
- 客户端发送ACK确认号,发送自己序号seq=x+1,发送对方确认号ack=y+1
tcp四次挥手
第一次挥手:客户端发出释放FIN=1,自己序列号seq=u,进入FIN-WAIT-1状态
第二次挥手:服务器收到客户端的后,发出ACK=1确认标志和客户端的确认号ack=u+1,自己的序列号seq=v,进入CLOSE-WAIT状态
第三次挥手:客户端收到服务器确认结果后,进入FIN-WAIT-2状态。此时服务器发送释放FIN=1信号,确认标志ACK=1,确认序号ack=u+1,自己序号seq=w,服务器进入LAST-ACK(最后确认态)
第四次挥手:客户端收到回复后,发送确认ACK=1,ack=w+1,自己的seq=u+1,客户端进入TIME-WAIT(时间等待)。客户端经过2个最长报文段寿命后,客户端CLOSE;服务器收到确认后,立刻进入CLOSE状态。
一个报文段的最大长度是在二次握手时协商确定的
同步SYN
终止FIN
seq序号:本报文段所发送的数据的第一个字节的序号
ack确认号 期望收到对方下一个报文段数据的第一个字节的序号,下一个报文段序号的值
ACK确认 ACK=1时确认号才生效, ACK=0确认号无效
[TCP和UDP的区别]
tcp首部报文段少20的字节(固定部分)
udp首部报文段固定8字节(源端口,目的端口,长度,检验和)
TCP与UDP区别总结:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议,直播,游戏等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
[接收方,窗口缓存满了会怎么样,怎么知道又有空间了,通知发送的包再丢了怎么办。]
把接收端端口rwnd置为0,这样
[使用udp实现tcp]
1、添加seq/ack机制,确保数据发送到对端(ack seq可以确保完整性和有序性)
2、添加发送和接收缓冲区,主要是用户超时重传。(有了缓冲区就能把发过来的数据缓存起来,而不是直接接收,某些数据丢失了可以等他重传)
3、添加超时重传机制。(丢失了重新传数据)
[TCP为什么最后要等2MSL]
等待重传时间,2个最长报文段寿命,如果最后的确认标志,序列号,确认号没有发过去就得重新发送,重传。
同时可以可以让这个连接里的报文段全部死亡,免得建立新连接时还有这个连接里的报文段干扰。
[浏览器输入URL到渲染页面的全过程;][浏览器输入URL后的流程]
先查浏览器的缓存,如果有的话直接显示。
- 浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。
DNS查询分为两种方式,一种是递归查询,一种是迭代查询。主机向本地域名服务器查询是递归查询, 如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。 - 得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议,http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层处理,所以还会用到TCP协议。如果采用https还会使用https协议先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。
- TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。
dns协议 以及具体工作过程,tcp传输流式文件,网络层的arp协议 ,路由转发 ,浏览器解析
[arp协议工作详解]
在网络访问层中,同一局域网中的一台主机要和另一台主机进行通信,需要通过 MAC 地址进行定位,然后才能进行数据包的发送。而在网络层和传输层中,计算机之间是通过 IP 地址定位目标主机,对应的数据报文只包含目标主机的 IP 地址,而没有 MAC 地址。因此,在发送之前需要根据 IP 地址获取 MAC 地址,然后才能将数据包发送到正确的目标主机,而这个获取过程是通过 ARP 协议完成的。
- 在局域网上面是有arp高速缓存的,直接缓存里找ip和mac地址对应关系
- 广域网先找在本网络上一个路由器ip地址对应的mac地址,转发数据给他, 在通过他去一个一个的转发不断地找下去就行了。
[tcp连接复用]
TCP连接复用技术通过将前端多个客户的HTTP请求复用到后端与服务器建立的一个TCP连接上。这种技术能够大大减小服务器的性能负载,减少与服务器之间新建TCP连接所带来的延时,并最大限度的降低客户端对后端服务器的并发连接数请求,减少服务器的资源占用。
一般情况下,客户端在发送HTTP请求之前需要先与服务器进行TCP三次握手,建立TCP连接,然后发送HTTP请求。服务器收到HTTP请求后进行处理,并将处理的结果发送回客户端,然后客户端和服务器互相发送FIN并在收到FIN的ACK确认后关闭连接。在这种方式下,一个简单的HTTP请求需要十几个TCP数据包才能处理完成。
采用TCP连接复用技术后,客户端(如:ClientA)与负载均衡设备之间进行三次握手并发送HTTP请求。负载均衡设备收到请求后,会检测服务器是否存在空闲的长连接,如果不存在,服务器将建立一个新连接。当HTTP请求响应完成后,客户端则与负载均衡设备协商关闭连接,而负载均衡则保持与服务器之间的这个连接。当有其它客户端(如:ClientB)需要发送HTTP请求时,负载均衡设备会直接向与服务器之间保持的这个空闲连接发送HTTP请求,避免了由于新建TCP连接造成的延时和服务器资源耗费。
1、客户端与负载均衡建立TCP连接后,发送HTTP请求(如Get请求),客户端会将自身浏览器所支持的功能和配置情况发送给负载均衡,如:是否支持压缩、支持的压缩算法、是否支持Keep-alive(连接保持)、连接保持的时间等;
2、负载均衡在收到HTTP请求后,会将其中的有关压缩的标记删除,然后将请求转发给服务器进行处理;
3、服务器将响应的内容转发给负载均衡;
4、负载均衡收到响应的内容后,依照与客户端之间协商的压缩算法对响应的内容进行压缩,然后将压缩后的内容发送回客户端;
5、客户端收到响应的内容后,由浏览器对网页内容进行解压缩并进行浏览。
[TCP拥塞控制方法;什么是TCP连接复用;TCP滑动窗口,发送窗口,接收窗口;]
接收窗口(rwnd) 接收方端口。来自接收端的流量控制。tcp报文窗口字段即rwnd接收窗口。
拥塞窗口(cwnd) 发送端根据自己网络值设置的窗口值,发送端流量控制。
发送窗口的上限=min[rwnd,cwnd]
拥塞窗口(cwnd,congestion window),其大小取决于网络的拥塞程度,并且动态地在变化。
拥塞控制的具体过程如下:
(1)TCP连接初始化,将拥塞窗口设置为1(1个MSS 一个最大报文段的大小)
(2)执行慢开始算法,cwnd按指数规律增长,直到cwnd=ssthresh(慢开始门限)时,开始执行拥塞避免算法,cwnd按线性规律增长
(3)当网络发生拥塞,把ssthresh值更新为拥塞前ssthresh值的一半,cwnd重新设置为1,按照步骤(2)执行
TCP滑动窗口
TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口通过自己的资源情况随时动态地调整发送窗口的上限值,接收端控制发送端。tcp报文段窗口字段就告诉对方发送窗口的大小。通过序号可以看发送了多少字节的数据。窗口的大小是对方发送窗口的上限,单位为字节。
滑动窗口的机制
发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保对端会对这些数据重传。遵循快速重传、快速恢复等规则。
快速重传(Fast retransmit)要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到3个重复确认(加上正确那个一共四个)就应当立即重传对方尚未收到的报文段,丢失的那一个应该是下一个,而不必继续等待设置的重传计数器时间到期。
快速恢复(Fast Recovery)。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。
(1)当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半
(2)由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
[TCP具有超时重传策略?如果一直超时怎么办?如何解决?]
tcp每发送一个报文段,就对这个报文段设置一次计时器,计时器时间到了还没收到确认,就重传报文段。
收到确认时间减去发送时间就是报文段的往返时延,加权平均就是平均往返时延(RTT),报文段每重传一次就将重传时间增大一倍,当没有发生报文段重传时才更新rtt和重传时间。
[如果让你来实现客户端和服务端文件发送和接收的进度条,你会怎么实现;;面试官接着问,你是如何判断已经发送的字节数的呢]
通过已发送的字节数比上总字节数来实现进度条, 通过tcp报文中的序号字段(可以知道收到了多少字节的数据)
[TCP的TIME_WAIT出现在哪一端?作用是什么?]
左端,
1、为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。
2、他还可以防止已失效的报文段。客户端在发送最后一个ACK之后,再经过经过2MSL,就可以使本链接持续时间内所产生的所有报文段都从网络中消失。免得下次服务新的连接时还有旧得报文段。
[TCP有哪些机制保证可靠传输。]
seq序号和ack确认号 保证有序完整、
重传机制
拥塞窗口
接收窗口(窗口)
接收缓冲区
[http1.0和http1.1] tcp选项字段的mss就是tcp报文段每个报文的最大数据长度
keep-alive能够保存页面/组件的状态,它还可以避免组件反复创建和渲染,有效提升系统性能。总的来说,keep-alive用于保存组件的渲染状态。
1 HTTP1.1中新增的请求方法:PUT、DELETE、OPTIONS、CONNECT、TRACE
2 HTTP1.1对HTTP1.0性能改善
1.1 长连接(Persistent Connection)
HTTP1.1支持长连接和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启长连接keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。HTTP1.0需要使用keep-alive参数来告知服务器端要建立一个长连接。
1.2 节约带宽
HTTP1.0中存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能。HTTP1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,客户端接收到100才开始把请求body发送到服务器;如果返回401,客户端就可以不用发送请求body了节约了带宽。
1.3 HOST域
在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname),HTTP1.0没有host域。随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都支持host域,且请求消息中如果没有host域会报告一个错误(400 Bad Request)。
1.4 缓存处理
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
1.5 错误通知的管理
在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除
1.6 内容协商
为了满足互联网使用不同母语和字符集的用户,一些网络资源有不同的语言版本(如中文版、英文版)。HTTP/1.0定义了内容协商(contentnegotiation)的概念,也就是说客户端可以告诉服务器自己可以接收以何种语言(或字符集)表示的资源。例如如果服务器不能明确客户端需要何种类型的资源,会返回300(Multiple Choices),并包含一个列表,用来声明该资源的不同可用版本,然后客户端在请求消息中包含Accept-Language和Accept-Charset头域指定需要的版本。
1.7 消息传递
HTTP消息中可以包含任意长度的实体,通常它们使用Content-Length来给出消息结束标志。但是,对于很多动态产生的响应,只能通过缓冲完整的消息来判断消息的大小,但这样做会加大延迟。如果不使用长连接,还可以通过连接关闭的信号来判定一个消息的结束。
HTTP/1.1中引入了Chunkedtransfer-coding来解决上面这个问题,发送方将消息分割成若干个任意大小的数据块,每个数据块在发送时都会附上块的长度,最后用一个零长度的块作为消息结束的标志。这种方法允许发送方只缓冲消息的一个片段,避免缓冲整个消息带来的过载。
[POST与GET的区别]
1、概括
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)
2、区别
1、get参数通过url传递,post放在request body中。
2、get请求在url中传递的参数是有长度限制的,而post没有。
3、get比post更不安全,因为参数直接暴露在url中,所以不能用来传递敏感信息。
4、get请求只能进行url编码,而post支持多种编码方式。
5、get请求会浏览器主动cache,而post支持多种编码方式。
6、get请求参数会被完整保留在浏览历史记录里,而post中的参数不会被保留。
7、GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
8、GET产生一个TCP数据包;POST产生两个TCP数据包。
[tcp黏包,tcp黏包怎么解决]
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
(1)发送方原因
TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:
只有上一个分组得到确认,才会发送下一个分组,收集多个小分组,在一个确认到来时一起发送
Nagle算法造成了发送方可能会出现粘包问题
(2)接收方原因
TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
如果发送方发送的多组数据本来就是同一块数据的不同部分,比如说一个文件被分成多个部分发送,这时当然不需要处理粘包现象
如果多个分组毫不相干,甚至是并列关系,那么这个时候就一定要处理粘包现象了
(1)发送方
对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。
(2)接收方
接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。
(2)应用层
应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。
解决办法:循环处理,应用程序从接收缓存中读取分组时,读完一条数据,就应该循环读取下一条数据,直到所有数据都被处理完成,但是如何判断每条数据的长度呢?
- 格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。
- 发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。
- 设置消息开始标识和消息长度
数据库
[手写了两个SQL的查询。用到了order by, limit, having, count, avg, desc这些关键字;面试官问,SQL查询语句中关键字的执行顺序是什么]
- from 2. where 3. group by 4. select 5. having 6. order by 7. limit
常用聚合函数:
① count 计数 ②sum 求和 ③ max 最大值 ④ min 最小值 ⑤ avg 平均值 。
count()聚合函数括号中可以存放的值。
① count(字段): 纪录null值,即表示满足条件的数据行里参数字段不为NULL的行
② count(1或):不记录null值
③ distinct 字段:去重
数据库执行效率(由高到低): > 1 > 主键 > 普通字段
having作用于组,筛选分组之后的纪录,having条件中可以包含聚合函数;而where作用于表,筛选聚合前的纪录,where条件中不能包含聚合函数。
limt a,b。 从a行开始取值,要取出的行数为b行。需要注意的是,数据表的行数下标从0开始。
[MYSQL的事务隔离级别;MYSQL ACID;MYSQL索引,主键索引,普通索引,唯一索引,回表;普通索引与唯一索引的区别;MYSQL最左匹配原则;]
事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted) 是 是 是
读已提交(read-committed) 否 是 是
可重复读(repeatable-read) 否 否 是 (默认)
序列化(serializable) 否 否 否
mysql事务基本要素(acid)
a: 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
c: 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
i: 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
d: 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
[什么时候需要使用数据库事务]
当数据库需要处理操作量大、复杂度高的数据的时候需要用到事务。用事务是为了保证数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。一个数据库事务通常包含了一个序列对数据库的读/写操作。它的存在包含有以下两个目的:
1、为数据库操作序列提供了一个从失败中恢复到正常状态的方法(原子性)
2. 同时提供了数据库即使在异常状态下仍能保持一致性的方法。(一致性)
2、当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。(隔离性)
4.保证写的数据不能回滚,不会丢失(持久性)
[给你一个场景:有很多个人,数据库里存了他们的姓名,性别,年龄,问你用什么作为索引查询更快;]
姓名,因为年龄,性别本来种类就很少,根本没必要使用索引,查询起来也较快。
[什么时候需要使用数据库索引]
1.在我们建立表的时候,其实默认已经将主键上建立唯一索引了。
2.当某个字段频繁的作为查询参数,查询条件的字段也建议使用,比如号码,唯一,且查询频率高
3.当我使用join进行一个链表查询,就适合使用,这样,外键也是一种查询条件频率高。
4.排序的字段,当我们对一些数据量较大的表进行一个查询且排序,例如日志表,都会使用到创建时间作为排序字段,并且来说,这些表的数据量都不小,这个时候,我们建立索引会有明显的效率提升。
5.还有我们做统计数据的时候,通常会对该字段进行一种数据的汇总统计,在分组查询的时候也是一样的意思,这个时候,也建议建立索引。
不适用
- 表记录较少,你一共就10条数据,我还建立一个索引?可能索引的数据存储都比实际数据都多。
- 还有一种就是上面已经说到的,高频的增删改表。比如流水表,秒级的增加数据。这种表结构,我们通常不建议。
- 查询条件几乎用不到的字段。这个自己理解。
- 还有就是那种,你选择他作为查询条件,但是几乎没有条件过滤的感觉的字段。例如,性别,你说你查询男和女有多大的过滤??
[DNS的过程,DNS劫持是什么;]
递归方式,迭代方式
- 先在本地域名服务器进行递归查询
- 本地域名服务器向根域名服务器发送请求
- 根服务器告诉本地域名服务器下一次应该查询的顶级服务器
- 查询顶级服务器
- 顶级服务器告诉本地服务器下一个要查询的权限域名服务器ip地址
- 本地域名服务器去查询权限域名服务器
- 权限域名服务器告诉本地域名服务器ip地址是多少
- 本地域名服务器告诉主机你要找的ip地址是多少
问题:
DNS服务器发送域名解析请求结果的时候,是通过UDP连接发送的,冒充服务器给我们回复错误的域名和ip地址,让我们去错误的地址。 - 在普通的局域网环境下,UDP的数据最大为1472字节最好(避免分片重组)。链路层1500
- 在网络编程中,Internet中的路由器可能有设置成不同的值(小于默认值),Internet上的标准MTU值为576,所以Internet的UDP编程时数据长度最好在576-20-8=548字节以内。(ip包20)
为什么要使用udp:
因为udp快啊,而且dns返回的是ip地址内容较短,不存在上下文的问题(udp面向报文段,内容不能连续)
[手写4条sql(like,in,or,between)]
SELECT * FROM Persons WHERE LastName NOT BETWEEN 'Adams' AND 'Carter'
SELECT * FROM Persons WHERE LastName IN ('Adams','Carter')
SELECT * FROM Persons WHERE City LIKE '%lon%' (like通配符,%是通配符,表示那个地方包含lon就行了字符串单引号)
SELECT * FROM Persons WHERE firstname='Thomas' OR lastname='Carter' (或)
CREATE INDEX PersonIndex ON Person (LastName)
[mysql有哪些引擎,优劣。备份怎么实现。]
B+Tree只在最末端叶子节点存数据,叶子节点是以链表的形势互相指向的。
聚集索引中,叶子节点的data直接包含数据;非聚集索引中,叶子节点存储数据地址的指针。
Myisam引擎(非聚集索引), Innodb引擎(聚集索引)(推荐使用)
一个存的地址,一个存的数据
[mysql中索引为什么会增加查询速度,索引的内部怎么实现?]
索引内部是b+,以前是全表遍历,现在时间复杂度低了很多。用索引去查询,查询时间变短了很多。
[主键索引是什么,B+树有什么好处,主键索引和普通索引有什么区别。]
主键索引:即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值;
- b+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”;
- b+树查询必须查找到叶子节点,b树只要匹配到即可不用管元素位置,因此b+树查找更稳定(并不慢);
- 对于范围查找来说,b+树只需遍历叶子节点链表即可,b树却需要重复地中序遍历
总结, 非主键索引的查询需要多扫描一颗索引树, 效率相对更低.(非主键索引存储的是主键的值,查询结束后还要用查询到的主键再去做一次
查询)
b+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”;
b+树查询必须查找到叶子节点,b树只要匹配到即可不用管元素位置,因此b+树查找更稳定(并不慢);
对于范围查找来说,b+树只需遍历叶子节点链表即可,b树却需要重复地中序遍历
[B树和B+树介绍一下,说说区别]
多叉平衡查找树
B树:
- 定义任意非叶子结点最多只有M个儿子,且M>2;
- 根结点的儿子数为[2, M];
- 除根结点以外的非叶子结点的儿子数为[M/2, M],向上取整;
- 非叶子结点的关键字个数=儿子数-1;
- 所有叶子结点位于同一层;
- k个关键字把节点拆成k+1段,分别指向k+1个儿子,同时满足查找树的大小关系。
特点: - 关键字集合分布在整颗树中;
- 任何一个关键字出现且只出现在一个结点中;
- 搜索有可能在非叶子结点结束;
- 其搜索性能等价于在关键字全集内做一次二分查找;
B+树特征:
- 有n棵子树的非叶子结点中含有n个关键字(b树是n-1个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(b树是每个关键字都保存数据)。
- 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
- 通常在b+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
- 同一个数字会在不同节点中重复出现,根节点的最大元素就是b+树的最大元素。
[B+树和普通的B树和用平衡树有什么区别?为什么不用后两种树。]
二叉平衡搜索树,任意节点左右子树两边高度不会超过一
红黑树是平衡树,但是不满足节点差为1,红的的左右节点一定是黑的,且叶子节点都是黑的空节点,且根节点到叶子节点经过的黑色节点相同,
少了一些限制,插入删除时没有那么多自旋,效率高,set,map底层用的这个。(数组,链表,红黑树)
[索引优化]
1、最左前缀
索引的最左前缀和和B+Tree中的“最左前缀原理”有关,举例来说就是如果设置了组合索引<col1,col2,col3>那么以下3中情况可以使用索引:col1,<col1,col2>,<col1,col2,col3>,其它的列,比如<col2,col3>,<col1,col3>,col2,col3等等都是不能使用索引的。
根据最左前缀原则,我们一般把排序分组频率最高的列放在最左边,以此类推。
2、带索引的模糊查询优化
在上面已经提到,使用LIKE进行模糊查询的时候,'%aaa%'不会使用索引,也就是索引会失效。如果是这种情况,只能使用全文索引来进行优化)。
为检索的条件构建全文索引,然后使用 SELECT * FROM tablename MATCH(index_colum) ANGAINST(‘word’);
3、使用短索引
字符串太长只取前面一部分
对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的 列,如果在前10 个或20 个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。
[查询慢怎么优化。]
1、没有索引或者没有用到索引(这是查询慢最常见的问题,是程序设计的缺陷)、
2、查询语句不好,没有优化(有的语句不让用缩引,比如like(%123%),前面有通配符的)
3、I/O吞吐量小,形成了瓶颈效应。
4、没有创建计算列导致查询不优化。
5、内存不足
6、网络速度慢
7、查询出的数据量过大(可以采用多次查询,其他的方法降低数据量)
8、锁或者死锁(这也是查询慢最常见的问题,是程序设计的缺陷)
9、sp_lock,sp_who,活动的用户查看,原因是读写竞争资源。
10、返回了不必要的行和列
[SQL,手写一个SQL的查询,里面用到了order by, limit, desc这些关键字]
答案: SELECT * FROM table1 ORDER BY age1 DESC LIMIT 2 (order by依照什么顺序进行排序,desc降序,asc升序,默认是升序
limit限制查询数目) distinct(去重,必须放在列名前,一次作用所有列)
[SQL查询语句是怎么查询的,即执行查询语句的过程]
mysql服务器大概分为连接器(管理连接,验证权限),分析器(词法分析,语法分析,ast树),优化器(优化执行过程),执行器(和引擎交互执行具体的sql语句)
- 应用程序把查询SQL语句发送给服务器端执行。
我们在数据库层执行SQL语句时,应用程序会连接到相应的数据库服务器,把SQL语句发送给服务器处理。 - 查询缓存
服务器在解析一个查询语句之前,如果查询缓存是打开的(MySQL默认打开,可以使用have_query_cache查看),在接收到查询请求后,并不会直接去数据库查询,而是在数据库的查询缓存中找是否有相对应的查询数据(某条给定的查询语句在第一次执行时,服务器会缓存这条查询语句和他返回的结果。),如果存在,那么在返回查询结果之前,MySQL会检查一次用户权限。这仍然无需解析查询SQL语句,因为查询缓存中已经存放了当前查询需要访问的表信息。如果权限没有问题,key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被则直接从缓存中拿到结果返回给客户端。 - 查询优化处理,生成执行计划 (没有命中缓存)
接下来服务器会将一个SQL转换成一个执行计划,而这个阶段包括:解析SQL、预处理、优化SQL执行计划,其中任何一个阶段出错都会导致查询进行不下去。 - 解析SQL:Mysql通过将SQL语句进行解析,并生成一棵对应的解析树。MySQL解析器将使用MySQL语法分析(语法规则验证)和解析查询,如将验证是否使用错误的关键字,或者关键字的顺序是否正确。
- 预处理:预处理器根据一些Mysql规则进一步检查解析树是否合法,如数据表和数据列是否存在,解析列名和别名,是否有歧义。接下来预处理器会验证用户权限(precheck)。查看用户是否有相应的操作权限。
- 优化SQL:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序,将SQL语句转化成执行计划,一条查询可以有很多种执行方式,最后都返回相同的结果,最后找到其中最好的执行计划(Mysql使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划的成本,选择其中成本最小的一个)
- Mysql根据相应的执行计划完成整个查询(此处的执行计划是一个数据结构) 由数据库引擎来完成innodb
- 将查询结果返回客户端(可以缓存就缓存)
[mvcc]
多版本并发控制
替代行锁和表锁实现数据库事务
悲观锁: 依赖数据库的锁机制来实现(行锁和表锁)
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁 依靠数据来实现(比如原子性automic,或者给数据加上版本号)
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
RR,RC(RC用的全是当前读,RR用的快照读,但是更新之后就是当前读,就会出现幻读问题)
快照读: 读的是快照版本(即比自己事物id小的但是最大的那个版本的结果, up_limit_id)
当前读概念:每次更新之前都会去获得最新的提交版本,然后再更新, (他自己读一定会用自己的版本号)
RR隔离级别下当执行普通的select查询时,innodb默认会执行快照读,相当于就是给你目前的状态找了一张照片,以后执行select 的时候就会返回当前照片里面的数据,当其他事务提交了也对你不造成影响,和你没关系,这就实现了可重复读(可以避免不可重复读)
INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识(这只是理论,innoDB实际是通过undo log来备份旧记录的)。(innodb有日志,即使数据没存盘,可以通过日志系统来恢复数据)
undo log主要分为两种:
insert undo log
代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
操作系统
[死锁的概念,避免死锁的具体方法。]
在许多应用中进程需要以独占的方式访问资源,当操作系统允许多个进程并发执行时可能会出现进程永远被阻塞现象,如两个进程分别等待对方所占的资源,于是两者都不能执行而处于永远等待状态,此现象称为死锁。
- 互斥条件:临界资源是独占资源,进程应互斥且排他的使用这些资源。
- 占有和等待条件:进程在请求资源得不到满足而等待时,不释放已占有资源,持续等待。
- 不剥夺条件:又称不可抢占,已获资源只能由进程自愿释放,不允许被其他进程剥夺。
- 循环等待条件:又称环路条件,存在循环等待链,其中,每个进程都在等待链中等待下一个进程所持有的资源,造成这组进程处于永远等待状态。
死锁避免主要有以下三种方法: - 死锁防止:破坏四大条件即可
- 死锁避免:
银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。 - 死锁检测和恢复:
死锁检测算法:
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生
死锁恢复算法
1.资源剥夺法 剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除。
2.进程回退法 根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制。
3.进程撤销法 撤销陷入死锁的所有进程,解除死锁,继续运行。 逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。
可选择符合下面条件之一的先撤销: 1.CPU消耗时间最少者 2.产生的输出量最小者 3.预计剩余执行时间最长者 4.分得的资源数量最少者后优先级最低者 - 系统重启法 结束所有进程的执行并重新启动操作系统。这种方法很简单,但先前的工作全部作废,损失很大。
[银行家算法(预防死锁的方法)]
安全状态: 如果存在一个由系统中所有进程构成的安全序列P1,…,Pn,则系统处于安全状态。安全状态一定是没有死锁发生。
不安全状态: 不存在一个安全序列。不安全状态不一定导致死锁。
银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法.
[缓存机制有了解吗?缓存的替换策略有哪些,说一下。]
[CPU调度算法有哪些。]
- 先到先服务(FCFS)调度算法:采用这种方案,先请求的进程先得到CPU,由于是非抢先调度,所以一个进程得到CPU后,会一直占用CPU到该进程结束。
- 最短作业优先(SJF)调度算法:这个算法将每个进程与下次CPU执行的长度关联起来,当CPU变为空闲时,它会被赋给CPU执行时间最短的进程。
- 优先级调度(priority-scheduling)算法:每个进程都有一个优先级,进程的执行顺序按照优先级。SJF算法也属于优先级调度算法,其优先级为CPU执行时间的倒数,优先级调度可以是抢占的或非抢占的。该算法的一个问题是可能导致无限阻塞或饥饿,一个解决的办法称为老化,每隔一段时间就将所有处于等待状态的进程的优先级提高一级,这样可以证明,在不超过一个常数的时间内,所有进程都会执行。
- 轮转(RR)调度算法:该算法是专门为分时系统设计的,类似于FCFS算法,但增加了抢占的以切换进程,将一个较小的时间单元定义为时间片,通常为10ms~100ms,就绪队列为一个循环队列,为每一个进程分配不超过一个时间片的CPU。
- 多级队列调度算法:分配多个队列,队列和队列之间有优先级的高低,高优先级的队列中的进程相对于低优先级队列中的进程有绝对高的优先级。
- 多级反馈队列调度算法:动态地调整进程所在的队列,防止饥饿的发生。
[缺页中断是什么? 页面置换算法?]
进程访问的内存中页不在内存中时就需要操作系统将其调入,这就是缺页中断。
opt 最佳置换算法 最佳置换算法选择的是淘汰后永不使用或者最长时间不访问的页面
fifo 先进先出页面置换算法
lru 最近最久未使用置换算法
clock 时钟置换算法可以认为是一种最近未使用算法,即逐出的页面都是最近没有使用的那个
[fork函数介绍一下,怎么用?fork的时候内存是怎么变化的?]
fork函数是Unix下以自身进程创建子进程的系统调用,一次调用,两次返回,如果返回是0,则是子进程,如果返回值>0,则是父进程(返回值是子进程的pid),这是众为周知的。还有一个很重要的东西是,在fork()的调用处,整个父进程空间会原模原样地复制到子进程中,包括指令,变量值,程序调用栈,环境变量,缓冲区,等等。所有东西全部赋值(接下来执行哪行语句都是一样的)
[io多路复用]
I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流.
[epoll用过吗?和select有什么区别,说一下内部实现。]
select轮询所有文件描述符即socket,返回哪些socket可以用于读取(准备好的文件描述符的个数),这样我们去read等的时候就不会阻塞(socket默认就是阻塞模式,可以调成非阻塞)
select函数,超时为0,错误为-1
最多监听1024个文件描述符
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
maxfdp 所有文件描述符的最大值加1
fd_set 1024位(bit),每一位对应一个i/o端口
timeout null 阻塞,无限时间,直到有一个发生变化为止 0 非阻塞,0秒
int epoll_create(size) // 创建epoll,开辟epoll结构
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 事件注册函数,把要监听的描述放在里面去
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);类似于select,等待执行(设置时间决定阻塞或者非阻塞)
首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。
①从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
② 此外,内核使用了slab机制,为epoll提供了快速的数据结构:
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
③ epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。
[epoll的水平触发(LT)和边缘触发(ET)了解吗?分别说一下特点和区别。]
水平触发(默认): 事件就绪后,用户可以选择处理或者不处理,内核会把事件给保存下来,下次调用epoll_wait时就绪事件还会交给用户
边缘触发: 就绪事件交给你后,我就不管了,避免事件重复处理,效率更高。
[内核态,用户态]
每个进程都有用户态堆栈和内核态堆栈,使用系统调用才使用内核态,内核态程序也需要执行,也许也有变量存堆栈。
cpu执行进程c被内核态中断切换去内核态执行,执行完后切换回来。
1)用户态切换到内核态的3种方式
a. 系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
b. 异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
c. 外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
[硬链接和软链接]
为了解决文件共享问题,Linux引入了软链接和硬链接。除了为Linux解决文件共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若1个inode号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名,使用ln创建。若文件用户数据块中存放的内容是另一个文件的路径名指向,则该文件是软连接。软连接是一个普通文件,有自己独立的inode,但是其数据块内容比较特殊。
操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。
inode包含文件的元信息,具体来说有以下内容:
- 文件的字节数
- 文件拥有者的User ID
- 文件的Group ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
- 链接数,即有多少文件名指向这个inode
- 文件数据block的位置
[阻塞队列具体是怎么实现的,怎么优化它。(锁的粒度以及不空的时候读不加锁)]
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。(阻塞队列一次只能有一个线程访问)
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当 队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者 也只从容器里拿元素。
[多线程模型](一般是指用户级线程和内核线程的对应关系)
- 多对一模型 多个用户级线程映射到一个内核级线程。每个用户进程只对应一个内核级线程。(早期)
优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可再多核处理机并行运行。 - 一对一模型 一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。 - 多对多模型 n用户级线程映射到m个内核级线程(n>=m)。每个用户进程对应m个内核级线程。
克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内存核级线程,开销太大的缺点
[socket编程了解多少,手写socket客户端服务端]
ServerSocket s = new ServerSocket("127.0.0.1", 8080);
while(true)
{
socket ss = s.accept(); // 阻塞住
}
socket ss = new socket()
进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
在三态模型中进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
匿名管道只能在具有亲缘关系的进程间使用,进程的亲缘关系通常指父子进程关系
命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
信号 (sinal) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
互斥量Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
事件(信号)Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作
[bio,nio]
同步阻塞,同步非阻塞
同步阻塞多线程实现高并发,同步非阻塞单线程实现高并发(io复用)
[linux对于内存的查看,对于进程的查看]
根据进程号查看进程相关信息占用的内存情况 pmap -d 进程号
top -u 用户名/进程名 按用户名或者进程名查看内存使用情况
ps 命令可以查看使用内存最高的进程,或者查看使用cpu占用率最高的进程
free 查看linux内存使用情况
mpstat 打印cpu情况
vmstat命令来查看上下文切换的次数, 其中cs列就是指上下文切换的数目(一般情况下, 空闲系统的上下文切换每秒大概在1500以下)
[父子进程间共享哪些资源。]
[多线程之间共享哪些资源。]
同一进程内的线程共享进程的大部分系统资源,包括堆、代码段、数据段,每个线程只拥有一些在运行中必不可少的私有属性,比如程序计数器,线程Id,栈、指令寄存器。
[多线程,多进程使用场景]
多进程模型的优势是CPU,cpu中断,cpu切换
多线程模型主要优势为线程间切换代价较小,因此适用于I/O密集型的工作场景,因此I/O密集型的工作场景经常会由于I/O阻塞导致频繁的切换线程。同时,多线程模型也适用于单机多核分布式场景。(任务具有并发性,即任务可以拆分为多个子任务,并发执行,能提高速度也得是有多核cpu才行)
[请你说一说Linux虚拟地址空间]
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换
好处:
1.扩大地址空间;
2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4.当进程通信时,可采用虚存共享的方式实现。
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片
虚拟内存的代价:
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
2.虚拟地址到物理地址的转换,增加了指令的执行时间。
3.页面的换入换出需要磁盘I/O,这是很耗时的
4.如果一页中只有一部分数据,会浪费内存。
[操作系统字节对齐]
32位操作系统四位四位的抓,64位8byte8byte的抓,尽量让一次抓完,字节对齐,放在一次可以抓完的地方
Struct A
{
Int a;
short b;
int c;
char d;
};
struct B{
Int a ;
int c;
short b;
char d;
};
[操作系统大端小端]
大端是指低字节存储在高地址;小端存储是指低字节存储在低地址。我们可以根据联合体来判断该系统是大端还是小端。因为联合体变量总是从低地址存储。
通过union来看是大端还是小端
union
{
char c;
int a;
}
union占用四个字节, 给a赋值1,看c是等于0还是1就知道是大端存储还是小端存储
[生产者消费者模型]
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,
所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
[请问怎么实现线程池]
核心线程(corePool):线程池最终执行任务的角色肯定还是线程,同时我们也会限制线程的数量,所以我们可以这样理解核心线程,有新任务提交时,首先检查核心线程数,如果核心线程都在工作,而且数量也已经达到最大核心线程数,则不会继续新建核心线程,而会将任务放入等待队列。
等待队列 (workQueue):等待队列用于存储当核心线程都在忙时,继续新增的任务,核心线程在执行完当前任务后,也会去等待队列拉取任务继续执行,这个队列一般是一个线程安全的阻塞队列,它的容量也可以由开发者根据业务来定制。
非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。
线程活动保持时间 (keepAliveTime):线程空闲下来之后,保持存货的持续时间,超过这个时间还没有任务执行,该工作线程结束。
饱和策略 (RejectedExecutionHandler):当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。
[请求分页系统、请求分段系统和请求段页式系统]
[linux awk命令,文件排序命令]
[linux会哪些命令,介绍10个]
ll
ls
cd /根目录 ~ home目录
cat 看最后一页
tail 指定行数
rm
mkdir
vim
:wq 保存并退出
:q 不保存退出
:q! 强制退出
mv
cp
关机
shutdown -h now 立刻关机
shutdown -h 5 5分钟后关机
poweroff 立刻关机
重启
shutdown -r now 立刻重启
shutdown -r 5 5分钟后重启
reboot 立刻重启
man 打开命令说明书
touch 新建文件
chmod 权限修改 rwx 读写可执行文件
第一位:-就代表是文件,d代表是文件夹
第一段(3位):代表拥有者的权限
第二段(3位):代表拥有者所在的组,组员的权限
第三段(最后3位):代表的是其他用户的权限
tar -zcvf ab.tar aa.txt bb.txt
或:tar -zcvf ab.tar *
[ linux四种锁]
锁包括互斥锁,RCU锁,自旋锁和读写锁
- 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
- 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
- 自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
- RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。
[请你讲述一下互斥锁(mutex)机制,以及互斥锁和读写锁的区别]
1、互斥锁和读写锁区别:
互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
互斥锁和读写锁的区别:
1)读写锁区分读者和写者,而互斥锁不区分
2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
[c++的四种锁]
互斥锁
条件锁
读写锁
自旋锁
[说一说你用到的锁>
生产者消费者问题利用互斥锁和条件变量可以很容易解决,条件变量这里起到了替代信号量的作用(信号量的PV操作)
[请你说一下僵尸进程]
1)正常进程
正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息,直到父进程通过wait / waitpid来取时才释放。保存信息包括:
1进程号the process ID
2退出状态the termination status of the process
3运行时间the amount of CPU time taken by the process等
2)孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
3)僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
僵尸进程是一个进程必然会经过的过程:这是每个子进程在结束时都要经过的阶段。
如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
危害:
如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
外部消灭:
通过kill发送SIGTERM或者SIGKILL信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源
内部解决:
1、子进程退出时向父进程发送SIGCHLD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。(默认不会处理,我们可以处理)
2. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
3.、fork两次,原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。
父进程和孙子进程一起去整了
int pid = fork()
if(pid > 0)
{
wait()
}
else
{
sleep(2);
int pid = fork()
if(pid > 0)
exec();
}
4. 父进程一直wait或者waitpid
ps命令
S 进程处于interruptable sleep状态
D 进程处于Uninterruptable sleep状态(这个状态应该看不见,io出问题,不能被kill,只能重启系统)
R 进程处于运行状态
Z 进程处于僵尸状态
T Stop模式,进程要么处于被调试状态
[常用线程模型]
1、Future模型
该模型通常在使用的时候需要结合Callable接口配合使用。
Future是把结果放在将来获取,当前主线程并不急于获取处理结果。允许子线程先进行处理一段时间,处理结束之后就把结果保存下来,当主线程需要使用的时候再向子线程索取。
Callable是类似于Runnable的接口,其中call方法类似于run方法,所不同的是run方法不能抛出受检异常没有返回值,而call方法则可以抛出受检异常并可设置返回值。两者的方法体都是线程执行体。
2、fork&join模型
该模型包含递归思想和回溯思想,递归用来拆分任务,回溯用合并结果。可以用来处理一些可以进行拆分的大任务。其主要是把一个大任务逐级拆分为多个子任务,然后分别在子线程中执行,当每个子线程执行结束之后逐级回溯,返回结果进行汇总合并,最终得出想要的结果。
这里模拟一个摘苹果的场景:有100棵苹果树,每棵苹果树有10个苹果,现在要把他们摘下来。为了节约时间,规定每个线程最多只能摘10棵苹树以便于节约时间。各个线程摘完之后汇总计算总苹果树。
3、actor模型
actor模型属于一种基于消息传递机制并行任务处理思想,它以消息的形式来进行线程间数据传输,避免了全局变量的使用,进而避免了数据同步错误的隐患。actor在接受到消息之后可以自己进行处理,也可以继续传递(分发)给其它actor进行处理。在使用actor模型的时候需要使用第三方Akka提供的框架。
4、生产者消费者模型
生产者消费者模型都比较熟悉,其核心是使用一个缓存来保存任务。开启一个/多个线程来生产任务,然后再开启一个/多个来从缓存中取出任务进行处理。这样的好处是任务的生成和处理分隔开,生产者不需要处理任务,只负责向生成任务然后保存到缓存。而消费者只需要从缓存中取出任务进行处理。使用的时候可以根据任务的生成情况和处理情况开启不同的线程来处理。比如,生成的任务速度较快,那么就可以灵活的多开启几个消费者线程进行处理,这样就可以避免任务的处理响应缓慢的问题。
5、master-worker模型
master-worker模型类似于任务分发策略,开启一个master线程接收任务,然后在master中根据任务的具体情况进行分发给其它worker子线程,然后由子线程处理任务。如需返回结果,则worker处理结束之后把处理结果返回给master。
c++
[请你来回答一下new和malloc的区别]
1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
3、new不仅分配一段内存,而且会调用构造函数,malloc不会。
4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5、new是一个操作符可以重载,malloc是一个库函数。
6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
[构造函数和析构函数可否为虚函数]
虚表是共享的,但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针。
不可以 虚函数表指针在构造函数中初始化,vptr指针都没有怎么去构造对象。
可以,直接调用子类析构函数了
[构造函数可否调用虚函数,会有什么后果]
结论:构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
[手写一个泛型函数。]
template<class t="">
T add(T a, T b)
{
return a + b;
}</class>
[说一下几个范式。]
第一范式:当关系模式R的所有属性都不能再分解为更基本的数据单位时,称R是满足第一范式,即属性不可分
第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式
第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,即非主属性不传递依赖于键码
[static的作用。]
全局变量
局部变量
成员变量
[map和set的区别,底层实现是什么。]
map是key,value
set是只有一个数据
底层是红黑树,是有序的里面的元素
[C++函数指针的概念和作用。]
函数指针让“函数”可以是一个“变量”。把函数变成了一个变量。
[C++内存管理是什么样的,分别存什么数据。]
虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
[C++中inlcude ""和<>的区别是什么。]
双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
- 对于使用双引号包含的头文件,查找头文件路径的顺序为:
当前头文件目录
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径 - 对于使用尖括号包含的头文件,查找头文件的路径顺序为:
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
[指针和引用的区别,sizeof一个指针和一个引用的区别。]
指针32位为4,64位为8
1.指针有自己的一块空间,而引用只是一个别名(不分配存储单元);
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
5.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
6.指针可以有多级指针(**p),而引用至于一级;
7.指针和引用使用++运算符的意义不一样;
8.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
[32位电脑进程大小为多少]
32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中03G是用户态空间,34G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:
3G用户空间和1G内核空间
[重载和重写。覆盖]
覆盖在类继承时,子类覆盖掉父类的方法
重载是在不同函数间,通过返回值,参数类型不同来重载,多个同名函数处理不同的问题。
重写是指子类重写父类的虚函数。
[深拷贝和浅拷贝的区别。]
浅拷贝就是拷贝构造函数,直接赋值语句,会有问题,比如删除指针时会出现问题
深拷贝就是用构造函数来进行赋值,而不是直接赋值语句来赋值,对象使用的是不同的内存空间。
[智能指针用过哪些?share和weak的区别。]
用于避免内存泄漏
shared_ptr是计数的,计数为0就把内存空间释放(shared_ptr计数是原子操作的,但是读写可能会有并发问题,多个一起读写同一块地址,可能会出错,线程不安全)
weak_ptr是不占用计数的,可以避免shared_ptr死锁,用shared_ptr来生成的,是一种访问手段而已,不管理生命周期
[在我们写程序的时候,为什么从来不用考虑寻址的空间大小?]
32位的电脑为2的32次方即4gb寻址能力
1、64bit CPU拥有更大的寻址能力,最大支持到16GB内存(因为目前cpu地址总线为34条,条,寻址范围2^10* 2^10* 2^10* 2^4=16G),而32bit只支持4G内存(有32位总线)
2、64位CPU一次可提取64位数据,比32位提高了一倍,理论上性能会提升1倍。但这是建立在64bit操作系统,64bit软件的基础上的。
[如何判断内存泄漏]
使用内存泄漏检测工具varglind,mtrace,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
1.堆内存泄漏 new malloc
2.系统资源未释放 socket bitmap handle
3.析构函数里的没有执行
[介绍一下内存溢出和处理方法。]
指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误
释放部分内存腾出空间然后重新运行程序就完事了
内存溢出原因:
内存中加载的数据量过于庞大,如一次从数据库取出过多数据
集合类中有对对象的引用,使用完后未清空,使得不能回收
代码中存在死循环或循环产生过多重复的对象实体
使用的第三方软件中的BUG
启动参数内存值设定的过小
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
mmap是用于多个进程映射同一文件实现内存共享的。
1 前言:先介绍一下普通的读写文件的原理,进程调用read/write系统调用后会陷入内核,内核开始读写文件,假设内核是在读文件,内核先把文件读取到内核缓冲区,然后把内核缓冲区的数据拷贝到用户缓冲区,实际上整个过程拷贝了两次数据,即先从文件到内核缓冲区,再从内核缓冲区到用户缓冲区;
2 概念:把某个文件映射到进程的地址空间,通过对地址空间的读写,实现对文件的读写,mmap系统调用可以使多个进程映射同一个普通文件来实现共享内存。普通文件映射到地址空间后,进程可以像访问内存的方式一样去访问该文件,这样不需要调用read/write系统调用,减少了用户、内核切换的开销;
[gdb调试]
1、GDB调试
GDB 是自由软件基金会(Free Software Foundation)的软件工具之一。它的作用是协助程序员找到代码中的错误。如果没有GDB的帮助,程序员要想跟踪代码的执行流程,唯一的办法就是添加大量的语句来产生特定的输出。但这一手段本身就可能会引入新的错误,从而也就无法对那些导致程序崩溃的错误代码进行分析。
GDB的出现减轻了开发人员的负担,他们可以在程序运行的时候单步跟踪自己的代码,或者通过断点暂时中止程序的执行。此外,他们还能够随时察看变量和内存的当前状态,并监视关键的数据结构是如何影响代码运行的。
2、条件断点
条件断点是当满足条件就中断程序运行,命令:break line-or-function if expr。
例如:(gdb)break 666 if testsize==100
[共享内存的几个api说一下。]
linux下共享内存
#include sys/shm.h
Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h中。
1)新建共享内存shmget
int shmget(key_t key,size_t size,int shmflg);
key:共享内存键值,可以理解为共享内存的唯一性标记。
size:共享内存大小
shmflag:创建进程和其他进程的读写权限标识。
返回值:相应的共享内存标识符,失败返回-1
2)连接共享内存到当前进程的地址空间shmat
void *shmat(int shm_id,const void *shm_addr,int shmflg);
shm_id:共享内存标识符
shm_addr:指定共享内存连接到当前进程的地址,通常为0,表示由系统来选择。
shmflg:标志位
返回值:指向共享内存第一个字节的指针,失败返回-1
3)当前进程分离共享内存shmdt
int shmdt(const void *shmaddr);
4)控制共享内存shmctl(和信号量的semctl函数类似,控制共享内存)
int shmctl(int shm_id,int command,struct shmid_ds *buf);
shm_id:共享内存标识符
command: 有三个值
IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
IPC_SET:设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。
IPC_RMID:删除共享内存
buf:共享内存管理结构体。
[linux信号量库]
#include <sys/sem.h>
有一系列函数
ipcs -m 共享内存
ipcs -s 查看信号量
ipcrm -sem id
ipcrm -m id
[协程]
我们知道操作系统在线程等待IO的时候,会阻塞当前线程,切换到其它线程,这样在当前线程等待IO的过程中,其它线程可以继续执行。当系统线程较少的时候没有什么问题,但是当线程数量非常多的时候,却产生了问题。一是系统线程会占用非常多的内存空间,二是过多的线程切换会占用大量的系统时间。
协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
线程切换
挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处
恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中
线程上下文切换会有什么问题呢?
上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。
直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小
减少线程切换的方法:
无锁并发编程、CAS算法(c++11 atomic关键字)、使用最少的线程和使用协程
c++内容
[c++如何实现共享内存]
#include <windows.h>
CreateFileMapping // 创建共享文件句柄
MapViewOfFile // 映射缓存区视图, 得到指向共享内存的指针
[c++字符串连接的几种方式]
- 产生一个新的字符串,直接拼接
- append 给他加在后面,原字符串上改动
- stringstream ss; ss<<a<<b; return ss.str(); // 字符串流,用字符串流和来合并然后输出
[C++中,一个空的类对象占多少字节,如果里面有一个char,一个int,一个虚函数,占多少字节。]
其实还有一个叫cpu
一个字节,即使这个对象是空的,还是需要给他一个独一无二的地址,即一个字节
一个char 一个字节(可能存在数据补齐的问题)
一个int 四个字节
一个虚函数 四个字节(vptr虚函数表指针)
[C++菱形继承是怎么解决的,什么情况下菱形继承会出现问题。]
b继承a,c继承a,d继承b和c,那么d里面会有两个A,产生二义性
[虚函数的工作机制说一下,一般用在什么场景下,哪些方法需要定义为虚函数。]
工作机制为虚函数表,在对象里面有一个vptr指针,虚函数表指针,有虚函数就会有这些东西,实现虚函数就把虚函数表里的函数指针进行替换。
当用子类给父类赋值时,为了使用子类的方法,就必须使用虚函数,而且有时候析构函数必须设为析构函数,不然析构执行的是父类的构造函数。
虚函数表在编译器确定就不改了,属于常量区,虚函数指针在构造时才更改。
[初始化列表的使用场景]
a)如果这个成员是个引用
b)如果是个const类型成员
c)如果你这个类是继承一个基类,并且基类中有构造函数,这个构造函数里边还有参数。
d)如果你的成员变量类型是某个类类型,而这个类的构造函数带参数时;
成员是按照他们在类中出现的顺序进行初始化的,而不是按照他们在初始化列表出现的顺序初始化的
[一致性哈希和普通哈希有什么区别]
一致性哈希算法能尽可能减少了服务器数量变化所导致的缓存迁移。
consistent(一致性) hash算法能够在一定程度上改善缓存的雪崩问题,它能够在移除/添加一台缓 存服务器时,尽可能小的改变已存在的key映射关系,避免大量key的重新映射。一致性哈希算法能尽可能减少了服务器数量变化所导致的缓存迁移。
先构造一个长度为2 32次方的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 23 2次方-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 2 32次方-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。
这种算法解决了普通余数Hash算法伸缩性差的问题。
散列函数
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:
- 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a?key + b,其中a和b为常数(这种散列函数叫做自身函数)
- 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
- 平方取中法:取关键字平方后的中间几位作为散列地址。
- 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
- 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
- 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
[c++新特性]
C++11 最常用的新特性如下:
auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
智能指针:C++11新增了unique_ptr, std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
初始化列表:使用初始化列表来对类进行初始化
右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
atomic原子操作用于多线程资源互斥操作
新增STL容器 (定长数组)以及tuple(多值的pair),unordered_map,unordered_set
[请你详细介绍一下C++11中的可变参数模板、右值引用和lambda这几个新特性。]
- C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。
template void print(T head, Args... args)
{
cout << head << ","; print(args...);
}
int main()
{
print(1, 2, 3, 4); return 0;
}
递归调用自己就行了 - C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,没有名字的就是右值。而在指C++11中,右值是由两个概念构成,将亡值和纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等。int &&a=1;
- lambda {}
std::for_each(begin(some_list), end(some_list), [&total](int x)
{
total += x;
});
1,如果异常没有被处理,最后 terminate() 结束整个程序;
2,terminate() 是整个程序释放系统资源的最后机会;
3,结束函数可以自定义,但不能继续抛出异常;
4,析构函数中不能抛出异常,可能导致 terminate() 多次调用;(因为terminate函数会调用析构函数导致重复调用terminate函数)
其他方面
[设计模式]
常见的设计模式如下:
- 单例模式:单例模式主要解决一个全局使用的类频繁的创建和销毁的问题。单例模式下可以确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。单例模式有三个要素:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。(一种是在代码开始之前就初始化static, 一种是判断为空时加锁mutex互斥锁)
- 工厂模式:工厂模式主要解决接口选择的问题。该模式下定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,使其创建过程延迟到子类进行。
- 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。(被关注人动态更新了,所有关注人收到消息)
- 装饰器模式:对已经存在的某些类进行装饰,以此来扩展一些功能,从而动态的为一个对象增加新的功能。装饰器模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。(比如登录被添加了qq登录,微信登录,手机号登录等)
负载均衡算法
1、轮询法
将请求按顺序轮流地分配到每个节点上,不关心每个节点实际的连接数和当前的系统负载。
优点:简单高效,易于水平扩展,每个节点满足字面意义上的均衡;
缺点:没有考虑机器的性能问题,根据木桶最短木板理论,集群性能瓶颈更多的会受性能差的服务器影响。
2. 随机法
将请求随机分配到各个节点。由概率统计理论得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配,也就是轮询的结果。
优缺点和轮询相似。
3、源地址哈希法
源地址哈希的思想是根据客户端的IP地址,通过哈希函数计算得到一个数值,用该数值对服务器节点数进行取模,得到的结果便是要访问节点序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会落到到同一台服务器进行访问。
优点:相同的IP每次落在同一个节点,可以人为干预客户端请求方向,例如灰度发布;
缺点:如果某个节点出现故障,会导致这个节点上的客户端无法使用,无法保证高可用。当某一用户成为热点用户,那么会有巨大的流量涌向这个节点,导致冷热分布不均衡,无法有效利用起集群的性能。所以当热点事件出现时,一般会将源地址哈希法切换成轮询法。
4. 加权轮询法
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
5. 加权随机法
与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
键值范围法
6. 键值范围法
根据键的范围进行负债,比如0到10万的用户请求走第一个节点服务器,10万到20万的用户请求走第二个节点服务器……以此类推。
优点:容易水平扩展,随着用户量增加,可以增加节点而不影响旧数据;
缺点:容易负债不均衡,比如新注册的用户活跃度高,旧用户活跃度低,那么压力就全在新增的服务节点上,旧服务节点性能浪费。而且也容易单点故障,无法满足高可用。
动态算法
1、最小连接数法
根据每个节点当前的连接情况,动态地选取其中当前积压连接数最少的一个节点处理当前请求,尽可能地提高后端服务的利用效率,将请求合理地分流到每一台服务器。俗称闲的人不能闲着,大家一起动起来。
优点:动态,根据节点状况实时变化;
缺点:提高了复杂度,每次连接断开需要进行计数;
实现:将连接数的倒数当权重值。
2、最快响应速度法
根据请求的响应时间,来动态调整每个节点的权重,将响应速度快的服务节点分配更多的请求,响应速度慢的服务节点分配更少的请求,俗称能者多劳,扶贫救弱。
优点:动态,实时变化,控制的粒度更细,跟灵敏;
缺点:复杂度更高,每次需要计算请求的响应速度;
实现:可以根据响应时间进行打分,计算权重。
3、观察模式法
观察者模式是综合了最小连接数和最快响应度,同时考量这两个指标数,进行一个权重的分配。
九大排序算法:
冒泡 稳定
选择 不稳定
插入 稳定
快排 不稳定
堆排 不稳定
归并排序 稳定
希尔排序 不稳定
基数排序 按照一位一位来排序 稳定
桶排序 O(N)+O(M∗(N/M∗log(N/M)))=O(N∗(log(N/M)+1)) N = M 时时间复杂度为O(1) 不稳定
桶排序 堆排序、快速排序、希尔排序、选择排序是不稳定的排序算法,而基数排序、冒泡排序、插入排序、归并排序是稳定的排序算法。
外排序:
二路归并
多路归并
[请你回答一下git Merge和rebase区别]
git rebase 会把另一个分支拷贝过来,然后把我的历史放在那个分支前面,形成一条线,那个分支就全都是老版本了
git merge 只解决一次冲突,两个分支的历史都能看见,按照时间线来看(但是如果直接git log不用工具来看的话那些提交会按照时间线严格排序,然后就会很不好看)
[大量数据的排序]
桶排序 n(只要桶选得足够小,就行,一分一个桶)
计数排序 n
基数排序 nk