BIO/NIO/AIO的异同

在计算机系统中,I/O(输入/输出)操作是程序与外部世界(如文件、网络等)交互的重要手段。现代操作系统提供了三种不同的I/O模型:BIO(阻塞I/O)、NIO(非阻塞I/O)和AIO(异步I/O),它们在设计思想、工作机制和适用场景上有着显著差异。本文将深入剖析这三种I/O模型的核心概念、实现原理以及各自的优缺点,以帮助读者理解不同I/O模型在系统设计中的应用。

BIO (Blocking I/O,阻塞I/O)

概念与原理

BIO是传统的I/O编程模型,其特点是简单直观但效率较低。在BIO模型中,当一个线程发起I/O操作后,该线程会被阻塞直到I/O操作完成。这种模型对应于POSIX标准中的最基本的I/O操作方式。

工作机制

BIO的工作机制基于线程阻塞原理。当应用程序调用read()等系统调用时,线程会从用户态切换到内核态。如果请求的数据尚未准备好,内核会将线程置为睡眠状态并移出运行队列,释放CPU资源给其他线程使用。

当数据准备就绪(如网络数据包到达)时,设备会触发中断,通知CPU数据已到达。内核将数据放入缓冲区,并将等待的线程重新唤醒,加入运行队列。线程获得CPU时间片后,将数据从内核缓冲区复制到用户空间,然后返回继续执行。

一次完整的BIO操作通常涉及两次用户态与内核态的切换:

  1. 第一次切换:应用程序调用read()等系统调用时,从用户态切换到内核态
  2. 第二次切换:系统调用完成后,从内核态切换回用户态

如果数据未就绪导致阻塞,在线程被唤醒后继续执行系统调用时,实际上不会产生额外的用户态与内核态切换,因为线程仍然在内核态执行。但这个过程会涉及上下文切换:当线程阻塞时让出CPU,当数据就绪后重新获得CPU执行权。这种上下文切换虽然不是用户态与内核态的切换,但同样会带来性能开销。

在服务器应用中,BIO通常采用”一个连接一个线程”的模式。每当接收到新的客户端连接,服务器就会创建或分配一个线程专门处理该连接的所有I/O操作。这导致高并发场景下线程数量剧增,系统资源消耗严重。

底层实现

BIO底层依赖操作系统的阻塞式系统调用。以网络读取为例,实现过程如下:

  1. 应用程序通过系统调用请求读取数据,控制权转移给内核
  2. 内核检查是否有数据可读,若无,将线程加入等待队列并挂起
  3. 网络数据到达时,网卡产生中断,内核处理数据并放入socket缓冲区
  4. 内核唤醒等待的线程,将其标记为可运行状态
  5. 线程恢复执行,将数据从内核缓冲区复制到用户空间
  6. 系统调用返回,线程继续在用户态执行

这种实现方式在并发连接较少时表现良好,但随着连接数增加,线程资源消耗(内存、上下文切换开销等)会迅速增长,导致系统性能下降。这就是所谓的”C10K问题”——传统BIO模型难以处理万级并发连接。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// BIO服务器示例
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// 阻塞直到有客户端连接
Socket clientSocket = serverSocket.accept();
// 为每个客户端创建一个新线程
new Thread(() -> {
try (InputStream in = clientSocket.getInputStream()) {
// 读取数据(阻塞操作)
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}

优缺点

优点

  • 设计简单,编程模型直观
  • 代码结构清晰,易于理解
  • 适合连接数较少且固定的应用

缺点

  • 性能低下,线程阻塞导致资源浪费
  • 可伸缩性差,线程数量增加会导致系统负担加重
  • 高并发场景下,频繁的线程创建和销毁会带来大量开销

NIO (Non-blocking I/O,非阻塞I/O)

概念与原理

NIO是一种非阻塞I/O模型,它允许线程在等待I/O操作完成的同时执行其他任务。NIO通过选择器(Selector)实现多路复用,使一个线程能够管理多个通道(Channel)的I/O操作,从而提高资源利用率。

工作机制

NIO的工作机制基于I/O多路复用和事件驱动。应用程序首先将通道注册到选择器上,指定关注的事件类型(如读、写、连接等)。然后通过选择器查询哪些通道已经准备好执行I/O操作。

在实际运行时,应用线程调用selector.select()方法等待事件发生。此方法可能会短暂阻塞,直到至少有一个通道准备就绪,或者超时。与BIO不同,这种阻塞是主动控制的,且一次阻塞可以等待多个I/O事件。

当有通道就绪时,select()方法返回,应用程序获取就绪通道的集合,然后遍历处理这些通道上的I/O事件。处理完成后,线程再次调用select()等待下一批事件。这种模式下,一个线程能够处理多个连接,大大提高了系统的并发处理能力。

用户态和内核态的切换方面,NIO在执行select()操作时会从用户态切换到内核态,然后返回时再切换回用户态。与BIO不同的是,一次select()调用可以处理多个通道的事件,减少了切换次数。当通道就绪后,读写操作也各需要一次用户态/内核态切换,但由于只处理已就绪的通道,不会发生阻塞等待。

底层实现

NIO底层依赖操作系统的I/O多路复用机制,如Linux的epoll、BSD的kqueue和Windows的IOCP等。以epoll为例,其实现过程如下:

  1. 应用程序创建epoll实例,相当于Java中的Selector对象
  2. 将需要监听的文件描述符(套接字)注册到epoll实例,指定关注的事件类型
  3. 调用epoll_wait()等待事件发生,这一步可能会短暂阻塞
  4. 当有事件发生时,内核将就绪的文件描述符和对应事件放入就绪列表
  5. epoll_wait()返回就绪事件的数量,应用程序遍历就绪列表处理I/O事件
  6. 处理完成后,再次调用epoll_wait()等待新的事件

这种机制避免了BIO中”一个连接一个线程”的资源消耗问题,实现了用较少的线程处理大量连接的目标。尤其是在连接数量多但活跃度不高的场景(如聊天服务器),NIO的优势更为明显。

需要注意的是,虽然NIO避免了不必要的阻塞,但应用程序需要自行处理数据的读取和缓冲,增加了编程复杂度。同时,由于采用轮询机制,若没有I/O事件发生,select()调用也会消耗CPU资源。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// NIO服务器示例
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);

Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
// 非阻塞或阻塞较短时间
selector.select();

Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();

if (key.isAcceptable()) {
// 处理新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
}
}
}
}

优缺点

优点

  • 单线程可以处理多个连接,提高资源利用率
  • 减少线程上下文切换的开销,提升系统性能
  • 适合高并发、大量连接但流量较小的场景

缺点

  • 编程复杂度高,学习曲线陡峭
  • 需要开发者自行处理数据的读写边界
  • 可能导致CPU负载较高,因为需要不断轮询就绪的通道

AIO (Asynchronous I/O,异步I/O)

概念与原理

AIO代表异步I/O,是最接近理想I/O模型的一种实现。在AIO模式下,应用程序发起I/O操作后立即返回,继续执行其他任务,当I/O操作完全完成(包括数据准备和数据从内核复制到用户空间)后,系统会通知应用程序。这是真正的非阻塞异步I/O模型。

工作机制

AIO的工作机制完全基于事件和回调。应用程序发起I/O请求时,会同时注册一个回调函数。之后,无论I/O是否完成,应用程序都可以执行其他任务,不需要等待或轮询。

当I/O操作完全完成后,操作系统会触发注册的回调函数,通知应用程序处理结果。这整个过程不需要应用程序主动查询I/O状态,实现了I/O操作与程序执行的完全分离。

从用户态和内核态切换的角度看,AIO在初始提交I/O请求时会有一次从用户态到内核态的切换。之后应用程序可以继续在用户态执行其他代码。当I/O操作完成时,内核会通过回调机制通知应用程序,此时会再发生一次从内核态到用户态的切换。与BIO和NIO相比,AIO的一个重要优势是完成数据从内核缓冲区到用户缓冲区的复制不需要应用程序参与。

底层实现

AIO的底层实现依赖于操作系统提供的异步I/O支持。在Windows系统上,AIO通过IOCP(I/O完成端口)实现;在Linux系统上,早期通过AIO系统调用实现,但功能有限,近期的高版本内核则提供了更强大的io_uring机制。

以Windows的IOCP为例,其实现过程如下:

  1. 应用程序创建I/O完成端口,并将套接字与之关联
  2. 应用程序发起异步I/O请求,同时提供完成回调函数或例程
  3. 应用程序继续执行其他任务,不需要等待I/O完成
  4. 当I/O操作完成时,系统将完成事件放入完成端口队列
  5. 应用程序中的工作线程从完成端口获取事件并处理
  6. 处理完成后,工作线程可以继续获取下一个完成事件

这种机制完全消除了应用程序对I/O状态的轮询,实现了更高效的资源利用。特别是在大文件传输、数据库访问等I/O密集型应用中,AIO能够显著提高系统吞吐量。

需要注意的是,由于AIO依赖操作系统的支持,在不同平台上的实现质量和性能可能有所差异。特别是在Linux系统上,传统的AIO支持较弱,只有近期的内核版本才提供了更完善的异步I/O支持。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// AIO服务器示例
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));

serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// 继续接受下一个连接
serverChannel.accept(null, this);

// 处理当前连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer buffer) {
if (bytesRead > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
// 可以继续读取或执行其他操作
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}

@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});

优缺点

优点

  • 真正的异步操作,I/O效率最高
  • 应用程序无需轮询,资源消耗更低
  • 适合I/O密集型应用,特别是读写操作耗时较长的场景

缺点

  • 编程模型更复杂,回调函数可能导致代码可读性下降
  • 依赖操作系统的异步I/O支持,在某些平台上实现不够完善
  • 调试和错误处理较为困难

三种I/O模型的对比

阻塞特性对比

  • BIO:完全阻塞,一个线程处理一个连接
  • NIO:非阻塞,但需要应用程序主动轮询I/O状态
  • AIO:完全非阻塞,异步通知I/O完成事件

编程复杂度

  • BIO:最简单,适合初学者
  • NIO:较复杂,需要理解Buffer、Channel、Selector等概念
  • AIO:最复杂,需要掌握回调和异步编程模式

性能对比

  • BIO:性能最低,不适合高并发场景
  • NIO:性能中等,适合连接数多但处理时间短的场景
  • AIO:理论上性能最高,适合I/O密集型应用

适用场景

  • BIO:连接数较少、业务处理简单的应用
  • NIO:高并发、需要管理大量连接的服务器应用
  • AIO:文件操作、数据库访问等I/O密集型应用

操作系统层面的I/O模型

从操作系统角度看,可以将I/O模型分为五类:

  1. 阻塞I/O(Blocking I/O):进程发起I/O系统调用后阻塞,直到I/O操作完成
  2. 非阻塞I/O(Non-blocking I/O):进程发起I/O系统调用后立即返回,需要轮询检查I/O是否完成
  3. I/O多路复用(I/O Multiplexing):通过select/poll/epoll等机制监控多个文件描述符
  4. 信号驱动I/O(Signal-driven I/O):进程发起I/O请求后继续执行,I/O就绪时接收信号通知
  5. 异步I/O(Asynchronous I/O):进程发起I/O请求后继续执行,I/O操作完全完成后接收通知

其中,Java的BIO对应阻塞I/O模型,NIO对应I/O多路复用模型,而AIO对应异步I/O模型。

I/O多路复用技术对比

Linux系统中的select、poll和epoll都属于I/O多路复用模型,它们的目标都是监控多个文件描述符,但实现机制和性能特点有所不同:

  1. select

    • 最早的I/O多路复用实现
    • 监控的文件描述符数量有限制(通常为1024)
    • 每次调用需要将整个文件描述符集合从用户态复制到内核态
    • 返回时需要遍历整个集合以找出就绪的描述符
    • 时间复杂度为O(n),随监控的描述符数量增加性能下降明显
  2. poll

    • select的改进版
    • 没有文件描述符数量的固定限制
    • 同样需要在每次调用时复制文件描述符集合
    • 也需要遍历整个集合查找就绪的描述符
    • 时间复杂度仍为O(n),大量描述符时性能同样会下降
  3. epoll

    • Linux特有的高性能I/O多路复用机制
    • 使用事件通知机制,只返回就绪的文件描述符
    • 描述符只在添加到监控集合时从用户态复制到内核态一次
    • 没有最大文件描述符数量的限制(除系统资源外)
    • 时间复杂度接近O(1),性能基本不随监控的描述符数量增加而下降
    • 在高并发场景下性能远优于select和poll

这三种机制都属于I/O多路复用模型,在Java的NIO实现中都可能被使用,具体使用哪种取决于操作系统平台。Java NIO会根据不同平台选择最优的多路复用实现,在Linux上优先使用epoll,在BSD系统上使用kqueue,在Windows上使用完成端口等。

实际应用举例

BIO应用场景

  • 简单的客户端应用
  • 连接数有限的小型服务器
  • 学习和教学环境

NIO应用场景

  • Netty网络应用框架
  • Tomcat 7+版本的连接器
  • Redis的Java客户端

AIO应用场景

  • 大型文件处理系统
  • 高性能数据库连接池
  • 实时消息处理系统

总结

理解BIO、NIO和AIO的区别对于设计高性能的网络应用至关重要。BIO提供了简单直观的编程模型但性能有限;NIO通过非阻塞I/O和多路复用提高了资源利用率;AIO则通过真正的异步操作进一步优化了I/O效率。根据应用场景的特点和需求,选择合适的I/O模型能够有效提升系统性能和可伸缩性。

在实际开发中,我们常常会结合使用这些I/O模型,或者通过成熟的框架如Netty间接使用它们,以便在保持代码简洁的同时获得高性能。

[操作系统]常见面试题--无序版

操作系统64位与32位的区别

内存寻址能力:64位操作系统通常最大可支持16EB的物理内存,32位由于地址空间的限制,最多只有4GB的物理内存
49aa9e63bf46602b175f49747ad27018.png
运算性能:64位CPU有更宽的通用寄存器和更大的指令集,一次可以处理更多数据

进程和线程的区别

  • 进程是系统资源分配和调度的最小单元,线程是CPU调度和分配的最小单位
  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源
  • 进程的创建、销毁、切换的开销大于线程
  • 线程不能独立执行,必须依存在进程中

线程快在哪?

  • 线程创建时有些资源不需要自己管理,直接从进程取用,线程只需要管理寄存器和栈的声明周期
  • 同一进程内多个线程共享数据,所以进程数据传输可以用零拷贝技术,不需要经过内核
  • 进程使用一个虚拟内存跟页表,然后多个线程共用这些虚拟内存,如果同进程内两个线程进行上下文切换比进程快很多

线程分为用户态线程和内核态线程,二者有什么区别?

用户态线程工作在用户空间,内核态线程工作在内核空间。用户态线程调度完全由进程负责,通常就是由进程的主线程负责。相当于进程主线程的延展,使用的是操作系统分配给进程主线程的时间片段。内核线程由内核维护,由操作系统调度。

用户态线程无法跨核心,一个进程的多个用户态线程不能并发,阻塞一个用户态线程会导致进程的主线程阻塞,直接交出执行权限。这些都是用户态线程的劣势。内核线程可以独立执行,操作系统会分配时间片段。因此内核态线程更完整,也称作轻量级进程。内核态线程创建成本高,切换成本高,创建太多还会给调度算法增加压力,因此不会太多。

实际操作中,往往结合两者优势,将用户态线程附着在内核态线程中执行。

说说协程

为了处理高并发、多连接下的数据读写,以往有两种解决方案。

  • 多进程:存在频繁调度切换的问题,同时HIA存在每个进程资源不共享的问题,需要额外引入进程间通信机制来解决。
  • 多线程:高并发场景的大量IO等待会导致多线程被频繁挂起和切换,非常消耗系统资源,同时多线程访问共享资源存在竞争问题

协程是一种比线程更加轻量级的微线程,一个线程可以拥有多个协程
协程运行在线程之上,当一个协程执行完成之后,可以选择主动出让,让另一个协程运行在当前线程之上。

协程并没有增加线程 数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。

因此,使用协程替换线程适用于有大量IO操作业务的情况下。一是降低了系统内存,二是减少了系统切换开销,因此系统的性能也会提升。
由于在协程中调用阻塞IO的方法会导致在该线程之上的所有协程都陷入阻塞。因此协程只有和异步IO结合起来才能发挥出最大的威力

进程通信

进程的用户地址空间是相互独立的,不可以互相访问,但内核空间是进程都共享的,所以进程之间要通信必须通过内核。进程间通信主要有管道、消息队列、共享内存、信号量、信号、Socket编程

  • 管道分为匿名管道和有名管道,管道是半双工通信的。管道的本质就是内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。

    • 匿名管道只能用于父子进程间的通信,有名管道只要可以访问路径就可以通过有名
      信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。
      信号的来源有硬件来源和软件来源,常见的有Ctrl+9 1111
  • Socket
    用于跨网络与不同主机上的进程进行通信,当然也可以完成同主机上的进程通信
    Socket的本质是一个编程接口(API)
    是应用层与TCP/IP协议的网络通信的抽象层。它对TCP/IP进行了封装,它把复杂的TCP/IP协议簇隐藏在Socket接口后面。对于用户来说,只需要进行一组简单的API就可以实现网络的连接C产生SIGINT信号,表示终止该进程;软件来源如kill -管道进行通信

    • 管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流
  • 消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会从消息队列中删除

    • 消息队列可以实现消息的随机查询,不一定要以先进先出的次序读取消息,也可以按消息类型读取,比有名管道的先进先出更有优势
    • 用户进程写入数据到消息队列时,会发生从用户态拷贝数据到内核态,用户进程读也会发生拷贝。如果数据量较大,,使用消息队列会造成频繁系统调用,会降低性能
  • 共享内存可以避免消息队列的拷贝消息、进行系统调用,允许不相干的进程将同一段物理内存连接到它们各自的地址空间中,使得这些进程可以访问同一个物理内存,这个物理内存就成为共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

    • 共享内存仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间来回拷贝,所以这是最快的一种进程通信方式。但是却容易造成数据冲突
  • 信号量,信号量是 一个整型计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据,保证共享内存在任何时刻只有一个进程在访问(保证互斥),并使得进程们能够按照某个特定的程序访问共享内存(同步)

    • 利用PV操作实现
    • P操作:将信号量值减一,表示申请占用一个资源。如果结果小于 0,表示已经没有可用资源,则执行 P 操作的进程被阻塞。如果结果大于等于 0,表示现有的资源足够你使用,则执行 P 操作的进程继续执行。
    • V操作:将信号量值加一,表示释放一个资源,即使用完资源后归还资源。
  • 信号

信号信号量完全不同!!!

信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。
信号的来源有硬件来源和软件来源,常见的有Ctrl+C产生SIGINT信号,表示终止该进程;软件来源如kill -9 1111

  • Socket
    用于跨网络与不同主机上的进程进行通信,当然也可以完成同主机上的进程通信
    Socket的本质是一个编程接口(API)
    是应用层与TCP/IP协议的网络通信的抽象层。它对TCP/IP进行了封装,它把复杂的TCP/IP协议簇隐藏在Socket接口后面。对于用户来说,只需要进行一组简单的API就可以实现网络的连接

信号量的值 大于 0 表示有共享资源可供使用,这个时候为什么不需要唤醒进程?

所谓唤醒进程是从就绪队列(阻塞队列)中唤醒进程,而信号量的值大于 0 表示有共享资源可供使用,也就是说这个时候没有进程被阻塞在这个资源上,所以不需要唤醒,正常运行即可。

信号量的值 等于 0 的时候表示没有共享资源可供使用,为什么还要唤醒进程?

V 操作是先执行信号量值加 1 的,也就是说,把信号量的值加 1 后才变成了 0,在此之前,信号量的值是 -1,即有一个进程正在等待这个共享资源,我们需要唤醒它。

使用信号量和 PV 操作实现进程的同步也非常方便,三步走:

  • 定义一个同步信号量,并初始化为当前可用资源的数量
  • 在优先级较的操作的后面执行 V 操作,释放资源
  • 在优先级较的操作的前面执行 P 操作,申请占用资源

虚拟内存的布局

49aa9e63bf46602b175f49747ad27018.png

虚拟内存是如何实现的

虚拟内存技术的实现是建立在离散分配的内存管理的基础上的

目前最常用的三种实现虚拟内存技术的方法:

  • 请求分页存储管理

  • 请求分段存储管理

  • 请求段页式存储管理
    以上三种方式,无论哪种,都需要以下三个条件:

  • 一定容量的内存和外存
    程序执行时,只需要将程序的一部分装入内存,就可以运行了

  • 缺页中断
    若需要执行的程序未在内存中(即“缺页/段”),则处理器会通知操作系统将相应的页/段调入到内存中,与此同时也会将不常用的页/段调出到外外存中

  • 虚拟地址空间
    逻辑地址转化为物理地址

用什么命令查看内存使用情况

top
这个命令会动态显示系统中各个进程的资源占用状况,包括CPU使用率和内存使用情况。按Shift + M可以按照内存使用量进行排序。

ps aux
该命令可以显示所有进程的详细信息,包括进程ID(PID)、用户、CPU和内存使用百分比等

  • top
  • pidstat
  • ps
  • pmap

select poll epoll

IO多路复用是用一个进程来维护多个Socket
而select poll epoll就是内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核获取多个事件
这三者都是在获取事件时,先把所有连接(文件描述符fd)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可

  • select
    将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生。检查的方式就是暴力的遍历
    当经检查到有事件产生后,将次Socket标记为可读或者可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
    所以,对于select这种方式,需要进行2次遍历文件描述符集合,一次在内核态、一次在用户态。还要发生2次拷贝文件描述符集合,先从用户空间传入内核空间中,再由内核修改后,再传出到用户空间中
    select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。

  • poll
    poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
    但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。

  • epoll

    • epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,红黑树的增删改一般时间复杂度是O(logn),由于有了红黑树来保存所有待检测的Socket,所有epoll机制不必要每次操作都传入整个Socket集合给内核,只需要传入一个待检测的Socket,减少了内核和用户空间大量数据拷贝和内存分配
    • epoll使用事件驱动的机制,其内核维护了一个链表来记录就绪事件,当某个Socket有事件发生时,通过回调函数,内核会将这个就绪事件加入到就绪时间列表当中,当用户调用epoll_wait函数时,只会返回有事件发生的文件描述符的个数,不需要轮询整个Socket集合,大大提高可检测效率