하나의 프로세스 크기가 너무 커서 메모리에 다 올리지 못할 때 가상 메모리를 이용하여 프로세스의 일부만 메모리에 올리는 방법을 사용한다. 그리고 이를 구체적으로 구현하는 방법으로 지금까지 페이징 기법을 살펴보았다.
그런데 이를 구현하는 추가적인 방법으로 Segmentation 기법도 존재한다.
먼저 virtual address space 가 지금까지 배웠던 것처럼 위와 같이 존재한다고 해보자.
그러면 가상 주소 공간은 0부터 점점 주소가 증가하는 1차원 주소 공간으로 존재할 것이다.
이때 이 공간에서 '컴파일러 프로그램'을 실행한다고 해보자.
컴파일러는 소스코드를 파싱하고 심볼 테이블을 만들고, 파스 트리를 만드는 과정을 수행할 것이다.
이때 이 과정에서 나온 결과물들을 저장해야 하는데, 각각을 segmentation 으로 영역을 나누고 나눠진 영역에 저장한다.
그런데 컴파일하는 소스코드가 너무 크면 심볼 테이블의 크기가 증가하다가 세그멘테이션 영역을 벗어나 다른 영역으로 넘칠 수도 있다.
이런 불편함을 해소하고자, 하나의 1차원 주소 공간에 모든 것을 다 저장하는 대신, 별도의 영역으로 나눠서 독립적으로 관리하자는 아이디어가 등장했다.
그래서 컴파일러 프로그램이 필요로 하는 심볼 테이블, 소스코드, 상수, 파스트리, 콜스택 영역 각각을 별개의 세그먼트로 만들어 두고 할당한 뒤, 이 안에서 각자 데이터를 저장하며 크기가 자유롭게 늘어나고 줄어들도록 하는 것이다.
이 공간들은 모두 별개의 독립적은 주소값을 가지므로, 서로의 영역을 침범할 일이 없다.
이제 기존의 페이징 기법과 세그멘테이션 기법을 비교한 것이 위와 같다.
1. 페이징에서는 프로그래머가 페이징 기법을 사용하고 있는지 알 필요가 없지만, 세그멘테이션은 알 필요가 있다.
(왜지..?)
2. 페이징에서는 1개의 선형 주소 공간을 쓰지만, 세그멘테이션에서는 여러개의 선형 주소 공간을 사용한다.
3. 두 기법 모두 가상 주소 공간의 크기는 실제 메모리 크기보다 커도 괜찮으며
4. 세그멘테이션에서는 데이터와 프로시저가 독립적으로 저장될 수 있으므로 각각 구분하고, 보호할 수 있다.
5. 페이징에서는 각 테이블의 크기가 커질 때 충돌할 수 있었지만, 세그멘테이션에서는 충돌하지 않고
6. 세그멘테이션은 유저간 프로시저의 공유가 용이하다는 장점이 있다.
유저가 서로 다른 프로세스를 돌리고 있을 때, 페이징은 각 프로세스가 별도의 주소 공간을 가지므로 공유하기 어렵지만, 세그멘테이션은 프로시저 영역의 테이블만 따로 떼어내서 공유하면 되므로 공유가 용이하다.
7. 페이징은 실제 메모리보다 더 큰 선형 공간을 얻기 위해서 등장했다면, 세그멘테이션은 프로그램과 데이터를 논리적으로 독립된 공간에 나누어 공유와 보호를 용이하게 하기 위해 등장했다.
Pure Segmentation
이제 세그멘테이션을 구현하는 다양한 방법을 살펴보자.
첫 번째로 Pure Segmentation은 세그멘테이션을 구현하는 제일 간단한 방법이다.
결국 프로그램을 실행할 때는 메모리에 올려야 하는데, 프로그램을 세그멘테이션이라는 조각단위로 나눠서 관리하는 것이니 각각의 조각을 메모리에 올리게 된다.
처음에 (a)와 같이 각각의 세그먼트 조각이 메모리에 올렸다고 해보자.
이 상태에서 8K 크기의 세그먼트 1이 나가고, 5K 크기의 세그먼트 7이 들어오게 되면 3K 크기의 빈 공간이 생긴다.
이런 빈 공간을 가리켜 checkerboarding 또는 external segmentation 이라고 부른다.
(a) ~ (d) 는 checkerboarding 이 점점 늘어나는 모습을 보여주고, (e) 에서는 세그먼트 조각을 한쪽으로 몰아서 10K의 큰 checkerboarding 으로 만든 것을 볼 수 있다. (compaction)
Pentium 메모리 관리 예제 (★)
위 그림은 펜티엄 CPU 내부의 모습을 간단하게 나타낸 그림이다.
펜티엄에서도 virtual memory를 지원하는데, 이때 페이징과 세그멘테이션을 모두 지원한다.
먼저 프로그램이 생성되는 주소를 logical address 라고 부른다.
logical address 는 크게 selector 부분과 offset 부분으로 나눠서 생각한다.
펜티엄에는 descriptor table 이라는 것이 존재한다.
selector를 해당 테이블의 인덱스로 보고 접근하여 엔트리 하나를 읽어들이는데, 이 엔트리를 가리켜 segment descriptor 라고 부른다. 세그먼트 디스크립터에는 이름에 담긴 의미대로 세그먼트의 시작 주소, 사이즈와 같은 세그먼트 정보가 들어있다.
세그먼트 디스크립터를 통해 세그먼트의 시작 주소를 알아낸 뒤에는 offset 을 더해서 linear address를 계산한다.
만약 페이징 기능이 꺼져있다면 (disabled) 이 주소는 physical address가 되어 바로 메모리에 접근할 수 있다.
만약 페이징 기능이 켜져있다면 (enabled) 이 주소는 virtual address가 되는데, 이때 펜티엄에서는 2-level page table을 사용한다.
위 그림에서 page directory 는 top-level 이 되고, page table 이 second-level 이 되어 각각의 엔트리로 부터 읽은 값과 offset을 조합하여 physical address를 계산한다.
(directory 부분을 인덱스로 찾아들어가서 directory 엔트리를 읽어 second-level page table의 시작주소를 알아내고, page 부분을 인덱스로 해서 page table 엔트리를 읽으면 page frame number 를 알 수 있어서 이 값과 offset 을 더하면 물리 주소가 나온다.)
이때 page directory 를 찾아갈 때는 page directory base register에 저장된 시작주소에 directory 값을 더해서 directory entry 값을 얻어온다.
세그먼트 디스크립터를 자세히 살펴보면 위와 같다.
세그먼트 디스크립터의 크기는 64bit 이며, 32bit 크기로 표현할 때 두 줄로 나타낸다.
Base 는 세그먼트의 시작 주소를 나타낸다. 0~15, 16~23, 24~31 세 부분으로 나누어서 32bit 정보가 저장되어 있다.
Limit 은 세그먼트의 크기를 나타내며, 0~19 까지 총 20bit 크기를 가질 수 있다.
이때 G bit (Granularity Bit)는 Limit 의 값이 byte 단위인지 page 단위인지를 정할 수 있어 세그먼트의 크기를 매우 큰 크기까지 지정할 수 있다. (펜티엄에서는 페이지가 4KB 이므로, 하나의 세그멘테이션 크기를 최대 4GB 까지 커버할 수 있다.)
DPL은 Descriptor Privilege Level을 나타내며, 뒤에서 설명할 Protection과 관련이 있다.
Type 필드는 이 세그먼트가 코드 세그먼트, 데이터 세그먼트, 스택 세그먼트, 콜 게이트 중 어떤 것인지를 나타내는데 사용한다.
(콜 게이트는 뒤에서 후술한다.)
이 그림은 selector 를 자세히 나타낸 모습을 보여준다.
selector 는 16bit 데이터로, 앞의 13bit는 descriptor table을 찾아갈 때 사용할 index로 사용된다.
14번째 bit는 global discriptor table (GDT) / local discriptor table (LDT) 을 선택하는 비트이다.
GDT는 시스템에 하나만 존재하는 테이블이고 LDT는 프로세스마다 존재하는 테이블이다.
GDT에는 운영체제, 디스크 관련 정보가 들어있고, LDT에는 프로세스가 갖고 있는 코드, 데이터, 스택 데이터가 쌓인다.
15~16번째 bit는 privilege level 을 나타내며, 0~3 사이의 값을 나타낸다.
뒤에서 자세히 설명한다.
위 그림은 주소 변환 과정을 간단하게 보여준다.
프로세스가 시작하면, 제일 먼저 '세그먼트 레지스터'의 셀렉터 값을 로딩하고 시작한다.
세그먼트 레지스터에는 코드 세그먼트 레지스터, 데이터 세그먼트 레지스터, 스택 세그먼트 레지스터가 있다.
코드 세그먼트 레지스터에는 코드 세그먼트를 가리키는 셀렉터를 저장하고, 데이터, 스택 세그먼트 레지스터에도 각각 해당 세그먼트를 가리키는 셀렉터를 저장한다.
이렇게 셀렉터 값이 레지스터에 올라오고 나서 명령어를 실행한다.
프로그램이 시작될 때는 코드 세그먼트 레지스터에 코드 세그먼트 셀렉터 값을 넣는다.
이 값은 일반적으로 LDT에서 읽어오도록 세팅된다.
이 값으로부터 실제 physical address를 계산하여 실제로 실행할 코드 데이터와 명령어를 가져온다.
(일단 강의록의 내용은 하나의 세그멘테이션에 대해서 주소 변환 과정을 소개하고, 교수님 말씀은 이 과정이 코드, 데이터, 스택 세그먼트 각각에 대해 레지스터값이 별도로 할당되고 실행되는 것으로 설명하셨다고 이해했다.)
이 그림은 만약 페이징이 enable 되어 계산한 Linear address 를 Virtual Address로 인식한 경우, 이로부터 실제 physical address 를 얻어오는 과정을 보여준다.
상위 10비트는 page directory 에서 page table 의 시작주소를 가져오는데 사용되고, 그 다음 10 비트는 페이지 테이블의 시작주소로부터 실제 page 정보가 담겨있는 페이지 테이블 엔트리를 얻어오는데 사용된다.
페이지 테이블 엔트리에는 페이지 프레임 넘버가 있으므로, 이 값과 offset을 더해서 실제 물리 메모리 주소를 계산할 수 있다.
Protection
펜티엄에는 protection과 관련하여 previledge level 이라는 것이 존재한다.
(descriptor table의 segment descriptor 엔트리에서 15~16비트가 나타냈던 값이다.)
이 레벨은 0~3 까지 값을 가지며, 작을수록 커널에 가까워지며 높은 보안을 가진다.
프로세스가 실행될 때, CPU 에는 PSW (Program State Word) 레지스터가 존재한다.
이 레지스터의 일부 비트가 previledge level을 나타낸다.
이 값과 segment descriptor 엔트리에서 읽은 값을 비교해서, PSW의 값이 읽어온 값 이상이라면 (약한 보안 수준) 세그먼트에 정상적으로 접근할 수 있다. 하지만 숫자가 더 작은 경우에는 접근할 수 없으며 트랩이 발생한다.
코드 세그먼트에서는 같은 레벨과 큰 레벨, 작은 레벨 모두 접근이 가능하다.
이때 레벨이 같은 경우에는 제약이 없지만, 만약 다른 레벨에 대해서 접근을 하는 경우에는 call gate 라는 것을 경유해서 제한된 수준에서 접근할 수 있다.
원래는 call 다음에 프로시저 주소가 와야 하지만, 이 경우에는 call 다음에 selector 값을 주면 디스크립터 테이블과 유사하게 call gate에서 셀렉터 값을 인덱스로 하는 프로시저를 찾아 호출한다.
위에서 만약 페이징 기법을 쓰고 싶지 않다면 비활성화해서 Linear Address를 바로 물리 주소로 활용할 수 있다고 하였다.
만약 세그멘테이션 기법을 쓰고 싶지 않다면 어떻게 할 수 있을까?
이때는 스택, 데이터, 코드 세그먼트 모두 동일한 selector 값을 주면 된다.
그리고 base 주소는 0으로 세팅하고, limit 을 최댓값으로 4GB만큼 쓰도록 만들면 4GB 크기의 가상 주소 공간 하나를 모든 세그먼트가 공유해서 쓰게 된다. (펜티엄에서도 페이징 기법만 단독으로 쓰는 것이 가능하다)
'CS > 운영체제' 카테고리의 다른 글
[운영체제] 19. 파일 시스템 구현 (0) | 2024.12.08 |
---|---|
[운영체제] 18. File & Directory (0) | 2024.12.07 |
[운영체제] 16. 페이지 교체 알고리즘 (0) | 2024.12.05 |
[운영체제] 15. 메모리 관리 (4) | 2024.12.03 |
[운영체제] 14. 스레드 스케줄링 (0) | 2024.10.21 |