Java NIO 散布读与聚集写【源码笔记】

目录

一、Native函数解读
1.矢量I/O结构体iovec
2.散布读readv()
3.聚集写writev()
二、Scatter/Gather接口
三、一个散布读示例
四、散布读JDK源码
1.流程图
2.源码注释
五、文章总结
六、参考资料
一、Native函数解读
1.矢量I/O结构体iovec
struct iovec {
caddr_t  iov_base; // @1
int      iov_len; // @2
}

代码解读 @1 iovbase Contains the address of a buffer 地址指向缓冲区,即readv接受或者writev发送的数据 @2 iovlen Contains the length of the buffer. 读取或者写入该buffer的长度

小结:散布读ScatterRead和聚集写GatherWrite的本地函数使用矢量I/O结构体iovec作为基本参数与系统交付。

2.散布读readv()

函数说明

readv - read a vector
ssize_t readv(int fildes, conststruct iovec *iov, int iovcnt);
The readv() function shall be equivalent to read(), exceptas described below. The readv() function shall place the input data into the iovcnt buffers specified by the members of the iov array: iov[0], iov[1], ..., iov[ iovcnt-1]. The iovcnt argument is valid if greater than 0and less than or equal to {IOV_MAX}.

函数示例

#include
#include
#include
...
ssize_t bytes_read;
int fd;
char buf0[20];
char buf1[30];
char buf2[40];
int iovcnt;
struct iovec iov[3]; // @1
iov[0].iov_base = buf0; // @2
iov[0].iov_len = sizeof(buf0); // @3
iov[1].iov_base = buf1;
iov[1].iov_len = sizeof(buf1);
iov[2].iov_base = buf2;
iov[2].iov_len = sizeof(buf2);
iovcnt = sizeof(iov) / sizeof(struct iovec); // @4
bytes_read = readv(fd, iov, iovcnt); // @5

代码解读 @1 定义结构体为iovec的数组iov,数组长度为3 @2 填充数组iov;iovec.iovbase即buffer区域 @3 填充数组iov;iovec.iovlen即buffer的长度 @4 iovcnt即数组iov的长度,即允许多少个iovec结构体,需小于IOVMAX;Linux中IOVMAX为1024 @5 执行散布读函数readv()调用利用系统特性填充多个缓冲区

小结:散列读readv()通过传入结构体iovec的数组;结构体iovec包含待填充的缓冲区及长度;利用操作系统特性直接完成缓冲区的填充;填充缓冲区的顺序即数组iov的顺序。

3.聚集写writev()

函数说明

writev - write a vector
ssize_t writev(int fildes, conststruct iovec *iov, int iovcnt);
The writev() function shall be equivalent to write(), exceptas described below. The writev() function shall gather output data from the iovcnt buffers specified by the members of the iov array: iov[0], iov[1], ..., iov[iovcnt-1]. The iovcnt argument is valid if greater than 0and less than or equal to {IOV_MAX}, asdefinedin.

函数示例

#include
#include
#include
ssize_t bytes_written;
int fd;
char*buf0 = "short string\n";
char*buf1 = "This is a longer string\n";
char*buf2 = "This is the longest string in this example\n";
int iovcnt;
struct iovec iov[3]; // @1
iov[0].iov_base = buf0; // @2
iov[0].iov_len = strlen(buf0); // @3
iov[1].iov_base = buf1;
iov[1].iov_len = strlen(buf1);
iov[2].iov_base = buf2;
iov[2].iov_len = strlen(buf2);
iovcnt = sizeof(iov) / sizeof(struct iovec); // @4
bytes_written = writev(fd, iov, iovcnt); // @5

代码解读 @1~@4 同散布读readv() @5 执行聚集写函数writev()调用利用系统特性将多个buffer数据一次调用写入

小结:聚集写函数writev()通过传入结构体iovec的数组;结构体iovec包含待写出的缓冲区数据及长度;利用操作系统特性一次调用将多个缓冲区一并写入;缓冲区的顺序即数组iov的顺序。

二、Scatter/Gather接口

Java NIO 散布读与聚集写【源码笔记】_第1张图片

小结:图中散布读接口ScatteringByteChannel,将数据从Channel依序读入到多个Buffer中;聚集写接口GatheringByteChannel,将数据从多个Buffer中依序写入到Channel中。

三、一个散布读示例
File file = newFile("/mytest/channletst.tmp");
RandomAccessFile randomAccessFile = newRandomAccessFile(file, "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
ByteBuffer part01 = ByteBuffer.allocate(2);
ByteBuffer part02 = ByteBuffer.allocate(3);
ByteBuffer[] buffers = { part01, part02 };
long bytesLength = fileChannel.read(buffers);
System.out.println("Read Length:"+ bytesLength);
System.out.println("Part01: "+ newString(part01.array()));
System.out.println("Part02: "+ newString(part02.array()));

输出

ReadLength:5
Part01: ab
Part02: cde

小结:文件channletst.tmp中有“abcde”字符;分别将数据从Channel读入到两个Buffer中即:part01和part02。

四、散布读JDK源码

由以上Native源码分析看出,矢量IO数据结构iovec是散布读和聚集写的核心部分,JDK源码实现也会围绕iovec结构体的封装展开。

1.流程图

Java NIO 散布读与聚集写【源码笔记】_第2张图片

小结:散布读的主要方法为IOUtil.read;其中主要流程为对矢量I/O的iovec结构体和iovec的数组进行封装。iovec对应封装类IOVecWrapper;iovec数组对应封装类AllocatedNativeObjectvecArray。

2.源码注释

代码位置:IOUtil.read

/**
 * @param fd 文件描述符
 * @param bufs 依序待读入数据的byte数组
 * @param offset 默认 0
 * @param length 默认bufs数组的长度
 * @param nd Native 方法调用
 * @return
 * @throws IOException
 */
staticlong read(FileDescriptor fd, ByteBuffer[] bufs, int offset, int length, NativeDispatcher nd)throwsIOException{
// 构造矢量IO封装类IOVecWrapper
IOVecWrapper vec = IOVecWrapper.get(length);
boolean completed = false;
// 标记数组中第几个元素
int iov_len = 0;
try{
// 默认 0 + bufs.length
int count = offset + length;
int i = offset;
// 循环每个buffer数组;将每个buffer匹配到iovec数组中
while(i < count && iov_len < IOV_MAX) {
// 获取待读入数据的缓冲区
ByteBuffer buf = bufs[i];
if(buf.isReadOnly())
thrownewIllegalArgumentException("Read-only buffer");
int pos = buf.position();
int lim = buf.limit();
assert(pos <= lim);
// 计算缓冲区剩余大小
int rem = (pos <= lim ? lim - pos : 0);
if(rem > 0) {
// 将信息记录在vec中
vec.setBuffer(iov_len, buf, pos, rem);
// 非堆外内存buffer需要转为堆外内存
// 转换的原因:系统散布读只能将数据装载到系统内存,而无法直接将数据转载到JVM堆空间
if(!(buf instanceofDirectBuffer)) {
// 分配堆外内存空间
ByteBuffer shadow = Util.getTemporaryDirectBuffer(rem);
vec.setShadow(iov_len, shadow);
// 重置buffer地址指向堆外内存空间
buf = shadow;
// 重置为堆外buffer位置
pos = shadow.position();
}
// vecArray记录了待存放数据buffer指针地址
vec.putBase(iov_len, ((DirectBuffer)buf).address() + pos);
// vecArray记录了待存放数据buffer的长度
vec.putLen(iov_len, rem);
iov_len++;
}
i++;
}
if(iov_len == 0)
return0L;
// 调用Native API;fd:文件描述符;vec.address:AllocatedNativeObject内存地址;iov_len:待存放byteBuffer数组长度
long bytesRead = nd.readv(fd, vec.address, iov_len);
//...
} finally{
//...
}
}

代码位置:IOVecWrapper.java

privateIOVecWrapper(int size) {
// size byte数组长度
this.size      = size;
// 构造ByteBuffer数组
this.buf       = newByteBuffer[size];
// 构造position数组
this.position  = newint[size];
// 构造remaining数组
this.remaining = newint[size];
// 构造影子缓冲区数组
this.shadow    = newByteBuffer[size];
// 分配内存空间;分配空间的大小=待填充数组长度size * SIZE_IOVEC
this.vecArray  = newAllocatedNativeObject(size * SIZE_IOVEC, false);
// 记录分配空间的启始地址
this.address   = vecArray.address();
}
void putBase(int i, longbase) {
// 计算存放该Buffer地址的位点
int offset = SIZE_IOVEC * i + BASE_OFFSET;
// 记录Buffer的内存地址
if(addressSize == 4)
vecArray.putInt(offset, (int)base);
else
vecArray.putLong(offset, base);
}
void putLen(int i, long len) {
// buffer长度存储的位点
int offset = SIZE_IOVEC * i + LEN_OFFSET;
// 存储buffer的长度
if(addressSize == 4)
vecArray.putInt(offset, (int)len);
else
vecArray.putLong(offset, len);
}
static{
// 本地指针长度4或者8
addressSize = Util.unsafe().addressSize();
LEN_OFFSET = addressSize;
// 两部分:需要分配buffer的指针地址以及buffer的长度
SIZE_IOVEC = (short) (addressSize * 2);
}

小结:AllocatedNativeObject分配了两倍addressSize大小的空间,分别存储了待填充Buffer的指针地址以及待填充Buffer的长度;每个Buffer地址对应iovec.iov_base,Buffer长度对应iovec.iov_len;AllocatedNativeObject指针地址对应iovec结构体数组;因此vec.address可以作为Native方法的参数传入。

IOV_MAX代码位置:jdk/src/solaris/native/sun/nio/ch

JNIEXPORT jint JNICALL
Java_sun_nio_ch_IOUtil_iovMax(JNIEnv*env, jclass this)
{
    jlong iov_max = sysconf(_SC_IOV_MAX); // @1
if(iov_max == -1)
    iov_max = 16; // @2
return(jint)iov_max;
}

@1 iovmax在Linux系统的默认值为1024 @2 iovmax最小值为16

POSIX.1 allows an implementation to place a limit on the number of
items that can be passed in iov.  An implementation can advertise its
limit by defining IOV_MAX inor at run time via the return
value from sysconf(_SC_IOV_MAX).  On modern Linux systems, the limit
is1024.BackinLinux2.0 days, this limit was 16

小结:矢量结构体iovec数组的长度需要小于iov_max;iov_max在Linux最大值为1024,最小值为16.

五、文章总结

1.矢量I/O通过iovec结构体来体现,与readv和wirtev操作相关的结构体;readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区;这两个函数被称为散布读/scatter read和聚集写/gather write。 

2.JDK源码的实现围绕iovec结构体以及iovec结构体数组的封装展开。

3.Scatter/Gather一个极其强大的工具,减少了数据来回移动,操作系统已经对此做了高度优化。 

4.聚集写原理与散布读类同,不再赘述。

六、参考资料
1.《Java NIO》
2.readv()函数说明

https://pubs.opengroup.org/onlinepubs/009695399/functions/readv.html

3.writev()函数说明

https://pubs.opengroup.org/onlinepubs/009695399/functions/writev.html

七、系列文章

系统层面I/O【原理笔记】
Java NIO缓存区基本操作【源码笔记】
Java NIO字节缓存区【源码笔记】
Java NIO通道概览与文件通道【源码笔记】
Java NIO文件锁和可中断通道【源码笔记】


「瓜农老梁  学习同行」

    

你可能感兴趣的:(Java NIO 散布读与聚集写【源码笔记】)