메모리 대응 입출력과 더불어 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

메모리 대응 입출력(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를 증가하는 이유가 그 이유입니다.

 

이상으로 메모리 대응 입출력에 대한 포스팅을 마치겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

C언어, C++에서는 메모리를 조금 더 쉽게 다루고자하는 함수가 몇가지 존재합니다. 그것들이 무엇이 있는지 설명과 예제를 통해서 알아보도록 하겠습니다. 메모리 관련 함수를 사용하기 위해서는 string.h를 include해야합니다.

 

0) string.h 헤더파일 추가

메모리 관련 함수를 사용하기 위해서 반드시 추가해주세요.

 

1) void* memset(void* source, int value, size_t n)

메모리 주소 source부터 시작해 n만큼 value로 메모리를 채웁니다. return 값은 메모리의 시작주소입니다.

간단하네요. 그러면 예제를 바로 보도록 하겠습니다.

#include <stdio.h>
#include <string.h>

int main() {

	int nums1[5];
	unsigned char nums2[5];
	int i;

	memset(nums1, 10, sizeof(nums1));
	memset(nums2, 10, sizeof(nums2));
	
	for (i = 0; i < 5; i++) {
		printf("nums1[%d] = %d \n", i, nums1[i]);
	}
	
	printf("\n");

	for (i = 0; i < 5; i++) {
		printf("nums2[%d] = %d \n", i, nums2[i]);
	}
}

 

nums1과 nums2를 5개의 배열로 잡는데 자료형이 다르군요. nums1는 int형(여기서는 4바이트), nums2는 unsigned char형(1바이트)입니다. 이후 둘의 메모리를 memset으로 10으로 초기화합니다.

어떤 결과가 나올까요? 두개의 for문에서 nums1과 nums2의 요소들이 전부 10으로 나올것 같은데 그럴까요?

실행결과

nums1[0] = 168430090
nums1[1] = 168430090
nums1[2] = 168430090
nums1[3] = 168430090
nums1[4] = 168430090

nums2[0] = 10
nums2[1] = 10
nums2[2] = 10
nums2[3] = 10
nums2[4] = 10

 

우리의 예상과는 조금은 다릅니다. memset내부에서 실제 10이란 값은 unsigned char로 변환되어 1바이트의 메모리에 그 값을 집어넣게 되는겁니다. 

그래서 4바이트인 int형은 이런식으로 메모리가 set이 됩니다.

00001010 00001010 00001010 00001010 -> 168430090

memset은 1바이트 단위의 메모리를 세팅합니다. 그래서 unsigned char 형의 nums2는 제대로 된 값을 읽을 수 있습니다.

 

2) void* memcpy(void* destination, const void* source, size_t num)

이 함수는 source의 메모리를 destination으로 num만큼 복사합니다. 이 함수에는 source나 destination이 num바이트 이상인지를 검사하지 않으므로 상당히 취약하며 이진데이터를 그대로 복사합니다. 그러니 중간에 NULL이 있는지 없는지 확인하지 않습니다. 아래의 예제를 봅시다.

#include <stdio.h>
#include <string.h>

int main() {

	unsigned char source[8];
	int destination[2];
	int i;

	memset(source, 10, sizeof(source));

	memcpy(destination, source, sizeof(source));

	for (i = 0; i < 2; i++) {
		printf("destination[%d] : %d\n", i, destination[i]);
	}
}

 

source는 8바이트이고 destination도 8바이트입니다. 우선 source를 10으로 전부 채운 후에 destination으로 메모리 복사를 하면 어떤 결과가 나올까요?

 

이전의 memset에서 보았듯 바이트 단위로 메모리가 복사되어 8바이트가 0000 1010으로 복사되는 것이지요.

00001010 00001010 00001010 00001010 -> 168430090

따라서 168430090의 값이 두 번 출력되게 됩니다.

실행 결과

destination[0] : 168430090
destination[1] : 168430090

 

 

3) int memcmp(const void* ptr1, const void* ptr2, size_t num)

메모리의 바이트를 비교합니다. ptr1과 ptr2가 num만큼 비교했을 때 같다면 0, 아니면 다른 값을 리턴합니다. strcmp와 비슷한 리턴 값을 보이는데, unsigned char으로 ptr1이 ptr2보다 크다면 양수, 작다면 음수를 리턴하게 됩니다.

 

#include <stdio.h>
#include <string.h>

int main() {

	unsigned char a[5] = { 0,1,2,3,4 };
	unsigned char b[5] = { 0,1,2,3,4 };

	printf("memcmp(a,b) = %d \n", memcmp(a, b,5));
	
	a[0] = 100;
	
	printf("memcmp(a,b) = %d \n", memcmp(a, b, 5));

	b[0] = 200;

	printf("memcmp(a,b) = %d \n", memcmp(a, b, 5));
}

 

처음 a,b는 정확히 같은 값을 갖고 있으므로 비교했을때 0이 리턴됩니다.

이후 a의 0번째 요소가 100으로 a가 b보다 더 크므로 비교했을때 양수가 리턴됩니다. 

그 다음 b의 0번째 요소가 200으로 a가 b보다 더 작으므로 음수가 리턴되지요.

실행 결과

memcmp(a,b) = 0
memcmp(a,b) = 1
memcmp(a,b) = -1

 

4) void* memchr(void* ptr, int value, size_t num)

memchr은 ptr에서 value를 찾을때 사용합니다. 즉 메모리에서 특정 값을 찾을 때 사용하는 함수입니다. 만약 값이 존재한다면 그 주소를 리턴하고 아니면 NULL을 반환합니다.

 

#include <stdio.h>
#include <string.h>

void printMemory(void *ptr) {
	if (ptr == NULL) {
		printf("메모리에 존재하지 않음\n");
	}
	else {
		printf("메모리에 %d가 존재. addr : %p \n", *((unsigned char*)ptr),ptr);
	}
}
int main() {

	unsigned char arr[5] = { 0,1,2,3,4 };
	unsigned char a = 4;
	unsigned char b = 5;
	int i;

	for (i = 0; i < 5; i++) 
		printf("arr[%d] : %d, %p\n", i, arr[i], &arr[i]);
	
	
	void* ptr=memchr(arr, a, 5);
	printMemory(ptr);

	ptr = memchr(arr, b, 5);
	printMemory(ptr);
}

 

현재 1바이트 배열 arr에는 0,1,2,3,4가 있습니다. 여기에서 a(4)와 b(5)를 찾을 겁니다. a는 존재하니까 ptr이 NULL이 아닌 a가 존재하는 그 주소를 반환하겠지요. b는 존재하지 않으므로 NULL이 반환됩니다.

 

실행결과

arr[0] : 0, 0093F75C
arr[1] : 1, 0093F75D
arr[2] : 2, 0093F75E
arr[3] : 3, 0093F75F
arr[4] : 4, 0093F760
메모리에 4가 존재. addr : 0093F760
메모리에 존재하지 않음

 

4가 있는 주소, 0093F760을 반환하는 것을 알 수 있네요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,