面试题清单.md

Linux 系统

详细描述 Linux 系统中,写入一个文件的过程

  1. 用户空间操作:
    • 应用程序通过调用标准库(如 C 语言的 stdio 库)或系统调用(如 write)来请求写入数据到文件。
    • 如果使用的是标准库函数(如 fwrite),首先这些数据可能会被暂存到应用程序的输出缓冲区中。当缓冲区满或者显式调用刷新(如 fflush)时,标准库会进一步调用相应的系统调用来实际执行写入操作。
  2. 系统调用:
    • 当执行写入的系统调用(如 write)时,控制权从用户空间传递到操作系统内核。
    • 内核接收到这个调用后,会进行必要的权限检查,确认调用进程有权写入指定文件。
  3. 虚拟文件系统(VFS):
    • Linux 使用虚拟文件系统(VFS)作为所有文件系统类型(如 ext4、xfs、btrfs 等)的抽象层。VFS 负责将通用的文件操作转换为特定文件系统的操作。
    • VFS 根据文件描述符找到对应的 inode(索引节点),inode 包含了文件的元数据以及指向文件实际数据块的指针。
  4. 文件系统操作:
    • 根据 VFS 和具体文件系统的操作,数据会被写入到文件系统的缓冲区。这一步可能涉及将数据拷贝到内核的页面缓存(page cache)。
    • 文件系统可能会执行一些优化,如合并连续的写入请求以减少实际的磁盘操作。
  5. 写回(Write-back):
    • 数据首先被写入到缓冲区中,并在一段时间后异步写回到磁盘。这个机制可以提高性能,因为它减少了磁盘 I/O 操作的次数。
    • 系统也可能通过“写穿”(write-through)策略直接写入磁盘,这取决于文件系统的配置和具体操作(如 fsync 系统调用)。
  6. 硬件层面:
    • 数据最终通过存储设备的驱动程序写入磁盘。如果使用的是固态硬盘(SSD),数据实际上是写入到 NAND 闪存单元;如果是传统的机械硬盘(HDD),数据则是写入到磁盘的物理扇区。
  7. 确认写入完成:
    • 一旦数据成功写入存储设备,相应的系统调用会完成,并将控制权返回给用户空间应用程序,同时返回写入操作的状态。

详细描述访问一个域名到浏览器显示页面的过程

  1. 域名解析:
    • 浏览器首先检查域名是否已在本地缓存中解析过,如果是,则直接使用该 IP 地址。如果没有,浏览器会发起一个 DNS (Domain Name System) 查询来将域名转换成 IP 地址。这可能涉及多级查询,从本地 DNS 服务器到根 DNS 服务器,再到负责该顶级域名(TLD)的服务器,最后是负责该具体域名的名称服务器。
  2. 建立连接:
    • 浏览器使用解析得到的 IP 地址尝试与目标服务器建立 TCP 连接。这一过程通常涉及三次握手,确保双方正确建立连接。
    • 如果网站使用 HTTPS,还需要进行 SSL/TLS 握手,以安全地加密之后的通信。
  3. 发送 HTTP 请求:
    • 一旦建立了 TCP 连接(和可选的 SSL/TLS 加密层),浏览器就会向服务器发送 HTTP 请求。这个请求包括请求行(如 GET / HTTP/1.1),请求头(包括用户代理、接受的内容类型等),以及可选的请求体。
  4. 服务器处理请求并响应:
    • 服务器接收到请求后,会根据请求的资源(如网页、图片等)进行处理。这可能涉及到服务器端的脚本或应用程序的执行,如数据库查询或动态内容的生成。
    • 处理完成后,服务器会将响应数据(如 HTML 页面)以及响应头(状态码、内容类型等)发送回浏览器。
  5. 浏览器处理响应:
    • 浏览器接收到服务器的响应数据后,首先会检查状态码。如果是成功的响应(如 200 OK),浏览器会继续解析返回的内容。
    • 浏览器解析 HTML 内容,并根据需要发起额外的请求来获取嵌入资源(如 CSS 文件、JavaScript 脚本、图片等)。
  6. 渲染页面:
    • 浏览器根据 HTML 和 CSS 构建 DOM (Document Object Model) 树和 CSSOM (CSS Object Model) 树,然后将它们合并成渲染树。
    • 浏览器根据渲染树来布局页面,计算每个元素的尺寸和位置。
    • 最后,浏览器绘制页面内容到屏幕上。
  7. JavaScript 执行:
    • 如果页面包含 JavaScript,浏览器还会解析和执行脚本。这可能会修改 DOM,触发重新渲染。

详细描述在两个Linux系统中双方发送接受 TCP 请求的过程

1. TCP 三次握手(建立连接)

  1. SYN 发送:
    • 主动方(客户端)发送一个 SYN(同步序列编号)报文给被动方(服务器),表示开始建立一个新的连接。这个 SYN 报文包含一个客户端选择的初始序列号(ISN)。
    • 此时,客户端进入 SYN-SENT 状态。
  2. SYN-ACK 接收:
    • 服务器收到 SYN 报文后,会返回一个 SYN-ACK 报文。这个报文包含服务器自己的初始序列号,以及对客户端 ISN 的确认(客户端 ISN+1)。
    • 服务器此时进入 SYN-RECEIVED 状态。
  3. ACK 发送:
    • 客户端收到 SYN-ACK 后,发送一个 ACK(确认)报文给服务器,确认号为服务器 ISN+1。
    • 发送完这个 ACK 报文后,客户端进入 ESTABLISHED 状态。服务器在收到这个 ACK 后也进入 ESTABLISHED 状态。
    • 此时,TCP 连接建立完成,双方可以开始数据传输。

2. 数据传输

  • 一旦 TCP 连接建立,两个 Linux 系统可以开始双向数据传输。应用数据被封装在 TCP 段中,每个 TCP 段都包含序列号和确认号,以确保数据的可靠传输和顺序到达。
  • TCP 提供流量控制和拥塞控制机制,确保网络条件变化时调整数据发送速率,优化性能和资源利用。

3. TCP 四次挥手(断开连接)

  1. FIN 发送:
    • 当一方(假设为客户端)完成数据发送后,它会发送一个 FIN 报文给服务器,表示没有数据发送了,请求关闭连接。
    • 客户端进入 FIN-WAIT-1 状态。
  2. ACK 接收:
    • 服务器收到 FIN 报文后,发送一个 ACK 报文作为回应,确认号为客户端的 FIN 序列号+1。
    • 服务器此时进入 CLOSE-WAIT 状态,客户端收到 ACK 后进入 FIN-WAIT-2 状态。
  3. FIN 发送(服务器端):
    • 服务器完成其数据发送后,也发送一个 FIN 报文给客户端,请求关闭连接。
    • 服务器进入 LAST-ACK 状态。
  4. ACK 接收:
    • 客户端收到服务器的 FIN 报文后,发送一个 ACK 报文作为回应,确认号为服务器的 FIN 序列号+1。
    • 客户端进入 TIME-WAIT 状态,等待足够的时间以确保服务器接收到最终的 ACK 报文,然后关闭连接

一个 tcp 请求的生命周期中进行的系统调用的过程

1. 建立连接

客户端
  1. socket():
    • 调用 socket() 创建一个新的套接字。这是与网络通信的端点,用于发送和接收数据。
  2. connect():
    • 使用 connect() 系统调用并指定服务器的地址和端口来尝试建立到服务器的连接。这将启动 TCP 三次握手过程。
    • 客户端首先发送 SYN 包给服务器,状态变为 SYN-SENT。
服务器
  1. socket():
    • 同样调用 socket() 创建一个套接字。
  2. bind():
    • 调用 bind() 将套接字绑定到一个本地地址和端口上。这通常是服务器监听客户端连接请求的端口。
  3. listen():
    • 通过 listen() 告诉系统该套接字是服务器的监听套接字,准备接受客户端连接请求。
  4. accept():
    • 服务器调用 accept() 阻塞等待客户端的连接请求。当接收到客户端的 SYN 请求后,服务器发送 SYN-ACK 响应,并进入 SYN-RECEIVED 状态。一旦客户端回应 ACK,accept() 返回,服务器进入 ESTABLISHED 状态,此时 TCP 连接建立。

2. 数据传输

  • write() / send():
    • 在连接建立后,双方可以使用 write()send() 系统调用发送数据。这些调用将应用程序的数据写入 TCP 套接字,由 TCP 协议处理数据的分段、封装和发送。
  • read() / recv():
    • 对应地,使用 read()recv() 系统调用来接收从对方发送过来的数据。这些调用从 TCP 套接字读取数据,如果没有可用数据,调用会阻塞,直到有数据到达。

3. 断开连接

发起关闭的一方
  1. close() / shutdown():
    • 当通信完成,一方(假设为客户端)使用 close()shutdown() 系统调用来关闭套接字。这会导致发送一个 FIN 包给对方,开始 TCP 四次挥手过程。
接收 FIN 的一方
  • 服务器在接收到客户端的 FIN 包后,内核通知应用程序套接字关闭(如果应用程序阻塞在 read() 调用中,则 read() 返回0,表示EOF)。此时,服务器也可以调用 close()shutdown() 来关闭连接并发送 FIN 包给客户端。

4. 清理

  • 在四次挥手后,当双方都发送并确认 FIN 包,TCP 连接被彻底关闭,操作系统会清理与该连接相关的资源。

详细描述 Linux系统中 epoll 多路复用机制和关键原理

在Linux系统中,epoll是一种高效的I/O事件通知机制,特别适用于处理大量并发连接的服务器程序。与传统的多路复用机制如selectpoll相比,epoll提供了更好的扩展性和性能,主要因为它能够避免selectpoll中的一些固有限制和性能瓶颈。

关键原理和特性

  1. 效率与可扩展性

    • epoll通过一种有效的方法管理大量的文件描述符(FD)。它使用一个红黑树(一种自平衡二叉查找树)来存储所有正在监视的FDs,这样查找、添加或删除FDs的操作都可以在对数时间内完成。与之相比,selectpoll需要遍历整个FD集合来检查状态,效率随着FD数量的增加而线性下降。
  2. 事件驱动

    • epoll是基于事件的,它只对活动的FD进行操作。当一个或多个FD状态发生变化时,epoll会将这些活动FD添加到一个就绪列表中,应用程序只需处理这个列表中的FD。这种方式避免了在大量FD中轮询寻找那些状态发生变化的FD,从而大大提高了效率。
  3. 两种工作模式

    • epoll支持两种工作模式:边缘触发(ET,Edge Triggered)和水平触发(LT,Level Triggered)。
      • 水平触发(LT):这是默认模式,当FD就绪时,epoll_wait会返回该FD,而且只要这个FD仍然处于就绪状态,epoll_wait会再次报告它。
      • 边缘触发(ET):在这种模式下,只有FD状态从不可用变为可用,或者有更多数据可读写时,epoll_wait才会返回FD。这要求应用程序必须处理所有的数据,直到没有更多可用为止,因为再次状态变化之前,epoll_wait不会再次报告该FD。
  4. 一次注册,多次使用

    • epoll允许应用程序只注册一次FD,除非FD的监听事件发生变化,否则无需重新注册。这与selectpoll每次调用时都需要重新构建FD集的方式形成鲜明对比。

使用过程

epoll的使用通常包括以下几个步骤:

  1. 创建epoll实例

    • 调用epoll_create函数创建一个epoll实例,它返回一个文件描述符,用于后续所有的epoll操作。
  2. 注册感兴趣的事件

    • 使用epoll_ctl添加、修改或删除要监控的文件描述符及其对应的事件。这些事件可以是读就绪、写就绪等。
  3. 等待事件发生

    • 调用epoll_wait等待一个或多个FD上的事件发生。epoll_wait可以指定超时时间,以防在指定时间内没有任何事件发生。
  4. 处理就绪的事件

    • epoll_wait返回时,会提供一个事件列表,应用程序遍历这个列表,处理所有就绪的事件。

优点

  • 高效:对于大规模FD集合,epoll提供了优越的性能。
  • 可扩展epoll的性能不会随着FD数量的增加而显著下降。
  • 灵活:支持边缘触发和水平触发,适应不同

描述 epoll 机制中进行网络请求多路复用的主流程

使用 epoll 机制进行网络请求多路复用涉及到创建 epoll 实例、向 epoll 实例注册感兴趣的事件(如可读、可写事件)、等待事件发生,以及处理发生的事件。这里以一个典型的网络服务器为例,描述使用 epoll 进行网络请求多路复用的主要流程:

1. 创建 epoll 实例

首先,服务器启动时创建一个 epoll 实例,用于后续的事件监听和处理。

1
2
3
4
5
int epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}

2. 注册感兴趣的事件

服务器的监听socket需要注册到 epoll 实例中,以便 epoll 能够通知应用程序有新的连接请求。此外,随着新的客户端连接到服务器,相应的客户端socket也需要注册到 epoll 实例中,以监听这些socket上的可读或可写事件。

1
2
3
4
5
6
7
struct epoll_event ev;
ev.events = EPOLLIN; // 表示对应的文件描述符可以读(如:TCP socket的远程关闭,有数据可读)
ev.data.fd = listen_fd; // listen_fd 是监听socket的文件描述符
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
perror("epoll_ctl: listen_fd");
exit(EXIT_FAILURE);
}

3. 等待事件发生

应用程序通过调用 epoll_wait 等待事件的发生。这个调用可以指定超时,以便在没有事件发生时不会无限期地阻塞。

1
2
3
4
5
6
7
8
#define MAX_EVENTS 10
struct epoll_event events[MAX_EVENTS];

int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}

4. 处理就绪的事件

epoll_wait 返回后,应用程序遍历事件数组,处理每个就绪的事件。如果事件与监听socket相关(表示有新的连接请求),服务器将接受这个连接,并将新的客户端socket注册到 epoll 实例中。如果事件与客户端socket相关(表示有数据可读或可写),服务器则进行相应的读或写操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_fd) {
// 处理新的连接请求
int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &addr_len);
if (conn_fd == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_fd); // 通常设置为非阻塞模式
ev.events = EPOLLIN | EPOLLET; // 可读事件,边缘触发模式
ev.data.fd = conn_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
perror("epoll_ctl: conn_fd");
exit(EXIT_FAILURE);
}
} else {
// 处理客户端socket的事件(读或写)
int fd = events[n].data.fd;
// 进行读或写操作...
}
}

通过以上流程,服务器能够高效地管理多个网络连接,使用单个线程或进程即可同时处理多个网络I/O操作,大大提高了网络应用程序的性能和吞吐量。

网络


面试题清单.md
https://abrance.github.io/2024/03/07/mdstorage/domain/招聘/面试题清单/
Author
xiaoy
Posted on
March 7, 2024
Licensed under