部分面经整理
- 一、计算机网络
- 1.1 为什么客户端的TIME-WAIT状态必须等待2MSL?
- 1.2 TIME-WAIT是服务器端的状态还是客户端的状态?
- 1.3 TIME-WAIT状态过多会产生什么后果?怎么处理?
- 1.4 关于CLOSE_WAIT状态【服务端】的解释
- 1.5 如果已经建立了连接,但是客户端出现故障了怎么办?
- 1.6 三次握手连接阶段,最后一次ACK包丢失,会发生什么?
- 1.7 四次挥手流程
- 1.8 为什么需要四次挥手?
- 1.9 FTP协议?
- 1.10 计算机网络的各层协议及作用?
- 1.11 TCP协议如何保证可靠性?
- 1.12 TCP的粘包与拆包
- 1.13 TCP与UDP头部字节定义
- 1.14 三次握手的流程?
- 1.15 三次握手的原因?
- 1.16 三次握手是否可以携带数据?
- 1.17 HTTP长连接和短连接的区别?
- 1.18 HTTP
- 1.19 UDP中使用connect的好处?
- 1.20 DNS协议
- 1.21 HTTP与HTTPS
- 2.22 TCP状态图
- 二、操作系统
- 2.1 怎么处理死锁?
- 2.2 进程调度策略
- 2.3 进程间通信方式
- 2.4 线程间通信方式
- 2.5 同步、异步I/O的本质区别?
- 2.6 进程与线程的区别?
- 2.7 引入协程的原因?
- 2.8 典型锁
- 2.9 死锁相关问题
- 2.10 乐观锁和悲观锁
- 2.11 系统调用与库函数的区别?
- 2.12 页面调度算法
- 2.13 进程调度算法
- 2.14 Linux的内存管理机制?
- 2.15 Linux虚拟内存
- 2.16 如何处理虚拟地址和物理地址的关系?
- 2.17 Linux信号
- 2.18 什么是线程池?
- 2.19 多进程与多线程
- 2.20 进程异步、同步和互斥
- 2.21 守护进程
- 2.22 孤儿进程
- 2.23 僵尸进程
- 2.24 select、poll、epoll
- 2.25 epoll有哪些触发模式,有啥区别?
- 2.26 select和epoll的区别
- 三、C++
- 3.1 delete和delete[]的区别?
- 3.2 深拷贝与浅拷贝
- 3.3 inline和define的区别
- 3.4 指针与引用的区别?
- 3.5 volatile关键字
- 3.6 extern关键字
- 3.7 static关键字与const关键字
- 3.8 重载(overload)与重写(override)
- 3.9 智能指针
- 3.10 vector与list的区别与应用?
- 3.11 STL中vector删除元素迭代器如何变化?为什么是两倍扩容?释放空间?
- 3.12 STL迭代器如何实现?
- 3.13 map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器?为什么用红黑树?
- 3.14 map和set的底层为什么使用红黑树而不是AVL树?即红黑树与AVL树的区别?
- 3.15 你知道Top K问题有哪些解决方法?
- 3.16 虚函数的开销问题
- 3.17 四种类型转换
- 四、数据库
- 五、设计模式
- 六、智力题
- 七、反问环节
一、计算机网络
1.1 为什么客户端的TIME-WAIT状态必须等待2MSL?
(1)确保ACK报文能够到达服务端,从而使服务端正常关闭连接。
四次挥手时,客户端第四次挥手的ACK报文不一定会到达服务端。服务端会超时重传FIN/ACK报文,此时如果客户端已经断开了连接,那么就无法相应服务端的二次请求,这样服务端迟迟收不到FIN/ACK报文的确认,就无法正常断开连接。
MSL是报文段在网络上存活的最长时间。客户端等待2MSL时间,即(客户端ACK报文1MSL超时+服务端FIN报文1MSL传输),就能够收到服务器端重传的FIN/ACK报文,然后客户端重传一次ACK报文,并重新启动2MSL计时器。如此保证服务端能够正常关闭。如果服务端重发的FIN没有成功地在2MSL时间里传给客户端,服务端则会继续超时重试直到断开连接。
(2)防止已失效的连接请求报文段出现在之后的连接中。
TCP要求在2MSL内不使用相同的序列号。客户端在发送完成最后一个ACK报文段后,再经过时间2MSL,就可以保证本连接持续的时间内产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。或者及时收到这些过时的报文,也可以不处理它。
1.2 TIME-WAIT是服务器端的状态还是客户端的状态?
TIME-WAIT是主动断开连接的一方会进入的状态,一般情况下都是客户端所处的状态,服务端一般设置不主动关闭连接。
TIME-WAIT需要等待2MSL,在大量短连接的情况下,TIME-WAIT会太多,这也会消耗很多系统资源。对于服务器来说,在HTTP协议里指定KeepAlive(浏览器重用一个TCP连接来处理多个HTTP请求),由浏览器来主动断开连接,可以一定程度上减少服务器的这个问题。
1.3 TIME-WAIT状态过多会产生什么后果?怎么处理?
1、产生的后果:
从服务端来讲,在大量短连接的情况下,TIME-WAIT会太多,这也会消耗很多系统资源,此时部分客户端就会显示连接不上。
从客户端来讲,客户端TIME-WAIT过多会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接。
2、解决方法:
(1)服务器可以设置SO_REUSEADDR套接字选项来避免TIME-WAIT状态,此套接字选项告诉内核,即便此端口正忙(处于TIME-WAIIT状态),也请继续并重用它。
(2)强制关闭,发送RST包越过TIMEE-WAIT状态,直接进入CLOSED状态。
1.4 关于CLOSE_WAIT状态【服务端】的解释
在被动关闭连接的情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。
如果服务器程序处于CLOSE_WAIT状态的话,说明套接字是被动关闭的。
通常CLOSEE_WAIT状态的持续时间应该很短。
1.5 如果已经建立了连接,但是客户端出现故障了怎么办?
或者说,如果三次握手阶段、四次挥手阶段的包丢失了怎么办?如“服务端重发FIN丢失”的问题。
简而言之,通过定时器+超时重传机制,尝试获取确认,直到最后会自动断开连接。
1.6 三次握手连接阶段,最后一次ACK包丢失,会发生什么?
服务端:
- 第三次的ACK在网络中丢失,那么服务端该TCP连接的状态为SYN_RECV,并且会根据TCP的超时重传机制,会等待3s、6s、12s后重新发送SYN+ACK包,以便客户端重新发送ACK包。
- 如果重发指定次数后,仍然未收到客户端的ACK应答,那么一段时间后,那么服务器就认为连接已经断开了,服务端会自动关闭这个连接。
客户端:
- 客户端认为这个连接已经建立,如果客户端向服务端发送数据,服务端将以RST包(Reset标识复位,用于异常的关闭连接)相应。此时,客户端知道第三次握手失败。
1.7 四次挥手流程
设定A为客户端,B为服务端,ACK在连接建立之后都为1.
- A发送连接释放报文,FIN=1。【停止发送数据,主动关闭TCP连接】
- B收到后发出确认,此时TCP属于半关闭状态,B能向A发送数据但是A不能向B发送数据。
- 当B不再需要连接时,发送连接释放报文,FIN=1.
- A收到后发出确认,进入TIME-WAIT状态,等待2MSL(最大报文存货时间)后释放连接。
- B收到A的确认后释放连接。
1.8 为什么需要四次挥手?
服务器在收到客户端的FIN报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回ACK报文段。
接下来可能会继续发送数据,在数据发送完后,服务器会向客户端发送FIN报文,表示数据已经发送完毕,请求关闭连接。服务器的ACK和FIN一般都会分开发送,从而导致多了一次,因此一共需要四次挥手。
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没右数据再发送时,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
1.9 FTP协议?
与一般的C/S应用不同点在于一般的C/S应用程序一般只会建立一个socket连接,这个连接同时处理服务器端和客户端的连接命令和数据传输,而FTP协议中将命令与数据分开传送的方法提高了效率。
FTP使用2个端口,一个数据端口和一个命令端口(也叫控制端口)。这两个端口一般是20(数据端口)和21(命令端口)。命令socket用来传送命令,数据socket用来传送数据。每一个FTP命令发送之后,FTP服务器都会返回一个字符串,其中包括一个响应代码和一些说明信息。其中的返回码主要是用于判断命令是否被成功执行了。
命令端口:
一般来说,客户端有一个socket用来连接FTP服务器的相关端口,它负责FTP命令的发送和接受返回的响应信息。一些操作如“登陆”、“改变目录”、“删除文件”,依靠这个连接发送命令就可完成。
数据端口:
- 对于有数据传输的操作,主要是显示目录列表,上传、下载文件,我们需要依靠另一个socket来完成。
- 如果使用被动模式,通常服务器端会返回一个端口号。客户端需要另开一个socket来连接这个端口,然后我们可根据操作来发送命令,数据会通过新开的一个端口传输。
- 如果使用主动模式,通常客户端会发送一个端口号给服务器端,并在这个端口监听。服务器需要连接到客户端开启的这个数据端口,并进行数据的传输。
主动模式(PORT):
- 主动模式下,客户端随机打开一个大于1024的端口向服务器的命令窗口P,即21端口发起连接,同时开放N+1端口监听,并向服务器发出“port
N+1”命令,由服务器从它自己的数据端口(20)主动连接到客户端指定的数据端口(N+1)。 - FTP的客户端只是告诉服务器自己的端口号,让服务器来连接客户端指定的端口。对于客户端的防火墙来说,这是从外部到内部的连接,可能会被阻塞。
被动模式(PASV):
- 为了解决服务器发起到客户端的连接问题,有了另一种FTP连接方式,即被动方式。命令连接和数据连接都有客户端发起,这样就解决了从服务器到客户端的数据端口的连接被防火墙过滤的问题。
- 被动模式下,当开启一个FTP连接时,客户端打开两个任意的本地端口(N>1024和N+1)。
- 第一个端口连接服务器的21端口,提交PASV命令。然后,服务器会开启一个任意的端口(P>1024),返回如“227 entering passive mode(127,0,0,1,4,18)”。它返回了227开头的信息,在括号中有以逗号隔开的六个数字,前四个指服务器的地址,最后两个,将倒数第二个乘256再加上最后一个数字,这就是FTP服务器开放的用来进行数据传输的端口。如得到227 entering passive mode(h1,h2,h3,h4,p1,p2),那么端口号是p1*256+p2,IP地址为h1.h2.h3.h4。这意味着在服务器上有一个端口被开放。客户端收到命令取得端口号之后,会通过N+1号端口连接服务器的端口P,然后在两个端口之间进行数据传输。
1.10 计算机网络的各层协议及作用?
分类:
- OSI七层模型:大而全,但是比较复杂、而且是先有了理论模型,没有实际应用。
- TCP/IP四层模型:是由实际应用发展总结出来的,从实质上讲,TCP/IP只有最上面三层,最下面一层没有什么具体内容,TCP/IP参考模型没有真正描述这一层的实现【网络接口层、网络层、传输层、应用层】。
- 五层模型:只出现在计算机网络教学过程中,这是对七层模型和四层模型的一个折中,既简洁与又能将概念阐述清楚。【物理层、数据链路层、网络层、传输层、应用层】
七层协议及作用:
- 应用层:为应用程序提供交互服务。在互联网中的应用层协议很多,如域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议等。
- 表示层:主要负责数据格式的转换,如加密解密、转换翻译、压缩解压缩等。
- 会话层:负责在网络中的两节点之间建立、维持和终止通信,如服务器验证用户登录便是由会话层完成的。
- 传输层:向主机进程提供通用的数据传输服务。改成主要有两种协议(TCP:提供面向连接的、可靠的数据传输服务;UDP:提供无连接的、不保证可靠性的数据传输服务)。
- 网络层:选择合适的路由和交换节点,确保数据及时传送。主要包括IP协议。
- 数据链路层:将网络层传下来的IP数据包组装成帧,并在相邻节点的链路上传送帧。
- 物理层:实现相邻节点间比特流的透明传输,尽可能屏蔽传送介质和通信手段的差异。
1.11 TCP协议如何保证可靠性?
TCP主要提供了检验和、序列号/确认应答、超时重传、滑动窗口、拥塞控制和流量控制等方法实现了可靠性传输。
- 检验和:通过检验和的方式,接收端可以检测出数据是否有差错或异常,假如有差错就会直接丢弃TCP段,重新发送。
- 序列号/确认应答:序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并丢掉重复序列号的数据。TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文,这个ACK报文当中带有对应的确认序列号,告诉发送方接收到了哪些数据,下一次的数据从哪里发。
- 滑动窗口:滑动窗口既提高了报文传输的效率,也避免了发送方过多的数据而导致接收方无法正常处理的异常。
- 超时重传:超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。最大超时时间是动态计算的。
- 拥塞控制:在数据传输过程中,可能由于网络状态的问题,造成网络拥堵,此时引入拥塞控制机制,在保证TCP可靠性的同时提高性能。
- 流量控制:如果主机A一直向主机B发送数据,不考虑主机B的接受能力,则可能导致主机B的接收缓冲区满了而无法再接收数据,从而导致大量数据包丢失,引发重传机制。而在重传过程中,若主机B的接收缓冲区仍未好转,则会将大量的时间浪费在重传数据上,降低重传数据的效率。所以引入流量控制机制,主机B通过告诉主机A自己接收缓冲区的大小,来使主机A控制发送来的数据量。流量控制与TCP协议报头中的窗口大小有关。
具体过程:
- 对数据包进行校验
- 对失序的数据包进行重新排列
- 丢弃重复的数据包
- 应答机制:当接收方接收到数据后,会发送确认报文段
- 超时重传:当发送方发送数据后,会启动一个重传计时器,超时未收到确认,就会重新发送数据
- 流量控制:可以保证发送方发送速率不会太快,接收方缓冲区不会溢出
1.12 TCP的粘包与拆包
解释:
- TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送。
为什么会产生粘包和拆包呢?【具体情况】
- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
- 接受数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
- 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
- 待发送数据大与MSS(最大报文长度),TCP在传输前将进行拆包,即TCP报文段长度-TCP头部长度>MSS。
解决方法:
- 发送端将每个数据包封装为固定长度;
- 在数据尾部增加特殊字符进行分割;
- 将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。
1.13 TCP与UDP头部字节定义
TCP:
- 源端口号和目的端口号:用于多路复用/分解来自或送到上层应用的数据。告诉主机报文段来自哪里,传给哪个上层协议或应用程序。
- 序列号:该报文段首字节的的字节流编号,用来解决网络包乱序问题。确认应答号:对发送来的TCP报文段的响应,值是收到的TCP报文段的序号值加1,用来解决不丢包的问题。序列号和确认应答号都用于实现可靠数据传输。
- 首部长度:标识TCP头部有多少字节,最长60.
- 窗口大小:接收窗口,告诉对方本端TCP缓冲区还有多少空间可以接收数据,用来做流量控制。
- 标志字段:ACK用于指示确认应答号值是否有效,置1表示包含一个对方已成功接收报文段的确认;RST用于重置一个已经混乱的连接,或拒绝一个无效的数据段或者连接请求;SYN用于连接建立过程,请求建立一个连接;FIN用于断开连接,表示发送方没有数据要传输了。
UDP:
- 目标端口和源端口:主要是告诉UDP协议应该把报文发给哪个进程。
- 包长度:该字段保存了UDP首部的长度跟数据的长度之和。
- 校验和:校验和是为了提供可靠的UDP首部和数据而设计,接收方使用检验和来检查该报文段中是否出现差错。
1.14 三次握手的流程?
假设A为客户端,B为服务器端。
首先B处于LISTEN(监听)状态,等待客户的连接请求。
A向B发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号:seq=x。
B收到连接请求报文,如果同意建立连接,则向A发送连接确认报文,SYN=1,ACK=1,确认号为ACK=x+1,同时也选择一个初始的序号seq=y。
A收到B的连接确认报文后,还要向B发出确认,确认号为ACK=y+1,序号为seq=x+1。
说明:
- SYN=1报文段不懈怠数据但要消耗掉一个序号
- ACK报文可以携带数据,不携带数据则不消化序号
- 在socket编程中,客户端执行connect()时,将触发三次握手
1.15 三次握手的原因?
从浪费资源的角度:
三次握手可以防止已经失效的连接请求报文突然又传输到服务器端导致服务器资源浪费。例如,客户端先发送了一个SYN,但由于网络阻塞,该SYN数据包在某个节点长期滞留。然后客户端又重传SYN数据包并正确建立TCP连接,然后传输完数据后关闭该连接。该连接释放后失效的SYN数据包才到达服务器端。在二次握手的前提下,服务器端会认为这是客户端发起的又一次请求,然后发送SYN,并在服务端创建socket套接字,一直等待客户端发送数据。但是由于客户端并没有发起新的请求,所以会丢弃服务端的SYN。此时服务器会一直等待客户端发送数据从而造成资源浪费。
可靠性角度:
本质是信道不可靠,但通信的双方需要就某个问题达成一致,三次通信是保证双方互相明确对方能收发的最小值;理论上讲不论握手多少次都不能确认一条信道是可靠的,但通过三次握手可以至少确认它是可用的,再加上握手次数不过是提高“它是可用的”这个结论的可信程度。另外TCP的可靠传输更多的是靠重传机制来保证的。
从初始序列号角度【本质】:
三次握手的本质是为了同步双方的初始序列号:三次握手的过程即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值的必经步骤。如果只是两次握手,之多只有连接发起方的起始序列号能被确认,另一方选择的序列号则得不到确认;TCP的三次握手是优化的结果,起始他应该是四次握手,由于是从零开始的建立连接,因此将SYN的ACK以及被动打开的SYN合并成了一个SYN-ACK。
握手的作用:
旨在确定两个双向的初始序列号,TCP用序列号来编址传输的字节,由于是两个方向的连接,所以需要两个序列号,握手过程不传输任何字节,仅仅确定初始序列号。
1.16 三次握手是否可以携带数据?
其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据。
原因如下:
假如第一次握手可以携带数据,如果有人要恶意攻击服务器,那它每次都在第一次握手中的SYN报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂重复发SYN报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击。二对于第三次的话,此时客户端已处于ESTABLISHED状态。对于客户端来说,他已经建立连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以可以携带数据。
1.17 HTTP长连接和短连接的区别?
长连接:
- 在HTTP1.1中默认使用长连接。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
短连接:
- 在HTTP1.0中默认使用短连接。也就是说,客户端和服务器进行一次HTTP操作,就建立一次连接,任务结束就中断连接。
1.18 HTTP
HTTP请求:
请求由三部分组成,分别是:请求行、请求头(首部行)、请求正文。
HTTP请求方法:
- GET请求读取一个Web页面。
- HEAD请求读取一个Web页面的首部。
- POST附加一个命名资源(如Web页面)。
- PUT请求存储一个Web页面。
- DELETE删除Web页面。
- TARCE用于测试,要求服务器送回收到的请求。
- CONNECT用于代理服务器。
- OPTION查询特定选项。
一次HTTP请求的过程:
- 根据域名和DNS解析到服务器的IP地址(DNS+CDN)。
- 通过ARP协议获得IP地址对应的物理机器的MAC地址。
- 浏览器对服务器发起TCP三次握手。
- 建立TCP连接后发起HTTP请求报文。
- 服务器响应HTTP请求,将响应报文返回给浏览器。
- 短连接情况下,请求结束则通过TCP四次挥手关闭连接,长连接在没有访问服务器的若干时间后,进行连接的关闭。
- 浏览器得到响应信息中的HTML代码,并请求HTML代码中的资源(如js、css、图片等)。
- 浏览器对页面进行渲染并呈现给用户。
1.19 UDP中使用connect的好处?
(1)UDP可以是一对一,多对一,一对多,或者多对多的通信,所以每次调用sendto()/recvfrom()时都必须指定目标IP和端口号。通过调用connect()建立一个端到端的连接,就可以和TCP一样使用send()/recv()传递数据,而不需要每次都指定目标IP和端口号。没有三次握手的过程。但限定了这个socket的使用范围:只允许从指定地址上获得数据包+向指定地址发送数据包(只限制了一方)。
(2)还可以通过在已建立连接的UDP套接字上,再次调用connect()实现以下功能:
- 指定新的IP地址和端口号。
- 断开连接。
这也与TCP有所不同,TCP套接字只能调用一次connect()函数。
1.20 DNS协议
DNS协议是用来将域名转换为IP地址(也可以将IP地址转换为响应的域名地址)。IP地址是面向主机的,而域名则是面向用户的。域名服务主要是基于UDP实现的,服务的端口号为53。
解析过程:
三种类型的DNS服务器,根DNS服务器,顶级域DNS服务器和权威DNS服务器。
- 1、首先浏览器会检查浏览器自身的DNS缓存中是否有域名对应的DNS缓存,没有的话进入第二步,否则解析完成。
- 2、接下来去查看系统的hosts文件(C:\Windows\System32\drivers\etc)是否有域名对应的IP地址,如果找到则停止解析,否则进入第三步。
- 3、浏览器发起DNS系统调用,向本地配置的首选DNS服务器发起域名解析请求(通过UDP协议向DNS的53端口发起请求)。
- 4、首先请求会在运营商的DNS服务器(本地服务器)上进行请求,如果找到对应的条目,且没有过期,则解析成功,否则进入第五步。
- 5、运营商的DNS服务器根据解析请求,迭代查询,首先找到根域名服务器的IP地址,然后找到根域的DNS地址,发送请求。
- 6、根域服务器收到请求后,根据域名,返回对应的顶级域的服务器IP地址,并返回给运营商DNS服务器。
- 7、运营商DNS服务器接收到根域名服务器传回来的顶级域名服务器IP地址后,向顶级域名服务器发送请求。
- 8、顶级域名服务器接收到请求后,返回该域名对应的权威域名服务器的IP地址,并返回给运营商DNS服务器。
- 9、运营商DNS服务器获得权威域名服务器的响应信息后,返回给请求的主机,DNS解析完成。
- 10、DNS主要是通过UDP通信,报文结构主要分为头部Header、查询部分Question、应答部分Answer/Authority/Addition。
1.21 HTTP与HTTPS
(1)HTTP和HTTPS的基本概念
- HTTP:是一个客户端和服务器端请求和应答的标准(TCP),用于从www服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。
- HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。HTTPS的协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全,另一种就是确认网站的真实性。
(2)HTTP与HTTPS有什么区别
HTTP协议传输的数据都是加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。简单来说,HTTPS协议是由SSL+HTTP协议构建的可以进行加密传输、身份认证的网络协议,要比HTTP协议安全。
HTTPS和HTTP的主要区别如下:
- HTTPS协议需要到ca申请证书,一般免费证书较少,因此需要一定费用。
- HTTP是超文本传输协议,信息是明文传输,HTTPS则是具有安全性的SSL加密传输协议。
- HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443.
- HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构成的可进行加密传输、身份认证的网络协议,比HTTP安全。
(3)HTTPS的工作原理
- 客户端使用HTTPS的URL访问Web服务器,要求与Web服务器建立SSL连接。
- Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
- 客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
- 客户端的浏览器根据双方统一的安全等级建立会话秘钥,然后利用网站的公钥将会话秘钥加密,并传送给网站。
- Web服务器利用自己的秘钥解密出会话秘钥。
- Web服务器利用会话秘钥加密与客户端之间的通信。
2.22 TCP状态图
- LISTEN:侦听来自远方的TCP端口的连接请求。
- SYN-SENT:在发送连接请求后等待匹配的连接请求(客户端)。
- SYN-RCVD:在收到和发送一个连接请求后等待对方连接请求的确认(服务器)。
- ESTABLISHED:代表一个打开的连接。
- FIN-WAIT-1:等待远程TCP连接中断请求,或先前的连接中断请求的确认。
- FIN-WAIT-2:从远程TCP等待连接中断请求。
- CLOSE-WAIT:等待从本地用户发来的连接中断请求。
- CLOSING:这种状态在实际情况中应该很少见,属于一种比较罕见的例外状态。正常情况下,当一方发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示一方发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?那就是当双方几乎在同时close()一个SOCKET的话,就出现了双方同时发送FIN报文的情况,这是就会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
- LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认。
- TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认。
- CLOSED:没有连接状态。
二、操作系统
2.1 怎么处理死锁?
(1)抢占资源:从一个或多个进程中抢占足够数量的资源,分配给死锁进程,已解除死锁状态。
(2)终止进程:终止系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态解脱出来。
2.2 进程调度策略
(1)先来先服务
(2)短作业优先
(3)最短剩余时间优先
(4)时间片轮询
(5)优先级调度
(6)多级反馈队列
2.3 进程间通信方式
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
2.4 线程间通信方式
线程间可以通过共享内存或基于网络来进行通信。
(1)通过共享内存进行通信:需要考虑并发问题,什么时候阻塞,什么时候唤醒。一个进程里的两个线程或一个机器里的两个线程,可以用共享内存进行通信。
(2)通过网络进行通信:通过网络连接将数据发送给对方,当然也要考虑并发问题,处理方式就是加锁等,两个机器之间的两个线程通信用网络通信。
2.5 同步、异步I/O的本质区别?
同步是阻塞模式,异步是非阻塞模式。
(1)同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到返回信息才继续执行下去。
(2)异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行效率。
2.6 进程与线程的区别?
基本概念:
- 进程是对运行时程序的封装,操作系统进行资源调度和分配的基本单位。
- 线程是进程的子任务,是CPU调度和分配的基本单位。同一个进程内多个线程之间可以并发运行,同时多个线程之间共享代码段、数据段、打开的文件资源等,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
区别:
- 调度:线程是调度的基本单位(PC,状态码,通用寄存器,线程及栈指针);进程是资源拥有和分配的基本单位(打开文件,堆,静态区,代码段等)。
- 并发性:一个线程只能属于一个进程,而一个进程可以有多个线程;进程间不会相互影响,一个线程崩溃将导致整个进程崩溃。
- 拥有资源:进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆)。但每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量)。
- 系统开销:线程比进程的开销小,线程的创建和终止比进程快,进程需要管理和释放的资源多,开销较大;线程共享进程的内存,线程之间数据传输更快,进程切换开销更小。
2.7 引入协程的原因?
协程概念:
- 本质是用户空间下的线程;
- 拥有自己的寄存器上下文和栈;
- 切换情况:先将寄存器上下文和栈保存,等切换回来时在进行恢复。
原因:
- 一是节省CPU,避免系统内核级的线程频繁切换,造成CPU资源浪费。二协程是用户态的线程,用户可以自行控制协程的创建与销毁,极大程度上避免了系统级线程上下文切换造成的资源浪费。
- 二是节约内存,在64位的Linux中,一个线程需要分配8M栈内存和64M堆内存,系统内存的制约无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。
- 三是稳定性,前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。
- 四是开发效率,使用协程在开发过程中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时IO请求等。
2.8 典型锁
A.互斥锁(mutex)–线程锁
- 一次只能一个线程有用互斥锁,其它线程只有等待。
线程间互斥机制:
- 属于sleep-waiting类型的锁。互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。
B.条件变量(cond)–线程锁
- 条件变量分为两部分:条件和变量。条件本事是由互斥量保护的,线程在改变条件状态前要先锁住互斥量,它利用线程间共享的全局变量进行同步的一种机制。
- 用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,知道某个特殊情况发生为止。
- 条件变量通过允许线程阻塞和等待另一个线程发送信号的方法补充了互斥锁的不足,也常和互斥锁一起使用,以免出现静态条件。
- 当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。
- 一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。
C.自旋锁(spin)–线程锁
- 属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,如果自旋锁已经被线程B所持有,那么线程A就会一直在core
0上进行忙等待并不停的进行锁请求,检查该自旋锁是否已经被线程B释放,知道得到这个锁为止。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。
缺点:
- 自旋锁一直占用CPU,在未获得锁的情况下,一直进行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,无疑会使CPU效率降低。
- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。
- 自旋锁只有在内核可抢占式或SMP的情况下才能真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持时间比较短的情况。
D.读写锁
- 多个读者可以同时进行读
- 写者必须互斥(只允许有一个写者写,也不能读者写者同时进行)
- 写者优先读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
2.9 死锁相关问题
死锁:
- 两个(多个)线程相互等待对方数据的过程,死锁的产生会导致程序卡死,不解锁程序将永远无法进行下去。在两个或多个并发进程中,如果每个进程持有某种资源而又等待着别的进程释放它或它们现在保持的资源,在为改变这种状态前都不能向前推进。
产生死锁的原因:
- 系统能够提供的资源比要求该资源的进程数少。
产生死锁的必要条件:
- 互斥条件。多个线程不可同时使用同一个资源。
- 不剥夺条件(非抢占)。当线程已经持有了资源,在自己使用完之前不能被其它线程获取。
- 持有并等待(部分分配)。A在获取资源1,又想申请资源2,而此时资源2被C持有,A会进入等待状态,但是A在等待的时候不会释放自己持有的资源1。
- 环路等待条件(循环等待)。在死锁发生时,两个线程获取资源的顺序构成了环形链。
死锁的预防:确保系统永远不会进入死锁状态。
(1)最常见的并且可行的就是使用资源有序分配法,来破坏环路等待条件。有序会让资源的执行速度变慢。
(2)破坏“持有并等待”条件
- 方法一:静态分配,每个进程在开始执行时申请它所需要的全部资源。简单安全,但降低了资源利用率,会发生饥饿现象。
- 方法二:允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
(3)破坏“不可剥夺”条件
- 一个进程不可获得其所需要的全部资源便处于等待状态,等待期间它占用的资源将被隐式的释放重新加入到系统的资源列表中,可以被其它进程使用。
- 而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
- 复杂且代价大,反复申请和释放会延长进程的周转周期,还会影响系统的吞吐量。
死锁避免:在使用前进行判断,只允许不会产生死锁的进程申请资源。
死锁避免是利用额外的检验信息,在分配资源时判断是否会出现死锁,只在不会出现死锁的情况下才分配资源。
两种避免方法:
- 如果一个进程的请求会导致死锁,则不启动该进程。
- 如果一个进程的增加资源请求会导致死锁,则拒绝该申请。
死锁的检测与解除:在检测到运行系统进入死锁,进行恢复。
常用的解除死锁的方法有:
- 抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态。
- 终止(或撤销)进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。
2.10 乐观锁和悲观锁
A.概念:
乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。
- 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果修改了数据则放弃操作,否则执行操作。
- 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
B.区别:
- 悲观锁的实现方式是加锁,加锁既可以是对代码块加锁,也可以是对数据加锁。
- 乐观锁的实现方式主要有两种:CAS机制和版本号机制。
C.应用场景:
(1)功能限制:
- 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
(2)竞争激烈程度:
- 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度。
- 当竞争不激烈(出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其它线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
- 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
2.11 系统调用与库函数的区别?
系统调用:
- 操作系统提供了一组特殊接口—系统调用。通过这组接口用户程序可以使用操作系统内核提供的各种功能。例如分配内存、创建进程、实现进程间通信等。
- 用户程序向操作系统提出请求的接口就是系统调用。所有的操作系统都会提供系统调用接口,只不过不同的操作系统提供的系统调用接口各不相同。
- 系统调用按功能分类:进程控制、进程间通信、文件系统控制、存储管理、网络管理、套接字控制、用户管理等。
库函数:
- 对系统调用的一种封装,因为系统调用是面对的是操作系统,包括Linux、Windows等,如果直接系统调用,会影响程序的移植性,所以使用了库函数。
用户编程接口API:
- 前面提到利用系统调用接口程序可以访问各种资源,但在实际开发中程序并不直接使用系统调用接口,而是使用用户编程接口(API)【各种库(最重要的就是C库)中的函数】。
不直接使用系统调用接口原因如下:
- 系统调用接口功能非常简单,无法满足程序的需求。
- 不同操作系统的系统调用接口不兼容,程序移植时工作量大。
2.12 页面调度算法
为什么要进行页面调度?
举个例子:假设某一时刻内存页已经被写满了,但这时又需要将一个页写到物理内存中,就需要将原本在物理内存中的某一页换出来。如果置换不当,就会导致刚刚被写入到内存的内容又被换出到硬盘中,减慢系统运行的速度。页面置换算法就是考虑将哪一页换出来以获得优良性能的方法。
1、最优算法
- 首先介绍最优算法,它需要知道以后要被用到的页,然后将不会被用到的页换出内存;如果所有页都会被用到,就把需要使用时间离现在最长的页换出,以尽量使不好的情况晚发生。这种方法能使系统获得最佳性能,但是,它是不可能实现的…因为当前无法获知以后哪些页要被用到。不过最优算法还是能够作为其他算法优秀程度的衡量。
2、先进先出算法(FIFO)
- FIFO算法的思想很简单,就是置换出当前已经待在内存里时间最长的那个页。FIFO算法的运行速度很快,不需要考虑其他的因素,需要的开销很少。但是正是由于没有考虑页面的重要性的问题,FIFO算法很容易将重要的页换出内存。
3、最近最少使用算法(Least Recently Used,LRU)
- 为获得对最优算法的模拟,提出了LRU算法。由于当前时间之后需要用到哪些页无法提前获知,于是记录当前时间之前页面的使用情况,认为之前使用过的页面以后还会被用到。在置换时,将最近使用最少的页面换出内存。此种方法的开销比较大。
2.13 进程调度算法
进程调度:在操作系统中调度是指一种资源分配。
调度算法是指: 根据系统的资源分配策略所规定的资源分配算法。
操作系统管理了系统的有限资源,当有多个进程(或多个进程发出的请求)要使用这些资源时,因为资源的有限性,必须按照一定的原则选择进程(请求)来占用资源。这就是调度。目的是控制资源使用者的数量,选取资源使用者许可占用资源或占用资源。
1、先来先服务
- 如果早就绪的进程排在就绪队列的前面,迟就绪的进程排在就绪队列的后面,那么先来先服务(FCFS: first come first service)总是把当前处于就绪队列之首的那个进程调度到运行状态。也就说,它只考虑进程进入就绪队列的先后,而不考虑它的下一个CPU周期的长短及其他因素。
2、时间片轮转法
- 系统将所有的可运行(即就绪)进程按先来先服务的原则排成一个队列,每次调度时把CPU分配给队首进程,并令其执行一个时间片。当执行的时间片用完时,将它送到队列末尾等待下一次执行。
3、优先权调度算法
- 为了照顾到紧迫型进程在进入系统后便能获得优先处理,引入了最高优先权调度算法。系统把CPU分配给运行队列中优先权最高的进程(想到了STL中的优先级队列),这时又可以把该算法分成两种方式:
- 非抢占式优先权算法(又称不可剥夺调度)。在这种方式下,系统一旦将CPU分配给运行队列中优先权最高的进程后,进程便一直执行下去,直至完成。这种调度算法主要用在批处理系统中。
- 抢占式优先权调度算法(又称可剥夺调度)。在这种方式下,系统同样把CPU分给优先权最高的进程,但是,当出现另一个优先权更高的进程后,就暂停原优先权最高的进程,而将CPU给新出现的优先权最高的进程。采用这种调度算法时,每当出现新的可运行进程时,就将它与当前运行进程进行优先权比较。这种方式的调度算法能更好的处理紧迫型进程的要求,常用于实时性要求较高的系统中,Linux目前就采用这种方式。
4、多级反馈队列算法
- 这是一种折中的调度算法,本质是总和了时间轮转调度和抢占式优先权调度的优点,即优先权高的进程先运行给定的时间片,相同优先权的进程轮流运行给定时间片。
2.14 Linux的内存管理机制?
- 程序文件段,包括程序的二进制可执行代码,只读;TEXT
- 已初始化数据段,包括已初始化的全局变量和静态常量;DATA
- 未初始化数据段,包括未初始化的静态变量和全局变量;BSS
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关);
- 栈段,包括函数的参数和局部变量、函数调用的上下文等。栈的大小是固定的,一般是8MB。当然系统也提供了参数,以便我们自己定义大小;栈区是从高地址位向低地址位增长的。
2.15 Linux虚拟内存
- 对32位处理器,虚拟内存空间为4G,每个进程都认为自己拥有4G的空间,实际上,在虚拟内存对应的物理内存上,可能只对应一点点的物理内存。
- 进程得到的这4G内存是一个连续的地址空间(这也是进程认为),实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。
- 由于存在两个内存地址,因此一个应用程序从编写到被执行,需要进行两次映射。第一次是映射到虚拟内存空间,第二次映射到物理内存空间。在计算机系统中,第二次映射的工作是由硬件和软件共同来完成的。承担这个任务的硬件部分叫存储管理单元MMU,软件部分就是操作系统的内存管理模块了。
2.16 如何处理虚拟地址和物理地址的关系?
(1)内存分页:
分页就是把整个虚拟和物理内存切成一段段固定大小的空间,连续且尺寸固定的内存空间叫页,Linux下每一页大小为4KB。虚拟内存和物理内存之间通过页表来映射。当进程访问的虚拟地址在页表中查不到时,系统就会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
- 采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存,解决了内存碎片的问题。
- 内存空间不足时,操作系统将正在运行的进程中,最近没使用的内存页面释放(暂时写入硬盘)需要的时候再加载进来。一次性只有少数的页,解决了交换效率低的问题。
- 分页使我们在加载程序时,不用一次性加载到物理内存,可以只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
虚拟地址分为:页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址。
(2)简单的分页缺陷:
- 空间上的缺陷:32位,单进程一个页4KB,虚拟内存4G,就有2的20次方个页,一个页表项4字节,结果是4G空间映射需要4*2^20=4MB存储页表。多进程100,需要400MB。
- 以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有100多万个页表项来映射,二级分页则需要1024个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。
(3)段页式内存管理:
地址结构就由段号、段内页号和页内位移三部分组成。
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。
可用软、硬件相结合的方式实现段页式地址变换,这样虽然增加了硬件成本和系统开销,但提高了内存的利用率。
2.17 Linux信号
信号是用户、系统、进程发送给目标进程的信息,通知某个状态的改变或系统异常。
2.18 什么是线程池?
- 1、对网络服务器,单位时间内必须处理数目巨大的连接请求,但是处理时间却是比较短的,系统不断的启动和关闭新线程,成本高,会消耗系统资源,以及带来切换线程的危险,从而可能导致系统资源的崩溃。这时我们可以使用线程池。
- 2、线程池是服务器预先创建的一组空闲线程,它们的集合称为线程池;这些线程都是出于阻塞状态,这些线程只占用一点内存,不占用CPU。
- 3、任务到来的时候,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。减少创建和销毁对象的次数,提高程序的运行效率。
- 4、应用范围:
-
- (1)需要大量的线程来完成任务,且完成任务的时间比较短,不适合具有可能会长时间运行(并因此阻塞其它任务)的任务。
-
- (2)对性能要求较为苛刻的应用,比如要求服务器迅速响应客户请求。
2.19 多进程与多线程
2.20 进程异步、同步和互斥
同步:
发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。对于多个进程的同步而言:表明多进程存在直接制约关系,有一定的先后执行顺序。
进程同步方式:
- 临界区:对临界资源进行访问的那段代码【为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查】
- 信号量:是一个整型变量。对其进行down和up操作;即为常见的P和V操作,down和up操作被设计成原语,不可分割。【down:大于0执行-1;等于0进程休眠,等待信号量大于0。up:信号量+1操作,唤醒睡眠的进程让他完成down操作】
- 信号量取0和1即为互斥量【0:表示临界区已经给加锁;1:表示临界区解锁】
异步:
当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。
- 状态:即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。
- 通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。
- 回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。
同步与异步的区别:请求发出后,是否需要等待结果,才能继续执行其他操作。
互斥:
- 多个进程在同一时刻只有一个进程能进入临界区【对临界资源进行访问的一段代码】。
2.21 守护进程
守护进程是指在后台运行的,没有控制终端与他相连的进程。他独立于控制终端,周期性的执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等。
创建守护进程的要点:
- 让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。
- 调用setsid()创建一个新对话期。守护进程需要摆脱父进程的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登陆会话、进程组合控制终端脱离。
- 禁止进程重新打开控制终端。经过前两步,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。
- 关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸载以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
- 将当前目录更改为根目录。
- 子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
- 处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程,从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单的将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。
2.22 孤儿进程
- 在一个启动的进程中创建子进程,这时候父子进程同时运行,但是父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称为孤儿进程(跟现实是一样的)。
- 操作系统是非常关爱运行的每一个进程的,当检测到某一个进程变成了孤儿进程,这时候系统中就会有一个固定的进程领养这个孤儿进程(有干爹)。如果使用Linux没有桌面终端,这个领养孤儿进程的就是init进程(PID=1),如果有桌面终端,这个领养孤儿进程的就是桌面进程。
- 系统为什么要领养孤儿进程呢?在子进程退出的时候,进程中的用户区可以自己释放,但是进程内核区的pcb资源自己无法释放,必须要由父进程来释放子进程的pcb资源,孤儿进程被领养之后,这件事干爹就可以代劳了,这样可以避免系统资源的浪费。
2.23 僵尸进程
- 在一个启动的进程中创建子进程,这时候就有了父子两个进程,父进程正常运行,子进程先于父进程结束,子进程无法释放自己的PCB资源,需要父进程来做这个事,但是如果父进程也不管,这时候子进程就变成了僵尸进程。
- 僵尸进程不能将它看成是一个正常的进程,这个进程已经死亡了,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。
- 僵尸进程的出现是由于这个已死亡的进程的父进程不作为造成的。
- 消灭僵尸进程的方法是,杀死这个僵尸进程的父进程,这样僵尸进程的资源就被系统回收了。
2.24 select、poll、epoll
多路复用接口select/poll/epoll是内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件。
(1)select
- 把已连接的socket放在一个文件描述符集合,调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生。
- 通过遍历,有事件产生就把此socket标记为可读/可写,然后再整个拷贝回用户态,用户态还需要遍历找到刚刚标记的socket。两次遍历+两次拷贝。
(2)poll
- 动态数组,以链表形式来组织,突破了select的文件描述符个数限制,当然还会受到系统文件描述符的限制。
(3)epoll
- 在内核里使用红黑树来跟踪进程所有待检测的文件描述符。把需要监控的socket通过epoll_ctl()函数加入到内核中的红黑树里。红黑树的增删查时间复杂度是O(logN),不需要每次操作都传入整个集合,只需要传入一个待检测的socket。减少了内核和用户空间的大量数据拷贝和内存分配。
- epoll使用事件驱动的机制,内核里维护了一个链表来记录就绪事件。当某个socket有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用epoll_wait()函数时,只会返回有事件发生的文件描述符的个数,不需要想select/poll那样轮询扫描这个socket集合,大大提高了检测效率。
2.25 epoll有哪些触发模式,有啥区别?
Epoll支持的事件触发模式:边缘触发ET和水平触发LT。
- 使用边缘触发模式时,当被监控的socket描述符上有可读事件发生时,服务器端只会从epoll_wait中苏醒一次,即使进程没有调用read函数从内核读取数据,也依然只能苏醒一次,因此我们程序要保证一次性将内核缓冲区中的数据读取完。
- 使用水平触发模式时,当被监控的socket上有可读事件发生时,服务器端不断的从epoll_wait中苏醒,直到内核缓冲区数据被read函数读取完才结束,目的是告诉我们有数据需要读取。
水平触发的意思是只要满足事件的法身条件,比如内核中有数据需要读,就一直不断的把这个事件传递给用户;边缘触发的意思是只有一次满足条件的时候才触发,之后就不会再传递同样的事件了。
2.26 select和epoll的区别
- Select和poll采用轮询的方式检查就绪事件,每次都要扫描整个文件描述符,复杂度O(N);epoll采用回调方式检查就绪事件,只会返回有事件发生的文件描述符的个数,时间复杂度O(1)。
- Select只工作在低效的LT模式,epoll可以在ET高效模式工作。
- Epoll时Linux所特有,而select一般操作系统均有实现。
- Select单个进程可监听的文件描述符fd数量被限制,即能监听端口的大小有限,64位是2048;epoll没有最大并发连接的限制,能打开的fd上限远大于2048.
- Select内核需要将消息传递到用户空间,都需要内核拷贝动作;epoll通过内核和用户空间共享一块内存来实现。
三、C++
3.1 delete和delete[]的区别?
delete 释放new分配的单个对象指针指向的内存
delete[] 释放new分配的对象数组指针指向的内存
3.2 深拷贝与浅拷贝
(1)浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针资源就会出现错误。
(2)深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,及时原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话就需要自己实现深拷贝。
3.3 inline和define的区别
主要区别:
- 宏在预编译时进行,只做简单的字符串替换。
- 内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并进行参数类型检查,具有返回值,可以实现重载。
内敛函数适用场景:
- 使用宏定义的地方都可以使用inline函数。
- 作为类成员接口函数来读写类的私有成员或保护成员,会提高效率。
为什么不能把所有函数写成内联函数:
- 函数体内的代码比较长,将导致内存消耗大。
- 函数体内有循环,函数执行时间要比函数调用开销大。
3.4 指针与引用的区别?
- 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是一个东西,是原变量的别名;
- 指针可以有很多级,引用只有一级;
- 指针可以为空,引用不能为NULL且在定义时必须初始化;
- 指针在初始化后可以改变指向,而引用在初始化后不可再改变;
- sizeof指针得到的是本指针的大小,sizeof引用的到的是引用所指向变量的大小;
- 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用可以;
- 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间;
- 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname&
varname形式;指针声明和定义可以分开,可以先只声明指针变量二部初始化,等用到时在指向具体变量; - 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
3.5 volatile关键字
定义:
- 【与const绝对对立的,是类型修饰符】影响编译器编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。
作用:
- 指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问。
3.6 extern关键字
定义:
- 声明外部变量【在函数或者文件外部定义的全局变量】。
3.7 static关键字与const关键字
static:
- 实现多个对象之间的数据共享+隐藏,并且使用静态成员还不会破坏隐藏原则;默认初始化为0。
const:
- 指针常量(const int* p = new int(2))和常量指针(int* const p = new
int(2))【区别方法:左定值,右定向】 - 拓展:顶层const:指针本身是常量;底层const:指针所致对象是常量。
- 若要修改const修饰的变量值,需要加上关键字volatile;若想修改const成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员。
3.8 重载(overload)与重写(override)
概念:
- 重载:指在同一范围定义的同名成员函数才存在重载关系。(函数名相同,参数类型和数目不同;重载和函数成员是否是虚函数无关)。
- 重写:在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数,且与基类的虚函数有相同的参数个数、参数类型、返回值类型。
区别:
- 本质区别:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性【不同函数之间的水平关系】,而后者实现的是运行时的多态性【不同函数之间的垂直关系】。
- 发生的位置:重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法具有相同的参数列表,有兼容的返回类型,比父类重写方法更好访问,不能比父类重写方法声明更多的异常。
- 返回类型:重载对返回类型没有特殊的要求,不能根据返回类型进行区分。
- 调用:重载中,根据调用是实参表与形参表的对应关系来选择函数体;重写中,调用方法根据对象类型决定。
3.9 智能指针
原理:
- 智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。
- RALL(资源获取即初始化):构造函数中申请资源,在析构函数中释放资源。基于该原理使用类来管理资源,将资源和对象的生命周期绑定。
常用的智能指针:
(1)shared_ptr
实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候回自动释放动态分配的资源。
- 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针。
- 每次创建类的新对象时,初始化指针并将引用计数置为1。
- 拷贝:当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针,并使得对象的引用计数加1。
- 赋值:对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数减为0,则删除对象),并增加右操作数所指对象的引用计数。
- 调用析构函数时,构造函数减少引用计数(如果引用计数减为0,则删除对象)。
(2)unique_ptr
unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的对象拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁)。
如果你拷贝一个unique_ptr,那么拷贝构造结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。
(3)weak_ptr
weak_ptr:弱引用。引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放,使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的声明周期,也就是说它只引用,不计数。
如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
(4)auto_ptr
主要是为了解决“有异常抛出时发生内存泄漏”的问题。因为发生异常而无法正常释放内存。
原理:构造时取得某个对象的控制权,在析构时释放该对象。创建auto_ptr类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放。
- auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为auto_ptr的构造;
- auto_ptr对象析构时会删除它所有的指针,所以使用时避免多个auto_ptr对象管理同一指针;
- auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],即auto_ptr不能管理数组;
- auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移;
- auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,所以不能在STL中使用。
3.10 vector与list的区别与应用?
vector:
- vector动态数组,容量动态增长;内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n);当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。
list:
- 双向链表,内存空间是不连续的;只能通过指针访问。
区别即是数组与链表在增删改查上的区别。
3.11 STL中vector删除元素迭代器如何变化?为什么是两倍扩容?释放空间?
size():已用空间大小,capacity():总空间大小。size()和capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。
由于动态增长会引起重新分配内存空间、拷贝原空间、释放空间,这些过程会降低程序效率。因此,可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。
vector 的reserve增加了vector的capacity,但是它的size没有改变!而resize改变了vector的capacity同时也增加了它的size!
原因如下:
- reserve是容器预留空间,但在空间内不真正创建元素对象,所以在没有添加新的对象之前,不能引用容器内的元素。加入新的元素时,要调用push_back()/insert()函数。
- resize是改变容器的大小,且在创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。此时再调用push_back()函数,是加在这个新的空间后面的。
- 两个函数的参数形式也有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,
第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。
两倍扩容的解释:不同的编译器,vector有不同的扩容大小。在vs下是1.5倍,在GCC下是2倍;空间和时间的权衡,简单来说,空间分配的多,平摊时间复杂度低,但浪费空间也多。
vector如何释放空间?
vector的内存占用空间只增不减,所有内存空间是在vector析构时才能被系统回收。【使用erase删除元素并不会改变空间的大小】可以使用swap函数来对空对象交换实现释放空间。vector().swap(v)。
3.12 STL迭代器如何实现?
- 顺序容器(序列式容器,如vector,deque)erase迭代器不仅使所指向被删除的迭代器失效,而且使被删除元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;it=c.erase(it);
- 关联容器(关联式容器,如map,set等)erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器。
3.13 map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器?为什么用红黑树?
- 他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高效的插入删除。
- 这里定义了一个模板参数,如果它是key那么它就是set,如果它是map,那么它就是map;底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value。
- 因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度较低。
3.14 map和set的底层为什么使用红黑树而不是AVL树?即红黑树与AVL树的区别?
- 红黑树不追求“完全平衡”,即不像AVL那样要求节点的高度差<=1,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增删节点时,根据不同情况,旋转次数会比红黑树多。
- 就插入节点导致树失衡的情况,AVL和红黑树都是最多两次树旋转来实现复衡,旋转量级是O(1)。删除节点导致失衡,AVL需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转量级为O(logN),而红黑树最多只需要旋转3次实现复衡,只需要O(1),所以说红黑树删除节点的效率更高。
- AVL的结构相较于红黑树更为平衡,因此AVL的查找效率更高。
总结:
- 引入红黑树是功能、性能、空间开销的折中结果。红黑树有着更良好的稳定性和完整的功能,性能表现不错,在诸如STL的场景中有着稳定的表现。
- 实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL;如果搜索,插入删除次数几乎差不多,应选择红黑树。
3.15 你知道Top K问题有哪些解决方法?
- 堆排序。借助STL中的优先级队列(priority_queue)。
- 快排。参考快排的思想,一次遍历使得左边的数都比他小,右边的数都比他大,看比他大的数的个数与K的关系。如果比K大继续往右边找,如果比K小往左边找,如果干好等于K就退出。这样不需要对整个数组进行排序,只对部分进行排序即可。
3.16 虚函数的开销问题
(1)表面上开销
空间开销:
- 包含虚函数的类生成一个虚函数表,导致程序的二进制文件大小会相应增大。
- 类的实例包含一个虚函数表指针,致使对象实例化空间占用增加一个指针大小【32位系统,4个字节】。
时间开销:
- 增加了一次内存寻址,通过虚函数表指针找到虚函数表,随对程序性能有一些影响,但是影响并不大。
(2)背后的开销【更深入的解释】
- 从汇编生成的代码来看,普通函数与虚函数的区别是,普通函数是一个直接调用,而虚函数是一个间接调用,直接调用与间接调用的区别就是跳转地址是否确定,直接调用的跳转地址是编译器确定的,而间接调用是运行到该指令时从寄存器中取出地址然后跳转。
- 直接调用而言,是不存在分支跳转的,因为跳转地址是编译器确定的,CPU直接去跳转地址取后面的指令即可,不存在分支预测,这样可以保证CPU流水线不被打断。
- 而对于间接寻址,由于跳转地址不确定,所以此处会有多个分支可能,这个时候需要分支预测器进行预测,如果分支预测失败,则会导致流水线冲刷,重新进行取值、译码等操作,对程序性能有很大影响。
3.17 四种类型转换
(1)static_cast:
- 可以实现C++中内置基本数据类型之间的相互转换,enum、struct、int、char、float等。它不能进行无关类型(如非基类和子类)指针之间的转换。
(2)const_cast:
- const_cast操作不能在不同的种类间转换。相反,它仅仅把一个它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。
(3)reinterpret_cast:
- (interpret是解释的意思,reinterpret即为重新解释,此标识符的意思即为数据的二进制形式重新解释,但是不改变其值。)有着和C风格的强制转换同样的能力。它可以转化任何内置的数据类型为其他任何的数据类型,也可以转化任何指针类型为其他的类型。它甚至可以转化内置的数据类型为指针,无须考虑类型安全或者常量的情形。不到万不得已绝对不用。
(4)dynamic_cast:
- 其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。
- 不能用于内置的基本数据类型的强制转换。
- dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。
- 使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,只有定义了虚函数的类才有虚函数表。
- 在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换 时,dynamic_cast具有类型检查的功能,比static_cast更安全。向上转换即为指向子类对象的向下转换,即将父类指针转化子类指针。向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。
四、数据库
4.1 数据事务的四大特性(ACID)
- 原子性:事务的最小工作单元,要么全成功,要么全失败。
- 一致性:事务开始和结束后,数据库的完整性不会被破坏。
- 隔离性:不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE(串行化)。
- 持久性:事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失。
4.2 union和join区别?
Union:
- 操作对象:两个或多个记录集(对应列要相同)
- 并集是来自多个集合的元素的组合
- 合并两个或多个SELECT语句的结果集:
- SELECT语句中使用的col长度,数据类型和列数在两个表中应相同
- 通过消除重复项,结果与第一个表的列名一起显示为两个表中数据的组合
- 要使用UNION,至少应有两个SELECT语句
- UNION的结果集列名与UNION运算符中第一个SELECT语句的结果集的列名相同。另一个SELECT语句的结果集列名将被忽略
JOIN:
- 操作对象:两个表或多个表。
- 连接是多个集合的叉积的子集;满足条件相同的列产生的结果集。
- 根据所使用的JOIN 的类型(外部、内部、笛卡尔型),显示查询中提到的所有列的结果。
4.3 数据库三范式
简单归纳:
- 第一范式(1NF):字段不可分;
- 第二范式(2NF):有主键,非主键字段依赖主键;
- 第三范式(3NF):非主键字段不能相互依赖。
解释:
- 1NF:原子性。字段不可再分,否则就不是关系数据库。
- 2NF:唯一性。一个表只说明一个事物。
- 3NF:每列都与主键有直接关系,不存在传递依赖。
4.4 SQL语言四大类别
数据定义语言(DDL)、数据操纵语言(DML)、数据查询语言(DQL)、数据控制语言(DCL)。
类别 | DDL | DML | DQL | DCL |
---|---|---|---|---|
创建对象 | 表、视图、索引同义词、聚簇 | ----- | ----- | ----- |
包含语句 | Creat、Drop、Alter | INSERT、UPDATE、DELETE | SELECT From Where | GRANT 授权 ROLLBACK 回滚 |
是否能回滚 | 隐性提交的!不能rollback;操作立即生效 | 必须提交才能生效!执行的操作会放到回滚段,可回滚 | ----- | ----- |
4.5 场景题:所在的公司选择MYSQL数据库作为数据存储,一天五万条以上的增量,预计运维三年,你有哪些优化手段?
(1)设计良好的数据库结构,允许部分数据冗余,尽量避免join查询,提高效率。选择合适的表字段数据类型和存储引擎,适当添加索引。
(2)MYSQL库主从读写分离。
(3)找规律分表,减少单表中的数据量提高查询速度。
(4)添加缓存机制,比如Memcached、Apc等。
(5)不经常改动的页面,生成静态页面。
(6)书写高效率的SQL。比如SELECT * FROM TABLE改为SELECT field_1,field_2 FROM TABLE。
4.6 索引与主键的区别
- 主键是为了标识数据库记录唯一性,不允许记录重复,且键值不能为空,主键也是一个特殊索引,数据表中只允许有一个主键,但是可以有多个索引。
- 使用主键的数据库会自动创建主索引,也可以在非主键上创建索引,方便查询效率。
- 索引可以提高查询速度,他就相当于字典的目录,可以通过它很快查询想要的结果,而不需要进行全表扫描。
- 主键索引外索引的值可以为空。主键也可以由多个字段组成,组成复合主键,同时主键肯定也是唯一索引。唯一索引表示该索引值唯一,可以由一个或几个字段组成,一个表可以有多个唯一索引。
4.7 索引的优点与缺点
优点:
- 创建唯一性索引,保证数据库表中每一行数据的唯一性。
- 大大加快数据的检索速度,这也是创建索引的最主要的原因。
- 加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
- 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
- 通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统性能。
缺点:
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
- 索引需要占用物理空间,处理数据表占据数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。
4.8 数据库索引采用B+树而不是B树,主要原因是什么?
主要原因:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。
4.9 文件索引和数据库索引为什么使用B+树?
- 方便扫库。B树必须用中序遍历的方法按序扫库,而B+树直接从叶子节点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持,这是数据库选用B+树的最主要原因。
- B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针(红色部分),因此其内部结点相对B树更小。如果把所有同一内部节点的关键字放在同一块盘中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多,相对来说IO读写次数也就降低了。
- B+树的查询效率更加稳定:由于内部节点并不是最终指向文件内容的结点,而是叶子结点中关键字的索引,所以,任何关键字的查找必须走一条从根节点到叶子结点的路,所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
4.10 听说过视图吗?那游标呢?
视图(子查询)是一种虚拟的表,通常是有一个表或者多个表的行或列的子集,具有和物理表相同的功能;两类:单表视图一般用于查询和修改,会改变基本表的数据;多表视图一般用于查询,不会改变基本表的数据。
作用:
- 简化了操作,把经常使用的数据定义为视图【设计聚合函数相关操作】
- 安全性,用户只能查询和修改能看到的数据
- 逻辑上的独立性,屏蔽了真实表的结构带来的影响【动态的数据的集合,数据是随着基表的更新而更新】
游标是对查询出来的结果集作为一个单元来有效的处理。游标是处理结果集的一种机制,定位到结果集中的某一行,多行数据进行读写。
作用:
- 定位到结果集中的某一行。
- 对当前位置的数据进行读写。
- 可以对结果集中的数据单独操作,而不是整行执行相同的操作。
4.11 MYSQL中为什么要有事务回滚机制?
恢复机制是通过回滚日志实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。当事务已经被提交后,就无法再次回滚了。
回滚作用:
- 能够在发生错误或者用户执行ROLLBACK时提供回滚相关的信息。
- 在整个系统发生崩溃、数据库进行直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。
五、设计模式
5.1 单例模式
单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
5.2 工厂模式
概述:一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。不过,在《设计模式》一书中,它将简单工厂模式看做是工厂方法模式的一种特例,所以工厂模式纸杯分成了工厂方法和抽象工厂两类。
(1)简单工厂模式(静态工厂方法模式)
概念:是创建型的一种。在简单工厂模式中,可以根据实际的参数不同返回不同的实例。同时在简单工厂模式中会定义一个类负责创建其它类的实例,被创建的实例也通常具有共同的父类。虽然实现了对象的创建和使用的分离,但是不够灵活,工厂类集合了所有产品的创建逻辑,职责过重,同时新增一个产品就需要在原工厂类内部添加一个分支,违反了开闭原则。并且若是有多个判断条件共同决定对象,则后期修改会越来越复杂。
实际应用:JDK中的DateFormate、Calendar类都有使用,通过不同参数返回我们需要的对象。
(2)工厂方法模式
概念:工厂方法模式中,将简单工厂中的工厂类变为一个抽象接口。负责给出不同工厂应该实现的方法,自身不再负责创建各种产品,而是将具体的创建操作交给实现该接口的子工厂类来做。通过多态的形式解决了简单工厂模式过多的分支问题。虽然在新增产品时不仅要新增一个产品类还要实现与之对应的子工厂,但是相较于简单工厂模式更符合开闭原则。
实际应用:JDK中的Collection接口中Iterator的实现。Collection中不同的实现类生产适合于自己的迭代器对象。
(3)抽象工厂模式
概念:工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。抽象工厂模式为工厂方法模式的进一步延伸,其将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产。虽然对于新增一个产品族很方便,并且也符合开闭原则,但是新增一个产品登记结构,会对整个工厂结构进行大改。
实际应用:Spring中的BeanFactory。
六、智力题
6.1 老鼠与毒药问题
有1000瓶水,其中一瓶有毒,只要老鼠喝下一小口毒水1天后就会死亡,你只有1天时间和10只老鼠,如何检验出哪一瓶水有毒?
- 2的每10次方对应10的每3次方,所以需要10只老鼠
- 将1~1000号瓶子的编号改成二进制,分别喂给二进制表示中为1的位的老鼠,比如第9瓶水表示为0000001001那么就给倒数第1只和倒数第4只老鼠喂第9瓶水
- 第二天死亡的老鼠所组成的二进制数,即表示那瓶毒水的编号
6.2 烧绳子问题
烧一根绳子需要一个小时,现有若干条相同的绳子,问如何计时15分钟? (类似双指针)
- 点燃绳子A的一头,同时点燃绳子B的两头
- 绳子B烧完的时候绳子A还剩一半,此时点燃绳子A的另一头开始计时
- 15分钟绳子A烧完
七、反问环节
7.1 技术面
- 如果有幸加入贵公司会给我安排什么工作?
- 对于新人的培养体系?
- 贵公司未来几年的发展规划?
- 您希望我们应届生应该具有什么特质?对于我学习方面的一些建议?
- 您面试到现在,看了这么多候选人,您觉得我相对于这个岗位,还有哪些差距需要改善?
- 这次面试多久可以出结果?是否还有下一轮?
7.2 hr面
- 公司的公司氛围、团队建设是怎样子的?
- 这个岗位出差、加班多吗?
- 新人有培训吗?
- 公司的晋升机制是什么样子的呢?
- 公司有餐补、房补、交通补助之类的吗?
- 当面试官问你的薪资要求时,你可以先问一下公司的薪酬体系
- 您认为考核这个岗位员工的最重要指标有哪些?
- 您觉得这个团队的氛围怎么样?