前端面试常见题-计算机基础
Source: 前端常见八股文
计算机基础
进程与线程区别是什么
进程是资源分配的基本单位;线程是 CPU 调度的基本单位
特性 | 进程 (Process) | 线程 (Thread) |
---|---|---|
根本区别 | 资源分配的基本单位 | CPU 调度的基本单位 |
资源拥有 | 拥有独立的地址空间、内存、文件句柄等系统资源。 | 不拥有资源,与同进程下的其他线程共享所有资源(如内存、文件)。只拥有少量私有资源(如执行栈、寄存器、程序计数器)。 |
内存空间 | 每个进程都有独立的内存空间。 | 同一进程内的所有线程共享同一块内存空间。 |
隔离性 | 进程间相互隔离,一个进程的崩溃不会直接影响其他进程。安全性高。 | 线程间不隔离,一个线程的崩溃可能会导致整个进程崩溃。安全性低。 |
切换开销 | 切换成本高。需要切换整个内存地址空间,保存和恢复大量的上下文信息。 | 切换成本低。只需保存和恢复少量寄存器和栈信息即可。 |
通信方式 | 进程间通信 (IPC) 复杂,需要通过管道、消息队列、共享内存等特定机制。 | 线程间通信简单,可以直接读写共享的全局变量和内存数据(但需要注意线程同步问题,如加锁)。 |
创建与销毁 | 创建和销毁的开销大。 | 创建和销毁的开销小。 |
讲讲 TCP 三次握手、四次挥手,为什么要三次握手、四次挥手
三次握手步骤分解:
-
第一次握手 (SYN):
- 客户端 -> 服务器
- 客户端想和服务器建立连接,于是发送一个特殊的报文段。
- 内容:
SYN = 1
(表示这是一个连接请求),seq = x
(x 是一个随机生成的初始序列号)。 - 状态:客户端进入
SYN_SENT
状态,等待服务器确认。
-
第二次握手 (SYN+ACK):
- 服务器 -> 客户端
- 服务器收到了客户端的请求,如果同意连接,就会做出响应。
- 内容:
SYN = 1
(我也发起一个连接请求),ACK = 1
(确认你的请求有效),seq = y
(y 是服务器随机生成的初始序列号),ack = x + 1
(我期望你下一个发来的数据序号是 x+1,以此确认我收到了你的第一个包)。 - 状态:服务器进入
SYN_RCVD
状态。
-
第三次握手 (ACK):
- 客户端 -> 服务器
- 客户端收到了服务器的同意和确认,它也需要给服务器一个确认。
- 内容:
ACK = 1
(确认收到你的确认),seq = x + 1
(我从这个序号开始发数据),ack = y + 1
(我期望你下一个发来的数据序号是 y+1)。 - 状态:客户端和服务器都进入
ESTABLISHED
状态,连接成功建立,可以开始传输数据了。
为什么要三次握手?而不是两次或四次?
核心目的:为了确认双方的发送和接收能力都正常,并同步初始序列号。
- 为什么不能是两次?
- 无法防止已失效的连接请求报文。这是一个主要原因。
- 场景:客户端发送了一个连接请求 A,但因为网络延迟,这个请求在某个地方滞留了。客户端等不到确认,就超时重发了一个新的请求 B。请求 B 正常建立了连接,数据传输完毕后释放了连接。这时,那个滞留已久的请求 A 突然到达了服务器。
- 如果是两次握手:服务器收到请求 A,会误以为是客户端又发起了一个新连接,于是发送确认,连接就建立了。但此时客户端并不会理会这个确认,因为它已经结束了。服务器就会一直等待客户端发送数据,白白浪费资源。
- 如果是三次握手:服务器收到请求 A,回复一个确认。但客户端的上下文已经改变,它知道自己没有发起这个连接,所以会忽略服务器的确认,不发送第三次握手。服务器收不到确认,就知道这是一个无效连接,不会建立连接。
- 为什么不需要四次?
- 三次已经足够验证双方的收发能力了。
- 第一次握手:服务器知道了客户端的发送能力正常。
- 第二次握手:客户端知道了服务器的发送和接收能力都正常。
- 第三次握手:服务器知道了客户端的接收能力正常。
- 至此,双方都确认了对方的收发能力没问题,可以开始通信。再增加一次握手就显得多余,只会增加连接建立的延迟。
四次挥手步骤分解:
- 第一次挥手 (FIN):
- 客户端 -> 服务器
- 客户端的数据已经发送完毕,它想断开连接,于是发送一个
FIN
报文。 - 内容:
FIN = 1
,seq = u
(u 是客户端最后发送数据的序列号+1)。 - 状态:客户端进入
FIN_WAIT_1
状态。
- 第二次挥手 (ACK):
- 服务器 -> 客户端
- 服务器收到了客户端的断开请求。它先回复一个确认,表示“我知道你想断开了”。
- 内容:
ACK = 1
,ack = u + 1
。 - 状态:服务器进入
CLOSE_WAIT
状态。客户端收到后进入FIN_WAIT_2
状态。 - 注意:此时服务器可能还有数据没发完,所以它不能马上关闭连接。这个阶段,连接处于半关闭状态,即客户端不能再发送数据,但服务器仍然可以发送。
- 第三次挥手 (FIN):
- 服务器 -> 客户端
- 服务器的数据也发送完毕了,现在它可以关闭连接了。它向客户端发送一个
FIN
报文。 - 内容:
FIN = 1
,ACK = 1
,seq = v
(v 是服务器最后发送数据的序列号+1),ack = u + 1
。 - 状态:服务器进入
LAST_ACK
状态,等待客户端的最后确认。
- 第四次挥手 (ACK):
- 客户端 -> 服务器
- 客户端收到了服务器的关闭请求,发送最后一个确认报文。
- 内容:
ACK = 1
,seq = u + 1
,ack = v + 1
。 - 状态:客户端进入
TIME_WAIT
状态,等待 2*MSL(Maximum Segment Lifetime,报文最大生存时间)后,才真正关闭连接。服务器收到这个 ACK 后,立即进入CLOSED
状态。
为什么要四次挥手?
核心原因:因为 TCP 是全双工的,需要给被动关闭方一个机会,确保它所有的数据都发送完毕了。
- 当客户端(主动方)说“我要关闭了”(第一次挥手),它只是表明自己没有数据要发送了。
- 服务器(被动方)收到后,先回复一个“知道了”(第二次挥手)。但此时服务器可能还有未发送完的数据,不能立刻关闭。所以不能把
ACK
和FIN
合并成一个报文。 - 服务器必须等到自己的数据也全部发送完毕后,才能发送“我也准备好了,可以关闭了”的
FIN
报文(第三次挥手)。 - 这样一来一回,就形成了四次挥手。
为什么客户端最后要等待 2*MSL?
这是为了确保连接的可靠关闭。主要有两个原因:
- 保证服务器能收到最后的 ACK:客户端发送的最后一个
ACK
报文可能会丢失。如果丢失了,服务器会因为收不到确认而超时重传第三次挥手的FIN
报文。如果客户端不等待,直接关闭,就无法响应这个重传的FIN
,导致服务器无法正常关闭。等待2*MSL
可以确保有足够的时间接收到服务器的重传并重新发送确认。 - 防止“已失效的连接请求报文”出现:与三次握手的原因类似。等待
2*MSL
可以确保本次连接中所有在网络中滞留的报文段都已消失,不会在下一次新的连接中(使用相同的端口号)出现,干扰新连接。
TCP 和 UDP 区别是什么
特性 | TCP (传输控制协议) | UDP (用户数据报协议) |
---|---|---|
连接性 | 面向连接 (Connection-oriented) | 无连接 (Connectionless) |
说明 | 在传输数据前,必须先建立连接(三次握手)。传输结束后,需要释放连接(四次挥手)。 | 发送数据前不需要建立连接,直接把数据包(数据报)扔给对方。 |
可靠性 | 可靠 | 不可靠 (尽力而为) |
说明 | 通过确认(ACK)、重传、序列号等机制,保证数据不丢失、不重复、按顺序到达。 | 不保证数据能否到达,也不保证顺序。数据包可能会丢失或乱序。 |
有序性 | 有序 | 无序 |
说明 | 数据是作为字节流按顺序发送和接收的。接收端会根据序列号对数据包进行排序。 | 每个数据报都是独立的,它们到达的顺序与发送的顺序可能不一致。 |
速度/效率 | 慢 | 快 |
说明 | 因为需要建立连接、发送确认、处理排序等,开销(Overhead)较大,传输速度相对较慢。 | 没有复杂的控制机制,开销小,传输速度快。 |
头部开销 | 大(TCP 头部大小为 20-60 字节) | 小(UDP 头部固定为 8 字节) |
说明 | 头部包含了序列号、确认号、窗口大小等大量控制信息。 | 头部只包含源/目的端口、长度和校验和等最基本的信息。 |
数据形式 | 字节流 (Byte Stream) | 数据报 (Datagram) |
说明 | 应用层数据被视为一连串无边界的字节流。TCP 可能会对数据进行拆分或合并。 | 应用层交下来的数据报,UDP 只会加上头部,不会进行任何拆分或合并。保留了消息的边界。 |
控制机制 | 有(流量控制、拥塞控制) | 无 |
说明 | 流量控制:防止发送方过快,导致接收方来不及处理。 拥塞控制:根据网络状况动态调整发送速率,避免网络拥堵。 | 以恒定的速率发送数据,不管网络是否拥堵,也不管接收方是否能处理。 |
选择 TCP 的场景 (可靠性优先)
当数据的完整性和顺序至关重要,不容许任何差错时,必须使用 TCP。
- Web 浏览 (HTTP/HTTPS):网页的 HTML、CSS、JS 文件必须完整无误地按顺序加载,否则页面会错乱或无法显示。
- 文件传输 (FTP, SFTP):传输文件时,一个字节的错误都可能导致整个文件损坏。
- 电子邮件 (SMTP, POP3, IMAP):你肯定不希望你的邮件内容丢失单词或段落。
- 远程终端 (SSH):你输入的命令和服务器返回的结果,都必须准确无误。
选择 UDP 的场景 (实时性/速度优先)
当对实时性要求极高,可以容忍少量数据丢失时,UDP 是更好的选择。
- 在线视频/直播 (Streaming):如果为了等一个丢失的数据包(比如画面中的一帧)而让整个视频卡顿,用户体验会非常糟糕。宁愿丢失一帧(画面出现瞬间花屏),也要保证流畅播放。
- 网络电话 (VoIP):和视频同理,实时通话的流畅性远比偶尔丢失一个声音采样点更重要。
- 在线游戏:玩家的位置、动作等信息需要被快速发送。如果一个数据包延迟到达,它包含的已经是过时的信息,不如直接丢弃,使用最新的数据包。
- DNS (域名系统):查询一个域名的 IP 地址,这个过程要求非常快。通常只是一次简单的请求和响应,如果查询失败(数据包丢失),客户端简单地重试一次即可,使用 UDP 开销最小。
实现一个LRU算法
利用Map的有序性,简单实现:
- get 操作:当获取一个元素时,为了把它标记为“最新使用”,我们先从
Map
中删除它,然后再重新插入。这样它就会被移动到Map
迭代顺序的末尾。 - put 操作:当缓存满了需要淘汰时,我们淘汰的就是
Map
迭代顺序中的第一个元素。
class LRU {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
}
}
最佳实践是使用「哈希表 + 双向链表」:
- 哈希表 (JavaScript 中的
Map
):用于实现 O(1) 的快速查找。Map
的键是数据的key
,值是指向链表节点的引用。 - 双向链表 (Doubly Linked List):用于维护数据的访问顺序。链表头部是“最新”的数据,链表尾部是“最旧”的数据。
class Node {
constructor(key, value) {
this.key = key;
this.value = value;
this.pre = null;
this.next = null;
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
}
_moveToHead(node) {
// 从原位置断开
node.prev.next = node.next;
node.next.prev = node.prev;
// 插入到头部之后;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
node.prev = this.head;
}
_removeTail() {
const tailNode = this.tail.pre;
tailNode.prev.next = this.tail;
this.tail.prev = tailNode.prev;
return tailNode;
}
get(key) {
if (!this.cache.has(key)) return -1;
const node = this.cache.get(key);
this._moveToHead(node);
return node.value;
}
put(key, value) {
if (this.cache.has(key)) {
const node = this.cache.get(key);
node.value = value;
this._moveToHead(node);
} else {
const newNode = new Node(key, value);
this.cache.set(key, newNode);
newNode.next = this.head.next;
this.head.next.prev = newNode;
this.head.next = newNode;
newNode.prev = this.head;
if (this.cache.size > this.capacity) {
const tailNode = this._removeTail();
this.cache.delete(tailNode.key);
}
}
}
}
前端面试常见题-计算机基础
https://www.yuque.com/baiyueguang-rfnbu/tr4d0i/rz15kr 作者
MeorinLime 梦灵
发布日期
2025 - 08 - 11