FileIntputStream
用于打开一个文件并获取输入流。
打开文件
我们来看看FileIntputStream
打开文件时,做了什么操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public FileInputStream(File file) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkRead(name); } if (name == null) { throw new NullPointerException(); } if (file.isInvalid()) { throw new FileNotFoundException("Invalid file path"); } fd = new FileDescriptor(); fd.attach(this); path = name; open(name); }
private void open(String name) throws FileNotFoundException { open0(name); }
private native void open0(String name) throws FileNotFoundException;
|
FileIntputStream
的构造函数,在Java层面做的事情不多:
- 检查是否有读取文件的权限
- 判断文件路径是否合法
- 新建
FileDescriptor
实例
- 调用
open0
本地方法
FileDescriptor
类对应操作系统的文件描述符,具体可以参考JDK源码阅读-FileDescriptor这篇文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| JNIEXPORT void JNICALL Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) { fileOpen(env, this, path, fis_fd, O_RDONLY); }
void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags) { WITH_PLATFORM_STRING(env, path, ps) { FD fd;
#if defined(__linux__) || defined(_ALLBSD_SOURCE) char *p = (char *)ps + strlen(ps) - 1; while ((p > ps) && (*p == '/')) *p-- = '\0'; #endif fd = JVM_Open(ps, flags, 0666); if (fd >= 0) { SET_FD(this, fd, fid); } else { throwFileNotFoundException(env, path); } } END_PLATFORM_STRING(env, ps); }
|
FileOutputStream#open
的JNI代码逻辑也比较简单:
- 如果是Linux或BSD,去掉path结尾的/,因为这些内核不需要
- 调用
JVM_Open
函数打开文件,得到文件描述符
- 调用
SET_FD
设置文件描述符到FileDescriptor#fd
SET_FD
用于设置文件描述符到FileDescriptor#fd
,具体可以参考JDK源码阅读-FileDescriptor这篇文章。
JVM_Open
根据其命名可以看得出来是JVM提供的函数,可以看出JDK的实现是分为多层的:Java-JNI-JDK,需要和操作系统交互的代码在JNI层面,一些每个操作系统都需要提供的真正底层的方法JVM来提供。具体的这个分层设计以后如果能有机会看JVM实现应该能有更深的理解。
JVM_Open
的实现可以在Hotspot虚拟机的代码中找到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| JVM_LEAF(jint, JVM_Open(const char *fname, jint flags, jint mode)) JVMWrapper2("JVM_Open (%s)", fname);
int result = os::open(fname, flags, mode); if (result >= 0) { return result; } else { switch(errno) { case EEXIST: return JVM_EEXIST; default: return -1; } } JVM_END
int os::open(const char *path, int oflag, int mode) {
if (strlen(path) > MAX_PATH - 1) { errno = ENAMETOOLONG; return -1; } int fd; int o_delete = (oflag & O_DELETE); oflag = oflag & ~O_DELETE;
fd = ::open64(path, oflag, mode); if (fd == -1) return -1;
{ struct stat64 buf64; int ret = ::fstat64(fd, &buf64); int st_mode = buf64.st_mode;
if (ret != -1) { if ((st_mode & S_IFMT) == S_IFDIR) { errno = EISDIR; ::close(fd); return -1; } } else { ::close(fd); return -1; } }
#ifdef FD_CLOEXEC { int flags = ::fcntl(fd, F_GETFD); if (flags != -1) ::fcntl(fd, F_SETFD, flags | FD_CLOEXEC); } #endif
if (o_delete != 0) { ::unlink(path); } return fd; }
|
可以看到JVM最后使用open64
这个方法打开文件,网上对于open64
这个资料还是很少的,我找到的是man page for open64 (all section 2) - Unix & Linux Commands,从中可以看出,open64
是为了在32位环境打开大文件的系统调用,但是不是标准的一部分。和open
+O_LARGEFILE
效果是一样的。参考:c - Wrapper for open() and open64() and see that system calls by vi uses open64() - Stack Overflow
这样完整的打开文件流程就分析完了,去掉各种函数调用,本质上只做了两件事:
- 调用
open
系统调用打开文件
- 保存得到的文件描述符到
FileDescriptor#fd
中
读取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public int read() throws IOException { return read0(); }
private native int read0() throws IOException;
public int read(byte b[]) throws IOException { return readBytes(b, 0, b.length); }
public int read(byte b[], int off, int len) throws IOException { return readBytes(b, off, len); }
private native int readBytes(byte b[], int off, int len) throws IOException;
|
可以看出,FileInputStream
的三个主要read方法,依赖于两个本地方法,先来看看读取一个字节的read0
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| JNIEXPORT jint JNICALL Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) { return readSingle(env, this, fis_fd); }
jint readSingle(JNIEnv *env, jobject this, jfieldID fid) { jint nread; char ret;
FD fd = GET_FD(this, fid); if (fd == -1) { JNU_ThrowIOException(env, "Stream Closed"); return -1; }
nread = IO_Read(fd, &ret, 1); if (nread == 0) { return -1; } else if (nread == -1) { JNU_ThrowIOExceptionWithLastError(env, "Read error"); } return ret & 0xFF; }
#define IO_Read handleRead
ssize_t handleRead(FD fd, void *buf, jint len) { ssize_t result; RESTARTABLE(read(fd, buf, len), result); return result; }
#define RESTARTABLE(_cmd, _result) do { \ do { \ _result = _cmd; \ } while((_result == -1) && (errno == EINTR)); \ } while(0)
|
read的过程并没有使用JVM提供的函数,而是直接使用open系统调用,为什么有这个区别,目前不太清楚。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| 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); }
#define BUF_SIZE 8192
jint readBytes(JNIEnv *env, jobject this, jbyteArray bytes, jint off, jint len, jfieldID fid) { jint nread; char stackBuf[BUF_SIZE]; char *buf = NULL; FD fd;
if (IS_NULL(bytes)) { JNU_ThrowNullPointerException(env, NULL); return -1; } if (outOfBounds(env, off, len, bytes)) { JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL); return -1; }
if (len == 0) { return 0; } else if (len > BUF_SIZE) { buf = malloc(len); if (buf == NULL) { JNU_ThrowOutOfMemoryError(env, NULL); return 0; } } else { buf = stackBuf; }
fd = GET_FD(this, fid); if (fd == -1) { JNU_ThrowIOException(env, "Stream Closed"); nread = -1; } else { nread = IO_Read(fd, buf, len); if (nread > 0) { (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf); } else if (nread == -1) { JNU_ThrowIOExceptionWithLastError(env, "Read error"); } else { nread = -1; } }
if (buf != stackBuf) { free(buf); } return nread; }
|
FileInputStream#read(byte[], int, int)
的主要流程:
- 检查参数是否合法(byte数组不能为空,off和len没有越界)
- 判断读取的长度,如果等于0直接返回0,如果大于BUF_SIZE需要在堆空间申请内存,如果
0<len<=BUF_SIZE
则直接在使用栈空间的缓存
- 调用
read
系统调用读取文件内容到内存中
- 从C空间的char数组复制数据到Java空间的byte数组中
重要收获:
- 使用
FileInputStream#read(byte[], int, int)
读取的长度,len一定不能大于8192!因为在小于8192时,会直接利用栈空间的char数组,如果大于,则需要调用malloc申请内存,并且还需要free释放内存,这是非常消耗时间的。
- 相比于直接使用系统调用,Java的读取会多一次拷贝!(思考:使用C标准库的fread和Java的read,复制次数是一样,还是fread会少一次?)
移动偏移量
1
| public native long skip(long n) throws IOException;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| JNIEXPORT jlong JNICALL Java_java_io_FileInputStream_skip(JNIEnv *env, jobject this, jlong toSkip) { jlong cur = jlong_zero; jlong end = jlong_zero;
FD fd = GET_FD(this, fis_fd); if (fd == -1) { JNU_ThrowIOException (env, "Stream Closed"); return 0; }
if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) { JNU_ThrowIOExceptionWithLastError(env, "Seek error"); } else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) { JNU_ThrowIOExceptionWithLastError(env, "Seek error"); } return (end - cur); }
#ifdef _ALLBSD_SOURCE #define open64 open #define fstat64 fstat #define stat64 stat #define lseek64 lseek #define ftruncate64 ftruncate #define IO_Lseek lseek #else #define IO_Lseek lseek64 #endif
|
获取文件可读取的字节数
1
| public native int available() throws IOException;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| JNIEXPORT jint JNICALL Java_java_io_FileInputStream_available(JNIEnv *env, jobject this) { jlong ret; FD fd = GET_FD(this, fis_fd); if (fd == -1) { JNU_ThrowIOException (env, "Stream Closed"); return 0; } if (IO_Available(fd, &ret)) { if (ret > INT_MAX) { ret = (jlong) INT_MAX; } else if (ret < 0) { ret = 0; } return jlong_to_jint(ret); } JNU_ThrowIOExceptionWithLastError(env, NULL); return 0; }
#define IO_Available handleAvailable
jint handleAvailable(FD fd, jlong *pbytes) { int mode; struct stat64 buf64; jlong size = -1, current = -1;
int result; RESTARTABLE(fstat64(fd, &buf64), result); if (result != -1) { mode = buf64.st_mode; if (S_ISCHR(mode) || S_ISFIFO(mode) || S_ISSOCK(mode)) { int n; int result; RESTARTABLE(ioctl(fd, FIONREAD, &n), result); if (result >= 0) { *pbytes = n; return 1; } } else if (S_ISREG(mode)) { size = buf64.st_size; } }
if ((current = lseek64(fd, 0, SEEK_CUR)) == -1) { return 0; }
if (size < current) { if ((size = lseek64(fd, 0, SEEK_END)) == -1) return 0; else if (lseek64(fd, current, SEEK_SET) == -1) return 0; }
*pbytes = size - current; return 1; }
|
关闭文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public void close() throws IOException { // 保证只有一个线程会执行关闭逻辑 synchronized (closeLock) { if (closed) { return; } closed = true; } // 关闭关联的Channel if (channel != null) { channel.close(); }
// 调用FileDescriptor的closeAll,关闭所有相关流,并调用close系统调用关闭文件描述符 fd.closeAll(new Closeable() { public void close() throws IOException { close0(); } }); }
|
关闭文件的流程可以参考JDK源码阅读-FileDescriptor
总结
FileInputStream
打开文件使用open
系统调用
FileInputStream
读取文件使用read
系统调用
FileInputStream
关闭文件使用close
系统调用
FileInputStream
修改文件当前偏移量使用lseek
系统调用
FileInputStream
获取文件可读字节数使用fstat
系统调用
- 使用
FileInputStream#read(byte[], int, int)
读取的长度,len一定不能大于8192!因为在小于8192时,会直接利用栈空间的char数组,如果大于,则需要调用malloc申请内存,并且还需要free释放内存,这是非常消耗时间的。
- 相比于直接使用系统调用,Java的读取文件会多一次拷贝!因为使用read读取文件内容到C空间的数组后,需要拷贝数据到JVM的堆空间的数组中
FileInputStream#read
是无缓冲的,所以每次调用对对应一次系统调用,可能会有较低的性能,需要结合BufferedInputStream
提高性能
参考资料