在 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

// 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

// 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

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

#   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

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_PATHAT_SYMLINK_NOFOLLOW 等。

系统调用的工作流程:

  1. 调用 vfs_fstatat 进行虚拟文件系统(VFS)层的操作,获取文件的 struct kstat 信息。
  2. 然后调用 cp_new_stat 将内核态的 kstat 转换为用户态期望的 struct stat 格式,并拷贝到用户空间。

4.1 vfs_fstatat 的实现

vfs_fstatat 的代码如下:

c

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

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

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() 时,或许可以想象一下:一行代码,一次内核之旅。

源码
@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;
}



本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com