flush_dcache_page와 kmap_atomic Jan 15, 2009

왜 file system code들 중 kmap_atomic과 flush_dcache_page가 있을까?
아는 분의 도움으로 이 문제에 대해서 생각해 볼 기회가 생겼다.

먼저, flush_dcache_page 함수의 용도를 먼저 알아야 한다.
David Miller가 작성한 문서에는 다음과 같이 되어 있다.


void flush_dcache_page(struct page *page)

Any time the kernel writes to a page cache page, _OR_
the kernel is about to read from a page cache page and
user space shared/writable mappings of this page potentially
exist, this routine is called.

NOTE: This routine need only be called for page cache pages
which can potentially ever be mapped into the address
space of a user process. So for example, VFS layer code
handling vfs symlinks in the page cache need not call
this interface at all.

The phrase "kernel writes to a page cache page" means,
specifically, that the kernel executes store instructions
that dirty data in that page at the page->virtual mapping
of that page. It is important to flush here to handle
D-cache aliasing, to make sure these kernel stores are
visible to user space mappings of that page.

즉, 커널이 page cache page에 write를 하는 경우, 또는 user space로 shared/writable로 매핑된 페이지를 읽으려고 하는 경우, 이 함수가 호출된다는 것이다. 또한 이 함수는 user-space로 매핑된 페이지 캐시의 경우에만 호출된다는 것이다. D-cache aliasing 문제를 막기 위함이다. ( cache aliasing 문제에 대해서는 James Bottomley가 작성한 다음 페이지에 잘 설명되있다. http://www.linuxjournal.com/article/7105)

D-cache aliasing 문제란, PIPT(Physical Index Physical Tag)가 아닌 VIVT(Virtual Index Virtual Tag) 또는 VIPT(Virtual Index Virtu Tag) cache 구조에서 발생하는 문제이다. cache의 서로 다른 line에 다른 virtual address로 인하여 같은 physical ram의 데이터가 들어가 있는 경우이다. D-cache aliasing이 발생하는 경우는, user application이 mmap 시스템 콜을 통하여 하나의 파일을 자신의 주소 공간 여러 virtual address에 매핑하는 경우, 또는 서로 다른 프로세스가 하나의 파일에 대해서 writable/shared 상태로 mapping을 서로 다른 virtual address를 통해서 같은 영역을 접근하는 경우 발생할 수 있다.

그럼 위의 flush_dcache_page 함수의 호출 경로 두 곳을 생각해보자.

1. page cache의 page 대하여 write operation을 호출하는 경우

이 경우는 당연히 flush_dcache_page를 호출해야 한다 왜냐하면 해당 page cache의 페이지는 다른 프로세스 또는 같은 프로세스의 다른 virtual address에 매핑되어 있을 수 있다. 그러므로 해당 page에 대한 write와 동시에 해당 page를 매핑하고 있는 모든 cache들을 flush해주어야 한다. 그렇지않은 경우 해당 page를 접근하는 다른 프로세스나 같은 프로세스의 다른 쓰레드는 stale한 데이터를 보게 될 수 있다.

2. shared/writable한 영역의 페이지를 커널이 읽을 때,

커널이 읽고 있는 페이지가 다른 프로세서에서 수행되고 있는 어플리케이션에 의해 write가 될 수 있기 때문에, 읽기 전 flush_dcache_page를 호출하여 cache를 먼저 flush 시킨 후 읽어야 한다.

Cache aliasing문제가 발생하는 것은 user application의 virtual address들로 인하여 하나의 물리 페이지가 여러 다른 virtual address space에 매핑된 경우에사 발생한다는 것을 유념하자.

자 그럼 squashfs_readpage를 이용해서 살펴보자.

다음과 같은 루틴이 있다.

452 for (i = start_index; i <= end_index && bytes > 0; i++,
453 bytes -= PAGE_CACHE_SIZE, offset += PAGE_CACHE_SIZE) {
454 struct page *push_page;
455 int avail = sparse ? 0 : min_t(int, bytes, PAGE_CACHE_SIZE);
456
457 TRACE("bytes %d, i %d, available_bytes %d\n", bytes, i, avail);
458
459 push_page = (i == page->index) ? page :
460 grab_cache_page_nowait(page->mapping, i);
461
462 if (!push_page)
463 continue;
464
465 if (PageUptodate(push_page))
466 goto skip_page;
467
468 pageaddr = kmap_atomic(push_page, KM_USER0);
469 squashfs_copy_data(pageaddr, buffer, offset, avail);
470 memset(pageaddr + avail, 0, PAGE_CACHE_SIZE - avail);
471 kunmap_atomic(pageaddr, KM_USER0);
472 flush_dcache_page(push_page);
473 SetPageUptodate(push_page);
474 skip_page:
475 unlock_page(push_page);
476 if (i != page->index)
477 page_cache_release(push_page);
478 }



이 함수의 인수로 넘어온 page에 대해서 먼저, kmap_atomic을 실행하여 operation을 한후, flush_dcache_page를 호출하는 것을 볼 수 있다. page가 인수로 넘어온 페이지는 새로 할당된 페이지 이기 때문에 굳이 flush_dcache_page를 호출할 필요는 없지만, grab_cache_page_nowait를 통해 반환된 페이지는 page cache의 페이지일수도 있기 때문에 flush_dcache_page를 호출하는 것이다. 의심의 여지가 없다. flush_dcache_page의 문서 내용대로 page cache 페이지에 write를 했으니 flush_dcache_page를 호출하는 것이다.

다음, kmap_atomic을 왜 호출 했는지 살펴보자.
그러기 위해선 kmap_atomic이 어떤 함수인지 먼저 알아야 한다. 이 함수는 커널에게 direct mapping되지 못한 메모리 공간에 커널이 접근하기 위해 필요한 함수이다. 대부분의 config설정 변경을 하지 않은 시스템에서는 커널은 1G의 메모리 주소를 갖는다. 그러므로 램의 1G 까지가 커널의 1G에 direct로 매핑될 수 있다고 생각할 수 있다. 하지만 그렇지는 않다. x86과 같은 경우 896M의 물리 램만이 direct mapping될 수 있다. 그 이유는 많은 서적들에서 얘기하고 있음으로 설명을 생략한다. 그럼 상위 1G 이상의 물리 램의 영역들은 어떻게 접근 할 것인가? 그것을 하기 위한 설비가 kmap_xxx 설비이다. atomic의 postfix가 붙은 것은 해당 매핑중 sleep되어서는 안되기 때문이다.

커널이 접근할 수 있는 1G 중, 896M를 제외한 나머지 영역은 여러 용도로 나누어져 예약이 되어 있다. 그 중 한 영역이 kmap_을 하기 위한 영역이다. 즉, 1G 이상의 메모리 영역을 일시적으로 (896M < x < 1G )주소에 매핑시켜(정확한 값은 아니다. 정확한 값은 arch마다 틀리며 지금 그거까지 찾아보기는 귀찮다. 적당히 하고 넘어가자) opeartion을 수행하고 다른 사용자를 위해 매핑을 끊어 버리는 것이다.

그렇다면 squashfs_readpage에 인수로 넘어온 page가 high memory의 page일 수 있다는 것인가?
=> 그렇다.

그렇다면 왜 high memory일 수 있을까?

=> 이것까지 생각해 봤다면 당신은 kernel newbie는 아니다.
그 이유는 page cache page들은 커널에게 있어서 있어도 그만, 없어도 그만인 페이지이다.
해당 페이지가 없다고 해서 시스템이 동작하는 데 문제가 있는 것은 아니다. 단순히 성능이 좀 떨어지겠지만.. 하지만 low memory(0~896M)의 메모리들은 커널의 동작에 있어 꼭 필요한 자료구조들과 데이터들을 관리하기 위해서 사용된다. 지금과 같이 데스크탑이 램 4G를 꼽는 경우를 생각해보면, 커널이 내부 수행을 위해 관리하는 자료구조들과 데이터들은 더욱 많은 low memory를 사용하게 된다. 그러므로 쓸데 없이 low memory를 낭비하는 것은 커널의 memory pressure를 높이는 꼴이 된다. 이는 시스템의 성능을 크게 저하시키게 된다.

이런 이유로 page cache 페이지들은 가능하면 high memory에 할당을 하려고 하는 것이다. 그러면 어떻게 그것을 알 수 있을까? 그것을 알기 위해서는 squashfs_readpage에 인수로 넘어오는 page 인수가 어떻게 어디서 할당이 되었느냐를 알아야 한다.

자, 찾아보자.

아까 얘기했었다. filemap_fault를 통해 호출이 되었을 것이라고.
아니나 다를까. 가보면 다음과 같은 code가 있다.


error = mapping->a_ops->readpage(file, page);


저 인수로 넘어가는 page는 과연 누가 할당했을까?
그 페이지는 page_cache_read 함수 안에서 page_cache_alloc_cold를 통해 할당되었음을 알 수 있다. 이때 인수로 넘어가는 address_space와 그 함수안에 mapping_gfp_mask를 보면, 결국 address_space의 flags에 어떤 값이 들어가 있느냐에 따라 페이지 할당 위치가 결정된다.

그러면 address_space의 flags에는 어떤 값이 들어가 있는지는 어떻게 아는가?
page_cache_read 함수를 보면 address_space를 구하기 위해서 file->f_mapping을 구해오는 것을 볼 수 있다. 그러므로 file의 f_mapping이 언제 어떻게 설정되느냐를 찾아 봐야 한다.

__dentry_open함수를 살펴보면, inode->i_mmaping 필드로 f->f_mapping을 지정하는 것을 볼 수 있다. 그러면 이번에는 inode->i_mapping 필드가 누가 언제 어떻게 지정하느냐를 또 찾아봐야 한다. 생각해보면 금방 알 수 있다. inode가 만들어질 때 하지 않겠는가!

그렇다면 squshfs 파일 시스템에서 inode를 어떻게 만드는지 살펴보아야 한다.
suqashfs_iget 함수를 살펴보니 알 수 있을 것 같다. iget_locked 함수를 통해서 해당 inode를 찾아보고 시스템에 없다면 get_new_inode_fast 함수를 통해 할당하는 것을 볼 수 있다. get_new_inode_fast 함수는 alloc_inode를 통해 inode를 할당하고 있다.
alloc_inode 함수는 squshfs의 경우에는 kmem_cache_alloc을 통해서 할당받게 되며 inode_init_always 함수를 통해 초기화 하는 것을 알 수 있다. 짐작할 수 있다. 저 함수안에서 초기화 할 것이라고...

mapping->flags = 0;
mapping_set_gfp_mask(mapping, GFP_HIGHUSER_MOVABLE);


위와 같이 초기화 하는 것을 알 수 있다. GFP_HIGHUSER_MOVABLE 함수는 __GFP_HIGHMEM을 포함하고 있다. 결국 해당 페이지는 high memory를 통해서 할당 받을 수 있다는 것이다.

마지막으로 간단히 GFP_HIGHUSER_MOVABLE에 대해서 설명하면, user가 사용하는 페이지들을 주로 high memory에 할당하는 memory fragementation avoidance 메커니즘이다. 즉, 시스템의 huge page 할당을 위해 연속된 큰 order의 페이지가 필요할 때, movable zone의 페이지들을 swap out시키거나 pageout 시켜, 연속된 메모리 공간을 할당하고자 하는 것이다.

1 개의 덧글:

Anonymous said...

...