파일시스템마운트
이번 강좌는 지난 강좌에 이어 파일시스템 마운트 과정의 나머지 부분에 대해 알아보기로 한다. 내용 중일부는 지난 호와
관련되기 때문에 지난 호와 같이 봐야 이해가 될 것이라 생각한다.
글 _ 김민찬 KLDP 멤버, 전문 프로그래머
연재 순서
① 커널 프로그래밍 환경 구축하기와 특징
② 모듈 구현하기
③ 리눅스 커널의 메모리 관리
④ 커널의 동기화에 관하여
⑤ 파일 시스템 마운트 1
⑥ 파일 시스템 마운트 2
alloc_inode 함수로 inode 할당
alloc_inode 함수를 좀 더 자세히 살펴보자. 이 함수는 리눅스커널에서 아주 중요한 역할을 하는 구조체 중하나인 inode를 할당하는 함수이다. inode는 많은 필드를 가지고 있으며 초기화 과정 또한 그리 만만치 않다. inode의 중요한 몇몇 필드(address_space,backing_dev_info, host, i_mapping)만을 alloc_inode 함수를 살펴보며 같이 보기로 하자.
struct inode {
struct hlist_node i_hash;
struct list_head i_list;
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
...
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
...
unsigned int i_blkbits;
unsigned long i_blksize;
unsigned long i_version;
unsigned long i_blocks;
unsigned short i_bytes;
unsigned char i_sock;
...
struct inode_operations *i_op;
struct file_operations *i_fop; /* former → i_op→ default_file_ops */
struct super_block *i_sb;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
...
/* These three should probably be a union */
struct list_head i_devices;
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
...
unsigned long i_state;
unsigned long dirtied_when; /* jiffies of first dirtying*/
unsigned int i_flags;
atomic_t i_writecount;
void *i_security;
union {
void *generic_ip;
} u;
};
코드 1. inode 구조체
alloc_inode 함수는, 인수로 받은 superblock의 alloc_inode함수 포인터가 정의돼 있다면 정의된 함수를호출해 inode를 할당받는다(이것 또한 hook이다. 커널의 VFS 구조는 파일시스템의 많은 유연성을 제공하기 위해 많은 hook을 제공한다).
하지만 rkfs에는 해당함수가 정의돼 있지 않기 때문에 커널은 inode_cachep을 통해 inode를 할당받는다.
static struct inode *alloc_inode(struct super_block *sb)
{
static struct address_space_operations empty_aops;
static struct inode_operations empty_iops;
static struct file_operations empty_fops;
struct inode *inode;
if (sb → s_op → alloc_inode)
inode = sb → s_op → alloc_inode(sb);
else
inode = (struct inode *)kmem_cache_alloc(inode_cachep, SLAB_KERNEL);
if (inode) {
struct address_space * const mapping =
&inode → i_data;
inode → i_sb = sb;
inode → i_blkbits = sb->s_blocksize_bits;
inode → i_flags = 0;
atomic_set(&inode → i_count, 1);
inode → i_sock = 0;
inode → i_op = &empty_iops;
inode → i_fop = &empty_fops;
inode → i_nlink = 1;
atomic_set(&inode→ i_writecount, 0);
inode → i_size = 0;
inode → i_blocks = 0;
inode → i_bytes = 0;
inode → i_generation = 0;
#ifdef CONFIG_QUOTA
memset(&inode→ i_dquot, 0, sizeof(inode→ i_dquot));
#endif
inode → i_pipe = NULL;
inode → i_bdev = NULL;
inode → i_cdev = NULL;
inode → i_rdev = 0;
inode → i_security = NULL;
inode → dirtied_when = 0;
if (security_inode_alloc(inode)) {
if (inode → i_sb → s_op → destroy_inode)
inode → i_sb →s_op → destroy_inode(inode);
else
kmem_cache_free(inode_cachep, (inode));
return NULL;
}
mapping → a_ops = &empty_aops;
mapping → a_ops = &empty_aops;
mapping → host = inode;
mapping → flags = 0;
mapping_set_gfp_mask(mapping,GFP_HIGHUSER);
mapping → assoc_mapping = NULL;
mapping → backing_dev_info = &default_backing_dev_info;
/*
* If the block_device provides a backing_dev_info for client
* inodes then use that. Otherwise the inode share the bdev's * backing_dev_info.
*/
if (sb → s_bdev) {
struct backing_dev_info *bdi;
bdi = sb → s_bdev→ bd_inode_backing_dev_info;
if (!bdi)
bdi = sb → s_bdev → bd_inod → i_mapping → backing_dev_info;
mapping → backing_dev_info = bdi;
}
memset(&inode → u, 0, sizeof(inode → u));
inode → i_mapping = mapping;
}
return inode;
}
코드 2. alloc_inde 함수
위의 함수에서 address_space는 page cache를 구현하는, 아주 중요한 역할을 하는 구조체이다.
또한 실제로 파일시스템에서 블록을 읽기 위한 기능을 구현하 는 address_space_operations 구조체를포함하기도 한다. 파일을 읽기 위한 함수 테이블 필드로는 inode_operations 구조체인 i_op와 file_operations 구조체인 i_fop가 있다.
inode_operations 구조체는 파일과 관련된 inode를 만들고 삭제하는 등의 역할을 하는 inode 관련 함수들의 모음이다.
반면, file_operations 구조체는 응용 프로그래머들이 일반적으로 사용하는 open, read, write, close, ioctl, mmap 등과 연관되는 함수이다.
일반적으로 file_operations의 함수들은 최종적으로 address_space_operations의 함수들을 호출해 실제적으로 block device에서 페이지들을 읽게 된다.
read-ahead 메커니즘 활용
다음으로 backing_dev_info 필드는 read-ahead와 관련된 정보를 저장하는 필드이다.
리눅스는 disk-based filesystem을 일반적으로 사용해왔다. 지금처럼 nand memory를 저장 장치로 사용하는 임베디드 리눅스가 보편화되기 전까지만 해도 그랬다. 따라서 리눅스 커널은 read-ahead 메커니즘을 사용해 파일시스템 성능을 개선했다.
disk-based 저장 장치는 특정 섹터를 찾기 위해 헤더와 실린 더를 이동해야하고, 따라서 그 속도가 섹터를 읽거나 쓰는 것에 비해 현저히 느리다. 때문에 한 섹터를 읽을 때 인접한 섹터들을 미리 읽어둬 페이지캐시에 저장해 놓는 기술 즉, readahead 기술을 활용했다.
이는 특정 프로그램이 특정 섹터를 필요로 한다면 조만간 인접한 다른 섹터도 필요로 할 확률이 높을 것이라는 낙관론적 방법에서 시작됐다. 하지만 nand와 같이 seek time이 거의
들지 않는 저장장치를 사용할 때 read-ahead가 주는 장점은 많이 줄어들며 심지어는 부팅타임에 시간을잡아먹는 요소로 작용하기도 한다.
read-ahead와 관련된 작업은 inode를 처음 할당할 때 시작 된다. 먼저 default_backing_dev_info의 전역변수의 주소를 address_space의 backing_dev_info에 저장한다. 하지만 inode를 할당하는 super block이block device와 관련돼있고, block device driver가 bd_inode_backing_dev_info 필드를 가지고 있다면 해당 block device의 bd_inode_
backing_dev_info 필드를 address_space의 backing_dev_info 필드로 초기화한다. 즉 default action을overriding 한다고 생각하면 된다.
다음으로 address_space의 host 필드는 해당 address_space와 관련된 inode에 대한 포인터이다. 마지막으로 현재 i_mapping 필드는 address_space 자기 자신을 가리킨다.
하지만 바뀔 수도 있다. 그럼 지금까지 rfks_fill_super에서 iget을 호출한 후 iget_locked까지 호출한 시점에서의 자료 구조는 다음과 같다.
코드에서 inode가 inode cache에서 발견되지 않아 새롭게 할당된 것이라면 sb의 read_inode 함수를 호출해 inode의 필드 중 몇몇을 채운다.
rkfs에는 rkfs_super_read_inode 함수가 정의돼 있으므로 이 함수를 호출하게 된다. superblock operation이 함수는 반드시 초기화돼 있어야만 한다. rkfs의 rkfs_super_read_inode 함수는 간단하다. inode의i_mtime, i_atime, i_ctime변수를 CURRENT_TIME으로 초기화하고 inode의 address_space의 a_ops 필드를 rkfs_apos로 지정한다.
이것은 address_space_operation으로 파일시스템에서 실제적으로 block device driver와 interface하는함수이다(사실direct로 block device driver와 file system이 통신하지 못한다. User 영역과 파일 시스템 사이에 VFS가 있는 것처럼 파일 시스템과 block device driver 사이에는 여러 block device들을 추상화한 generic block device layer가 또 있다). 이 구조체
는 다음과 같이 초기화돼있다.
static struct address_space_operations rkfs_aops = {
.readpage = rkfs_readpage,
.writepage = rkfs_writepage,
.prepare_write = rkfs_prepare_write,
.commit_write = rkfs_commit_write
}
코드 3. rkfs의 address_space_operations
이 함수들은 sys_read과 sys_write가 호출되었을 때, 또는 demand paging에 의해 프로세스 페이지 테이블에 매핑될 때 실제로 사용되며 disk 장치에게 명령을 내리는 함수이다(장치에게 바로 명령하는 것은 아니고 General Block Device Layer에게 명령을 내린다).get을 호출했던 rkfs_fill_super(지난 호 참조) 함수로 다시 돌아가 보자. iget을 통해서 할당받은 inode는 file system의 root inode이다. inode의 나머지 필드 중 중요 필드 몇몇을 채운다.
rkfs_root_inode → i_op = &rkfs_iops; //
set the inode ops
rkfs_root_inode → i_mode = S_IFDIR|S_IRWXU;
rkfs_root_inode → i_fop = &rkfs_fops;
위에서 중요한 필드는 i_ip와 i_fop를 채우는 것이다. 이 테이블들은 앞으로 파일에 관련된 operation들을처리할 때 사용되게 될 것이다. 이 테이블들은 각각 다음과 같다.
static struct inode_operations rkfs_iops = {
lookup: rkfs_inode_lookup
}
static struct file_operations rkfs_fops = {
open: rkfs_file_open,
read: &generic_file_read,
readdir: &rkfs_file_readdir,
write: &generic_file_write,
release: &rkfs_file_release,
fsync: simple_sync_file
}
코드 4. rkfs의 inode_operations와 file_operations
superblock의 root dentry 할당 과정
지금까지 superblock을 할당할 때 커널을 hook하기 위한 rkfs_fill_super 함수의 첫 번째 phase를 살펴보았다. 다음은 superblock의 root dentry를 할당하는 과정이다. 이 과정은d_alloc_root 함수를 통해 이뤄진다.
struct dentry * d_alloc_root(struct inode * root_inode)
{
struct dentry *res = NULL;
if (root_inode) {
static const struct qstr name = { .name = "/",.len = 1 };
res = d_alloc(NULL, &name);
if (res) {
res → d_sb = root_inode → i_sb;
res → d_parent = res;
d_instantiate(res, root_inode);
}
}
return res;
}
코드 5. d_alloc_root
d_alloc_root 함수는 root dentry를 할당한다. 그러기 위해서 먼저 qstr 구조체를““/””로 초기화한 후d_alloc을 호출한다. 이 함수는 dcache에서 dentry를 할당한 후 여러 필드들을 초기화해 반환한다. 할당받은 dentry에 parent(parent는 아규먼트로 패스된다)가 있다면 parent의d_subdirs 리스트에 dentry → d_child 를 이용해 연결한다.
다음 할당받은 dentry를 다음과 같이 sb와 연결한다. 하지만 지금은 a_alloc_root 함수이기 때문에parent는 없다. 함수이름을 보라. root 아닌가? 그러므로 res → d_parent를 자기 자신으로 지정한 후d_instantiate 함수를 호출한다.
d_instantiate 함수는 dentry를 위한 inode 정보를 채우는 함수이다.
이 함수가 호출될 때 반드시 dentry의 d_alias는 비어있어야만 한다. 이 함수는 inode의 i_dentry 연결리스트
에 entry →d_alias를 연결한다. 그런 후 dentry의 d_inode 필드를 inode로 지정한다.
이렇게 해서 get_sb_single 함수가 호출한 gets인 rfks_get_sb 함수가 완료된다. 이제는 get_sb_single 함수의 마무리이다.
sget을 통해 할당받은 superblock의 s_flags에 MS_ACTIVE 플래그를 add하고 do_remount_sb를 호출한다 .
do_remount_sb 함수는 설명하지 않는다.
이번에는 get_sb를 호출했던 do_kern_mount로 돌아가자.
alloc_vfsmnt를 통해서 할당받은 vfsmount의 구조체에 나머지 필드들을 다음과 같이 채운다.
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;
코드 6. do_kern_mount 함수의 iget 다음 부분
do_kern_mount를 호출했던 do_new_mount 함수로 돌아가서 do_add_mount를 namespace의 mount tree에 새로운 vfsmount를 넣는다.
이렇게 해서 파일시스템 마운트에 관해 2회 연속으로 자세히 알아보았다. 너무 구체적으로 들어가 다소어려울 수도 있고, 필자의 설명이 부족해 이해하기 힘들 수 있을 것이라 생각한다. 파일시스템은 커널에서매우 복잡한 부분 중 하나이다. 필자는 개인적으로 리눅스 커널에 정말 관심이 있고 이제 운영체제를 공부하기 시작하려는 분들에게 파일시스템과 메모리 관리시스템을 공부해보라고 추천하고 싶다. 이 부분들은 상당히 복잡하면서도 도전해볼만한 가치가 있는 매우 흥미로운 부분이기 때문이다
출처 : 공개 SW 리포트 12호 페이지 56 ~ 61 발췌(2008년 5월) - 한국소프트웨어 진흥원 공개SW사업팀 발간
'L I N U X' 카테고리의 다른 글
Installing OpenCV 2.2 in Ubuntu 11.04 (1) | 2012.02.09 |
---|---|
/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 |