TECH 으로 돌아가기
TECH HACKER NEWS 8분 읽기 22 READS

잃어버린 malloc(): 초기 유닉스의 메모리 관리에서 배우는 것

잃어버린 malloc(): 초기 유닉스의 메모리 관리에서 배우는 것
SOURCE IMAGE · HACKER NEWS

한 개발자가 블로그 글 'The lost malloc()'에서 50여 년 전 유닉스 소스 코드를 뒤지며 초기 C 프로그램이 메모리를 어떻게 다뤘는지 추적했다. 그 여정은 1973년 11월에 나온 유닉스 4판(V4)에서 시작한다. C 언어 자체는 그 전해인 1972년 2판에서 처음 등장했지만 본격적으로 쓰이지는 않았고, V4에 이르러서야 커널 대부분이 (글쓴이의 표현으로는 '중세 C'로) C로 작성됐다. 흥미로운 사실은 V4가 담긴 테이프가 영영 사라진 줄 알았다가 작년에야 벨 연구소의 창고를 정리하던 중 발견됐다는 점이다. 오늘날 POSIX 환경에서 쓰는 많은 요소가 이미 이 시점에 자리 잡고 있었다.

이름 없는 아레나와 두 개의 malloc

당시 C '컴파일러'는 하나의 단일 프로그램이 아니라 C 코드에서 오브젝트 파일을 얻기까지 순서대로 연결해 쓰는 여러 도구의 묶음이었다. 그중 원시적인 최적화 도구(V5에서는 'C object code improver'로 불렸다)의 소스에는 전형적인 선형 할당기(linear allocator)가 들어 있다. 크기를 정렬하고, 영역이 충분한지 확인한 뒤 모자라면 sbrk()로 힙을 늘리고, 실패하면 그냥 종료해버린다. 그래서 호출하는 쪽은 매번 NULL을 검사할 필요가 없었고 실제로도 하지 않았다. sbrk()로 힙을 확장해도 기존 포인터가 무효화되지 않는다는 장점이 있었다. 그리고 메모리를 해제하지 않았는데, 모든 것이 프로그램 수명 내내 존재하므로 해제할 이유가 없었기 때문이다.

정작 글쓴이가 제목에 담은 '잃어버린 malloc'은 데니스 리치의 디렉터리에 있던 malloc.c다. 여기에는 오늘날의 malloc()/free()의 조상인 malloc()과, f가 아닌 m을 쓴 mfree()가 들어 있다. 다만 인터페이스가 다르다. 두 함수 모두 첫 인자로 mp('map')를 받고, 호출하는 쪽은 coremap과 swapmap이라는 객체를 넘긴다. systm.h를 보면 이 둘은 각각 코어(메모리)와 스왑 자원을 나타내는 평평한 메모리 영역이다. 즉 이 malloc()과 mfree()는 명시적으로 지정된 메모리 영역 위에서 동작했고, mfree()는 해제할 객체의 크기까지 인자로 받았다.

영역 기반 인터페이스의 실무적 함의

글쓴이는 이 인터페이스가 전통적인 malloc()/free()보다 훨씬 낫다고 평가한다. 어떤 라이브러리가 이런 식으로 메모리 영역을 받아 쓴다면, 그 라이브러리에 위임한 영역 전체를 통째로 버리는 것만으로 모든 자원을 회수할 수 있다. 메모리를 구획으로 나눌 수 있으니 힙 손상 버그의 파급도 줄고, 서드파티 코드가 할당할 수 있는 메모리 양을 손쉽게 제한할 수도 있다. 이는 오늘날 아레나(arena) 할당기가 내세우는 장점과 정확히 겹친다. 실제로 'arena'라는 용어 자체가 1975년 봄에 나온 6판(V6)의 어셈블리 소스에 처음 등장한다.

다만 글쓴이는 데니스와 켄이 이런 이점을 염두에 두고 설계했다기보다는 하드웨어 제약 때문에 메모리를 '코어'와 '스왑'으로 나눴다고 본다. 존 라이언스의 유명한 6판 주석서를 보면, 두 종류의 자원마다 사용 가능한 영역 목록을 '맵'(coremap 또는 swapmap)으로 유지하고, 함수에 적절한 맵 포인터를 넘겨줌으로써 malloc과 mfree 자체는 자신이 다루는 자원의 종류를 알 필요가 없게 했다. 그리고 이때의 할당 알고리즘은 선형 할당이 아니라 주소순으로 정렬된 블록 리스트에서 최초 적합(first-fit)으로 찾고 인접 블록을 병합(coalesce)하는 방식이었다. 이것이 mfree()가 존재하는 이유이기도 하다.

매크로, 그리고 new의 뿌리

이 글의 또 다른 축은 'sizeof 계산을 매번 호출부에 늘어놓는 실수'를 당시 사람들은 어떻게 다뤘는가다. 대부분의 코드는 그냥 malloc/alloc을 직접 부르며 크기 계산과 오류 검사를 호출부마다 인라인으로 반복했다. 그러다 1979년 1월의 7판에서 C++의 new와 비슷하게 할당을 감싸는 매크로가 등장하는데, 흥미롭게도 포트란 77 컴파일러와 make에서만 발견된다. 두 프로그램 모두 스튜어트 펠드먼이 작업한 것이어서, 당시 여러 사람이 각자의 방식으로 같은 문제에 부딪히고 있었음을 짐작하게 한다. 7판은 assert() 매크로가 처음 등장하고 man 페이지가 대폭 늘어난 판이기도 하다.

1985년 2월의 8판에 이르면 할당한 메모리를 기본적으로 0으로 초기화해주는 매크로가 나타나고, 우리가 아는 전통적인 malloc()/free() 인터페이스로 전환된 것으로 보인다. 같은 8판에는 '중세 C++'인 Cfront도 들어 있다. 그렇다면 스트롭스트룹이 이 매크로들에서 new를 착안했을 법도 하지만, 그는 'A History of C++'에서 new가 Simula에서 왔다고 밝힌다. 할당과 생성자 호출을 한 번에 처리하는 new 연산자는 Simula에서 빌려왔고, 가비지 컬렉터에 의존하지 않기 위해 delete를 짝으로 두었다는 것이다. 그는 new가 free store 사용을 더 편하고 덜 위험하게 만들어 사용이 늘었고, 그 결과 malloc()이 실제 시스템에서 가장 흔한 성능 병목이 됐다고도 적었다.

뿌리를 더 거슬러 올라가면, new는 1967년의 Simula 67, 더 정확히는 1964년 SIMULA I 보고서에 나오는데 거기서는 문법적 모호성을 해소하는 용도였고 나머지 경우엔 가독성을 위한 장식이었다고 한다. Simula는 다시 1962년 SIMSCRIPT의 영향을 받았는데, SIMSCRIPT에는 new 대신 지정한 양만큼 저장 공간을 할당하고 식별 번호를 부여하는 CREATE 문이 있었다. 이후 9판의 롭 파이크가 만든 Sam 에디터 헤더에는 일반화된 new() 매크로가 보이지만, 여전히 거의 모든 프로그램은 malloc을 직접 부르며 호출부마다 크기 계산과 오류 검사를 되풀이했다. 글쓴이는 그것이 얼마나 오류를 부르기 쉬운지 알면서도 관성 때문이었으리라 추측한다.

이 글의 가치는 '옛날에는 이랬다'는 향수가 아니라, 반세기 전 코드가 지금의 아레나·영역 기반 할당 논의를 이미 품고 있었다는 점을 짚는 데 있다. 명시적 영역을 넘겨받아 통째로 회수하고 사용량을 구획별로 제한하는 설계는 현대 C/C++ 실무에서도 메모리 안전성과 자원 관리를 끌어올리는 유효한 접근이다. 물론 이는 개인 블로거의 코드 고고학적 해석이며 당시 설계자들의 의도를 직접 증언으로 확인한 것은 아니라는 한계는 감안해 읽을 필요가 있다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://napcakes.nekoweb.org/posts/malloc/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...