Posts Tagged ‘filesystem’

在内核中对文件进行读写

September 28th, 2015

我们知道在用户态下,使用各种文件的系统调用即可对文件进行读写操作,open()、write()、read()等。这些调用最后都会通过内核的VFS模型,调用到设备驱动函数,这里我们可以简单看一下open()、write()、read()驱动函数接口:

static int xxx_open(struct inode *inode, struct file *file);
static int xxx_release(struct inode *inode, struct file *file);
static ssize_t xxx_read(struct file *filp, char __user *ubuf,
                                  size_t usize, loff_t *off);
static unsigned int xxx_poll(struct file *file, poll_table *wait);
static long xxx_ioctl(struct file *f, unsigned int cmd,
                                  unsigned long arg);
ssize_t xxx_write(struct file *filp, const char __user *ubuf,
                          size_t usize, loff_t *off)

实际上在内核驱动调用的函数传入的参数远比我们在用户态下看到的复杂的多,那么问题来了:如果在内核态下对文件进行读取?当然我们不能在内核中使用syscall了,这里我们有两种方式:1.将全部的驱动函数导出 2.使用VFS在内核态中的接口。

将全部的驱动函数导出的方式虽然可行,但是这样做等于将函数暴露在全局,不利于封装,不推荐使用这种方式。第二种是我们推荐的方式,内核为开发者提供了filp_open(),filp_close(),vfs_read(),vfs_write(),vfs_fsync()接口,我们只需要调用这些接口即可在内核态下对文件进行操作。

首先我们要包含头文件:

#include <linux/fs.h>
#include <asm/segment.h>
#include <asm/uaccess.h>
#include <linux/buffer_head.h>

当然了,很多时候我们在内核层下封装了这些接口,使其可以像用户态下open那般简单易用。不过我们要注意open的返回值不再是fd,而是struct file *类型的指针!而path就是路径,flag是读写权限。

struct file* file_open(const char* path, int flags, int rights) {
    struct file* filp = NULL;
    mm_segment_t oldfs;
    int err = 0;

    oldfs = get_fs();
    set_fs(get_ds());
    filp = filp_open(path, flags, rights);
    set_fs(oldfs);
    if(IS_ERR(filp)) {
        err = PTR_ERR(filp);
        return NULL;
    }
    return filp;
}

关闭一个文件:

void file_close(struct file* file) {
    filp_close(file, NULL);
}

读取文件的封装接口参数比较多,第一个参数是文件指针,第二个是偏移量,第三个是buffer,第四个是读取的大小。与用户态下的read()类似!

int file_read(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_read(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}   

写数据到文件中:

int file_write(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_write(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}

立即回写到磁盘,同步文件:

int file_sync(struct file* file) {
    vfs_fsync(file, 0);
    return 0;
}

 

http://stackoverflow.com/questions/1184274/how-to-read-write-files-within-a-linux-kernel-module
https://en.wikipedia.org/wiki/Virtual_file_system

VFS Data Structure关系(3) – 跨文件系统的文件操作分析

October 28th, 2014

通过对VFS 以及文件系统的分析,我们现在来分析一下跨文件系统的文件传输

http://www.lizhaozhong.info/archives/1080

http://www.lizhaozhong.info/archives/1110

比如从一个floppy传输到harddisk(ext2——>ext4)

首先我们明确一个概念:一切皆文件!

在linux中,无论是普通的文件,还是特殊的目录、设备等,VFS都将它们同等看待成文件,通过统一的文件操作界面来对它们进行操作。操作文件时需先打开;打开文件时,VFS会知道该文件对应的文件系统格式;当VFS把控制权传给实际的文件系统时,实际的文件系统再做出具体区分,对不同的文件类型执行不同的操作。

原理:将ext2格式的磁盘上的一个文件a.txt拷贝到ext4格式的磁盘上,命名为b.txt。这包含两个过程,对a.txt进行读操作,对b.txt进行写操作。读写操作前,需要先打开文件。由前面的分析可知,打开文件时,VFS会知道该文件对应的文件系统格式,以后操作该文件时,VFS会调用其对应的实际文件系统的操作方法。所以,VFS调用vfat的读文件方法将a.txt的数据读入内存;在将a.txt在内存中的数据映射mmap()到b.txt对应的内存空间后,VFS调用ext4的写文件方法将b.txt写入磁盘;从而实现了最终的跨文件系统的复制操作。

关于mmap()使用

fs/open.c

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
    struct open_flags op;
    int fd = build_open_flags(flags, mode, &op);
    struct filename *tmp;

    if (fd)
        return fd;

    tmp = getname(filename);
    if (IS_ERR(tmp))
        return PTR_ERR(tmp);

    fd = get_unused_fd_flags(flags);
    if (fd >= 0) {
        struct file *f = do_filp_open(dfd, tmp, &op);
        if (IS_ERR(f)) {
            put_unused_fd(fd);
            fd = PTR_ERR(f);
        } else {
            fsnotify_open(f);
            fd_install(fd, f);
        }
    }
    putname(tmp);
    return fd;
}

下面我们来分析下linux-3.14.8的内核(Not Finished!)

sys_open()
    |
    |------do_sys_open()
    |           |
    |           |------get_unused_fd_flags()
                |
                |------do_filp_open()
                            |
                            |----path_openat()
                            |       |
                            |       |----link_path_walk()

VFS Data Structure关系(2)

October 27th, 2014

之前分析了文件系统主要的数据结构inode,dentry,super_block,file.

为了加快文件的一些操作,还引入了中间的数据结构。

struct file_system_type(include/linux/fs.h)

file_system_type结构用来描述具体的文件系统的类型信息。被Linux支持的文件系统,都有且仅有一 个file_system_type结构而不管它有零个或多个实例被安装到系统中。

struct file_system_type {
    const char *name;
    int fs_flags;
#define FS_REQUIRES_DEV     1
#define FS_BINARY_MOUNTDATA 2
#define FS_HAS_SUBTYPE      4
#define FS_USERNS_MOUNT     8   /* Can be mounted by userns root */
#define FS_USERNS_DEV_MOUNT 16 /* A userns mount does not imply MNT_NODEV */
#define FS_RENAME_DOES_D_MOVE   32768   /* FS will handle d_move() during rename() internally. */
    struct dentry *(*mount) (struct file_system_type *, int,
               const char *, void *);
    void (*kill_sb) (struct super_block *);
    struct module *owner;
    struct file_system_type * next;
    struct hlist_head fs_supers;

    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;
    struct lock_class_key s_vfs_rename_key;
    struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];

    struct lock_class_key i_lock_key;
    struct lock_class_key i_mutex_key;
    struct lock_class_key i_mutex_dir_key;
};

和路径查找相关:

struct nameidata(include/linux/namei.h)

struct nameidata {
    struct path path;
    struct qstr last;
    struct path root;
    struct inode    *inode; /* path.dentry.d_inode */
    unsigned int    flags;
    unsigned    seq, m_seq;
    int     last_type;
    unsigned    depth;
    char *saved_names[MAX_NESTED_LINKS + 1];
};

被Linux支持的文件系统,都有且仅有一个file_system_type结构而不管它有零个或多个实例被安装到系统 中。每安装一个文件系统,就对应有一个超级块和安装点。也就是说如果有多个ext4 文件系统,也就有多个ext4的super_block 与 vfsmount 。

超级块通过它的一个域s_type指向其对应的具体的文件系统类型。具体的 文件系统通过file_system_type中的一个域fs_supers链接具有同一种文件类型的超级块。同一种文件系统类型的超级块通过域s_instances链 接。

 

通过下图,我们可以看到第一个与第三个super_block都指向同一个file_system_type

super_block

 

从下图可知,进程通过task_struct中的一个域files_struct files来找到它当前所打开的文件对象;而我们通常所说的文件 描述符其实是进程打开的文件对象数组的索引值。文件对象通过域f_dentry找到它对应的dentry对象,再由dentry对象的域d_inode找 到它对应的索引结点,这样就建立了文件对象与实际的物理文件的关联。

这里有一个非常重要的数据结构

struct files_struct(include/linux/fdtable.h)

/*
 * Open file table structure
 */
struct files_struct {
  /*
   * read mostly part
   */
    atomic_t count;
    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
    spinlock_t file_lock ____cacheline_aligned_in_smp;
    int next_fd;
    unsigned long close_on_exec_init[1];
    unsigned long open_fds_init[1];
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

我们可以看到fd_array 这个二维fd连接一个struct file类型的数据结构,这个struct file就是连接进程与文件的媒介,比dentry,inode更加高级!

super_block_and_task

 

 

通过学习VFS文件结构,我们就可以理解跨文件系统传输的基本原理。

下面的图我是按照kernel 3.14.8来画的,可以清晰的看出task_struct文件结构:

 

QQ图片20141027214645

 

 

VFS Data Structure关系(1)

October 20th, 2014

 VFS是管理具体文件系统的接口,Linux可支持数十种文件系统,不同的文件系统可以同时共存于一个系统之中。这些不同类型的文件系统并不是各自封闭的,会进行文件复制和移动等。

VFS是在各种具体的文件系统之上建立了一个抽象层,它屏蔽了不同文件系统间的差异。它之所以可以将各种文件系统纳入其中,是因为它提供了一个通用的文件系统模型。

VFS主要通过一组数据结构来描述文件对象。其中有四个基本的结构体:

超级块(struct super_block):它描述一个已安装了的文件系统。

索引结点(struct inode):它描述一个文件。

目录项(strcut dentry):它描述文件系统的层次结构。一个完整路径的每个组成部分都是一个目录项。比如打开/home/lzz/code/hello.c时,内核分别为/,home/,lzz/,code/,hello.c创建相应的目录项。

文件(struct file):它描述一个已被进程打开的文件。

位置:

block

VFS采用面向对象的思想,在上述每一个结构体中即包含描述每个文件对象属性的数据,又包含对这些数据进行操作的函数指针结构体。也就是说,上述四个基本的结构体中,每一个结构体中又嵌套了一个子结构体,这个子结构体包含了对父结构体进行各种操作的函数指针。

struct dentry (include/linux/dcache.h)

为了方便对目标文件的快速查找,VFS引入了目录项。目标文件路径中的每一项都代表一个目录项,比如/home/test.c中,/,home,test.c都分别是一个目录项。这些目录项都属于路径的一部分,并且每个目录项都与其对应的inode相联系。如果VFS得到了某个dentry,那么也就随之得到了这个目录项所对应文件的inode,这样就可以对这个inode所对应的文件进行相应操作。所以,依次沿着目标文件路径中各部分的目录项进行搜索,最终则可找到目标文件的inode。

与超级块和索引结点不同的是,目录项在磁盘上并没有对应的实体文件,它会在需要时候现场被创建。因此,在目录项结构体中并没有脏数据字段,因为目录项并不会涉及重写到磁盘。

d_inode:与该目录项相关联的索引结点;

struct super_block (include/linux/fs.h)

超级块结构代表一个已经安装了的文件系统,其存储该文件系统的有关信息。对于一个基于磁盘的文件系统来说,这类对象存放于磁盘的特定扇区中;对于非基于磁盘的文件系统,它会在该文件系统的使用现场创建超级块结构并存放在内存中。

s_inode 代表该文件系统中所有的索引结点形成一个双联表,该字段存放这个链表的头结点;

s_files:该文件系统中所有已被打开的文件形成一个双联表,该字段存放这个链表的头结点;

struct file(include/linux/fs.h)

每当一个进程打开一个文件时,内存中有会有相应的file结构体。因此,当一个文件被多个进程打开时,这个文件就会有多个对应的文件结构体。但是,这些文件结构体对应的索引结点和目录项却是唯一的

struct inode (include/linux/fs.h)

索引结点结构体用来描述存放在磁盘上的文件信息。每当内核对磁盘上的文件进行操作时,就会将该文件的信息填充到一个索引结点可以代表一个普通的文件,也可以代表管道或者设备文件等这样的特殊文件

i_sb_list:每个文件系统中的inode都会形成一个双联表,这个双链表的头结点存放在超级块的s_inodes中。而该字段中的prev和next指针分别指向在双链表中与其相邻的前后两个元素;

i_list:VFS中使用四个链表来管理不同状态的inode结点。inode_unused将当前未使用的inode链接起来,inode_in_use将当前正在被使用的inode链接起来,超级块中的s_dirty将所有脏inode链接起来,i_hash将所有hash值相同的inode链接起来。i_list中包含prev和next两个指针,分别指向与当前inode处于同一个状态链表的前后两个元素。

VFS

 

关于此图的说明:

1. 图中平行结构之间的箭头表示这两者之间仍有有若干个类似结点相连接;

PS:在linux 3.14.8 中,从task_struct到struct file有一个过程:

task_struct -> struct files_struct -> struct file -> struct path -> struct dentry

2. 图中阴影部分所示的进程并不是以实际的关系来表示的;

3. 图中彩色线条示意的场景:三个进程分别打开同一个文件。进程1和进程2打开同一路径的文件,因此两者的打开文件对应同一个目录项;而进程3打开的是一个硬链接文件,因此对应的目录项与前两者不同;

4.索引结点中i_sb_list链表是链接一个文件系统中所有inode的链表,因此相邻的inode之间均会由此链表链接;而i_list链接的是处于同一个状态的所有inode。所以,相邻inode之间并不一定链接在一起。