강좌

HOME > 강좌 >
강좌| 리눅스 및 오픈소스에 관련된 강좌를 보실 수 있습니다.
 
Step by Step 커널 프로그래밍 강좌⑥
조회 : 12,345  


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_opdefault_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(&inodei_writecount, 0);

inode i_size = 0;

inode i_blocks = 0;

inode i_bytes = 0;

inode i_generation = 0;

#ifdef CONFIG_QUOTA

memset(&inodei_dquot, 0, sizeof(inodei_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_bdevbd_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 함수는 간단하다. 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 systemroot 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 함수가 호출한 getsrfks_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사업팀 발간

 


[원글링크] : https://www.linux.co.kr/home2/board/subbs/board.php?bo_table=lecture&wr_id=1644


이 글을 트위터로 보내기 이 글을 페이스북으로 보내기 이 글을 미투데이로 보내기

 
한국소프트웨어진흥원 공개SW사업팀