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에게 "이 파일을 내 메모리와 연결해줘!" 하고 부탁하는 것이다.
디스크에 있는 외부 파일을 마치 내 프로세스 안에 원래 있던 메모리처럼 사용할 수 있게 해주는 기능이군요