在 Java 中获取文件大小是一个再常见不过的操作,只需调用 File.length() 即可。但这一行简单的代码背后,却跨越了从 Java 标准库到操作系统内核的多个层次。本文将以 OpenJDK 17 和 Linux 6 内核源码为基础,深入剖析 File.length() 在 Linux 系统上的完整调用链,带你一窥现代软件栈的精巧设计。
1. Java 层:File.length() 与 UnixFileSystem
Java 的 java.io.File 类提供了 length() 方法,用于返回文件的长度。其实现依赖于底层文件系统,在 Unix 类系统(包括 Linux)上,最终委托给 UnixFileSystem 的 getLength 方法:
java
csharp
// java.io.File
public long length() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(path);
}
return fs.getLength(this); // fs 是 FileSystem 实例
}
UnixFileSystem 是 OpenJDK 针对 Unix 平台的实现,它的 getLength 是一个 native 方法:
java
arduino
// java.io.UnixFileSystem
public native long getLength(File f);
这意味着真正的工作将交由 JVM 中的本地代码完成。
2. JNI 层:UnixFileSystem.c 中的实现
在 OpenJDK 源码的 UnixFileSystem.c 文件中,我们可以找到 Java_java_io_UnixFileSystem_getLength 函数,它正是上述 native 方法的 JNI 实现:
c
ini
JNIEXPORT jlong JNICALL
Java_java_io_UnixFileSystem_getLength(JNIEnv *env, jobject this,
jobject file)
{
jlong rv = 0;
WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
struct stat64 sb;
if (stat64(path, &sb) == 0) {
rv = sb.st_size;
}
} END_PLATFORM_STRING(env, path);
return rv;
}
这段代码的核心步骤:
- 使用
WITH_FIELD_PLATFORM_STRING宏从 Java 的File对象中提取文件路径(C 字符串)。 - 调用 POSIX 函数
stat64获取文件状态信息,填充到struct stat64结构体中。 - 如果调用成功,从结构体的
st_size字段获取文件大小并返回;否则返回 0。
这里的 stat64 是一个关键点:它是什么?为什么不是普通的 stat?
3. libc 层:stat64 如何与内核交互
stat64 是 C 标准库(glibc)提供的函数,用于获取文件状态,并特别支持大文件(文件大小超过 2GB)。在 64 位系统上,stat64 通常就是 stat 的别名,因为 64 位系统原生支持大文件。但为了兼容 32 位环境,glibc 提供了不同的版本。
在 Linux 内核代码中,我们看到了一个有趣的宏定义:
c
arduino
# define stat64 __stat64_time64
这暗示 stat64 实际上被重定义为 __stat64_time64,这是一个针对 64 位时间戳的版本(避免 2038 年问题)。而 __stat64_time64 最终会通过系统调用陷入内核。
在 x86_64 架构上,glibc 的 stat 系列函数通常使用 newfstatat 系统调用(系统调用号 262)。例如,stat 可能通过 newfstatat(AT_FDCWD, path, &statbuf, 0) 实现。这是因为现代 Linux 内核推荐使用 newfstatat 这类更具扩展性的系统调用,它可以基于目录文件描述符解析相对路径,同时支持 AT_EMPTY_PATH 等标志。
4. 内核层:newfstatat 系统调用
现在进入内核空间。我们看到的 Linux 内核源码中定义了 newfstatat 系统调用:
c
go
SYSCALL_DEFINE4(newfstatat, int, dfd, const char __user *, filename,
struct stat __user *, statbuf, int, flag)
{
struct kstat stat;
int error;
error = vfs_fstatat(dfd, filename, &stat, flag);
if (error)
return error;
return cp_new_stat(&stat, statbuf);
}
它接受四个参数:
dfd:目录文件描述符(可为AT_FDCWD表示当前工作目录)。filename:文件路径(用户空间指针)。statbuf:用于返回struct stat的用户空间缓冲区。flag:标志位,如AT_EMPTY_PATH、AT_SYMLINK_NOFOLLOW等。
系统调用的工作流程:
- 调用
vfs_fstatat进行虚拟文件系统(VFS)层的操作,获取文件的struct kstat信息。 - 然后调用
cp_new_stat将内核态的kstat转换为用户态期望的struct stat格式,并拷贝到用户空间。
4.1 vfs_fstatat 的实现
vfs_fstatat 的代码如下:
c
ini
int vfs_fstatat(int dfd, const char __user *filename,
struct kstat *stat, int flags)
{
int ret;
int statx_flags = flags | AT_NO_AUTOMOUNT;
struct filename *name;
// 处理 AT_EMPTY_PATH 特例(用于 fstat 模拟)
if (dfd >= 0 && flags == AT_EMPTY_PATH) {
char c;
ret = get_user(c, filename);
if (unlikely(ret))
return ret;
if (likely(!c))
return vfs_fstat(dfd, stat);
}
name = getname_flags(filename, getname_statx_lookup_flags(statx_flags), NULL);
ret = vfs_statx(dfd, name, statx_flags, stat, STATX_BASIC_STATS);
putname(name);
return ret;
}
它首先检查是否使用了 AT_EMPTY_PATH 且路径为空(即 fstat 场景),若是则直接调用 vfs_fstat 通过文件描述符获取状态。否则,通过 getname_flags 将用户路径拷贝到内核空间,然后调用 vfs_statx 执行真正的状态获取。
4.2 vfs_statx 与文件大小获取
vfs_statx 是 VFS 层的核心函数,它会根据路径进行查找,最终调用具体文件系统(如 ext4、xfs)的 getattr 操作,从 inode 中读取文件大小等元数据。struct kstat 中的 size 字段最终被赋值为文件的逻辑大小(即 i_size)。
4.3 cp_new_stat:将内核态信息拷贝回用户态
cp_new_stat 负责将 struct kstat 转换为用户空间可见的 struct stat,并进行溢出检查(例如确保文件大小在 32 位系统上不超过 2GB)。对于 64 位系统,这部分通常只是简单赋值,然后通过 copy_to_user 将数据写回用户缓冲区。
c
arduino
static int cp_new_stat(struct kstat *stat, struct stat __user *statbuf)
{
struct stat tmp;
// ... 赋值与溢出检查
tmp.st_size = stat->size;
// ...
return copy_to_user(statbuf, &tmp, sizeof(tmp)) ? -EFAULT : 0;
}
5. 完整调用链
现在,我们可以将所有的环节串联起来,形成从 Java 到内核的完整调用链:
text
scss
File.length()
-> UnixFileSystem.getLength() (native method)
-> Java_java_io_UnixFileSystem_getLength (JNI)
-> stat64(path, &sb) [libc]
-> __stat64_time64 [libc]
-> syscall(SYS_newfstatat, AT_FDCWD, path, &stat, 0) [syscall entry]
-> kernel_newfstatat()
-> vfs_fstatat()
-> vfs_statx()
-> file system's getattr() [e.g., ext4_getattr()]
-> inode->i_size
-> cp_new_stat()
-> copy_to_user()
6. 设计与兼容性考量
从上述追踪中,我们可以看到几个精妙的设计点:
- 层次分明:Java API、JNI、libc、系统调用、VFS、具体文件系统,每一层各司其职,隔离了变化。
- 大文件支持 :使用
stat64和 64 位st_size字段,确保能够处理超过 2GB 的文件。 - 64 位时间支持 :
__stat64_time64的引入,解决了 2038 年问题。 - 系统调用的演进 :
newfstatat替代了传统的stat,提供了更灵活的参数和扩展性。 - 用户/内核空间隔离 :通过
copy_to_user安全传递数据,防止内核信息泄露。
7. 结语
一个简单的 File.length() 方法,背后竟隐藏着如此复杂的路径。从 Java 程序员的角度看,我们只需调用一个方法;但从系统工程师的角度,这趟旅程跨越了用户态、内核态、VFS 抽象层和具体文件系统。理解这个调用链,不仅有助于排查性能问题(如大量 stat 调用导致的开销),也能加深对整个操作系统与运行时协作机制的认识。
下次当你调用 file.length() 时,或许可以想象一下:一行代码,一次内核之旅。
源码
ini
@Override
public native long getLength(File f);
JNIEXPORT jlong JNICALL
Java_java_io_UnixFileSystem_getLength(JNIEnv *env, jobject this,
jobject file)
{
jlong rv = 0;
WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
struct stat64 sb;
if (stat64(path, &sb) == 0) {
rv = sb.st_size;
}
} END_PLATFORM_STRING(env, path);
return rv;
}
# define stat64 __stat64_time64
262 common newfstatat sys_newfstatat
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
__X64_SYS_STUBx(x, name, __VA_ARGS__) \
__IA32_SYS_STUBx(x, name, __VA_ARGS__) \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#if !defined(__ARCH_WANT_STAT64) || defined(__ARCH_WANT_SYS_NEWFSTATAT)
SYSCALL_DEFINE4(newfstatat, int, dfd, const char __user *, filename,
struct stat __user *, statbuf, int, flag)
{
struct kstat stat;
int error;
error = vfs_fstatat(dfd, filename, &stat, flag);
if (error)
return error;
return cp_new_stat(&stat, statbuf);
}
#endif
static int cp_new_stat(struct kstat *stat, struct stat __user *statbuf)
{
struct stat tmp;
if (sizeof(tmp.st_dev) < 4 && !old_valid_dev(stat->dev))
return -EOVERFLOW;
if (sizeof(tmp.st_rdev) < 4 && !old_valid_dev(stat->rdev))
return -EOVERFLOW;
#if BITS_PER_LONG == 32
if (stat->size > MAX_NON_LFS)
return -EOVERFLOW;
#endif
INIT_STRUCT_STAT_PADDING(tmp);
tmp.st_dev = new_encode_dev(stat->dev);
tmp.st_ino = stat->ino;
if (sizeof(tmp.st_ino) < sizeof(stat->ino) && tmp.st_ino != stat->ino)
return -EOVERFLOW;
tmp.st_mode = stat->mode;
tmp.st_nlink = stat->nlink;
if (tmp.st_nlink != stat->nlink)
return -EOVERFLOW;
SET_UID(tmp.st_uid, from_kuid_munged(current_user_ns(), stat->uid));
SET_GID(tmp.st_gid, from_kgid_munged(current_user_ns(), stat->gid));
tmp.st_rdev = new_encode_dev(stat->rdev);
tmp.st_size = stat->size;
tmp.st_atime = stat->atime.tv_sec;
tmp.st_mtime = stat->mtime.tv_sec;
tmp.st_ctime = stat->ctime.tv_sec;
#ifdef STAT_HAVE_NSEC
tmp.st_atime_nsec = stat->atime.tv_nsec;
tmp.st_mtime_nsec = stat->mtime.tv_nsec;
tmp.st_ctime_nsec = stat->ctime.tv_nsec;
#endif
tmp.st_blocks = stat->blocks;
tmp.st_blksize = stat->blksize;
return copy_to_user(statbuf,&tmp,sizeof(tmp)) ? -EFAULT : 0;
}
int vfs_fstatat(int dfd, const char __user *filename,
struct kstat *stat, int flags)
{
int ret;
int statx_flags = flags | AT_NO_AUTOMOUNT;
struct filename *name;
/*
* Work around glibc turning fstat() into fstatat(AT_EMPTY_PATH)
*
* If AT_EMPTY_PATH is set, we expect the common case to be that
* empty path, and avoid doing all the extra pathname work.
*/
if (dfd >= 0 && flags == AT_EMPTY_PATH) {
char c;
ret = get_user(c, filename);
if (unlikely(ret))
return ret;
if (likely(!c))
return vfs_fstat(dfd, stat);
}
name = getname_flags(filename, getname_statx_lookup_flags(statx_flags), NULL);
ret = vfs_statx(dfd, name, statx_flags, stat, STATX_BASIC_STATS);
putname(name);
return ret;
}