지난 포스팅 막바지에 설명한 Memory Mapping에 대해 조금 더 알아보자. Memory Mapping의 가장 기본적인 원리는 Paging이다. Process의 Logical Address Space가 Page로 나뉘어 있고, Physical Address Space엔 그 Page 크기와 같은 크기의 Frame이 있다.
이때, 이 Logical Address와 Physical Address 사이의 Mapping엔 각 Process마다 개별적으로 존재하는 Page Table이 관장한다고 했다. 아래 그림과 같이 말이다.
Page Table은 모든 Process에게 각각 존재하고, Process Context Switch 시 교체 대상이다. ★
Paging 기법이 적용된 Logical Address의 Format은 다음과 같다.
"Page Number + Page Offset"
Page Number p : Page Table의 Index로, Physical Memory 내의 각 Page의 Base Address를 포함한다. ★★
Page Offset d : 어떤 Frame에서부터 '얼마만큼 떨어져 있느냐'를 의미한다. 즉, Base Address와 합쳐져서 Physical Memory Address를 나타낸다. ★★
ex) Page Offset의 비트 개수가 14개라면, 2^14까지 표현이 가능하고, 이는 16K이다. Offset이 16K까지 있다는 것은 무엇을 의미할까?
ex) 반대로, Page Size가 4K이면 2^12이므로 Offset을 위한 비트 개수가 12개라는 것이다. ★
ex) 그렇다면, Page Number p를 표현하는데에 20비트가 쓰이면, 이것은 무슨 의미일까?
2^20까지 표현할 수 있다. 그렇다. 이는 곧 Page Table의 Index 개수이자, Page의 개수, Frame의 개수를 나타내는 것이다. ★★★
만약, Frame Number라는 Value를 4Bytes에 저장한다고 하면, 이 Page Table의 사이즈는 몇일까?그렇다. 1M x 4Bytes = 4MB가 되는 것이다. ★★★★★
~> 우리는 이러한 Translation 기법이 존재한다는 것을 기억해야한다. 추후 Page만을 제대로 다룰 때 다시 자세히 설명하도록 하겠다. (Paging은 다다음 Chapter의 Dynamic Relocation 개념 이후에 다룰 것임)
이번엔 잠시 Memory Virtualization 설명을 멈추고, 그 Abstraction 위에서 동작하는 API들에 대해 알아보자.
Heap Segment를 관리하는 함수 (in libc)
Heap Segment의 Size를 변경하는 System Call (APIs)
~> malloc과 같은 C-Library Function이 brk, sbrk, mmap과 같은 Operating System Call을 이용해 Heap Segment를 관리하는 것이다. ★
~~> 즉, malloc 그 자체는 System Call이 아니다!!!
#include <stdlib.h>
void* malloc(size_t size)
malloc : Heap 공간에 메모리 영역을 할당한다.
size_t size : 할당하고자 하는 Memory Block의 Size (Byte 단위)
할당에 성공하면 해당 Memory Block의 시작 주소를 가리키는 void Type의 Pointer를 반환한다.
할당에 실패하면 NULL Pointer를 반환한다.
Process Address Space 상에 Memory를 할당하는 것이다. Process 입장에선 Physical Memory를 모른다. 그냥 내가 이미 가지고 있는, 할당받은 주소 공간 내에서, 그중에서 Heap이라는 공간에 메모리 영역을 잡는 행위일 뿐이다.
즉, Process가 malloc 등으로 Dynamic Memory를 할당(요청)하더라도, 이는 사실 추가적인 Physical Memory를 얻는 것이 아니다. ★★★
Virtual Address Space의 특정 공간을 새롭게 사용할 수 있는 '권한'을 얻는 것일 뿐이다. ★★★
이때, 이 '할당된 공간'은 Logical View에선 Contiguous(Linear)하지만, Physical View에선 그러한 연속성이 Guarantee되지 않는다. ★
또한, malloc 시 '논리적으로' 만들어진 공간은 그 직후에 바로 Physical Memory에 맵핑되는 것이 아니다. ★★★ (현대적 Process Loading 개념. 추후 다룰 것)
~> 변수 a는 Local Variable이므로 Stack Segment에 존재한다. ({ } 내에 선언했다 가정)
~> Newly Allocated Memory Block in Heap Segment는 100Bytes의 크기이다.
=> 그런데, 이때, 이 할당 공간은 Physical Memory에 바로 Mapping되지 않는다. 왜냐? 무쓸모일 수 있으니까!
--> "*a = 10;"과 같이, 실제 해당 공간에 대한 Process의 Access가 이뤄질 때 비로소 Physical Memory Mapping이 시작된다. ★★★
여담) malloc 시 sizeof 연산자를 사용하는 것은 일종의 Convention이다. 이때, sizeof에는 아래와 같은 주의 사항이 존재한다. 사실 중요한 것은 아니지만, 그래도 알아두면 좋을 내용이다.
/* Example 1 */
int *x = malloc(10 * sizeof(int));
printf(“%d\n”, sizeof(x));
/* Example 2 */
int x[10];
printf(“%d\n”, sizeof(x));
~> Example1의 출력은 무엇일까? 그렇다. 4이다. 왜냐? x는 Pointer Variable이고, Pointer는 32-Bit Machine 기준 항상 Word Size, 4Bytes이므로! sizeof(x)는 결국 Pointer Variable의 Size를 묻는 것이다.
~> Example2의 출력은 무엇일까? 4? 아니다. 40이다. 여기서 x는 Array의 Name으로 Pointer Constant이다. 포인터 상수는 해당 포인터가 가리키는 전체 공간을 나타낸다. 이 경우엔 sizeof(x)가 x라는 배열의 전체 사이즈가 된다. (주의) ★★★
#include <stdlib.h>
void free(void* ptr)
free : malloc에 의해 할당된 메모리 공간을 해제한다.
void *ptr : malloc에 의해 할당된 Memory Block을 가리키는 Pointer이다.
성공/실패 여부 상관 없이 Return은 없다. ★
참고로, 이미 알다시피, malloc하지 않은 공간에 대한 free나, 이미 free한 공간에 대한 free는 System적인 Error를 만들어낼 수 있는 위험한 행위이다.
한편, malloc의 반환 값을 받아들였던 User Stack 내의 Local Pointer는 해당 공간 free 이후엔 Invalid Pointer가 된다.
char *src = “hello”;
char *dest;
strcpy(desst, src);
~> src Pointer는 선언과 동시에 초기화되어 "Hello\0"라는 문자열을 가리키지만, dest Pointer는 초기화되지 않았기 때문에 그냥 Stack(또는 Data) Segment에 4Byte로만 존재한다.
=> 이때, 이 4Byte에 대한 4Byte 이상 크기의 Data를 입히려고 시도하면 Segmentation Fault가 일어나 Program이 죽게 된다.
--> 이 Code를 아래와 같이 수정해 Segfault를 방지할 수 있다.
char *src = “hello”;
char *dest = (char*)malloc(strlen(src) + 1);
strcpy(desst, src);
~> 이때, NULL Character를 고려해서 strlen에 1을 더하고 있음을 기억하자.
char *src = “hello”;
char *dest = (char*)malloc(strlen(src));
strcpy(desst, src);
~> 이때, 만약, 위와 같이 strlen에 1을 더하는 것을 깜빡하면, '상대적으로 작은 메모리 공간에 큰 메모리 공간 Data를 복사'하는 행위가 이뤄진다.
~~> Heap 상황 등에 따라 Program이 제대로 동작할수도, 아닐수도 있다.
int *x = (int*)malloc(sizeof(int));
printf(“%d\n”, *x);
while (1)
malloc(4);
char *p = NULL;
{
char c;
p = &c;
}
int *x = (int*)malloc(sizeof(int));
free(x);
free(x);
~> Pointer, Dynamic Allocation을 다룰 때에는 상기한 상황들을 주의하도록 하자. 기초적인 내용들이지만, 누구나 그렇듯, 이런 기초를 간과하다가 원인 모를 에러를 맞이하는 것이다.
(특히 C Programming에서)
#include <unistd.h>
int brk(void *addr)
void *sbrk(intptr_t increment);
brk : OS에게 Heap Segment 확장을 요청한다.
break Pointer : Heap Segment의 End 위치를 가리키는 Pointer이다. (上방향)
brk System Call은 넘겨받은 addr이 가리키는 위치로 New break Pointer를 설정한다.
sbrk System Call은 현재 break Pointer 위치에서 넘겨받은 increment Bytes만큼을 늘리거나 줄이는 기능을 한다. (sbrk = size + brk)
~> User Program에선 brk/sbrk를 직접 사용할 일은 없다. ★
Dynamic Memory Allocator는 Heap 공간이 부족하다는 것을 알게 되면 알아서 brk, sbrk 등의 System Call을 호출해 Heap의 사이즈를 늘린다. OS에게 "나 Heap 더 필요해.. 좀만 더 키워주라.." 하는 것이다.
최초에 Heap Segment를 마련할 땐 그리 크게 설정하지는 않는다. Heap을 많이 사용하지 않는 Program도 상당 수 존재하기 때문이다.
참고로, Heap이 brk나 sbrk로 크기가 확장되었다고 하더라도, 그 커진 영역이 기존 영역과 실제 Physical Memory 상에서 반드시 Linear하진 않을 수 있다.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void) {
void *cur_brk, *temp_brk = NULL;
temp_brk = cur_brk = sbrk(0); // sbrk(0) : 현재 Break Pointer 위치 ★★
printf("Break Pointer Location 1: %p\n", cur_brk);
brk(cur_brk + 4096); // 현위치에서 4K 정도 이동하자!(= sbrk(4096))
cur_brk = sbrk(0); // 4K = 2^12 -> 0x00001000 ★★
printf("Break Pointer Location 2: %p\n", cur_brk);
brk(temp_brk);
cur_brk = sbrk(0); // 다시 원래로 돌아옴
printf("Break Pointer Location 3: %p\n", cur_brk);
return 0;
}
(출력)
Break Pointer Location 1: 0x8e74000
Break Pointer Location 2: 0x8e75000
Break Pointer Location 3: 0x8e74000
~> Current break Pointer 위치에 4096(4K)을 더했을 때, 현재 위치의 Hexadecimal 표현에 0x00001000이 더해졌음을 주목하자.
~~> 4K는 2^12로, 12비트를 의미한다. 12비트는 Hex 기준 3개의 Digit이다. 3개의 Digit가 표현할 수 있는 Range를 꽉채워서 이동한다는 것이므로, 0x00001000이 더해지는 것이다. ★★★
#include <sys/mman.h>
void *mmap(void *ptr, size_t length, int prot, int flags, int fd, off_t offset)
mmap : length 사이즈의 메모리 공간을 ptr이 가리키는 주소에 할당한다.
Virtual Address Space 상에 특정 크기의 메모리 덩어리를 할당하는 것이다. ★
malloc( ) Function 구현 시 mmap을 사용하는 경우도 있다(주로 초기 Setting시). 특히나 크기가 큰 메모리 블록을 할당할 때 mmap을 사용하면 좀 더 효율적이라고 알려져 있다. ★
참고로, 이러한 Dynamic Memory Allocation 목적으로 사용될 경우 fd 인자엔 -1, offset 인자엔 0이 기입되어야 한다. (File 사용 x, ptr 위치에 그대로 할당하겠다는 의미)
~> ptr에 NULL을 넘기는 경우 OS가 자동으로 'mmap이 Mapping할 메모리 위치'를 선정한다.
ptr = mmap(0, 40, flag, MAP_SHARED, fd, 0)); // fd가 가리키는 파일에 40바이트 맵핑!!!
if (ptr == MAP_FAILED) exit(EXIT_FAILURE);
mmap을 사용하면, 우리가 일반적으로 File을 접근할 때 사용하는 read, write와 같은 System Call을 사용하지 않아도 Pointer를 통해 간단하게, 그리고 직접적으로 Data를 접근하고 변경할 수 있다.
즉, Access 시의 Overhead가 적어 좀 더 속도가 빠르다. (mmap의 장점) ★★
대신, Pointer 관리에 좀 더 신경을 써야겠지?
~> fd가 가리키는 File 내에서, offset으로 넘긴 위치에서부터 length 사이즈 만큼의 Contents가 Process에 Mapping된다. ★ (여기선 Offset이 0이므로 File 시작부터 바로 할당하는 것!)
~~> 이때, offset엔 반드시 Page Size의 배수가 들어와야 한다. ★★★
===> Paging 기법에선 Page 단위로 I/O를 하기 때문!
/* example.c Program */
#include <stdio.h>
/* Other headers... */
int main(int argc, char *argv[]) {
int fd, offset;
char *data;
struct stat sbuf; // Directory Listing 등에 쓰이는 stat System Call
/* … */
if ((fd = open("example.c", O_RDONLY)) == -1) {
perror("open");
exit(1);
}
if (stat("example.c", &sbuf) == -1) { // Get File Size
perror("stat");
exit(1);
}
offset = atoi(argv[1]); // Get Offset
/* … */
data = mmap((caddr_t)0, sbuf.st_size, PROT_READ, MAP_SHARED, fd, 0));
// fd가 가리키는 File의 시작점부터 st_size만큼의 Bytes를 Mapping!
// 당연, Process의 Virtual Address Space에도 그만큼이 할당!
/* … */
/* mmap을 통한 직접적 File 접근 */
// file의 시작으로부터 30바이트 위치를 찍어줌.
printf("Byte at offset %d : '%c'\n", offset, data[offset]);
return 0;
}
(출력)
> ./example 30
Byte at offset 30 : 'd'
~> example.c라는 Program은 mmap을 수행해 Process Address Space 안에 특정 사이즈의 메모리 블록을 만들고, 해당 블록을 자기 자신의 File에 대해 맵핑한다. 이어, File의 시작점(0번 Offset)부터 (argv[1])번째에 해당하는 문자를 출력하는 것이다.
~~> 위 예시에선, example.c의 첫 번째 문자 '/'(0번 Offset)을 기준으로 30번 Offset에 위치한 'd'라는 문자를 출력하고 있는 것! (실제 Linux Server에서 검증 완료)
금일 포스팅은 여기까지이다.