파일시스템마운트
이번 강좌에서는 파일 시스템의 동작과정에 대하여 알아보기로 한다. 파일 시스템의 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 함수는 rfks의file_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사업팀 발간
'L I N U X' 카테고리의 다른 글
/etc/bash.bashrc /root/.bash_profile 의 차이 (0) | 2012.02.09 |
---|---|
Step by Step 커널 프로그래밍 강좌⑥ (0) | 2012.02.08 |
Step by Step 커널 프로그래밍 강좌④ (0) | 2012.02.08 |
Step by Step 커널 프로그래밍 강좌③ (0) | 2012.02.08 |
Step by Step 커널 프로그래밍 강좌② (0) | 2012.02.08 |