tiny 웹서버를 구현하다보면 등장하는 Mmap()
.
정적 컨텐츠를 제공해주는 serve_static()
함수 내부에 있다.
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp;
char filetype[MAXLINE];
char buf[MAXBUF];
get_filetype(filename, filetype);
int n = snprintf(buf, MAXBUF,
"HTTP/1.0 200 OK\r\n"
"Server: Tiny Web Server\r\n"
"Connection: close\r\n"
"Content-length: %d\r\n"
"Content-type: %s\r\n\r\n",
filesize, filetype);
if (n < 0) {
fprintf(stderr, "Header formatting error!\n");
return;
}
Rio_writen(fd, buf, n);
printf(">>> Response headers:\n%s", buf);
fflush(stdout);
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize); // ⬅️ 여기 ‼️
}
처음엔 '메모리랑 뭔가 관련 있겠지?' 하고 넘겼는데,
이 함수, 생각보다 꽤 똑똑하고 중요한 일을 하고 있었다.
이 글에서는 mmap()
이 도대체 왜 쓰이고,
어떻게 동작하고,
정말 효율적인 방법이 맞는지 를 알아보려고 한다.
우리가 흔히 쓰는 read()
함수는 어떤 파일을 열고 데이터를 읽어들이는 함수이다.
이 함수는 파일의 내용을 복사해서 내 메모리 버퍼에 "갖다놓는" 역할을 한다.
📌 버퍼(Buffer)란?
버퍼 = 데이터를 잠깐 담아두는 '그릇'이자 '중간 저장 공간'
char buf[100]; // 이게 버퍼! read(fd, buf, 100);
fd
: 파일에서buf
: 메모리 버퍼 (여기에 100바이트를 복사해서 넣음)read
: "야, 이 파일에서 100바이트 읽어서 buf에 담아줘!"디스크 → 메모리로 옮길 때,
"잠깐 담아두는 공간(버퍼)"이 꼭 필요하다.**
그런데 mmap()
은 방식이 완전히 다르다.
파일을 메모리에 '복사'하는 게 아니라, '붙인다'.
이게 바로 핵심이다.
mmap()
은 파일을 가상 메모리 주소 공간에 연결해준다.
복사 없이, 그냥 포인터처럼 파일을 내 메모리 어딘가에 딱 연결해주는 방식이다.
mmap()
을 호출하면,
운영체제(OS)가 파일의 내용을 메모리 주소 공간에 '연결'해준다.
내 코드에서는 그 주소를 그냥 배열처럼 접근할 수 있다!
char *src = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, fd, 0);
// 이제 src[0], src[1], ..., src[filesize-1] 직접 읽을 수 있음!
❗ 이때 src에 있는 건 복사된 내용이 아니라,
파일의 진짜 내용이 들어 있는 가상 메모리 주소다.
(OS가 그 주소에 파일 내용을 "붙여준" 거다.)
📄 [파일] → [커널 버퍼] → [유저 버퍼]
(read는 두 번 복사)
📄 [파일] ↔ [가상 메모리 주소]
(mmap은 주소 연결만 함)
Tiny 서버는 정적 컨텐츠(예: .html, .jpg, .gif)를 처리할 때 이렇게 했다.
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); // 파일을 mmap으로 메모리에 연결!
Close(srcfd);
Rio_writen(fd, srcp, filesize); // 이 주소에서 바로 클라이언트에게 전송
Munmap(srcp, filesize);
즉, 파일을 메모리에 올려서 클라이언트에 바로 전송해주는 구조다.
read는 파일 → 커널 → 사용자 버퍼로 데이터를 복사해야 한다.
하지만 mmap은 파일을 메모리에 그대로 연결해주기 때문에 복사가 없다.
필요한 페이지만 불러오는 구조다.
그리고 운영체제는 read-ahead로 그 다음 페이지도 미리 읽어둔다.
그래서 순차적으로 접근할수록 빠르고 효율적이다.
운영체제는 mmap된 영역을 페이지 캐시에 넣고 관리한다.
여러 프로세스가 같은 파일을 mmap하면 캐시도 공유돼서 훨씬 빠르게 동작한다.
mmap
으로 연결된 메모리를 처음 접근할 때 OS는 page fault를 발생시켜서
디스크에서 해당 페이지를 읽어온다.
그래서 mmap
이 느려지는 순간은 딱 ‘처음 읽을 때’이다.
하지만 이후에는 캐시에 들어가 있어서 빨라진다.
사실 mmap()
도 read()
와 마찬가지로 시스템 콜(System Call)이다.
즉, 단순한 함수 호출이 아니라 운영체제 커널에 직접 요청하는 무거운 작업이다.
사용자가 직접 파일에 접근할 수 없기 때문에,
OS에게 "이 파일을 내 메모리와 연결해줘!" 하고 부탁하는 것이다.
디스크에 있는 외부 파일을 마치 내 프로세스 안에 원래 있던 메모리처럼 사용할 수 있게 해주는 기능이군요