본문 바로가기

L I N U X

Step by Step 커널 프로그래밍 강좌⑤


파일시스템마운트

 

 

이번 강좌에서는 파일 시스템의 동작과정에 대하여 알아보기로 한다파일 시스템의 read/write 대한 내용은 다른 많은 리눅스 커널 책에도  소개되어 있다그러므로 이번 강좌와 다음 강좌에서는 파일 시스템의 마운트 과정에 대하여 자세히 분석하며 파일 시스템의 동작 과정을 이해하도록  예정이다.

 

 _ 김민찬 KLDP 멤버전문 프로그래머

 

연재 순서

① 커널 프로그래밍 환경 구축하기와 특징

② 모듈 구현하기

③ 리눅스 커널의 메모리 관리

④ 커널의 동기화에 관하여

⑤ 파일 시스템 마운트 1

⑥ 파일 시스템 마운트 2

 

sys_mount 함수 호출 과정을 항상 참조하라

이번 강좌에서는 파일 시스템의 분석을 위해 간단한 파일 시스템을 사용하며 분석할 것이다. rkfs라는 파일 시스템이며 이 파일 시스템에 관련된 사항은 다음 URL 참조하면 된다.

 

http://www.geocities.com/ravikiran_uvs/articles/rkfs.html

 

먼저 사용자가 파일시스템을 mount 하게 되면 커널의 다른 system call들과 마찬가지로 커널의sys_mount 함수가 호출된다.

먼저 sys_mount 분석하기 전에 주의 사항이 있다 함수는 여러 중요한 함수들이 굉장히 깊이 있게연결되어 있다그러므로 소스를 분석하다 보면 자신이 지금 어디에 서있는지 길을 잃어버리는 경우가 많다그러므로 항상 [그림 1] 참조하여 자신이 지금 어느 곳에 서있는지 기억하고 있어야  것이다.

 

sys_mount 인자로 넘겨받은 데이터들을 커널 메모리에 복사한 실제적인 mount operation 처리하는 do_mount 함수를 호출한다.

do_mount 함수는 기본적인 sys_mount 함수의 C++ overriding 같은 역할을 한다 넘겨받은 파라미터flags 따라서 실제 일을 담당하는 함수  하나를 호출해준다호출되는 함수들은 다음과 같다.

 

● do_remount : flags MS_REMOUNT 옵션이 있을 

● do_loopback : flags MS_BIND 옵션이 있을 

● do_move_mount : flags MS_MOVE 있을 

● do_new_mount:  외의 모든 경우

 

위의 함수를 호출하기 먼저 sys_mount로부터 넘겨받은 파라미터들의 간단한 유효성 검사를 수행한다 다음 mount point dentry, vfsmount 등의 구조체를 얻어오기위하여 path_lookup 함수를 호출한다.vfsmount 다음과 같은 필드를 갖는다.

 

 

 

struct vfsmount

{

struct list_head mnt_hash;

struct vfsmount *mnt_parent; // 우리의 vfsmount가 마운트된 파일이 속해있는 vfsmount

struct dentry *mnt_mountpoint; /* mount point 파일 과 관련된 dentry /*

struct dentry *mnt_root; /* root of the mounted tree */

struct super_block *mnt_sb; /* vfsmount root inode 관련된 superblock */

struct list_head mnt_mounts; /*  vfsmount에 children vfsmount들의 리스트 /*

struct list_head mnt_child; /* child간의 연결 리스트 /*

atomic_t mnt_count;

int mnt_flags;

int mnt_expiry_mark; /* true if marked for expiry */

char *mnt_devname; /* Name of device e.g./dev/dsk/hda1 */

struct list_head mnt_list;

struct list_head mnt_fslink; /* link in fs-specific expiry list */

struct namespace *mnt_namespace; /* containing namespace */

};

 1. vfsmount 구조체

 

 

do_new_mount 함수에 대한 호출

다음으로 do_new_mount 함수에 대한 호출을 살펴볼 것이다.

do_new_mount 다음과 같이 호출된다.

 

static int do_new_mount(struct nameidata *nd, char *type, int flags,

int mnt_flags, char *name, void *data)

{

struct vfsmount *mnt;

if (!type || !memchr(type, 0, PAGE_SIZE))

return -EINVAL;

/* we need capabilities... */

if (!capable(CAP_SYS_ADMIN))

return -EPERM;

mnt = do_kern_mount(type, flags, name, data);

if (IS_ERR(mnt))

return PTR_ERR(mnt);

return do_add_mount(mnt, nd, mnt_flags, NULL);

}

코드 1. do_new_mount

 

 함수는 파라미터로 nameidata 구조체와 type, flags,mnt_flags, mount되는 device name, data_page받는다. 함수는  함수는 do_kern_mount 함수를 호출하여 superblock, dentry, address_space 관련된 구조체들을 생성하고 연결한다그런  do_add_mount 함수를 호출하여namespace tree 연결시킨다 함수를 자세히 살펴보기로 하자.

 

 

do_kern_mount() 함수는 다음과 같다.

 

struct vfsmount *

do_kern_mount(const char *fstype, int flags, const

char *name, void *data)

{

struct file_system_type *type = get_fs_type(fstype);

struct super_block *sb = ERR_PTR(-ENOMEM);

struct vfsmount *mnt;

int error;

char *secdata = NULL;

if (!type)

return ERR_PTR(-ENODEV);

mnt = alloc_vfsmnt(name);

if (!mnt)

goto out;

...

sb = type → get_sb(type, flags, name, data);

if (IS_ERR(sb))

goto out_free_secdata;

error = security_sb_kern_mount(sb, secdata);

if (error)

goto out_sb;

mnt → mnt_sb = sb;

mnt → mnt_root = dget(sb → s_root);

mnt → mnt_mountpoint = sb → s_root;

mnt → mnt_parent = mnt;

mnt → mnt_namespace = current → namespace;

up_write(&sb → s_umount);

put_filesystem(type);

return mnt;

코드 2. do_kern_mount

 

이 함수는 먼저 get_fs_type에 아규먼트로 파스된 fstype을 패스 하여 mount 하려는 파일시스템의 구조 체적인 file_system_type 구조체를 찾는다. 

 

struct file_system_type {

const char *name;

int fs_flags;

struct super_block *(*get_sb)

(struct file_system_type *, int,

const char *, void *);

void (*kill_sb) (struct super_block *);

struct module *owner;

struct file_system_type * next;

struct list_head fs_supers;

};

코드 3. file_system_type 구조체

 

 구조체는 register_filesystem 함수로 파일시스템을 등록할  파라미터로 사용된다 구조체의 필드 가장 중요한 필드는 get_sb 함수 포인터이다 함수는 파일시스템 개발자가 커널에 있는 VFS(Virtual Filesystem Layer) hook(갈고리) 걸어 놓는 것이다이렇게 callback 함수를 두는 이유는 VFS general 인터페이스를 이용하되 자신에 입맛에 맞게 부분을 수정하려는 것이다위의 함수 get_sb는 superblock 대한 초기화를 담당하게 되는데  파일시스템별로 superblock많은 필드들이 서로 다르다그러므로 커널은 위와 같은 hook   있는 인터페이스를 제공하여 파일시스템 개발자들에게 꿈과 희망을 주는 것이다.

 다음으로는 alloc_vfsmnt 호출하여 아규먼트로 넘어온 name 해당하는 vfsmount 구조체를 만든다.
 
이때 vfsmount mnt_cache 통하여 만들어지며 여러 필드들이 초기화된다아규먼트로 패스된name vfsmount 구조체필드의 mnt_devname 저장된다.

 

파일 시스템의 Specific Layer 넘어가자

다음은 get_sb 호출한다 부분에서 우리는 최초로 VFS generic layer에서 파일 시스템의Specific Layer로 넘어온 것이다필자는 filesystem specific 부분에 대한 이해를 돕기 위하여 간단한 ram file system rfks 예를 들어 설명한다. (rfks 소스 - 부록 참고). get_sb 함수는 rfksfile_system_type 다음과 같이 정의되어 있다

 

static struct file_system_type rkfs = {

name: "rkfs",

get_sb: rkfs_get_sb,

kill_sb: rkfs_kill_sb,

owner: THIS_MODULE

};

코드 4. rkfs file_system_type

 

 

rkfs filesystem 이름은rkfs이며 나머지는 필드에서 보는 바와 같이 초기화되어 있다그러므로 get_sb함수는 rfks_get_sb 함수를 호출한다 함수는 미리 커널에 정의되어 있는 get_sb_single 함수를 호출하는 단순한 wrapper 함수이다하지만 함수를 호출할 때 파라미터로 rfks_fill_super 함수의 포인터를 패스한다.

 

static struct super_block *

rkfs_get_sb(struct file_system_type *fs_type,int flags, const char *devname, void *data, struct vfsmount *mnt)

{

/* rkfs_fill_super this will be called to fill the superblock */

return get_sb_single( fs_type, flags, data, &rkfs_fill_super, mnt);

}

코드 5. rkfs_get_sb

 

이것도 위와 같은 hook 거는 함수이다 hook 새로이 할당받은 superblock rkfs 알맞은 데이터로 채워 넣기 위해서이다아래의 superblock 구조체는 file system에서 inode 만큼이나 중요한 역할을 하는 구조체이다대부분의 필드들은 이름이 직관적이어서 굳이 설명이 필요 없을  같다. s_list, s_files, s_instances 필드들은  용도를 보게  것 이다.

 

struct super_block {

struct list_head s_list; /* Keep this first */

dev_t s_dev; /* search index; _not_kdev_t */

unsigned long s_blocksize;

unsigned long s_old_blocksize;

unsigned char s_blocksize_bits;

unsigned char s_dirt;

unsigned long long s_maxbytes; /* Max file size*/

struct file_system_type *s_type;

struct super_operations *s_op;

...

unsigned long s_flags;

unsigned long s_magic;

struct dentry *s_root;

struct list_head s_inodes; /* all inodes */

struct list_head s_dirty; /* dirty inodes */

struct list_head s_io; /* parked for writeback */

struct hlist_head s_anon; /* anonymous dentries for (nfs) exporting */

struct list_head s_files;

struct block_device *s_bdev;

struct list_head s_instances;

...

}

코드 6. superblock 구조체

get_sb_single 함수는 fs/super.c 정의되어 있다 함수는 sget이라는 커널함수를 호출하여superblock 할당받고 superblock s_root 설정되어 있지 않다면 사용자의 hook여기서는 rkfs_fill_super 함수를 호출하여 할당 받은 superblock 사용자의 입맛에 맞게 초기화하게되는 것이다. rkfs_fill_super 함수는 다음과 같다 함수는 파일시스템 개발자가 자신의 구미에 맞게superblock 초기화 하기 위해 등록해 놓은 callback함수이다 함수는 get_sb 함수가 호출될 rkfs_get_sb 함수에 의해서 패스된다.

 

static int

rkfs_fill_super(struct super_block *sb, void *data,

int silent)

{

printk("RKFS: rkfs_fill_supern" );

...

sb → s_magic = RKFS_MAGIC;

sb → s_op = &rkfs_sops; // super block operations

sb → s_type = &rkfs; // file_system_type

rkfs_root_inode = iget(sb, 1); // allocate an inode

rkfs_root_inode → i_op = &rkfs_iops; // set theinode ops

rkfs_root_inode → i_mode = S_IFDIR|S_IRWXU;

rkfs_root_inode → i_fop = &rkfs_fops;

if(!(sb → s_root = d_alloc_root(rkfs_root_inode))) {

iput(rkfs_root_inode);

return -ENOMEM;

}

return 0;

코드 7. rkfs_fill_super

 

 코드는 superblock operation table 지정하고 file_system_type rkfs 주소로 지정한다그런 iget을 호출해서 rkfs root inode 위한 inode 할당받고 inode i_op, i_fop operation table 지정한다그리고 마지막으로 d_alloc_root 함수를 방금 할당 받은 root inode 파라미터로 패스하여 호출한다이것은 inode 관련된 dentry 

환하게 된다.

여기까지의 과정을 살펴보면 다음과 같다.

 

 

Superblock 중요한 필드들

그럼 지금부터 위의 함수들을 따라서 더욱 깊은 곳으로 들어 보도록 하자.

현재 kernel control path rkfs rkfs_fill_super 함수가 가지고 있다고 가정하자 함수는 sget 통해서할당받은 superblock 기본적인 필드들을 채운다. rkfs_fill_super 

수는 먼저 기본적인 필드들을 지정한다. superblock 필드는 매우 많지만 중요한  가지만 살펴보기로한다.

 

● s_op : superblock operation 함수 포인터 테이블

● s_type : rkfs file_system_type

● s_root : superblock root inode 대한 dentry

 

 

먼저 s_op 필드를 살펴보면 다음과 같다.

 

static struct super_operations rkfs_sops = {

read_inode: rkfs_super_read_inode,

statfs: simple_statfs, /* handler from libfs */

write_inode: &rkfs_super_write_inode

};

코드 8. rkfs superblock operations

 

 

나머지 필드들은 0으로 채워진다 필드의 함수 포인터들의 사용은 사용이 일어날  자세히 알아보기로 하고 지금은 다음으로 일단 넘어가자. s_type 필드는 이미 전에 살펴보았다.

마지막으로 s_root 대한 것을 알아보자. s_root dentry 만들기 위해서는 dentry 관련될 inode가 필요하다 inode 생성하기 위해 iget 함수를 superblock 할당될 inode number 파라미터로 패스하여호출하여 호출한다.

 

static inline struct inode *iget(struct super_block

*sb, unsigned long ino)

{

struct inode *inode = iget_locked(sb, ino);

if (inode && (inode → i_state & I_NEW)) {

sb → s_op → read_inode(inode);

unlock_new_inode(inode);

}

return inode;

}

코드 9. iget

 

 

iget 함수는 iget_locked 함수를 호출해서 inode hash table에서 파라미터로 패스된 ino 번호를 가진inode 찾는다해당 inode 발견되지 않으면 새로운 inode 할당해서 반환하게 된다할당받은inode 기존에 inode cache에 들어있던 inode 아니고 새로운 inode라면 sb->s_op->read_inode, rkfs에서는 rkfs_super_read_inode 호출하게 된다지금 필자가 설명하고 있는 내용은 새로운 파일시스템의 mount 과정이므로 당연히 inode cache 들어있지 않을 것이다그러므로rkfs_super_read_inode 함수가 호출된다 함수는 address_space operation table setup 하는 중요한 역할을 한다  함수에 대해서는 iget_locked가 호출하는 ifind_fast get_new_inode_fast 설명한뒤에 설명하기로 한다.

 

iget_locked 함수는 ifind_fast 함수를 호출해서 inode cache에서 해당 ino inode 찾게되고 발견하면inode 반환하고 그렇지 않다면 get_new_inode_fast 함수를 호출하여 새로 inode 할당한다이때 inode 찾는 방식은 hash 사용하게 되며 hash table 주소는inode_hashtable 변수에 있다다음 hash_table에서 slot 얻기 위해서 hash 함수를 다음과 같이 호출하여 첫번째 slot 찾아낸다.

 

struct hlist_head *head = inode_hashtable + hash(sb, ino);

코드 10. inode cache에서 hash 함수

지금, ifind_fast 지금 inode 찾을  없다왜냐하면 우리는 filesystem mount 하는 중이기 때문이다.아직 어떤inode 할당되어 있지 않은 상황이다그러므로 get_new_inode_fast 함수를 호출해서 새로운inode 할당받는다 함수는 제일 먼저 alloc_inode(sb) 호출한다새로운 inode 할당받은 후에inode i_no 파라미터로 패스받은 ino로지정하고 inode 자료구조를 inode_in_use sb->s_inodes 연결한다.

 

 

inode_in_use 시스템에 현재 사용중인 inode 관리하는 리스트이고 sb  s_inodes sb 할당된inode 관리하는 리스트이다그리고 마지막으로 inode_hashtable inode 를 연결하여 inode cache넣는다

 

이번 강좌에서는 파일시스템의 마운트 과정에 대한 일부를 살펴보았다다음 강좌에서는 이번 강좌에 이어 파일시스템마운트의 나머지 과정에 대해 살펴볼 예정이다.

 

 

출처 : 공개 SW 리포트 11호 페이지 56 ~ 61 발췌(2008 3) - 한국소프트웨어 진흥원 공개SW사업팀 발간