23-OS(22)
1. 用户态、内核态
用户态、内核态是OS中的两种运行模式,描述了不同的权限级别和访问方式
用户态(User Mode)
- 在用户态下,程序只能访问有限的资源和功能。eg:内存、CPU寄存器等
- 用户态下的程序不能直接OS的核心部分。eg:对硬件的直接访问
- 大多数应用程序在用户态下运行,包括常见的软件:浏览器、文字处理器等
内核态(Kernel Mode)
- 在内核态下,OS拥有对系统所有资源和硬件的完全控制权
- 内核态下的代码可以执行特权指令,访问所有内存区域,并处理中断和异常
- OS的核心部分,eg:调度程序、内存管理器等
用户态、内核态之间的切换由OS控制,通常发生在系统调用、中断或异常处理等情况下。当一个程序需要访问OS提供的服务或请求更高权限时,会触发从用户态到内核态的切换。这种切换的开销相对较高,因为涉及到保存和恢复进程的状态
2. 进程之间的通信方式
根据不同的场景选取不同的通信方式
- 管道(Pipes)
- 管道是一种单向通信方式,用于在父、子进程之间或同一主机上的不同进程之间传递数据。它可以是匿名的,也可以是命名的
- 命名管道(Named Pipes)
- 一种在内核中维护的消息链表,允许进程通过发送消息实现通信
- 与匿名管道类似,但具有一个在文件系统中有名的路径,允许不相关的进程之间进行通信
- 消息队列(Message Queues)
- 消息队列允许一个进程向另一个进程发送消息,消息在队列中按顺序存储,并且接收方可以按需接收
- 共享内存(Shared Memory)
- 共享内存允许多个进程访问同一块内存区域,从而实现快速的数据交换
- 但需要注意同步问题,以避免竞态条件和数据一致性问题
- 信号(Signals)
- 信号是一种异步通信方式,用于通知进程发生了某种事件
- 进程可以使用系统定义的信号(如SIGINT、SIGTERM等)或自定义信号,发送给目标进程
- 信号量(Semaphores)
- 信号量是一种计数器,用于控制对共享资源的访问,实现进程间的同步和互斥
- 通过对信号量的操作(增加、减少),进程可以进行互斥访问共享资源
- 套接字(Sockets)
- 是一种在网络编程中常用的通信方式,也可以用于本地进程间通信
- 文件(File)
- 进程可以通过读写文件来进行通信,这种方式通常用于进程之间的间接通信
- 相对于其他通信方式而言效率较低,适用于少量数据或需要长期保存数据的情况
1. 管道
import java.io.*;
public class PipeExample {
public static void main(String[] args) throws IOException {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
Thread writerThread = new Thread(() -> {
try {
pos.write("Hello from writer".getBytes());
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
});
Thread readerThread = new Thread(() -> {
try {
int data;
while ((data = pis.read()) != -1) {
System.out.print((char) data);
}
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
});
writerThread.start();
readerThread.start();
}
}
2. 套接字
import java.io.*;
import java.net.*;
public class SocketExample {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(9999);
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String message = in.readLine();
System.out.println("Received from client: " + message);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
out.println("Hello from server");
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.*;
import java.net.*;
public class SocketClientExample {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost", 9999);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("Hello from client");
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String response = in.readLine();
System.out.println("Received from server: " + response);
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 进程几种状态
- 创建态(New):进程正在被创建,OS正在为其分配资源
- 就绪态(Ready):进程已经准备好运行,但还未被调度执行。通常是因为等待 CPU 时间片而处于等待状态
- 运行态(Running):进程正在 CPU 上执行指令,处于活跃状态
- 阻塞态(Blocked):进程因为某些原因(eg:等待 I/O 完成、等待某个事件发生等)暂时无法执行,进入阻塞状态,直到条件满足后才能重新进入就绪态
- 终止态(Terminated):进程已经执行完毕,或者被OS提前终止,处于结束状态。在结束后,OS会回收其占用的资源
4. OS进程的调度算法
随着计算机系统的发展和应用场景的多样化,进程调度算法也在不断演化和完善。从最早的先来先服务(FCFS)到最新的多级反馈队列调度算法,每种算法都有着自己的特点和适用场景
1. 进程调度算法种类
- 先来先服务(FCFS):非抢占式调度算法,按顺序进行调度,短作业等待时间会由于前面长作业过长导致过长
- 短作业优先服务(SJF):最短运行时间有限,但存在饥饿问题
- 最短剩余时间有限(SRTN):按剩余运行时间的顺序进行调度,因为 CPU 时间片是只有非常短暂(毫秒),一个任务不会立刻被执行完毕,因此当有一个新作业到达时,将整个运行时间和当前进程的剩余时间进行比较。如果新进程需要的时间更少,就挂起当前运行的线程,优先运行新进程
- 时间片轮转调度算法:所有进程按先来先服务排成队列,CPU 时间片优先给队首进程,执行一个时间片,时间片用完,时钟终端,该队首进程停止,送到末尾,此时第二个进程就会被送到队首,但是时间片大小的控制十分重要
- 优先级调度:每个进程分配一个优先级,优先级调度算法
- 多级反馈队列:时间片轮转+优先级调度,多个队列时间片大小不同,每个队列的优先级也不同,可以根据实际情况进行调整
2. 进程调用函数
进程调用函数为OS提供了对进程的创建、执行、控制和终止等基本操作的支持,使得OS能够有效地管理进程,并实现进程之间的通信和协作
fork()
- 作用:创建一个新的进程,新进程是调用进程的副本(子进程),并行执行。子进程复制了父进程的内存空间、代码段、数据段等内容
- 返回值:在父进程中返回子进程的进程ID,在子进程中返回0
exec()
系列函数(eg:execvp
、execlp
等)- 作用:加载并执行一个新的程序文件,替换当前进程的地址空间和代码段为新程序的地址空间和代码段。常用于在一个进程中执行另一个程序
- 返回值:如果执行成功,则不返回;如果执行失败,则返回-1
wait()
、waitpid()
- 作用:父进程调用wait()或waitpid()函数来等待子进程的终止,并获取子进程的终止状态
- 返回值:成功时返回终止的子进程ID,失败时返回-1
exit()
- 作用:终止当前进程的执行,释放进程所占用的资源,并返回退出状态给父进程
- 参数:可以指定一个退出状态码,用于告知父进程当前进程的执行情况
kill()
- 作用:向指定的进程发送信号,用于终止进程、改变进程的行为等
- 参数:需要指定要发送的信号及接收信号的进程ID
signal()
- 作用:为当前进程安装信号处理器,用于处理接收到的各种信号
- 参数:指定信号的类型以及信号处理函数
5. 什么是软、硬中断
- 软中断:由OS、应用程序指令生成的中断。eg:系统调用(syscall)
- 硬中断:由硬件设备(eg:网卡、硬盘)发出的中断信号,用于通知 CPU 发生了某个事件,需要处理。当外部设备需要 CPU 的注意时,它会发送一个硬件中断信号给 CPU,CPU 会停止当前执行的程序,转而执行与该中断相关的中断处理程序
1. 区别
- 引发对象
- 硬中断是由外设引发的
- 软中断是执行中断指令产生的,无需外部施加中断请求信号
- 提供中断号
- 硬中断的中断号是由中断控制器提供的
- 软中断的中断号由指令直接指出,无需使用中断控制器
- 耗时
- 硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部
- 软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部
6. 什么是分段、分页
MMU(内存管理单元 Memory Management Unit)将虚拟地址翻译为物理地址的主要机制,其中两种就是分段和分页,第三种是段页
- 分段:将逻辑地址空间划分为若干个不同长度的段(segments),每个段代表程序中的一个逻辑单元。eg:代码段、数据段、堆栈段等。分段机制下的虚拟地址由两部分构成:段号和段内偏移量
- 分页:将逻辑地址空间和物理内存空间划分为固定大小的页(pages),通常为连续的 2 的幂大小。eg:4 KB或 4 MB。分页机制下的虚拟地址由两部分组成:页号和页内偏移量
1. 区别和联系
相同
- 都是非连续内存管理的方式
- 都是将虚拟地址映射到物理地址的机制
不同
- 分段会有外部内存碎片问题(内存块不连续,导致无法进行完整分配问题)
- 分页是从内存利用率的角度进行考虑,分段是从用户角度进行考虑,用于数据保护
- 分页的大小固定,由OS决定;分段大小不确定,由用户程序决定
7. 零拷贝
1. 传统拷贝流程图
2. 目的
零拷贝用于减少数据在内存之间的复制次数,从而提高数据传输的效率和性能
- 在零拷贝技术中,数据从一个存储位置移动到另一个存储位置时,系统不需要将数据从源位置复制到中间缓冲区,然后再从缓冲区复制到目标位置
- 相反,数据可以直接从源位置传输到目标位置,减少了复制操作
- 注意:零拷贝减少的是 CPU 拷贝和上下文切换次数, DMA 两次拷贝是必不可少的
3. 实现方式
mmap + Write
:直接从内核缓冲区拷贝到 Socket 缓冲区,减少一次到用户缓存区的次数,因此 CPU 拷贝少一次SendFile
技术:减少上下文切换次数,原本通过Read
和Write
系统调用,现在改为SengFile
只要调用一次,减少上下文切换,原本要四次,现在两次- 用户应用程序发出
sendfile
系统调用,上下文从用户态切换到内核态;然后通过 DMA 控制器将数据从磁盘中复制到内核缓冲区中 - 然后 CPU 将数据从内核空间缓冲区复制到 socket 缓冲区
sendfile
系统调用返回,上下文从内核态切换到用户态- DMA 异步将内核空间 socket 缓冲区中的数据传递到网卡
- 用户应用程序发出
- 采用 DMA 收集拷贝功能的
SendFile
:可以将 CPU 拷贝减少到 0 次。可以直接从内核空间缓冲区中将数据读取到网卡,无需将内核空间缓冲区的数据再复制一份到 socket 缓冲区,从而省去了一次 CPU 拷贝
8. 常用的Linux命令
ls
:列出目录中的文件和子目录cd
:切换当前工作目录pwd
:显示当前工作目录的路径mkdir
:创建新目录rm
:删除文件或目录cp
:复制文件或目录mv
:移动文件或目录cat
:连接文件并打印到标准输出grep
:在文件中搜索指定模式nano / vim
:文本编辑器,用于编辑文件chmod
:修改文件权限chown
:修改文件的所有者和所属组tar
:打包和解包文件gzip
或 gunzip:压缩和解压缩文件top
:显示系统中正在运行的进程和系统资源的使用情况ps
:显示当前运行的进程kill
:终止进程ifconfig
或 ip:配置网络接口信息ping
:测试与另一个主机的网络连接ssh
:通过安全的远程连接协议(SSH)登录到远程主机
9. I/O是什么
I/O(input / output)即输入/输出:指内存与外部设备之间的交互(数据拷贝)
- 磁盘 I/O:硬盘和内存之间的输入输出
- 读取本地文件的时候,要将磁盘的数据拷贝到内存中,修改本地文件的时候,需要把修改后的数据拷贝到磁盘中
- 网络 I/O:网卡与内存之间的输入输出
- 当网络上的数据到来时,网卡需要将数据拷贝到内存中。当要发送数据给网络上的其他人时,需要将数据从内存拷贝到网卡里
为什么都要跟内存交互呢?
指令最终是由 CPU 执行的,究其原因是 CPU 与内存交互的速度远高于 CPU 和这些外部设备直接交互的速度
10. 为什么网络I/O会被阻塞
- 数据传输速度不均衡:在网络通信中,发送方、接收方的数据传输速度可能不一致。当发送方发送数据快于接收方处理数据的速度时,接收方的缓冲区可能会满,导致发送方的 I/O 操作被阻塞,直到接收方处理完数据
- 网络拥塞:当网络中的流量过大或网络传输带宽不足时,会导致网络拥塞。在拥塞的情况下,数据包的传输可能会延迟或丢失,导致发送方的 I/O 操作被阻塞
- 阻塞式I/O操作:在一些传统的网络编程模型中,eg:阻塞式I/O,当进行网络读取或写入操作时,程序会一直等待直到数据就绪或发送完成。这种模型下,I/O 操作是同步阻塞的,会导致程序在等待数据的过程中被阻塞
- 低效的处理方式:当程序在处理 I/O 操作时,可能存在一些低效的处理方式,eg:使用循环轮询来检查数据的就绪状态。这种方式会导致 CPU 资源浪费,并且在等待数据就绪时会导致 I/O 操作阻塞
- 网络延迟(Latency):数据在网络中传输需要一定的时间,称为网络延迟。当数据在传输过程中受到延迟,程序可能会在等待数据返回时被阻塞
1. 解决方案
- 多线程处理: 使用多线程可以在进行网络通信时不阻塞主线程,从而提高程序的并发性能。通过将网络 I/O 操作放在单独的线程中进行,可以避免主线程被阻塞
- 非阻塞 I/O(Non-blocking I/O): 使用非阻塞 I/O 技术,程序可以在进行网络通信时立即返回,而不必等待数据的返回。通过轮询或事件驱动的方式,程序可以检查是否有数据可用,从而实现异步的网络通信
- 异步 I/O(Asynchronous I/O): 异步 I/O 允许程序在进行网络通信时不必等待数据的返回,而是通过回调或事件通知的方式在数据到达时进行处理。这种方式可以提高程序的并发性能和响应速度
- 使用缓存和流控制: 使用缓存可以减少对网络的频繁访问,提高了数据的读取和写入效率。同时,合理地使用流控制机制可以调整数据的传输速率,避免网络拥塞和阻塞
11. I/O模型
以 read
调用,即读取网络数据为例来展开 I/O 模型
1. 同步阻塞I/O
当用户程序的线程调用 read 获取网络数据时
- 首先这个数据得有,也就是网卡得先收到客户端的数据
- 然后这个数据有了之后,需要拷贝到内核中,然后再被拷贝到用户空间内,这整一个过程用户线程都是被阻塞的
- 假设没有客户端发数据过来,那么这个用户线程就会一直阻塞等着,直到有数据。即使有数据,那么两次拷贝的过程也得阻塞等着
优点:
- 简单。调用 read 之后就不管了,直到数据来了且准备好了进行处理即可
缺点:
- 一个线程对应一个连接,一直被霸占着,即使网卡没有数据到来,也同步阻塞等着
2. 同步非阻塞I/O
同步非阻塞I/O 基于同步阻塞I/O 进行了优化:
- 在没数据的时候可以不再傻傻地阻塞等着,而是直接返回错误,告知暂无准备就绪的数据!
- 注意,从内核拷贝到用户空间这一步,用户线程还是会被阻塞的
- 这个模型相比于同步阻塞 I/O 而言比较灵活。eg:调用 read 如果暂无数据,则线程可以先去干干别的事情,然后再来继续调用 read 看看有没有数据
但是如果用户线程就是取数据然后处理数据,不干别的逻辑,这个模型又有问题
- 等于你不断地进行系统调用,如果你的服务器需要处理海量的连接,那么就需要有海量的线程不断调用,上下文切换频繁
3. I/O多路复用
- 多路:指的是多条连接
- 复用:指的是用一个线程就可以监控多条连接
和同步非阻塞 I/O 差不多啊,其实不太一样,线程模型不一样
- 只用一个线程查看多个连接是否有数据已准备就绪
- 往
select
注册需要被监听的连接,由select
来监控所管理的连接是否有数据已就绪,如果有则可以通知别的线程来read
读取数据,这个 read 和之前的一样,还是会阻塞用户线程
用少量的线程去监控多条连接,减少了线程的数量,降低了内存的消耗且减少了上下文切换的次数
4. 信号驱动式I/O
select
虽然不阻塞了,但是得轮询查询是否有数据已经准备就绪,是否可以让内核通知我们数据到了
- 信号驱动 I/O,由内核告知数据已准备就绪,然后用户线程再去
read
(还是会阻塞)
为什么市面上用的都是 I/O 多路复用而不是信号驱动?
- 因为应用通常用的都是 TCP 协议,而 TCP 协议的 socket 可以产生信号事件有七种
- 也就是说不仅仅只有数据准备就绪才会发信号,其他事件也会发信号,而这个信号又是同一个信号,所以应用程序无法区分到底是什么事件产生的这个信号
所以应用基本上用不了信号驱动 I/O,如果应用程序用的是 UDP 协议,那是可以的,因为 UDP 没这么多事件
5. 异步I/O
信号驱动 I/O 虽然对 TCP 不太友好,但是这个思路对的:往异步发展,但是它并没有完全异步,因为其后面那段 read
还是会阻塞用户线程,所以算是半异步
- 思路清晰:让内核直接把数据拷贝到用户空间之后再告知用户线程,来实现真正的非阻塞I/O!
- 异步 I/O 就是用户线程调用
aio_read
- 然后包括将数据从内核拷贝到用户空间那步,所有操作都由内核完成
- 当内核操作完毕之后,再调用之前设置的回调,此时用户线程就拿着已经拷贝到用户控件的数据可以继续执行后续操作
在整个过程中,用户线程没有任何阻塞点,这才是真正的非阻塞I/O
12. 同步、异步区别
同步、异步指的是:当前线程是否需要等待方法调用执行完毕
- 异步调用:代码逻辑相对而言不太直观,需要借助回调或事件通知,这在复杂逻辑下对编码能力的要求较高
- 同步调用:直来直去,等待执行完毕拿到结果紧接着执行下面的逻辑,对编码能力的要求较低,也更不容易出错
所以会发现有很多方法它是异步调用的方式,但是最终的使用还是异步转同步
- 向线程池提交一个任务,得到一个 future,此时是异步的
- 然后你在紧接着在代码里调用
future.get()
,那就变成等待这个任务执行完成,这就是所谓的异步转同步 - 像 Dubbo RPC 调用同步得到返回结果就是这样实现的
13. 阻塞、非阻塞区别
阻塞、非阻塞指的是:当前接口数据还未准备就绪时,线程是否被阻塞挂起
- 阻塞: 当前线程还处于 CPU 时间片当中,调用了阻塞的方法,由于数据未准备就绪,则时间片还未到就让出 CPU
- 非阻塞: 当前接口数据还未准备就绪时,线程不会被阻塞挂起,可以不断轮询请求接口,看看数据是否已经准备就绪
结论:
- 同步&异步:当数据还未处理完成时,代码的逻辑处理方式不同
- 阻塞&非阻塞:当数据还未处理完成时(未就绪),线程的状态
- 阻塞和同步看起来都是等,但是本质上不一样,同步的时候可没有让出 CPU
14. 同步,异步,阻塞,非阻塞I/O区别
不同场景下同一个名词意义可能不同。这篇关于同步、异步、阻塞、非阻塞这几个概念是基于 I/O 场景
前提:程序和硬件之间隔了个OS,而为了安全考虑,Linux 系统分了:用户态和内核态
I/O 操作有两个步骤:
- 发起 I/O 请求
- 实际 I/O 读写,即数据从内核缓存拷贝到用户空间
阻塞 I/O 和非阻塞 I/O。按照上文,其实指的就是用户线程是否被阻塞,这里指代的步骤1(发起I/O请求)。
- 阻塞 I/O:指用户线程发起 I/O 请求时,如果数据还未准备就绪(eg:暂无网络数据接收),就会阻塞当前线程,让出 CPU
- 非阻塞 I/O:指用户线程发起 I/O 请求时,如果数据还未准备就绪(eg:暂无网络数据接收),也不会阻塞当前线程,可以继续执行后续的任务
可以发现,这里的阻塞和非阻塞其实是指用户线程是否会被阻塞
同步 I/O 和异步 I/O。根据 I/O 响应方式不同而划分的
- 同步 I/O:指用户线程发起 I/O 请求时,数据是有的,那么将进行步骤2(实际 I/O 读写),这个过程用户线程是要等待着拷贝完成
- 异步 I/O:指用户线程发起 I/O 请求时,数据是有的,那么将进行步骤2(实际 I/O 读写),拷贝的过程中不需要用户线程等待,用户线程可以去执行其它逻辑,等内核将数据从内核空间拷贝到用户空间后,用户线程会得到一个“通知”
在 I/O 场景下同步和异步说的其实是内核的实现,因为拷贝的执行者是内核
关于 I/O 的阻塞、非阻塞、同步、异步:
- 阻塞和非阻塞指的是发起 I/O 请求后,用户线程状态的不同,阻塞I/O在数据未准备就绪的时候会阻塞当前用户线程,而非阻塞 I/O 会立马返回一个错误,不会阻塞当前用户线程
- 同步和异步是指,内核的 I/O 拷贝实现,当数据准备就绪后,需要将内核空间的数据拷贝至用户空间,如果是同步 I/O 那么用户线程会等待拷贝的完成,而异步 I/O则这个拷贝过程用户线程继续执行,当内核拷贝完毕之后会“通知”用户线程
15. BIO、NIO、AIO
BIO、NIO、AIO 都是 Java 的 IO 模型
- BIO(Blocking IO):传统的 IO 模型,它在读写数据时会阻塞线程,直到数据读写完成,适用于并发不高的场景
- NIO(Non-blocking IO):是 Java 新 IO 模型,它在读写数据时不会阻塞线程,而是通过轮询的方式检查是否有数据可读写,适用于并发量较高的场景
- AIO(Asynchronous IO):是 JDK 7 开始引入的新 IO 模型,它的读写方式与 NIO 相似,但在读写数据时,不需要自己手动轮询是否有数据可读写,而是交由系统完成,适用于高并发且处理较大数据量的场景
总的来说,BIO 的并发处理能力较差,NIO 的并发处理能力较好,但使用起来较为复杂,AIO 的并发处理能力最好,但也是最为复杂的一种 IO 模型。选择适合自己场景的 IO 模型是非常重要的
- BIO:同步阻塞I/O,在这种模型下只能是来一个连接用一个线程,连接多并发大的话服务器顶不住这么多线程的
- NIO:同步非阻塞I/O,熟知的 IO 多路复用就是 NIO,适合用在连接多、每次传输较为短的场景
- AIO:异步I/O,调用了之后就不管了,数据来了自动会执行回调方法。异步可以有效的减少线程的等待,减少了用户线程拷贝数据的那段等待,效率更高
16. 什么是Channel
- 往通道里写数据,也可以从通道里读数据,它是双向的
- 而与之配套的是 Buffer,也就是往一个通道里写数据,必须要将数据写到一个 Buffer 中,然后写到通道里。从通道里读数据,必须将通道的数据先读取到一个 Buffer 中,然后再操作
在 NIO 中 Channel 有多种类型
1. SocketChannel
- 对标 Socket,可以直接将它当做所建立的连接
- 通过 SocketChannel ,利用 TCP 协议进行读写网络数据
2. ServerSocketChannel
- 对标 ServerSocket,服务端创建的 Socket
- 作用:监听新建连的 TCP 连接,为一个新连接创建对应的 SocketChannel。通过新建的 SocketChannel 就可以进行网络数据的读写,与对端交互
3. DatagramChannel
- UDP 协议,是无连接协议
- 利用 DatagramChannel 可以直接通过 UDP 进行网络数据的读写
4. FileChannel
- 文件通道,用来进行文件的数据读写
- 客户端:客户端创建一个 SocketChannel 用于连接至远程的服务端
- 服务端:服务端利用 ServerSocketChannel 绑定一个端口,然后监听新连接的到来。接收新连接之后,为其创建一个 SocketChannel
随后,客户端和服务端就可以通过这两个 SocketChannel 相互发送和接收数据
17. 什么是Buffer
Buffer:内存中可以读写的一块地方,叫缓冲区,用于缓存通道的读写数据
- Java NIO Buffer 的 API,这个 API 很不好用,稍微漏写了点,就容易出 bug,而且还有很多优化之处。所以 Netty 没用 Java NIO Buffer 而是自己实现了一个 Buffer,叫 ByteBuf
为什么 Channel 必须和 Buffer 搭配使用?
其实网络数据是面向字节的,但是我们读写的数据往往是多字节的,假设不用 Buffer ,就得一个字节一个字节的调用读和调用写,效率低。用buffer,才能更好地处理完整的数据,方便异步的处理等
18. 什么是Selector
I/O多路复用的核心
- 一个 Selector 上可以注册多个 Channel ,一个 Channel 就对应了一个连接,因此一个 Selector 可以管理多个 Channel
- 当任意 Channel 发生读写事件时,通过
Selector.select()
就可以捕捉到事件的发生,因此利用一个线程,死循环的调用Selector.select()
,相当于利用一个线程管理多个连接,减少了线程数,减少了线程的上下文切换,节省了线程资源
// 1. 创建一个 Selector
Selector selector = Selector.open();
// 2. 被管理的 Channel 注册到 Selector 上,并声明感兴趣的事件
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
// 3. 事件一共有四种类型,同时对多种类型的事件感兴趣
SelectionKey key = channel.register(selector, Selectionkey.OP_READ | SelectionKey.OP_WRITE);
// 4. 当 Channel 发生读或写事件,可以得知有事件发生
Selector.select()
// 三个重载方法
int selectNow() // 不论是否有无事件发生,立即返回
int select(long timeout) // 至多阻塞 timeout 时间(或被唤醒),如果提早有事件发生,提早返回
int select() // 一直阻塞着,直到有事件发生(或被唤醒)
// 返回值就是就绪的通道数,一般判断大于 0 即可进行后续的操作
// 5. 获得了一个类型为 Set 的 selectedKeys 集合
Set selectedKeys = selector.selectedKeys();
SelectedKeys
的方法和成员:
- 得知当前发生的是什么事件,有
isAcceptable
、isReadable
等 - 获得对应的 channel 进行相应的读写操作,还有获取 attachment 等
- 可以通过迭代器遍历所有发生事件的连接,然后进行操作
while(true) {
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove(); // 执行完毕之后,需要在循环内移除自己
}
}
还有个方法就是 Selector.wakeup()
,可以唤醒阻塞着的 Selector
// 如果 Channel 要和 Selector 搭配,那它必须得是非阻塞的,即配置
channel.configureBlocking(false);
Selector 处理事件时,必须快,如果长时间处理某个事件,那么注册到 Selector 上的其他连接的事件就不会被及时处理,造成客户端阻塞
19. 什么是Reactor
是一个编程模式或者说一个架构模式,常用在服务端网络通信相关的模块
- Reactor 直译到中文是反应堆,虽然直译不贴切,但确实跟“反应”有关
- Reactor 核心:能一对多的根据不同请求做出不同响应
- 场景:在网络 I/O 中基于非阻塞 I/O,且需要少量的线程接待大量的连接
- 一个线程接待一个连接,每当连接上有事件产生,则线程会对应事件作出响应
- 对应着很早之前的网络编程模型,那时候网上没那么多用户,并且也就只有阻塞 I/O,如果连接没有反应,那么线程就得阻塞等待着 I/O 事件的发生
- 由一个线程接待多个连接,不论哪个连接上有事件产生,这个线程都会根据事件找到对应的处理逻辑来作出响应
- 对应现在流行的基于非阻塞 I/O 的 I/O 多路复用,这种场景就适合海量用户的情况,服务端可用很少线程来处理数量庞大的连接
Reactor 论文图:
官方回答:
- Reactor 是服务端在网络编程时的一个编程模式,主要由一个基于 Selector (底层是
select/poll/epoll
)的死循环线程,也称为 Reactor 线程。将 I/O 操作抽象成不同的事件,每个事件都配置对应的回调函数,由 Selector 监听连接上事件的发生,再进行分发调用相应的回调函数进行事件的处理
20. Select、Poll、Epoll区别
Select、Poll 和 Epoll 是用来实现 I/O 多路复用的机制,主要用于同时监视多个文件描述符的状态变化,包括可读、可写和异常事件。通过这些机制,可以在单个线程中同时处理多个 I/O 操作,提高系统的性能和效率
1. Select
- 最古老的一种 I/O 多路复用机制,可用于同时监视多个文件描述符的状态变化
- 使用了一个 fd_set 集合来保存需要监视的文件描述符,并提供了三个不同的集合来分别表示可读、可写、异常事件
- 缺点:效率较低,因为它采用轮询的方式来检查每个文件描述符的状态变化,当文件描述符数量较大时,效率会明显下降
2. Poll
- 对 Select 的改进,使用一个 pollfd 结构体数组来保存需要监视的文件描述符及其关注的事件
- 不受文件描述符数量的限制,因为它是基于链表实现的,不需要遍历整个集合
- 效率比 Select 高一些,但仍然不适用于大规模的连接
3. Epoll
- 是 Linux 下的一种高性能 I/O 多路复用机制,相比于 Select 和 Poll,它具有更高的性能和更好的扩展性
- 使用了一个事件表来保存需要监视的文件描述符,并使用回调机制来通知应用程序发生的事件
- 采用了内核事件通知机制,可以避免轮询,只有在有事件发生时才会唤醒应用程序,因此效率更高
- 支持
Edge-triggered
模式和Level-triggered
模式,可以根据需要选择合适的模式
21. 什么是物理、逻辑地址
物理地址: 是内存中存储单元的实际地址,也称为真实地址。它是指计算机系统中 RAM(随机访问存储器)中的每个存储单元的唯一标识。在OS的内存管理中,物理地址是指内存中实际存放数据的位置,由硬件控制器直接访问
逻辑地址: 是程序中使用的虚拟地址,也称为虚拟地址。它是指程序中对内存的抽象表示,与实际的物理存储位置无关。逻辑地址由程序员或OS定义,用于访问内存中的数据。在计算机系统中,逻辑地址被映射到物理地址上,这个过程由内存管理单元(MMU)负责完成。逻辑地址空间可以大于物理地址空间,因为使用了分页或分段等技术,允许对内存进行灵活的管理和地址映射
1. 逻辑地址转换成物理地址?
地址转换:OS通过 CPU 芯片中的一个组件 MMU(Memory Management Unit,内存管理单元) 将逻辑(虚拟)地址转换为物理地址
2. 逻辑地址作用
- 内存管理: 逻辑地址提供了一种抽象的方式来管理内存。通过使用逻辑地址,程序可以将其内存访问操作与实际的物理存储位置解耦。这使得OS能够对内存进行更灵活的管理,包括内存的分配、回收和保护
- 地址空间隔离: 逻辑地址空间可以使不同的程序或进程彼此隔离,从而确保它们不会相互干扰或访问对方的内存空间。每个程序都有自己的逻辑地址空间,其中包含了程序执行所需的代码、数据和堆栈等信息
- 虚拟化和抽象: 逻辑地址提供了一种虚拟化和抽象的方式,使得程序员可以更方便地编写和管理程序,而无需关注底层的物理硬件细节。这种抽象使得程序能够在不同的计算机系统上运行,而无需修改其代码