从 Java File.length() 到 Linux 内核:一次系统调用追踪之旅

在 Java 中获取文件大小是一个再常见不过的操作,只需调用 File.length() 即可。但这一行简单的代码背后,却跨越了从 Java 标准库到操作系统内核的多个层次。本文将以 OpenJDK 17 和 Linux 6 内核源码为基础,深入剖析 File.length() 在 Linux 系统上的完整调用链,带你一窥现代软件栈的精巧设计。

1. Java 层:File.length() 与 UnixFileSystem

Java 的 java.io.File 类提供了 length() 方法,用于返回文件的长度。其实现依赖于底层文件系统,在 Unix 类系统(包括 Linux)上,最终委托给 UnixFileSystemgetLength 方法:

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

系统调用的工作流程:

  1. 调用 vfs_fstatat 进行虚拟文件系统(VFS)层的操作,获取文件的 struct kstat 信息。
  2. 然后调用 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;
}
相关推荐
原来是猿2 小时前
Linux - 基础IO【中】
linux·运维·服务器
为美好的生活献上中指2 小时前
*Java 沉淀重走长征路*之——《Java Web 应用开发完全指南:从零到企业实战(两万字深度解析)》
java·开发语言·前端·html·javaweb·js
主角1 72 小时前
Linux系统安全
linux·运维·系统安全
li星野2 小时前
QT面试题
java·数据库·qt
不光头强2 小时前
抽象类和接口的区别
java·开发语言·python
xiaoye37082 小时前
Spring 的自动装配 vs 手动注入
java·spring
好学且牛逼的马2 小时前
Spring Boot 核心注解完全手册
java·spring boot·后端
彭于晏Yan2 小时前
Spring Boot监听Redis Key过期事件
java·spring boot·redis
weixin_418270532 小时前
window上codex 安装
java