[SW정글 63일차] 의도한 예외로 사용자 프로그램 실행을 허용하는 os 완성하기

rg.log·2022년 11월 20일
1

SW 사관학교 JUNGLE

목록 보기
14/31

pintos user program 과제를 진행하며 코드를 까도 까도 생겨나는 궁금증을 공부하며 정리해보았다.
이번 주차는 사용자 프로그램 실행을 허용하는 시스템 부분을 구현해야 한다. 한 번에 여러 프로세스를 로드하고 실행할 때 사용자 프로그램은 전체 기계를 가지고 있다는 착각에 빠져살고, 우리는 그 환상을 유지시켜주기 위해 메모리, 일정 및 기타 상태를 올바르게 관리해야 한다.
코치님께서 넌지시 던진 말과 같이 과제를 시작해 코드를 파다 보니 공부를 해야할 게 보인다.

예외의 종류

1) 인터럽트

프로세서 외부에 있는 입출력 디바이스로부터의 시그널의 결과.
특정 인스트럭션을 실행해 발생한 것이 아니라는 의미의 비동기적이다.

나머지 예외의 종류들은 지금의 인스트럭션을 실행한 결과로 동기적으로 일어난다.

2) 트랩

의도적 예외상황으로, 트랩 핸들러는 제어를 다음 인스트럭션으로 리턴한다.
system call이라고 알려진 사용자 프로그램과 커널 사이의 프로시저와 유사한 인터페이스를 제공한다.

3) 오류

fault는 error와 다르다. 핸들러가 오류 조건을 정정할 수 있기 때문이다.
해서 제어를 오류를 발생시킨 인스트럭션으로 돌려주어 거기서부터 재실행한다.
예를 들어, page fault가 대표적이다. 이는 인스트럭션이 가상 메모리 테이블을 참조했을 때 대응되는 실 메모리 page가 존재하지 않아 디스크에서 가져와야 할 때 발생한다. page fault 핸들러는 디스크에서 적절한 페이지를 로드해서 오류를 발생시킨 인스트럭션으로 제어를 넘겨준다. 이 인스트럭션이 다시 생행될 때는 적절한 페이지가 메모리에 있게 되어 이제는 오류를 발생시키지 않고 동작할 수 있게 된다.

우리 pintos 코드에서는 사용자 프로그램이 커널 가상 메모리에 접근 시도할 때 page_fault 발생,
커널에서 매핑되지 않은 사용자 가상 주소의 메모리에 접근하려 할 때 page_fault가 발생한다.
오늘 벌써 만났다.🤪

4) 중단

대개는 DRAM이나 SRAM이 고장날 때 발생하는 parity error와 하드웨어같이 복구할 수 없는 치명적 에러에서 발생한다.


리눅스/x86-64 시스템 콜

리눅스는 파일을 읽거나 쓸 때, 새로운 프로세스를 만들 때 응용프로그램이 사용할 수 있는 수백 개의 시스템 콜을 제공한다.
C 프로그램은 syscall 함수를 사용해 직접 시스템 콜을 호출할 수 있고, 이를 편리한 wrapper 함수를 통해 제공한다.

x86-64 시스템에서 시스템 콜은 syscall이라 부르는 트랩 인스트럭션을 통해 제공된다.
리눅스 시스템 콜에 전달되는 모든 인자들은 스택보다 범용 레지스터를 통해 이뤄진다.
%rax는 시스템 콜 번호를 보관
%rdi, %rsi, %rdx, %r10, %r8, %r9 에는 최대 6개의 인자들을 보관할 수 있다.
시스템 콜에서 리턴될 때, 레지스터 %rcx, %r11의 값들이 지워지고 %rax가 리턴 값을 보관한다.

코드를 통해 알아보자. 왼쪽 코드에서 작성한 write과 exit 시스템 콜을 직접 호출하는 syscall 인스트럭션을 사용하는 어셈블리 버전을 오른쪽에서 보여준다.

아래는 레지스터의 역할을 정리한 것이다.


프로세스

예외 상황은 프로세스 개념을 OS 커널이 제공할 수 있게 하는 기본 구성 블록이다. 오잉?

자, 프로그램은 실행될 때 마치 프로그램 단 한 개만 시스템에서 돌아간다는 착각한다. 자신이 프로세서와 메모리를 독점해 사용하고 있다고 말이다.

1. 자신이 프로세서를 독점해 사용한다 착각한다.

위와 같이 동작해도 문제가 생기지 않는 건 프로세서가 정지할 때마다 프로그램의 메모리 위치나 레지스터 내용에 변경 사항 없이 프로그램은 멈추었던 인스트럭션에서부터 순차적으로 다시 실행된다.

프로그램은 어떤 프로세스의 문맥(context)에서 돌아간다. 문맥은 프로그램이 정확히 돌아가기 위해 필요한 상태로 구성되어 있는데, 메모리에 저장된 프로그램의 코드와 데이터, 스택, 범용 레지스터의 내용, 프로그램 카운터, 열려 있는 파일 식별자 등을 포함한다.

터미널 쉘에 /bin/ls -l foo bar 라고 입력해서 프로그램(실행 목적파일)을 실행하고자 하면, 쉘은 새로운 프로세스를 생성한다. 이 생성된 프로세스의 문맥에서 해당 파일이 실행된다.

2. 자신이 메모리를 독점해 사용한다 착각한다.

프로세스별로 각각 다른 프로세스에 의해 읽히거나 쓰일 수 없는 자신만의 주소공간을 갖는다.

주소 공간의 아랫부분은 코, 데이터, 힙, 스택 세그먼트를 갖는 사용자 프로그램을 위해 예약되고,
윗부분은 커널을 위해 예약되어 있다. 이 윗부분이 바로 커널이 프로세스를 대신해 인스트럭션을 실행할 때 사용하는 코드, 데이터, 스택을 포함하고 있는 곳이다. 예를 들면, 응용 프로그램이 system call을 실행할 때를 말한다.

+ 사용자 및 커널 모드

OS가 제공하는 완벽한 프로세스 추상화를 위해 프로세서는 응용프로그램이 접근할 수 있는 주소공간뿐 아니라 응용프로그램이 실행할 수 있는 인스트럭션들을 제한하는 방식을 제공해야 한다.

이는 프로세스가 현재 가지고 있는 특권을 저장하는 일부 제어 레지스터로 모드 비트를 제공하는 것이다. 1이면 사용자 모드이고, 0이면 커널 모드를 뜻한다.

모드 비트가 설정되면 프로세스는 커널 모드로 동작한다. 커널 모드에서 돌고 있는 프로세스는 인스트럭션 집합의 어떤 인스트럭션도 실행 가능하며, 시스템 내의 어떤 메모리 위치도 접근 가능하다.
<-> 모드 비트가 세팅되지 않을 때, 프로세스는 사용자 모드에서 돌고 있으므로 프로세서를 멈추고, 모드 비트 변경 및 입출력 연산 초기화같은 특수 인스트럭션을 실행할 수 없다.

잘못된 사용자로부터 OS와 사용자 서로를 보호하기 위해서이다.

대신 사용자 프로그램은 시스템 콜을 통해 커널 코드와 데이터에 간접 접근해야 한다!

응용 코드를 실행하는 프로세스의 처음은 사용자 모드에 있다. 프로세스가 사용자 모드에서 커널 모드로 집입하기 위해선 인터럽트, 오류, 트랩 시스템 콜 같은 예외를 통해서 가능하다.

위 그림에서 process A는 read 시스템 콜을 실행해서 커널에 트랩을 건다. 이와 같이 예외가 발생해 제어가 예외 핸들러로 넘어가면, 프로세서는 사용자 모드에서 커널모드로 변경한다.
커널의 트랩 핸들러는 디스크 컨트롤러에게 DMA 전송을 요청하고, 디스크 컨트롤러가 데이터를 디스크에서 메모리로 전송한 후 프로세서에게 인터럽트를 걸도록 디스크를 제어한다. 이 때 핸들러는 커널 모드에서 돌아간다.
그 후 데이터가 도착했다는 인터럽트를 통해 다시 제어가 응용 코드로 돌아오면 프로세서는 모드를 커널 모드에서 다시 사용자 모드로 변경하며, read 시스템 콜 이후 나오는 인스트럭션 위치로 리턴하는 문맥 전환(context switch)을 수행한다.

제어의 흐름이 이해가 안간다면

인스트럭션 A에 대응하는 주소가 a이라고 하자. a에서 k로의 전환은 제어이동(제어흐름)이라고 한다.
가장 간단한 제어흐름은 점진적인 순서로, a와 b가 메모리에 나란히 있는 경우다.
일반적으로 점진적 흐름에 갑작스런 변화가 생기는 a와 b가 인접해 있지 않을 때는 jump, call 같은 친근한 인스트럭션에 의해 발생한다.

그러나 내부 프로그램 변수에도 표시되지 않는 등의 변화에도 반응해야 한다.
예를 들면 pintos 지난 주차(1주차)에서 보았던 하드웨어 타이머는 규칙적인 간격으로 꺼지는데 시스템은 이걸 처리해줄 수 있어야 한다. 또한 web proxy 주차에서 공부한 자식 프로세스를 생성하는 부모 프로세스는 자신의 자식이 종료될 때 알림을 받아야 한다.

현대 시스템은 제어흐름의 갑작스런 변화를 만드는 방법으로 이런 상황에 반응한다. 이와 같은 급격한 변화를 예외적 제어흐름이라고 한다.
예를 들어, 우리는 운전하다가 갑작스럽게 칼치기하며 들어오는 차에 아무런 반응없이 있어서는 안된다. 브레이크를 밟으면 속도를 늦추든, 클락션을 울리든, 핸들을 틀든 반응을 하며 안전 운전을 진행해야 한다.
컴퓨터도 우리와 같다. 하드웨어 수준에서 검출되는 이벤트들은 예외 핸들러로 갑작스런 제어 이동을 발생시킨다. 마치 운전하던 우리가 생각지 못한 움직임을 발생시켰듯. 또한 운영체제 커널 수준의 문맥 전환을 통해 사용자 프로세스에서 다른 프로세스로 제어가 이동한다.

현재 구현하고 있는 OS의 문맥 전환은 의도한 예외적 제어 흐름이다.

피드백은 언제나 환영입니다.😌

참고. CS:APP 8장

0개의 댓글