병가받고 한달 뽀지게 쉬는중인데 중간중간 대전같은데 끌려가서 일하고 있느라,
나름 또 바빠버려서 정말 관심있는 커널 MM 에 대한 공부가 중단되었었다..
정리하는 셈 치고, 또한 최근 P 모사에서 OOM killer 가 동작되버려 kdump 조차 안된 상황(?)을 보니,
급 급해져서 살짜쿵 내용좀 정리해 본다...
사실 올 초반부터 OOM killer 의 성능을 향상시키기 위한 수 많은 토의가 있었다..
시작은
fork-bomb 를 검색하는 것에서 시작하였으나, 용자 '
David' 의 주도로 인해,
반지원정대 못지않은 큰 여정이 되어버렸다...
Andrew 는
David 의 패치를 검토하고, 또 싸우기도 많이 하면서 결국
2010-6-11 자로
패치를 적용시킨다... 이 패치에 대한 스토리와 내용은 너무 길고 길어서 여기서 단순간에 쓰기엔 어려우니
다음으로 미루도록 하고, 다시 말하지만, 내가 나름대로 연구해본 OOM killer 의 동작 과정에 대해 보기로 한다.
코드를 설명해야 하는 것을 최대한 배재하려고 하나, 나역시 프로그래머라고는 할 수 없는
시스템엔지니어일 뿐이기에, 좀 꼬일수도 있을것 같다... 대충 보자.. 쿨럭
우선 OOM killer 가 호출되는 조건 ( 시스템 리턴밸류 및 시스템콜 ) 에 대해서 짚어보자.
1. mm_fault_error
이 mm_fault_error 함수가 호출 되는 시점은 시스템의 page fault handler 가 호출되어,
fault 된 페이지를 처리하려고 하다가 필요한 만큼의 메모리를 할당시키지 못하고 반환되어 버릴때
호출이 되며, 일반적으로는 먼저 OOM killer 가 페이지할당 요청이 발생했을대,
그 함수 (어플이든 시스템이든) 내에서 먼저 동작하여 페이지를 회수해야하지만,
그게 이루어지 지지 못했을 경우 그 error 가 page fault handler 로 전달 되면서,
최종적으로 mm_fault_error 가 호출되 OOM killer 의 동작이 수행되는 것이다.
밑에 설명할 다른 OOM 과는 다른데, 이 mm_fault_error 를 통한 OOM kill 의 경우
OOM 상황의 context 를 보관 할 수 없게 되어 어떤 상황에 어떤 문맥에서
발생이 된건지 알아낼 수가 없게된다. (니미럴)
이유는 page fault handler 에서 페이지처리를 하려고 하는 도중
handler 에서 allocate 에러가 전달되어 영향받아버리는 것이므로,
컨텍스트를 정상적으로 보존하지 못하기 때문일 것 같다..
이런 경우가 OOM killer 가 동작했을 텐데 그냥 행업 혹은 패닉이 뜬것 같이 보일때,
또는, OOM kill 이 왜 동작한건지 나타나질 않을때
또는 P 사 kdump 중 OOM 호출로 덤프가 종료되는 경우등..
2. __alloc_pages_may_oom
실제 대부분 알고 있는 움킬러가 바로 이녀석이 호출될때 나오는 녀석들인데,
OOM kill 의 동작은 여기서 제대로 알 수 있다.
우선 Linux 에서 page alloacator 의 페이지 할당 실패가 발생할 경우엔 kswapd 를 활성화 시켜,
background 에서 reclaim 을 수행시킨다. (일정 정책에 의한 소프트한 메모리 reclaim 방법)
그래도 페이지 할당이 실패할 경우 direct reclaim 을 수행한다.
뭐 디테일하게 설명하면 한도 끝도 없으니 다시 그냥 움킬러의 동작 맥락위주로 돌아가기로 한다.
이렇게 direct reclaim 마져 실패하게 되면, 드디어 커널은 __alloc_pages_may_oom 을 호출한다.
이녀석은 memory zonelist ( 현재 할당을 요구한 메모리존 이하의 모든 존 ) 에
OOM lock 을 만들어 holding 을 시도한다.
물론 OOM lock 을 통한 memory zone holding 은 모든 존에서 전부 메모리가 부족하여 OOM killer 가
동작중이라면, 실패하게 되어 진행을 못해 HANG 상황까지 불러오게 된다... ;P
성공하게 되면 OOM 함수 ( out_of_memory ) 를 호출하며,
OOM 함수에서는 constrained_alloc 을 호출하여 페이지 할당 요청에서
강제된(constrain) 부분이 있었는지 파악하게 되며,
constrain 에 속하는 경우는 다음 두가지 경우가 해당된다.
1. MPOL_BIND 등의 메모리 정책 때문에 강요및 강제할당이 된 것이 있는지,
2. CPUSET 의 softwall 에 의한 constrain
( cpuset 을 통해 cpu 사용영역을 소프트적으로 제한한 것을 뜻함 )
1번의 경우에는 oom_kill_process 가 호출되어 current 에 들어가 있는 프로세스를 kill 하게 되며,
2번의 경우 혹은 constrained_alloc 이 없다면 __out_of_memory 함수를 호출하여,
대상 프로세스들을 검색하게 된다.
여기서 __out_of_memory 내부 플로우까지 설명해야 하나 고민을 무지하게 했으나....
그냥 하자... 하다 길어지면 짤르지 머.. 쿨럭.
이 녀석은 victim process 를 찾기 위해, select_bad_process를 호출하는데,
여기서 물론 커널 스레드와 init 프로세스들은 제외된다. (우리가 일반적으로 보는 OOM 동작임)
이녀석이 재밌는건 대상 프로세스를 찾기위해 프로세스를 검색하는 과정인데,
이미 종료중인 프로세스나, 종료가 되서 종료처리중인 프로세스가 있을 수 있고, 이 경우에는
reserved memory 를 사용하도록 되어 있는데, 이 영역에 존재하는 프로세스가 있다면,
스캐닝을 종료하고 반환된다.
왜냐면, 종료 과정에서 KILL 시그널을 처리하기 전까지, 일련의 작업을 마무리하기위해
메모리가 필요할 수도 있는데, 이때 사용 되는 것이 reserved memory 영역이고,
이것을 사용해버리거나 다른 것들을 이 영역에 올려버리게 된다면,
실제 종료하려던 것이 메모리부족으로 종료가 제대로 안되 livelock 상태에 처하게 되는걸 막기위함이다.
또한 KILL 시그널을 처리하고 있는 그 과정 ( do_exit ) 에서도 이 경우에는 구지 죽일 필요가 없는
자진반납하려고 하는 녀석이기때문에 따로 죽이지 않겠다는 것이다.
아까 설명했듯이 이게 아직 current 에 존재한다면 좀더 빨리 종료되는걸 돕기위해 reserved memory 를
사용 할 수 있도록 하여 종료에 대한 보장을 해주는 역할또한 한다.
위에 말한 두가지 종료에 대한 익셉션 ( 종료중이거나, 종료처리중인 ) 외의 검색되는 프로세스들은
oom score 를 통해 점수계산으로 죽는 우선순위가 결정되 죽여버린다.... ;P
이 점수를 계산하는 것은 badness 함수에서 처리된다.
badness 는 다음과 같은 룰에 따른다.
1. 최소한 양의 작업만 잃게 되는 것.
2. 많은 메모리가 반환 되는 것.
3. 메모리 Leak 으로 의심되는 녀석을 판단하여 죽일 것.
4. 메모리 확보를 위해 죽이는 프로세스의 수가 최소가 되도록 할 것.
5. 사용자도 죽었으면 하는 프로세스를 죽일 것. (쿨럭)
이 점수들은 oom_adj 를 통해 정할 수 있다. ( Kernel Document 의 filesystem/proc.txt 참조 )
Badness Score (뭔가 좀 받기싫은 점수다 ㅠㅠ) 들은 total_vm 크기를 기준으로 계산된다 ( 중요 )
이 점수계산에는 몇가지 플래그들이 있는데,
예를들어 OOM_DISABLE 플래그나 KSM 혹은 swapoff 시 최대점수를 무조건 주는 플래그 등
다 설명할 필요는 없을 것 같고, 다시 점수계산 알고리즘을 추가 설명하도록 한다.
total_vm 크기 기준에, 몇가지 프로세스 상태 플래그들, 또 한가지 쓰레드에 대한 계산인데,
task 의 자식프로세스들중 (자꾸와따가따해 흑) 현재 Task 와 mm 이 같지 않은 task 들의
total_vm 를 2로 나눈 값을 검색된 프로세스의 (victim) 점수에 더하여,
fork 를 많이 한 fork-bomb 를 처리하기 위한 계산을 한다.
게다가 단순 fork-bomb 처리외에도 child process들을 parent 보다 먼저 죽이기 위해서다. (멋지다!!!)
자 이젠 CPU 자원에 대한 점수계산이다.
이것은 간단하게 프로세스의 실행 시간과 CPU 사용율 을 기준으로 점수처리를 한다.
오래 실행되었을수록, 많은 일을 하고 있을 가능성이 높고,
CPU 사용량이 많을수록 메모리자원 소비가 크다고 볼 수 있기 때문이다.... nice 영역도 고려된다.
이렇게 고려된 각각의 point 들을 가지고, odd_adj 에 가감되어
가장 큰값을 기준으로 최종 죽을 녀석들이 결정되는 것이다!!!
이렇게 결정된 victim process 들은 다시 위에 1번에서 말한 oom_kill_process 에 의해 처형이 집행되고
이 함수는 자식들부터 먼저 죽이며 자식들을 죽일 수 없을 경우 부모를 죽인다. (잔인한가 ㅠㅠ)
실제 죽이는 것은 oom_kill_task 가 한다.
(간혹 덤프디버깅하다보면 혹은 sysrq 켜서 테스트할때 스택 뿌려보다보면 보인다.)
oom_kill_task 는 __oom_kill_task 시스템콜을 통해, SIGKILL 을 보내게 되며,
프로세스의 time_slice 를 HZ (헤르츠단위) 로 올려 reserved memory 영역 (종료를 위해 사용되는 영역)
사용을 허용할 수 있도록 특정 플래그를 지정시켜 종료시킨다.
자..이렇게 길게 얘기한걸 다시 줄여서 함수 플로우로만 설명해 보겠다.
그림그릴까?? 가만..
그냥 스케치북에.. 쿨럭
1. page allocator 실패시
2. mm_fault_error 발생 시
위와 같은 함수와 동작 원리를 알고 이제 깊이 파다보면, background reclaim, direct reclaim 등이 있고,
reclaim 중에서는 또 write back 등 I/O 관련되는 것들이 주루룩 나오고,
그 전에 일단 커널 메모리 매핑이나 vm 매핑........ 엄청 쏟아진다... 머리 깨지며,
한달 동안 이것만 붙잡고 밥안먹으면서 살아도 모르겠다 ㅠㅠ
아참 참고로, 6월 적용된 패치로 인해 앞으로의 커널에서의 움킬러의 동작 방식.
즉, 메모리 reclaim 등에 대한 일부 부분들은 바뀌게 되었으니 참조로만 알아두길 바란다.
PS. 사실 내가 공부하면서 이해하고 있는 부분을 재정리 한 글이기때문에 부족함이 많을 수도 있다..
가르침 주실 분들은 언제든지 가르침 주시길 희망함 :)
스프링노트에 다시 좀 정리해서 플로우별로 정리했다...
http://mirr.springnote.com/pages/6484805