一、操作系统概念
1、内核态和用户态
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺。
为什么要有用户态和内核态?
由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据。并发送到网络,CPU划分出两个权限等级
用户态和内核态。
什么时候会发生内核态和用户态的切换
【用户态在需要申请外部资源的时候会切换至内核态】。比如执行系统调用、发生中断、异常等,内核态执行完成会回退至用户态。
2、系统调用
由操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口(Applicaion Programming Interface,AP)。【应用程序】
同系统之间的接口。
系统调用是操作系统开发的接口,开发者可以使用【系统调用】获取系统资源。就是操作系统的代码开放了一些接口让你使用,比如创建个文件,读取个文件。
常见的系统调用如下:
1、和进程、线程相关fork创建一个子进程
2、文件相关的creat chmod chown read从一个文件描述符中读取内容write——向一个文件描述符中写入内容close——关闭文件描述符
3、设备相关的 read write
4、信息相关的 get…
5、通信相关的pipe
3、系统中断
中断的分类:
【中断源】是指能够引起中断的原因。一台【处理器】可能有很多中断源但按其性质和处理方法,大致可分为如下五类。
1、机器故障中断,比如掉电。
2、程序性中断。现行程序本身的异常事件引起的,可分为以下三种:一是程序性错误,非法操作和除数为零等;二是产生特殊的运算结果,例如定点溢出;三是程序出现某些预先确定要跟踪的事件,跟踪操作主要用于程序调试。有些机器把程序性中断称为“异常”,不称为中断。
3、输入-【输出设备】中断,10中断。
4、外中断。来自控制台【中断开关】、计时器、时钟或其他设备,这类中断的处理较简单,实时性强。
5、调用管理程序。用户程序利用专用指令“调用管理程序”发【中断请求】,是用户程序和操作系统之间的联系桥梁。
系统中断有什么好处:
1、分时操作,解决cpu的快速处理和慢速IO设备的问题。
2、实时处理,word中可以一边打字一边做拼写检查。
3、故障处理,会优先处理鼓掌。
4、DMA
DMA(Direct Memory Access,直接存储器访问),它允许不同速度的硬件装置来沟通,而不需要依赖于CPU的大量中断负载。否则,CPU需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU对于其他的工作来说就无法使用。
当cpu需要访问外设(磁盘、网卡、usb)的数据时,将任务丢给DMA,有DMA负责利用总线将数据先拷贝到内存,DMA传输前,CPU要把总线控制权交给DMA控制器,而在结束DMA传输后,DMA控制器应立即把总线控制权再交回给CPU.传输结束后,发出中断信号,通知cpu。
5、数据结构位图bitmap
有一个场景:需要你统计你的同事的一个月的打卡记录。
你要怎么做,创建三十几个变量,0代表没打卡,1代表也打卡?
事实上我们使用一个int能表示:
11111111 10101111 11111111 11111110
一个int四个字节,就是三十二位,从第0位开始算第一天的打卡记录,那么有三十二位足够了,因为一个月最多也就31天。
我们能很简单的看出他第10天和12天没有打卡。
二、NIO概览
1、NIO简介
Java NIO 是 java 1.4, 之后新出的一套IO接口NIO中的N可以理解为Non-blocking,不单纯是New。
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
2、NIO的特性/NIO与IO区别:
-
IO是面向流的,NIO是面向缓冲区的;
-
IO流是阻塞的,NIO流是不阻塞的;
-
NIO有选择器,而IO没有。
3、读数据和写数据方式:
从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。
4、NIO相关的系统调用
1、Select系统调用
那么select的具体流程是什么呢?
1、应用程序创建socket,生成文件描述符,并生成bitmap,使用hash的方式将bitmap的对应位置置一。
2、执行系统调用,将bitmap拷贝至内核空间,根据bitmap遍历对应的文件描述符,一旦有事件产生就返回。
3、用户程序遍历文件描述符,处理请求。
4、应用程序不停的调用select即可。
select模型已经很不错了,但是依然有不足的地方:
1.bitmap位图上限是1024,所以能监控的fd最多也就这么多。
2.fset位图不可重用,每次赋值全部清零,状态全部丢失。
3.fset位图需要不断的进行用户空间到内核空间的拷贝。
4.每次查找时间复杂度都是O(n)。
说句实话,如果没有更好的选择方案,这都不是问题。
2、Poll系统调用
Poll工作原理与Select基本相同,不同的只是将位图数组改成数组,也有资料说是链表,没有了最大连接数1024的限制,依然有fd集合的拷贝和O(n)的遍历过程。
3、Epoll系统调用
为解决fd集合拷贝的问题,epoll采用用户态和内核态共享epoll_fds集合。当调用epoll_wait系统调用时,内核态会去检查有哪些fd有事件,检查完毕后会将共享的epoll_fds集合重排序,将有事件的fd放在前面,并返回有事件的fd个数。
客户端收到返回的个数,就不需要全部遍历,而是直接处理fd。
三、NIO编程
Java NlO三大核心部分
1.Bufer(缓冲区):每个客户端连接都会对应一个Buffer,读写数据通过缓冲区读写。
2.Channel(通道):每个channel用于连接Buffer和Selector,通道可以进行双向读写。
3.Selector(选择器):一个选择器可以对应多个通道,用于监听多个通道的事件。Selector可以监听所有的channel是否有数据需要读取,当某个channel有数据时,就去处理,所有channel都没有数据时,线程可以去执行其他任务。
1、Buffer(缓冲区)
Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels;
Buffer本质上就是一块内存区;
一个Buffer有三个属性是必须掌握的,分别是:capacity容量、position位置、limit限制。
Buffer的常见方法:
-
Buffer clear()
-
Buffer flip()
-
Buffer rewind()
-
Buffer position(int newPosition)
public static void main(String[] args) {
//创建一个Int型的buffer,大小为5。相当于创建了一个大小为5的int数组
IntBuffer buffer = IntBuffer.allocate(5);
//往buffer中添加数据
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put(i);
}
//buffer读写切换,之前为写数据,调用flip后切换为读
buffer.flip();
//读取数据
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
}
2、Channel
NIO的Channel通道类似于流,但是通道可以同时读写,而流只能读或写。
Channel只是一个接口,里面有各种实现类。
通过FileChannel和ByteBuffer将数据写入文件。
public static void main(String[] args) throws IOException {
//创建一个文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("E://a.txt");
//通过文件输出流得到一个FileChannel
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个buffer并写入数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("swimcode".getBytes());
//方转,让指针指向数组开头
buffer.flip();
//将Buffer中数据写入FileChannel中
fileChannel.write(buffer);
fileChannel.close();
fileOutputStream.flush();
fileOutputStream.close();
}
3、Selector
服务端
public static void main(String[] args) throws IOException {
// 1. 创建服务端监听 channel 并配置为非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 2. 配置 channel 监听的套接字
serverSocketChannel.bind(new InetSocketAddress(8080));
// 3. 创建IO多路复用选择器并把通道注册到 Selector 上,
// 并注册感兴趣的事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 4. 在死循环中不断监听
while (true) {
//阻塞的方法。返回值代表事件的通道的个数
// 0 超时
//-1 错误
int select = selector.select();
if (select == 0) {
continue;
}
//只要走到这里,必然说明,发生了事情,有可读,可写,可连接的channel
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 取消选择键
iterator.remove();
// 5. 处理 accept 就绪事件
if (key.isAcceptable()) {
System.out.println("=== 服务端收到 accept 事件 ===");
// 5.1 获取来自客户端的连接,创建 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
// 5.2 把该 Channel 注册到 selector 上(也就是理论上的把多个客户端的channel注册到selector上,用一个线程轮询)
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 6. 处理 selector 上的 read 事件
if (key.isReadable()) {
// 6.1 建立与 client 的通信链路
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = -1;
// 6.2 把 client 发送过来的数据读到 buffer 里面,并输出
while ((len = channel.read(byteBuffer)) != -1) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
}
}
}
}
客户端
public static void main(String[] args) throws Exception {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress("localhost", 8080));
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if (selectionKey.isConnectable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
if (client.isConnectionPending()) {
client.finishConnect();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((LocalDateTime.now() + " 连接成功").getBytes());
writeBuffer.flip();
client.write(writeBuffer);
ExecutorService executorService = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
executorService.submit(() -> {
while (true) {
try {
writeBuffer.clear();
InputStreamReader input = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(input);
String sendMsg = br.readLine();
writeBuffer.put(sendMsg.getBytes());
writeBuffer.flip();
client.write(writeBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);
if (count > 0) {
String receivedMsg = new String(readBuffer.array(), 0, count);
System.out.println(receivedMsg);
}
}
}
selectionKeys.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
client.read(readBuffer);
if (count > 0) {
String receivedMsg = new String(readBuffer.array(), 0, count);
System.out.println(receivedMsg);
}
}
}
selectionKeys.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}