注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

由sed in-place选项的原子性看文件系统一些结构  

2015-02-15 14:34:47|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、问题的引入
在使用sed工具时,注意到sed有一个in-place选项,关于该功能的说明展示为
       -i[SUFFIX], --in-place[=SUFFIX]

              edit files in place (makes backup if extension supplied)
这个选项的意思就是在没有提供后缀的情况下,现场替换原始文件,这里就有一个问题:会不会出现出现原始的文件被修改了一半,此时系统出现问题(极端情况下就是断电,这种情况是无解的),导致系统残留了一个半成品,相当于整个文件不可用的情况?
其实这个命令行本身给出了一点提示,就是使用了backup文件,方法简单而直观,就是先把原始文件的“处理结果”保存在一个文件,当所有处理完成之后,通过rename这个系统调用将新生成的处理之后的文件替换为原始文件。这种思想也就是保证原子性的一个重要实现手段:RCU(Read Copy Update),内核中使用的RCU锁是基于这个原理,而linux的btrfs文件系统中实现文件的事务性操作也是基于这个基本的实现思想。就是先在备份上修改,然后再使用修改之后的内容替换原始内容,这里通常假设“修改”是一个耗时的操作,而“更新”是一个轻量级的、可以互斥的原子性操作,从而将“原子性”的要求限制在一个非常小的时间窗口中。

由于sed的源代码并不多,而且man手册也提供了这个命令的一些线索,可以轻松找到sed源代码中关于这个文件操作的包含了“rename”的关键操作,函数代码拷贝一份,以作为备忘sed-4.0.6\sed\execute.c:
static void
closedown(input)
  struct input *input;
{
  input->read_fn = read_always_fail;
  if (!input->fp)
    return;
  if (input->fp != stdin) /* stdin can be reused on tty and tape devices */
    ck_fclose(input->fp);

      if (in_place_extension && output_file != NULL)
{
 if (strcmp(in_place_extension, "*") != 0)
   {
     char *backup_file_name = get_backup_file_name(input->in_file_name);
     rename (input->in_file_name, backup_file_name);
     free (backup_file_name);
   }

 ck_fclose (output_file);
 rename (input->out_file_name, input->in_file_name);
 free (input->out_file_name);
}

  input->fp = NULL;

  if (separate_files)
    rewind_read_files ();
}

二、操作系统对rename的操作及原子性保证
先看下man手册对于该系统调用的原子性说明:
       If  newpath  already  exists  it will be atomically replaced (subject to a few conditions; see ERRORS below), so that
       there is no point at which another process attempting to access newpath will find it missing.
sys_rename-->>sys_renameat-->>do_rename-->>lock_rename
mutex_lock(&p1->d_inode->i_sb->s_vfs_rename_mutex);
……
mutex_lock_nested(&p1->d_inode->i_mutex, I_MUTEX_PARENT);
mutex_lock_nested(&p2->d_inode->i_mutex, I_MUTEX_CHILD);
在该函数中,锁定该文件所在文件系统上的rename互斥锁,这也就意味着同一个文件系统上只能有一个rename操作在进行;同时锁定两个文件使用的inode结构,从而避免在rename的过程中两个文件被删除或者做其它的操作,如果此时有一个进程正在对文件执行写入操作(这个写入操作的意思是执行write系统调用一致没有返回到用户态,那怎么制造这种情况呢?现在想到一个直接的办法就是在进程中申请几十G的文件,让它core掉,在操作系统写core文件时,用户态执行rename操作试下,不过我没有验证这种方法)

之后传递给具体文件系统来实现文件的重命名工作,对于大家最为熟悉的ext2文件系统ext2_rename,它的大致流程是这样的:
static int ext2_rename (struct inode * old_dir, struct dentry * old_dentry,
struct inode * new_dir, struct dentry * new_dentry )
{
……
old_de = ext2_find_entry (old_dir, old_dentry, &old_page);
……
if (new_inode) {//目的文件已经存在情况下的rename
struct page *new_page;
struct ext2_dir_entry_2 *new_de;

err = -ENOTEMPTY;
if (dir_de && !ext2_empty_dir (new_inode))
goto out_dir;

err = -ENOENT;
new_de = ext2_find_entry (new_dir, new_dentry, &new_page);
if (!new_de)
goto out_dir;
inode_inc_link_count(old_inode);//增加源文件的inode引用计数
ext2_set_link(new_dir, new_de, new_page, old_inode);//建立新的"目录项"到源文件的链接,目录下一个name为新目的文件名的ext2_dir_entry_2结构中inode为源文件的inode。
new_inode->i_ctime = CURRENT_TIME_SEC;
if (dir_de)
drop_nlink(new_inode);
inode_dec_link_count(new_inode);//原始目的文件inode引用级数递减。
} else {
……
}
……
/*
* Like most other Unix systems, set the ctime for inodes on a
  * rename.
* inode_dec_link_count() will mark the inode dirty.
*/
old_inode->i_ctime = CURRENT_TIME_SEC;

ext2_delete_entry (old_de, old_page);//删除rename的源文件
inode_dec_link_count(old_inode);
……
}
在这个过程中,如果操作系统崩溃,按照写回的顺序,目的文件ext2_dir_entry_2中的inode已经,此时可能出现脏数据,就是目的文件的原始文件使用的inode无法被文件系统回收,但是不影响操作的原子性,因为原子性是保证这个最为关键的ext2_dir_entry_2结构中name和inode的对应。其实回过头来我们再看文件系统的这种结构,它本质上和后来出现的互联网时代流行的DNS的功能相同,也就是完成从便于操作和记忆的字符串形式转换为文件系统最为喜欢和易于处理的数值形式。
三、不同文件系统之间文件重命名的问题
在不同的文件系统之间重命名,从前面的源代码分析可以看到,它没有一个统一的文件系统超级块的s_vfs_rename_mutex结构用来锁定,而且ext2文件系统对于重命名也没有做这种跨越文件系统的保护处理。内核对于这种问题的处理也相当的简单有效,那就是禁止跨越不同文件系统之间的重命名。
static int do_rename(int olddfd, const char *oldname,
int newdfd, const char *newname)
……
error = do_path_lookup(olddfd, oldname, LOOKUP_PARENT, &oldnd);
if (error)
goto exit;

error = do_path_lookup(newdfd, newname, LOOKUP_PARENT, &newnd);
if (error)
goto exit1;

error = -EXDEV;
if (oldnd.mnt != newnd.mnt)
goto exit2;
四、内核中文件相关基本结构的互斥变量说明
1、struct file 
include\linux\fs.h:
struct file {
……
const struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
……
loff_t f_pos;
……
}
这里的f_count表示该文件被多少个句柄共享。从用户态来看,当通过open系统调用打开一个文件时,系统就分配一个这也的内核态结构。这种情况适用于不同进程打开同一个文件,也适用于同一个进程打开同一个文件。但是不同的用户态文件描述符可能会指向内核中的同一个内核的struct file结构,典型的情况下,用户通过dup(sys_dup-->>fget-->>atomic_inc_not_zero(&file->f_count))增加一个内核file的引用级数;另一种情况是用户非直接的通过fork系统调用来和父进程共享打开的文件描述符(sys_fork-->>do_fork-->>copy_process-->>copy_files-->>dup_fd-->>get_file).其实,内核中不同的struct file结构最为关键的是有自己的读写上下文,也就是f_pos字段。
2、struct dentry
linux-2.6.21\include\linux\dcache.h
struct dentry {
atomic_t d_count;
……
};
这个是一个文件系统目录项在内存中的表示,关键作用是为了维持这个结构在内存中的有效期,当这个值非零时,表示这个结构还有其它地方在引用(听起来是不是好像废话一样?)。而刚好这个结构通常的有效期都非常短,短到用户态几乎感觉不到他的存在(虽然它可能无时无刻不存在)。典型情况下,当我们执行open一个路径下的文件时,此时系统会执行这一串的字符串到对应的dentry进而到对应的inode之间的查找和转换关系。典型的路径为
sys_open-->>do_sys_open-->>do_filp_open-->>open_namei-->>path_lookup_open-->__path_lookup_intent_open-->>do_path_lookup--->>link_path_walk-->>__link_path_walk
我们可以注意到一个现象:如果文件正在被使用(打开,例如一些日志文件),即使这个文件被删除,依然可以通过这个打开的句柄来操作这个文件。进一步来说,通过/proc/$pid/fd/文件夹可以看到文件的完整路径。这里有一个文件,系统如何维护一个内存中的dcache的引用计数,从这个字符串名字到inode节点转换的调用连中,对于dentry的引用是像狗熊掰玉米一样,掰一个,丢一个,具体来说在do_lookup-->>__d_lookup函数中增加dentry的引用计数,然后在path_to_nameidata函数中通过dput(nd->dentry)来释放前一个dentry的引用计数,不过对于路径的最后一个dentry没有释放。以下面操作为例:tsecer @harry: tail -f rmdir/harry/tsecer/foo 
在路径查找中,rmdir、harry、tsecer、这几个dentry在找到foo之后就被释放了,只有foo的dentry增加了1,那么问题是,如何保证foo的上层目录一直在内存中存在呢?因为通过/proc/$pid/fd/查看文件完整路径时其父dentry一定是要在内存中存在的(整个流程没有判空处理)。这里的关键操作在于每个dentry被创建时的代码
struct dentry *d_alloc(struct dentry * parent, const struct qstr *name)

……
atomic_set(&dentry->d_count, 1);
……
if (parent) {
dentry->d_parent = dget(parent);
dentry->d_sb = parent->d_sb;
} else {
INIT_LIST_HEAD(&dentry->d_u.d_child);
}
也就是当分配一个dentry下的新dentry时,增加父dentry的引用计数,这样假设我执行10个进程来执行tail -f rmdir/harry/tsecer/foo 命令,则rmdir、harry、tsecer它们的引用计数为2(创建时1,分配子dentry时递增,当该值为1时执行dput表示该dentry可以被回收),而foo的引用技术为11。

3、struct inode 
linux-2.6.21\include\linux\fs.h
struct inode {
…… atomic_t i_count;
unsigned int i_nlink;
其中的i_count意义同样是表示该结构的内存生存期,而i_nlink表示的则是一个文件被“硬连接”的数量,即通过ln执行的次数。值得注意的是,符号连接并不会增加inode的引用计数,这一点可以在linux-2.6.21\fs\ext2\namei.c文件的ext2_link与ext2_symlink的代码实现中可以看到。
五、这些关系的集中体现
这些变量的典型使用在sys_close-->>filp_close函数的执行流程中,在这个系统调用中,涉及到了上面所说的所有结构引用计数的使用。
  评论这张
 
阅读(371)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017