Linux kernel v4.4에서 간단한 블록 장치 드라이버 만들어보기

커널 콜스택 확인

어플에서 시스템 콜을 호출하면 커널 레벨로 진입하고, 커널 레벨로 진입한 이후의 함수 호출들은 dump_stack() 함수를 써서 확인할 수 있습니다. 드라이버를 만들때도 써봤지요.

mybrd_make_request_fn()함수에 dump_stack()을 넣고 실행해보겠습니다. queue_mode값을 MYBRD_Q_BIO로 바꾸면 콜스택을 조금 줄일 수 있습니다.

diff --git a/mybrd.c b/mybrd.c
index 11fb9af..cf8b717 100644
--- a/mybrd.c
+++ b/mybrd.c
@@ -62,7 +62,7 @@ struct mybrd_device {
 };
 
 
-static int queue_mode = MYBRD_Q_MQ;
+static int queue_mode = MYBRD_Q_BIO;
 static int mybrd_major;
 struct mybrd_device *global_mybrd;
 #define MYBRD_SIZE_4M 4*1024*1024
@@ -295,7 +295,7 @@ static blk_qc_t mybrd_make_request_fn(struct request_queue *q, struct bio *bio)
        pr_warn("start mybrd_make_request_fn: block_device=%p mybrd=%p\n",
                bdev, mybrd);
 
-       //dump_stack();
+       dump_stack();
        
        // print info of bio
        sector = bio->bi_iter.bi_sector;

 이제 커널을 부팅하고 dd 명령을 이용해서 읽기쓰기를 해보면 콜스택이 출력됩니다.

다음은 제가 쓰기를 했을 때 제 환경에서 출력된 콜스택입니다.

[  194.612304] Call Trace:
[  194.612474]  [<ffffffff8130dadf>] dump_stack+0x44/0x55
[  194.612833]  [<ffffffff814fac32>] mybrd_make_request_fn+0x42/0x260
[  194.613306]  [<ffffffff812efc8e>] generic_make_request+0xce/0x1a0
[  194.613761]  [<ffffffff812efdc2>] submit_bio+0x62/0x140
[  194.614158]  [<ffffffff8119fa78>] submit_bh_wbc.isra.38+0xf8/0x130
[  194.614571]  [<ffffffff811a188d>] __block_write_full_page.constprop.43+0x10d/0x3a0
[  194.615098]  [<ffffffff810ac8d5>] ? add_timer_on+0xd5/0x130
[  194.615523]  [<ffffffff811a1e50>] ? I_BDEV+0x10/0x10
[  194.615858]  [<ffffffff811a1e50>] ? I_BDEV+0x10/0x10
[  194.616238]  [<ffffffff811a1c60>] block_write_full_page+0x140/0x160
[  194.616654]  [<ffffffff811a2853>] blkdev_writepage+0x13/0x20
[  194.617107]  [<ffffffff8112344e>] __writepage+0xe/0x30
[  194.617518]  [<ffffffff81123e67>] write_cache_pages+0x1d7/0x4c0
[  194.617942]  [<ffffffff81194a30>] ? __mark_inode_dirty+0x2c0/0x310
[  194.618412]  [<ffffffff81123440>] ? domain_dirty_limits+0x120/0x120
[  194.618798]  [<ffffffff8111a4ee>] ? unlock_page+0x5e/0x60
[  194.619222]  [<ffffffff8112418c>] generic_writepages+0x3c/0x60
[  194.619616]  [<ffffffff81125ed9>] do_writepages+0x19/0x30
[  194.619972]  [<ffffffff8111b2ec>] __filemap_fdatawrite_range+0x6c/0x90
[  194.620456]  [<ffffffff8111b357>] filemap_write_and_wait+0x27/0x70
[  194.620923]  [<ffffffff811a29e9>] __blkdev_put+0x69/0x220
[  194.621325]  [<ffffffff811a3156>] ? blkdev_write_iter+0xd6/0x100
[  194.621723]  [<ffffffff811a2f97>] blkdev_put+0x47/0x100
[  194.622102]  [<ffffffff811a3070>] blkdev_close+0x20/0x30
[  194.622502]  [<ffffffff8116f7e7>] __fput+0xd7/0x1e0
[  194.622851]  [<ffffffff8116f929>] ____fput+0x9/0x10
[  194.623227]  [<ffffffff810702ae>] task_work_run+0x6e/0x90
[  194.623585]  [<ffffffff810021a2>] exit_to_usermode_loop+0x92/0xa0
[  194.624041]  [<ffffffff81002b2e>] syscall_return_slowpath+0x4e/0x60
[  194.624567]  [<ffffffff8188f30c>] int_ret_from_sys_call+0x25/0x8f

다음은 읽기를 했을 때 콜스택입니다.

[  143.111883]  [<ffffffff8130dadf>] dump_stack+0x44/0x55
[  143.113572]  [<ffffffff814fac32>] mybrd_make_request_fn+0x42/0x260
[  143.115554]  [<ffffffff811654cf>] ? kmem_cache_alloc+0x2f/0x130
[  143.117391]  [<ffffffff812efc8e>] generic_make_request+0xce/0x1a0
[  143.118880]  [<ffffffff812efdc2>] submit_bio+0x62/0x140
[  143.120195]  [<ffffffff81127f49>] ? lru_cache_add+0x9/0x10
[  143.121662]  [<ffffffff811a8e85>] mpage_readpages+0x135/0x150
[  143.123364]  [<ffffffff811a1e50>] ? I_BDEV+0x10/0x10
[  143.124759]  [<ffffffff811a1e50>] ? I_BDEV+0x10/0x10
[  143.126213]  [<ffffffff8115f497>] ? alloc_pages_current+0x87/0x110
[  143.128042]  [<ffffffff811a2818>] blkdev_readpages+0x18/0x20
[  143.129704]  [<ffffffff81126493>] __do_page_cache_readahead+0x163/0x1f0
[  143.131482]  [<ffffffff811265eb>] ondemand_readahead+0xcb/0x250
[  143.132949]  [<ffffffff81126959>] page_cache_sync_readahead+0x29/0x40
[  143.134541]  [<ffffffff8111b89a>] generic_file_read_iter+0x46a/0x570
[  143.136190]  [<ffffffff811a31b0>] blkdev_read_iter+0x30/0x40
[  143.137665]  [<ffffffff8116d7d2>] __vfs_read+0xa2/0xd0
[  143.139124]  [<ffffffff8116dfe1>] vfs_read+0x81/0x130
[  143.140442]  [<ffffffff8116ec61>] SyS_read+0x41/0xa0
[  143.141714]  [<ffffffff8188f1ae>] entry_SYSCALL_64_fastpath+0x12/0x71

커널 소스를 따라가면서 한번 콜스택대로 함수가 호출되는지 확인해보세요. 중간중간에 static으로 선언된 함수들은 콜스택에 나타나지 않는다는 것도 알 수 있고, 어떻게 호출되는지 알 수 없는 콜백함수들도 있습니다.

예를들어 blkdev_read_iter함수는 __vfs_read() 함수에 직접적으로 호출되는게 아닙니다. __vfs_read()함수를 보면

ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
    	   loff_t *pos)
{
	if (file->f_op->read)
		return file->f_op->read(file, buf, count, pos);
	else if (file->f_op->read_iter)
		return new_sync_read(file, buf, count, pos);
	else
		return -EINVAL;
}
EXPORT_SYMBOL(__vfs_read);

file 구조체에서 f_op값을 읽는데 file 구조체에 뭐가 들어있는지 그냥 봐서는 알 수가 없습니다.

커널은 인터페이스 디자인을 매우 신경써서 구현합니다. 왜냐면 파일시스템 개발자와 페이지 캐시 개발자가 다르고, 페이지 캐시 개발자와 블럭 레이어 개발자가 다르기 때문입니다. 그냥 다른게 아니라 나라도 다르고, 일하는 시간대도 다르고, 소속도 다르고, 같은건 커널을 개발한다는 것 뿐인 개발자들이 함께 개발하는 코드이므로 모듈화를 잘해야하고 인터페이스 정의도 매우 깐깐하게 합니다. 그래야 서로 다른 레이어/모듈간에 독립적으로 개발될 수 있겠지요. 만약 이쪽에서 개발한걸 다른쪽에서 그대로 써야한다면 수많은 개발자들이 서로의 결과물을 기다리다가 데드락이 걸릴 것입니다.

그래서 커널 소스를 보면 수많은 콜백함수들을 보게됩니다. 그럴때마다 코드를 처음 분석하는 입장에서는 막막할 때가 많습니다. 제가 주로 쓰는 방법은 구조체 이름으로 검색해보는 것입니다. struct file_operations타입의 객체를 어디선가 정적으로 정의하고 있기때문에 file 구조체에서 가져다쓰고있는 것이겠지요. 그러니 일단 커널 소스 전체에서 struct file_operations를 한번 검색해보는 것입니다. 그럼 분명 어디선가 struct file_operations를 정의하고있고, 정의된 객체를 file 구조체에 등록하는 함수가 있을 것입니다. 한번 찾아보겠습니다.

~/work/linux-torvalds $ /bin/grep file_operations * -R | wc
   3195   20545  264089

이번에는 운이 안좋았습니다. 너무 많네요. 3195개의 file_operations 정의가 있습니다. cscope등으로 검색해봐도 너무 많습니다. 이럴때는 별수없이 다른 책이나 구글 등을 통해서 파악할 수밖에 없습니다.

다행히 file_operations 구조체는 많이 쓰이는 것만큼 설명 자료가 많습니다. 우리는 블럭 장치를 분석하고 있으므로 블럭 장치만 생각한다면 결국은 struct def_blk_fops가 우리가 찾는 객체입니다. 자세한 설명은 Understanding the linux kernel 책을 참고하세요. 너무나 오랫동안 많이 사용되는 구조체이므로 자세한 설명이 있습니다.

어쨌든 지금은 def_blk_fop가 file->f_ops에 저장되어있다는 것만 생각하고 넘어가겠습니다. 그러면 결국은 다음과 같은 콜스택을 얻을 수 있게 됩니다.

READ: Sys_read
--> __vfs_read
(--> new_sync_read)
--> def_blk_fops.read_iter = blkdev_read_iter
--> generic_file_read_iter
(--> do_generic_file_read)
--> (find_get_page &)page_cache_sync_readahead
--> ondemand_readahead
--> __do_page_cache_readahead
--> read_pages
--> mapping->a_ops->readpages = blkdev_readpages
--> mpage_readpages
--> submit_bio
--> generic_make_request
--> mybrd_make_request_fn
WRITE: int_ret_from_sys_call
--> syscall_return_slowpath
--> exit_to_usermode_loop
--> task_work_run
--> __fput
--> blkdev_close
--> blkdev_put
--> __blkdev_put
--> filemape_write_and_wait
--> __filemap_fdatawrite_range
--> do_writepages
--> generic_writepages
--> write_cache_pages
--> __writepage
--> mapping->a_ops->writepage = blkdev_writepage
--> block_write_full_page
--> submit_bh_wbc
--> submit_bio
--> generic_make_request
--> mybrd_make_request_fn

그런데 쓰기에서 콜스택이 이상한게 보입니다. Sys_write같이 뭔가 시스템콜같은 함수 이름이 나타나야하는데 뜬금없이 시스템 콜이 끝나는것 같은 int_ret_from_sys_call이라는 함수가 호출됩니다.

여기서 또 커널을 분석할 때 자주 막히는 지점이 나타납니다. 어떤 처리가 synchronouse하게 되면 그냥 함수들의 호출 관계가 바로 나타납니다. 데이터를 읽는 것은 어플에게 당장 데이터를 줘야하므로 처리를 지연시킬 수 없습니다. 바로 그 순간에 데이터를 읽어와야합니다. 그게 메모리에있는 버퍼에서 읽어오던, 장치를 읽어서 읽어오던 데이터를 가져와야합니다. 그런데 쓰기는 다릅니다. 어플은 그냥 쓰기만하면 되고, 커널은 이 데이터를 언제 최종 블럭 장치에 써야할지 결정할 수 있습니다. 데이터를 잃어버리지만 않는다면 당장 블럭 장치에 접근할 필요가 없어집니다. 따라서 이 데이터가 언제 블럭 장치에 들어갈지는 분석하기가 어려워집니다.

비동기 데이터 처리를 설명하자면 제 지식도 한계가 있으므로 지금은 일단 write의 시스템 콜부터 페이지 캐시까지의 콜스택만 따로 뽑아보겠습니다. 이때는 mybrd 드라이버가 호출되는게 아니므로 mybrd 드라이버를 아무리 돌려봐야 알 수가 없습니다. 그냥 read의 콜스택을 보고 코드를 따라가보는게 빠릅니다. blkdev_read_iter라는 함수가 있으면 당연히 blkdev_write_iter라는 함수도 있을거라는 계산을 가지고 코드를 따라가보면 대강 다음과 같은 콜스택을 얻을 수 있습니다.

__vfs_write
--> new_sync_write
--> blkdev_write_iter
--> generic_perform_write 
--> a_ops->write_begin(&write_end) 
--> blkdev_write_begin(&blkdev_write_end) 
--> block_write_begin

읽기에서 얻은 콜스택과 완전히 대칭되는 함수들이 있어서 별로 어렵지 않게 찾을 수 있었습니다.

그리고 콜 스택 중간에 알수없는 a_ops 객체가 나타납니다. 이것은 일단 block_dev.c 파일에 있는 def_blk_aops 구조체라고 생각하고 넘기겠습니다. 뒤에 페이지 캐시를 제대로 분석할 때 제대로 소개하겠습니다.

댓글

댓글 본문
작성자
비밀번호
버전 관리
gurugio
현재 버전
선택 버전
graphittie 자세히 보기