[WEEK 07] 컴퓨터 시스템 - 8. 예외적인 제어흐름

신호정 벨로그·2021년 9월 25일
0

Today I Learned

목록 보기
34/89

프로세서에 전원을 처음 공급하는 시점부터 전원을 끌 때까지 프로그램 카운터는 연속된 값들을 가정한다.

a(0), a(1), ..., a(n-1)

인스트럭션 I(k)에 대응되는 주소a(k)이다.

a(k)에서 a(k+1)로의 전환제어이동이라고 부른다.

제어이동의 배열은 제어흐름 또는 프로세서의 제어흐름이라고 부른다.

각각의 I(k)와 I(K+1)이 메모리에 서로 나란히 있는 경우, 가장 간단한 유형의 점진적인 순서제어흐름이다.

현대의 시스템들은 제어흐름의 갑작스럽 변화를 만드는 방법으로 예외적인 제어흐름(ECF: Exceptional Control Flow)이라 하는 급격한 변화에 반응한다.

  1. ECF를 이해하면 중요한 시스템 개념을 이해하는 데 도움이 된다. ECF는 운영체제가 입출력, 프로세스, 가상메모리를 구현하기 위해 사용하는 기본 메커니즘이다.

  2. ECF를 이해하면 어떻게 응용이들이 운영체제와 상호작용하는지를 이해하는 데 도움이 된다. 응용이는 트랩(trap) 또는 시스템 콜(system call)이라고 알려진 ECF의 한 가지 형태를 사용해서 운영체제로부터 서비스를 요청한다.

  3. 운영체제는 새로운 프로세스를 만들거나 프로세스가 종료하기를 기다리거나 반응하는 등의 작업을 위한 강력한 ECF 메커니즘을 응용프로그램에 제공한다.

  4. ECF를 이해하면 동시성을 이해하는 데 도움이 된다. ECF는 컴퓨터 시스템에서 동시성을 구현하는 기본 메커니즘이다.

  5. ECF를 이해하면 소프트웨어적인 예외상황이 어떻게 동작하는지 이해하는 데 도움이 될 것이다.

8.1 예외상황

예외상황은 부분적으로는 하드웨어와 운영체제에 의해서 구현된 예외적인 제어흐름의 한 가지 형태다.

예외상황어떤 프로세서 상태의 변화에 대한 대응으로, 제어흐름의 갑작스럽 변화다.

어느 경우이든지 프로세서가 이벤트가 발생했다는 것을 감지하면, 예외 테이블이라고 하는 점프 테이블을 통해서 이 특정 종류의 이벤트를 처리하기 위해 특별히 설계된 운영체제 서브루틴(예외처리 핸들러)으로 간접 프로시저 콜을 하게 된다.

예외처리 핸들러가 처리를 끝마치면, 예외상황을 발생시킨 이벤트의 종류에 따라서 세 가지의 경우가 발생한다.

  1. 핸들러는 제어를 현재 인스트럭션 I(curr)로 돌려준다. 이 인스트럭션은 이벤트가 발생했을 때 실행되고 있던 인스트럭션을 말한다.

  2. 핸들러는 제어를 I(next)로 돌려주는데, 이 인스트럭션은 예외상황이 발생하지 않았더라면 다음에 실행되었을 인스트럭션이다.

  3. 핸들러는 중단된 프로그램을 종료한다.

8.1.1 예외처리

한 시스템 내에서 가능한 예외상황의 종류마다 중복되지 않는 양의 정수를 예외번호로 할당하고 있다.

이 숫자들의 일부는 프로세서 설계자가 부여하고, 나머지 번호는 운영체제 커널(운영체제의 메모리가 상주하는 부분) 설계자가 할당한다.

시스템 부팅 시, 운영체제는 예외 테이블이라고 하는 점프 테이블을 할당하고 초기화해서 엔트리 k가 예외상황 k에 대한 핸들러의 주소를 갖는다.

런타임(시스템이 프로그램을 실행하고 있을 때)에 프로세서는 이벤트가 발생했다는 것을 감지하고, 대응되는 예외번호 k를 결정한다.

8.1.2 예외의 종류

  1. 인터럽트

인터럽트는 프로세서 외부에 있는 입출력 디바이스로부터의 시그널의 결과비동기적으로 발생한다.

하드웨어 인터럽트를 위한 예외 핸들러는 인터럽트 핸들러라고 부른다.

인터럽트 핸들러는 응용 프로그램의 제어흐름에서 다음 인스트럭션으로 제어를 돌려준다.

  1. 트랩과 시스템 콜

트랩의도적인 예외상황으로 어떤 인스트럭션을 실행한 결과로 발생한다.

인터럽트 핸들러와 마찬가지로 트랩 핸들러는 제어를 다음 인스트럭션으로 리턴한다.

시스템 콜은 사용자 프로그램과 커널 사이의 프로시저와 유사한 인터페이스를 제공하는 것이다.

트랩 핸들러는 응용 프로그램에서 제어를 다음 인스트럭션으로 돌려준다.

  1. 오류(fault는 error와 다르다)

오류핸들러가 정정할 수 있을 가능성이 있는 에러 조건으로부터 발생한다.

오류가 발생하면 프로세서는 제어를 오류 핸들러로 이동해준다.

오류 핸들러는 오류가 복구될 수 있는지 여부에 따라 오류를 발생시킨 인스트럭션을 재실행하거나 중단한다.

  1. 중단

중단은 대개 DRAM이나 SRAM이 고장날 때 발생하는 패리티 에러하드웨어 같은 복구할 수 없는 치명적인 에러에서 발생한다.

중단 핸들러는 응용 프로그램을 종룔하는 커널의 중단처리 루틴으로 제어를 전달한다.

8.1.3 리눅스/x86-64 시스템에서의 예외상황

리눅스/x86-64 오류와 중단

  1. 나누기 에러

  2. 일반 보호 오류

  3. 페이지 오류

  4. 머신 체크

리눅스/x86-64 시스템 콜

8.2 프로세스

프로세스의 고전적인 정의는 '실행 프로그램의 인스턴스'이다.

시스템 내의 각 프로그램은 어떤 프로세스의 문맥에서 돌아가며, 문맥은 프로그램이 정확하게 돌아가기 위해서 필요한 상태로 구성된다.

8.2.1 논리적인 제어흐름

만일 프로그램의 실행을 단일 스텝으로 실행하기 위해서 디버거를 사용하고자 한다면, 우리의 실행 목적파일 내에 들어 있거나 프로그램과 동적으로 런타임에 링크된 공유 객체 내의 인스트럭션들에게 일련의 프로그램 카운터 PC 값들이 대응된다.

프로그램 카운트 값들의 배열논리적 제어흐름이라고 부른다.

하나의 프로세서를 사용해서 여러 프로세스들이 교대로 돌아간다.

프로세서가 정지할 때마다 프로그램의 메모리 위치나 레지스터의 내용에 변경되는 사항없이 프로그램 실행은 순차적으로 다시 실행된다.

8.2.2 동시성 흐름

공동으로 실행되는 흐름의 일반적인 현상을 동시성이라고 부르며, 자신의 실행시간이 다른 흐름과 겹치는 논리흐름을 동시성 흐름이라고 부른다.

8.4 프로세스의 제어

8.4.1 프로세스 ID 가져오기

각각의 프로세스는 고유의 양수 프로세스 ID(PID)를 가진다.

getpid 함수는 호출하는 함수의 PID를 리턴하며, getppid 함수는 자신의 부모의 PID를 리턴한다.

getpid와 getppid 루틴은 pid_t 타입의 정수 값을 리턴하며, 이 타입은 리눅스 시스템에서 types.h 정수로 정의되어 있다.

8.4.2 프로세스의 생성과 종료

프로세스는 실행 중, 정지, 종료 세 가지의 상태 중의 하나이다.

  1. 실행 중: 프로세스는 CPU에서 실행하고 있거나 실행을 기다리고 있으며, 궁극적으로 커널에 의해서 스케줄될 것이다.

  2. 정지: 프로세스의 실행은 정지한 상태이고 스케줄되지 않는다.

  3. 종료: 프로세스는 영구적으로 정지된다. 프로세스는 세 가지 이유 중의 하나로 종료된다.

(1) 프로세스를 종료하는 시그널을 받았을 때
(2) 메인 루틴에서 리턴할 때
(3) exit 함수를 호출할 때

exit 함수는 종료 상태 status로 프로세스를 종료한다.

부모 프로세스는 fork 함수를 불러서 자식 프로세스를 생성한다.

자식 프로세스는 코드, 데이터 세그먼트, 힙, 공유된 라이브러리, 사용자 스택을 포함하는 부모의 사용자 수준 가상 주소공간과 동일하지만 분리된 복사본을 갖는다.

부모와 새롭게 생성된 자식 간의 가장 중요한 차이는 이들이 서로 다른 PID를 가진다는 것이다.

fork 함수

/* fork 함수 */
#include <stdio.h>

int main() {
    pid_t pid;
    int x = 1;

    pid = fork();

    /* Child Process */
    if (pid == 0) {
        printf("child: x = %d\n", ++x);
        exit(0);
    
    /* Parent Process */
    printf("parent: x = %d\n", --x);
    exit(0);
    }
}

fork 함수는 부모에 의해서 한번 호출되지만 리턴은 두 번 한다.

부모와 자식은 동시에 돌아가는 별도의 프로세스들이다.

각 프로세스는 동일한 사용자 스택, 지역변수 값들, 힙, 전역변수 값, 동일한 코드를 가진다.

그러나 부모와 자식이 별도의 프로세스이므로 자신만의 사적 주소공간을 가진다.

8.4.5 프로그램의 로딩과 실행

execve 함수

execve 함수는 실행가능 목적파일 filename인자 리스트 argv, 환경변수 리스트 envp을 사용해서 로드하고 실행한다.

/* execve 함수 */
#include <unistd.h>

int execve(const char *filename, const char *argv[], const char *envp[]);

execve 함수는 파일 이름을 찾을 수 없는 에러가 있는 경우에만 호출하는 프로그램으로 리턴하기 때문에 execve 함수는 한 번 호출되고 리턴하지 않는다.

getenv 함수

getenv 함수는 환경 배열에서 "name=value" 스트링을 검색한다. 찾게 되면 해당하는 값에 대한 포인터를 리턴하고, 그 외의 경우에는 NULL을 리턴한다.

/* getenv 함수 */
#include <stdlib.h>

char *getenv(const char *name);

setenv 함수

환경 배열이 "name=oldvalue" 형태의 스트링을 포함하면, unsetenv 함수는 이것을 삭제하고 setenv 함수는 oldvalue를 newvalue로 교체한다. overwriterk 0이 아닌 경우에만 그렇다. 만일 name이 없다면 setenv 함수는 "name=newvalue"를 배열에 추가한다.

/* setenv 함수 */
#include <stdlib.h>

int setenv(const char *name, const char *newvalue, int overwrite);

void unsetenv(const char *name);

프로그램과 프로세스

프로그램은 코드와 데이터가 합쳐진 것으로 디스크 상에 목적파일이나 주소공간에 세그먼트로 존재할 수 있다.

프로세스는 실행 중에 있는 프로그램의 특정 사례다.

프로그램은 항상 어떤 프로세스의 컨텍스트 내에서 돌아간다.

fork 함수는 부모의 복제인 새로운 자식 프로세스에서 동일한 프로그램을 실행시킨다.

execve 함수는 새 프로그램을 현재 프로세스의 컨텍스트 내에서 로드하고 실행한다.

새 프로그램은 여전히 같은 PID를 가지며, execve 함수를 호출할 때 열려 있던 모든 파일 식별자를 물려받는다.

8.5 시그널

시그널은 프로세스와 커널이 다른 프로세스를 중단하도록 하는 상위수준의 소프트웨어 형태의 예외적 제어흐름이다.

8.5.1 시그널 용어

시그널을 목적지 프로세스로 전달하는 것은 두 단계로 이루어진다.

  1. 시그널 보내기: 커널은 목적지 프로세스의 컨텍스트 내에 있는 일부 상태를 갱신해서 시그널을 목적지 프로세스로 보낸다.

시그널은 두 가지 이유 중의 하나로 배달된다.

(1) 커널이 0으로 나누기나 자식 프로세스의 종료 같은 시스템 이벤트로 감지했다.

(2) 어떤 프로세스가 커널에 명시적으로 시그널을 목적지 프로세스에 보낼 것을 요구하기 위해서 kill 함수를 호출하였다.

  1. 시그널 받기: 목적지 프로세스는 배달된 신호에 대해서 커널이 어떤 방식으로 반응해야 할 때 목적지 프로세스는 시그널을 받는다.

프로세스는 시그널 핸들러라고 부르는 사용자 수준 함수를 실행해서 시그널을 무시하거나, 종료하거나, 획득할 수 있다.

8.5.2 시그널 보내기

프로세스 그룹

모든 프로세스는 정확히 한 개의 프로세스 그룹(process group)에 속하며, 이것은 양수 ID로 식별한다.

getpgrp 함수는 현재 프로세스의 프로세스 그룹 ID를 리턴한다.

/* getpgrp 함수 */
#include <unistd.h>

pid_t getpgrp(void);

기본적으로, 자식 프로세스는 자신의 부모와 동일한 프로세스 그룹에 속한다.

프로세스는 자신의 프로세스 그룹 또는 다른 프로세스의 그룹을 setpgid 함수를 사용해서 변경할 수 있다.

/* setpgid 함수 */
#include <unistd.h>

int setpgid(pid_t pid, pid_t pgid);

setpgid 함수는 프로세스 PID의 프로세스 그룹을 pgid로 변경한다.

kill 함수

프로세스는 kill 함수를 호출해서 시그널을 다른 프로세스로 보낸다.

/* kill 함수 */
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

만일 pid가 0보다 크면, kill 함수는 시그널 번호 sig를 프로세스 pid로 보낸다.

만일 pid가 0이면 kill 함수는 호출하는 프로세스인 자신을 포함해서 프로세스 그룹 내 모든 프로세스에 시그널 sig를 보낸다.

만일 pid가 0보다 작으면, ㅏill은 시그널 sig를 프로세스 그룹 내의 모든 프로세스로 보낸다.

8.8 요약

예외적 제어흐름은 컴퓨터 시스템의 모든 수준에서 일어나며, 컴퓨터 시스템에 동시성을 제공하는 기본 메커니즘이다.

컴퓨터에는 네 종류의 예외상황이 존재한다.

언터럽트는 타이머 칩이나 디스크 컨트롤러가 프로세서 칩 상의 인터럽트 핀을 설정할 때 비동기적으로 발생한다.

제어는 오류를 발생시킨 인스트럭션의 다음에 오는 인스트럭션으로 리턴한다.

오류와 중단은 인스트럭션 실행의 결과로 동기적으로 발생한다.

트랩은 응용프로그램에게 운영체제 시스템 코드 내부로 제어된 엔트리 포인트를 제공하는 시스템 콜을 구현하는데 사용된다.

운영체제 수준에서, 커널을 ECF를 사용해서 프로세스의 근본적인 개념을 제공한다.

프로세스는 응용 프로그램에서 (1) 각 프로그램에 자신이 프로세서를 혼자서 사용하고 있다는 착각을 느끼게 하는 논리적 제어흐름과 (2) 각 프로그램이 메인 메모리를 혼자서 사용하는 착각을 제공하는 사적 주소공간 두 개의 추상화를 제공한다.

운영체제와 응용 프로그램 사이의 인터페이스에서 응용 프로그램은 자식 프로세스를 만들고, 자식들이 정지하는 것 또는 종료된 것을 기다리고, 새 프로그램을 실행하고, 다른 프로세스가 보낸 시그널을 잡는다.

0개의 댓글