1. 引言
在Linux操作系统中,mkdir命令用于创建目录,这是每个用户都习以为常的基础操作。然而,在这条简单的命令背后,内核中却隐藏着一套复杂而精密的执行流程:从用户态的系统调用入口,到虚拟文件系统(VFS)的抽象层,再到具体文件系统(如ext4)的底层实现,每一步都涉及权限校验、路径解析、磁盘数据结构更新、日志记录等多个环节。
本文将基于Linux内核6.8.12版本的源码,深入剖析mkdir系统调用的完整实现。我们将沿着代码的执行路径,从SYSCALL_DEFINE2(mkdir)出发,依次经过do_mkdirat、vfs_mkdir,最终深入到ext4文件系统的ext4_mkdir、ext4_add_entry等函数。通过逐行分析关键代码,揭示内核如何安全、高效地在磁盘上创建一个新的目录。
文章会兼顾整体逻辑的清晰性与具体实现的细节,帮助读者建立起从用户命令到硬件存储的完整知识链条。
2. 系统调用入口:sys_mkdir
在Linux中,系统调用是用户空间与内核空间交互的唯一接口。mkdir命令对应的系统调用号为83,其内核入口函数通过SYSCALL_DEFINE2宏定义:
c
arduino
SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)
{
return do_mkdirat(AT_FDCWD, getname(pathname), mode);
}
SYSCALL_DEFINE2是内核中用于定义系统调用的标准宏,数字2表示有两个参数。pathname:用户空间传递的路径名字符串指针(如/home/user/test)。mode:新建目录的权限模式(如0755),其类型umode_t本质上是unsigned int,但内核会对其做权限掩码处理。
这里有一个重要细节:getname(pathname)负责将用户空间的路径字符串安全地拷贝到内核空间,并封装成struct filename结构体。该结构体不仅包含路径字符串,还带有引用计数,用于管理内核路径名的生命周期。AT_FDCWD是一个特殊文件描述符值,表示"当前工作目录"(Current Working Directory)。这意味着如果pathname是相对路径,则相对于进程的当前目录进行解析。
因此,真正的核心实现在do_mkdirat函数中。
3. 核心处理函数:do_mkdirat
do_mkdirat是VFS层提供的通用目录创建函数,它接受一个目录文件描述符dfd、一个内核路径名name以及目录模式mode。让我们逐步解析其代码:
c
ini
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;
}
3.1 路径查找与最终分量创建:filename_create
最关键的一步是调用filename_create。该函数执行以下任务:
- 根据
dfd和name查找路径,找到新目录所在的父目录,并获取父目录的struct path(包含dentry和mount信息)。 - 同时,它会尝试创建 路径的最后一个分量(即新目录名对应的
dentry),但不会将其链接到父目录中。也就是说,它返回一个尚未与任何inode关联的dentry,并持有父目录的path。 - 参数
lookup_flags初始为LOOKUP_DIRECTORY,这表示要求路径的最后一个分量不能是已存在的文件(如果是已存在的目录则会返回错误,除非最后分量不存在)。
如果filename_create执行成功,path指向父目录的路径,dentry是新建目录的目录项(dentry)。如果失败,返回错误码(通过PTR_ERR转换)。
3.2 安全钩子:security_path_mkdir
Linux安全模块(如SELinux、AppArmor)会在此处介入。security_path_mkdir检查当前进程是否有权限在父目录下创建指定名称的目录。注意第三个参数:mode_strip_umask(path.dentry->d_inode, mode)。mode_strip_umask会根据父目录的umask过滤掉用户传递的mode中的某些权限位,得到最终要使用的权限。这是因为创建目录时,最终的权限 = mode & ~current->fs->umask,但还要考虑父目录的默认ACL等。
3.3 调用VFS创建函数:vfs_mkdir
如果安全钩子通过,则调用vfs_mkdir执行真正的目录创建。注意传递的参数:mnt_idmap(path.mnt)处理挂载点的id映射(用于用户命名空间),path.dentry->d_inode是父目录的inode,dentry是新目录的dentry,mode是已处理过的模式。
3.4 清理与重试机制
done_path_create(&path, dentry)会释放path和dentry的引用,并解锁父目录。如果vfs_mkdir返回-ESTALE(陈旧的文件句柄,常见于NFS),且lookup_flags尚未包含LOOKUP_REVAL,则设置该标志并跳转到retry重新执行路径查找。这种机制保证了在网络文件系统中遇到陈旧缓存时能够自动重新验证。
最后,putname(name)释放之前getname获得的内核路径名结构。
4. VFS层的通用创建函数:vfs_mkdir
vfs_mkdir是独立于具体文件系统的VFS标准接口。它执行所有文件系统通用的检查和操作,然后调用具体文件系统提供的mkdir回调。
c
ini
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;
}
4.1 基础权限检查:may_create
may_create函数检查当前进程是否可以在父目录dir中创建新条目。它会检查:
- 父目录的可写权限(
S_IWUSR)和执行权限(S_IXUSR)。 - 如果父目录属于"粘滞位"(Sticky Bit)目录(如
/tmp),则还需检查进程是否有权覆盖或删除其他用户的文件?但对于创建来说,粘滞位不阻止创建新文件/目录,而是阻止非所有者删除/重命名。实际上may_create主要检查写权限,粘滞位的处理在更上层。 - 此外,
may_create还会检查inode是否处于不可变或仅追加状态,以及是否有足够的磁盘配额等。
4.2 文件系统是否支持mkdir
if (!dir->i_op->mkdir):每个inode都有一组操作函数表inode_operations。对于目录inode,通常都实现了mkdir函数(如ext4目录的.mkdir = ext4_mkdir)。如果未实现(例如某些特殊文件系统是只读的),则返回-EPERM。
4.3 模式预处理:vfs_prepare_mode
vfs_prepare_mode根据父目录的idmap和原始mode,生成最终要传递给文件系统的mode。它会确保位S_IFDIR被清除(因为mkdir已经隐含目录类型),并应用当前进程的umask。最后一个参数0表示不做额外的强制位调整。S_IRWXUGO | S_ISVTX是允许的位掩码,即用户/组/其他的读写执行位加上粘滞位。
4.4 安全模块再次检查
security_inode_mkdir是另一个安全钩子,它基于inode和dentry进行更细粒度的检查,例如检查安全上下文是否允许在指定目录下创建特定名称的目录。
4.5 目录链接数限制
if (max_links && dir->i_nlink >= max_links):max_links通常是文件系统的最大链接数限制(例如ext4的EXT4_LINK_MAX为65000)。父目录每创建一个子目录,其硬链接数会增加(因为在Unix中,每个目录都包含..指向父目录,所以父目录的链接数等于其子目录数+2)。当达到上限时,返回-EMLINK。
4.6 调用具体文件系统的mkdir
所有检查通过后,调用dir->i_op->mkdir,也就是ext4的ext4_mkdir。如果成功,则通过fsnotify_mkdir发送文件系统事件通知(供inotify、fanotify等使用)。
5. ext4文件系统中的mkdir实现
现在进入具体文件系统层。ext4是Linux上最常用的日志文件系统之一,其目录inode操作表定义如下:
c
ini
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,
};
可以看到,.mkdir成员被赋值为ext4_mkdir。
5.1 ext4_mkdir主流程
c
ini
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;
}
5.1.1 初步检查与配额初始化
EXT4_DIR_LINK_MAX(dir):检查父目录的链接数是否已达到ext4的上限(EXT4_LINK_MAX,通常是65000)。如果超过,返回-EMLINK。dquot_initialize(dir):初始化磁盘配额子系统,确保后续写操作不会超过配额限制。
5.1.2 计算事务所需的预留块数
ext4是日志文件系统,任何修改文件系统元数据的操作都需要在一个事务(journal transaction)中进行。credits表示这个操作需要预留多少元数据块(即可能修改的块数)。计算公式:
EXT4_DATA_TRANS_BLOCKS(dir->i_sb):基础事务块数,包括inode块、位图块、目录块等。EXT4_INDEX_EXTRA_TRANS_BLOCKS:如果目录启用了索引(htree),还需要额外的块数用于索引结构。+3:额外的安全边界。
5.1.3 分配新的inode并启动事务
ext4_new_inode_start_handle是一个关键函数,它完成以下工作:
- 分配一个新的inode(从inode表中找到一个空闲位置)。
- 设置inode的基本属性:所有者、时间戳、初始链接数为1(目录默认nlink为1,之后会添加
.和..后变为2)。 - 如果启用了日志,该函数会同时启动一个日志事务(调用
ext4_journal_start_sb),并根据credits预留空间。返回值是一个新分配的inode指针,并且当前事务的handle已经保存在当前任务的journal info中,可通过ext4_journal_current_handle()获取。 - 参数中
S_IFDIR | mode指定inode类型为目录,并合并用户指定的权限模式。
5.1.4 初始化新目录的内容
获取新inode后,设置其操作函数表:i_op指向ext4_dir_inode_operations,i_fop指向ext4_dir_operations(定义了目录的读取、迭代等函数)。
ext4_init_new_dir(handle, dir, inode)负责在新目录的磁盘块中创建标准的两项:.(当前目录)和..(父目录)。这个函数会:
- 分配第一个数据块(通常为4KB)给新目录。
- 在该块的起始位置写入
ext4_dir_entry_2结构,分别对应.和..,并设置正确的inode号。 - 如果启用了元数据校验和(metadata_csum),还会在块尾部添加tail校验信息。
5.1.5 将目录项添加到父目录
ext4_add_entry(handle, dentry, inode)是核心函数,它将新目录的名称和inode编号作为一条记录插入到父目录的目录文件内容中。我们将在后文单独详细分析。
5.1.6 错误处理与重试
如果ext4_add_entry失败(例如返回-ENOSPC表示父目录已满),则跳转到out_clear_inode:
clear_nlink(inode)将inode的链接数清零。ext4_orphan_add(handle, inode)将该inode添加到"孤儿列表"中(日志会跟踪这些未完全创建的inode,以便在系统崩溃后删除它们)。unlock_new_inode(inode)解锁(因为ext4_new_inode_start_handle返回的是带锁的新inode)。- 标记inode脏,停止当前事务,并
iput(inode)(释放引用,如果nlink为0则会触发删除)。 - 最后跳转到
out_retry,若错误为-ENOSPC且文件系统允许重试分配(ext4_should_retry_alloc),则重新尝试整个创建过程。
5.1.7 成功后的操作
ext4_inc_count(dir):父目录的链接数加1(因为新目录的..指向父目录)。ext4_update_dx_flag(dir):如果父目录的目录项数量增加后达到了启用索引(htree)的阈值,则更新其inode标志。ext4_mark_inode_dirty(handle, dir)将父目录的inode标记为脏,以便事务提交时写入磁盘。d_instantiate_new(dentry, inode):将新创建的dentry与inode关联起来,并标记dentry为已使用。这会使得该dentry进入dcache(目录项缓存)。ext4_fc_track_create(handle, dentry):如果启用了fast commit特性,记录此次创建操作以便快速恢复。- 如果父目录要求同步写入(
IS_DIRSYNC(dir)),则调用ext4_handle_sync(handle)强制事务立即提交。
5.1.8 停止事务
ext4_journal_stop(handle)减少事务的引用计数,如果引用计数降到0且事务有足够的空间,则可能触发事务的提交。所有元数据修改(inode位图、块位图、目录项块、inode表等)此时尚未真正写入磁盘,但会在日志提交过程中一并写入。
5.2 父目录中插入目录项:ext4_add_entry
ext4_add_entry负责将目录项(文件名+inode号)写入父目录的磁盘数据中。由于ext4支持多种特性(内联目录、目录索引、校验和等),这个函数相当复杂。我们重点分析其主干逻辑:
c
ini
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;
/* 如果索引目录损坏,回退到线性目录模式 */
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;
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;
}
5.2.1 预处理:加密、Unicode、文件名准备
- 如果文件名是加密的(fscrypt)且未提供密钥,则
fscrypt_is_nokey_name返回真,函数返回-ENOKEY。 - 如果文件系统启用了Unicode严格编码,且父目录设置了大小写折叠(casefolded),则验证文件名是否合法UTF-8字符串。
ext4_fname_setup_filename:根据目录的加密/大小写折叠设置,对文件名进行预处理(例如,若需要大小写不敏感比较,则将文件名转换为规范形式)。结果存储在fname中,后续插入时会使用。
5.2.2 内联目录(inline data)优先
ext4支持小目录存储在inode自身的数据块中(inline_data特性),以避免为极小的目录分配磁盘块。ext4_try_add_inline_entry尝试将新目录项插入到内联数据区。如果成功(返回1),则直接完成;如果内联区已满(返回0),则继续下面的普通逻辑。
5.2.3 目录索引(dx/h-tree)处理
is_dx(dir)判断该目录是否启用了索引(即使用哈希树结构以加速大目录的查找)。如果启用了,调用ext4_dx_add_entry将新项插入到索引树中。如果成功,直接返回;如果返回ERR_BAD_DX_DIR(表示索引结构损坏),则清除目录的索引标志,回退到传统线性目录模式,并继续执行。
5.2.4 线性扫描现有目录块
如果目录没有启用索引,或者索引回退后,函数会遍历目录的所有现有块(从0到blocks-1,其中blocks = dir->i_size / blocksize)。对每个块:
- 调用
ext4_read_dirblock读取该块(如果块号大于实际存储的块数,则返回NULL,此时会通过ext4_bread创建新块并跳转到add_to_new_block)。 - 调用
add_dirent_to_buf尝试将目录项添加到该块中。如果返回-ENOSPC表示该块已满,则继续下一个块;否则如果是其他错误或成功,则退出循环。 - 特殊优化:如果目录只有一个块且未使用索引,但文件系统支持索引特性,则调用
make_indexed_dir将该单块目录转换为索引目录,然后重新尝试插入。
如果所有现有块都没有空闲空间,则调用ext4_append在目录文件末尾追加一个新的块,并初始化该块的首个ext4_dir_entry_2(设置de->inode=0,rec_len为整个块大小减去校验和尾巴)。然后再次调用add_dirent_to_buf将条目写入这个新块。
5.2.5 实际添加逻辑:add_dirent_to_buf
add_dirent_to_buf是真正将目录项数据写入缓冲区的函数。我们重点分析其实现:
c
scss
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;
}
/* 此时缓冲区已加入事务,可以安全修改 */
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);
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;
}
- 如果调用方没有提供具体的
de指针(即未预先定位空闲位置),则调用ext4_find_dest_de在当前buffer中查找一个能够容纳新目录项的空闲区域。该函数会考虑目录项对齐、大小计算以及是否存在ext4_dir_entry_tail。 ext4_journal_get_write_access告诉日志子系统,我们要修改这个缓冲区,需要将其元数据复制到日志中(或进行写前拷贝),以便事务回滚时恢复。ext4_insert_dentry执行实际的插入操作:它根据fname提供的文件名(可能已经加密或折叠)和长度,计算出需要的rec_len,然后在de指向的位置构建一个新的ext4_dir_entry_2,设置inode为传入的新inode编号,并调整相邻目录项的rec_len以分割空闲区域。- 更新父目录的时间戳(修改时间和变更时间),增加版本号,标记父目录的inode为脏。
ext4_handle_dirty_dirblock将修改后的目录块标记为脏,并记录在事务中,等待提交。
6. 错误恢复与重试机制
在整个mkdir路径中,错误处理随处可见。最典型的场景是磁盘空间不足或日志空间不足。ext4实现了重试逻辑:
- 在
do_mkdirat中,若遇到-ESTALE则重试并重新验证路径。 - 在
ext4_mkdir中,若分配inode或添加目录项时返回-ENOSPC,且文件系统支持重试(ext4_should_retry_alloc返回真),则会跳转到retry重新执行整个操作。ext4_should_retry_alloc的实现会检查是否有其他进程正在释放磁盘块,或者是否正在进行垃圾回收,从而提示可以重试。
需要注意的是,重试仅发生在同一系统调用上下文中,不会无限循环(通常重试次数由retries参数限制,ext4_mkdir中通过retries变量递增并传递给ext4_should_retry_alloc)。
7. 整体流程回顾与总结
通过以上分析,我们可以清晰地勾勒出mkdir命令在内核中的完整执行路径:
- 用户态 :调用
mkdir()库函数,触发系统调用sys_mkdir。 - 系统调用入口 :
sys_mkdir拷贝路径名,调用do_mkdirat,传入AT_FDCWD表示当前目录。 - VFS路径解析与创建 :
do_mkdirat调用filename_create,在父目录下找到或创建新目录的dentry。 - 安全与权限检查 :
security_path_mkdir调用LSM钩子,vfs_mkdir进行通用的inode权限、链接数限制检查。 - 具体文件系统操作 :
vfs_mkdir回调ext4_dir_inode_operations.mkdir,即ext4_mkdir。 - ext4的事务与元数据分配 :
ext4_mkdir计算所需日志块数,调用ext4_new_inode_start_handle分配新inode并启动事务。 - 初始化新目录 :调用
ext4_init_new_dir在新目录的数据块中创建.和..项。 - 父目录中添加目录项 :
ext4_add_entry负责将新目录的条目插入父目录,可能涉及内联数据、索引目录、线性扫描等多种路径。 - 元数据更新与事务提交:更新父目录链接数、时间戳,标记脏inode和脏块,最终停止事务(延迟提交或同步提交)。
- 缓存与通知 :
d_instantiate_new将dentry与inode关联,fsnotify_mkdir触发文件系统事件,通知用户空间监听器。
整个过程中,日志机制确保了元数据操作的一致性,即使系统崩溃,回放日志也能恢复正确状态。重试机制增加了在资源紧张情况下的成功率,而VFS的抽象层使得添加新的文件系统变得简单。
8. 小结与思考
本文基于Linux 6.8.12内核源码,详细讲解了mkdir系统调用的实现。我们从顶层入口一直分析到底层ext4的磁盘数据结构修改,展示了内核如何将一条简单的命令转化为一系列严谨、高效的原子操作。理解这段代码不仅有助于掌握文件系统的工作原理,也能为调试性能问题、开发新的文件系统特性打下坚实基础。
值得一提的是,现代文件系统的复杂性:ext4中目录项的内联、索引树、校验和、加密、大小写折叠等功能使得原本简单的"创建目录"变得丰富多彩。内核开发者在这些特性之间保持了良好的兼容性和回退机制,体现了工程上的成熟考虑。
希望通过本文,读者能够对Linux内核中文件系统路径有更深刻的认识,并在实际工作中运用这些知识。 #源码
83
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;
}