OS - 2.2 (MV) (2) Memory System Calls

hyeok's Log·2022년 10월 15일
1

OperatingSystems

목록 보기
10/14
post-thumbnail
post-custom-banner

Virtual to Physical Mapping

  지난 포스팅 막바지에 설명한 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를 나타낸다. ★★

        • Physical Address = Frame Start + Page Offset d
    • ex) Page Offset의 비트 개수가 14개라면, 2^14까지 표현이 가능하고, 이는 16K이다. Offset이 16K까지 있다는 것은 무엇을 의미할까?

      • 그렇다. Page(Frame)의 크기가 16K라는 것이다. ★★★
    • ex) 반대로, Page Size가 4K이면 2^12이므로 Offset을 위한 비트 개수가 12개라는 것이다. ★

    • ex) 그렇다면, Page Number p를 표현하는데에 20비트가 쓰이면, 이것은 무슨 의미일까?

      • 2^20까지 표현할 수 있다. 그렇다. 이는 곧 Page Table의 Index 개수이자, Page의 개수, Frame의 개수를 나타내는 것이다. ★★★

        • 1M개 만큼의 Page가 있는 것이다!
      • 만약, Frame Number라는 Value를 4Bytes에 저장한다고 하면, 이 Page Table의 사이즈는 몇일까?그렇다. 1M x 4Bytes = 4MB가 되는 것이다. ★★★★★


~> 우리는 이러한 Translation 기법이 존재한다는 것을 기억해야한다. 추후 Page만을 제대로 다룰 때 다시 자세히 설명하도록 하겠다. (Paging은 다다음 Chapter의 Dynamic Relocation 개념 이후에 다룰 것임)


Memory APIs

  이번엔 잠시 Memory Virtualization 설명을 멈추고, 그 Abstraction 위에서 동작하는 API들에 대해 알아보자.

  • Heap Segment를 관리하는 함수 (in libc)

    • malloc, calloc, realloc, free 등
  • Heap Segment의 Size를 변경하는 System Call (APIs)

    • brk, sbrk 등

~> malloc과 같은 C-Library Function이 brk, sbrk, mmap과 같은 Operating System Call을 이용해 Heap Segment를 관리하는 것이다. ★
~~> 즉, malloc 그 자체는 System Call이 아니다!!!


malloc( ) Function

#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 개념. 추후 다룰 것)

    • 해당 동적할당 메모리 공간에 Process가 실제로 Access할 경우에만 Physical Memory Mapping이 진행된다. (공간 효율을 위해) ★★★
      • 왜냐, Process라고 꼭 Heap을 사용하는 것은 아니니까!

~> 변수 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라는 배열의 전체 사이즈가 된다. (주의) ★★★


free( ) Function

#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가 된다.

    • 허나, Invalid Pointer이지만 그대로 Stack에 남아있긴 하다. (Scope가 유효할 때)

Cautions

  • (1) 할당되지 않은 Character Pointer에 String Copy를 하는 행위
    • Segmentation Fault를 일으킨다.
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); 
  • (2) 할당은 했지만 초기화하지 않은 Heap Memory 영역에 대한 참조
    • 참조에 따라 다르지만, Program이 죽지 않더라도, 이상한 값을 사용하는 꼴이 되기 때문에 정확한 Program 동작이 이뤄지지 않을 것이다.
      • '이상한 값'이란, 반드시 쓰레기 값만을 의미하는 것이 아니라, 기(旣)할당이 있을 경우 해당 공간에 저장했던 Data Value가 나타날 수도 있다. ★

while (1)
	malloc(4);
  • (3) Dynamic Memory Allocation 후 Free를 하지 않는 행위 (Memory Leakage)
    • 당장은 Program에게 Crucial한 위협이 되진 않지만, 이러한 메모리 누수가 지속될 경우 어느 순간 malloc이 실패하게 될 것이다. Heap Segment가 부족해져서 말이다.
      • OS가 이를 막는다. exit(-1)이 됨.
        • 메모리 누수는 공간 효율을 떨어트리고, 나아가 보안 위협으로 번질 수 있기 때문!

char *p = NULL;
{
	char c;
    p = &c;
}
  • (4) Dangling Pointer Error
    • Pointer가 가리키고 있는 Memory 공간이 특정 이유로 인해 해제되는 문제이다. 의도한 해제이면 당연 문제가 없겠지만, 의도치 않게 해제되어 User Program이 이를 모르고 그대로 해당 Invalid Pointer를 사용하는 문제이다. ★★
      • 아무것도 가리키지 않는 Pointer를 의미한다. Program이 Invalid Pointer로 Memory에 Access하는 일이 벌어진다.
        • 알다시피, Pointer가 Local Data를 참조하다가 Local Data의 Scope가 끝나 메모리가 해제되는, 그러한 상황에서 자주 발생한다. ★

int *x = (int*)malloc(sizeof(int));
free(x);
free(x);
  • (5) Double Free Problem
    • 이미 Free한 공간을 한 번 더 Free하는 행위를 의미한다. 과거 SP부터 본 포스팅까지 여러 차례 언급했듯, 이러한 행위는 System에 중대한 위협이 될 수 있는 위험한 행위이다.

~> Pointer, Dynamic Allocation을 다룰 때에는 상기한 상황들을 주의하도록 하자. 기초적인 내용들이지만, 누구나 그렇듯, 이런 기초를 간과하다가 원인 모를 에러를 맞이하는 것이다.
(특히 C Programming에서)


brk( ) System Call

#include <unistd.h>
int brk(void *addr)
void *sbrk(intptr_t increment);	
  • brk : OS에게 Heap Segment 확장을 요청한다.

    • malloc Function은 이러한 brk System Call을 이용해 그 기능을 구현한다. ★
      • 과거 SP에서 다룬 Dynamic Memory Allocation 개념이 기억나는가? 그렇다. 그때 열심히 공부한 바로 그 개념이다. (이때 이미 자세히 다뤘기에 본 포스팅에서 상세한 설명은 하지 않을 것임)
  • 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이 더해지는 것이다. ★★★


mmap( ) System Call

#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 상에 특정 크기의 메모리 덩어리를 할당하는 것이다. ★

      • 이때, 할당 공간은 굳이 Heap일 필요는 없다. 자유롭다.
    • 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 System Call은 Process Virtual Address Space에 메모리 공간을 할당한 후, 해당 공간을 특정 File에 Mapping시키는 것도 가능하다.
    • 바로 위의 예시 코드처럼 말이다. 40Bytes의 메모리 공간이 VAS에 할당되고, 그 공간이 fd가 가리키는 File에 맵핑된다.
      • 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에서 검증 완료)



  금일 포스팅은 여기까지이다.

post-custom-banner

0개의 댓글