이번주 LWN 에서 내가 흥미로와 하는 내용중 하나인 메모리 관련 기사가 올라와 번역해 본다.
많은 다른 작업들이 그렇듯, 커널에서의 메모리 할당은 유저스페이스에서 이루어지는 것에 비해 훨씬 복잡하게 이루어 진다.
메모리 할당을 위한 커널 API 들은 이런 복잡성을 반영하며 수없이 발전해 왔는데, 이런 복잡성이 곡 높은 수준의 진화가 진행되었다는 것을 의미하지 않는다고 한다. 높은 수준의 진화가 가장 최적의 진화가 아닐 수 있다는 논란이 일고 있다고 한다.
많은 개발자들은 이런 복잡해진 메모리 할당관련 API 들은 Task 기반의 처리에 적합하지 않다고 조심스럽게 이야기 하며 점점 패치의 방식을 바꿔가고 있다고 필자는 전하고 있다.
커널에서 메모리 할당에 대한 복잡성은 어떤 주어진 상황에서 커널이 무엇을 해야 하고, 또 무엇을 할 수 있는지에 기반하여 제한된 상태로 운용되어 진다.
종종 발생하는 예로, 어떤 이벤트를 위한 블록을 기다리는 것이 허락되지 않은 상태의 컨텍스트 안에서 메모리 요청이 이루어 질 경우, 커널은 그에 대한 어떤 sleeping lock 없이 메모리할당을 수행해야한다.
또 가끔, 메모리가 빡빡한 상태에서 더 많은 메모리의 해제를 요구하는 일부 프로세스의 요청이 발생할 경우, 커널은 최후수단으로 메모리 풀에 대한 접근을 허락해야 할 경우가 생기고, 이 경우 반드시 이미 할당되어 있는 메모리 상에서만 접근이 가능하도록 제한을 해야 한다.
이와 같이 커널이 메모리 할등을 위해 그 컨텍스트 상태에 대한 제한 및 제약사항을 모두 관리해야 하는 경우가 있고, 이런 상황에 대한 Trace 를 위해 현재는 모든 메모리 할당 관련 함수들에 GFP flag 를 인자를 사용하는 방식을 취하고 있다고 한다.
예를 들어 kmalloc 의 원형을 보자면, gfp_t flags 라는 것이 보인다 :
void *kmalloc(size_t size, gfp_t flags);
이 GPF flags 라는 플래그는 다양한 제약조건에 대한 코드를 포함한다.
예를들어 GFP_ATOMIC 는 Atomic context 상황에서 수행되고 있으며 Sleep 상태가 허용되지 않는 것을 나타내고 있으며,
GFP_DMA32 의 경우 요청된 메모리가 Device 에서 물리적으로 접근 할 수 있는 32bit DMA 영역에 제한하여 Allocate 되어야 한다는 것을 나타낸다.
이 GFP 에 대한 모든 내용은 <linux/gfp.h> 를 참조하면 된다고 한다. ( 한번 살펴보면 재밌는 케이스들이 상상될 것이다... 아님 말고 ㅎ )
Two Types of GFP
자 여기까지 설명 되었으면 이 GFP 가 문제적 놈이구나 라는 것을 알 수 있다.
GFP 는 크게 두가지 타입이 있다고 한다.
그것은 GFP_DMA32 와 같은 메모리 할당에 필요한 속성(Type) 을 지정하는 속성과,
GFP_ATOMIC 과 같이 컨텍스트 안에서 발생하는 논리적 제약에 대한 속성이다.
특히 이 컨텍스트에 대한 타입이 왜 복잡도를 높이게 되냐면, 이러한 컨텍스트에 대한 내용은 실제 메모리 할당 함수가 수행되기 전까지는 어떤 상황정보를 갖고 있는지 알 수 없기 때문이고, 상위 레벨의 코드에서 하위 레벨 코드에 이런 상황정보를 전달하는 과정에서 또 연결된 함수마다 context 타입의 GFP 인자를 삽입해 관리해야 한다는 점이다.
필자는 아래와 같은 USB 관련 함수를 예를 들었다.
int usb_submit_urb(struct urb *urb, gfp_t mem_flags);
이 함수보다 상위함수들은 이 함수의 gfp flag 를 반드시 확인해야 하고, 추적한뒤, 이 조건에 맞게 메모리를 할당 할 수 있는 함수를 호출하게 되는데,
이 과정에서 다시 또 컨텍스트의 변화를 감지하여 또 그 변화된 GFP flag 에 맞춰 다시 또 정확한 함수를 찾아 전달해야 한다는 복잡성이 발생한다는 것... (복잡하지? ㅎㅎㅎ 참조할게 자꾸 꼬리를 문다는 것이다.)
일부 사람들은 이 상황은 첫단추부터 잘못끼운것과 같은 근본적 문제가 있으며, 연결된 함수들을 호출하는 방법보다 실행중인 스레드에 컨텍스트 추적정보를 직접적으로 추가하는 방법이 더 낫다고 주장하고 있다고 한다.
GFP_NOFS
호출중인 Context 에 대한 특정한 플래그중 하나이며, 메모리 할당자가 어떠한 파일시스템 관련 코드를 호출하지 않도록 하는 플래그인데, 이는 메모리 할당자가 메모리를 더 할당하기 위해 Dirty page 에 대한 Writeback 을 막는 역할을 한다.
즉, 파일시스템 관련 함수에서 다시 파일시스템을 호출하면서 발생하는 Deadlock 상태를 막는 한편, 메모리를 다시 쓰고 더 많은 메모리를 할당하는 과정에서 발생 할 수 있는 Stack Overflow 를 방지하기 위해 사용되는 녀석이다.
문제는 이 플래그가 사용되는 함수만해도 1,300 개가 넘는 인스턴스가 발견되었고, Michal Hocko 는 2016년 열린 Linux Storage, Filesystem, and Memory-Management summit 에서, 이 플래그로 인해 불필요하게 너무 많은 메모리 제약이 발생하여 시스템 성능에 악화를 초래한다고 주장한 것이다.
물론 "미안하지만 안전우선이야" 라는 선택을 하는것을 비난할 수는 없다지만, 보다 더 정확하게 상태추적이 가능한 플래그나 방법을 사용하여 이 통합적 제약을 가져오는 플래그를 제거해 나아가야 한다고 주장하였다.
그래서 그는 filesystem 관련 호출이 반드시 되면 안되는 컨텍스트, 관련 호출이 제약될 필요가 없는 컨텍스트를 구분하는 플래그를 추가하여 개선하는 방식을 발표했고, XFS 와 EXT4 를 기준으로 적용되고 있다고 한다.
이는 이미 존재하는 함수에서 영감을 얻었고,
unsigned int memalloc_noio_save(void); void memalloc_noio_restore(unsigned int flags);
Ming Lei 가 2013 년 커널 3.9 에 추가한 것이라고 한다.
이런식으로 메모리 관리에 대한 코드는 보다 세분화 되고 정확한 동작을 위하여 컨텍스트에 대한 정보를 함수 인자를 통해 전달하지 않고, 해당 스레드의 컨텍스트와 함께 저장되는 방향으로 바꿔야 할 필요가 있으며, 이는 기존의 빠른 진화가 아닌 기존 형태를 바꾸기 위한 변화에 속하므로 느린 방식의 진화에 속한다.
기존 GFP flags 들에 대한 의존을 모두 바꾸는 것에는 큰 시간이 걸릴 수 있으며, 이 새로운 API 로 변경하기 위해 많은 시간과 노력이 들어 단순한 스크립트로는 수행 될 수 없다.
필자는 컨텍스트를 명시적으로 추적하도록 하는 일은 더 많은 일을 만드는 것이라고 생각 할 수 있겠지만, 이는 더 나은 미래를 위한 필수 불가결한 과정이며, 모두 완료가 될 경우, 커널의 메모리 할당 API 가 추구하는 방향과 더 잘 일치할 것이라고 생각하며,
커널의 메모리 할당 관련 개발에 걸린 시간은 고작 25년 밖에 주어지지 않았으므로, 우리가 이제와서 또 변화하는 것은 전혀 놀라운 일이 아니라고 생각한다... 며 필자는 글을 마무리 했다.
----
즉, 기존의 빠른 발전을 위해 여러가지 추가해 왔던 메모리 관리 관련 기능들을 예로 들면서,