Egloos | Log-in
F/OSS study
F/OSS study
[Linux] pageflags로 살펴본 메모리의 일생
Linux: 2.6.39-rc3
arch: x86_64


커널은 물리 메모리를 (페이지 단위로) 관리하기 위해 page 구조체를 사용한다.
page 구조체에는 해당 페이지 프레임에 관한 정보가 저장되어 있는데
그 중 가장 중요한 정보는 바로 페이지가 현재 어떠한 상태에 있는지를 나타내는 flags 필드이다.
(추가적으로 flags 필드에는 시스템의 메모리 구성에 따라
해당 페이지가 속한 node, zone, section 등의 정보도 함께 기록될 수 있다.)

이러한 상태 정보는 pageflags라는 열거형 (enum) 상수로 정의되어 있으며, PG_ 라는 prefix로 시작하지만
이를 직접 접근하기 보다는 PageLocked() 와 같은 보조 함수(accessor)를 통해 접근하게 된다.

여기에서는 시스템에서 메모리가 사용됨에 따라 이러한 flags 정보가 어떻게 설정되고
그에 따라 다른 page 구조체의 필드들이 관리되는 방식을 대략적으로 살펴보고자 한다.

먼저 시스템 부팅 과정에서 모든 페이지들은 PG_reserved 상태로 초기화된다. (memmap_init_zone)
이 후 커널이 필요한 여러 페이지들을 (bootmem 할당자를 통해) 할당하고 나면
남은 모든 페이지들은 버디 시스템으로 관리하기 위해 해제된다. (__free_pages_bootmem)

이 단계를 거쳐서 PG_reserved 상태로 남아있는 페이지들은 커널이 중요하게 사용하는 것들이므로
(커널 코드/데이터 영역, memmap, 장치가 사용하는 자원 등) 절대 해제되거나 다른 용도로 사용될 수 없다.

버디 시스템에 포함된 페이지들은 필요할 때 언제나 할당 가능한 페이지들이며
연속된 페이지의 양에 따라 2^order 단위로 관리한다.
원래는 해당 페이지가 버디 시스템에 속해 있음을 나타내기 위해 PG_buddy 플래그가 사용되었지만
2.6.38에서 transparent huge page 기능이 도입되면서 PG_compound_lock 플래그가 추가됨에 따라
PG_buddy 플래그를 추가하고 대신 page 구조체의 mapcount 필드의 값을 통해 이를 판단하도록 하였다.
(하지만 소스 코드 상의 호환성을 위해 여전히 PageBuddy 함수는 존재한다.)

참고로 mapcount 필드는 해당 페이지가 사용자 프로세스의 주소 공간에 매핑된 수를 저장하는 것이므로
사용하지 않는 (free) 페이지가 매핑되는 것은 불가능하기 때문에 안전하게 사용할 수 있다.
또한 버디 시스템에 속한 페이지의 기본 참조 카운트 값은 (count 필드) 1로 초기화되고
(사실 버디 시스템 내의 페이지들은 의미 상 참조 카운트 값이 0이어야 하겠지만
어차피 메모리 할당이 일어났을 때 1로 초기화 시킨 후 반환해야하니 미리 증가시켜둔 것 같다..)
해당 order의 가장 첫 페이지의 private 필드에는 order 값을 저장해 둔다. (set_page_order)

이제 커널이 사용하기 위해 페이지를 할당받는 경우를 생각해보자.
커널은 많은 경우 slab 할당자를 통해 (kmalloc) 메모리를 할당받게 되는데
이렇게 할당받은 페이지들은 PG_slab 플래그가 설정되며 항상 커널 내부적으로만 사용되고
사용 중에는 page reclaim으로부터 제외되므로 별다른 참조 카운트 관리 없이 사용된다.
또한 이 때에는 page 구조체의 많은 필드들을 다른 용도 (slab, inuse, freelist)로 사용한다.

그 외 커널이 사용하는 경우는 직접 버디 시스템을 이용하여 페이지를 할당받은 후
별도로 페이지에 대한 참조를 관리하게 되며 이러한 경우에 대해서는 별다른 플래그가 필요치 않다.

많은 경우 페이지는 디스크에 저장된 파일의 내용을 메모리에 저장하기 위한 페이지 캐시로 사용된다.
페이지 캐시는 시스템의 성능 향상을 위해 커널이 내부적으로 사용하는 메모리 영역이므로
(물론 사용자 프로세스가 파일 매핑을 통해 페이지 캐시에 직접 접근할 수 있기는 하다)
언제든 회수하여 다른 용도로 사용될 수 있어야 하기 때문에 이를 효율적으로 관리하기 위해
LRU (Least Recently Used)라는 방식의 리스트를 통해 관리하게 된다.

따라서 페이지 캐시로 이용되는 페이지들은 PG_lru 플래그가 설정되며,
page 구조체의 lru 필드는 각 memory zone 별로 존재하는 LRU 리스트에 연결된다.
사실 이 LRU는 active/inactive의 두 리스트로 구현되어 있으며 페이지 캐시에 저장된 데이터에
접근할 때 마다 inactive 리스트 (초기값)에서 active 리스트로 옮겨진다.

하지만 매번 파일에 접근할 때마다 리스트를 이동하는 것은 그리 적절치 않으므로
두 번의 접근이 일어났을 때 리스트를 이동하도록 하였고 이러한 정보를 저장하기 위해
PG_active와 PG_referenced라는 두 플래그를 이용한다. (mark_page_accessed)

페이지 캐시 내의 데이터에 접근하는 경우에는 항상 추가적인 참조 카운트를 유지하고 있어야
사용 도중 해당 페이지가 회수되어 버리는 상황을 피할 수 있다. 하지만 정상적인 경우
페이지 캐시를 검색하여 페이지를 찾으면 자동으로 참조 카운트를 증가시켜 주기 때문에 (find_get_page)
실질적으로는 페이지를 사용한 후에 참조 카운트를 감소시키는 것만 신경쓰면 된다. (page_cache_release)

페이지 캐시의 내용은 항상 파일의 최신 변경 사항을 담고 있어야 하며
이를 위해서 PG_uptodate 플래그가 사용된다. 이 플래그는 디스크에서 파일의 내용을 읽어오거나
새로운 데이터를 페이지에 쓰는 경우에 설정되며, 디스크 I/O 시에 오류가 발생하면 지워질 수 있다.
또한 이러한 디스크 오류가 발생한 경우에는 PG_error 플래그가 추가로 설정된다.

페이지 캐시에 파일의 내용이 존재하지 않거나 페이지가 PG_uptodate로 설정되지 않아서
디스크 접근이 필요한 경우 혹은 페이지 캐시의 내용을 변경하는 경우에는
먼저 페이지에 lock을 걸고 접근해야 하며 이를 위해서 PG_locked 플래그가 사용된다.
(페이지 lock은 PG_locked 플래그를 이용한 bit lock으로 구현된다.)

(파일에 대한 최초 접근을 제외한) 일반적인 read는 디스크 접근 없이 페이지 캐시의 내용 만을 이용하는데
이 때는 페이지에 대한 lock을 걸지않기 때문에 (단순히 참조 카운트 만 증가시킨다)
동시에 수행 중인 write가 있다면 아직 완료되지 않은 상태의 데이터를 읽을 수도 있다.
물론 여러 write 들 간에는 별도의 mutex를 이용하여 원자적인 실행을 보장한다.

write가 일어나면 페이지의 내용이 변경되었으므로 이를 나타내기 위해 PG_dirty 플래그를 이용한다.
PG_dirty 플래그가 설정되면 페이지 캐시의 radix tree에도 DIRTY 태그를 추가하여
이 후 적절한 시점에 writeback이 일어나도록 해 준다. (set_page_dirty)

writeback을 위해서는 먼저 해당 페이지가 디스크 상에 존재하는 위치 (block) 정보를 알아내야 한다.
기존의 파일 뒤에 새로운 내용을 추가한 경우라면 이 시점에 디스크 블록이 할당될 수 있다.
블록 정보를 알아내거나 새로운 블록을 할당하는 작업은 파일 시스템 별로 제공하는
get_block 콜백 함수를 통하게 되며 이렇게 필요한 블록이 할당되었다면
이를 표시하기 위해 PG_mappedtodisk 플래그가 설정된다.

또한 디스크 블록 정보를 유지하기 위해 별도의 buffer head가 사용되는데
이는 page 구조체의 private 필드를 이용하기 때문에 PG_private 플래그가 추가적으로 설정되며
또한 참조 카운트도 증가하게 된다. (attach_page_buffers)

실제로 writeback이 진행되는 중에는 PG_writeback 플래그가 설정되는데
이 때에는 도중에 페이지의 내용이 바뀌지 않도록 먼저 페이지에 lock을 걸고
PG_writeback 플래그를 설정한 후에 lock을 해제해야 한다.
만약 페이지의 내용을 변경하기 위해 lock을 획득한 경우라면 반드시 PG_writeback 플래그를 확인하여
writeback이 이루어지는 도중에 데이터를 수정하지 않도록 해야 한다. (wait_on_page_writeback)
또한 writeback을 진행하기 전에는 PG_dirty 플래그를 지운다. (clear_page_dirty_for_io)

페이지가 사용자 프로세스에게 직접 할당되는 경우는 메모리 매핑을 통해 관리한다.
파일 매핑의 경우 페이지 캐시를 함께 이용하므로 추가적인 플래그가 필요치는 않지만
매핑이 생성될 때 마다 (사실은 매핑 생성 후 실제로 페이지에 접근했을 때)
page 구조체의 mapcount 필드의 값이 증가한다. (page_add_file_rmap)

익명 매핑인 경우 최초에는 모든 내용이 0일 것이므로 ZERO_PAGE라고하는 특별한 페이지를 사용한다.
이 후 write 접근 시 page fault handler가 새로운 페이지를 할당하고 PG_swapbacked 플래그를 설정한 뒤
mapcount를 증가시키고 LRU 리스트에 추가한다. (add_page_new_anon_rmap)

이 때 주의해야 할 점은 파일 매핑인 경우라도 비공유(private) 매핑인 경우는
write 발생 시 변경된 데이터는 더 이상 파일에 속하지 않으므로 익명 매핑된 페이지와 같이 처리되며,
익명 매핑인 경우라도 공유(shared) 매핑인 경우는 tmpfs 상에 /dev/zero에 대한 가상의 파일을 생성하여
내부적으로는 파일 매핑과 같이 처리한다는 사실이다. (shmem_zero_setup)

(파일) 매핑된 페이지의 dirty 상태를 관리하는 것은 커널에게는 골치 아픈 작업일 수 있다.
왜냐하면 사용자 프로세스에게 페이지에 대한 write 권한을 주고 나면
커널 입장에서는 해당 프로세스가 실제로 데이터를 변경했는지 알아내기가 어렵기 때문이다.

이를 위해 msync() 시스템 콜을 제공하여 커널이 해당 매핑의 변경 상태를 검사하도록 하는데
MS_SYNC를 인자로 넘긴 경우 단순히 매핑한 파일 내용 전체를 디스크에 기록한다.
MS_ASYNC의 경우 실제 디스크에 기록하는 대신 페이지 테이블의 dirty 비트를 검사하여
해당 페이지에 대한 쓰기 접근이 있었는지 검사한 후 PG_dirty 플래그를 설정하여
이 후에 writeback 시 해당 페이지를 디스크에 기록하도록 했었다.

하지만 현재는 msync() 시에 이러한 페이지 테이블을 직접 검사하는 대신
페이지에 대한 write 권한을 없애서 (page fault를 통해) 쓰기 접근이 일어나는 경우
커널이 바로 알 수 있도록 변경하여 page fault 시 해당 페이지에 PG_dirty 플래그 설정 후
다시 페이지 테이블의 write 권한을 주는 방식으로 페이지의 dirty 상태를 추적하고 있다.
또한 writeback이 일어나고 나면 PG_dirty 플래그를 지우면서 다시 페이지 테이블의 write 권한을 없애서
이 후의 쓰기 접근을 알 수 있게 하고 있다. (page_mkclean)

커널에서 사용 중인 일부 페이지와 같이, 사용자에게 할당된 페이지들도 시스템의 메모리가 부족해지면
필요에 따라 회수되어 다른 용도로 사용될 수 있다. 이 때 원래의 내용은 디스크 (파일/swap)에
기록되기 때문에 나중에 해당 프로세스가 원래의 내용에 접근하게되면 다시 디스크에서 읽어와야 한다.

이러한 작업은 디스크 접근 속도로 인해 매우 느리게 수행되는데
사용자 프로세스 중에도 매우 긴급히 처리되어야 하는 작업이 있을 수 있으므로
이 경우 메모리 회수로 인해 데이터 접근 속도가 느려지지 않도록 하기 위해
특정 데이터 (페이지)를 메모리 상에 고정시키도록 (회수되지 않도록) 요청할 수 있다.
mlock() 및 mlockall() 시스템 콜이 이러한 용도로 사용되며
이를 통해 lock이 걸린 페이지들은 PG_mlocked 플래그가 설정된다.

또한 (비슷한 경우로) 어떠한 이유에서건 해당 페이지를 회수할 수 없는 경우 (!page_evictable)
(대부분 mlock이 걸린 페이지거나 혹은 ramfs 등에 할당된 페이지인 경우에 해당한다)
이를 나타내기 위해 PG_unevictable 플래그가 추가적으로 사용된다. (add_page_to_unevictable_list)

정상적인 경우라면 페이지를 모두 사용하고 난 후에 다시 버디 시스템으로 반환하게 되지만
시스템 내에 사용할 수 있는 여유 메모리가 부족하게 되면 메모리 회수 과정이 시작된다.
일반적으로 커널이 사용하고 있는 메모리들은 회수 대상에서 제외되지만
페이지 캐시를 비롯한 몇몇 캐시들은 회수 대상에 포함되며
경우에 따라 사용자에게 할당한 메모리 중 일부를 회수해야 하는 경우도 있다.

페이지 캐시 및 파일 매핑의 경우 만약 페이지의 내용이 수정되었다면 (PG_dirty 플래그 확인)
이를 먼저 디스크에 기록해야 하는데 이 때 위에서 간단히 살펴본 writeback 과정이 진행되지만
메모리 회수로 인한 writeback 임을 나타내기 위해 추가로 PG_reclaim 플래그가 설정된다. (pageout)

익명 매핑의 경우 수정된 내용은 swap 영역에 기록되는데
이 때 디스크 기록 도중 동일한 (공유된) 페이지에 대한 접근을 처리하기 위해
먼저 해당 페이지를 swap cache에 넣어두며 이를 나타내기 위해 PG_swapcache 플래그를 설정한다.
이 경우 PG_private 플래그가 설정되진 않지만 page 구조체의 private 필드에
해당 페이지가 저장된 swap 영역의 위치를 기록한 후 참조 카운트를 증가시킨다. (__add_to_swap_cache)

이렇게 대략적으로 페이지가 사용되는 과정을 살펴보았다.
마지막으로 CONFIG_PROC_PAGE_MONITOR 옵션이 선택된 경우 커널은 페이지의 상태 정보를
/proc/kpageflags 파일을 통해 읽어볼 수 있도록 해준다. (pageflags와 정확히 동일하지는 않다)

이는 /proc 디렉터리의 다른 파일과 달리 텍스트가 아닌 바이너리 형태의 정보를 제공해주며
각 페이지 별로 64비트 크기의 플래그 정보를 읽을 수 있지만 각 비트의 값을 정의한
kernel-page-flags.h 헤더 파일이 공개되지 않았기 때문에 해당 파일을 직접 읽어보거나
커널 소스 내에 포함된 Documentation/vm/pagemap.txt 파일을 참조해야 한다.

또한 커널 소스의 Documentation/vm/page-types.c 파일에는 /proc/kpageflags 파일을 이용하는
예제 소스가 공개되어 있으므로 이를 참조하는 것도 좋은 방법일 것이다.

by namhyung | 2011/04/14 23:07 | Kernel | 트랙백 | 덧글(7)
트랙백 주소 : http://studyfoss.egloos.com/tb/5512112
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by 불타는심장 at 2012/02/16 12:09
매번 감사합니다. 여기서 많이 배워갑니다^^
좋은글 많이많이 부탁드립니다. - 열혈학생 드림
Commented by namhyung at 2012/02/18 12:50
도움이 되셨다니 다행입니다. ^^
Commented by sheadroad at 2012/02/28 11:22
안녕하세요. 항상 많이 배우고 있는 나그네 입니다.
궁금한 점이 있어 질문 드리게 되었습니다.

page flags 일생 중 0x0 으로 세팅되어 있는 상황도 있는지요 ?
덤프를 확인해 보면 적지않은 page 들의 flag 가 0x0 으로 세팅되어 있었습니다.

제가 지금 풀고 있는 문제는..slab 할당된 page를 free_block으로 free 하려다가 발생한 kernel panic 입니다만
free 하려는 page 가 slab flag 가 세팅되지 않고 0x0으로 세팅되어 있어 bugon 함수가 불렸습니다.

왜 그렇게 됐는지 확인해 보려 합니다만;
도무지 누가 그랬는지 알 수가 없어서요^^;

초면에 이렇게 긴 질문을 드리게 되어 죄송합니다.
Commented by namhyung at 2012/02/29 15:43
안녕하세요.. 답글 남겨 주셔서 감사합니다.

지금은 메모리 쪽을 안본지 조금 되서 정확히 기억이 나지 않습니다만... ;;
제 시스템에서 page-types 프로그램을 실행해 본 결과
대부분의 페이지들의 flags 값이 0x0 인 것을 보니
buddy system에 있는 페이지들이 이렇게 표시되는 것 같습니다.

buddy system은 order 별로 이루어지는 페이지 블록 중에서
첫번째 페이지 만을 이용하여 관리하기 때문에 나머지 페이지들은 0 값을 가질 것입니다.
더구나 2.6.38 이후라면 PG_buddy도 flags를 이용하지 않기 때문에 첫번째 페이지도 0이 될 듯합니다.

또한 말씀하신 문제의 경우 저도 비슷한 경험이 있는데
SLAB이 아닌 SLUB 할당자를 이용하는 경우 (buddy system과 비슷하게) 첫번째 페이지를 제외한
나머지 페이지들에는 PG_slab 비트가 설정되지 않아서 실수로 free_page() 등을 호출한 경우
free_pages_check()에서 이를 검출하지 못했던 경험이 있었습니다.
SLAB의 경우는 모든 페이지가 해당 비트가 설정되어서 bad_page 경고를 보여주었습니다.
혹시 이와 같은 경우가 아닌지 검토해 보시기 바랍니다.

그럼 수고하세요..
Commented by 익룡 at 2013/03/13 10:30
감사합니다. 많이 배워갑니다.
Commented by 국대컴공 at 2013/04/18 10:24
요새 리눅스에 대해서 공부를 하고 있는데, 정말 쉽지 않은 것 같습니다 ㅠㅠ 확실히 진입장벽이 있긴 하군요 ㅠㅠ 맨날 핼로우월드만 하다가.. 정말 좋은 포스팅입니다~ 공부 많이하고갑니다~
Commented by morenice at 2013/11/26 17:26
흐름을 이해할 수 있는글이라 좋았어요. 감사합니다.

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶

카테고리
General
Application
System
Kernel
Book
Tips
태그
glibc SMP synchronization C algorithm build emacs vcs script memory kernel perf compiler awk CAaQA3 patch bash scm x86 gcc documentation computer-architecture block-layer CARM linux elf git blktrace sed binutils
전체보기
이글루 파인더

최근 등록된 덧글
informsi yang bagus dan b..
by agen qnc at 06/22
informasi yang bagus dan b..
by agen qnc at 06/22
informasi yang bagus dan b..
by agen qnc at 06/22
최근 등록된 트랙백
Tod's Ferrari Homme
by Tods Pas Cher,Kodak did ..
Mocassin Femme
by Mocassins Homme, I got so..
natural garcinia cambogia
by
rss

skin by jiinny


X