본문 바로가기
CSE/시스템프로그래밍

[시스템 프로그래밍] 예외적인 제어흐름 - 프로세스

by 잔디🌿 2025. 12. 2.

    컴퓨터는 다음과 같이 시간이 흐름에 따라 주어진 일을 한다.

     

    보통 제어의 흐름을 변경하는 명령어는 call, return 등이다. 하지만 이정도로 쓸만한 시스템을 만드는 것은 불가능하다

    따라서 시스템은 예외적인 제어흐름을 위한 메커니즘을 필요로 한다.(파일을 열어야 하는 경우, 사용자가 ctrl-c를 눌렀을 경우...)

     

    예외적인 제어흐름

    하위 매커니즘

    예외 - 시스템 이벤트에 대한 반응으로 제어흐름을 변경, 하드웨어와 os를 함께 사용

    0으로 나누기, 잘못된 메모리 접근, 타이머 인터럽트 등...

    시작 주체가 하드웨어이다.

     

    상위 매커니즘

    프로세스 컨텍스트 전환(os, 하드웨어 타이머 구현), 시그널(os로 구현), nonlocal 점프

     

    예외상황(Exceptions)

    특정한 이벤트에 대한 반응(0으로 나누기, ctrl+c..)으로 os 커널로 제어가 전환되는 것을 말함

     

    커널 : OS의 메모리 상주 부분이다.

     

    cpu가 user의 code(현재 사용하던 프로그램)을 실행하다가, exception(0으로 나누는 상황)이 발생하면 메모리에 상주하는 os 코드(handler)를 실행하여 이를 해결한다.

     

    각 이벤트는 예외번호 k를 가진다.(id라고 생각하면 됨)

    k는 exception table의 인덱스이다. 즉 0번 예외를 실행시켜야한다면, exception table 0번에 0번 exception handler의 첫번째 주소를 가지고 있으므로 이 주소를 현재 cpu 레지스터의 rip에 넣어 실행된다.(rip는 cpu가 다음으로 처리해야할 명령어의 주소를 적는 레지스터이다.)

     

    Exception의 제어흐름 체계는 위와 같다.

    동기와 비동기로 나뉘는데 이는 현재 실행중인 명령어 때문에 발생했는가를 기준으로 판단하게 된다.

    비동기의 경우, 현재 실행중인 명령어와 상관 없이 외부 이벤트로 발생한다.(키보드 입력, 디스트 i/o 완료 등)

    동기의 경우, 명령어 실행이 완료된 직후 발생한다.

     

    비동기형(인터럽트)

    비동기형은 위에서 언급했듯 프로세서의 외부 사건으로 발생한다.

    여기서 프로세서란 cpu를 말한다. 프로세스는 메모리에 적재되어 실행되거나 대기중인 프로그램을 의미한다.

    프로세서 내부에는 인터럽트 발생을 알기 위해서 세팅되는 인터럽트 핀이 있다. 프로세서는 이를 통해서 인터럽트의 발생을 확인한다.

    인터럽트 실행으로 인해 실행해야하는 핸들러를 실행한 다음, 기존 실행중이던 프로그램을 마저 실행하기 위해서 직전 실행 명령어 다음 명령어로 복귀한다.

     

    인터럽트의 예시는 다음과 같다.

     

    동기형 예외

    명령어를 실행한 결과로 발생하는 사건이다.

    Traps

    명령어의 결과로 발생하는 의도적인 예외이다.(파일을 열고 닫기...)

    예시로는 system calls, breakpoint traps, special instructions가 있다.

    이를 처리한 후 다음 명령어로 복귀하게 된다.

     

    Faults

    핸들러가 정정할 수 있는 에러의 결과로 발생한다.

    page fault, protection faults, floating point 등이 있다. 

    fault를 일으킨 명령을 다시 실행하거나, 복구 불가한 에러일경우, aborts한다.

     

    Aborts

    하드웨어 오류와 같이 복구 불가한 에러의 결과로 발생한다.

    패러티 에러, 시스템 체크 에러 등이 있다. 이러한 에러의 경우 응용 프로그램으로 복귀할 수 없기 때문에 현재 프로그램을 종료하여 해결한다.

     

    시스템 콜

    시스템 콜은 동기형 -> traps에 속해 있다.

    이런식으로 프로그램을 실행할 때 필요한 명령을 한다.

     

    예제를 보자

    이 프로그램은 프로그램 실행 중 파일을 열어야 할 일이 생겼다.

    open의 syscall 번호는 2번이므로, rax(syscall에게 번호 전달하기 위해) 2를 넣는다.

    그 다음 syscall을 호출하여 커널에 진입힌다. 커널 내의 open file을 실행한 후, 리턴한다.

    rax 내부에 커널에서의 리턴값이 담겨있다. 이 값을 통해 성공 여부를 확인한다.

     

    여기서 파일 읽기도 그냥 프로그램 명령어 안에 넣어서 같이 진행하면 되는거 아닌가? 라는 생각이 들 수 있다.

    하지만 컴퓨터에 따라서 하드웨어에 접근하는 방식은 너무 다르고, 프로그램이 직접 하드웨어에 접근하게 하는건 위험하다. 따라서 각 컴퓨터마다 하드웨어를 다루는 코드는 커널 영역에 상주하고 있다.

    이 덕분에 프로그램들은 위와 같이 하드웨어에 접근할 때 번호만 있으면 실행하는 컴퓨터가 어떤 컴퓨터인지 몰라도 원하는 프로세스를 실행할 수 있다.

     

    Fault :  페이지 오류

    page falut는 사용자 메모리의 특정 페이지가 현재 메모리에 위치하지 않고 하드디스크에 위치해있을 때 나타난다.

    위와 같이 0x8049..에 값을 넣어야하는데 이 값이 포함된 페이지가 메인메모리(RAM)에 존재하지 않고 하드디스크에 존재한다.

    이때 오류를 처리하고 오류를 발생시킨 명령어를 다시 실행해야한다. 

    이 경우, os가 페이지를 생성하여 메모리에 로드한 후, 이전에 에러가 났엇던 movl 명령어를 다시 실행하는 방식으로 진행된다.

     

     

    프로세스 

    프로세스는 운영체제가 만들어주는 프로그램의 한 실행 예이다.

    프로세스는 프로그램에 두개의 중요한 추상화를 제공한다.

    논리적인 제어흐름 : 각 프로그램이 cpu를 독점하는 것처럼 보이도록 한다.

    사적인 주소공간 : 각 프로그램이 메인메모리를 독점하는 것처럼 보이도록 한다.

     

    이것이 가능한 이유는 프로세스의 실행이 서로 교대로 실행되고, 주소공간은 가상메모리 시스템에 의해 관리되기 때문이다.

     

    이 경우, 컴퓨터 내에서 가지고 있는 프로세스의 수는 123개이지만, active인 상태인 것은 5개 뿐이다

    단일 프로세서(cpu)는 다수의 프로세스를 동시에 실행한다.

    하나의 프로세스를 수행하던 중 다른 프로세스를 실행할 때에는 기존의 레지스터들을 메모리에 보관하고 다음 프로세스를 실행하기 위해 스케줄링(어떤 프로세스를 얼마나 실행할것인지)를 진행한다.

     

    이렇게 다른 프로세스를 실행하기 위해서 보관된 레지스터들을 가져오고 주소공간을 전환하는 것을 문맥전환(context switch)라고 한다.

     

    여기까지는 cpu가 한대일 경우(옛날꺼)이다.

    현대에는 cpu가 여러개이다. 컴퓨터를 살 때 ~코어 cpu이렇게 써있는데 이게 cpu의 갯수를 의미한다.

     

    여러개의 cpu가 한개의 칩에 포함되어있고, 메인메모리는 모두 공유한다. 각 코어는 별도의 프로세스를 실행 가능하다.

     

    각 프로세스는 자신만의 논리적인 제어흐름을 가진다.

    두 프로세스가 실행되는 시간이 서로 중첩되면 동시에 실행된다고 부르고 그렇지 않으면 순차적으로 실행된다고 정의한다.

    프로세스 A&B, A&C는 동시에 실행된다. 하지만 프로세스C는 B가 모두 다 끝나고 실행이 시작되므로, C&B는 순차적으로 실행된다고 볼 수 있다.

     

    위와 같이 cpu는 여러 프로세스를 번갈아가면서 실행하지만, 사용자는 동시에 실행된다고 느낀다.(cpu가 움직이는 시간이 매우 빠르므로)

     

    문맥전환(Context Switch)

    프로세스는 커널이라고 불리는 운영체제에 의해서 관리된다.

    여기서 중요한 점은 커널은 프로세스가 아니고, 유저 프로세스의 일부로 사용된다.

    한개의 프로세스에서 다른 프로세스로 넘어가는 것을 문맥전환이라고 한다. 문맥전환을 할 때에는 cpu의 레지스터 값을 메모리에 저장하고, 가져오는 등의 과정이 필요하므로 이를 해결하기 위해서 커널 코드가 실행된다.

     

    프로세스 상태

    프로세스 상태는 위와 같다.

    프로세스가 생성되면 ready상태에 있다가 running 상태로 되어 실행되게 된다. 이 때 i/o 등의 이유로 다른 하드웨어의 이벤트가 기다려야할 때에는 wait를 한 후, 다시 ready 상태로 들어가고 실행되게 된다.

    그 다음 위와 같은 이유로 종료되면 terminated 상태가 된다.

     

    프로세서의 제어

    Unix는 다음과 같은 프로세스 제어 기능을 제공한다.

     

    프로세스 id 가져오기

    각 프로세스는 고유한 Id를 가진다. 

    위와 같은 명령어로 현재 또는 부모 프로세스의 id를 얻을 수 있다.

     

    프로세스 만들기 : fork

    int fork(void) 함수는 호출하는 프로세스(부모 프로세스)와 동일한 프로세스(자식 프로세스)를 생성한다.

    자식 프로세스는 0을 리턴하고, 부모 프로세스는 자식 프로세스의 pid를 리턴한다.

    위와 같은 코드에서 fork() 함수를 호출하여 자식프로세스가 생성되었다.

    자식프로세스는 fork()가 리턴되는 시점부터 시작해서 부모 프로세스 그대로(코드 그대로) 쭉 실행된다.

    자식프로세스의 경우 fork()의 리턴값이 0이므로 child가 출력되고, 부모는 자식의 pid가 리턴되므로, parent가 출력된다.

    이처럼 fork() 함수는 한번 호출되지만, 두번 리턴한다는 특징이 있다.

    이때 child와 parent 둘 중 누가 먼저 출력될지는 아무도 모른다.

     

    부모와 자식은 동일한 상태로 시작하지만, 각각의 사본을 가진다. 즉, 변수들은 부모와 자식이 다르게 가질 수 있다.

    하지만 출력파일 식별자는 동일하다. 따라서 같은 콘솔 창에 출력문이 뜨게 된다.

     

    이런식으로 프로세스 그래프를 그려서 이해하면 편리하다.

     

    프로세스 종료하기 : exit

    종료 상태 status 값을 가지고 종료한다. 정상 리턴시 status는 0이다.

    atexit() 함수는 exit 할 때 실행할 함수를 등록한다.

    Zombies : 좀비 프로세스

    프로세스가 종료되도, 여전히 시스템 자원을 점유하는 문제가 있다. 

    zombie란 종료되었지만, 아직 정리가 되지 않은 프로세스이다.

    자식 프로세스가 부모프로세스보다 먼저 종료되면 좀비 프로세스가 된다.

     

    <프로세스 정리 과정>

    부모가 자신의 종료한 프로세스를 수행함

    부모는 종료 상태 정보를 넘겨받고, 커널은 종료된 프로세스를 시스템에서 제거한다.

    만약 부모가 정리하지 않는 경우에는 init process가 정리한다.

    또한 장기간 동작하는 프로세스들은 자신의 좀비 자식들을 정리해야한다.

     

    위 코드를 보면, 자식 프로세스는 금방 종료되지만, 부모 프로세스는 무한 루프에 빠진다.

    이 경우 자식 프로세스는 종료되어 좀비 프로세스가 된다. 

    ps로 실행중인 프로세스를 보면 defunct로 보이는 좀비가 된 자식 프로세스와 부모 프로세스가 보인다.

    이 때 부모 프로세스 pid인 6639를 kill을 활용해서 죽이면, init 프로세스가 해당 부모의 자식프로세스를 입양하여 자식 좀비를 제거한다.

     

    또한 부모가 종료되었는데 자식이 여전히 살아있는 경우도 있다.

    이 경우 자식은 고아 프로세스가 된다. 이 때도 마찬가지로 자식 프로세스를 죽이면 init()이 자식프로세스를 정리해준다.