如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O
序
接上篇,继续研究第(3)个问题:
(1)传统IO中也有BufferInputStream,BufferReader,和NIO中的Buffer相比有什么不同?
(2)传统IO中的Reader,Writer也能实现编码解码,和NIO中的Charset相比有什么不同?
(3)传统IO对流read,writer,NIO对通道read,writer有什么不同?是否支持一些新的IO操作?
(4)传统IO里面没有Selector这个特性,看不懂,它能带来什么好处?
注意,本章内容和第一章内容有部分重复(读取文件),但本章以研究C源码为主,毕竟JAVA本身没有直接IO的能力,想要接近本质,当然得到linux中去寻找。
一、基本概念
1、什么是流
我们常用的如:FileInputStream,BufferInputStream,Reader等等,除了会用还是得问下自己什么是流(Stream)。官方定义如下:
A stream is a sequence of data. A program uses an input stream to read data from a source, one item at a time,A program uses an output stream to write data to a destination, one item at time
Stream是一个数据序列,它反向代理了数据来源(Source)或 数据目标(Destination)。 我们使用输入/输出流对Source和Destination读/写数据,其中Source和Destination有:磁盘文件,程序,内存或其他设备等。一次读/写一项数据( byte 或 byte[])。
2、什么是通道
我们常用的如:FileChannel,写第一章的时候提到了Channel的含义,官方定义如下:
A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.
Channel是一个实体连接,它将实体和Buffer连接在一起,操作Buffer就是操作实体在内存中的地址。比Stream更接近底层。其中Buffer有DirectByteBuffer、HeapByteBuffer( 同样需要先获取一个临时的DirectByteBuffer)。实体有:硬件设备,文件,socket等。通道支持多种不同IO操作,不仅仅是读写。
如果你想问:
(1)Buffer、DirectByteBuffer、HeapByteBuffer是什么?
(2)也不要再说NIO和IO的区别就是前者读取数据块,后者逐个读取数据字节,使用流同样可以读取数据块(byte[]),那么它和使用通道读取(Buffer)有什么区别?
请参考《深度探索-JAVA NIO VS 传统 IO(一)Buffer》
3、什么是文件描述符
在阅读C源码之前必须理解文件描述符FileDescriptor,简称fd。
Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open
file, an open socket, or another source or sink of bytes.
源于linux中的概念,一切皆文件,fd表示打开的文件,socket,其他字节源或接收端(如:键盘,控制台)的句柄。
对于文件而言,将句柄可以理解为文件指针或索引,应用程序通过句柄和操作系统交互。
二、从流中读取数据
InputStream是输入流的基础接口,主要有两种read方法:
public int read() throws IOException //读取1个字节
public int read(byte b[], int off, int len) throws IOException //读取字节序列
InputStream的实现类有很多,我们具体研究FileInputStream。
1、打开文件(初始化流)
(1)JAVA调用链

(2)JAVA与C源码交互过程

(3)native方法C源码
FileInputStream.c
NIEXPORT void JNICALL
Java_java_io_FileInputStream_open(JNIEnv *env, jobject this, jstring path) {
fileOpen(env, this, path, fis_fd, O_RDONLY); //fis_fd对应JAVA对象FileInputStream的成员变量fd,使用只读模式打开文件
}
io_util_md.c
void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
......
fd = handleOpen(ps, flags, 0666); //打开文件,获取fd
if (fd != -1) {
SET_FD(this, fd, fid); //将fd的值设置到JAVA对象FileInputStream的成员变量fd中
} else {
throwFileNotFoundException(env, path); //抛出文件找不到异常
}
......
}
FD
handleOpen(const char *path, int oflag, int mode) {
FD fd;
RESTARTABLE(open64(path, oflag, mode), fd); //调用linux open64函数,获取文件描述符fd
......
return fd;
}
注意,文件描述符fd会出现很多次,基本概念第一部分已经讲过,在FileInputStream.java中对应的字段是:
private final FileDescriptor fd;
2、关闭文件(关闭流)
(1)JAVA调用链

(2)JAVA与C源码交互过程

(3)native方法C源码
FileInputStream_md.c
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_close0(JNIEnv *env, jobject this) {
fileClose(env, this, fis_fd); //fis_fd对应JAVA对象FileInputStream的成员变量fd
}
io_util_md.c
void
fileClose(JNIEnv *env, jobject this, jfieldID fid)
{
FD fd = GET_FD(this, fid); //获取JAVA对象FileInputStream的成员变量fd的值
......
SET_FD(this, -1, fid); ////将JAVA对象FileInputStream的成员变量fd的值设置为-1,表示流已关闭
......
} else if (close(fd) == -1) { //调用linux close方法关闭fd
JNU_ThrowIOExceptionWithLastError(env, "close failed");
}
}
3、从文件中读取单个字节
(1)JAVA调用链

(2)JAVA与C源码交互过程

(3)native方法C源码
FileInputStream.c
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_read(JNIEnv *env, jobject this) {
return readSingle(env, this, fis_fd);
}
io_utils.c
jint
readSingle(JNIEnv *env, jobject this, jfieldID fid) {
jint nread;
char ret; //声明时就分配1个字节内存
FD fd = GET_FD(this, fid); //获取JAVA对象FileInputStream的成员变量fd的值(打开文件时设置)
if (fd == -1) {//fd为-1,表示流已关闭
JNU_ThrowIOException(env, "Stream Closed");
return -1;
}
nread = IO_Read(fd, &ret, 1); //调用handleRead函数
if (nread == 0) { /* EOF */
return -1; //读不到数据,返回-1
} else if (nread == -1) { /* error */
JNU_ThrowIOExceptionWithLastError(env, "Read error");
}
return ret & 0xFF; //只返回低8位,即一个字节
}
io_util_md.h
#define IO_Read handleRead
io_util_md.c
ssize_t
handleRead(FD fd, void *buf, jint len)
{
ssize_t result;
RESTARTABLE(read(fd, buf, len), result); //调用linux read函数,从fd中读取1个字节
return result;
}
4、从文件中读取字节序列
(1)JAVA调用链

(2)JAVA与C源码交互过程

(3)native方法C源码
FileInputStream.c
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
jbyteArray bytes, jint off, jint len) {
return readBytes(env, this, bytes, off, len, fis_fd); //这里bytes对应上层JAVA方法中的byte b[]
}
io_utils.c
jint
readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
jint off, jint len, jfieldID fid)
{
jint nread;
char stackBuf[BUF_SIZE]; //声明数组,分配8192字节(1个char1个字节)
char *buf = NULL;
FD fd;
if (IS_NULL(bytes)) { //bytes不能为空,JAVA中必须传入一个已创建的数组,在读取完数据之后,需要转存到bytes中返回
JNU_ThrowNullPointerException(env, NULL);
return -1;
}
if (outOfBounds(env, off, len, bytes)) { //byte剩余长度 < len,报数组越界
JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
return -1;
}
if (len == 0) {
return 0;
} else if (len > BUF_SIZE) { //需要读取的字节序列长度超出8K字节,重新申请空间
buf = malloc(len); //buf指向新申请的空间
if (buf == NULL) {
JNU_ThrowOutOfMemoryError(env, NULL);
return 0;
}
} else {
buf = stackBuf;//如果需要读取的字节序列长度在8K以内,则buf指向此内存空间
}
fd = GET_FD(this, fid); //获取JAVA对象FileInputStream的成员变量fd的值(打开文件时设置)
if (fd == -1) { //fd为-1,表示流已关闭(调用过close方法)
JNU_ThrowIOException(env, "Stream Closed");
nread = -1;
} else {
nread = IO_Read(fd, buf, len); //readSingle中已分析过,最终调用linux read函数,从fd中读取len长度字节存入buf指向的内存空间
if (nread > 0) {
(*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf); //从buf对应的内存空间拷贝数据到需要输出的bytes,交给JAVA
} else if (nread == -1) {
JNU_ThrowIOExceptionWithLastError(env, "Read error");
} else { /* EOF */ //nread == 0返回-1
nread = -1;
}
}
if (buf != stackBuf) {
free(buf); //释放重新申请的空间(malloc)
}
return nread; //返回读取字节数
}
a、int read()方法返回的是读取的字节,而int read(byte[] , int , int )返回的是读取到的字节数,虽然意义不同,但都已-1作为文件结束(nread == 0)。包括后面FileChannel.read也是以-1作为读取不到数据的标识。
b、int read()和int read(byte[] , int , int ) 方法在JAVA API文档提到This method blocks until input data is available,这导致有些人认为传统IO就是阻塞IO,其实这句话不完全准确。它们在底层C源码中调用的都是IO_Read方法,对应于linux的read方法。而对于linux来说,是否阻塞是文件属性,在文件被打开时就决定了:普通文件默认不阻塞,管道、套接字默认阻塞。可以在调用open()和fcntl()打开文件(锁定文件)时设置为非阻塞(O_NONBLOCK)。
c、39行SetByteArrayRegion方法的调用,就是传统IO多拷贝的问题,因此才会有NIO的”零拷贝“的说法。
三、从通道中读取数据
FileChannel是通道的抽象类,主要的read方法:
public abstract int read(ByteBuffer dst) throws IOException;
FileChannel的实现类是FileChannelImpl。
1、获取通道
(1)从已创建的流中获取通道
FileChannel fc = new FileInputStream("src/out2.txt").getChannel();
当创建FileInputStream时,open文件(二-1)并将fd回写到FileInputStream.fd中,getChannel()只是将fd以及其他信息一起通过FileChannelImpl构造方法构造出FileChannel对象。
private FileChannelImpl(FileDescriptor fd, boolean readable,
boolean writable, boolean append, Object parent)
{
this.fd = fd;
this.readable = readable;
this.writable = writable;
this.append = append;
this.parent = parent; //注意这里的parent传入的FileInputStream自己
this.nd = new FileDispatcherImpl(append);
}
(2)通过FileChannel创建
FileChannel fc = FileChannel.open(Paths.get("src/out2.txt"),
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
(3)JAVA调用链

(4)native方法C源码
UnixNativeDispatcher.c
JNIEXPORT jint JNICALL
Java_sun_nio_fs_UnixNativeDispatcher_open0(JNIEnv* env, jclass this,
jlong pathAddress, jint oflags, jint mode)
{
jint fd;
const char* path = (const char*)jlong_to_ptr(pathAddress); //地址转指针
RESTARTABLE(open64(path, (int)oflags, (mode_t)mode), fd); //使用linux open函数打开文件获取fd
if (fd == -1) {
throwUnixException(env, errno);
}
return fd;
}
通过linux open打开文件获取fd后,同样是和其他信息一起调用FileChannelImpl构造方法构造出FileChannel对象。
2、关闭通道

FileChannel调用close方法
protected void implCloseChannel() throws IOException {
.......
if (parent != null) { //parent是FileInputStream自己,不为空
((java.io.Closeable)parent).close(); //这里parent就是当前的FileInputStream对象
}
......
}
可以看到,这里实际调用的是FileInputStream的close方法(二-2)。
3、从通道读取数据
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
try (FileChannel fc = new FileInputStream(FILE).getChannel()) {
while (fc.read(bb) != -1) {
bb.flip();
// System.out.println(Arrays.toString(bb.array()));
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
(1)fc.read调用的是IOUtil.read方法,这里只讨论常用的HeapByteBuffer(包含了DirectByteBuffer的创建和释放)
static int read(FileDescriptor fd, ByteBuffer dst, long position,
NativeDispatcher nd)
throws IOException
{
......
ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining()); //申请临时空间
try {
int n = readIntoNativeBuffer(fd, bb, position, nd); //从文件读取数据到临时空间
bb.flip();
if (n > 0)
dst.put(bb); //将临时空间拷贝到目标Buffer的数组变量中
return n;
} finally {
Util.offerFirstTemporaryDirectBuffer(bb); //释放临时空间
}
}
(2)JAVA调用链

(3)JAVA与C源码交互过程

(4)native方法C源码
主要包含5个native方法:
a、allocateMemory、setMemory、freeMemory对应的C源码
unsafe.cpp
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
......
void* x = os::malloc(sz, mtInternal); //分配内存
......
return addr_to_java(x); //分配的内存地址转换为java中的long类型返回
UNSAFE_END
UNSAFE_ENTRY(void, Unsafe_SetMemory(JNIEnv *env, jobject unsafe, jlong addr, jlong size, jbyte value))
UnsafeWrapper("Unsafe_SetMemory");
......
char* p = (char*) addr_from_java(addr); //将地址转换为指针
Copy::fill_to_memory_atomic(p, sz, value); //将p指向的内存空间全部初始化为value(java传入的是0)
UNSAFE_END
UNSAFE_ENTRY(void, Unsafe_FreeMemory(JNIEnv *env, jobject unsafe, jlong addr))
UnsafeWrapper("Unsafe_FreeMemory");
void* p = addr_from_java(addr); //地址转换为指针
......
os::free(p);//释放指针指向的内存
UNSAFE_END
b、read0对应的C源码
FileDispatcherImpl.c
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_read0(JNIEnv *env, jclass clazz,
jobject fdo, jlong address, jint len)
{
jint fd = fdval(env, fdo); //读取fd
void *buf = (void *)jlong_to_ptr(address); //把地址转指针
return convertReturnVal(env, read(fd, buf, len), JNI_TRUE); //read方法先被调用,将fd中的数据读入到buf指向的空间中
}
IOUtils.c
jint
convertReturnVal(JNIEnv *env, jint n, jboolean reading) //这里n参数是linux 函数 read()返回的值,读取的字节数
{
if (n > 0) /* Number of bytes written */
return n; //如果读取到的数据长度大于0,直接返回
else if (n == 0) {
if (reading) { // 注意上面传过来的固定是JNI_TRUE,因此如果读取不到数据,会返回-1,不会返回0
return IOS_EOF; /* EOF is -1 in javaland */
} else {
return 0;
}
}
......
}
c、getByte对应的C源码
unsafe.cpp
......
{CC"get"#Byte, CC"("ADR")"#B, FN_PTR(Unsafe_GetNative##Byte)}, \
......
UNSAFE_ENTRY(java_type, Unsafe_GetNative##Type(JNIEnv *env, jobject unsafe, jlong addr)) \
UnsafeWrapper("Unsafe_GetNative"#Type); \
void* p = addr_from_java(addr); \ //地址转换为指针
......
java_type x = *(volatile native_type*)p; \ //获取指针指向内存的值
......
return x; \
UNSAFE_END \
四、向流中写入数据
OutputStream是输出流的基础接口,主要有两种write方法:
public void write(int b) throws IOException
public void write(byte b[]) throws IOException
OutputStream实现类有很多,这里主要研究FileOutputStream。
1、打开文件(初始化流)
(1)JAVA调用链

2)native方法C源码
FileOutputStream_md.c
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_open(JNIEnv *env, jobject this,
jstring path, jboolean append) {
fileOpen(env, this, path, fos_fd,
O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC)); //可读写,可创建标识打开文件
}
注意,如果构造FileOutputStream构造时传入了append为true,这里增加O_APPEND标识(可追加)
fileOpen源码上文分析过(二-1),主要作用是使用linux open函数打开文件,获取到文件描述符fd,回写到FileOutputStream的成员变量fd暂存。
2、关闭文件(关闭流)
(1)JAVA调用链

(2)native方法C源码
FileOutputStream_md.c
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_close0(JNIEnv *env, jobject this) {
fileClose(env, this, fos_fd);
}
fileClose源码上文分析过(二-2),主要作用使用linux close函数关闭文件,并将fd设置为-1,回写到FileOutputStream的fd成员变量。
3、向文件中写入单个字节
(1)JAVA调用链

(2)native方法C源码
FileOutputStream_md.c
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_write(JNIEnv *env, jobject this, jint byte, jboolean append) {
writeSingle(env, this, byte, append, fos_fd); //append为true,表示可追加写
}
io_util.c
void
writeSingle(JNIEnv *env, jobject this, jint byte, jboolean append, jfieldID fid) {
// Discard the 24 high-order bits of byte. See OutputStream#write(int)
char c = (char) byte; //byte将要写入的字节
jint n;
FD fd = GET_FD(this, fid); //获取JAVA对象FileOutputStream的成员变量fd的值(打开文件时设置)
if (fd == -1) { //fd=-1表示流已经调用过close方法,关闭了
JNU_ThrowIOException(env, "Stream Closed");
return;
}
if (append == JNI_TRUE) {
n = IO_Append(fd, &c, 1); //和IO_Write一样都调用handleWrite,且参数相同,是否可追加是在open打开文件时设置
} else {
n = IO_Write(fd, &c, 1); //调用handleWrite
}
if (n == -1) {
JNU_ThrowIOExceptionWithLastError(env, "Write error");
}
}
io_util_md.h
#define IO_Write handleWrite
#define IO_Append handleWrite
io_util_md.c
ssize_t
handleWrite(FD fd, const void *buf, jint len)
{
ssize_t result;
RESTARTABLE(write(fd, buf, len), result); //linux write方法写入fd
return result;
}
4、向文件中写入字节序列
(1)JAVA调用链

(2)JAVA与C源码交互过程

(3)native方法C源码
FileOutputStream_md.c
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,
jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {
writeBytes(env, this, bytes, off, len, append, fos_fd); //bytes为要写入的字节序列,off为写入的起始位置,len为写入的长度
}
io_util.c
void
writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
jint off, jint len, jboolean append, jfieldID fid)
{
jint n;
char stackBuf[BUF_SIZE]; //申请8K字节临时内存空间,1个字符对应1个字节
char *buf = NULL;
FD fd;
if (IS_NULL(bytes)) { //需要写入的字节序列bytes不能为空
JNU_ThrowNullPointerException(env, NULL);
return;
}
if (outOfBounds(env, off, len, bytes)) { //byte剩余长度 < len,报数组越界
JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
return;
}
if (len == 0) {
return;
} else if (len > BUF_SIZE) { //写入的长度大于一开始申请8K临时内存
buf = malloc(len); //则需要重新申请更大的内存,并将buf指针指向它
if (buf == NULL) {
JNU_ThrowOutOfMemoryError(env, NULL);
return;
}
} else {
buf = stackBuf; //如果内存够用,则将buf指针指向一开始申请的8K临时内存
}
(*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf); //从bytes拷贝数据到buf指向的临时内存空间
if (!(*env)->ExceptionOccurred(env)) {
off = 0;
while (len > 0) {
fd = GET_FD(this, fid); //获取JAVA对象FileOutputStream的成员变量fd的值(打开文件时设置)
if (fd == -1) { //fd为-1表示已经调用过close函数关闭流
JNU_ThrowIOException(env, "Stream Closed");
break;
}
if (append == JNI_TRUE) {
n = IO_Append(fd, buf+off, len); //同writeSingle中handleWrite,将buf指向的临时内存数据写入fd
} else {
n = IO_Write(fd, buf+off, len); //同writeSingle中handleWrite,将buf指向的临时内存数据写入fd
}
if (n == -1) {
JNU_ThrowIOExceptionWithLastError(env, "Write error");
break;
}
off += n;
len -= n;
}
}
if (buf != stackBuf) {
free(buf); //如果重新申请了临时内存空间,则释放它
}
}
五、向通道中写入数据
try (FileChannel fc = new FileOutputStream(FILE).getChannel();) {
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
for (int i = 0; i < BSIZE; i ++) {
bb.put((byte) 'x');
}
bb.flip();
fc.write(bb);
} catch (IOException e) {
e.printStackTrace();
}
通道的获取和关闭上文已经分析过(三-1,三-2),这里只研究fc.write方法。
(1)fc.write调用的是IOUtil.write方法,这里只讨论HeapByteBuffer(包含了DirectByteBuffer的创建和释放)。
static int write(FileDescriptor fd, ByteBuffer src, long position,
NativeDispatcher nd)
throws IOException
{
......
ByteBuffer bb = Util.getTemporaryDirectBuffer(rem); //创建临时DirectByteBuffer
try {
bb.put(src); //将数据来源Buffer(HeapByteBuffer内部是数组)写入临时DirectByteBuffer
bb.flip();
......
int n = writeFromNativeBuffer(fd, bb, position, nd); //将临时DirectByteBuffer写入文件fd
......
return n;
} finally {
Util.offerFirstTemporaryDirectBuffer(bb); //释放临时DirectByteBuffer
}
}
(2)JAVA调用链

(3)JAVA与C源码交互过程

(4)native方法C源码
其中allocateMemory、setMemory、freeMemory上文已经分析(三-3-(4)-a)。
a、putByte对应的C源码
unsafe.cpp
......
{CC"put"#Byte, CC"("ADR#B")V", FN_PTR(Unsafe_SetNative##Byte)}
......
UNSAFE_ENTRY(void, Unsafe_SetNative##Type(JNIEnv *env, jobject unsafe, jlong addr, java_type x)) \
UnsafeWrapper("Unsafe_SetNative"#Type); \
......
void* p = addr_from_java(addr); \ //地址转指针
*(volatile native_type*)p = x; \ //向指针指向的内存写入字节
......
UNSAFE_END \
b、write0对应的C源码
FileDispatcherImpl.c
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_write0(JNIEnv *env, jclass clazz,
jobject fdo, jlong address, jint len)
{
jint fd = fdval(env, fdo); //参数传入fd,获取值
void *buf = (void *)jlong_to_ptr(address); //地址转指针
return convertReturnVal(env, write(fd, buf, len), JNI_FALSE); //linux write方法先被调用,将buf指向的内存写入fd
}
convertReturnVal上文已分析(三-3-(4)-b),用于返回写入的字节数。
六、通道的应用场景
1、传输文件
使用流
byte[] bytes = new byte[1024];
try (FileInputStream in = new FileInputStream("src/out2.txt");
FileOutputStream out = new FileOutputStream("src/out4.txt");) {
while (in.read(bytes) != -1) {
out.write(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
使用通道
try (FileChannel in = new FileInputStream("src/out2.txt").getChannel();
FileChannel out = new FileOutputStream("src/out3.txt").getChannel();) {
in.transferTo(0, in.size(), out);
} catch (IOException e) {
e.printStackTrace();
}
(1)JAVA调用链

(2)native方法C源码
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,
jint srcFD,
jlong position, jlong count,
jint dstFD)
{
#if defined(__linux__)
off64_t offset = (off64_t)position;
jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count); //调用linux sendFile64函数从一个文件赋值到另一个文件
......
return n;
......
这里的sendFile,我们用man查看下:
man 2 sendfile
sendfile() copies data between one file descriptor and another.
Because this copying is done within the kernel, sendfile() is
more efficient than the combination of read(2) and write(2),
which would require transferring data to and from user space.
从描述上可以看出,sendfile在内核中从一个fd 拷贝到另一个fd,效率要好于read 加 write的组合。
(3)使用流和通道传输文件的区别
a、使用流

注意,上图中,用户态内核态切换4次:系统调用read ->返回用户程序 -> 系统调用write -> 返回用户程序,并且数据拷贝6次。
(每个应用场景不同拷贝次数不同,我们是JAVA应用,其中有C端创建的临时空间)
b、使用通道

注意,上图中用户态内核态切换2次:系统调用sendFile64 -> 结果返回用户程序,并且数据拷贝2次。这就是所谓的”零拷贝“。
2、内存映射
当我们需要对大体积文件进行修改或读取其中的某些部分时,最好用的是内存映射。
try (FileChannel fc = FileChannel.open(Paths.get("src/out2.txt"),
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE);) {//底层同样使用FileChannelImpl.open方法
MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());
for (int i = (int) (fc.size() - 5); i < fc.size(); i++ ) {
buffer.put(i, (byte) 'y'); //将文件最后5个字节改写为y
}
for (int i = (int) (fc.size() - 10); i < fc.size(); i++) {
char c = (char) buffer.get(i);
System.out.print(c + " "); //输出文件的最后10个字节
}
} catch (Exception e) {
e.printStackTrace();
}
控制台输出:x x x x x y y y y y
(1)JAVA调用链

根据map0获取的文件地址后,构造MappedByteBuffer时,传入的是读写模式,因此调用的是initDBBConstructor方法反射,最终创建的是DirectByteBuffer对象(MappedByteBuffer的子类)。
private static void initDBBConstructor() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
Class<?> cl = Class.forName("java.nio.DirectByteBuffer");
Constructor<?> ctor = cl.getDeclaredConstructor(
new Class<?>[] { int.class,
long.class,
FileDescriptor.class,
Runnable.class });
ctor.setAccessible(true);
directByteBufferConstructor = ctor;
} catch (ClassNotFoundException |
NoSuchMethodException |
IllegalArgumentException |
ClassCastException x) {
throw new InternalError(x);
}
return null;
}});
}
(2)native方法C源码
map方法获取到的MappedByteBuffer对象实际是DirectByteBuffer,put和get方法中的native函数已分析过(五-(4)-a,三-3-(4)-c),这里只研究map0函数。
FileChannelImpl.c
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jobject fdo = (*env)->GetObjectField(env, this, chan_fd); //获取FileChannelImpl中的fd属性
jint fd = fdval(env, fdo); //获取fd属性值
int protections = 0;
int flags = 0;
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
protections = PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) { //我们传入的FileChannel.MapMode.READ_WRITE
protections = PROT_WRITE | PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_PRIVATE;
}
mapAddress = mmap64( //linux mmap64 将文件映射进内存
0, /* Let OS decide location */
len, /* Number of bytes to map */
protections, /* File permissions */
flags, /* Changes are shared */
fd, /* File descriptor of mapped file */
off); /* Offset into file */
......
return ((jlong) (unsigned long) mapAddress); //返回映射的内存地址
}
这里的mmap方法,我们用man查看下:
man 2 mmap
mmap() creates a new mapping in the virtual address space of the
calling process. The starting address for the new mapping is speci-
fied in addr. The length argument specifies the length of the map-
ping.
从描述可以看出,mmap返回的是文件映射到内存的一个虚拟地址。我们操作这个虚拟地址就是在直接操作文件。
七、解答开篇问题
回到最初,让我们解答最开始的问题:
(3)传统IO对流read,writer,NIO对通道read,writer有什么不同?是否支持一些新的IO操作?
通过上面二~五的C端源码分析,我们可以站在更高的角度,清晰的看到:
(1)使用流或通道读取文件,思路大致相同,都是先申请一个临时内存空间,再从文件中读取数据到临时内存空间,最后将临时内存空间内容拷贝到目标数组中。(HeapByteBuffer中也同样内置数组)
(2)使用流或通道写入文件,思路大致也相同,都是先申请一个临时内存空间,再将数据写入到临时内存空间,最后从临时内存空间写入到文件。
(3)区别是流就像一个完整功能的API,临时内存空间是C端的一个字符数组,完全由C端维护(从内存申请到释放),JAVA无感。而通道则更像是将完整功能切分为原子的API,临时内存空间是DirectByteBuffer(其中保留了申请的内存地址),C端只提供这个临时内存空间最小的原子API。组装逻辑完全交给由java端。
(4)综上
流:高封装性,调用端代码简单,应用场景固定,适合简单业务场景,如:简单读或简单写等。
通道:低封装性,灵活性更高,根据应用场不同,可以灵活组装,避免中间不必要的数据拷贝,因此性能更好,适合复杂业务场景,如:更快的传输文件,大文件的内存映射等。
未完待续!
本文深入探讨JAVA传统IO与NIO的工作原理,通过对比流与通道在读写操作上的差异,揭示NIO在传输文件及内存映射方面的优势。
1079





