본문 바로가기

L I N U X

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



파일시스템마운트

 

이번 강좌는 지난 강좌에 이어 파일시스템 마운트 과정의 나머지 부분에 대해 알아보기로 한다내용 일부는 지난 호와

관련되기 때문에 지난 호와 같이 봐야 이해가  것이라 생각한다.

 

 _ 김민찬 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 blockblock device 관련돼있고, block device driver bd_inode_backing_dev_info 필드를 가지고 있다면 해당 block device bd_inode_

backing_dev_info 필드를 address_space backing_dev_info 필드로 초기화한다 default actionoverriding 한다고 생각하면 된다.

다음으로 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 함수는 간단하다. inodei_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 아규먼트로 패스된다) 있다면 parentd_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사업팀 발간