面试题清单.md
Linux 系统
详细描述 Linux 系统中,写入一个文件的过程
- 用户空间操作:
- 应用程序通过调用标准库(如 C 语言的
stdio
库)或系统调用(如write
)来请求写入数据到文件。 - 如果使用的是标准库函数(如
fwrite
),首先这些数据可能会被暂存到应用程序的输出缓冲区中。当缓冲区满或者显式调用刷新(如fflush
)时,标准库会进一步调用相应的系统调用来实际执行写入操作。
- 应用程序通过调用标准库(如 C 语言的
- 系统调用:
- 当执行写入的系统调用(如
write
)时,控制权从用户空间传递到操作系统内核。 - 内核接收到这个调用后,会进行必要的权限检查,确认调用进程有权写入指定文件。
- 当执行写入的系统调用(如
- 虚拟文件系统(VFS):
- Linux 使用虚拟文件系统(VFS)作为所有文件系统类型(如 ext4、xfs、btrfs 等)的抽象层。VFS 负责将通用的文件操作转换为特定文件系统的操作。
- VFS 根据文件描述符找到对应的 inode(索引节点),inode 包含了文件的元数据以及指向文件实际数据块的指针。
- 文件系统操作:
- 根据 VFS 和具体文件系统的操作,数据会被写入到文件系统的缓冲区。这一步可能涉及将数据拷贝到内核的页面缓存(page cache)。
- 文件系统可能会执行一些优化,如合并连续的写入请求以减少实际的磁盘操作。
- 写回(Write-back):
- 数据首先被写入到缓冲区中,并在一段时间后异步写回到磁盘。这个机制可以提高性能,因为它减少了磁盘 I/O 操作的次数。
- 系统也可能通过“写穿”(write-through)策略直接写入磁盘,这取决于文件系统的配置和具体操作(如
fsync
系统调用)。
- 硬件层面:
- 数据最终通过存储设备的驱动程序写入磁盘。如果使用的是固态硬盘(SSD),数据实际上是写入到 NAND 闪存单元;如果是传统的机械硬盘(HDD),数据则是写入到磁盘的物理扇区。
- 确认写入完成:
- 一旦数据成功写入存储设备,相应的系统调用会完成,并将控制权返回给用户空间应用程序,同时返回写入操作的状态。
详细描述访问一个域名到浏览器显示页面的过程
- 域名解析:
- 浏览器首先检查域名是否已在本地缓存中解析过,如果是,则直接使用该 IP 地址。如果没有,浏览器会发起一个 DNS (Domain Name System) 查询来将域名转换成 IP 地址。这可能涉及多级查询,从本地 DNS 服务器到根 DNS 服务器,再到负责该顶级域名(TLD)的服务器,最后是负责该具体域名的名称服务器。
- 建立连接:
- 浏览器使用解析得到的 IP 地址尝试与目标服务器建立 TCP 连接。这一过程通常涉及三次握手,确保双方正确建立连接。
- 如果网站使用 HTTPS,还需要进行 SSL/TLS 握手,以安全地加密之后的通信。
- 发送 HTTP 请求:
- 一旦建立了 TCP 连接(和可选的 SSL/TLS 加密层),浏览器就会向服务器发送 HTTP 请求。这个请求包括请求行(如
GET / HTTP/1.1
),请求头(包括用户代理、接受的内容类型等),以及可选的请求体。
- 一旦建立了 TCP 连接(和可选的 SSL/TLS 加密层),浏览器就会向服务器发送 HTTP 请求。这个请求包括请求行(如
- 服务器处理请求并响应:
- 服务器接收到请求后,会根据请求的资源(如网页、图片等)进行处理。这可能涉及到服务器端的脚本或应用程序的执行,如数据库查询或动态内容的生成。
- 处理完成后,服务器会将响应数据(如 HTML 页面)以及响应头(状态码、内容类型等)发送回浏览器。
- 浏览器处理响应:
- 浏览器接收到服务器的响应数据后,首先会检查状态码。如果是成功的响应(如 200 OK),浏览器会继续解析返回的内容。
- 浏览器解析 HTML 内容,并根据需要发起额外的请求来获取嵌入资源(如 CSS 文件、JavaScript 脚本、图片等)。
- 渲染页面:
- 浏览器根据 HTML 和 CSS 构建 DOM (Document Object Model) 树和 CSSOM (CSS Object Model) 树,然后将它们合并成渲染树。
- 浏览器根据渲染树来布局页面,计算每个元素的尺寸和位置。
- 最后,浏览器绘制页面内容到屏幕上。
- JavaScript 执行:
- 如果页面包含 JavaScript,浏览器还会解析和执行脚本。这可能会修改 DOM,触发重新渲染。
详细描述在两个Linux系统中双方发送接受 TCP 请求的过程
1. TCP 三次握手(建立连接)
- SYN 发送:
- 主动方(客户端)发送一个 SYN(同步序列编号)报文给被动方(服务器),表示开始建立一个新的连接。这个 SYN 报文包含一个客户端选择的初始序列号(ISN)。
- 此时,客户端进入 SYN-SENT 状态。
- SYN-ACK 接收:
- 服务器收到 SYN 报文后,会返回一个 SYN-ACK 报文。这个报文包含服务器自己的初始序列号,以及对客户端 ISN 的确认(客户端 ISN+1)。
- 服务器此时进入 SYN-RECEIVED 状态。
- ACK 发送:
- 客户端收到 SYN-ACK 后,发送一个 ACK(确认)报文给服务器,确认号为服务器 ISN+1。
- 发送完这个 ACK 报文后,客户端进入 ESTABLISHED 状态。服务器在收到这个 ACK 后也进入 ESTABLISHED 状态。
- 此时,TCP 连接建立完成,双方可以开始数据传输。
2. 数据传输
- 一旦 TCP 连接建立,两个 Linux 系统可以开始双向数据传输。应用数据被封装在 TCP 段中,每个 TCP 段都包含序列号和确认号,以确保数据的可靠传输和顺序到达。
- TCP 提供流量控制和拥塞控制机制,确保网络条件变化时调整数据发送速率,优化性能和资源利用。
3. TCP 四次挥手(断开连接)
- FIN 发送:
- 当一方(假设为客户端)完成数据发送后,它会发送一个 FIN 报文给服务器,表示没有数据发送了,请求关闭连接。
- 客户端进入 FIN-WAIT-1 状态。
- ACK 接收:
- 服务器收到 FIN 报文后,发送一个 ACK 报文作为回应,确认号为客户端的 FIN 序列号+1。
- 服务器此时进入 CLOSE-WAIT 状态,客户端收到 ACK 后进入 FIN-WAIT-2 状态。
- FIN 发送(服务器端):
- 服务器完成其数据发送后,也发送一个 FIN 报文给客户端,请求关闭连接。
- 服务器进入 LAST-ACK 状态。
- ACK 接收:
- 客户端收到服务器的 FIN 报文后,发送一个 ACK 报文作为回应,确认号为服务器的 FIN 序列号+1。
- 客户端进入 TIME-WAIT 状态,等待足够的时间以确保服务器接收到最终的 ACK 报文,然后关闭连接
一个 tcp 请求的生命周期中进行的系统调用的过程
1. 建立连接
客户端
socket()
:- 调用
socket()
创建一个新的套接字。这是与网络通信的端点,用于发送和接收数据。
- 调用
connect()
:- 使用
connect()
系统调用并指定服务器的地址和端口来尝试建立到服务器的连接。这将启动 TCP 三次握手过程。 - 客户端首先发送 SYN 包给服务器,状态变为 SYN-SENT。
- 使用
服务器
socket()
:- 同样调用
socket()
创建一个套接字。
- 同样调用
bind()
:- 调用
bind()
将套接字绑定到一个本地地址和端口上。这通常是服务器监听客户端连接请求的端口。
- 调用
listen()
:- 通过
listen()
告诉系统该套接字是服务器的监听套接字,准备接受客户端连接请求。
- 通过
accept()
:- 服务器调用
accept()
阻塞等待客户端的连接请求。当接收到客户端的 SYN 请求后,服务器发送 SYN-ACK 响应,并进入 SYN-RECEIVED 状态。一旦客户端回应 ACK,accept()
返回,服务器进入 ESTABLISHED 状态,此时 TCP 连接建立。
- 服务器调用
2. 数据传输
write()
/send()
:- 在连接建立后,双方可以使用
write()
或send()
系统调用发送数据。这些调用将应用程序的数据写入 TCP 套接字,由 TCP 协议处理数据的分段、封装和发送。
- 在连接建立后,双方可以使用
read()
/recv()
:- 对应地,使用
read()
或recv()
系统调用来接收从对方发送过来的数据。这些调用从 TCP 套接字读取数据,如果没有可用数据,调用会阻塞,直到有数据到达。
- 对应地,使用
3. 断开连接
发起关闭的一方
close()
/shutdown()
:- 当通信完成,一方(假设为客户端)使用
close()
或shutdown()
系统调用来关闭套接字。这会导致发送一个 FIN 包给对方,开始 TCP 四次挥手过程。
- 当通信完成,一方(假设为客户端)使用
接收 FIN 的一方
- 服务器在接收到客户端的 FIN 包后,内核通知应用程序套接字关闭(如果应用程序阻塞在
read()
调用中,则read()
返回0,表示EOF)。此时,服务器也可以调用close()
或shutdown()
来关闭连接并发送 FIN 包给客户端。
4. 清理
- 在四次挥手后,当双方都发送并确认 FIN 包,TCP 连接被彻底关闭,操作系统会清理与该连接相关的资源。
详细描述 Linux系统中 epoll 多路复用机制和关键原理
在Linux系统中,epoll
是一种高效的I/O事件通知机制,特别适用于处理大量并发连接的服务器程序。与传统的多路复用机制如select
和poll
相比,epoll
提供了更好的扩展性和性能,主要因为它能够避免select
和poll
中的一些固有限制和性能瓶颈。
关键原理和特性
效率与可扩展性:
epoll
通过一种有效的方法管理大量的文件描述符(FD)。它使用一个红黑树(一种自平衡二叉查找树)来存储所有正在监视的FDs,这样查找、添加或删除FDs的操作都可以在对数时间内完成。与之相比,select
和poll
需要遍历整个FD集合来检查状态,效率随着FD数量的增加而线性下降。
事件驱动:
epoll
是基于事件的,它只对活动的FD进行操作。当一个或多个FD状态发生变化时,epoll
会将这些活动FD添加到一个就绪列表中,应用程序只需处理这个列表中的FD。这种方式避免了在大量FD中轮询寻找那些状态发生变化的FD,从而大大提高了效率。
两种工作模式:
epoll
支持两种工作模式:边缘触发(ET,Edge Triggered)和水平触发(LT,Level Triggered)。- 水平触发(LT):这是默认模式,当FD就绪时,
epoll_wait
会返回该FD,而且只要这个FD仍然处于就绪状态,epoll_wait
会再次报告它。 - 边缘触发(ET):在这种模式下,只有FD状态从不可用变为可用,或者有更多数据可读写时,
epoll_wait
才会返回FD。这要求应用程序必须处理所有的数据,直到没有更多可用为止,因为再次状态变化之前,epoll_wait
不会再次报告该FD。
- 水平触发(LT):这是默认模式,当FD就绪时,
一次注册,多次使用:
epoll
允许应用程序只注册一次FD,除非FD的监听事件发生变化,否则无需重新注册。这与select
和poll
每次调用时都需要重新构建FD集的方式形成鲜明对比。
使用过程
epoll
的使用通常包括以下几个步骤:
创建epoll实例:
- 调用
epoll_create
函数创建一个epoll实例,它返回一个文件描述符,用于后续所有的epoll
操作。
- 调用
注册感兴趣的事件:
- 使用
epoll_ctl
添加、修改或删除要监控的文件描述符及其对应的事件。这些事件可以是读就绪、写就绪等。
- 使用
等待事件发生:
- 调用
epoll_wait
等待一个或多个FD上的事件发生。epoll_wait
可以指定超时时间,以防在指定时间内没有任何事件发生。
- 调用
处理就绪的事件:
epoll_wait
返回时,会提供一个事件列表,应用程序遍历这个列表,处理所有就绪的事件。
优点
- 高效:对于大规模FD集合,
epoll
提供了优越的性能。 - 可扩展:
epoll
的性能不会随着FD数量的增加而显著下降。 - 灵活:支持边缘触发和水平触发,适应不同
描述 epoll 机制中进行网络请求多路复用的主流程
使用 epoll
机制进行网络请求多路复用涉及到创建 epoll
实例、向 epoll
实例注册感兴趣的事件(如可读、可写事件)、等待事件发生,以及处理发生的事件。这里以一个典型的网络服务器为例,描述使用 epoll
进行网络请求多路复用的主要流程:
1. 创建 epoll
实例
首先,服务器启动时创建一个 epoll
实例,用于后续的事件监听和处理。
1 |
|
2. 注册感兴趣的事件
服务器的监听socket需要注册到 epoll
实例中,以便 epoll
能够通知应用程序有新的连接请求。此外,随着新的客户端连接到服务器,相应的客户端socket也需要注册到 epoll
实例中,以监听这些socket上的可读或可写事件。
1 |
|
3. 等待事件发生
应用程序通过调用 epoll_wait
等待事件的发生。这个调用可以指定超时,以便在没有事件发生时不会无限期地阻塞。
1 |
|
4. 处理就绪的事件
当 epoll_wait
返回后,应用程序遍历事件数组,处理每个就绪的事件。如果事件与监听socket相关(表示有新的连接请求),服务器将接受这个连接,并将新的客户端socket注册到 epoll
实例中。如果事件与客户端socket相关(表示有数据可读或可写),服务器则进行相应的读或写操作。
1 |
|
通过以上流程,服务器能够高效地管理多个网络连接,使用单个线程或进程即可同时处理多个网络I/O操作,大大提高了网络应用程序的性能和吞吐量。