본문 바로가기
일상,취미

[TIL 35일차] 운영체제의 가상화

by 진득한진드기 2024. 8. 14.

오늘 한일

오늘은 펌웨어 추출을 위한 스크립트 수정을 하느라 하루를 다 보낸 것 같다.

 

애초에 windows에서 binwalk나 unblob 같은 추출 기능들이 사용이 불가능하니 결국 docker에 올려서 추출하기로 했다.

 

docker 설치하고.... 띄우고 docker 에서 command로 호출하는 shell 파일은 따로 파일별로 핸들링하는 python코드로 작성되어있다. 근데 shell파일에서 시스템 콜인 exec로 python "loacl file_path" 형식으로 호출하게 되면 exec는 해당 프로세스를 종료하고 특정 프로세스를 실행하기 때문에 그 뒤에 있는 코드들이 실행되지 않을수도 있다.

 

특정 파이썬 파일을 실행하고 이후에 실행될 코드가 존재한다면 python file_name 형식으로 실행하여 실행흐름을 이어가야한다.

 

 

퇴근 후 공부

기초 소켓 프로그래밍 책을 다 읽어서 프로젝트를 해볼까 하다가...... 이미 미러링 데이터 송수신 로직 처리는 이미 다했고, 멀티 서버 모델이 아니기 때문에 과하게 배우는 느낌이 강했다. 게임 서버 개발을 하는거 아니면 취미 영역이기에.... 

 

그래서 전에 봤던 1회독 했던 운영체제 책을 다시 보려고 한다.

 

남들은 공룡 책을 봤다고 하지만 나는 운영체제 아주 쉬운 세 가지 이야기 라는 책을 본 경험이 있다.

 

공룡책의 1100쪽에 비하면 얇지만 이것도 800쪽 가까이 되서..... 만만치는 않다.

 

애초에 운영체제 책들 자체가 특정 목적으로 가지고 집필한 느낌이 강한 것들이 많다.

 

운영체제 보안을 중점적으로 집필한 책도 있고, 운영체제 네트워크 관련되서 집필된 책도 있고....

 

내가본 이 책은 정말 어려운 운영체제의 내용에서 4가지 파트를 쉽게 알려주고 싶은 생각이 드는 책이다.

 

1. 프로세스,쓰레드 (스케줄링), 2. 시스템 콜. 3. 가상 메모리 4. 파일 시스템

 

이외에도 다른 파트들이 있지만 가장 알려주고 싶다는 생각이 드는 키워드는 저렇게 4개 였다.

 

실제로 책에도 운영체제는 사용자에게 3가지 환상을 준다고 적혀있다.

 

1.가상화

2.병행성

3.영속성 

 

가상화 파트에서는 프로세스가 동시에 일어나는 것 같은 환상, 메모리를 보다 크고 확장성 있게 사용하는 가상메모리(가상화)가 있고, 

병행적으로(병렬적) 돌아갈 때의 데이터를 어떻게 동기화 할 것인가를 자세히 알려주고 해당 코드도 대략적으로 알려준다.

영속성에서는 파일 시스템을 어떻게 동작하고 관리 하는지를 알려준다.... 데이터 손실 방지를 위한 저널링이라던가.... 하는 것들을 말이다.

 

책에 아주 자세히 적혀져 있는것은 아니지만 보면 컴퓨터에 대한 흐름이 어느정도는 잡히는 느낌도 들고 운영체제의 대단함을 느낄 수 있는 책인 것 같다.

 

컴퓨터 공부를 어느정도 했다면 요약을 먼저 보고 내용을 훑으면서 봐도 좋은 책인것 같다.

 

2회독은 빠르게 할 수 있기를 바라며..... 

 

일반적인 컴퓨터 모델의 동작

 

요즘 마이크로프로세서는 다음과 같이 CPU에서 동작한다.
반입(fetch) -> 해석(decode) -> 실행(execute)

이를 깊게 생각할 필요는 없고 우리가 필요한 부분을 추상화해서 보아야 한다.

너무 어렵다고 생각할 수 있지만 우리는 운영체제에서 제공하는 표준화된 함수를 제공해서 장치에 접근이 가능하다.그리고 그것들을 사용해서 관련된 동작을 일으킬수 있다.

운영체제를 사용자 편의적이게 만드는건 당연하지만 이게 1순위는 아니다.
1순위는 운영체제에서 중요한건 오버헤드 최소화이다.

그렇다고 저 두개가 다라고 생각하면 오산이다. 예기치 못한 상황도 신경써야한다.
예를 들어 쓰기 중에 시스템에 중간에 고장이 날 수 있으므로 최근의 운영체제들은 저널링이나 쓰기 시 복사 와 같은 기법이 사용되고 있다.

운영체제를 하나의 라이브러리도 볼수있다.

근데 운영체제는 라이브러리라고 하면 파일 시스템과 같은 자료의 기록과 보호라는 역할을 수행하기 힘들어진다.

Atlas 라는 컴퓨팅 시스템에 의해 시스템 콜이라는 아이디어가 발명되고, 이는 일반 라이브러리와 운영체제가 구분되는 사건이다.

시스템과 프로시저 호출의 결정적 차이는 시스템 콜은 제어를 운영체제에게 넘길 때 하드웨어 특권 수준을 상향 조정한다는 것이다.

사용자 응용프로그램은 사용자 모드라고 불리는 상태에서 실행된다.

응용프로그램은 패킷송신이 불가능 한것처럼 사용 영역을 격리하는 것이다.

우리가 아는 시스템 콜은 보통 트랩(trap)이라고 하는 특별한 하드웨어 명령어를 이용하여 호출한다.

하드웨어는 미리 지정된 트랩 핸들러(trap handler) 함수에게 제어권을 넘기고 특권 수준을 커널 모드로 격상 시킨다.

입출력 또는 메모리 할당 등과 같은 작업이 가능해지고, 서비스를 완료하면 return-from-trap 특수 명령어를 사용하여 사용자에게 제어권을 넘긴다.

하드웨어는 trap 명령어를 수행할 때 주의가 필요하다. 호출된 프로세스의 필요한 레지스터들을 저장해야 한다. 운영체제가 return-from-trap 명령어 실행 시 사용자 프로세스로 제대로 리턴할 수 있도록 하기 위함이다. 

 

예를 들면 x86에서는 프로그램 카운터, 플래그와 다른 몇 개의 레지스터를 각 프로세스의 커널 스택 에 저장한다. 

 

return -from-trap 명령어가 이 값들을 스택에서 pop하여 사용자 모드 프로그램의 실행을 다시 시작한다.

 

커널은 부팅시 트랩 테이블을 만들고 이를 이용해서 시스템을 통제한다. 자세한 내용은 밑에서 말하겠다.

 

그리고 해당 운영체제 책에는 나오지 않았지만 커널 스택은 단 하나를 공유하는 것이 아닌 프로세스마다 따로 존재한다. 오해하지말길.....

 

 

동시성 및 가상화

 

CPU를 가상화하기 위해서 운영체제는 여러 작업들이 동시에 실행되는 것처럼 보이도록 물리적 CPU를 공유함.

CPU 시간을 나누어 씀으로써 가상화처럼 보이게 (가상화)가 가능하게 하는것

이렇게 너무 자주 나눠가지게 되면 성능저하가 올 수 있고, 두번째로 제어 문제가 생긴다.

CPU에 대한 통제를 유지하면서 프로세스를 효율적으로 실행시킬 수 있는 방법은 무엇일까? 생각해보자

운영체제는 자원 관리 책임자로서 제어 문제는 굉장히 중요하다.


기본 원리 : 제한 적 직접 실행


운영체제 개발자들은 프로그램을 빠르게 실행하기 위해 제한적 직접 실행(Limited Direct Execution)이라는 기법을 개발했다.

프로그램을 CPU상에서 직접 실행하는 것이다.

따라서 운영체제가 프로그램을 실행하기 시작할 때 프로세스 목록에 해당 프로세스 항목을 만들고 메모리를 할당하며 프로그램 코드를 디스크에서 탑재하고 진입점 루틴 혹은 비슷한 무엇을 찾아 그 지점으로 분기하여 사용자 코드를 실행하기 시작한다.

실행 순서는 다음과 같다.

1. 프로세스 목록의 항목을 생성
2. 프로그램 메모리 할당
3. 메모리에 프로그램 탑재
4. argc/argv를 위한 스택 셋업
5. 레지스터 내용삭제
6. call main() 실행
7. main에서 return 명령어 실행
8. 프로세스 메모리반환
9. 프로세스 목록에서 항목 제거


근데 이렇게 처리하게 되면 CPU 가상화에 있어 결함을 일으킬 수 있다.
프로그램을 직접 실행시킨다면 프로그램이 운영체제가 원치 않는 일을 하지 않는다는것을 보장 할 수 없다.

두 번째로는 프로세스 실행 시, 운영체제는 어떻게 프로그램의 실행을 중단하고 다른 프로세스로 전환할 것인가?
즉 시분할로 어떻게 구현할 것인가?

문제점을 개선해가며 생각해보자.


문제점 1 : 제한된 연산


직접 실행의 장점은 빠르게 실행, 즉 속도이다. 기본적으로 프로그램이 하드웨어 CPU에서 실행되기 때문이다.
그러나 CPU에서 직집 실행시키면 새로운 문제가 발생한다. 만일 프로세스가 특수한 종류의 연산을 수행하기를 원한다면 어떻게 될 것인가? 이러한 연산에는 디스크 입출력 요청이나 CPU 또는 메모리 같은 시스템 자원에 대한 추가할당 요청 등이 포함된다.

프로세스가 디스크에 대하여 입출력하는 것을 제한하지 않으면 프로세스는 전체 디스크를 읽고 쓸 수 있기 때문에 접근 권한을 검사하는 기능이 의미가 없어진다.

이 때문에 사용자 모드 (user mode)라고 알려진 새로운 모드가 도입되었다.
사용자 모드에서 실행되는 코드는 할 수 있는 일이 제한된다.


문제점 2 : 프로세스간 전환


직접 실행의 두 번째 문제점은 프로세스 전환이 가능해야한다는 것.

프로세스의 전환을 당연히 간단해야한다. 프로세스의 전환은 실행 중인 프로세스를 멈추고 다른 프로세스를 실행하는 것이다. 말로 쓰자면 간단해보이지만, 실제로는 매우 까다로운 문제이다. 프로세스가 실행중이라는 것은 운영체제는 실행 중이지 않다는 것을 의미한다. 운영체제가 실행하고 있지 않다면 어떻게 프로세스를 전환할까?

운영체제가 실행 중이 아니라면 운영체제는 어떠한 조치도 취할 수 없을텐데 어떻게 이게 가능할까?


협조방식 : 시스템 콜 호출시 까지 대기


협조(cooperative) 방식으로 알려진 방법은 과거의 몇몇 시스템에서 채택되었던 방식이다. 매킨토시 운영체제 초기 버전도 채택했었다. 이 방식은 각 사용자 프로세스가 비정상적인 행동은 하지 않을 것으로 간주한다.

CPU를 장기간 사용해야하는 프로세스들은 다른 프로세스들이 CPU를 사용할 수 있도록 주기적으로 CPU를 반납할거라 믿는다.

마치 착한 사람들이 사는곳처럼

각 레지스터 값들을 저장해줘야한다. 그래야 CPU를 반납했던 프로세스가 추후에 다시 실행을 계속할 수 있다. CPU 반납 문제의 핵심은 응용프로세스가 어떻게 제어권을 운영체제에게 넘기느냐 로 결정된다.

대부분의 프로세스는 시스템 콜을 자주 호출하는 것을 알려져있다. 시스템 콜을 호출하면 자연스럽게 운영체제 코드가 실행되며, 제어권이 운영체제로 넘어가게 된다. 파일 열기 읽기 다른 컴푸터에 메시지 송신 등 할때 시스템 콜이 호출된다. 협조 방식을 사용하는 운영체제는 yield 시스템 콜을 제공한다.

이 시스템 콜은 운영체제에게 제어를 넘겨 응용 프로그램이 비정상적인 행위를 하게 되면 운영체제에게 제어가 넘어간다.

예를 들어 응용프로그램이 어떤 수를 0으로 나누는 연산을 실행하거나 허가되지 않는 메모리에 접근을 시도하면 운영체제로 trap이 일어난다. 그러면 운영체제는 다시 CPU를 획득하여 해당 프로세스를 종료할 수 있다.

협조방식은 근본적으로 수동적이다. CPU 제어권 획득을 위해 운영체제는 시스템 콜이 호출되기를 기다리거나 불법적인 연산이 일어나기를 대기하는 것이다. 실수든 의도적이든 프로세스가 무한 루프에 빠져 시스템 콜을 호출 할 수 없다면 문제가 발생한다.


비협조 방식 : 운영체제가 제어권 확보 


프로세스가 시스템 콜을 호출하지 않을 경우, 제어권을 어떻게 하면 운영체제로 넘길 수 있을까 생각해보자.

하드웨어의 도움을 받는 방법이다.

협조 방식 운영체제의 경우 프로세스가 무한 루프에 빠졌을때 이를 해결할 수 있는 방법이 1가지 뿐이다.  =  재부팅 

시스템 콜의 호출이 없더라도 운영체제에게 제어권을 넘길 수 있는 방법은 뭐일까?

이는 타이머 인터럽트라는 것을 사용한다.

타이머는 수 밀리초마다 인턴럽트라 불리는 하드웨어 신호를 발생시키도록 프로그램 가능하다.

인터럽트가 발생했을 때 이를 처리하는 것은 운영체제의 가장 중요한 역할중 하나이다.

인터럽트가 발생하면 운영체제는 현재 수행 중인 프로세스를 중단시키고 해당 인터럽트에 대한 인터럽트 핸들러를 실행한다.

운영체제는 타이머 인터럽트가 발생 시 실행해야 할 코드의 주소를 기록해두어야한다. 

컴퓨터 부팅시, 운영체제는 컴퓨터에서 정의된 각 인터럽트에 대해, 관련 인터럽트 핸들러의 위치를 테이블 형태로 메모리에 초기화 시킨다.

부팅 시, 운영체제는 타이머를 시작한다. 타이머가 시작되면 타이머 인터럽트가 발생할 때마다 제어권이 운영체제에게 넘어간다.

그러면 프로그램이 비정상적인 움직임을 가져가도 제어권을 가져올 수 있어 적절히 처리가 가능하다.

물론 타이머를 끌 수 도있다.

인터럽트 발생 시에는 시스템 콜 호출과 동일하기 실행 중이던 프로그램의 상태를 저장한다. 추후 return-from-trap 명령어가 프로그램을 다시 시작할 수 있도록 하기 위함이다. 이 작업은 하드웨어적으로 실행된다. 다양한 레지스터가 커널 스택에 저장되고, return-from-trap 명령어를 통하여 복원된다.

 


문맥의 저장과 복원

 


운영체제가 제어권을 다시 획득하면 중요한 결정을 내려야 한다. 현재 실행중인 프로세스를 계속 실행할 것인지 다른 프로세스로 전환할것인지.

이 결정은 운영체제의 스케줄러라는 부분에 의해 내려진다. 

결정되면 문맥교환이라 불리는 코드를 실행한다.

문맥교환(context switching)은 현재 실행 중인 프로세스의 레지스터의 값들을 커널 스택 같은 곳에 저장하고 새로 실행될 프로세스의 커널 스택으로부터 레저스터 값을 복원하는 것이 전부다. 그렇게 함으로써 운영체제는 return-from-trap 명령어가 마지막으로 실행 될 떄 현재 실행중이던 프로세스로 리턴하는 것이 아니라 다른 프로세스로 리턴하여 실행으르 다시 시작할 수 있다.

현재 프로세스의 문맥(레지스터 값들)을 메모리에 저장하고 새로이 실행될 프로세스 문맥을 CPU로 읽어들이는 작업은 주로 어셈블리 코드를 사용하여 작성된다. 실행속도 때문에.....

운영체제는 현재 실행 중인 프로세스의 범용 레지스터, PC뿐만 아니라 현재 커널 스택 포인터를 저장한다. 새로 실행될 프로세스의 범용 레지스터, PC, 커널 스택으로 전환 한다. 이런식으로 하면 운영체제가 마지막으로 return-from-trap 명령어를 실행하면 새로이 실행될 프로세스의 커널 스택에서 레지스터 값들을 복원하여 새로운 프로세스의 실행이 시작된다. \


병렬 실행으로 인한 문제


만약 시스템 콜을 처리하는 도중에 타이머 인터럽트가 발생하면 어떡하지 라는 생각을 할 수 있다.
또는 하나의 인터럽트를 처리하고 있을 떈 다른 인터럽트가 발생하면 어떡하지란 생각 또한....

이는 실제로 발생하고 처리하는 도중에는 세삼한 주의 가 필요하다.

간단하게 생각하면 인터럽트를 처리하는 동안 인터럽트를 불능화 시키는 것이다.

하나의 인터럽트가 처리되고 있는 동안에는 다른 어떤 인터럽트도 CPU에게 전달되지 않게 하는 것.

이는 신중하게 사용해야한다. 인터럽트가 장기간 불능화 되면 손실되는 인터럽트가 생기게 된다. 운영체제는 내부 자료구조가 동시에 접근되는 것을 방지하기 위해 다양한 락 기법을 개발해 왔다.

커널 내부의 각종 자료 구조들이 락으로 보호 되기 떄문에, 커널 내부에서 다수의 작업들이 동시에 진행되는 것이 가능하다.
병행성 부분에서 살펴보겠지만 이런 락의 사용으로 인해 운영체제 전체의 구성과 작동이 매우 복잡해질 수 있다.


요약


- CPU는 최소한 두 가지 실행 모드를 지원해야한다. 제한 적인 사용자 모드, 특권을 가진 커널모드가 있다.

- 일반적인 응용프로그램은 유저모드에서 실행되며 시스템 콜을 사용하여 커널로 트랩행 운영체제의 서비스를 요청한다.

- 트랩 인스트럭션은 레지스터 상태를 저장하고, 하드웨어 상태를 커널모드로 변경하며 운영체제내의 트랩테이블로 이동한다.

- 트랩 테이블은 부팅 시 OS에 의해 설정되고, 사용자 프로그램에 의해 쉽게 수정 될수 없는지 확인해야한다. 

이 모든 것은 프로그램을 효율적으로 실행하지만 운영체제 제어를 잃지 않는 제한적 직접 실행 방식의 일부이다.

- 일단 프로그램이 실행 되면 OS는 유저프로그램이 영원히 실행되는 것을 막기 위해 하드웨어 메커니즘인 타이머 인터럽트를 사용한다. 이것은 비협조적 방식의 CPU 스케줄링이다.

- 때때로 OS는 타이머 인터럽트나 시스템 콜 실행중에 현재 프로세스에서 다른 프로세스로 전환 할 수 있다. 이를 컨텍스트 스위치(문맥 전환)이라고 한다.