之前一直对IO一知半解,所以整理下IO各种概念与IO读取数据的流程,先了解一下专有名词:
(1)用户空间 / 内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
(2)进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。
(3)进程阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
(4)文件描述符
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统而windows为句柄的概念。
(5)、句柄
所谓句柄实际上是一个数据,是一个Long (整长型)的数据。
句柄是WONDOWS用来标识被应用程序所建立或使用的对象的唯一整数,WINDOWS使用各种各样的句柄标识诸如应用程序实例,窗口,控制,位图,GDI对象等等。WINDOWS句柄有点象C语言中的文件句柄。
从上面的定义中的我们可以看到,句柄是一个标识符,是拿来标识对象或者项目的,它就象我们的姓名一样,每个人都会有一个,不同的人的姓名不一样,但是,也可能有一个名字和你一样的人。从数据类型上来看它只是一个16位的无符号整数。应用程序几乎总是通过调用一个WINDOWS函数来获得一个句柄,之后其他的WINDOWS函数就可以使用该句柄,以引用相应的对象。
如果想更透彻一点地认识句柄,我可以告诉大家,句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是住留在内存的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是,如果您真的这样认为,那么您就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,依此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?
为了解决这个问题,Windows操作系统为各应用程序腾出一些内存储地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的。Windows内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配给的,当系统卸载时(Unload)又释放给系统。
句柄地址(稳定)→记载着对象在内存中的地址────→对象在内存中的地址(不稳定)→实际对象,WINDOWS程序中并不是用物理地址来标识一个内存块,文件,任务或动态装入模块的,相反的,WINDOWS API给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作
(6)缓存I/O
缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
(7)网络分组
分组的概念是大多数计算机网络都不能连续地传送任意长的数据,所以实际上网络系统把数据分割成小块,然后逐块地发送,这种小块就称作分组(packet)
IO读写经历的两个过程:
阶段1 wait for data 等待数据准备
阶段2 copy data from kernel to user 将数据从内核拷贝到用户进程中
IO,其实意味着:数据不停地搬入搬出缓冲区而已(使用了缓冲区)。比如,用户程序发起读操作,导致“ syscall read ”系统调用,就会把数据搬入到 一个buffer中;用户发起写操作,导致 “syscall write ”系统调用,将会把一个 buffer 中的数据 搬出去(发送到网络中 or 写入到磁盘文件)。DMA(Direct Memory Access,直接内存存取,不需要CPU参与) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。
整个IO过程的流程如下:
1)程序员写代码创建一个缓冲区(这个缓冲区是用户缓冲区):哈哈。然后在一个while循环里面调用read()方法读数据(触发"syscall read"系统调用)
byte[] b = new byte[4096];
while((read = inputStream.read(b))>=0) {
total = total + read;
// other code…
}
2当执行到read()方法时,其实底层是发生了很多操作的: ①内核给磁盘控制器发命令说:我要读磁盘上的某某块磁盘块上的数据。–kernel issuing a command to the disk controller hardware to fetch the data from disk. ②在DMA的控制下,把磁盘上的数据读入到内核缓冲区。–The disk controller writes the data directly into a kernel memory buffer by DMA ③内核把数据从内核缓冲区复制到用户缓冲区。–kernel copies the data from the temporary buffer in kernel space 这里的用户缓冲区应该就是我们写的代码中 new 的 byte[] 数组。
操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。
对于操作系统而言,JVM只是一个用户进程,处于用户态空间中。而处于用户空间的进程是不能直接操作底层的硬件的。而IO操作就需要操作底层的硬件,比如磁盘。因此,IO操作必须得借助内核的帮助才能完成(中断,trap),即:CPU切换执行的进程会有用户态到内核态的切换。
我们写代码 new byte[] 数组时,一般是都是“随意” 创建一个“任意大小”的数组。比如,new byte[128]、new byte[1024]、new byte[4096]…,即用户缓冲区,但是,对于磁盘块的读取而言,每次访问磁盘读数据时,并不是读任意大小的数据的,而是:每次读一个磁盘块或者若干个磁盘块(这是因为访问磁盘操作代价是很大的,而且我们也相信局部性原理) 因此,就需要有一个“中间缓冲区”–即内核缓冲区。先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区。这也是为什么我们总感觉到第一次read操作很慢,而后续的read操作却很快的原因吧。因为,对于后续的read操作而言,它所需要读的数据很可能已经在内核缓冲区了,此时只需将内核缓冲区中的数据拷贝到用户缓冲区即可,并未涉及到底层的读取磁盘操作,当然就快了
网络io分为多种,具体内容看如下链接: 五种网络IO,同步异步,阻塞非阻塞
套接字soecktIO是一种IO,因为内核创建一个套接字相当于是创建一个文件描述符,对文件的读写其实就是IO操作。当我们进程种调用socket的read(),write()方法实际上是调用内核的read(),write()方法来进行数据的读取的与写入操作的。
使用场景:java中的NIO的SelectorImpl类中就实现了IO的多路复用,从客户端链接到服务端的socket,在服务端都注册到Selector类上的一个列表里返回一个selectKey(代表选择器与socket的绑定关系,还有这个socket),然后selector调用内核的selec接口或则epllo接口,把socket列表(即文件描述符列表(文件描述符用一个long整形数值表示))与socket感兴趣的事件传到select接口的参数列表,由内核完成对socket列表的遍历,监听每个socket是否有相应的事件发生,如果有相应的socket感兴趣的事件发生,则把socket的数据结构相应的事件发生标识修改。再由selector调用selectionKeys()。返回所有的socket当前的状态,遍历selectionKeys。判断每个key的状态,然后进行事件的处理。
多路复用技术:多路指多个socket,复用指用一个线程处理多个socket,或一个线程池处理多个socket多路复用技术的底层是使用select,poll,epoll三种操作系统提供的方法与NIO的特性。操作系统提供的多路复用的三种技术的区别
socket对于linux为一个文件描述符,对于windows系统为一个句柄。要建立两个不同主机间进行网络通信,就必须有一个为服务端,一个为客户端。
服务端流程
一、服务端启动先调用socket()内核接口创建一个文件描述符即ServerSocket,这个socket数据结构里没有远程客户端的地址与端口,(1)、调用bind接口这个socket是用来绑定本机地址与端口的 。(2)、然后调用listen接口,告诉内核在我这个ServerSocket上,监听是否有客户端的链接进来,内核就会建立两个队列,一个SYN队列,表示接受到请求,但未完成三次握手的连接,另一个是ACCEPT队列,表示已经完成了三次握手的队列,内核监听到有链接进来就根据情况放到这两个队列中。(3)调用accept()接口,这个接口会阻塞调用的线程(如何设置获取阻塞套接字即阻塞IO),直到内核的accept队列有值,然后内核返回新的socket,给我们调用的这个serverSocket, 这个返回的newSocket会在自己的数据接口里保存自己这个进程的地址与端口号还会保存远程客户端的进程与端口号。通过这个newsocket就可以与远程的客户端进行信息交互。
二、客户端流程
客户端的主机进程调用socket()内核接口创建一个描述符,(1)、调用connect()函数与服务端的进程建立链接。则客户端的socket数据结构里就会保存远程服务端的地址与端口,就可以进行消息的交互了。
以下图片为TCP客户端与服务端建立的流程,来源与其他博客:
UDP客户端与服务端建立的流程,图片来源其他博客:
更详细的解释看如下博文
socket原理解释
服务端的ServerSocketChannel调用accpet原理
Reactor模式(反应器模式)事件驱动结构的一种实现。是一种处理一个或多个客户端并发进行服务请求的。将服务端接收请求与事件处理分离,从而提高系统处理并发的能力,java的NIO的reactor模式是基于系统内核的多路复用技术实现的。
java中的socketChannel注册到selector上后,会生成一个selectionKey。
键对象表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用 SelectionKey对象的 cancel( )方法。可以通过调用 isValid( )方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( )方法时(或者一个正在进行的 select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey 将被返回。
SelectionKey 类定义了四个便于使用的布尔方法来为您测试这些比特值:isReadable( ),isWritable( ),isConnectable( ), 和 isAcceptable( )。这个key中的属性为:
channel:当前那key绑定的channel
selector:当前key绑定的selector,表示channel与selectro的一一对应关系
interestOps:当前channel感性趣的事件即内核需要监听这个socketChannel发生的事件
readyOps:当前key正在发生的事件。
选择键包含两个操作集**,操作集为位运算值,每一位表示一种操作.**
interestOps 集合:当前channel感兴趣的操作,此类操作将会在下一次选择器select操作时被交付,可以通过selectionKey.interestOps(int)进行修改.
readyOps 集合:表示此选择键上,已经就绪的操作.每次select时,选择器都会对ready集合进行更新;外部程序无法修改此集合.
selectionKey调用isReadable( ),isWritable( ),isConnectable( ), 和 isAcceptable( )这些方法时,都是那readyOps值与定义好的位来做 与运算。
OP_WRITE等在selectionKey中定义了今天变量
public static final int OP_ACCEPT = 1 << 4;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_WRITE = 1 << 2;
public static final int OP_READ = 1 << 0;
java中的NIO的SelectorImpl类中就实现了IO的多路复用,从客户端链接到服务端的socket,在服务端都注册到Selector类上的一个列表里返回一个selectKey(代表选择器与socket的绑定关系,还有这个socket),然后selector调用内核的selec接口或则epllo接口,把socket列表(即文件描述符列表(文件描述符用一个long整形数值表示))与socket感兴趣的事件传到select接口的参数列表,由内核完成对socket列表的遍历,监听每个socket是否有相应的事件发生,如果有相应的socket感兴趣的事件发生,则把socket的数据结构相应的事件发生标识修改。再由selector调用selectionKeys()。返回所有的socket当前的状态,遍历selectionKeys。判断每个key的状态,然后进行事件的处理。
如下代码来源与其他博客:
`import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import javax.swing.text.html.HTMLDocument.Iterator;
/**
Simple echo-back server which listens for incoming stream connections and
echoes back whatever it reads. A single Selector object is used to listen to
the server socket (to accept new connections) and all the active socket
channels.
@author zale (zalezone.cn)
/
public class SelectSockets {
public static int PORT_NUMBER = 1234;
public static void main(String[] argv) throws Exception
{
new SelectSockets().go(argv);
}
public void go(String[] argv) throws Exception
{
int port = PORT_NUMBER;
if (argv.length > 0)
{ // 覆盖默认的监听端口
port = Integer.parseInt(argv[0]);
}
System.out.println("Listening on port " + port);
ServerSocketChannel serverChannel = ServerSocketChannel.open();// 打开一个未绑定的serversocketchannel
ServerSocket serverSocket = serverChannel.socket();// 得到一个ServerSocket去和它绑定
Selector selector = Selector.open();// 创建一个Selector供下面使用
serverSocket.bind(new InetSocketAddress(port));//设置server channel将会监听的端口
serverChannel.configureBlocking(false);//设置非阻塞模式
serverChannel.register(selector, SelectionKey.OP_ACCEPT);//将ServerSocketChannel注册到Selector
while (true)
{
// This may block for a long time. Upon returning, the
// selected set contains keys of the ready channels.
int n = selector.select();
if (n == 0)
{
continue; // nothing to do
}
java.util.Iterator it = selector.selectedKeys().iterator();// Get an iterator over the set of selected keys
//在被选择的set中遍历全部的key
while (it.hasNext())
{
SelectionKey key = (SelectionKey) it.next();
// 判断是否是一个连接到来
if (key.isAcceptable())
{
ServerSocketChannel server =(ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
registerChannel(selector, channel,SelectionKey.OP_READ);//注册读事件
sayHello(channel);//对连接进行处理
}
//判断这个channel上是否有数据要读
if (key.isReadable())
{
readDataFromSocket(key);
}
//从selected set中移除这个key,因为它已经被处理过了
it.remove();
}
}
}
// ----------------------------------------------------------
/*
A SelectionKey object associated with a channel determined by
the selector to be ready for reading. If the channel returns
an EOF condition, it is closed here, which automatically
invalidates the associated key. The selector will then
de-register the channel on the next select call.
一个选择器决定了和通道关联的SelectionKey object是准备读状态。如果通道返回EOF,通道将被关闭。
并且会自动使相关的key失效,选择器然后会在下一次的select call时取消掉通道的注册
/
protected void readDataFromSocket(SelectionKey key) throws Exception
{
SocketChannel socketChannel = (SocketChannel) key.channel();
int count;
buffer.clear(); // 清空Buffer
// Loop while data is available; channel is nonblocking
//当可以读到数据时一直循环,通道为非阻塞
while ((count = socketChannel.read(buffer)) > 0)
{
buffer.flip(); // 将缓冲区置为可读
// Send the data; don’t assume it goes all at once
//发送数据,不要期望能一次将数据发送完
while (buffer.hasRemaining())
{
socketChannel.write(buffer);
}
// WARNING: the above loop is evil. Because
// it’s writing back to the same nonblocking
// channel it read the data from, this code can
// potentially spin in a busy loop. In real life
// you’d do something more useful than this.
//这里的循环是无意义的,具体按实际情况而定
buffer.clear(); // Empty buffer
}
if (count < 0)
{
// Close channel on EOF, invalidates the key
//读取结束后关闭通道,使key失效
socketChannel.close();
}
}
// ----------------------------------------------------------
/*
The newly connected SocketChannel to say hello to.
*/
private void sayHello(SocketChannel channel) throws Exception
{
buffer.clear();
buffer.put(“Hi there!\r\n”.getBytes());
buffer.flip();
channel.write(buffer);
}
}`