프로세서에서 전력이 공급된 시점부터 전력 공급이 끊기는 시점까지 PC는 다음과 같은 값들을 갖는다. a는 명령어 I의 시작 주소를 의미한다. 이와 같이 실행하는 명령어에 따라 PC의 값이 변해가는 흐름을 제어 흐름(Control Transfer)라고 부른다.
제어 흐름의 양상은 크게 3종류이다.
- 메모리에 연속적으로 할당되어 있는 명령어들을 순차적으로 실행하는 경우. 이 경우는 가장 기본적이면서 대부분의 시간을 차지하는 제어 흐름에 해당한다.
- 프로그램 변수로 표현되는 프로그램 상태의 변화에 반응하여 제어 흐름이 갑자기 바뀌는 경우. 대표적으로 jump, call, return 등의 명령어를 수행하는 경우가 이에 해당한다.
- Exceptional Control Flow(ECF). 이는 시스템 상태의 변화에 반응하여 제어 흐름이 갑자기 바뀌는 경우를 말한다.
Exceptional Control Flow. 줄여서 ECF는 컴퓨터 시스템의 모든 수준에서 발생한다. 이를 하드웨어, 운영체제, 응용 프로그램 수준에서 나타날 수 있게 비교해보면 다음과 같다. 다만 이는 엄밀한 구분은 아님을 인지하자.
- 하드웨어 : 하드웨어 수준에서 특정 이벤트의 발생이 감지되면 그 이벤트에 해당하는 예외 핸들러로 제어가 이동된다.
- 운영체제 : 커널은 context switch를 통해서 한 유저 프로세스가 가지고 있던 제어를 또 다른 프로세스에게 넘겨줄 수 있다.
- 응용 프로그램 : 응용 프로그램들은 시스템 콜(또는 트랩)을 통해 운영체제에 집입하여 특정 서비스를 요청할 수 있다.
그럼 ECF를 왜 공부해야 할까?
- ECF를 이해하면 중요한 시스템 개념을 이해하는데 도움이 된다. 왜냐하면, 이것은 운영체제가 입출력, 프로세스, 가상메모리를 구현하기 위해 사용하는 기본 메커니즘이기 때문이다.
- ECF를 이해하면 어떻게 응용들이 운영체제와 상호작용하는지를 이해하는데 도움이 된다. 응용 프로그램은 trap or system call 이라고 알려진 ECF의 한 가지 형태를 사용해서 운영체제에게 서비스를 요청한다. 예를들어, 응용 프로그램은 시스템 콜을 통해 운영체제에게 디스크에 데이터를 쓰거나, 네트워크로부터 데이터를 읽거나, 새로운 프로세스를 만들거나, 현재 프로세스를 종료시키기 위한 서비스를 요청할 수 있다.
- ECF를 이해하면 새로운 application을 작성하는데 도움이 된다. 운영체제는 응용 프로그램에게 여러 가지 동작을 수행할 수 있는 강력한 ECF 매커니즘을 제공한다. 예를들어, 새로운 프로세스를 만들거나 특정 프로세스들이 종료되기를 기다릴 수 있고, 더불어서 다른 프로세스들에게 특정 이벤트의 발생을 알리거나 이와 같은 이벤트를 감지하여 특정 방식으로 반응할 수 있다.
- ECF를 이해하면 동시성을 이해하는데 도움이 된다. 컴퓨터 시스템의 동시성은 ECF를 기반으로 구현된다. 동시성의 대표적인 예시로는 응용 프로그램의 실행을 간섭하는 예외 핸들러나 시그널 핸들러, 그리고 겹치는 시간대에 동시에 실행되는 프로세스나 쓰레드등이 있다.
- ECF를 이해하면 소프트웨어적인 예외상황이 어떻게 동작하는지 이해하는데 도움이 된다. 소프트웨어 예외는 프로그램이 에러 발생시에 비지역성 점프를 하도록 해준다. 비지역성 점프는 응용수준의 ECF이며, C에서는 setjmp, longjmp 함수로 제공된다.
응용프로그램이 운영체제와 상호작용하는 것들은 모두 ECF를 중심으로 돌아간다. 우선, 하드웨어와 운영체제의 교차점에 놓은 예외들이 있고, 응용 프로그램에게 운영체제 내부로 엔트리 포인트를 제공하는 예외인 시스템이 있다, 그 후 추상화 단계를 하나 올라가서 응용 프로그램과 운영체제의 교차점에 위한 프로세스와 시그널이 있다. 마지막 비지역성 점프도 ECF 응용수준의 형태이다.
예외
예외란 프로세서 상태의 변화에 대한 반응으로 나타나는 제어 흐름의 갑작스러운 변화를 뜻하며, 하드웨어와 운영체제의 협력에 의해 구현되는 낮은 수준의 ECF이다.
다음은 보편적인 예외 발생 및 처리 양상을 보여주는데, 프로세서는 명령을 실행하던 도중 특정 프로세서의 상태가 변화했음을 감지한다. 이러한 프로세세 상태의 변화를 이벤트라고 한다. 이때, 이벤트는 명령의 실행과 유관할 수 있고, 무관할 수도 있다.
프로세서는 특정 이벤트의 발생을 감지하는 순간에 예외 테이블이라고 불리는 점프 테이블을 참조해서 해당되는 예외 핸들러로의 간접 프로시저 호출을 수행한다. 그렇게 호출된 예외 핸들러가 처리를 마치면 다음 3가지 동작 중 하나를 수행하게 된다. 여기서 어떤 동작을 수행할지는 예외의 종류에 따라 결정된다.
- I_curr로 리턴
- I_next로 리턴
- abort 루틴으로 리턴하여 해당 프로그램을 종료
예외 처리
한 시스템 내에서 발생할 수 있는 모든 예외는 자신만의 예외 번호를 가진다. 여기서 어떤 예외 번호는 프로세서를 설계하는 사람들에 의해 부여되며, 또 어떤 예외 번호는 커널(메모리에 상주하는 운영체제 코드)을 설계하는 사람들에 의해 부여된다.
- 프로세서를 설계하는 사람들에 의해 부여되는 예외 번호 : divide by zero, page fault 등
- 커널(메모리에 상주하는 운영체제 코드)을 설계하는 사람들에 의해 부여되는 예외 번호 : system call, 외부 입출력 장치에 의한 interrupt
예외처리가 진행되는 순서는 다음과 같다.
- 컴퓨터에 파워가 공급되어 시스템이 부팅되는 순간에 운영체제는 예외 테이블을 메모리에 할당 및 초기화한다. 여기에는 각 예외에 해당하는 예외 핸들러의 주소들이 저장된다.
- 프로세서는 런타임 시에 특정 이벤트의 발생을 감지하면 해당 이벤트의 예외 번호 k를 결정한다.
- 프로세서는 예외 테이블의 K번째 엔트리를 참조함으로써 해당되는 예외 핸들러를 간접적으로 호출한다.
다음 그림은 프로세서가 예외 테이블의 k번째 엔트리에 접근하기 위한 주소를 계산하는 방식을 보여준다. 여기서 예외 테이블의 시작 주소는 예외 테이블 베이스 레지스터(Exception Table Base Register)라고 불리는 특별한 종류의 CPU 레지스터에 저장되어 있다.
그럼 일반적인 함수 호출과 예외 핸들러의 호출 차이는 무엇일까? 그 차이는 다음과 같다.
- 스택에 푸시하는 복귀 주소가 현재 명령어의 주소일 수도 있고, 다음 명령어의 주소일 수도 있다.
- 복귀 주소뿐 아니라 다른 추가적인 프로세서 상태 정보도 스택에 푸시한다. 이는 예외 핸들러가 리턴할 때 모든 프로세서 상태 정보를 되돌려 놓기 위함이다. 실제로 x86-64 CPU는 예외 핸들러 호출 시 컨디션 코드 등의 프로세서 상태 정보를 저장하고 있는 EFLAGS라는 레지스터의 값을 스택에 푸시한다.
- 커널 모드로 실행된다. 즉, 일반적인 함수와 달리 예외 핸들러는 시스템 자원들에 대한 접근 권한을 갖느다. 따라서 데이터들도 유저 스택이 아닌 커널 스택에 푸시가 된다.
일단 하드웨어가 예외 발생을 감지하여 예외 핸들러의 호출까지 마치고나면, 나머지 작업은 예외 핸들러(소프트웨어)에게 맡긴다. 그리고 처리를 끝낸 예외 핸들러는 예외 종류에 따라 원래 프로그램의 실행 흐름으로 돌아가거나 abort 루틴으로 리턴하여 해당 프로그램을 종료시킨다. 만약 돌아가야 하는 상황이라면 "Return from Interrupt"라는 특별한 명령어를 실행하여 원래 프로그램의 실행 흐름으로 돌아가도록 한다. 이 명령어는 스택에 푸시되어 있는 복귀 주소와 각종 프로세서 상태 정보를 pop 하여 원래대로 되돌려놓고, 유저 모드로 돌아가는 경우라면 현재 프로세서의 상태를 유저 모드로 바꿔주는 역할을 수행한다.
예외 종류
예외는 크게 4종류로 구분할 수 있다.
Interrupt
프로세스 외부의 입출력 장치들로부터 전달받는 신호에 의해 발생하는 예외로 명령어의 실행 결과로 발생하는 예외가 아니기 때문에 비동기적 예외에 해당한다. 인터럽트에 해당하는 예외 핸들러는 인터럽트 핸들러라고 부르며, 인터럽트 핸들러는 처리가 끝나면 다음 명령어로 리턴한다. 외부 입출력 장치가 인터럽트를 발생시키는 방법은 간단하다.
Trap (System Call)
특정 명령어를 실행하여 의도적으로 발생시키는 예외로 명령어의 실행 결과로 발생하는 예외이므로 동기적 예외에 해당한다. 트랩에 해당하는 예외 핸들러는 Trap Handler라고 부르며, 트랩 핸들러도 처리가 끝나면 인터럽트 핸들러와 마찬가지로 다음 명령어로 리턴한다.
트랩의 가장 중요한 용도는 일반적인 함수와 유사한 인터페이스로 커널의 서비스를 요청하는 것이다. 이를 System Call이라고 한다. 커널이 제공하는 서비스의 대표적인 예로는 read(파일 읽기), fork(프로세스 만들기), execve(새로운 프로그램 로드하기), exit(현재 프로세스 종료시키기) 등이 있다. 프로세서는 유저 프로그램이 시스템 콜을 통해 이러한 서비스들을 요청할 수 있도록 syscall_n 명령어를 제공한다. 이 명령어를 실행하여 호출되는 예외 핸들러는 레지스터로 전달되는 인자들의 값을 적절히 해석한 뒤, 요청된 커널 루틴을 호출하게 된다. 여기서 인자로 전달되는 시스템 콜의 번호는 요청하려는 시스템 콜의 고유 번호이다. 이는 커널에 존재하는 시스템 콜 테이블에서 요청된 커널 루틴의 주소를 저장하는 엔트리의 인덱스에 해당한다. 주의해야 할 점은 시스템 콜 테이블과 예외 테이블은 다른 것을 기억하자.
Fault
특정 명령어의 실행 결과로 초래된 회복 가능한 에러에 의해 발생하는 예외로 명령어의 실행 결과로 발생하는 예외이므로 동기적 예외에 해당한다. Fault Handler가 예외 핸들러로 에러를 고치는 것에 성공하면 현재 명령어로 리턴하고, 에러를 고칠 수 없으면 커널에 존재하는 abort 루틴으로 리턴하여 해당 프로그램을 종료시킨다. 이러한 Fault의 대표적인 예시는 바로 Page Fault이다. 즉, 명령어가 메인 메모리에 존재하지 않는 가상 페이지에 접근을 시도하면 예외가 발생하여 Page Fault Handler가 호출된다. 핸들러는 디스크에 위치해있는 요청된 가상 페이지를 메인 메모리에 로드하게 되고, 로드 작업이 완료되면 Faulting 명령어로 복귀한다.
Abort
특정 명령어의 실행 결과로 초래된 회복 불가능한 에러에 의해 발생하는 예외로 명령어의 실행 결과로 발생하는 예외이므로 동기적 예외에 해당한다. Abort Handler가 예외 핸들러로 다른 핸들러들과 달리 원래 프로그램의 실행 흐름으로 리턴하지 않고, 무조건 abort 루틴으로 리턴하여 프로그램을 종료시킨다. Paritiy 에러와 같이 치명적인 하드웨어 에러들에 의해 발생하는 예외가 여기에 해당한다.
x86-64 리눅스 예외
다음은 x86-64 리눅스 시스템에서 발생할 수 있는 예외인데, 예외 번호 0 ~ 31은 시스템 설계자가 부여한 번호이고, 예외 번호 32 ~ 255은 리눅스 커널 설계자가 정의한 번호이다. 이 것이 인터럽트 및 트랩에 해당한다.
정의 주체 | 예외 번호 | 설명 | 구분 | 핸들러 동작 |
CPU (x86-64) | 0 | Divide error | Fault | Abort ("Floating exceptions") |
13 | General protection fault | Fault | Abort ("Segmentation faults") | |
14 | Page fault | Fault | Restarting the faulting instruction | |
18 | Machine check | Abort | Abort | |
운영체제 (리눅스) | 32-255 | OS-defined exceptions | Interrupt or trap | - |
다음은 리눅스가 제공하는 시스템 콜의 대표적인 몇가지 사례들이다.
번호 | 이름 | 기능 | 번호 | 이름 | 기능 |
0 | read | Read file | 33 | pause | Suspend process until signal arrives |
1 | write | Write file | 37 | alarm | Schedule delivery of alarm signal |
2 | open | Open file | 39 | getpid | Get process ID |
3 | close | Close file | 57 | fork | Create process |
4 | stat | Get info about file | 59 | execve | Execute a program |
9 | mmap | Map memory page to file | 60 | _exit | Terminate process |
12 | brk | Reset the top of the heap | 61 | wait4 | Wait for a process to terminate |
32 | dup2 | Copy file descriptor | 62 | kill | Send signal to a process |
C 프로그램은 syscall 함수를 실행해서 시스템 콜을 수행할 수 있으나, 보통은 C 표준 라이브러리가 제공하는 Wrapper 함수들을 사용하여 시스템 콜을 수행하게 된다. 이와 같은 시스템 콜 및 그것의 Wrapper 함수들을 사용해서 시스템 콜을 수행한다.
프로세스
프로세스란 실행중인 프로그램의 한 인스턴스를 의미하며, 각 프로세스는 특정 문맥에서 실행된다. 문맥(Context)이란 프로그램이 올바르게 실행되기 위해 필요한 상태 정보들의 집합이다. 예를들어, 메모리의 스택 및 프로그램 코드/데이터, 범용 레지스터, 프로그램 카운터, 환경 변수, 파일 기술자 등이 이에 해당한다.
쉡에서 실행 파일의 이름을 입력하면 쉘은 새로운 프로세스를 하나 생성한 뒤에 그 프로세스의 문맥에서 해당 실행 파일을 실행한다. 이처럼 각 응용 프로그램은 새로운 프로세스를 생성한 뒤 그 문맥에서 자신의 코드 혹은 다른 응용 프로그램의 코드를 실행하는 것이 가능하다.
프로세스라는 개념에 의해 각 응용 프로그램에게 제공되는 핵심적인 2가지 추상화가 있다.
하나는 자신이 프로세스를 독차지하고 있는 듯한 착각을 제공하는 하나의 독립적인 논리적 제어 흐름(Independent Logical Control)이고, 여기서 동시성 흐름의 개념이 나오는데 또 다른 논리적 흐름과 실행이 시간적으로 겹치는 논리적 흐름을 말한다. 그리고 각 프로세스가 프로세서를 돌아가면서 사용하는 현상을 멀티태스킹이라고 부른다. 또한 만약 코어가 여러 개인 CPU에서 서로 다른 코어의 논리적 흐름이 겹치다면 이를 병렬적 흐름(Parallel Flow)라고 부른다.
하나는 자신이 메모리를 독차지하고 있는 듯한 착각을 제공하는 하나의 사적인 주소 공간(Private Address Space)이다.
주소를 n 비트로 표현하는 컴퓨터에서 주소 공간이란 2^n개의 주소로 이루어진 집합을 의미한다. 프로세스는 각 프로그램에게 사적 주소 공간을 제공함으로써 그 프로그램이 메모리를 독차지하고 있는 듯한 착각을 만든다. 또한 각각의 사적 주소 공간의 구조는 모두 동일하다는 특징을 가진다. x86-64 리눅스 시스템에서 각 프로세스는 다음과 같은 구조의 사적 주소 공간을 가진다.
User and Kernel Mode
CPU 특정 컨트롤 레지스터의 Mode Bit는 현재 프로세스의 특권 유무를 나타낸다. 모든 비트가 세팅되어 있으면 Kernel Mode 또는 Supervisor Mode이며, Mode Bit가 세팅되어 있지 않으면 UserMode이다. 여기서 특권 유무가 실행 가능한 명령어의 범위와 접근 가능한 메모리의 범위를 결정한다.
Kernel Mode Process는 특권을 가지기 때문에 어떤 명령어든지 실행할 수 있고, 시스템 내 모든 메모리 주소 공간에 접근할 수 있다.
반면에, User Mode Process는 특권이 필요한 몇몇 명령어들을 실행할 수 없다. 또한 주소 공간 중 커널 영역의 데이터와 코드에 직접 접근할 수 없다. 대신에 시스템 콜 인터페이스를 통해 간접적으로만 커널 영역의 데이터와 코드에 접근할 수 있다.
물론 최초에 응용 프로그램이 실행될 때 생성되는 프로세스는 User Mode이다. 그리고 프로세스가 User Mode에서 Kernel Mode로 바뀌는 경우는 interrupt, fault, system call 등의 예외가 발생할 때이다. 예외가 발생하면 제어가 예외 핸들러로 넘어가면서 프로세서에 의해 커널 모드로 바뀌고, 응용 프로그램의 코드로 돌아갈 때 프로세서에 의해 모드가 다시 유저 모드로 바뀐다.
Context Switch
문맥 전환은 멀티태스킹을 구현하는 기본적인 매커니즘으로 운영체제 커널에 의해 수행이 되는 높은 수준의 ECF에 해당한다. 문맥 전환 매커니즘은 낮은 수준의 ECF에 해당하는 예외 매커니즘을 기반으로 구현된다.
커널은 각 프로세스의 문맥을 관리한다. 문맥은 커널이 잠들어있는 프로세스를 다시 실행하는데 필요한 모든 상태 정보를 의미한다.
프로세스 A를 실행하고 있을 때 또 다른 프로세스로 제어가 넘어가야 하는 특정 이벤트가 발생하면 커널의 Scheduler 루틴이 호출된다. 이때 스케쥴러는 프로세스 A를 잠들게 하고, 현재 잠들어 있는 다른 프로세스들 중에서 새로 실행할 프로세스 B를 선택한다. 이러한 과정을 스케쥴링이라고 한다. 그리고 이를 프로세스 B가 스케쥴러에 의해 스케쥴되었다고 한다. 이렇게 새로 실행할 프로세스 B가 선택되면, 스케쥴러는 프로세스 A에서 프로세스 B로의 문맥 전환을 수행한다. 이렇게 Context Switch는 현재 프로세스의 문맥을 저장하고, 새로 실행할 프로세스의 문맥을 복원하며, 제어를 해당 프로세스로 넘겨준다.
그렇다면 문맥 전환은 언제 발생하는가?
- 커널이 system call을 수행하고 있을 때 발생할 수 있다. 만약 어떤 시스템 콜이 특정 이벤트의 발생을 기다린다면, 이는 스케쥴러를 호출하여 현재 프로세스를 잠들게 하고, 다른 프로세스에게 제어를 넘겨주는 문맥 전환을 수행한다. 예를 든다면 read system call은 디스크에게 요청한 데이터가 도착할 때까지 다른 프로세스로의 문맥 전환을 수행하고, sleep system call은 정해진 시간 동안 현재 프로세스를 잠들게하고, 다른 프로세스로의 문맥 전환을 수행한다.
- 또한 문맥 전환은 interrupt의 결과로 발생할 수 있다. 예를 들어, 시스템들은 주기적으로 타이머 interrupt를 발생시키는 매커니즘을 가지는데, 타이머 인터럽트가 발생하면 커널은 현재 프로세스가 너무 오래 실행되었다고 판단하고 새로운 프로세스로의 문맥 전환을 수행한다.
인용
https://it-eldorado.tistory.com/51
'책 - 요약 정리 > CSAPP' 카테고리의 다른 글
Network Programming (0) | 2023.11.17 |
---|---|
Virtual Memory (0) | 2023.11.08 |
linking (0) | 2023.11.07 |
CSAPP - Procedures (1) | 2023.11.01 |