你贴出的源码其实串起了一条完整链路:Java File.list() → glibc readdir → getdents64 系统调用 → VFS iterate_dir → ext4 ext4_readdir 。这条链路决定了 ls -U 看到的顺序,也决定了 Tomcat 扫描 lib/WEB-INF/lib 下 jar 时底层枚举到的原始顺序。
先给结论:
ls -u小写是按访问时间排序,不是哈希序;ls -U大写才是"不排序",直接按readdir返回顺序输出。- 在 ext4 默认启用
dir_index的现代系统上,readdir返回的是 htree 目录哈希序。 - Java
File.list()本身不排序,它拿到的就是readdir顺序。 - Tomcat 加载
lib目录下 jar 时,通常会先File.list(),然后Arrays.sort(),所以最终类加载路径是 文件名字典序 ,而不是 ext4 哈希序。WEB-INF/lib通常也会排序。若某处直接使用未排序的目录列表,才会暴露哈希序。
下面沿着你提供的源码逐层证明。
一、Java 层:File.list() 不排序
你贴出的 JDK 源码:
c
ini
JNIEXPORT jobjectArray JNICALL
Java_java_io_UnixFileSystem_list(JNIEnv *env, jobject this,
jobject file)
{
DIR *dir = NULL;
struct dirent *ptr;
int len, maxlen;
jobjectArray rv, old;
jclass str_class;
str_class = JNU_ClassString(env);
CHECK_NULL_RETURN(str_class, NULL);
WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
dir = opendir(path);
} END_PLATFORM_STRING(env, path);
if (dir == NULL) return NULL;
len = 0;
maxlen = 16;
rv = (*env)->NewObjectArray(env, maxlen, str_class, NULL);
if (rv == NULL) goto error;
/* Scan the directory */
while ((ptr = readdir(dir)) != NULL) { // 【注释】直接按 readdir 返回顺序遍历
jstring name;
if (!strcmp(ptr->d_name, ".") || !strcmp(ptr->d_name, ".."))
continue;
if (len == maxlen) {
old = rv;
rv = (*env)->NewObjectArray(env, maxlen <<= 1, str_class, NULL);
if (rv == NULL) goto error;
if (JNU_CopyObjectArray(env, rv, old, len) < 0) goto error;
(*env)->DeleteLocalRef(env, old);
}
#ifdef MACOSX
name = newStringPlatform(env, ptr->d_name);
#else
name = JNU_NewStringPlatform(env, ptr->d_name);
#endif
if (name == NULL) goto error;
(*env)->SetObjectArrayElement(env, rv, len++, name); // 【注释】原样放入数组,没有排序
(*env)->DeleteLocalRef(env, name);
}
closedir(dir);
...
}
关键点:while ((ptr = readdir(dir)) != NULL) 之后,直接 SetObjectArrayElement。Java 没有做任何排序 。JDK 文档说 File.list() 不保证顺序,原因就在这里------它把底层顺序原封不动地暴露出来。
二、glibc 层:readdir 只是缓冲区搬运工
再看你贴出的 glibc 2.39 源码:
c
ini
struct dirent64 *
__readdir64 (DIR *dirp)
{
struct dirent64 *dp;
int saved_errno = errno;
#if IS_IN (libc)
__libc_lock_lock (dirp->lock);
#endif
if (dirp->offset >= dirp->size)
{
/* We've emptied out our buffer. Refill it. */
size_t maxread = dirp->allocation;
ssize_t bytes;
bytes = __getdents64 (dirp->fd, dirp->data, maxread); // 【注释】缓冲区空才向内核要一批目录项
if (bytes <= 0)
{
if (bytes == 0 || errno == ENOENT)
__set_errno (saved_errno);
#if IS_IN (libc)
__libc_lock_unlock (dirp->lock);
#endif
return NULL;
}
dirp->size = (size_t) bytes;
dirp->offset = 0;
}
dp = (struct dirent64 *) &dirp->data[dirp->offset];
dirp->offset += dp->d_reclen; // 【注释】按内核返回的字节流顺序逐个切分
dirp->filepos = dp->d_off;
#if IS_IN (libc)
__libc_lock_unlock (dirp->lock);
#endif
return dp;
}
readdir 每次调用只是从 dirp->data 缓冲区里取出一个 dirent64。缓冲区里的内容来自 __getdents64,而 __getdents64 只是发起系统调用:
c
arduino
ssize_t
__getdents64 (int fd, void *buf, size_t nbytes)
{
if (nbytes > INT_MAX)
nbytes = INT_MAX;
return INLINE_SYSCALL_CALL (getdents64, fd, buf, nbytes); // 【注释】进入内核
}
glibc 不会重排目录项。顺序在内核返回数据时已经固定。
三、内核系统调用层:交给 VFS
你贴出的 getdents64 系统调用:
c
ini
SYSCALL_DEFINE3(getdents64, unsigned int, fd,
struct linux_dirent64 __user *, dirent, unsigned int, count)
{
struct fd f;
struct getdents_callback64 buf = {
.ctx.actor = filldir64,
.count = count,
.current_dir = dirent
};
int error;
f = fdget_pos(fd);
if (!f.file)
return -EBADF;
...
error = iterate_dir(f.file, &buf.ctx); // 【注释】交给 VFS 的 iterate_dir
...
}
系统调用本身没有排序逻辑,它把"遍历目录"的请求转给 VFS。
四、VFS 层:调用具体文件系统的 iterate_shared
你贴出的 iterate_dir:
c
ini
int iterate_dir(struct file *file, struct dir_context *ctx)
{
struct inode *inode = file_inode(file);
...
int res = -ENOTDIR;
if (!file->f_op->iterate_shared)
goto out;
res = security_file_permission(file, MAY_READ);
if (res)
goto out;
res = fsnotify_file_perm(file, MAY_READ);
if (res)
goto out;
res = down_read_killable(&inode->i_rwsem);
if (res)
goto out;
res = -ENOENT;
if (!IS_DEADDIR(inode)) {
ctx->pos = file->f_pos;
res = file->f_op->iterate_shared(file, ctx); // 【注释】调用具体文件系统的 iterate_shared
file->f_pos = ctx->pos;
fsnotify_access(file);
file_accessed(file);
}
inode_unlock_shared(inode);
out:
return res;
}
EXPORT_SYMBOL(iterate_dir);
VFS 只是路由。对于 ext4 目录,file->f_op 是:
c
ini
const struct file_operations ext4_dir_operations = {
.llseek = ext4_dir_llseek,
.read = generic_read_dir,
.iterate_shared = ext4_readdir, // 【注释】ext4 目录遍历入口
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.fsync = ext4_sync_file,
.release = ext4_release_dir,
};
所以最终进入 ext4_readdir。
五、ext4 层:htree 决定哈希序
你贴出的 ext4_readdir 关键片段:
c
scss
static int ext4_readdir(struct file *file, struct dir_context *ctx)
{
...
if (is_dx_dir(inode)) {
err = ext4_dx_readdir(file, ctx); // 【注释】目录有 htree 索引时走哈希树遍历
if (err != ERR_BAD_DX_DIR)
return err;
...
}
if (ext4_has_inline_data(inode)) {
int has_inline_data = 1;
err = ext4_read_inline_dir(file, ctx, &has_inline_data);
if (has_inline_data)
return err;
}
...
while (ctx->pos < inode->i_size) {
...
while (ctx->pos < inode->i_size
&& offset < sb->s_blocksize) {
de = (struct ext4_dir_entry_2 *) (bh->b_data + offset);
...
offset += ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize);
if (le32_to_cpu(de->inode)) {
pr_debug("ext4_readdir: found %.*s\n", de->name_len, de->name);
if (!IS_ENCRYPTED(inode)) {
if (!dir_emit(ctx, de->name,
de->name_len,
le32_to_cpu(de->inode),
get_dtype(sb, de->file_type))) // 【注释】按当前块内目录项顺序输出
goto done;
} else {
...
}
}
ctx->pos += ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize);
}
...
}
done:
err = 0;
errout:
fscrypt_fname_free_buffer(&fstr);
brelse(bh);
return err;
}
关键分支是:
c
scss
if (is_dx_dir(inode)) {
err = ext4_dx_readdir(file, ctx); // 【注释】哈希树遍历
if (err != ERR_BAD_DX_DIR)
return err;
}
现代 Linux 发行版格式化 ext4 时默认启用 dir_index。因此大多数目录都走 ext4_dx_readdir,遍历的是目录的 B 树索引。叶块中的目录项按文件名哈希值 排列,所以 readdir 返回的是哈希序。
如果目录很小、没有启用 htree,或者 htree 损坏回退,才会走线性扫描。这时顺序大致是创建顺序,但删除文件后新文件可能填入空洞,顺序就不再稳定。
六、ls -U 和 Tomcat libs 的顺序
ls -U 是"不排序",直接输出 readdir 顺序。所以在 ext4 htree 目录上,你看到的就是哈希序。默认 ls 看起来是字母序,是因为 ls 自己在用户态排序。
Tomcat 加载 lib 目录下 jar 时,底层同样要先枚举目录。Tomcat 的 common.loader 默认包含 ${catalina.home}/lib/*.jar。解析通配符时,Tomcat 会调用 File.list() 获取目录项。你贴出的 Java_java_io_UnixFileSystem_list 已经证明:File.list() 本身返回的是 readdir 顺序,不排序。所以 Tomcat 拿到的原始数组在 ext4 htree 下就是哈希序。
但 Tomcat 通常不会直接依赖这个原始顺序。ClassLoaderFactory 在展开 *.jar 时,一般会:
java
scss
String[] files = dir.list(); // 底层是 ext4 哈希序
Arrays.sort(files); // Tomcat 显式排序
for (String file : files) {
if (file.endsWith(".jar")) {
// 按字典序加入类加载路径
}
}
因此 Tomcat lib 下 jar 的最终查找顺序是 文件名字典序 。WEB-INF/lib 下 jar 通常也会在 WebappClassLoaderBase 启动时对 resources.list("/WEB-INF/lib") 结果排序后加入。Tomcat 这样做是为了可重复性,避免不同机器、不同 ext4 目录哈希布局导致类加载顺序漂移。
所以完整答案是:
- 底层
File.list()/readdir:ext4 htree 下是哈希序; - Tomcat
lib加载 :Tomcat 自己Arrays.sort(),所以是字典序; - 若某处未排序:就会暴露 ext4 哈希序。
实践上不要依赖 jar 扫描顺序来消解同名类冲突。需要稳定顺序时,显式排序;需要避免冲突时,不要放同名类,或使用更明确的类加载隔离策略。 #源码
scss
@Override
public native String[] list(File f);
JNIEXPORT jobjectArray JNICALL
Java_java_io_UnixFileSystem_list(JNIEnv *env, jobject this,
jobject file)
{
DIR *dir = NULL;
struct dirent *ptr;
int len, maxlen;
jobjectArray rv, old;
jclass str_class;
str_class = JNU_ClassString(env);
CHECK_NULL_RETURN(str_class, NULL);
WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
dir = opendir(path);
} END_PLATFORM_STRING(env, path);
if (dir == NULL) return NULL;
/* Allocate an initial String array */
len = 0;
maxlen = 16;
rv = (*env)->NewObjectArray(env, maxlen, str_class, NULL);
if (rv == NULL) goto error;
/* Scan the directory */
while ((ptr = readdir(dir)) != NULL) {
jstring name;
if (!strcmp(ptr->d_name, ".") || !strcmp(ptr->d_name, ".."))
continue;
if (len == maxlen) {
old = rv;
rv = (*env)->NewObjectArray(env, maxlen <<= 1, str_class, NULL);
if (rv == NULL) goto error;
if (JNU_CopyObjectArray(env, rv, old, len) < 0) goto error;
(*env)->DeleteLocalRef(env, old);
}
#ifdef MACOSX
name = newStringPlatform(env, ptr->d_name);
#else
name = JNU_NewStringPlatform(env, ptr->d_name);
#endif
if (name == NULL) goto error;
(*env)->SetObjectArrayElement(env, rv, len++, name);
(*env)->DeleteLocalRef(env, name);
}
closedir(dir);
/* Copy the final results into an appropriately-sized array */
if (len < maxlen) {
old = rv;
rv = (*env)->NewObjectArray(env, len, str_class, NULL);
if (rv == NULL) {
return NULL;
}
if (JNU_CopyObjectArray(env, rv, old, len) < 0) {
return NULL;
}
}
return rv;
error:
closedir(dir);
return NULL;
}
/* Read a directory entry from DIRP. */
struct dirent64 *
__readdir64 (DIR *dirp)
{
struct dirent64 *dp;
int saved_errno = errno;
#if IS_IN (libc)
__libc_lock_lock (dirp->lock);
#endif
if (dirp->offset >= dirp->size)
{
/* We've emptied out our buffer. Refill it. */
size_t maxread = dirp->allocation;
ssize_t bytes;
bytes = __getdents64 (dirp->fd, dirp->data, maxread);
if (bytes <= 0)
{
/* Linux may fail with ENOENT on some file systems if the
directory inode is marked as dead (deleted). POSIX
treats this as a regular end-of-directory condition, so
do not set errno in that case, to indicate success. */
if (bytes == 0 || errno == ENOENT)
__set_errno (saved_errno);
#if IS_IN (libc)
__libc_lock_unlock (dirp->lock);
#endif
return NULL;
}
dirp->size = (size_t) bytes;
/* Reset the offset into the buffer. */
dirp->offset = 0;
}
dp = (struct dirent64 *) &dirp->data[dirp->offset];
dirp->offset += dp->d_reclen;
dirp->filepos = dp->d_off;
#if IS_IN (libc)
__libc_lock_unlock (dirp->lock);
#endif
return dp;
}
libc_hidden_def (__readdir64)
#if _DIRENT_MATCHES_DIRENT64
strong_alias (__readdir64, __readdir)
weak_alias (__readdir64, readdir64)
weak_alias (__readdir64, readdir)
/* The kernel struct linux_dirent64 matches the 'struct dirent64' type. */
ssize_t
__getdents64 (int fd, void *buf, size_t nbytes)
{
/* The system call takes an unsigned int argument, and some length
checks in the kernel use an int type. */
if (nbytes > INT_MAX)
nbytes = INT_MAX;
return INLINE_SYSCALL_CALL (getdents64, fd, buf, nbytes);
}
libc_hidden_def (__getdents64)
weak_alias (__getdents64, getdents64)
SYSCALL_DEFINE3(getdents64, unsigned int, fd,
struct linux_dirent64 __user *, dirent, unsigned int, count)
{
struct fd f;
struct getdents_callback64 buf = {
.ctx.actor = filldir64,
.count = count,
.current_dir = dirent
};
int error;
f = fdget_pos(fd);
if (!f.file)
return -EBADF;
// yym-gaizao
// ========== 添加打印路径的代码 ==========
char *path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
if (path_buf) {
char *path = d_path(&(f.file)->f_path, path_buf, PATH_MAX);
if (!IS_ERR(path))
pr_debug("getdents64: file fd=%d, name: %s\n", fd, path);
else
pr_debug("getdents64: file fd=%d (d_path error)\n", fd);
kfree(path_buf);
} else {
pr_debug("getdents64: file fd=%d (no memory for path)\n", fd);
}
error = iterate_dir(f.file, &buf.ctx);
if (error >= 0)
error = buf.error;
if (buf.prev_reclen) {
struct linux_dirent64 __user * lastdirent;
typeof(lastdirent->d_off) d_off = buf.ctx.pos;
lastdirent = (void __user *) buf.current_dir - buf.prev_reclen;
if (put_user(d_off, &lastdirent->d_off))
error = -EFAULT;
else
error = count - buf.count;
}
fdput_pos(f);
return error;
}
int iterate_dir(struct file *file, struct dir_context *ctx)
{
struct inode *inode = file_inode(file);
// yym-gaizao
// ========== 添加打印路径的代码 ==========
char *path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
if (path_buf) {
char *path = d_path(&file->f_path, path_buf, PATH_MAX);
if (!IS_ERR(path))
pr_debug("iterate_dir: %s, ino=%lu\n", path, inode->i_ino);
else
pr_debug("iterate_dir: (error), ino=%lu\n", inode->i_ino);
kfree(path_buf);
} else {
pr_debug("iterate_dir: (no memory for path)\n");
}
int res = -ENOTDIR;
if (!file->f_op->iterate_shared)
goto out;
res = security_file_permission(file, MAY_READ);
if (res)
goto out;
res = fsnotify_file_perm(file, MAY_READ);
if (res)
goto out;
res = down_read_killable(&inode->i_rwsem);
if (res)
goto out;
res = -ENOENT;
if (!IS_DEADDIR(inode)) {
ctx->pos = file->f_pos;
res = file->f_op->iterate_shared(file, ctx);
file->f_pos = ctx->pos;
fsnotify_access(file);
file_accessed(file);
}
inode_unlock_shared(inode);
out:
return res;
}
EXPORT_SYMBOL(iterate_dir);
const struct file_operations ext4_dir_operations = {
.llseek = ext4_dir_llseek,
.read = generic_read_dir,
.iterate_shared = ext4_readdir,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.fsync = ext4_sync_file,
.release = ext4_release_dir,
};
static int ext4_readdir(struct file *file, struct dir_context *ctx)
{
unsigned int offset;
int i;
struct ext4_dir_entry_2 *de;
int err;
struct inode *inode = file_inode(file);
struct super_block *sb = inode->i_sb;
struct buffer_head *bh = NULL;
struct fscrypt_str fstr = FSTR_INIT(NULL, 0);
err = fscrypt_prepare_readdir(inode);
if (err)
return err;
if (is_dx_dir(inode)) {
err = ext4_dx_readdir(file, ctx);
if (err != ERR_BAD_DX_DIR)
return err;
/* Can we just clear INDEX flag to ignore htree information? */
if (!ext4_has_metadata_csum(sb)) {
/*
* We don't set the inode dirty flag since it's not
* critical that it gets flushed back to the disk.
*/
ext4_clear_inode_flag(inode, EXT4_INODE_INDEX);
}
}
if (ext4_has_inline_data(inode)) {
int has_inline_data = 1;
err = ext4_read_inline_dir(file, ctx,
&has_inline_data);
if (has_inline_data)
return err;
}
if (IS_ENCRYPTED(inode)) {
err = fscrypt_fname_alloc_buffer(EXT4_NAME_LEN, &fstr);
if (err < 0)
return err;
}
while (ctx->pos < inode->i_size) {
struct ext4_map_blocks map;
if (fatal_signal_pending(current)) {
err = -ERESTARTSYS;
goto errout;
}
cond_resched();
offset = ctx->pos & (sb->s_blocksize - 1);
map.m_lblk = ctx->pos >> EXT4_BLOCK_SIZE_BITS(sb);
map.m_len = 1;
err = ext4_map_blocks(NULL, inode, &map, 0);
if (err == 0) {
/* m_len should never be zero but let's avoid
* an infinite loop if it somehow is */
if (map.m_len == 0)
map.m_len = 1;
ctx->pos += map.m_len * sb->s_blocksize;
continue;
}
if (err > 0) {
pgoff_t index = map.m_pblk >>
(PAGE_SHIFT - inode->i_blkbits);
if (!ra_has_index(&file->f_ra, index))
page_cache_sync_readahead(
sb->s_bdev->bd_inode->i_mapping,
&file->f_ra, file,
index, 1);
file->f_ra.prev_pos = (loff_t)index << PAGE_SHIFT;
bh = ext4_bread(NULL, inode, map.m_lblk, 0);
if (IS_ERR(bh)) {
err = PTR_ERR(bh);
bh = NULL;
goto errout;
}
}
if (!bh) {
/* corrupt size? Maybe no more blocks to read */
if (ctx->pos > inode->i_blocks << 9)
break;
ctx->pos += sb->s_blocksize - offset;
continue;
}
/* Check the checksum */
if (!buffer_verified(bh) &&
!ext4_dirblock_csum_verify(inode, bh)) {
EXT4_ERROR_FILE(file, 0, "directory fails checksum "
"at offset %llu",
(unsigned long long)ctx->pos);
ctx->pos += sb->s_blocksize - offset;
brelse(bh);
bh = NULL;
continue;
}
set_buffer_verified(bh);
/* If the dir block has changed since the last call to
* readdir(2), then we might be pointing to an invalid
* dirent right now. Scan from the start of the block
* to make sure. */
if (!inode_eq_iversion(inode, file->f_version)) {
for (i = 0; i < sb->s_blocksize && i < offset; ) {
de = (struct ext4_dir_entry_2 *)
(bh->b_data + i);
/* It's too expensive to do a full
* dirent test each time round this
* loop, but we do have to test at
* least that it is non-zero. A
* failure will be detected in the
* dirent test below. */
if (ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize) < ext4_dir_rec_len(1,
inode))
break;
i += ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize);
}
offset = i;
ctx->pos = (ctx->pos & ~(sb->s_blocksize - 1))
| offset;
file->f_version = inode_query_iversion(inode);
}
while (ctx->pos < inode->i_size
&& offset < sb->s_blocksize) {
de = (struct ext4_dir_entry_2 *) (bh->b_data + offset);
if (ext4_check_dir_entry(inode, file, de, bh,
bh->b_data, bh->b_size,
offset)) {
/*
* On error, skip to the next block
*/
ctx->pos = (ctx->pos |
(sb->s_blocksize - 1)) + 1;
break;
}
offset += ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize);
if (le32_to_cpu(de->inode)) {
//yym-gaizao
pr_debug("ext4_readdir: found %.*s\n", de->name_len, de->name);
if (!IS_ENCRYPTED(inode)) {
if (!dir_emit(ctx, de->name,
de->name_len,
le32_to_cpu(de->inode),
get_dtype(sb, de->file_type)))
goto done;
} else {
int save_len = fstr.len;
struct fscrypt_str de_name =
FSTR_INIT(de->name,
de->name_len);
/* Directory is encrypted */
err = fscrypt_fname_disk_to_usr(inode,
EXT4_DIRENT_HASH(de),
EXT4_DIRENT_MINOR_HASH(de),
&de_name, &fstr);
de_name = fstr;
fstr.len = save_len;
if (err)
goto errout;
if (!dir_emit(ctx,
de_name.name, de_name.len,
le32_to_cpu(de->inode),
get_dtype(sb, de->file_type)))
goto done;
}
}
ctx->pos += ext4_rec_len_from_disk(de->rec_len,
sb->s_blocksize);
}
if ((ctx->pos < inode->i_size) && !dir_relax_shared(inode))
goto done;
brelse(bh);
bh = NULL;
}
done:
err = 0;
errout:
fscrypt_fname_free_buffer(&fstr);
brelse(bh);
return err;
}