[리눅스] 메모리 대응 입출력 기본 설명 및 사용법 예제(mmap, memcpy, munmap, msync)
메모리 대응 입출력과 더불어 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.
https://reakwon.tistory.com/233
메모리 대응 입출력(memory-mapped I/O)
메모리 대응 입출력 기법은 디스크의 파일을 메모리에 한 부분인 버퍼에 대응을 시켜 읽거나 쓰는 동작을 할 수 있는 기법입니다. 그래서 파일 입출력을 위한 read나 write를 사용할 필요가 없습니다. 메모리 대응 입출력을 사용하려면 커널에 파일을 메모리에 대응(mapping)시키겠다고 정보들을 알려줘야합니다. 그렇기 위해서 우리는 mmap함수를 비롯해 몇가지 함수를 사용합니다. 그래서 마지막에는 메모리 대응 입출력을 활용하여 파일을 copy하는 프로그램을 만들어보도록 하겠습니다.
1. mmap 함수
mmap은 아래와 같이 정의가 되어있습니다. 주어지는 정보들은 아래와 같습니다.
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
- 반환값 : 성공시 메모리의 맵핑된 주소를 반환합니다. void* 형으로 자료형에 따라 알맞게 변환할 수 있습니다. 실패시에는 MAP_FAILED를 반환합니다. MAP_FAILED는 (void*) -1을 의미합니다.
- addr : 메모리 대응 영역의 시작주소를 뜻합니다. 보통 0을 넣어 적절한 주소를 반환시킵니다. 이때 addr은 시스템의 가상 메모리 페이지의 크기 배수로 설정이 되어야한다는 것을 주의하시기 바랍니다.
- length : 얼마만큼의 메모리를 초기화 할 것인지 크리를 정합니다. 이때 offset 이후부터라는 점을 기억하시기 바랍니다. offset은 아래의 설명이 있습니다.
- prot : 메모리를 보호하는 방식을 설정합니다. 아래의 표와 같이 정의가 되어있습니다. 인수의 이름은 꽤나 직관적이니 뭐 굳이 자세한 설명은 필요없을 것 같네요.
prot 인수 | 설명 |
PROT_READ | 메모리 영역 읽기 가능 |
PROT_WRITE | 메모리 영역 쓰기 가능 |
PROT_EXEC | 메모리 영역 실행 가능 |
PROT_NONE | 메모리 영역 접근 불가 |
- flags : 메모리 대응 영역에 특성을 설정합니다. 여기서는 세가지만 설명하는데 리눅스 혹은 다른 구현에서는 많은 옵션이 존재할 수 있습니다.
flag 이름 | 설명 |
MAP_FIXED | 반환값이 정확히 전달받은 addr의 주소와 같은데, 이 flag는 이식성을 저하시키므로 사용하지 않는 것을 권합니다. |
MAP_SHARED | 영역에 대응된 파일을 수정합니다. 그러므로 메모리의 수정이 일어나면 파일의 수정이 일어납니다. 아래의 MAP_PRIVATE나 MAP_SHARED 중 하나를 설정해야합니다. |
MAP_PRIVATE | 유추가 되죠? 영역에 대응된 파일을 수정하는 MAP_SHARED와는 달리 메모리 대응 영역만 수정이 일어날뿐 실제 파일에는 수정이 일어나지 않습니다. 이를 비공유 복사본이 생성된다고 합니다. |
- fd : 대응시킬 파일 서술자를 뜻합니다. fd는 어디서 얻어오죠? open에서 얻어올 수 있습니다. 네, mmap을 사용하기 전에 file을 열어야합니다.
- offset : 파일의 시작에서 부터 얼마만큼 떨어진 영역을 메모리에 대응시킬것인가를 뜻합니다. 이떄 offset은 가상 메모리 페이지 크기의 정수배여야합니다.
> 가상 메모리 페이지 크기
그렇다면 가상 메모리 파이지 크기는 어떻게 알 수 있을까요? 아래의 코드로 얻어올 수 있습니다. page_size = sysconf(_SC_PAGESIZE); |
2. munmap
메모리 대응 영역은 프로세스가 종료되면 자동으로 해제가 되는데, munmap을 사용하여 직접 해제시킬 수 있습니다.
#include <sys/mman.h>
int munmap(void *addr, size_t length);
- 반환값 : 성공시 0, 실패시 -1을 의미하며 errno에 에러 정보가 저장됩니다.
- addr : 해제할 메모리 영역의 주소를 지정합니다.
- length : 얼마만큼의 영역을 해제할 것인지 size를 지정합니다.
3. memcpy
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
메모리를 copy하는 함수입니다. 단순히 설명하면 src의 내용을 dest로 n만큼을 복사합니다. 성공시 dest의 주소를 반환하여 줍니다.
4. msync
MAP_SHARED으로 메모리를 대응시켰을 경우 일정 시간이 지나면 파일로 내용을 동기시키긴 하지만, msync 함수를 호출하면 변경된 내용이 바로 실제 파일로 적용이 됩니다. 단, MAP_PRIVATE는 파일에 변경사항이 저장되지 않는 다는 점을 기억하세요.
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
addr과 length는 mmap의 내용과 같습니다. flags의 내용은 대충 아래 표와 같습니다.
flag | 설명 |
MY_ASYNC | 페이지들이 호출 반환 후에 방출되게 하고 싶으면 이 인수를 사용합니다. |
MY_SYNC | 쓰기들이 실제로 완료된 후에 호출이 반환되게 하려면 이 flag를 사용합니다. 둘 중 하나는 설정해야합니다. |
5. 메모리 대응 입출력을 이용한 파일 복사 프로그램
cp 명령 아시죠? cp src dst 를 하게 되면 src의 내용이 dst라는 파일로 복사가 됩니다. 아래는 이를 메모리 대응 입출력을 통해서 구현해보는 소스코드입니다.
#include <stdio.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char *argv[]){
int srcfd, dstfd; //src 파일 서술자, dst 파일 서술자
void *src, *dst; //src 메모리 주소, dst 메모리 주소
size_t copysz; //다음 copy할 메모리 내용 size
struct stat sbuf;
off_t fsz = 0; //다음 읽기, 쓰기를 기록할 위치(offset)
long page_size; //시스템의 PAGE SIZE
if((srcfd = open(argv[1], O_RDONLY)) < 0) {
fprintf(stderr, "can't open %s for reading \n",argv[1]);
exit(1);
}
if((dstfd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0777)) < 0){
fprintf(stderr, "can't open %s for writing\n", argv[2]);
exit(1);
}
//file 사이즈 얻기 위한 용도
if(fstat(srcfd, &sbuf) < 0){
fprintf(stderr, "fstat error\n");
exit(1);
}
if(ftruncate(dstfd, sbuf.st_size) < 0){
fprintf(stderr, "ftruncate error\n");
exit(1);
}
page_size = sysconf(_SC_PAGESIZE);
printf("page_size : %ld\n", page_size);
while(fsz < sbuf.st_size){
if((sbuf.st_size - fsz ) > page_size)
copysz = page_size;
else
copysz = sbuf.st_size - fsz;
//src 주소 설정
if((src = mmap(0, copysz, PROT_READ, MAP_SHARED, srcfd, fsz))
== MAP_FAILED){
fprintf(stderr, "mmap error for input \n");
printf("error : %s\n",strerror(errno));
exit(1);
}
//dst 주소 설정 , 여기서 MAP_SHARED를 MAP_RPIVATE로 바꾸면? dst파일에 저장되지 않는다.
if((dst = mmap(0, copysz, PROT_READ|PROT_WRITE, MAP_SHARED, dstfd, fsz)) == MAP_FAILED){
fprintf(stderr, "mmap error for output\n");
exit(1);
}
//src -> dst로 내용 복사
memcpy(dst, src, copysz);
//메모리 해제
munmap(src, copysz);
munmap(dst, copysz);
//복사한 내용만큼 다음 메모리 위치를 이동시킬 offset 증가
fsz += copysz;
}
exit(0);
}
argv[1]은 복사할 파일 이름, argv[2]는 복사하여 나온 파일 이름입니다. ftruncate 함수로 출력 파일의 크기를 설정해줍니다.
ftruncate는 아래와 같이 정의되어 있습니다. 간단히 설명하면 파일 서술자 fd의 길이를 length로 잘라버린다는 겁니다. 즉, 크기 지정한다고 보면 됩니다.
#include <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t length);
이후 sysconf로 현재 나의 컴퓨터의 PAGE SIZE를 가져옵니다. 아까 말했듯이 addr과 offset은 PAGE SIZE의 배수여야한다고 했습니다.
while 루프 안에서는 파일을 끝까지 복사할때까지 계속 반복합니다.
while(fsz < sbuf.st_size){
//... 파일 복사 ...//
}
while 루프 안의 로직은 어렵지 않습니다 .아까 배운 mmap을 통해서 src와 dst의 메모리를 대응 시킨 뒤에 memcpy 함수로 src의 내용을 dst의 내용으로 복사합니다. 그 이후 munmap으로 메모리를 해제하네요. 그 다음 page_size를 넘을만큼 그다면 다시 page_size 이후의 내용을 읽어야겠죠? fsz를 증가하는 이유가 그 이유입니다.
이상으로 메모리 대응 입출력에 대한 포스팅을 마치겠습니다.