说明

本文档主要描述Linux kernel写文件API sys_write的详细过程。内核版本为4.7。

分析过程中如果遇到具体文件系统api,以ext2作为分析对象。

sys_write

入口

sys_write的实现位于内核代码的fs/read_write.c中:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,               
size_t, count) {

    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;
    if (f.file) {
        loff_t pos = file_pos_read(f.file);
        ret = vfs_write(f.file, buf, count, &pos);
        if (ret >= 0)                        
            file_pos_write(f.file, pos);
        fdput_pos(f);
    }
    return ret;
}

看上面的代码可以知道,sys_write的核心实现是vfs_write。

vfs_write

ssize_t vfs_write(struct file *file, const char __user *buf, 
        size_t count, loff_t *pos)
{
    ssize_t ret;
    // 进行权限检查
    if (!(file->f_mode & FMODE_WRITE))
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_WRITE))
    return -EINVAL;
    if (unlikely(!access_ok(VERIFY_READ, buf, count)))
        return -EFAULT; 
    ret = rw_verify_area(WRITE, file, pos, count);
    if (!ret) {
        if (count > MAX_RW_COUNT)
            count =  MAX_RW_COUNT;
        // 不清楚这是干什么
        file_start_write(file);
        ret = __vfs_write(file, buf, count, pos);
        if (ret > 0) {                         
            fsnotify_modify(file);                         
            add_wchar(current, ret);
        }                 
        inc_syscw(current);
        // 不太清楚这是干什么
        file_end_write(file);
    }
    return ret;
}

rw_verify_area()是对文件进行强制锁检查,关于内核文件强制锁机制,具体请参考这篇文章,这里不再赘述。而最核心的写逻辑是在__vfs_write函数内实现。

ssize_t __vfs_write(struct file *file, const char __user *p, 
    size_t count, loff_t *pos)
{
    if (file->f_op->write)
        return file->f_op->write(file, p, count, pos);
    else if (file->f_op->write_iter)
        return new_sync_write(file, p, count, pos);
    else
        return -EINVAL;
}

这里调用了特定文件系统的实现,根据前面的约定,我们以ext2文件系统为例,看看其特定实现,ext2实现的vfs定义的文件接口如下:

const struct file_operations ext2_file_operations = {
.llseek         = generic_file_llseek,
.read_iter      = generic_file_read_iter,
.write_iter     = generic_file_write_iter,
.unlocked_ioctl = ext2_ioctl,
......
};

结合上面的__vfs_write来分析,我们知道对于ext2文件系统的文件,最终的写入流程进入了generic_file_write_iter()函数中。

generic_file_write_iter

ssize_t generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    struct file *file = iocb->ki_filp;
    struct inode *inode = file->f_mapping->host;
    ssize_t ret;
    // 为什么在写入时对inode加锁?
    inode_lock(inode);
    ret = generic_write_checks(iocb, from);
    if (ret > 0)
        ret = __generic_file_write_iter(iocb, from);
    inode_unlock(inode);
    if (ret > 0)
        ret = generic_write_sync(iocb, ret);
    return ret;
}

这里真正执行写入的是__generic_file_write_iter,但是在写入之前先对文件inode加锁,不知是为何考虑?

ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    struct file *file = iocb->ki_filp;
    struct address_space * mapping = file->f_mapping;
    struct inode    *inode = mapping->host;
    ssize_t         written = 0;
    ssize_t         err;
    ssize_t         status;

    current->backing_dev_info = inode_to_bdi(inode);
    // 处理O_DIRECT方式写,忽略
    if (iocb->ki_flags & IOCB_DIRECT) {
        ......
    } else {
        written = 
        generic_perform_write(file, from, iocb->ki_pos);
        if (likely(written > 0))
            iocb->ki_pos += written;
        }
out:
    current->backing_dev_info = NULL;
    return written ? written : err;
}

我们忽略O_DIRECT方式的文件写入,缓存写则是使用了函数generic_perform_write。

ssize_t generic_perform_write(struct file *file,                               struct iov_iter *i, loff_t pos)
{
    struct address_space *mapping = file->f_mapping;
    const struct address_space_operations *a_ops = mapping->a_ops;
    long status = 0;
    ssize_t written = 0;
    unsigned int flags = 0;
    // 对要写入的缓存page,执行三个步骤
    // 1. write_begin:准备好要写入的page
    // 2. copy_into_page:将数据写入page
    // 3. write_end: 处理后事 
    do {
        struct page *page;
        unsigned long offset;
        unsigned long bytes;
        size_t copied;
        void *fsdata;
        offset = (pos & (PAGE_SIZE - 1));
        bytes = min_t(unsigned long, PAGE_SIZE - offset,                                               
        iov_iter_count(i));
again:

        status = a_ops->write_begin(file, mapping, pos, bytes, 
            flags, &page, &fsdata);
        if (unlikely(status < 0))
            break;
        ......
        copied = 
    iov_iter_copy_from_user_atomic(page, i, offset, bytes);  
        // 这个flush在x86架构下好像为空?            
        flush_dcache_page(page);
        status = a_ops->write_end(file, mapping, pos, bytes, 
            copied, page, fsdata);
        if (unlikely(status < 0))
            break;
        copied = status;
        cond_resched();
        iov_iter_advance(i, copied);
        if (unlikely(copied == 0)) {
            bytes = min_t(unsigned long, PAGE_SIZE - offset,                                                
              iov_iter_single_seg_count(i));
             goto again;
        }
        pos += copied;
        written += copied;               
        balance_dirty_pages_ratelimited(mapping);
    } while (iov_iter_count(i));
    return written ? written : status;
}

这里写入的逻辑其实很简单,将传入的数据写入cache page中,但是注意这里并不负责sync,如果文件是O_SYNC方式写的话,调用者需要处理此逻辑。

对于每个page的写入,过程是比较简单的,主要分为三步走:

  1. write_begin: 写前准备,准备好要写入的cache page
  2. copy_data_to_page: 执行数据的拷贝
  3. write_end: 写后清理。

上述过程1和3与文件系统相关,我们同样只关注ext2的实现。ext2的write_begin实现方法是ext2_write_begin。

write_begin

我们前面说过,write_begin()的主要作用的准备好cache page:就是将cache page中填充好文件数据,因为接下来要将新写入数据拷贝至该page,因此在写之前必须要

  • 将数据从磁盘读入内存缓存
  • 保证该page内的数据是干净的,即与磁盘上的数据保持一致。
static int
ext2_write_begin(struct file *file, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata)
{
    int ret;
    ret = block_write_begin(mapping, pos, len, flags, pagep,                              ext2_get_block);
    if (ret < 0)               
        ext2_write_failed(mapping, pos + len);
    return ret;
}

该函数主要调用block_write_begin来准备好page:

int block_write_begin(struct address_space *mapping, loff_t pos, unsigned len, unsigned flags, struct page **pagep, get_block_t *get_block)
{
    pgoff_t index = pos >> PAGE_SHIFT;
    struct page *page;
    int status; 
    // 首先查找或者分配一个page
    // 注意:这里会将page lock住
    page = grab_cache_page_write_begin(mapping, index, flags);
    if (!page)
        return -ENOMEM;
    status = __block_write_begin(page, pos, len, get_block);
    if (unlikely(status)) {
        ......     
    }
    *pagep = page;
    return status;
}

该函数主要又干了两件事:

  1. 查找或者分配page
  2. 调用__block_write_begin来填充该page
int __block_write_begin(struct page *page, loff_t pos, 
        unsigned len, get_block_t *get_block)
{
    unsigned from = pos & (PAGE_SIZE - 1);
    unsigned to = from + len;
    struct inode *inode = page->mapping->host;
    unsigned block_start, block_end;
    sector_t block;
    int err = 0;
    unsigned blocksize, bbits;
    struct buffer_head *bh, *head, *wait[2], **wait_bh=wait;

    head = create_page_buffers(page, inode, 0);
    blocksize = head->b_size;
    bbits = block_size_bits(blocksize);
    block = (sector_t)page->index << (PAGE_SHIFT - bbits);
    for(bh = head, block_start = 0; bh != head || !block_start;
    block++, block_start=block_end, bh = bh->b_this_page) {
    block_end = block_start + blocksize;
    if (block_end <= from || block_start >= to) {
        if (PageUptodate(page)) {
            if (!buffer_uptodate(bh))                                    
                set_buffer_uptodate(bh);
        }
        continue;
    }
    if (buffer_new(bh))                         
        clear_buffer_new(bh);
    if (!buffer_mapped(bh)) {
        err = get_block(inode, block, bh, 1);
        if (err)                                 
            break;
        ......
}

填充page的具体原理是将page内的每个buffer与物理磁盘块建立映射。

当然,在这里我们并不一定要填充页面内的所有buffer_head,我们还得看看要写入的数据区间位于page的哪个或者那些buffer_head(看函数内部变量from和to的计算)。对于没有落在我们关心的区间范围内的buffer_head,我们可以忽略。

如果[from, to]落在某个buffer_head,那我们需要:

  1. 如果该buffer_head尚未与物理磁盘块建立映射,if (!buffer_mapped(bh)),此时需要调用文件系统特定的get_block()方法来将两者相互关联,对于ext2文件系统来说,该方法便是ext2_get_block。
  2. 接下来判断如果buffer_head对应的内存缓存数据和磁盘上的不一致,那需要将数据从磁盘上读出。这调用了函数ll_rw_block()。
  3. 最后,如果过程2中真的要去从磁盘中读数据,那么还需要等着数据读取完成才能继续往下走,因此,对每个需要从磁盘读取的buffer_head,调用wait_on_buffer() 等着它完成。

至此,我们把更新page的第一个步骤write_begin详细过程基本描述清楚了,主要就是准备好要写的page。

关于buffer_head在内核中的来龙去脉以及与page的关系,请参考我的另外一篇文章”buffer_head: 过去、现在和未来”

接下来第二个步骤就是将要写入的数据拷贝至page。该过程调用函数iov_iter_copy_from_user_atomic,这没什么好说的,我们选择性忽略,需要注意的一点是:在此过程中page是会被锁住的。

write_end

写入完成后,再调用write_end结束本次写入,这也是由具体文件系统实现的,对ext2来说,实现为ext2_write_end,而ext2_write_end主要又是调用了generic_write_end。

int generic_write_end(struct file *file, 
    struct address_space, *mapping, 
    loff_t pos, unsigned len,
    unsigned copied, struct page *page, void *fsdata)
{
    struct inode *inode = mapping->host;
    loff_t old_size = inode->i_size;
    int i_size_changed = 0;          
    copied = block_write_end(file, mapping, pos, len, 
        copied, page, fsdata);

    // 判断本次写入有没有导致大小变化
    // 如果有,接下来需要标记inode为脏,等待刷到磁盘中
    if (pos+copied > inode->i_size) {                 
        i_size_write(inode, pos+copied);
        i_size_changed = 1;
    }
    unlock_page(page);         
    put_page(page);
    if (i_size_changed)                 
        mark_inode_dirty(inode);
    return copied;
}

而这里:

  1. 首先调用block_write_end()来更新page被写入新数据的buffer_head的状态,更新为Dirty,便于以后被刷新至磁盘;
  2. 接下来判断本次写入是否导致文件大小变化,如果是,还需要更新inode的size属性;
  3. 解锁page,因为我们前面说过page在进行数据更新的时候需要被锁住,既然更新完了我们理所应当要解锁了;
  4. 接下来判断如果上面inode的size属性被更新了,需要标记该inode为脏,合适的时候也得一并更新至磁盘上。

总结

  1. 文件写入前需要对inode加锁,这是为什么
  2. 对每个page更新时需要对page加锁,这是为了防止page并发更新以及脏读
  3. 如果使用buffer_head的方式进行内存页面和物理磁盘的映射,在写入前需要完成这种映射关系的建立,在写入完成后要标记特定buffer_head的Dirty标志位的设置
  4. 在这里有一事不明的是:为什么在写入之前要对inode加锁,这样的话,对文件的写入其实就已经被串行化了,也就不用再搞什么强制锁或者建议锁了。不知道我这种理解是否准确?