从mkdir命令到磁盘:Linux内核目录创建过程深度解析

1. 引言

当我们在终端输入mkdir newdir时,一个崭新的目录就在文件系统中诞生了。这个看似简单的操作,背后却是操作系统内核中VFS(虚拟文件系统)、具体文件系统(如ext4)、日志系统、缓存管理等多个模块协作的结果。本文将基于Linux内核6.8.12版本的源码,逐层剖析mkdir系统调用的实现细节。为了让读者更容易理解,我们会在关键代码行添加中文注释,解释每一段代码的意图和作用。

全文将沿着如下路径展开:

  • 用户态系统调用入口

  • 核心函数do_mkdirat:路径解析与准备

  • VFS通用创建函数vfs_mkdir:权限与一致性检查

  • ext4文件系统的ext4_mkdir:inode分配、日志事务、目录初始化

  • 父目录中添加目录项:ext4_add_entryadd_dirent_to_buf

  • 错误处理和重试机制

通过本文,你将看到内核如何保证即使在系统崩溃、磁盘空间紧张等异常情况下,mkdir操作依然能做到"要么全部成功,要么全部失败"(原子性)。


2. 系统调用入口:sys_mkdir

mkdir系统调用在内核中定义为SYSCALL_DEFINE2(mkdir, ...)。这是内核用来定义系统调用的标准宏,数字2表示有两个参数。

c

复制代码
// 系统调用: mkdir(const char *pathname, umode_t mode)
SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)
{
    // 使用 AT_FDCWD 表示相对路径基于当前工作目录
    // getname() 将用户空间路径字符串安全拷贝到内核空间
    return do_mkdirat(AT_FDCWD, getname(pathname), mode);
}

中文注释

  • SYSCALL_DEFINE2:定义一个系统调用函数,名称mkdir,两个参数。

  • const char __user *pathname:用户空间传来的路径指针,__user标记表示不能直接解引用。

  • umode_t mode:新建目录的权限模式(如0755),但实际会被当前进程的umask过滤。

  • getname(pathname):复制用户路径到内核,返回struct filename *,该结构体内含引用计数和路径字符串。

  • AT_FDCWD:是一个特殊的文件描述符值(-100),表示"当前工作目录",用于相对路径解析。


3. 核心处理:do_mkdirat

do_mkdirat是VFS层中处理目录创建的核心函数,它不依赖于特定文件系统。

c

复制代码
int do_mkdirat(int dfd, struct filename *name, umode_t mode)
{
    struct dentry *dentry;
    struct path path;
    int error;
    unsigned int lookup_flags = LOOKUP_DIRECTORY;  // 要求路径最后分量不能是文件

retry:
    // 文件名创建: 查找父目录,并创建最后分量的dentry(但尚未关联inode)
    dentry = filename_create(dfd, name, &path, lookup_flags);
    error = PTR_ERR(dentry);      // 如果返回错误码,转换
    if (IS_ERR(dentry))           // 检查dentry是否有效指针
        goto out_putname;

    // 安全模块检查(SELinux等),同时使用mode_strip_umask过滤umask
    error = security_path_mkdir(&path, dentry,
            mode_strip_umask(path.dentry->d_inode, mode));
    if (!error) {
        // 调用VFS创建函数,传入id映射(用于用户命名空间)
        error = vfs_mkdir(mnt_idmap(path.mnt), path.dentry->d_inode,
                          dentry, mode);
    }
    // 释放路径和dentry的引用,并解锁父目录
    done_path_create(&path, dentry);
    
    // 如果错误是 ESTALE(陈旧句柄,常见于NFS),则重试并强制重新验证
    if (retry_estale(error, lookup_flags)) {
        lookup_flags |= LOOKUP_REVAL;
        goto retry;
    }
out_putname:
    putname(name);   // 释放内核路径名结构
    return error;
}

中文注释

  • LOOKUP_DIRECTORY标志:要求路径最终分量必须是目录,如果已存在且为普通文件则会报错。

  • filename_create():这是关键函数,它完成路径查找,返回父目录的path和新目录的dentry(该dentry处于未链接状态)。

  • mode_strip_umask():根据父目录的默认ACL和当前进程的umask,调整用户传入的mode,得到最终权限。

  • retry_estale():检测-ESTALE错误并决定是否需要重试(网络文件系统常用)。

  • done_path_create():释放filename_create中获取的引用,防止内存泄漏。


4. VFS通用创建:vfs_mkdir

vfs_mkdir负责所有文件系统通用的检查,然后调用具体文件系统的mkdir回调。

c

复制代码
/**
 * vfs_mkdir - 创建目录的统一入口
 * @idmap:   id映射(用于用户命名空间)
 * @dir:     父目录的inode
 * @dentry:  新目录的dentry(尚未关联inode)
 * @mode:    最终权限模式(已去除umask)
 */
int vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
              struct dentry *dentry, umode_t mode)
{
    int error;
    unsigned max_links = dir->i_sb->s_max_links;  // 文件系统允许的最大链接数

    // 检查是否可以在父目录中创建条目(写权限、不可变属性、配额等)
    error = may_create(idmap, dir, dentry);
    if (error)
        return error;

    // 父目录的inode操作表必须实现mkdir回调
    if (!dir->i_op->mkdir)
        return -EPERM;

    // 最后调整模式(保留常规权限位和粘滞位)
    mode = vfs_prepare_mode(idmap, dir, mode, S_IRWXUGO | S_ISVTX, 0);
    
    // 第二个安全钩子:基于inode和dentry的检查
    error = security_inode_mkdir(dir, dentry, mode);
    if (error)
        return error;

    // 如果父目录的链接数达到上限(例如65000),不能增加子目录
    if (max_links && dir->i_nlink >= max_links)
        return -EMLINK;

    // 调用具体文件系统的mkdir函数(ext4_mkdir, xfs_mkdir等)
    error = dir->i_op->mkdir(idmap, dir, dentry, mode);
    if (!error)
        fsnotify_mkdir(dir, dentry);   // 通知inotify/fanotify目录创建事件
    return error;
}

中文注释

  • max_links:文件系统限制,避免目录嵌套过深导致链接数溢出。ext4中最大为65000。

  • may_create():检查父目录是否可写,以及文件是否具有不可变属性(S_IMMUTABLE)。

  • vfs_prepare_mode():根据父目录的idmap和当前进程的fs->umask,计算最终mode。第三个参数是允许的位掩码。

  • security_inode_mkdir():LSM框架的第二次检查,某些模块需要在此进行更细粒度的控制。

  • fsnotify_mkdir():文件系统通知机制,用于监听目录变化。


5. ext4文件系统中的mkdirext4_mkdir

ext4是Linux上最广泛使用的日志文件系统。它的目录inode操作表中包含了.mkdir = ext4_mkdir

c

复制代码
// ext4目录的inode操作函数表
const struct inode_operations ext4_dir_inode_operations = {
    .create     = ext4_create,
    .lookup     = ext4_lookup,
    .link       = ext4_link,
    .unlink     = ext4_unlink,
    .symlink    = ext4_symlink,
    .mkdir      = ext4_mkdir,          // 就是下面这个函数
    .rmdir      = ext4_rmdir,
    .mknod      = ext4_mknod,
    // ... 其他操作
};

下面是ext4_mkdir的完整带注释实现:

c

复制代码
static int ext4_mkdir(struct mnt_idmap *idmap, struct inode *dir,
                      struct dentry *dentry, umode_t mode)
{
    handle_t *handle;              // 日志事务句柄
    struct inode *inode;           // 新分配的inode
    int err, err2 = 0, credits, retries = 0;

    // 检查父目录链接数是否达到ext4上限
    if (EXT4_DIR_LINK_MAX(dir))
        return -EMLINK;

    // 初始化磁盘配额(如果启用了quota)
    err = dquot_initialize(dir);
    if (err)
        return err;

    // 计算这次操作需要预留多少日志块(元数据修改块数)
    credits = (EXT4_DATA_TRANS_BLOCKS(dir->i_sb) +
               EXT4_INDEX_EXTRA_TRANS_BLOCKS + 3);
retry:
    // 分配新的inode并启动日志事务
    // 参数:idmap, 父目录, 文件类型S_IFDIR|mode, 文件名, ...
    inode = ext4_new_inode_start_handle(idmap, dir, S_IFDIR | mode,
                                        &dentry->d_name,
                                        0, NULL, EXT4_HT_DIR, credits);
    handle = ext4_journal_current_handle();  // 获取当前事务的handle
    err = PTR_ERR(inode);
    if (IS_ERR(inode))
        goto out_stop;

    // 设置新目录的inode操作函数表(目录专用)
    inode->i_op = &ext4_dir_inode_operations;
    inode->i_fop = &ext4_dir_operations;    // 目录的file_operations
    
    // 初始化新目录的内容:创建 '.' 和 '..' 条目
    err = ext4_init_new_dir(handle, dir, inode);
    if (err)
        goto out_clear_inode;
    
    // 将新目录的inode标记为脏,等待写入磁盘
    err = ext4_mark_inode_dirty(handle, inode);
    if (!err)
        // 在父目录中添加目录项(文件名 -> 新inode的映射)
        err = ext4_add_entry(handle, dentry, inode);
    
    if (err) {
out_clear_inode:
        // 出错处理:清除链接数,将inode加入孤儿列表(日志会跟踪)
        clear_nlink(inode);
        ext4_orphan_add(handle, inode);
        unlock_new_inode(inode);          // 释放新inode的锁
        err2 = ext4_mark_inode_dirty(handle, inode);
        if (unlikely(err2))
            err = err2;
        ext4_journal_stop(handle);        // 停止事务
        iput(inode);                      // 释放inode引用(由于nlink=0会被删除)
        goto out_retry;
    }
    
    // 成功:父目录链接数加1(因为新目录有 '..' 指向它)
    ext4_inc_count(dir);
    
    // 如果目录项数量达到阈值,可能需要启用dx(索引)特性
    ext4_update_dx_flag(dir);
    err = ext4_mark_inode_dirty(handle, dir);
    if (err)
        goto out_clear_inode;
    
    // 将dentry与inode关联,并标记dentry为有效(放入dcache)
    d_instantiate_new(dentry, inode);
    ext4_fc_track_create(handle, dentry);   // 记录fast-commit日志(如果启用)
    
    // 如果父目录要求同步(O_SYNC或dirsync挂载选项),则立即提交事务
    if (IS_DIRSYNC(dir))
        ext4_handle_sync(handle);

out_stop:
    if (handle)
        ext4_journal_stop(handle);         // 停止事务(可能触发提交)
out_retry:
    // 如果错误是ENOSPC(空间不足)且文件系统允许重试,则重新尝试
    if (err == -ENOSPC && ext4_should_retry_alloc(dir->i_sb, &retries))
        goto retry;
    return err;
}

中文注释重点

  • ext4_new_inode_start_handle():这是最关键的步骤,它会在磁盘上分配一个空闲inode,填充基本属性(uid、gid、时间、模式等),并同时启动一个日志事务,返回handle

  • ext4_init_new_dir():新目录必须包含...两个特殊目录项,该函数负责分配目录的第一个数据块并写入这两项。

  • ext4_add_entry():将dentry的名称和新inode的编号写入父目录的目录文件中。

  • 错误处理中的ext4_orphan_add():如果创建过程失败,将未完成的inode添加到孤儿列表,日志回放时会自动删除它,防止磁盘inode泄露。

  • d_instantiate_new():建立VFS层面dentry与inode的关联,之后lookup就能找到该目录。


6. 在父目录中添加目录项:ext4_add_entry

父目录本质上是一个特殊文件,内容由ext4_dir_entry_2结构组成。ext4_add_entry负责将新目录的条目插入到这个文件中。

c

复制代码
static int ext4_add_entry(handle_t *handle, struct dentry *dentry,
                          struct inode *inode)
{
    struct inode *dir = d_inode(dentry->d_parent);  // 父目录的inode
    struct buffer_head *bh = NULL;
    struct ext4_dir_entry_2 *de;
    struct super_block *sb;
    struct ext4_filename fname;
    int retval;
    int dx_fallback = 0;
    unsigned blocksize;
    ext4_lblk_t block, blocks;
    int csum_size = 0;

    // 如果启用了元数据校验和,目录块尾部会保留校验和空间
    if (ext4_has_metadata_csum(inode->i_sb))
        csum_size = sizeof(struct ext4_dir_entry_tail);

    sb = dir->i_sb;
    blocksize = sb->s_blocksize;   // 通常为4096字节

    // 如果文件名被加密但未提供密钥,无法操作
    if (fscrypt_is_nokey_name(dentry))
        return -ENOKEY;

#if IS_ENABLED(CONFIG_UNICODE)
    // 如果文件系统支持Unicode且目录启用了大小写折叠,验证文件名是否为有效UTF-8
    if (sb_has_strict_encoding(sb) && IS_CASEFOLDED(dir) &&
        utf8_validate(sb->s_encoding, &dentry->d_name))
        return -EINVAL;
#endif

    // 根据目录的加密/折叠设置,预处理文件名(如转换为规范形式)
    retval = ext4_fname_setup_filename(dir, &dentry->d_name, 0, &fname);
    if (retval)
        return retval;

    // 首先尝试内联目录(目录数据存储在inode内,适用于小目录)
    if (ext4_has_inline_data(dir)) {
        retval = ext4_try_add_inline_entry(handle, &fname, dir, inode);
        if (retval < 0)
            goto out;
        if (retval == 1) {   // 成功添加到内联数据
            retval = 0;
            goto out;
        }
        // retval == 0 表示内联区已满,需要转换为普通块目录
    }

    // 如果目录启用了索引(htree/dx),尝试通过索引添加
    if (is_dx(dir)) {
        retval = ext4_dx_add_entry(handle, &fname, dir, inode);
        if (!retval || (retval != ERR_BAD_DX_DIR))
            goto out;
        // 如果索引目录损坏,回退到线性模式
        if (ext4_has_metadata_csum(sb)) {
            EXT4_ERROR_INODE(dir, "Directory has corrupted htree index.");
            retval = -EFSCORRUPTED;
            goto out;
        }
        ext4_clear_inode_flag(dir, EXT4_INODE_INDEX);  // 清除索引标志
        dx_fallback++;
        retval = ext4_mark_inode_dirty(handle, dir);
        if (unlikely(retval))
            goto out;
    }

    // 线性扫描现有目录块(非索引模式)
    blocks = dir->i_size >> sb->s_blocksize_bits;  // 目录文件包含的块数
    for (block = 0; block < blocks; block++) {
        bh = ext4_read_dirblock(dir, block, DIRENT);
        if (bh == NULL) {
            // 如果块不存在(文件空洞),创建新块并跳转到添加新块流程
            bh = ext4_bread(handle, dir, block,
                            EXT4_GET_BLOCKS_CREATE);
            goto add_to_new_block;
        }
        if (IS_ERR(bh)) {
            retval = PTR_ERR(bh);
            bh = NULL;
            goto out;
        }
        // 尝试将目录项添加到当前缓冲区
        retval = add_dirent_to_buf(handle, &fname, dir, inode, NULL, bh);
        if (retval != -ENOSPC)   // 不是空间不足的错误就退出
            goto out;

        // 如果目录只有一个块且尚未使用索引,但文件系统支持索引,则转换为索引目录
        if (blocks == 1 && !dx_fallback &&
            ext4_has_feature_dir_index(sb)) {
            retval = make_indexed_dir(handle, &fname, dir,
                                      inode, bh);
            bh = NULL;  // make_indexed_dir 已经释放了bh
            goto out;
        }
        brelse(bh);   // 释放当前块,继续下一个块
    }

    // 所有现有块都没有空间,在目录文件末尾追加一个新块
    bh = ext4_append(handle, dir, &block);
add_to_new_block:
    if (IS_ERR(bh)) {
        retval = PTR_ERR(bh);
        bh = NULL;
        goto out;
    }
    de = (struct ext4_dir_entry_2 *) bh->b_data;
    de->inode = 0;   // 初始化为空目录项
    // 设置rec_len为整个块减去校验和尾巴的长度
    de->rec_len = ext4_rec_len_to_disk(blocksize - csum_size, blocksize);

    if (csum_size)
        ext4_initialize_dirent_tail(bh, blocksize);  // 初始化尾部校验和结构

    // 最后调用通用的添加函数,将条目写入新块
    retval = add_dirent_to_buf(handle, &fname, dir, inode, de, bh);
out:
    ext4_fname_free_filename(&fname);
    brelse(bh);
    if (retval == 0)
        ext4_set_inode_state(inode, EXT4_STATE_NEWENTRY);
    return retval;
}

中文注释

  • ext4_filename:封装了经过加密/折叠处理的文件名,以及其哈希值等。

  • ext4_try_add_inline_entry():尝试将目录项添加到inode的内联数据区(如果目录很小)。

  • is_dx(dir):判断目录是否使用了索引树(提升大目录查找性能)。

  • make_indexed_dir():将单块线性目录转换为索引目录(htree),这是ext4的优化特性。

  • ext4_append():在目录文件末尾新增一个块,并返回映射到该块的buffer_head。


7. 实际写入目录块:add_dirent_to_buf

这个函数负责在已经确定有空闲空间的缓冲区中,实际修改目录块数据,插入新的目录项。

c

复制代码
static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
                             struct inode *dir, struct inode *inode,
                             struct ext4_dir_entry_2 *de,
                             struct buffer_head *bh)
{
    unsigned int blocksize = dir->i_sb->s_blocksize;
    int csum_size = 0;
    int err, err2;

    if (ext4_has_metadata_csum(inode->i_sb))
        csum_size = sizeof(struct ext4_dir_entry_tail);

    // 如果调用方没有预先指定de(空闲位置),就在当前缓冲区内查找合适位置
    if (!de) {
        err = ext4_find_dest_de(dir, inode, bh, bh->b_data,
                                blocksize - csum_size, fname, &de);
        if (err)
            return err;
    }

    // 告诉日志系统我们要修改这个缓冲区,需要将其加入事务
    BUFFER_TRACE(bh, "get_write_access");
    err = ext4_journal_get_write_access(handle, dir->i_sb, bh,
                                        EXT4_JTR_NONE);
    if (err) {
        ext4_std_error(dir->i_sb, err);
        return err;
    }

    /* 现在缓冲区已经安全地加入事务,可以修改内容 */
    // 插入目录项:调整相邻项的rec_len,在de处填充新的条目
    ext4_insert_dentry(dir, inode, de, blocksize, fname);

    // 更新父目录的时间戳(修改时间和变更时间)
    inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir));
    // 如果目录需要启用索引标志(比如大小增长到阈值),更新之
    ext4_update_dx_flag(dir);
    inode_inc_iversion(dir);          // 增加inode版本号
    err2 = ext4_mark_inode_dirty(handle, dir);  // 标记父目录的inode为脏

    // 将修改后的目录块标记为脏,并记录到事务中
    BUFFER_TRACE(bh, "call ext4_handle_dirty_metadata");
    err = ext4_handle_dirty_dirblock(handle, dir, bh);
    if (err)
        ext4_std_error(dir->i_sb, err);
    
    // 返回错误:如果err或err2有错,优先返回err(目录块错误)
    return err ? err : err2;
}

中文注释

  • ext4_find_dest_de():扫描一个目录块(从bh->b_data开始,长度blocksize - csum_size),寻找一块能够容纳新目录项的空闲区域。它会考虑现有目录项的空隙,以及尾部可能存在的未使用空间。

  • ext4_journal_get_write_access():对于ext3/4日志,修改缓冲区前必须调用此函数,它会将缓冲区内容复制到日志的副本中(写前拷贝),以便事务回滚时恢复。

  • ext4_insert_dentry():核心内存操作:根据fname的长度(可能经过加密后变长)计算需要占用的空间,调整前一个或后一个目录项的rec_len,然后在de位置写入新目录项的inode号、文件名长度、文件类型、校验等。这个函数还会更新目录块的校验和(如果启用)。

  • ext4_handle_dirty_dirblock():将脏目录块加入日志事务的待写列表,等待事务提交时写入磁盘。


8. 错误处理与重试机制

整个mkdir调用链中,内核设计了多层错误处理和重试逻辑,确保高可靠性。

8.1 VFS层的-ESTALE重试

do_mkdirat中:

c

复制代码
if (retry_estale(error, lookup_flags)) {
    lookup_flags |= LOOKUP_REVAL;
    goto retry;
}

-ESTALE通常发生在网络文件系统(NFS)中,表示客户端缓存的目录项已经过期。此时内核会重新验证路径,再次尝试。

8.2 ext4层的-ENOSPC重试

ext4_mkdir的末尾:

c

复制代码
if (err == -ENOSPC && ext4_should_retry_alloc(dir->i_sb, &retries))
    goto retry;

ext4_should_retry_alloc()会检查是否有其他进程正在释放磁盘空间,或者是否正在进行在线碎片整理。如果有可能获得空间,就返回true,重新尝试整个创建过程。retries参数限制了最大重试次数(通常内部实现为最多尝试几次后放弃)。

8.3 孤儿inode清理

如果在创建过程中ext4_add_entry失败,代码会跳转到out_clear_inode

c

复制代码
clear_nlink(inode);
ext4_orphan_add(handle, inode);
unlock_new_inode(inode);
...
iput(inode);
  • clear_nlink()将inode的链接数设为零,这样iput()时就会触发删除。

  • ext4_orphan_add()将该inode添加到"孤儿列表",该列表记录在日志中。如果系统在此时崩溃,日志回放时会看到孤儿inode并自动将其删除,避免inode泄漏。

8.4 日志回滚

如果事务中的任何一步失败,ext4_journal_stop()会终止当前事务。由于我们之前调用了ext4_journal_get_write_access(),日志系统拥有所有修改过的数据块的旧副本,因此事务回滚时能够恢复所有元数据到操作前的状态。这保证了mkdir的原子性。


9. 完整流程总结

下图(文字描述)展示了mkdir的完整调用链:

text

复制代码
用户态: mkdir("test", 0755)
   |
   v
系统调用: sys_mkdir -> do_mkdirat(AT_FDCWD, name, mode)
   |
   v
VFS路径解析: filename_create() 获取父目录path和新目录dentry
   |
   v
安全检查: security_path_mkdir()
   |
   v
VFS通用操作: vfs_mkdir()
   | - may_create() 权限检查
   | - dir->i_op->mkdir 回调
   v
ext4具体操作: ext4_mkdir()
   | - 计算日志credits
   | - ext4_new_inode_start_handle() 分配inode并启动事务
   | - ext4_init_new_dir() 初始化新目录(.和..)
   | - ext4_add_entry() 在父目录中添加条目
   |    | - 处理内联/索引/线性模式
   |    | - add_dirent_to_buf() 实际写入目录块
   | - d_instantiate_new() 关联dentry和inode
   | - ext4_journal_stop() 结束事务(可能提交)
   v
返回用户态

整个过程中,日志机制保证了元数据更新的原子性,而重试机制提高了在资源竞争下的成功率。最终,一个新目录在磁盘上被创建,并在VFS的dcache中留下缓存,供后续访问使用。


10. 延伸讨论与思考

通过阅读内核源码,我们可以获得一些深入的洞察:

  1. 目录是一种特殊的文件 :在ext4中,目录文件的内容是由ext4_dir_entry_2组成的记录列表。ls命令就是通过读取目录文件的内容来获取子项的名称和inode号,然后再根据inode去获取更多属性(如类型、大小)。

  2. 性能优化的双刃剑:ext4引入了目录索引(htree),使得大目录的查找复杂度从O(n)降为O(log n)。但索引本身也需要维护,增加了创建条目时的开销。为此,内核只在目录大小超过一定阈值时才自动建立索引。

  3. 日志的写前拷贝ext4_journal_get_write_access()并不立即复制整个块,而是采用"预留空间+延迟复制"的策略,只有在真正修改前才会进行拷贝,这减少了日志带宽消耗。

  4. umask的时机 :用户传递的mode会在mode_strip_umaskvfs_prepare_mode中被两次调整,最终影响到磁盘上inode的i_mode。为什么不在用户态就调整好?因为文件系统可能位于挂载了idmap的命名空间中,需要根据映射后的用户ID和组ID重新计算umask。

  5. dentry与inode的生命周期d_instantiate_new()将dentry与inode绑定,这样后续的路径查找就能通过dcache直接命中。如果创建失败,这个dentry会被标记为负(negative),不会与inode关联。

  6. 安全性 :Linux安全模块(LSM)在security_path_mkdirsecurity_inode_mkdir两个钩子处拦截,分别提供路径和inode粒度的访问控制。这种双重检查允许灵活的策略(例如,某些LSM可能需要在知道最终路径名后才能决策)。

11. 结语

本文基于Linux内核6.8.12源码,逐行分析了mkdir系统调用的完整实现路径。我们从用户态入口出发,穿越VFS抽象层,深入到ext4的磁盘数据结构修改,最后又回到错误处理和事务提交。通过添加详细的中文注释,希望能帮助读者更容易地理解内核代码的意图。

学习内核源码需要耐心和细致,但每一次深入都会带来莫大的收获。无论是调试文件系统问题,还是开发新的内核特性,掌握这些基础实现都将是你最坚实的后盾。

#源码

cpp 复制代码
83	common	mkdir			sys_mkdir

SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)
{
	return do_mkdirat(AT_FDCWD, getname(pathname), mode);
}

int do_mkdirat(int dfd, struct filename *name, umode_t mode)
{
	struct dentry *dentry;
	struct path path;
	int error;
	unsigned int lookup_flags = LOOKUP_DIRECTORY;

retry:
	dentry = filename_create(dfd, name, &path, lookup_flags);
	error = PTR_ERR(dentry);
	if (IS_ERR(dentry))
		goto out_putname;

	error = security_path_mkdir(&path, dentry,
			mode_strip_umask(path.dentry->d_inode, mode));
	if (!error) {
		error = vfs_mkdir(mnt_idmap(path.mnt), path.dentry->d_inode,
				  dentry, mode);
	}
	done_path_create(&path, dentry);
	if (retry_estale(error, lookup_flags)) {
		lookup_flags |= LOOKUP_REVAL;
		goto retry;
	}
out_putname:
	putname(name);
	return error;
}


/**
 * vfs_mkdir - create directory
 * @idmap:	idmap of the mount the inode was found from
 * @dir:	inode of @dentry
 * @dentry:	pointer to dentry of the base directory
 * @mode:	mode of the new directory
 *
 * Create a directory.
 *
 * If the inode has been found through an idmapped mount the idmap of
 * the vfsmount must be passed through @idmap. This function will then take
 * care to map the inode according to @idmap before checking permissions.
 * On non-idmapped mounts or if permission checking is to be performed on the
 * raw inode simply pass @nop_mnt_idmap.
 */
int vfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
	      struct dentry *dentry, umode_t mode)
{
	int error;
	unsigned max_links = dir->i_sb->s_max_links;

	error = may_create(idmap, dir, dentry);
	if (error)
		return error;

	if (!dir->i_op->mkdir)
		return -EPERM;

	mode = vfs_prepare_mode(idmap, dir, mode, S_IRWXUGO | S_ISVTX, 0);
	error = security_inode_mkdir(dir, dentry, mode);
	if (error)
		return error;

	if (max_links && dir->i_nlink >= max_links)
		return -EMLINK;

	error = dir->i_op->mkdir(idmap, dir, dentry, mode);
	if (!error)
		fsnotify_mkdir(dir, dentry);
	return error;
}
EXPORT_SYMBOL(vfs_mkdir);

/*
 * directories can handle most operations...
 */
const struct inode_operations ext4_dir_inode_operations = {
	.create		= ext4_create,
	.lookup		= ext4_lookup,
	.link		= ext4_link,
	.unlink		= ext4_unlink,
	.symlink	= ext4_symlink,
	.mkdir		= ext4_mkdir,
	.rmdir		= ext4_rmdir,
	.mknod		= ext4_mknod,
	.tmpfile	= ext4_tmpfile,
	.rename		= ext4_rename2,
	.setattr	= ext4_setattr,
	.getattr	= ext4_getattr,
	.listxattr	= ext4_listxattr,
	.get_inode_acl	= ext4_get_acl,
	.set_acl	= ext4_set_acl,
	.fiemap         = ext4_fiemap,
	.fileattr_get	= ext4_fileattr_get,
	.fileattr_set	= ext4_fileattr_set,
};


static int ext4_mkdir(struct mnt_idmap *idmap, struct inode *dir,
		      struct dentry *dentry, umode_t mode)
{
	handle_t *handle;
	struct inode *inode;
	int err, err2 = 0, credits, retries = 0;

	if (EXT4_DIR_LINK_MAX(dir))
		return -EMLINK;

	err = dquot_initialize(dir);
	if (err)
		return err;

	credits = (EXT4_DATA_TRANS_BLOCKS(dir->i_sb) +
		   EXT4_INDEX_EXTRA_TRANS_BLOCKS + 3);
retry:
	inode = ext4_new_inode_start_handle(idmap, dir, S_IFDIR | mode,
					    &dentry->d_name,
					    0, NULL, EXT4_HT_DIR, credits);
	handle = ext4_journal_current_handle();
	err = PTR_ERR(inode);
	if (IS_ERR(inode))
		goto out_stop;

	inode->i_op = &ext4_dir_inode_operations;
	inode->i_fop = &ext4_dir_operations;
	err = ext4_init_new_dir(handle, dir, inode);
	if (err)
		goto out_clear_inode;
	err = ext4_mark_inode_dirty(handle, inode);
	if (!err)
		err = ext4_add_entry(handle, dentry, inode);
	if (err) {
out_clear_inode:
		clear_nlink(inode);
		ext4_orphan_add(handle, inode);
		unlock_new_inode(inode);
		err2 = ext4_mark_inode_dirty(handle, inode);
		if (unlikely(err2))
			err = err2;
		ext4_journal_stop(handle);
		iput(inode);
		goto out_retry;
	}
	ext4_inc_count(dir);

	ext4_update_dx_flag(dir);
	err = ext4_mark_inode_dirty(handle, dir);
	if (err)
		goto out_clear_inode;
	d_instantiate_new(dentry, inode);
	ext4_fc_track_create(handle, dentry);
	if (IS_DIRSYNC(dir))
		ext4_handle_sync(handle);

out_stop:
	if (handle)
		ext4_journal_stop(handle);
out_retry:
	if (err == -ENOSPC && ext4_should_retry_alloc(dir->i_sb, &retries))
		goto retry;
	return err;
}

/*
 *	ext4_add_entry()
 *
 * adds a file entry to the specified directory, using the same
 * semantics as ext4_find_entry(). It returns NULL if it failed.
 *
 * NOTE!! The inode part of 'de' is left at 0 - which means you
 * may not sleep between calling this and putting something into
 * the entry, as someone else might have used it while you slept.
 */
static int ext4_add_entry(handle_t *handle, struct dentry *dentry,
			  struct inode *inode)
{
	struct inode *dir = d_inode(dentry->d_parent);
	struct buffer_head *bh = NULL;
	struct ext4_dir_entry_2 *de;
	struct super_block *sb;
	struct ext4_filename fname;
	int	retval;
	int	dx_fallback=0;
	unsigned blocksize;
	ext4_lblk_t block, blocks;
	int	csum_size = 0;

	if (ext4_has_metadata_csum(inode->i_sb))
		csum_size = sizeof(struct ext4_dir_entry_tail);

	sb = dir->i_sb;
	blocksize = sb->s_blocksize;

	if (fscrypt_is_nokey_name(dentry))
		return -ENOKEY;

#if IS_ENABLED(CONFIG_UNICODE)
	if (sb_has_strict_encoding(sb) && IS_CASEFOLDED(dir) &&
	    utf8_validate(sb->s_encoding, &dentry->d_name))
		return -EINVAL;
#endif

	retval = ext4_fname_setup_filename(dir, &dentry->d_name, 0, &fname);
	if (retval)
		return retval;

	if (ext4_has_inline_data(dir)) {
		retval = ext4_try_add_inline_entry(handle, &fname, dir, inode);
		if (retval < 0)
			goto out;
		if (retval == 1) {
			retval = 0;
			goto out;
		}
	}

	if (is_dx(dir)) {
		retval = ext4_dx_add_entry(handle, &fname, dir, inode);
		if (!retval || (retval != ERR_BAD_DX_DIR))
			goto out;
		/* Can we just ignore htree data? */
		if (ext4_has_metadata_csum(sb)) {
			EXT4_ERROR_INODE(dir,
				"Directory has corrupted htree index.");
			retval = -EFSCORRUPTED;
			goto out;
		}
		ext4_clear_inode_flag(dir, EXT4_INODE_INDEX);
		dx_fallback++;
		retval = ext4_mark_inode_dirty(handle, dir);
		if (unlikely(retval))
			goto out;
	}
	blocks = dir->i_size >> sb->s_blocksize_bits;
	for (block = 0; block < blocks; block++) {
		bh = ext4_read_dirblock(dir, block, DIRENT);
		if (bh == NULL) {
			bh = ext4_bread(handle, dir, block,
					EXT4_GET_BLOCKS_CREATE);
			goto add_to_new_block;
		}
		if (IS_ERR(bh)) {
			retval = PTR_ERR(bh);
			bh = NULL;
			goto out;
		}
		retval = add_dirent_to_buf(handle, &fname, dir, inode,
					   NULL, bh);
		if (retval != -ENOSPC)
			goto out;

		if (blocks == 1 && !dx_fallback &&
		    ext4_has_feature_dir_index(sb)) {
			retval = make_indexed_dir(handle, &fname, dir,
						  inode, bh);
			bh = NULL; /* make_indexed_dir releases bh */
			goto out;
		}
		brelse(bh);
	}
	bh = ext4_append(handle, dir, &block);
add_to_new_block:
	if (IS_ERR(bh)) {
		retval = PTR_ERR(bh);
		bh = NULL;
		goto out;
	}
	de = (struct ext4_dir_entry_2 *) bh->b_data;
	de->inode = 0;
	de->rec_len = ext4_rec_len_to_disk(blocksize - csum_size, blocksize);

	if (csum_size)
		ext4_initialize_dirent_tail(bh, blocksize);

	retval = add_dirent_to_buf(handle, &fname, dir, inode, de, bh);
out:
	ext4_fname_free_filename(&fname);
	brelse(bh);
	if (retval == 0)
		ext4_set_inode_state(inode, EXT4_STATE_NEWENTRY);
	return retval;
}

/*
 * Add a new entry into a directory (leaf) block.  If de is non-NULL,
 * it points to a directory entry which is guaranteed to be large
 * enough for new directory entry.  If de is NULL, then
 * add_dirent_to_buf will attempt search the directory block for
 * space.  It will return -ENOSPC if no space is available, and -EIO
 * and -EEXIST if directory entry already exists.
 */
static int add_dirent_to_buf(handle_t *handle, struct ext4_filename *fname,
			     struct inode *dir,
			     struct inode *inode, struct ext4_dir_entry_2 *de,
			     struct buffer_head *bh)
{
	unsigned int	blocksize = dir->i_sb->s_blocksize;
	int		csum_size = 0;
	int		err, err2;

	if (ext4_has_metadata_csum(inode->i_sb))
		csum_size = sizeof(struct ext4_dir_entry_tail);

	if (!de) {
		err = ext4_find_dest_de(dir, inode, bh, bh->b_data,
					blocksize - csum_size, fname, &de);
		if (err)
			return err;
	}
	BUFFER_TRACE(bh, "get_write_access");
	err = ext4_journal_get_write_access(handle, dir->i_sb, bh,
					    EXT4_JTR_NONE);
	if (err) {
		ext4_std_error(dir->i_sb, err);
		return err;
	}

	/* By now the buffer is marked for journaling */
	ext4_insert_dentry(dir, inode, de, blocksize, fname);

	/*
	 * XXX shouldn't update any times until successful
	 * completion of syscall, but too many callers depend
	 * on this.
	 *
	 * XXX similarly, too many callers depend on
	 * ext4_new_inode() setting the times, but error
	 * recovery deletes the inode, so the worst that can
	 * happen is that the times are slightly out of date
	 * and/or different from the directory change time.
	 */
	inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir));
	ext4_update_dx_flag(dir);
	inode_inc_iversion(dir);
	err2 = ext4_mark_inode_dirty(handle, dir);
	BUFFER_TRACE(bh, "call ext4_handle_dirty_metadata");
	err = ext4_handle_dirty_dirblock(handle, dir, bh);
	if (err)
		ext4_std_error(dir->i_sb, err);
	return err ? err : err2;
}
相关推荐
我是一颗柠檬1 小时前
【Redis】字符串与哈希Day3(2026年)
数据库·redis·后端·database
念何架构之路1 小时前
接入层Nginx
运维·nginx
sakoba1 小时前
MySQL常见问题学习
数据库·学习·mysql
小二·1 小时前
向量数据库深度对比:PGVector vs Qdrant vs Milvus vs Chroma(附性能测试数据)
数据库·wpf·milvus
wanhengidc1 小时前
云手机 跨设备无缝衔接
运维·服务器·人工智能·智能手机·云计算
sxlishaobin1 小时前
SSH远程免密登录的两种方式
运维·ssh
sleven fung1 小时前
Milvus 向量数据库
开发语言·数据库·python·langchain·milvus
赵渝强老师2 小时前
【赵渝强老师】崖山数据库的数据字典
数据库·oracle
java_cj2 小时前
MySQL 8.0 新特性深度解析:降序索引、Doublewrite Buffer 与 redo log 无锁优化
数据库·mysql