기본 하드웨어
CPU 가 직접 접근할 수 있는 유일한 범용 저장 장치는?
- 메인 메모리
- 레지스터
→ 따라서 모든 실행되는 명령어와 데이터들은 CPU 가 직접 접근 가능한 메인 메모리와 레지스터에 있어야 함. → 데이터가 메모리에 없다면 일단 메모리로 이동시켜야 함.
레지스터
- CPU 클록(Clock)의 1사이클 내에 접근 가능.
- 일부 처리 코어들은 레지스터에 있는 명령어 해독과 간단한 연산을 클록 틱 하나, 또는 그 이상의 속도로 처리함.
메인 메모리
메인 메모리는 레지스터와 다르다.
- 메인 메모리에 접근을 하기 위해서는 많은 CPU 클록 틱 사이클이 소요된다.
- 메인 메모리가 레지스터 보다 코어랑 더 머니까요?
- 이 경우 CPU 가 필요한 데이터가 없어서 명령어를 수행 못하고 지연되는(스톨 stall) 상황이 발생하게 된다.
해결법 : 캐시
CPU 와 메모리 사이에 빠른 속도의 메모리(캐시)를 추가하면 된다.
- 캐시를 관리하면 하드웨어는 어떠한 운영체제의 도움 없이 메모리 접근 속도를 향상할 수 있다.
메모리의 기능
- 물리 메모리의 상대적인 접근 속도 차이를 고려하기
- 올바른 동작 보장하기
- 시스템이 올바르게 동작하기 위해서는 사용자 프로그램으로부터 운영체제를 보호하고, 사용자 프로그램 사이도 서로 보호해야 함.
- 운영체제가 CPU ↔ 메모리 접근 중에 개입하면 성능이 떨어지니까, 이 보호 기법은 반드시 하드웨어가 지원해야 함.
메모리의 올바른 동작 보장하는 법
개별적인 메모리 공간을 분리하기 위해 특정 프로세스만 접근할 수 있는 합법적인(legal) 메모리 주소 영역을 설정하여 프로세스가 합법적인 영역만을 접근하도록 하는 것이 필요하다.
- 기준 레지스터와 상한 레지스터를 이용하여 보호한다.
→ 여기서 합법적이란, 특정 프로세스만 합법적으로 접근할 수 있는 것을 뜻한다. 다른 프로세스가 접근하면 “불법”이 되는 영역이라고 볼 수 있다.
기준 레지스터 | 가장 작은 합법적인 물리 메모리의 주소 값 저장 |
상한 레지스터 | 주어진 영역의 크기를 저장 |
- 예시) 기준 레지스터 값이 “300040”, 상한 레지스터 값이 “120900”
- 프로그램 주소는 30040 ~ 42094 까지
사용자 모드에서 수행되는 프로그램이 운영체제 메모리 공간이나 다른 사용자 프로그램의 메모리 공간에 접근하면?
- 운영체제는 치명적인 오류로 간주하여 트랩 trap 을 발생시킨다.
- 우연이든 의도적이든 사용자 프로그램이 운영체제나 다른 사용자프로그램의 코드나 데이터 구조를 수정하는 것을 막는다.
운영체제와 레지스터
- 기준, 상한 레지스터는 특권 명령을 사용하여 운영체제에 의해서만 적재(load) 된다.
- 사용자 프로그램이 레지스터 내용을 변경할 수 없다. → 특권 명령은 커널 모드에서만 수행되기 때문에..^^
- 운영체제는(커널모드에서는) 메모리 영역 / 사용자 메모리 영역 접근에 제약이 없음.
커널의 역할
- 사용자 프로그램을 사용자 메모리 영역에 적재
- 오류가 발생하면 프로그램을 덤프(dump out)
- 시스템 콜의 매개변수 변경
- 사용자 메모리로부터의 입출력 외에 다른 많은 서비스들..
- 예) 멀티 프로세스 시스템 운영체제는 “컨텍스트 스위칭” 에서
- 한 프로세스의 상태를 레지스터 → 메인 메모리로 저장
- 다음 프로세스의 컨텍스트를 메인 메모리 → 레지스터로 저장
주소 할당
운영체제가 프로세스를 실제로 물리 메모리에 적재하는 방법
- 원시 프로그램에서 주소는 숫자가 아닌 심볼(변수 count 와 같은 형태) 형태로 표현된다.
- 컴파일러는 이 심볼 주소를 재배치 가능 주소로 바인딩시킨다.
- 예) 이 모듈의 첫번째 바이트로부터 열네 번째 바이트 주소
- 링커(linker) 나 로더(loader)가 재배치 가능 주소를 절대 주소(물리 주소)로 바인딩 시킨다.
- 예) 74014 번지
- 바인딩 : 한 주소 공간에서 다른 주소 공간으로 맵핑하는 것
바인딩이 이루어지는 시점으로 구분하기
- 컴파일 시간 바인딩
- 절대 코드를 생성
- 예) 사용자 프로세스가 R번지로부터 시작한다는 걸 미리 안다?
- 컴파일러는 번역할 코드를 그 위치에서 시작함.
- 하지만 만약 위치(메모리 위치)가 변경되어야 하면 이 코드는 다시 컴파일되어야 함.
- 적재 시간 바인딩 (컴파일 시점에 바인딩할 위치를 모른다면)
- 컴파일러가 이진 코드를 재배치 가능 코드로 만듬.
- 심볼과 진짜 번지 수와의 바인딩은 “프로그램이 메인메모리에 실제로 적재되는 시간”에 이루어짐.
- 시작 주소가 변경되어도 아무때나 사용자 코드를 다시 적재하면 됨.
- 실행 시간 바인딩 (프로세스가 실행하는 중간에 메모리 위치를 옮겨야 한다면)
- 이걸 위해서는 메모리 관리 장치(MMU) 가 필요하다.
논리 vs 물리 주소 공간
- 논리 주소 : CPU가 생성하는 주소
- 물리 주소 : 메모리가 취급하는 주소(메모리 주소 레지스터(MAR)에 주어지는 주소)
가상 주소를 물리 주소로 매핑해주는 MMU
가장 단순한 MMU 기법
- 재배치 레지스터 값이 14000 일 때, 프로세스가 346 번지에 엑세스한다면 → 사실은 14346 번지에 엑세스 하는 것.
- 사용자 프로그램은 “실제 물리 주소” 에 접근하지 않는다.
- 그냥 0 ~ 346 번지에 대한 포인터를 생성하고 저장, 연산, 다른 주소와 비교 등을 하는 것.
- 하지만 사용자 프로그램의 요청으로 어떤 주소로 갈 적에는, 먼저 기준 레지스터에 바인딩되어 실제 주소로 바인딩된다.
- 사용자 프로그램 → 논리 주소 사용.
- 메모리 하드웨어 → 논리 주소를 실제 주소로 바꾼 것 사용.
- 참조된 메모리 주소의 실제 위치는 실제 실행 시간에 결정된다.
💡사용자 프로그램은 논리 주소만을 만들어낼 뿐이니 메모리 위치가 0 에서 max 까지만 있다고 생각할 것이다. 이 논리 주소는 MMU 를 거쳐 실제 물리 주소로 변환되어야 한다. 별도의 물리 주소 공간에 연결되어야 하는 논리 주소 공간의 개념은 올바른 메모리 관리에 핵심 개념이다.
동적 적재 Dynamic Loading
- 각 루틴이 실제 호출되기 전까지는 메모리에 올라오지 않고, 재배치 가능한 상태로 디스크에서 대기하고 있음.
- main 프로그램이 메모리에 올라와 실행했을 때
- 이 루틴이 이미 메모리에 적재됐는지를 조사.
- 만약 적재되어 있지 않으면, 재배치 가능 연결 적재기가 불려 요구된 루틴을 메모리로 가져옴.
- 이런 변화를 테이블에 기록
- CPU 제어가 중단되었던 루틴으로 다시 보내짐.
장점
- 루틴이 필요한 경우에만 적재 됨.
- 전체 프로그램 크기가 클 수 있지만 사용되는(적재된) 부분이 훨씬 작을 경우.
- 오류 처리 루틴과 같은 아주 간혹 발생하지만 실행할 코드가 많은 경우 유용.
💡동적 적재는 운영체제로부터 특별한 지원이 필요없고, 사용자 프로그램이 직접 설계하고 책임진다.
동적 연결 및 공유 라이브러리
동적 연결 라이브러리(DLL)는 사용자 브로그램에 연결되는 시스템 라이브러리
- 동적 연결은 “연결(linking)” 이 실행 시까지 미루어지는 것.
- 이 기능이 없다면 시스템의 각 프로그램은 실행 가능 이미지에 해당 언어 라이브러리(또는 최소한 프로그램이 참조하는 루틴)의 사본을 포함해야 함. → 실행 가능 이미지 크기 증가, 메인 메모리 낭비
- 이런 라이브러리를 여러 프로세스간에 공유도 가능하여 메인 메모리에 DDL 인스턴스 하나만 있으면 됨.
운영체제의 도움이 필요
- 동적 연결 라이브러리는 라이브러리 갱신(예: 버그수정)으로 확장 가능.
- 새로운 버전으로 교체 가능하고, 그럼 모든 프로그램은 새로운 라이브러리 버전을 사용하게 될 것.
- 동적 연결과 공유 라이브러리는 일반적으로 운영체제의 도움이 필요하다.
- 왜? : 운영체제만이 루틴이 있는지 검사해줄 수 있고, 여러 프로세스가 같은 메모리 주소를 공유하도록 해줄 수 있으니까..
DDL 은 예를 들어…
→ 크롬에서 pdf 파일을 열 수 있지만 크롬을 켰을 때는 당장은 pdf 라이브러리를 연결하지 않고, 실제로 pdf 파일을 열 때만 pdf 라이브러리를 메모리에 올리는 것. 등등
연속 메모리 할당
각 프로세스는 다음 프로세스가 적재된 영역과 인접한 하나의 메모리 영역에 배치된다.
- 연속 메모리 할당을 위해 메모리 보호 문제를 해결해야 한다.
메모리 보호
프로세스가 자신이 소유하지 못한 메모리를 접근할 수 없게 강제하자.
- 시스템이 상한 레지스터/재배치 레지스터를 가지고 있다면 가능.
- 재배치 레지스터 : 가장 작은 물리 주소의 값 저장. (예:10040)
- 상한 레지스터 : 논리 주소의 범위 값을 저장 (예: 74600)
- 10040 ~ 74600 이 범위가 프로그램에 할당된 메모리 범위
- MMU(동적→물리 변환) 는 동적으로 논리 주소에 재배치 레지스터 값을 더함으로서 주소를 변환하는 역할.
디스패처 Dispatcher
- 컨텍스트 스위칭을 담당
- 컨텍스트 스위칭의 일환으로 재배치 레지스터와 상한 레지스터에 정확한 값을 적재.
- CPU 에 의해서 생성되는 모든 주소는 이 레지스터들의 값을 참조해서 확인 작업을 거침.
- 이것이 운영체제와 다른 사용자 프로그램을 현재 수행 중인 사용자 프로그램의 접근으로부터 보호하는 방법!
💡재배치 레지스터(논리 → 물리 주소 변환)를 사용함으로서 주소는 실행 중에도 얼마든지 변경될 수 있음을 알 수 있다.
메모리 할당
가변 크기 파티션 할당
가장 간단한 방법은 → 프로세스를 메모리의 가변 크기 파티션에 할당한다.
- 각 파티션에는 정확히 하나의 프로세스만 적재
- 운영체제가 사용 가능한 메모리 부분과, 사용 중인 부분을 나타내는 테이블 유지.
- 프로세스가 시스템에 들어오면, 운영체제는 각 프로세스가 메모리를 얼마나 요구하고, 사용 가능한 메모리 공간이 어디에 얼마나 있는지를 고려해 공간을 할당한다.
- 처음에는 모든 메모리가 사용자 프로세스에 사용 가능하며, 하나의 큰 사용 가능한 메모리 블록인 hole 로 간주한다.
- hole : 나뉘고 합쳐지는 가용 메모리 블럭
- 이처럼 두개의 연속되지 않은 hole 이 생기면, ps5 와 똑같은 양의 메모리를 차지한 ps9 이 있더라도 hole 이 두개로 나뉘어있어 제대로 적재하기 힘든 사태가…
동적 메모리 할당 문제
가용 공간에 메모리 할당을 어떤 식으로 좋게좋게 할 것인가?
- 최초적합
- 첫 번째로 사용 가능한 가용 공간 할당.
- 지난번 검색이 끝났던 곳에서 시작할 수 있으므로 성능 최적화 가능 → 속도가 제일 빠름!
- 최적 적합
- 사용 가능한 공간 중 가장 작은 것을 택한다.
- 처음~끝까지 검색해서 할당 가능한 제일 작은 공간을 사용하는 것.
- 최악 적합
- 가장 큰 가용 공간을 택한다.
- 얘도 처음~끝까지 검색해서 할당 가능한 제일 큰 공간을 사용한다.
→ 최초 적합과 최적 적합이 시간/메모리 이용 효율 측면에서 최악 적합보다 좋다고 입증 됨.
→ 최초 적합/최적 적합은 공간 효율성은 비슷하나 최초 적합은 일반적으로 속도가 더 빠름.
💡다만, 최초 적합과 최적 적합은 모두 외부 단편화(external fragmentation) 로 인해 어려움을 겪는다.
단편화
프로세스들이 메모리에 적재되고 제거되는 일이 반복되면 어떤 가용 공간은 너무 작은 조각이 된다.
- 외부 단편화는 이처럼 유휴 공간들을 모두 합치면 충분한 공간이 되지만 그것들이 너무 작은 조각들로 여러 곳에 분산되어 있을 때 발생.
- 최악의 경우 모든 프로세스 사이마다 못 쓰는 가용 공간 발생 ㄷㄷ
- 이 모든 가용 공간들을 합쳐 하나의 큰 가용 공간을 만들면 여러 프로세스를 실행시킬 수 있을 텐데..
메모리 할당 전략은 단편화 크기에 영향을 받는다.
어떤 시스템에서는 최초 적합이, 다른 시스템에서는 최적 적합이 우수할 수 있음.
- 어느 쪽(위쪽에서 첫 번째 또는 아래 쪽에서 첫 번째) 가용 공간을 할당할 것인가도 고려해야 함.
- 그래도 어떤 알고리즘을 사용하더라도 외부 단편화는 문제로 남는다..
내부 단편화
일반적으로는 메모리를 아주 작은 공간들로 분할하고, 프로세스가 요청하면 할당을 항상 이 분할된 크기의 정수배로만 해주는 것이 보통이다.
- 이 경우 할당된 공간이 요구된 공간보다 약간 더 클 수도?
- 18,464B 크기의 가용 공간에 18,462 공간을 요구한다면 2B 의 hole 이 남을 것.
- 이 남는 부분이 바로 내부 단편화
- 18,464B 크기의 가용 공간에 18,462 공간을 요구한다면 2B 의 hole 이 남을 것.
외부 단편화 해결 법
- 압축
- 메모리 모든 내용을 한군데로 몰고 모든 가용 공간을 다른 한군데로 몰아서 큰 블록을 만드는 것.
- 하지만 압축이 항상 가능한 건 아니다 → 재배치가 어셈블 또는 적재 시에 정적으로 행해지면 압축 실행 불가
- 압축이 가능하더라도 그 비용을 검토해봐야 함.
- 가장 간단한 압축 알고리즘 → 모든 프로세스를 한쪽 끝으로 이동시켜 몰아넣는 것
- 비용이 너무 많이 든다.
- 페이징
- 한 프로세스의 논리 주소 공간을 여러 개의 비연속적인 공간으로 나눠 필요한 크기의 공간이 가용하다면 물리 메모리를 프로세스에 할당.
- 걍 한 마디로 메모리를 분할하여 할당하고 페이징하는 것.
- 그래서 이걸 사용해 문제를 해결한다.
다음 편에서는 페이징으로 뵙겠습니다.^_^
'개발공부 개발새발 > OS' 카테고리의 다른 글
OS ) 파일 시스템 File System (0) | 2025.04.08 |
---|---|
OS ) 메인 메모리 - 2편 : 페이징, 34/64비트, ARM (0) | 2025.02.15 |
OS ) 교착 상태 DeadLocks (0) | 2025.01.23 |
OS ) 동기화 도구 -> 락 (Mutex, 세마포) (0) | 2025.01.21 |
OS ) 스레드 Thread (0) | 2025.01.15 |