728x90

Free Space Management

새 주소 공간을 만들었더니 듬성 듬성 빈 공간이 생겼어!! Oh my god!! 지져스!
이 빈 공간을 정리를 하고 싶어! 어떻게? 할까..잘하면 돼!!!

모든 메모리 관리 시스템에 대한 근본적인 측면에 대한 논의, 바로 Free space management다!
이 장은 메모리 가상화와는 거리가 조금 멀다.

관리하고 있는 공간이 고정 크기의 단위로 나누어져있다면 관리가 쉽다. 그런 경우 고정 크기 단위의 리스트를 유지하고 클라이언트가 그 중 하나를 요청하면 첫 번째 항목을 반환하면 되기 때문이다.
하지만 그렇게 쉬우면 이 챕터를 하지 않았겠지?

빈 공간 관리가 더 어렵고 흥미로운 경우는 관리하는 공간이 가변-크기 빈공간의 집합으로 구성되어있는 경우다. 영문으로 variable-sized units!
그리고 malloc(), free()에서 처럼 사용자 수준 메모리 할당 라이브러리에서,
Physcial Memory를 Segmentaion으로 관리시 External fragmentation이 발생한다.

단편화 = fragmentation

내부 단편화 Internal fragmentation
일정 크기의 페이지에 프로세스 할당시 프로세스의 크기가 페이지 보다 작을 경우 발생
외부 단편화 External fragmentation
여유 공간이 여러 조작으로 나뉘는 현상

외부단편화와 내부단편화

외부, 내부 단편화

Q. 30바이트의 공간을 메모리 공간에 요청해도, Free space 공간의 합이 30이라도 응 요청이 실패된다. 이게 문제라서 이제부터 Free Space를 관리해보자!


Assumptions

malloc(size_t size) and free(void *ptr)

  • malloc(size_t size) takes a single parameter, which is time number of bytes requested by the application.
    free(void *ptr) takes a pointer and frees the corresponding chunk → the user does not inform the library of its size → the library must be able to figure out how big a chunk of memory.

The space for memory allocation is heap.

The generic data structure used to manage free space in the heap is some kind of free list.

  • This structure contains references to all of the free chunks of space in the managed region of memory.

Internal fragmentation

  • If allocator hands out chunks of memory bigger than that requested, any unasked-for space in such a chunk is considered internal fragmentation.
    We will focus on external fragmentation.

Once the memory is handed out to a client, it cannot be relocated to another location in memory
→ No compaction of free space is possible.

The allocator manages a contiguous region of bytes.


Low-level Mechanisms~!

First, we’ll discuss the basics of splitting and coalescing(병합), common techniques in most any allocator.
Second, we’ll show how one can track the size of allocated regions quickly and with relative ease.
Finally, we’ll discuss how to build a simple list inside the free space to keep track of what is free and what isn’t.

Splitting and Coalescing

A free list contains a set elements that describe the free space still remaining(남아) in the heap.

The free list for this heap would have two elements on it.
One entry describes the first 10byte free segment (bytes 0-9), and one entry describes the other free segment (bytes 20-29)

🏳‍🌈 10바이트를 초과하는 모든 요청은 실패하여 NULL를 반환할 것이다.
→그럼 10바이트보다 작은 값을 요청하면 어떻게 될까????

이 경우 splitting이 일어난다. 요청을 만족시킬 수 있는 빈 chunk를 찾아 이를 둘로 분할한다.
1바이트 요청이 발생하고 allocator가 리스트의 두번째 원소를 사용하여 요청을 충족시켰다고 가정하자. malloc()은 20을 반환하고 최종 리스트는 위 그림과 같이 된다.


요청이 특정 빈 청크의 크기보다 작은 경우 Splitting기법을 사용한다. Splitting에 동반되는 기법은 빈 공간의 coalescing이다.

응용 프로그램이 free(10)을 요청하여 힙 중간 공간을 반환하면 free space가 list에 추가된다.
하지만 10bite 길이의 chunk 3개로 나누어져 있어서 만약 사용자가 20bite를 요청할 시 단순 list 탐색시 빈 청크를발견하지 못하고 실패한다. 탐색: traversal

! allocator가 이 문제를 방지하기 위하여 할 수 있는 일은 memroy chunk가 반환될 때 free space를 병합하는 것이다. < 엄청 간단하지? 그렇지만 이해는 쉽지 않단다. ㅎㅎ..>:
→ 메모리 청크를 반환할 때 청크의 주와 바로 인접한 빈 청크의 주소를 살펴본다. 새로 해제된 빈 공간이 왼쪽의 빈청크와 바로 인접해있다면 그들을 하나의 더 큰 빈 청크로 병합한다.

이 상태가 할당이 한번도 일어나지 않은 최초의 힙 리스트의 모양이다. coalescing 기법을 사용하여 allocator가 커다란 빈 공간을 응용 프로그램에게 제공할 수 있다는 것을 더 보장할 수 있다.


Tracking The Size of allocated Regions 할당된 공간의 크기 파악

free(void ptr) Interface는 size를 매개변수로 받지 않는다. (포인터를 할당받아서 리턴하는걸요?)
*우리는 포인터가 인자로 전달되면 malloc 라이브러리는 해제되는 메모리 영역의 크기를 신속하게 파악하여 그 공간을 빈 공간 리스트에 추가할 수 있다고 가정한다. 이 작업을 보통 allocator는 추가 정보를 header block에 저장한다. header block은 메모리에 유지되며 보통 해제된 chunk 바로 직전에 위치한다.

Ex) ptr이 가리키는 크기 20bite의 할당된 블록을 검토하고 있다.

사용자는 malloc()을 호출하고 그 결과를 ptr에 저장하였다고 가정하자.
예를 들어 ptr = malloc(20); Header는 적어도 할당된 공간의 크기는 저장해야한다(이 경우 20)

typedef struct __header_t{
    int size;
    int magic; // 부가적인 무결성 검사를 제공하기 위한 변수
} header_t; 

void free(void *ptr){
    header_t *hptr = (void *)ptr - sizeof(header_t);
...

↑ 사용자가 free(ptr)을 호출하면 라이브러리는 header의 시작 위를 파악하는 간단한 포인터 연산

헤더를 가리키는 포인터를 얻어내면, 라이브러리는 매직 넘버가 기대하는 값과 일치하는지 비교하여 sanity check를 실시한다. (assert(htpr→magic == 1234567)) 그 다음 새로 헤제된 영역의 크기를 간단한 수학을 토해 계산한다.(즉 헤더의 크기를 영역의 크기에 더함)
<!— 주의할 점이 있다. 빈 영역의 크기는 헤더 크기 더하기 사용자에게 할당된 영역의 크기가 된다.


Embedding A Free List

지금까지 free list의 개념만 다루었지만 이제는 구현을 생각해보자. 보통 새로운 노드를 위한 공간을 할당할 때 malloc()을 호출한다. 하지만 메모리 할당 라이브러리 루틴에서는 불가능하다. 대신 빈 공간 내에 리스트를 구축해야한다.
4096바이트 크기의 메모리 chunck가 있다고 하자. 즉 힙의 크기는 4KB이다. 이를 free list로 관리 하기 위해서는 먼저 list를 초기화해야한다. 처음에 리스트는 (4096-헤더 size)길이의 항목 하나를 가지고 있다.

// 이 list의 노드에 대한 코드
typedef struct __node_t {
    int size;
    struct __node_t *next;
} node_t;

heap을 초기화하고 heap에 free list의 첫 번째 요소를 넣는 코드를 살펴보자. heap은 system call mmap()을 호출하여 얻어진 영역에 구축된다고 가정한다. 이 방법은 유일한 방법은 아니다.

// mmap()이 free list의 chucnk에 대한 포인터를 반환
node_t *head = mmap(NULL, 4096, PROT_READIPROT_WRITE,
        MAP_ANONIMAP_PRIVATE, -1, 0);
head->size = 4096-sizeof(node_t);
head->next = NULL;

이 코드의 실행 후 리스트는 크기 4088의 항목 하나만을 가지게 된다. 매우 작은 힙이다. head 포인터는 이 영역의 시작 주소를 담고 있다. 영역의 시작 주소를 16KB라고 가정하자.

이때 heap의 모양은 위 그림과 비슷할 것이다.
이제 100bite 메모리 chucnk가 요청되었다고 생각해보자.
이 요청을 처리하기 위해 라이브러리는 먼저 충분한 크기의 chunk를 찾는다. 하나의 빈 청크(4088)만이 존재하기 때문에 이 청크가 선택된다. 요청을 처리하기에 충분히 큰 크기, 앞서 말한 것처럼 빈 영역의 크기 더하기 헤더의 크기를 충족시킬 수 있는 청크와 나머지 빈 청크 두 개로 분할한다.

100바이트에 대한 요청이 오면 라이브러리는 기존 하나의 빈 청크 중 108바이트를 할당하고 할당 영역을 가리키는 ptr를 반환한다. 그리고 free()를 사용할 수 있도록 할당된 공간 직전 8바이트에 헤더 정보를 넣는다. 그런 후 하나 남은 빈 노드를 3980바이트로 축소한다.

이제 100바이트씩 3번 할당한 힙을 보자. 이번에는 free()를 통해 일부 메모리를 반환하면 어떤 일이 벌어질까?
free list는 여전히 head가 가리키는 하나의 노드로 구성되어 있지만 세번의 분할 이후 3764로 축소된 모습이다.

이 예제에서 free(16500)을 호출하여 할당 영역 중 가운테 chunck를 반환한다. 16500은 메모리 영역의 시작 주소 16384, 이전 메모리 청크의 크기 108, 해제되는 청크의 헤더 8바이트를 모두 더해서 나온 값이다. 그림에서는 sptr로 나타난다.

라이브러리는 신속히 빈 공간의 크기를 파악하고, 빈 청크를 free list의 헤드 쪽에 삽입한다고 가정한다면 위그림과 동일하다.

이제 빈 공간 리스트의 첫번째 원소는 작은 빈 청크(100바이트크기이며 list의 헤드가 가리킨다.) 두번째 원소는 큰 빈 청크 3764이다. 드디어 우리 리스트가 하나 이상의 원소를 가진다.

마지막으로 마지막 2개의 사용 중인 청크가 헤제된다고 하자. 병합이 없으면 17.7과 같이 작은 단편으로 이루어진 free list가 될 것이다. 해결책은 리스트를 순회하면서 인접한 청크를 Coalescing 하면 된다.

Growing The Heap

heap space가 부족한 경우 단순히 NULL을 반환하면된다. 어떤 경우에도 유일한 대안이다.

졌잘싸다. ㅋㅋㅋㅋㅋ
대부분의 전통적인 allocator는 적은 크기의 힙으로 시작하며 모두 소진하면 OS로부터 더 많은 메모리를 요청한다. allocator는 heap을 확장하기 위해서 특정 시스템콜(Unix 시스템에서는 sbrk)을 호출한다. sbrk 요청을 수행하기 위해서 OS는 빈 물리 페이지를 찾아 요청 프로세스의 주소 공간을 매핑한 후, 새로운 힙의 마지막 주소를 반환한다. 이로써 큰 힙을 사용할 수 있고 요청은 성공적으로 충족될 수 있다.


Basic Strategies

strategies:전략
일부 기법에 대해 간략히 살펴보았고, 빈 공간 할당을 위한 기본 전략에 대해 살펴보자..

Best Fit

So simple!

  1. free list를 검색하여 요청한 크기와 같거나 더 큰 빈 메모리 청크를 찾는다.
  2. 후보자 그룹 중에서 가장 작은 크기의 청크를 반환한다. 이를 best-fit chunk라고 부른다.
    배경: 사용자가 요청한 크기에 가까운 블럭을 반환함으로써 best fit은 공간의 낭비를 줄이려고 노력한다. 이 행동에 비용도 수반된다. 정교하지 않는 구현은 해당 빈 블럭을 찾기 위해서 항상 전체를 검색해야하기때문에 엄청난 성능 저하를 초래한다.

Worst Fit ↔ Best Fit

가장 큰 빈 청크를 찾아 요청된 크기만큼만 반환하고 남는 부분은 free list에 계속 유지한다. Worst Fit은 Best Fit방식에서 발생할 수 있는 수많은 작은 청크 대신에 커다란 빈 청크를 남기려고 시도한다. 그러나 다시 한번 항상 빈 공간 전체를 탐색해야 하기 때문에 이 방법 역시 높은 비용을 지불해야한다.

First Fit

First Fit는 간단하게 요청보다 큰 첫 번째 블럭을 찾아서 요청만큼 반환한다. 먼저와 같이 남은 빈 공간은 후속 요청을 위해 계속 유지한다. First Fit은 속도가 빠르다는 점이 장점이다. 원하는 블럭을 찾기 위해 항상 free list 전체를 탐색할 필요가 없다. 때때로 list의 시작에 크기가 작은 객체가 많이 생길 수 있다. 따라서 allocator는 free list의 순서를 관리하는 방법이 쟁점이다. 한가지 방법으로 address-based ordering(주소-기반 정렬)을 사용하는 것이다. 리스트를 주소로 정렬하여 coalescing을 쉽게하고, 단편화로 감소시킨다.

Next Fit

항상 리스트의 처음부터 탐색하는 대신 다음 적합 알고리즘은 마지막으로 찾았던 원소를 가리키는 추가의 포인터를 유지한다. 아이디어는 free space 탐색을 list 전체에 더 균등하게 분산시키는 것이다. list의 첫부분에만 단편이 집중적으로 발생하는 것을 방지한다. 이러한 접근 방식은 전체 탐색을 하지 않기때문에 First Fit의 성능과 비슷하다.

반응형
728x90

마치 구름이 낀 것 마냥 앞이 보이지 않는다.

가정: 프로세스 주소 공간 전체를 Memory에 loding했다.
base, bound 레지스터로 OS는 Process를 Physcial Memory의 다른 부분으로 쉽게 재배치할 수 있다.
Stack, Heap 사이에 사용되지 않는 큰 공간이 존재한다. not in use 공간 때문에 낭비가 심하다. Address Space공간이 Physcial Memory보다 큰 경우 실행이 매우 어렵다.


Segmentation

대용량 주소 공간을 어떻게 자원할까?
↑ 4GB, 32GB bit 주소 공간을 상상해보자.
프로그램은 단지 수 MB만 사용함에도 불하고 주소 공간 전체에 메모리 LOAD 어떻게 할까?
Basic idea: Instead of having just one base and bounds pair in our MMU, why not have a base and bounds pair per logical segment of the address space?

  • Segmentation 논리
  • 각각의 address space 단 한쌍의 base ,bound 레지스터를 이용해서 표현하는 것이 아닌, address space에 존재하는 logical segement마다 base랑 bound 레지스터 pair를 할당하는 것

A segment is just a contiguous portion of the address space of a particular length.

Segmentation을 사용하면 OS는 각 Segment를 물리 메모리의 각기 다른 위치에 배치할 수 있고, 사용되지 않는 가상 주소 공간이 물리 메모리를 차지하는 것을 방지할 수 있다.

1960년초 아이디어: MMU 안에 오직 하나의 base, bound 쌍이 존재 X, address space의 논리적인 segement마다 base bound 쌍이 존재한다.

세그멘트 위반(segment violation) or 세그멘트 폴트(segment fault) : 프로세스가 자신의 주소 범위를 넘어 잘못된 접근을 하여 하드웨어가 트랩을 발생시키고 OS는 프로세스를 종료시키는 것

우리가 기준으로 삼는 address space에는 CODE, STACK, Heap 세 종류의 Segment가 있고, 실제 OS에서는 여러개의 segment가 있다.

Ex) 주소 공간을 물리 메모리에 배치하려고 한다. 각 세그멘트의 base와 bound 상을 이용하여 Segment들을 독립적으로 물리 메모리에 배치할 수 있다.

64KB의 물리 메모리에 3개의 Segment와 OS용으로 예약된 16KB 영역이 존재한다.

사용 중인 메모리에만 Physical space이 할당된다.

Address translation

가상 주소 100번지를 참조한다고 가정하자. 가상 주소 100번지는 Code Segment에 속한다. 참조가 일어나면 하드웨어는 base 값에 이 Segment의 Offset을 더해 물리 주소를 만든다.

↑ ( offset(100) + start address of code segment (32KB) = 100 + 32KB = 32868 )

가상 주소 4200의 Heap을 살펴보자. 가상 주소 4200에 Heap의 base 34KB를 더하면 물리 주소 39016이 나온다. 이 주소는 올바른 주소가 아니다. 먼저 Heap 안에서의 offset를 알아한다. Heap은 가상 주소 4KB(4096)에서 시작하기 때문에 offset은 실제로 4200-4096 = 104이다.

↑ ( offset(104) + start address of code segment (34KB) = 104 + 34KB = 34920 )

SUMMARY

접근하고자 하는 주소에 접근하는 방법

virtual address space 상에서 각 segment 시작 부분에서 접근하고자 하는 주소까지의 offset을 먼저 구해야함.

offset + 접근하고자 하는 segment의 base regset 값 = 실제 Physical address

Segment 구분하는 방법 2가지:


Explicit approach( address에 segment 정보를 추가하는 방법 )

가상 주소의 최상위 몇 비트를 기준으로 주소 공간을 여러 segement를 나누는 것을 말한다.
주소 공간을 Segment로 나타내기 위해서 최상위 2비트를 이용한다.
00→ Code segment
01→Heap segment
11→Stack segment
나머지 비트는 offset으로 사용한다.

이 Virtual address.는 14bit짜리다.

↑ The virtual address of 4200 → 4200 이진 숫자 01 0000 0110 1000
By adding the base register to the offset, the hardware arrives at the final physical address.
offset은 bound 검사도 쉽게 만든다.
offset이 bound보다 작은지 여부만 검사하면 된다. 그렇지 않으면 주소가 잘못된 것이다.
base와 bound 쌍을 배열 형식으로 저장할 경우 (segment당 하나의 항목), 원하는 물리 주소를 얻기 위하여 다음과 같은 작업을 하게 된다.

Implict approach

주소가 어떻게 생성되었나를 관찰하여 Segement를 결정한다. 만약 주소가 PC(program counter)에 생성되었다면 (즉, Instruction fectch) 주소는 Code segment에 있을 것이고, Stack 혹은 base Pointer에 있다면 Stack segment에 있는 것이다. 나머지 경우에는 Heap영역에 있을 것이다.

What About The Stack?


Stacks grow backwards → translation must proceed differently.
negative offset < offset - Stack_Max_Szie이 나온다.

순방향 1, 역방향 0을 표시하는 1bit 추가.

 

이 예에서 가상주소 15KB에 접근하려고 가정해보자. 이 주소는 물리주소 27KB에 매핑되어야 한다. 이 가상 주소를 이진 형태로 바꾸면 11 1100 0000 0000이 된다. 하드웨어는 상위 2비트 11를 사용하여 세그멘트를 지정한다. 이를 고려하면 3KB offset 이 남는다. 올바른 음수 오프셋을 얻기위해서 3KB에서 Segment의 최대 크기를 빼야한다. 이 예에서는 4KB이고 3KB-4KB = -1KB이다. 이제 Base(28KB)에 더하면 올바른 물리 주소 27KB를 얻게 된다. Bound 검사는 음수 Offset의 절대값이 Segment의 크기보다 작다는 것을 확인하여 계산할 수있다.

Support for sharing


Protection bit

메모리 절약을 위해서 때로는 메모리 세그먼트를 공유한다. 코드 공유가 일반적이고, 공유를 지원하 위해서 Protection bit를 하나 추가하여 지원한다.

By setting a code segment to read-only, the same code and be shared across multiple processes.

The OS secretly shares memory which cannot be modified by the process.

다음 장 내용:

OS Support

위와 같은 방식으로 이용한다면 여러 군데 듬성듬성 not in use 공간이 생기고 이는 OS가 재정렬, 재집합을 해준다. 이에 대한 내용은 다음장에서 설명해준다.

반응형
728x90

 CPU Virtualization 부분에서, LDE(Limited direct execution) 기법에 대해 집중적으로 다룬다.
CPU 가상화의 아이디어는 간단하다. → 대부분 프로그램은 하드웨어에서 직접 실행된다.
But 프로세스가 System Call을 호출하러가나 Timer Interrupt 발생할 때 특정 순간에는 OS가 개입하여 Process issue가 발생하지 않도록 한다.
OS는 약간의 hardware지원을 받아 efficient Virtualization을 제공하기 위해 실행 프로그램의 방해가 안 되도록한다. 중요한 순간에는 OS가 관여하여 하드웨어를 직접 제어한다.
현대 OS의 목표: efficiency, Control


Virtualizing memory

Virtualization을 제공하는 동시에 efficiency and control을 모두 추구한다.
Efficiency → Hardware 지원 활용할 수 밖에 없다.
처음에 몇개의 Register만 사용 점점 TLB → page table 등으로 사용하는 것이 늘어난다. 점점 복잡해진다.
Control → 응용 프로그램이 자기 자신의 메모리 이외에는 다른 메모리에 접근하지 못한다는 것을 OS가 보장한다는 것을 의미한다. → 쉽게 말해서 OS가 메모리 침범을 막아준다.
↑ 하드웨어의 도움이 필요하다.

flexibility 측면에서→ VM system이 필요한 상황이 있다.

  • 원하는 대로 주소공간을 사용하기
  • 프로그래밍하기 쉬운 시스템을 만들기를 원한다.

핵심 문제는 아래 참고 ↓


Quecstions

How can we build efficient virtualization of memory?
How do we provide the flexibility needed by applications?
How do we maintain control over which memory locations an application can access,
and thus ensure that application memory accesses are properly restricted?
How do we do all of this efficiently?


Hardware-based address translation →(짧게) address translation

address translation은 Limited Direct Execution에서 부가적으로 사용되는 기능이라고 생각할 수 있다.

  • the hardware transforms each memory access (e.g., an instruction fetch, load, or store), changing the virtual address provided by the instruction to a physical address where the desired information is actually located.
  • Hardware만으로 Memory Virtualzation을 구현할 수 없다. low level 기능들을 제공하여 translation을 가속화시키는 도움을 주지만 혼자서는 안된다.
    OS는 정확한 변환이 일어날 수 있도록 하드웨어를 셋업하기 위하여 관여한다. OS는 메모리의 빈 공간과 사용중인 공간을 알고 있어야하고, 메모리 사용을 제어하고 관리한다.

The program has its own private memory, where its own code and data reside.
↑ illusion을 만들어야한다.
Reality(현실) → Many programs are actually sharing memory at the same time.

Assumptions


Simple memory virtualization

  1. The user's address space must be placed continuously in physical memory.
  2. The size of the address space is not too big → it is less than the size of physical memory.
  3. Each address space is exactly the same size.

차차 가정을 완화하면서 실제적인 Memory Virtualzation을 이끌어낸다.

Example


void func(){
    int x = 3000;
    x = x+3;
}


128: movl 0x0(%ebx), %eax; 0+ebx를 eax에 저장
132: addl $0x03, %eax; eax 레지스터에 3을 더한다.
135: movl %eax, 0x0(%ebx); eax를 메모리에 다시 저장.

  • 컴파일러는 이 코드를 어셈블리 코드로 변환하고 그결과 x86 어셈블리로 위 코드처럼 표기된다.

The three-instruction code sequence is located at address 128.
The value of the variable x at address 15KB. The initial value of x is 3000.
The sequence of memory accesses.

  • Fetch instruction at address 128
  • Execute this instruction (load from address 15 KB)
  • Fetch instruction at address 132
  • Execute this instruction (no memory reference)
  • Fetch the instruction at address 135
  • Execute this instruction (store to address 15 KB)

 

 

실제로는 0~16KB는 OS가 사용한다.

프로그램 관점에서 address space은 주소 0부터 시작하여 최대 16KB까지이다.
→ 프로그램이 생성하는 모든 메모리 참조는 이 범위 내에 있어야한다.
To virtualize memory, the OS wants to place the process somewhere else in physical memory, not necessarily at address 0.

프로세스 주소

공간이 실제로는 다른 물리 주소에 배치되어 있을 때, 주소 0번지부터 시작하는 가상 주소 공간의 환상을 어떻게 제공할 수 있을까?

Dynamic(Hardware-based) Relocation = base and bound


1950년 후반의 첫번째 time-sharing machines에게 base and bound 아이디어가 채택되었다.
base and bound을 dynamic relocation라고도 함.

각 CPU마다 2개의 hardware register가 필요하다. 바로 base, bound(limit) register이다.

base와 bound

  • 우리가 원하는 위치에 주소 공간을 배치할 수 있게 한다.
  • 배치와 동시에 프로세스가 오직 자신의 주소 공간에만 접근한다는 것을 보장한다.

Each program is written and compiled as if it is loaded at address zero.

When a program starts running, the OS decides where in physical memory it should be loaded and sets the base register to that value.

When any memory reference is generated by the process, it is translated.


위의 예제를 분석하며 이해하기

128: movl 0x0 (%EBX), % eax
PC(Program counter)는 128로 설정된다. hardware가 이 instruction을 fech할 때, 먼저 PC값을 base register의 값 32KB(32768)에 더해 32896의 물리주소를 얻는다.

그런 후 hardware는 물리주소에서 명령어를 가져온다. 그리고 프로세서는 명령어의 실행을 시작한다. 얼마후 프로세스는 15KB의 값을 load하라는 명령어를 내린다. 이 주소를 프로세서가 받아 다시 base register(32KB)를 더하고 물리주소 47KB에 원하는 내용을 탑재한다.

address translation: Transforming a virtual address into a physical address.
그리고 이 주소의 재배치는 실행 시에 일어나고, 프로세스가 실행을 시작한 이후에도 주소 공간을 이동할 수 있기 때문에, dynamic relocation 이라고도 불린다.


그럼 bound(limit) register는 어디다 쓸까?

bound register는 보호를 지원하기 위해 존재한다. 앞에서 다룬 간단한 예에서 bound register는 항상 16KB로 설정된다. Process가 bound보다 큰 가상 주소 또는 음수인 가상 주소를 참조하면 CPU는 예외를 발생시키고 Process는 종료될 것이다. → bound의 요점은 프로세스가 생성한 모든 주소가 합법적이고 프로세스의 범위에 있다는 것을 확인하는 것이다.
The bounds register is there to help with protection. Specifically, the processor will first check that the memory reference is within bounds to make sure it is legal.


Memory Management unit→ MMU

base와 bound register는 cpu 칩 상에 존재하는 hardware structer이다. CPU당 1쌍.
Address translation에 도움을 주는 Processer의 일부를 MMU라고 부르기도 한다.

bound register 두가지 방식으로 정의

  1. 주소 공간의 크기를 저장하는 방식

하드웨어는 가상 주소를 base 레지스터에 더 하기전에 먼저 bound 레지스터와 비교한다.

  1. 주소 공간의 마지막 물리주소를 저장하는 방식

하드웨어는 먼저 base 레지스터를 더하고 그 값이 bound 앞에 있는지 검사한다.

 

두가지 모두 논리적으로 같다.


Example Translations

A process with an address space of size 4KB(비현실적) has been loaded at the physical address of 16KB.
예에서 볼수 있는 것처럼 Physcial address를 얻기 위해서는 간단하게 Virtual address에 base address를 더해주면 된다.
! virtual address가 너무 크거나 음수 일경우에 Fault를 일으키고 예외가 발생하게 된다.


Hardware Support

Two different CPU modes(Kernel, user mode)

  • The OS runs in privileged mode(or kernel mode), where it has access to the entire machine.
  • Applications run in user mode, where they are limited in what they can do.

Processor status word의 A single bit → indicates which mode the CPU is currently running in.

특정한 순간(system call, 다른 종류의 예외나 interrupt 발생) CPU는 모드를 전환한다.
The hardware must also provide the base and bounds registers themselves, as part of MMU of the CPU.
The hardware should provide special instructions to modify the base and bounds registers.

↑ CPU가 하나라고 가정했을 때, 프로세스가 변경되니 base, bound 값도 변경되어야한다.
The CPU must be able to generate exceptions in situations where a user program tries to access memory illegally.

  • The CPU should stop executing the user program and arrange for the OS “out-of-bounds” exception handler to run.

  • 번역

따로 교수님 설명 녹음 추가할 예정

Virtual address는 Program Counter에 존재한다.

뜬금포 지식 → Free list


운영체제는 프로세스에게 메모리를 할당할 수 있도록 사용되지 않는 메모리 공간의 리스트를 유지한다. 다양한 자료구조가 사용될 수있으나, 가장 간단한 자료구조는 free list다.
이 리스트는 현재 사용 중이지 않은 물리 메모리 영역의 리스트다.

Operating System Issues


하드웨어 지원과 OS 관리가 결합되면 간단한 Virtual Memory를 구현할 수있다. →base and boud 방식의 virtual memory구현을 위해서 OS가 반드시 개입해야하는 세개의 시점이 존재한다.

First, the OS must take action when a process is created, finding space for its address space in memory.
Fortunately, given our assumptions that each address space is (a) smaller than the size of physical memory and (b) the same size, this is quite easy for the OS

  • When a new process is created, the OS will have to search a data structure (often called a free list) to find room for the new address space and then mark it used.

Second, the OS must do some work when a process is terminated (i.e., when it exits gracefully, or is forcefully killed because it misbehaved), reclaiming all of its memory for use in other processes or the OS. 프로세스가 종료하면, 운영체제는 종료한 프로세스의 메모리를 다시 free list에 넣고 연관된 자료구조를 모두 정리한다.

Third, the OS must also perform a few additional steps when a context switch occurs.
-There is only one base and bounds register pair on each CPU, after all, and their values differ for each running program
-The OS must save and restore the base-and-bounds pair when it switches between processes.
-When the OS decides to stop running a process, it must save the values of the base and bounds registers to memory, in some per-process structure such as the process structure or process control block (PCB).

The OS must provide exception handlers

  • 정리
  • 사용자가 특정 프로그램 실행을 지시하는 순간 프로세스 생성 → 프로세스가 사용할 address space할당
    할당을 위해서 시스템 메모리에서 빈 공간을 찾아 프로세스가 사용할 공간 마련
    빈 공간과 사용중인 공간 중에 어느 프로세스가 어느 공간을 사용하고 있는지, 어떤 공간이 비어있는지 기록할 free list가 존재
    프로세스의 동작이 종료되었을 때(동작을 완료해서/강제종료)OS가 이 프로세스가 사용하던 주소 공간을 회수함.
    회수하고 이 프로세스의 주소 공간에 있던 모든 정보를 삭제하고 free list에 정보처리
    정보처리 방법 1. 메모리에 존재하고 잇는 정보를 안 지우고 free list에 비어있다 기록 2. 메모리에 있는 정보 초기화도 하고 free list에 기록도 함.

  1. 실행 중인 프로세스를 중단시킨다.
  2. 현재 위치에서 새 위치로 주소 공간을 복사한다.
  3. 프로세스 구조체에 저장된 base 레지스터를 새 위치를 가리키도록 갱신한다.
  4. 예외 핸들러 혹은 호출된 함수를 제공한다.
  • 부팅할 때 커널 명령어를 사용하여 핸들러를 설치한다.
반응형
728x90

Type of Memory


In running a C program, there are two types of memory that are allocated.

  • Stack memory: The allocations of deallocations of it are mangaged implicitly by compiler for a programmer(so called as automatic memory).

A stack frame stores return address, function parameters, and tempral variables etc.

  • Heap memory: The allocations and deallocations are explicitly handled by the programmer.
void func() {
    int *x = (int *)malloc(sizeof(int));
    ...
}

malloc() 함수


malloc() 호출은 매우 간단하다. Heap에 요철할 공간의 크기를 넘겨 주면, 성공했을 경우 새로 할당된 공간에 대한 포인터를 사용자에게 반환하고 실패할 경우 NULL을 반환한다.

#include <stdlib.h>
...
void *malloc(size_t size);
double *d = (double *) malloc(sizeof(double));
int *x = malloc(10 * sizeof(int));
주의-- 필요로 하는 저장공간을 정확하게 입력 해줘야함. 특히 string
-> 시스템에서 각각의 연산자와 몇 byte를 사용하는지 어떻게 알까? -> 그래서 sizeof()를 사용.

free() call


To free heap memory that is no longer in use, programmers simply call free().

int *x = malloc(10* siezof(int));
...
free(x);

Automatic memory management

With such a facility, a programmer calls something akin to malloc() to allocate memory (usually new) but never have to call something to free space. ← 다른 언어들.

A garbage collector runs after and figures out what memory you no longer have references to and frees it.

JAVA로 구현한 것이 별도의 부하가 발생. Memroy translation, java virtual machine에서 동작하기 때문. 즉 JAVA는 java virtual machine이라는 별도의 부하가 존재하고 이 내에서는 또 Garbage Collector가 돌아다니면서 resource들을 먹고 있기 때문에 C로 구현했을때보다 느리다.

Common Errors

JAVA, C etc .. → stack overflow
C, C++ → segmentation fault 발생.

 

 

Forgetting to allocate memory

char *src = "hello";
char *dst; // oops! unallocated
strcpy(dst, src); // segfaut and die

 

Not allocating enough memory

char *src = "hello";
char *dst = (char *)malloc(strlen(src)); // too small
strcpy(dst, src); // work properly

Forgetting to initialize allocated memory

Forgetting to free memory → Memory leak

Freeing memory befroe you are done with it.

Freeing memory repeatedly

Calling free() incorrectly in(부) correct

Two levels of memory management


The first level of memory management is performed by the OS, which hands out memory to processes when they run, and takes it back when process exit (or otherwise die).

The second level of management is within each process, for example within the heap when you call malloc() and free().

Even if a programmer fails to call free(), the OS will reclaim all the memory of the processes when the program is finished running.

반응형
728x90

초기 시스템

The OS was a set of routines (a library, really) that sat in memory(starting at physical address 0 in this example), and there would be one running program (a process) that currently sat in physical memory (starting at physical address 64k in this example) and used the rest of memory.


Multiprogramming

Multiple processes were reday to run at a given time, and the OS would switch between them.

Increment of the effective utilization of the CPU.

Time Sharing

One approach: to run one process for a short while, giving it full access to all memory, then stop it, save all of its states to some kind of disk, load some other process's state, run it for a while... SLOS TOO SLOW! Saving the entire contents of memory to disk?? ← disk에 저장하면 느리다.

Time sharing - continued

Alternative: To leave processes in memory while switching between them, allowing the OS to implement time sharing efficiently.

Ex)
In the diagram, there are three processes (A, B, and C) and each of them have a small part of the 512KB physical memory carved out for them.
Assuming a single CPU, the OS chooses to run one of the processes (say A), while the others (B and C) sit in the ready queue waiting to run.

allowing multiple programs to reside concurrently in memory makes protection an important issue;

Unfortunately, this approach has a big problem: it is way too slow, particularly as memory grows


The Address Space ← protection을 높이기 위해 개발됨.

However, we have to keep those pesky users in mind, and doing so requires the OS to create an easy to use the abstraction of physical memory. → address space

운영체제의 메모리 개념을 이해하는 것이 메모리를 어떻게 가상화할지를 이해하는 핵심이다.
The address space of a process contains all of the memory states of the running program.→ Code, stack, heap

Ex)

the program code is static so after placement won't need any more space.
Heap and stack should be able to grow so they are placed at opposite end of the address space.

Virtualization of memory

우리는 운영체제가 메모리를 가상화(virtualizingmemory)한다고 말한다. 왜냐하면 실행 중인 프로그램은 자신이 특정 주소의 메모리에(예를 들어0) load되고 매우 큰 주소 공간을(예를 들어,32비트 또는64비트) 가지고있다고 생각하기 때문이다. 현실은 다르다

The OS in tandem with some hardware support should make sure that the memory operation of a process occur at the correct virtual address not in the address of address space.

Virtualization Memory Goal

  1. transparency(투명성)
  • The OS should implement virtual memory in a way that is invisible to the running program.
  • The program shouldn't be aware of the fact that memory is virtualized; rather, the program behaves as if it has its own private physical memory.
  1. efficiency(효율성)
  • The OS should strive to make the virtualization as efficient as possible, both in terms of time (i.e., not making programs run much more slowly) and space (i.e., not using too much memory for structures needed to support virtualization).
  • In implementing time-efficient virtualization, the OS will have to rely on hardware support, including hardware features such as TLBs (which we will learn about in due course).
  1. protection.(보호)
  • The OS should make sure to protect processes from one another as well as the OS itself from processes.
  • When one process performs a load, a store, or an instruction fetch, it should not be able to access or affect in any way the memory contents of any other process or the OS itself (that is, anything outside its address space).
  • Protection thus enables us to deliver the property of isolation among processes; each process should be running in its own isolated cocoon, safe from the ravages of other faulty or even malicious processes.

⭐Isolation(고립)

Protection enables us to deliver the property of isolation among processes → each process should be running in its own isolated cocoon.

By using memory isolation, the OS ensures that running programs cannot affect the operation of the underlying OS.
Some modern OS's take isolation even further by walling off pieces of the OS from other pieces of the OS(microkernel).

반응형
728x90

sometimes referred to as a fair-share scheduler.
Proportional-share is based around a simple concept:
Instead of optimizing for turn-around or response time, a scheduler might instead try to guarantee that each job obtain a certain percentage of CPU time.

lottery scheduling: ← Proportional Share의 좋은 예
The basic idea is quite simple: 다음 실행될 프로세스를 추첨을 통해 결정한다. 더 자주 수행되어야하는 프로세스는 당첨 기회를 더 많이 준다.


lottery scheduling

Underlying lottery scheduling is one very basic concept: tickets, which are used to represent the share of a resource that a process (or user or whatever) should receive.
The per-cent of tickets that a process has represents its share of the system resource in question.

Ex) Imagine two processes, A and B, and further that A has 75 tickets while B has only 25.

Lottery scheduling achieves this probabilistically (but not deterministically) by holding a lottery every so often (say, every time slice). Holding a lottery is straightforward: the scheduler must know how many total tickets there are (in our example, there are 100)

The scheduler then picks a winning ticket, which is a number from 0 to 99.
Assuming A holds tickets 0 through 74 and B 75 through 99, the winning ticket simply determines whether A or B runs.

Basic Concepts

Random approaches have at least three advantages over more traditional decisions.
First, random often avoids strange corner-case behaviours that a more traditional algorithm may have trouble handling.
Second, random also is lightweight, requiring the little state to track alternatives.
Finally, random can be quite fast.
As long as generating a random number is quick, making the decision is also, and thus random can be used in a number of places where speed is required.
- Random number generator를 만드는 것은 굉장히 어렵다. -


Ticket Mechanisms

One way is with the concept of ticket currency.
Currency allows a user with a set of tickets to allocate tickets among their own jobs in whatever currency they would like. the system then automatically converts said currency into the correct global value.

For example, assume users A and B have each been given 100 tickets.
User A is running two jobs, A1 and A2, and gives them each 500 tickets (out of 1000 total) in User A’s own currency. User B is running only 1 job and gives it 10 tickets (out of 10 total).

Another useful mechanism is ticket transfer.
With transfers, a process can temporarily hand off its tickets to another process.
This ability is especially useful in a client/server setting, where a client process sends a message to a server asking it to do some work on the client’s behalf. To speed up the work, the client can pass the tickets to the server and thus try to maximize the performance of the server while the server is handling the client’s request.

Finally, ticket inflation can sometimes be a useful technique.

With inflation, a process can temporarily raise or lower the number of tickets it owns.
Rather, inflation can be applied in an environment where a group of processes trust one another
in such a case, if any one process knows it needs more CPU time, it can boost its ticket value as a way to reflect that need to the system, all without communicating with any other processes


구현(Implementation)

//counter: used to track if we've found the winner yet
int counter = 0;

//winner: use some call to a random number generator to get a value,
//        between 0 and the total # of tickets
int winner = getrandom(0, totaltickets);

//current: use this to walk through the list of jobs
node_t *current = head;

//loop until the sum of ticket values is > the winner
while(current) {
    counter = counter + current->tickets;
    if (counter > winner)
        break; // found the winner
    current = current->next;
}

노드를 이어준 링크드 리스트 형태로 구현되어있으며 winner에는 랜덤 넘버수가 있고 A는 0-99, B는 100-149,C는 150-399까지 자신의 수를 가지고 있고 링크드형태로 앞에서부터 순차적으로 훑어간다.
counter란 숫자를 A 더하고 B더하고 이런식으로 진행된다. 그리고 counter값이 winner(A,B,C 중 하나)보다 값이 초과된다면 티켓의 당첨자가 된다.

How To Assign Tickets?

One problem we have not addressed with lottery scheduling is: how to assign tickets to jobs?


Why Not Deterministic? → Stride scheduling

random is simple but it occasionally will not deliver the exact right proportions.
Waldspurger invented stride scheduling, a deterministic fair-share scheduler

current = remove_min(queue); //pick client with minimum pass
schedule(current);
current->pass += current->stride;
insert(queue, current);

Example: Tree processes A, B, and C with stride values of 100, 200, and 40 and all with pass value initially at 0.

A가 선택이 되서 실행이 된다면 pass값은 A의 stride value 만큼 증가한다.

Stride Scheduling 예제 설명 음성.m4a
2.52MB


The Linux Completely Fair Scheduler CFS

To achieve its efficiency goals, CFS aims to spend very little time making scheduling decisions.

Basic operation

Goal: To fairly divide a CPU evenly among all competing process.
vruntime(virtual runtime): A simple counting-based technique. vruntime은 위에 pass와 비슷하다.
As each process runs, it accumulates vruntime.
Int the most basic case, each process's vruntime increases at the same time, in proportion with physical (real) time.
When a scheduling decision occurs, CFS will pick the process with the lowest vruntime to run next.

Q: How does the scheduler know when to stop the currently running process, and run the next one?
A: If CFS switches too often, fairness is increased. If CFS switches less often, performance is increased.

Control parameters

sched_latency: CFS uses this value to determine how long one process should run before considering a switch.
Example:
For n processes, CFS divides the value of sched_latency by n to get a per-process time slice.
CFS then schedules the first job and runs it until it has used the per-process time slice, then checks to see if there is a job with lower vruntime to run instead.

만약 sched_latency가 48초라고 했을 때 이를 프로세스 수로 나눈다. 즉 sched_latency/process number 이렇게 하면 현재 프로세스 수는 4임으로 각 프로세스가 실행 시간은 12초가 된다.
만약 프로세스사 실행 도중 끝나거나 새로운 프로세스가 실행된다면 어떻게 될까? 도중에 끝난다면 각 프로세스당 동작 시간이 늘어날 것이다. 왜냐면 sched_latency는 고정 되어있고 process number가 줄어 각 할당 된 동작시간이 늘어난다. 반대로 새로운 프로세스가 들어왔다면 각 할당된 동작 시간이 너무 작아질 수가 있게 된다. 그렇게 된다면 어떻게 해야할까?

이를 막기 위해서 다른 parameter가 있다. 바로 min_granularity
CFS will never set the time slice of a process to less than this value, ensuring that not too much time is spent in scheduling overhead.

CFS utilizes a periodic timer interrupt, which means it can only make decisions at fixed time intervals.

This interrupt goes off frequently (e.g., every 1 ms), giving CFS a chance to wake up and determine if the current job has reached the end of its run.

CFS also enables controls over process priority, enabling users or administrators to give some processes a higher share of the CPU.

Nice level of a process: The nice parameter can be set anywhere from 20 to +19 for a process, with a default of 0.

Positive nice values imply lower priority, and negative values imply higher priority.

CFS maps the nice value of each process to a weight:

Using Red-Black trees
When the scheduler has to find the next job to run, it should to so as quickly as possible. 
Searching through a long list is wasteful.
CFS keeps processes in a red-black tree.
CFS keeps only running (or runnable) processes in the RB tree.
Example: Ten jobs with vruntime 1, 5, 9, 10, 14, 18, 17, 21, 22, 24.

반응형
728x90

지난 시간에 배운 이야기

핵심: CPU를 어떻게 Virtualization을 하느냐?
방법: Time-Sharing을 한다.
조그만한 Time slot을 나눈다 → 각각의 프로세스에 Time slot을 하나씩 배정한다
Process가 Time Slot을 다 쓰고 안 내놓는 가능성이 있었다. ⇒ 초창기 Cooperative Approach 방식
이 방식은 Prcocess가 자신의 load를 다 끝날 때까지 CPU를 손에 잡고 있거나 처리 시간이 너무 오래 걸린다면 알아서 CPU를 내놓을 것이라고 기대했었다.

Non-Cooperative Approach: 하드웨어 타이머를 사용하기로함
하드웨어 타이머가 완료되었을 때 찾아가야하는 트랩핸들러를 부팅 과정에서 기록을 해놓는다. 그리고 타이머가 완료되면은 운영체제한테 제어권을 넘겨준다. 그러면 운영체제는 새로운 프로세스를 선정해서 CPU를 넘겨준다.
이 과정은 Context-Switching을 하는 기본 machism이 된다.

이 장에서는 Process를 어떻게 선정할지에 대해 배우게 된다.


Workload Assumption

Workload ← 일련의 프로세스들이 실행하는 상황

이 Assumption은 우리의 이해를 돕기 위해 설정 된 것이다.

  1. Each job runs for the same amount of time.
  2. All jobs arrive at the same time.
  3. Once started, each job runs to completion.
  4. All jobs only use the CPU(i.e., they perform no I/O
  5. The run-time of each job is Known.

→ 앞으로 하나씩 가정을 완화하면서 실제 Scheduling에 다가간다.

Scheduling metrics

Turnaround time → 반환 시간 → 성능 측면의 평가 기준


Policies

First In, Firsst Out(FIFO)

FIFO scheduling or FCFS(First Come, First Served) scheduling.
It is simple and easy to implement.?

Example: Imagine three jobs(A,B,C) arrives in the system at roughly the same time.
but A arrived faster than B, and B arrived faster than C. Assume each job runs for 10 seconds.
What will the average turn-around time be for these jobs?

T_A =10, T_B = 20, T_C = 30; (A+B+C)/3 = (60)/3 = 20; Average Turnaround = 20;

Example: Let's again assume three jobs but this time A runs for 100 seconds while B and C run for 10 seconds. ← workload의 1번 Assumption을 완화시켰다. FIFO에서 성능이 구리다...!!!!
workload assumption 1 → Each job runs for the same amount of time.

A = 100, B = 110, C = 120, (A+B+C)/3 = 110 << - 😫 Convy effect 현상이 발생!! 어떻게 해결할까?

GeeksforGeeks Convey

Convoy effect: a number of relatively-short potential consumers of a resource get queued behind a heavyweight resource consumer.


Shortest job first(SJT)

FIFO의 문제(Convoy effect)를 해결해보자.
It runs the shortest job first, then the next shortest, and so on.

Q: What is the average turn-arround time for the above case?

B = 10, C = 20, A = 120 , 150/3 =50이다.

Example: Assume Arrives at t = 0 and needs to run for 100 seconds, whereas B and C arrive at t =10 and each needs to run for 10seconds. What is the average turn-around time?

↑ 이제 workload의 Assumption 2번을 완화한다. A가 B와 C보다 먼저 도착하면 성능이? 구리다.
workload assumption 2 → All jobs arrive at the same time.

A = 100, B= 110-10 = 100, C = 120-10 = 110, Avearge Turn-Arround time = 103.33; ← 😫


Shortest-Time-to-Completion First(STCF)

이제는 실행 시간이 긴 A가 먼저 도착했을 때 어떻게 해결할까? SJF의 문제를 해결해보자.

Workload Assumption 3번을 완하해보자.

workload assumption 3 → Once started, each job runs to completion.

Additional machinery: when another job arrives, the scheduler can preempt the currently running job and decide to run another job.
STCF or Preemptive Shortest Job Frist(PSJF): SJT + preemption.

막간 지식

Non-Preemptive Scheduling
한 프로세스가 CPU를 할당받아 실행 중이면 다른 프로세스들이 강제적으로 뺏을 수 없는 스케줄링 방식
예시: FIFO(FCFS), SJF, HRN 등

Preemptive Scheduling
한 프로세스가 CPU를 할당받아 실행중이라도 다른 프로세스가 현재 프로세스를 중지시키고 CPU를 강제적으로 뺏을 수 있는 스케줄링 방식
예시: RR, SRT, Multi Level Queue, Multi Level Feedback Queue 등

선점 기법, 비선점 기법

▲ 좀 더 알아보기: 두 개의 장단점 비교가 잘 되어있는 사이트

Any time a new job enters the system, the STCF scheduler determines which of the remaining jobs (including the new job) has the least time left, and schedules that one.

A = 120-0 B = 20-10 C = 30-10 150→ 50 평균 값


A new metric: response time

Response time 정의: the time form when a job arrives in a system to the first time it is scheduled.

작업의 길이를 미리 알고 있고, 작업이 오직 CPU만 사용하며 평가 기준이 Turnaround Time하나라면 STCF는 매우 훌륭한 정책이다.
Time-sharing computer의 등장하고 사용자는 terminal에서 작업하여 상호작용이 원활하기 위한 성능이 요구된다. 그래서 Respon Time이 등장한다.


American Robin

Round Robin

기본 발상: Instead of running jobs to completion, RR runs a job for a time slice(or scheduling quantum) and then switches to the next job in the run queue.
RR is sometimes called Time-Slicing.

위의 예의 RR의 A,B,C가 시스템에 동시에 도착하고 각각 5초간 실행된다고 가정하자. RR의 average response time은 (0+1+2)/3 = 1이고, SJF average respone time은 (0+5+10)/3 = 5이다.

Time Slice는 짧아질 수록 Response time 기준으로 RR이 성능은 더 좋아진다. time slice가 짧아질 수록 The cost of switching이 전체 성능에 큰 영향을 미치게된다.

The cost of switching: OS actions of saving and restoring a few registers, state information in CPU caches, TLBs (translation lookaside buffer), branch predictors, and other on-chip hardware.
Switching to another job causes the current state to be flushed and new state relevant to the currently-running job to be brought in.
What is the turn-around time of RR?

앞의 예를 다시 보자. A, B, C 각 작업은 5초간 실행되고 동시에 도착하며, RR은 1초의 time slice를 가진다. A는 13, B는 14, C는 15에 종료하고 평균 14의 turn-around time을 가진다. 매우 안좋다.
RR은 각 작업을 잠깐 실행하고 다음 작업으로 넘어가고 하면서 가능한 한 각 작업을 늘리는 것이 목표다. turn-around time은 완료 시간만 고려하기 때문에 RR은 거의 최악이다. 단순한 FIFO보다 성능이 좋지 않게 된다.
Turnaround time vs response time. RR은 공정하다.
SJF, STCF은 Turn-around time에 최적화하지만 respone time은 나쁘다.
RR은 respone time을 최적화하지만 turn-around time은 나쁘다.
아직 workload assumption 4와 5가 남았다.

4 . All jobs only use the CPU(i.e., they perform no I/O )

5. The run-time of each job is Known.


Incorporatin I/O

workload assumption 4 → All jobs only use the CPU를 완화해보자.

A scheduler clearly has a decision to make when a job initiates an I/O request because the currently-running job won't be using the CPU during the I/O; it is waiting for I/O completion.
The scheduler also has to make a decision when the I/O completes. when that occurs, an interrupt is raised, and the OS runs and moves the process that issued the I/O from blocked back to the ready state. It could decide to run the job at that point.

Example: A와 B가 있다고 하자. 각 작업은 50msec의 CPU 시간을 필요로 한다. A는 10mesc동안 실행한 후 I/O 요청을 한다.(I/O 시간은 10msec라고 가정한다.) B는 I/O 작업이 없다.
Scheduler는 A를 먼저 실행시키고, B를 다음에 실행시킨다.

STCF Scheduler를 구축하려고 가정해보고 A가 5개의 10msec 작업으로 분할. B는 하나의 50msec의 CPU를 요청한다는 사실을 어떻게 활용할까?

일반적인 접근 방식은 A는 각 10msec 하위 작업을 독립적인 작업으로 다루는 것이다. 시스템이 시작할 때 10msec 작업들과 50msec를 스케줄하는 것이다. STCF의 경우 가장 짧은 작업을 선택한다. A다. A를 실행하고 B를 선점하여 10msec동안 실행한다. 입출력이 끝나기를 기다리는 동안 CPU는 다른 프로세스에 의해 사용되어 연산의 중첩이 가능해진다.

Overlap(중첩)은 높은 이용률을 가능하게 한다. 가능하면 시스템을 활용도를 극대화하기 위해 연산을 Overlap되게 시랭한다.


The fundamental problem

First, it would like to optimize turn-around time, which is done by running shorter jobs first(SJF); but, the OS doesn't generally know how long a job will run for, exactly the knowledge that algorithms like SJT(or STCF) require.
Second, MLFQ would like to make a system feel responsive to interactive users(i.e., users sitting and staring at the screen, waiting for a process to finish), and thus minimize response time; unfortunately, algorithms like Round Robin reduce response time but are terrible for turn-around time.

Question: How TO SCHEDULE WITHOUT RERFECT KNOWLEDGE?

How can we design a scheduler that both minimizes response time for interactive jobs while also minimizing turn-around time without a priority knowledge of job length?


MLFQ: Basic Rules

Assumption: The MLFQ has a number of distinct queues, each assigned a different priority level.
At any given time, a job that is ready to run is on a single queue.
MLFQ uses priorities to decide which job should run at a given time.

Basic rules for MLFQ:
Rule 1: if Priority(A) > Priority(B), A runs (B donsen't).
Rule 2: if Priority(A) = Priority(B), A&B run in RR.

The key to MLFQ scheduling: How the scheduler sets priorities?
MLFQ varies the priority of a job based on its observed behaviour.

Example

If a job repeatedly relinquishes the CPU while waiting for input from the keyboard, MLFQ its priority high.
if a job uses the CPU intensively for long periods of time, MLFQ will reduce its priority.
MLFQ will try to learn about processes as they run, and thus use the history of the job to predict its future behaviour.


Attempt #1: How to Change Priority

First attempt

Rule 3: When a job enters the system, it is placed at the highest priority(the topmost queue).
Rule 4a: If a job uses up an entire time slice while running, its priority is reduced(i.e., moves down one queue).
Rule 4b: If a job gives up the CPU before the time slice is up, it stays at the same priority level.

Ex 1) Long-running Job Over time

작업은 최고 우선 순위로 진입한다.→ Q2, 10msec timeslice가 하나 지나면 Schduler는 작업의 우선 순위를 한 단계 낮추어 해당 작업을 Q1으로 이동시킨다. 다시 하나의 Time Slice 동안 Q1에서 실행한 후 작업은 마침내 가장 낮은 우선순위를 가지게 되고 Q0으로 이동된다. 이후엔 Q0에 계속 머무른다.

Ex 2) Along Came A Short Job

좀 더 복잡한 예를 살펴보낟. MLFQ가 어떻게 SJF에 근접할 수 있는지 이해해보자.
이 예에서는 2개의 작업이 존재한다. A는 오래 실행되는 CPU 위주 작업, B는 짧은 대화형 작업.
A는 얼마동안 이미 실행온 상태이고, B는 이제막 도착했다고 가정한다.
→ CPU 위주 작업들처럼 A(검은색)는 가장 낮은 우선순위 큐에서 실행되고 있다. B(회색)는 T=100에 시스템에 도착하고 가장 높은 우선순위 큐에 놓여진다. 실행 시간이 짧기 때문에 (단지 2ms), 두 번의 time slice를 소모하면 B는 바닥의 큐에 도착하기 전에 종료한다. A는 낮은 우선 순위에서 실행을 재개한다.

이 예에서 이 알고리즘의 주 목표를 알 수 있다. Scheduler는 작업이 짧은지 긴지 모른다. 일단 짧은 작업이라고 가정하여 높은 우선순위를 부여한다. 진짜 짧으면 빨리 실행하고 바로 종료된다. 길다면 천천히 아래 큐로 이동하게 되고 스스로 긴 배치형 작업이라는 것을 증명한다. 이러한 방식을 MLFQ는 SJF를 근사할 수 있다.

Ex 3) What About I/O?

다음으로 I/O 작업을 수행하는 예를 살펴보자. Rule 4b가 말하는 것처럼, Process가 Time Slice를 소진하기 전에 Processeor를 양도하면 같은 Priority를 유지하게 한다. ← 이 규칙 의도는 간단하다.
대화형 작업이 키보드나 마우스로부터 사용자 입력을 대기하며 자주 입출력을 수행하면 Time Slice가 종료되기 전에 CPU를 양도하게 될 것이다. 그런 경우 동일한 Priority를 유지하게 되는 것이다.

위 예는 이 규칙이 동작하는 예를 보이고 있다. B(회색)는 대화형 작업으로서 I/O를 수행하기 전에 1msec동안만 실행된다. A(검정색)는 긴 배치형 작업으로 B와 CPU를 사용하기 위하여 경쟁한다. B는 CPU를 계속해서 양도하기 때문(대화형 작업이라 계속 CPU를 양도한다.)에 MLFQ방식은 B를 가장 높은 우선순위로 유지한다.


Problems With Our Current MLFQ

현재의 MLFQ는 단순하다. CPU를 긴 작업들과 짧은 작업들 사이에 공유하고, I/O 중심 대화형 작업을 빨리 실행시키기 때문에 잘 동작하는 것처럼 보이지만 심각한 결점을 가진다.

1. Starvation

시스템에 너무 많은 대화형 작업이 존재하면 그들이 모든 CPU 시간을 소모하게 될 것이고 따라서 긴 실행 시간 작업은 CPU 시간을 할당받지 못할 것이다.← Starvation

2. Smart (or wicked) user

Smart user는 Scheduler를 자신에게 유리하게 동작하도록 다시 작성할 수 있다. 일반적으로 Schduler를 속여서 지정된 몫보다 더 많은 시간을 할당하도록 만드는 것을 말한다. 현재까지의 MLFQ는 다음 공격에 취약하다. Time Slice가 끝나기 전에 아무 파일을 대상으로 I/O 요청을 내려 CPU를 양도한다. 제대로 된다면 Time Slice 99%를 실행하고 CPU를 양도하게 되면 CPU를 거의 독점할 수 있다.

3. Transition of behaviors

프로그램은 시간이 지남에 따라 동작이 변할 수 있다. CPU 위주 작업이 대화형 작업으로 바뀔 수 있다. 현재 구현 방식으로는 그런 작업은 다른 대화형 작업들과 같은 대우를 받을 수 없다.


Attempt #2: The Priority Boost

규칙을 보완하여 Starvation 문제를 방지할 수있는지 살펴보자. 간단한 아이디어는 주기적으로 모든 작업의 Priority를 boost(상향 조정)하는 것이다. 일단 간단한 방법: 모두 최상위 큐로 보내는 것이다.

Rule 5: After some time period S, move all the jobs in the system to the topmost queue.

새 규칙은 두가지 문제를 한번에 해결한다.

  1. Processes are guaranteed not to starve: by sitting in the top queue, a job will share the CPU with other high-priority jobs in a RR fashion.
  2. If a CPU-bound job has become interactive, the scheduler treats it properly once it has received the priority boost.

Attempt #3: Better Accounting

해결해야 할 문제가 하나 더 있다. Scheduler를 자신에게 유리하게 동작시키는 것을 어떻게 막을 수 있을까? 이러한 일을 가능하게 만든 주범은 Rule 4a, Rule 4b다.

Rule 4a: If a job uses up an entire time slice while running, its priority is reduced(i.e., moves down one queue).
Rule 4b: If a job gives up the CPU before the time slice is up, it stays at the same priority level.

두 규칙은 작업이 Time Slice가 끝나기 전에 CPU를 양보하여 Priority를 유지가 가능하게 한다.
여기서 해결책은 MLFQ의 각 단계에서 CPU 총 사용 시간을 측정하는 것.
Scheduler는 현재 단계에서 프로세스가 소진한 CPU 사용 시간을 저장한다. 프로세스가 Time Slice에 해당하는 시간을 모두 소진하면 다음 Priority Queue로 강등된다. Time Slice를 한번에 소진하든 짧게 여러번 소진하든 상관 없다. 규칙 4a와 4b를 합쳐 4로 재정의한다.

Rule 4: One job uses up its time allotment at a given level (regardless of how many times it has given up the CPU), its priority is reduced.

EX) Workload가 Scheduler를 자신에게 동작시키려고 할 때 예전 규칙 4a, 4b일 때 행동(왼쪽), 새로운 규칙 4(오른쪽)을 아래 그림을 통해 아 수 있다. 이런 자신에게 유리하도록 조작하는데 대한 방지책이 없다면 프로세스는 time slice가 끝나기 직전에 입출력 명령어를 내릴 수 있어 CPU 시간을 독점할 수 있다. 방지책이 마련되면 프로세스의 입출력 행동과 무관하게 아래 단계 큐로 천천히 이동하게 되어 CPU를 자기 몫 이상으로 사용할 수 없게 된다.

MLFQ Sheduling에는 여러 다른 쟁점이 남아있다.

  1. 필요한 변수들을 Scheduler가 어떻게 설정해야 하는가.
  • 몇 개의 Queue가 존재하는가?
  • Queue당 Time slice의 크기는 얼마로 해야하는가?
  • Starvation을 피하고 행동을 반영하기 위하여 얼마나 자주 Priority가 Boost해야하는가?

이런 쟁점의 질문은 workload에 대해 충분히 경험하고 계속 조정해나가면서 균형점을 찾아야한다.

MLFQ 요약

이 알고리즘은 Multi level queue를 가지고 있으며, 지정된 작업의 priority를 정하기 위하여 feedback를 사용한다. 과거에 보여준 행동이 priority의 지침이 된다. 작업이 시간이 지남에 따라 어떻게 행동하고 그에 맞게 어떻게 처리하는지에 대해 주의를 기울여야한다.

지금까지 배운 MLFQ의 Rule이다.

Rule 1: if Priority(A) > Priority(B), A runs (B donsen't).
Rule 2: if Priority(A) = Priority(B), A&B run in RR.
Rule 3: When a job enters the system, it is placed at the highest priority(the topmost queue).
Rule 4: One job uses up its time allotment at a given level (regardless of how many times it has given up the CPU), its priority is reduced.
Rule 5: After some time period S, move all the jobs in the system to the topmost queue.

MLFQ는 작업의 특성에 따라 정보 없이, 작업의 실행을 관찰한 후 그에 따라 Priority를 지정한다.
MLFQ는 Turn-around time, respone time을 모두 최적화한다.
짧게 실행되는 대화형 작업에 대해서는 우수한 전반적인 성능을 제공한다.→ SJF, STCF와 유사
오래 실행되는 CPU-집중 workload에 대해서는 공정하게 실행하고 조금이라도 진행되도록 한다.

영문 요약 내용

MLFQ is interesting for the following reason: instead of demandinga prioriknowledge of the nature of a job, it observes the execution of ajob and prioritizes it accordingly. In this way, it manages to achieve thebest of both worlds: it can deliver excellent overall performance(similarto SJF/STCF) for short-running interactive jobs, and is fair and makesprogress for long-running CPU-intensive workloads. For this reason,many systems, including BSD UNIXderivatives [LM+89, B86], Solaris[M06], and Windows NT and subsequent Windows operating systems[CS97] use a form of MLFQ as their base scheduler.


🐌아이콘 제작자: Smashicons

Smashicons 디자인의 무료 벡터 아이콘

반응형
728x90

༼ つ ◕_◕ ༽つ Process 배울 때

Virtualization of CPU = mechanism + policy란
사실을 우리는 배웠다.🤔
이제 그 중 mechanism에 대해 배운다. 상당히 혼미하다. 😣!


Mechanism: Limited Direct Execution

Introduction

  • By time sharing the CPU, virtualization is achieved. THEN..
  • Performance: how can we implement virtualization without adding excessive overhead to the system?
  • Control: how can we run process efficiently while retaining control over the CPU?

❓ Obtaining high performance while maintaining control is thus one of the central challenges in building an operating system.

❤ Direct execution

  • Just run the program directly on the CPU.
  • Thus, when the OS wishes to start a program running, it creates a process entry for it in a process list, allocates some memory for it, loads the program code into memory, locates its entry point (i.e., the main() routine or something similar), jumps to it. and starts running the user's code.

But Direct execution gives rise to a few problems in our quest to virtualize the CPU.

🔆 Questions

  1. Simple thing: if we just run a program, how can the OS make sure the program doesn't do anything that we don't want it to do while running it efficiently?
  2. when we are running a process, how does the operating system stop it from running and switch to another process, thus implementing the time-sharing we require to virtualize the CPU?위에서부터 아래로 실행되니 위에서 아래로 읽어주자
    • 번역

💥Problem #1: restricted Operations

Direct execution has the obvious advantage of being fast; The program runs natively on the hardware CPU and thus executes as quickly as one would expect.
But running on the CPU introduces a problem:

  1. what if the process wishes to perform some kind of restricted operation?

User Mode

Code that runs in user mode is restricted in what it can do.

Kernel mode

In which the operating system runs.
In this mode, code that runs can do what it likes, including privileged operations such as issuing I/O requests and executing all types of restricted instructions.

💥What should a program to when it wishes to perform some kind of privileged operation?

→ To enable this, virtually all modern hardware provides the ability for user programs to perform a system call.
System call: system calls allow the kernel to carefully expose certain key pieces of functionality to user programs.
(such as accessing the file system, creating and destroying processes, communicating with other process, and allocating more memory.)

🤩To execute a system call, a program must execute a special trap instruction.

This instruction jumps into the kernel and raises the privilege level to kernel mode; once in the kernel, the system can now perform whatever privileged operation are needed.

✔When finished, the OS calls a special return-from-trap instruction,

which returns into the calling user program while simultaneously reducing the privilege level back to user mode.

The hardware needs to be a bit careful when executing a trap, in that it must make sure to save enough of the caller's registers in order to be able to return correctly when the OS issues the return-from-trap instruction.

커널 스택(kernel stack) : 플래그와 레지스터들을 저장하는 자료구조(각 프로세스마다 있음)

커널 모드와 사용자 모드를 이동할 때 하드웨어에 의해 PC와 레지스터가 저장되고 복원되는 용도


Trap table

Q: How does the trap know which code to run inside the OS?

A: The kernel sets up a trap table at boot time.

OS가 하드웨어에게 예외 사건이 일어났을 때 어떤 코드를 실행해야 하는지 저장하는 자료구조

trap은 trap table을 사용하여 발생시킨다.

The OS informs the hardware of the locations of trap handlers.

Trap handlers = OS는 특정 명령어를 사용하여 하드웨에게 이 위치를 알려주고 하드웨어는 문제를 해결한다. OS는 하드웨어에게 trap handler의 위치를 알려주고 하드웨어는 이 위치를 기억했뒀다가 예외가 발생했을 때 문제를 해결한다.

Once the hardware is informed, it remembers the location of handlers and thus the hardware knows what to do (i.e., what code to jump to) when system calls and other exceptional events take place.

When the machine boots up, it does so in privileged mode, and thus is free to configure machine hardware as need be.
One of the first things the OS does is to tell the hardware what code to run when certain exceptional events occur.

System-call number: to specify the exact system call, a system call number is usually assigned to each system call.


The user code is thus responsible for placing the desired system-call number in a register or at a specified location on the stack→ the OS, when handling the system call inside the trap handler, examines this number, ensures it is valid, and, if it is, executes the corresponding code.

This level of indirection serves as a form of protection; user code cannot specify an exact address to jump to, but rather mus request a particular service via number.


  • 번역
    • LDE 프로토콜 진행 과정
    (전반부, 부팅 시)
    1. 커널은 트랩 테이블을 초기화하고 CPU는 테이블의 위치를 기억한다.
    2. 커널은 커널 모드에서만 사용할 수 있는 명령어를 이용하여 수행한다.
    (후반부, return-from-trap을 이용하여 사용자 프로세스를 시작할 때)
    1. 새로운 프로세스를 위한 노드를 할당하여 프로세스 리스트에 삽입하고 메모리를 할당한다.
    2. return-from-trap 명령어로 CPU를 사용자 모드로 전환하고 프로세스 실행을 시작한다.
    3. 프로세스가 시스템콜을 호출하면 OS로 다시 트랩된다.
    4. OS는 시스템 콜을 처리하고 return-from-trap 명령어를 사용하여 다시 제어를 프로세스에게 넘긴다.
    5. 프로세스는 자신의 할 일을 다 하면 main()에서 리턴한다.
    6. 종료할 때 exit() 시스템을 호출하고 다시 OS체제로 트랩된다.

Two phases in the limited direct execution protocol.

First phase: the kernel initializes the trap table, and the CPU remembers its location.

Second phase: The kernel sets up a few things(e.g., allocating a node on the process list, allocating memory) before using a return-from-trap instruction to start the execution of the process; this switches the CPU to user mode and begins running the process.

When the process wishes to issue a system call, it traps back into the OS, which handles it and once again returns control via a return-from-trap to the process.

If a process is running on the CPU, this by definition means the OS is not running.


💥Problem #2: Switching Between Process

💥Q: How can the operating system regain control of the CPU so that it can switch between processes?

A: cooperative approach

cooperative approach → system Call 기다리기
In this style, the OS trusts the processes of the system to behave reasonably. Processes that run for too long are assumed to periodically give up the CPU so that the OS can decide to run some other task.

Most processes transfer control of the CPU to the OS by making system calls.

Systems like this often include an explicit yield system call, which des nothing except to transfer control to the OS so it can run other processes.

💥Thus, in cooperative scheduling system, the OS regains control of the CPU by waiting for a system call or an illegal operation of some kind to take place.💥


A Non-Cooperative Approach: The OS Takes Control

Q: How can the OS gain control of the CPU even if processes are not being cooperative? What can the OS do to ensure a rogue process does not take over the machine?

A: A timer interrupt

Interrupt

중단, 새치기. CPU가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치나 예외상황이 발생하여 처리가 필요한 경우 CPU에게 알려 처리할 수 있도록 하는 것을 말한다.

종류:Hardware Interrupt(I/O), Software Interrupt(예외상황,system call)
자세한 내용은 아래 블로그에 제대로 정리되어 있다.

[OS기초] 인터럽트 제대로 이해하기

A timer device can be programmed to raise an interrupt every so many millisecond.
when the interrupt is raised, the currently running process is halted, and a pre-configured interrupt handler in the OS runs.

TIP: The addition of a timer interrupt gives the OS the ability to run again on a CPU even if processes act in a non-cooperative fashion. Thus, this hardware feature is essential in helping the OS maintain control of the machine.


Important decision

결정→Whether to continue running the currently-running process or switch to a different one.
↑This decision is made by a part of the operating system known as the scheduler.

Context switch

개념 : All the OS has to do is save a few register values for the currently-executing process (onto its kernel stack, for example) and restore a few for the soon-to-be-executing process(from its kernel stack).

By doing so, the OS thus ensures that when the return-from-trap instruction is finally executed, instead of returning to the process that was running, the system resumes execution of another process.

프로세스 전환을 위해서 운영체제는 저서준 어셈블리 코드를 사용하여 현재 실행 중인 프로세스의 A few registers(General purpose registers, PC, the kernel stack pointer)에 저장한다.


요약

CPU 가상화를 구현하기 위한 핵심적인 저수준 기법에 관해 설명한 챕터. 이 기법을 제한적 직접 실행이라고 한다.
아이어디는 간단: CPU에서 실행하고 싶은 프로그램을 실행시킨다. 그러나 OS는 운영체제가 CPU를 사용하지 못하더라도 프로세스의 작업을 제한할 수 있도록 하드웨어를 셋업해야한다.

일상 생활에서 이러한 방법이 있다. 아이가 있다면 방에 아기 보호 장치를 설치한다. 위험한 물건이 있다면 서랍에 넣어두고 잠그거나 하는 일들이다. 이러면 아이에게 위험한 물건들을 막아 놓아 아이는 자유롭게 방을 안전하게 돌아다니게 할 수 있다. 이렇듯 운영체제는 CPU에게 안전 장치를 준비해 놓는다.

우선 부팅할 때 트랩 핸들러 함수를 셋업하고 인터럽트 타이머를 시작 시키고 그런 후에 제한 모드에서만 프로세스가 실행 하도록 한다. 이러면 OS는 프로세스를 효율적으로 실행할 수 있다.

다음 챕터에서는 특정 시점에 어떤 프로세스를 실행시켜야할까란 질문에 스케쥴러가 답하는 질문에 배운다.


✨아이콘 제작자 Smashicons

Smashicons 디자인의 무료 벡터 아이콘

반응형

+ Recent posts