프로세스의 개념#
이 장에서는 운영체제의 핵심 개념 중 하나인 프로세스에 대해 다룹니다. 프로세스는 실행 중인 프로그램을 의미하며, 프로그램 자체는 디스크에 저장된 명령어와 데이터의 집합입니다. 운영체제는 이 명령어와 데이터를 실행하여 프로그램을 작동시킵니다.
사용자는 여러 프로그램을 동시에 실행하기를 원합니다. 예를 들어, 웹 브라우저, 이메일, 게임, 음악 플레이어 등을 동시에 실행하는 것입니다. 운영체제는 실제로 한정된 CPU를 가지고 있음에도 불구하고, 여러 개의 프로세스가 동시에 실행되는 것처럼 만드는 기술, 즉 CPU 가상화를 통해 이를 가능하게 합니다.
이러한 환상을 만들기 위해, 운영체제는 시분할 방식을 사용하여 한 프로세스를 잠시 실행한 후 다른 프로세스로 전환하는 작업을 반복합니다. 이 과정을 통해, 여러 프로세스가 동시에 실행되는 것처럼 보이게 합니다. 하지만 이 방식은 프로세스마다 성능이 다소 저하될 수 있습니다.
운영체제가 이런 작업을 잘 처리하기 위해서는 저수준의 기술적 방법, 즉 메커니즘과, 어떤 프로세스를 언제 실행시킬지 결정하는 정책이 필요합니다. 메커니즘은 운영체제가 필요한 기능을 구현하는 방법을 말하며, 정책은 운영체제가 어떤 결정을 내리기 위한 규칙이나 알고리즘입니다. 예를 들어, 여러 프로그램이 실행 가능할 때 어느 것을 먼저 실행할지 결정하는 스케줄링 정책이 이에 해당합니다.
이처럼 운영체제는 복잡한 작업을 처리하면서도 사용자에게 편리하고 효율적인 환경을 제공하기 위해 설계되었습니다.
시분할과 공간분할
시분할은 운영체제가 자원을 효율적으로 공유하는 중요한 방법 중 하나입니다. 여러 사용자나 프로그램이 동시에 자원을 사용하는 것처럼 보이게 하기 위해, 운영체제는 짧은 시간 동안 한 사용자(또는 프로그램)에게 자원을 할당하고, 그 다음 사용자에게 순차적으로 할당하는 방식으로 자원(CPU나 네트워크 링크 등)을 관리합니다. 이와 반대되는 개념으로 공간 분할이 있습니다. 공간 분할에서는 자원의 ‘공간’을 여러 사용자나 프로그램에게 나누어 줍니다. 디스크가 좋은 예인데, 디스크의 경우 특정 공간(블록)이 한 파일에 할당되면, 그 파일이 삭제되기 전까지 다른 파일이 그 공간을 사용할 가능성이 낮습니다.
프로세스의 개념[1]#
운영체제는 실행 중인 프로그램을 프로세스라는 개념으로 제공합니다. 프로세스를 간단하게 표현하면, 실행 중에 접근하거나 영향을 받은 자원의 목록이라고 할 수 있습니다.
프로세스를 이해하려면 하드웨어 상태를 이해해야 합니다. 프로그램은 실행 중에 하드웨어 상태를 읽거나 변경합니다. 이때 가장 중요한 하드웨어 구성 요소는 메모리입니다. 메모리는 명령어와 데이터를 저장합니다. 프로세스가 접근할 수 있는 메모리는 프로세스의 구성 요소입니다.
레지스터도 프로세스의 하드웨어 상태를 구성하는 중요한 요소입니다. 많은 명령어가 레지스터를 직접 읽거나 변경합니다. 따라서 프로세스를 실행하려면 레지스터도 필요합니다.
프로세스의 하드웨어 상태를 구성하는 레지스터 중 특별한 레지스터가 있습니다. 프로그램 카운터(PC)는 실행 중인 명령어를 알려줍니다. PC는 명령어 포인터(IP)라고도 불립니다. 스택 포인터(SP)와 프레임 포인터(FP)는 함수의 변수와 리턴 주소를 저장하는 스택을 관리하는 데 사용됩니다.
프로그램은 영구 저장장치에 접근하기도 합니다. 이 입출력 정보는 프로세스가 현재 열어 놓은 파일 목록으로 표현됩니다.
핵심 내용:
프로세스는 실행 중인 프로그램의 개념입니다.
프로세스는 메모리, 레지스터, 파일 등의 자원을 사용합니다.
프로그램 카운터는 실행 중인 명령어를 알려줍니다.
스택 포인터와 프레임 포인터는 스택을 관리하는 데 사용됩니다.
프로세스는 영구 저장장치에 접근할 수 있습니다.
정책과 구현의 분리
핵심 개념:
고수준 정책: 운영체제가 무엇을 해야 하는지 결정하는 규칙
저수준 기법: 운영체제가 어떻게 작업을 수행하는지 구현하는 방법
설계 패러다임:
고수준 정책과 저수준 기법을 분리
이점:
정책 변경 용이: 정책 변경 시 기법 변경 필요 없음
모듈성 향상: 코드 재사용 및 유지 관리 용이
확장성 향상: 새로운 정책 및 기법 추가 용이
예시:
정책: 어느 프로세스를 실행할 것인가?
기법: 스케줄링 알고리즘 (예: 라운드 로빈, 우선 순위 기반)
결론:
고수준 정책과 저수준 기법 분리는 운영체제 설계의 중요한 패러다임이며, 이는 모듈성, 확장성, 유지 관리 용이성을 향상시킵니다.
프로세스 API[2]#
운영체제가 반드시 API로 제공해야 하는 몇몇 기본 기능은 다음과 같습니다. 이 API등은 형태는 다르지만 모든 현대 운영체제에서 제공됩니다.
생성(Create): 쉘에 명령어를 입력하거나, 응용 프로그램의 아이콘을 더블 클릭하여 프로그램을 실행시키면, 운영체제는 새로운 프로세스를 생성합니다.
제거(Destroy): 많은 프로세스는 실행되고 할 일을 다하면 스스로 종료하지만 스스로 종료하지 않는 경우가 있기 때문에 운영체제는 불필요한 프로세스를 종료시키는 기능을 제공합니다.
대기(Wait): 특정 프로세스의 작업이 끝날 때까지 기다려야 할 때가 있어 여러 종류의 대기 인터페이스가 제공됩니다. 이는 다른 프로세스와의 작업을 동기화하거나, 특정 조건이 충족될 때까지 기다리는 데 사용될 수 있습니다.
각종 제어(Miscellaneous Control): 프로세스를 일시정지 하거나 다시 시작하는 등의 여러 가지 제어 기능들이 제공됩니다. 이는 프로세스의 동작을 조절하거나 문제를 해결하는데 도움이 됩니다.
상태(Status): 프로세스의 현재 상태 정보를 얻어내는 인터페이스도 제공됩니다. 이는 프로세스가 얼마 동안 실행되었는지 또는 프로세스가 어떤 상태에 있는지 등이 포함됩니다.
프로세스 생성 : 좀 더 자세하게[3]#
프로그램을 실행하기 위해 운영체제가 가장 먼저 하는 일은 프로그램의 코드와 정적 데이터(초기값이 있는 변수 등)를 메모리, 즉 프로세스의 주소 공간으로 불러오는 것입니다. 이를 ‘로딩(loading)’이라고 합니다. 프로그램은 보통 디스크나 요즘에는 SSD에 실행 파일 형태로 저장되어 있는데, 운영체제는 이 파일에서 필요한 부분을 읽어 메모리에 적재합니다.
초기의 운영체제는 프로그램 실행 전에 코드와 데이터 전체를 메모리에 로딩했지만, 최근의 운영체제는 이를 지연시켜 필요할 때만 메모리에 적재하는 방식을 사용합니다. 이를 정확히 이해하려면 페이징(paging)과 스와핑(swapping)에 대한 지식이 필요한데, 이는 추후 메모리 가상화를 다룰 때 자세히 설명하겠습니다.
코드와 정적 데이터를 메모리에 로딩한 후, 운영체제는 프로그램 실행을 위해 몇 가지 준비 작업을 더 수행합니다.
스택(stack) 메모리 할당: C 프로그램은 지역 변수, 함수 인자, 리턴 주소 등을 저장하기 위해 스택을 사용합니다. 운영체제는 이를 위한 메모리를 할당하고, main() 함수의 인자(argc, argv)로 스택을 초기화합니다.
힙(heap) 메모리 할당: C 프로그램에서 힙은 동적으로 할당되는 메모리 공간입니다. malloc()으로 메모리를 요청하고 free()로 반환하는 식으로 사용되며, 주로 가변 크기의 자료구조(연결 리스트, 해시 테이블, 트리 등)를 위해 쓰입니다.
입출력 초기화: 운영체제는 프로그램의 입출력을 위한 초기화 작업도 수행합니다. 예를 들어 유닉스 시스템에서는 각 프로세스에 표준 입력(STDIN), 표준 출력(STDOUT), 표준 에러(STDERR)에 대한 파일 디스크립터를 제공합니다.
이러한 준비 과정이 모두 끝나면, 운영체제는 프로그램의 시작점(entry point), 즉 main() 함수로 제어를 이동시켜 프로그램 실행을 시작합니다. 이를 통해 CPU의 제어권이 새로 생성된 프로세스로 넘어가고, 프로그램이 실행되기 시작하는 것입니다.
argc와 argv
argc와 argv는 C 프로그램의 main() 함수가 받는 인자들입니다. 이들은 프로그램 실행 시 명령줄에서 프로그램에 전달되는 정보를 담고 있습니다.
argc (argument count): int 타입의 값으로, 프로그램 실행 시 명령줄에서 전달된 인자의 개수를 나타냅니다. 프로그램의 이름도 인자로 포함되므로, argc는 항상 1 이상의 값을 갖습니다.
argv (argument vector): char* 타입의 배열로, 실제 전달된 인자들의 값을 문자열 형태로 저장하고 있습니다. argv[0]는 보통 프로그램의 이름이 되고, argv[1]부터는 명령줄에서 전달된 인자들이 순서대로 저장됩니다.
예를 들어, 다음과 같이 프로그램을 실행했다고 가정해 봅시다.
$ ./myprogram arg1 arg2
이 경우, argc는 3이 되고, argv는 다음과 같은 값을 갖게 됩니다.
argv[0]: “./myprogram”
argv[1]: “arg1”
argv[2]: “arg2”
프로그램은 이러한 argc와 argv의 값을 통해 명령줄 인자를 받아 처리할 수 있게 되는 것입니다. 운영체제는 프로그램을 실행할 때 이 값들을 적절히 설정하여 main() 함수에 전달하게 됩니다.
프로세스 상태[4]#
프로세스 상태(state)를 단순화하면 다음 세 상태 중 하나에 존재할 수 있다.
실행 (Running): 실행 상태에서 프로세스는 프로세서에서 실행 중이다. 즉, 프로세스는 명령어를 실행하고 있다.
준비 (Ready): 준비 상태에서 프로세스는 실행할 준비가 되어 있지만 운영체제가 다른 프로세스를 실행하고 있는 등의 이유로 대기 중이다.
대기 (Blocked): 프로세스가 다른 사건을 기다리는 동안 프로세스의 수행을 중단시키는 연산이다. 흔한 예 : 프로세스가 디스크에 대한 입출력 요청을 하였을 때 프로세스는 입출력이 완료될 때까지 대기 상태가 되고, 다른 프로세스가 실행 상태로 될 수 있다.
이를 그림으로 표현하면 아래에 있는 이미지와 같이 될 것이다.
위에서 보이는 듯 프로세스는 ‘준비’ 상태와 ‘실행’ 상태를 운영체제의 정책에 따라 이동한다. 프로세스는 운영체제의 스케줄링 정책에 따라 스케줄이 되면 ‘준비’ 상태에서 ‘실행’ 상태로 전이한다.
‘실행’ 상태에서 ‘준비’ 상태로의 전이는 프로세스가 나중에 다시 스케줄 될 수 있는 상태가 되었다는 것을 의미한다. 프로세스가 입출력 요청 등의 이유로 대기 상태가 되면 요청 완료 등의 이벤트가 발생할 때까지 대기 상태로 유지된다. 이벤트가 발생하면 프로세스는 다시 준비 상태로 전이되고 운영체제의 결정에 따라 바로 다시 실행될 수 있다.
두 개의 프로세스가 어떻게 전이될 수 있는지를 한번 알아보자. 먼저 실행 중인 두 프로세스가 있다고 했을 때, 각 프로세스가 오직 CPU만 사용하고 입출력을 행하지 않을 때의 프로세스 상태 추이를 나타내면 아래와 같다.
위에서 보이다시피 첫 번째 프로세스가 어느 정도 실행한 후에 입출력을 요청한다. 그 순간 프로세스는 대기 상태가 되고 다른 프로세스에게 실행 기회를 준다. 이걸 자세히 보면 아래와 같다.
순서를 자세히 살펴보면,
Process0은 입출력을 요청하고 요청한 작업이 완료되기를 기다린다
프로세스는 디스크를 읽거나 네트워크로부터 패킷을 기다릴 때 대기 상태로 전이한다.
운영체제는 Process0이 CPU를 사용하지 않는다는 것을 감지하고, Process1을 실행시킨다.
Process1이 실행되는 동안 입출력이 완료되고 Process0은 준비 상태로 다시 전이된다.
Process1은 종료되고, Process0이 실행되어 종료된다.
이처럼 간단한 예에서조차 운영체제가 내려야 할 결정은 매우 많다. 우선 시스템이 Process0이 입출력을 요청할 때 Process1의 실행 여부를 결정해야 한다. Process1을 실행키로 한 결정은 CPU를 계속 동작시키므로 자원 이용률을 높인다. 또한, 시스템은 Process0이 요청한 입출력이 완료되었을 때, Process0을 바로 실행하지 않고 실행 중이던 Process1을 계속 실행하였다. 이게 좋은 결정이었는지는 확실하지 않다.
운영체제는 스케줄러를 통해 이러한 결정을 내린다. 운영체제의 스케줄러는 차후에 자세히 다룰 예정이다.
자료구조#
운영체제 역시 일종의 프로그램이기에, 다른 프로그램들처럼 여러 정보를 저장하고 관리하기 위한 자료구조를 갖고 있습니다.
예를 들어, 운영체제는 프로세스의 상태를 파악하기 위해 ‘프로세스 리스트’라는 자료구조를 유지하는데, 이 리스트에는 실행 대기 중인 프로세스들의 정보가 담겨 있습니다. 또한 현재 실행 중인 프로세스가 무엇인지 알기 위한 별도의 자료구조도 관리합니다.
아울러 운영체제는 입출력 작업 등으로 인해 대기(blocked) 상태에 있는 프로세스도 추적해야 합니다. 해당 입출력이 완료되면, 운영체제는 이 정보를 토대로 대기 중이던 프로세스를 깨워 실행 가능한 상태(ready)로 만들어 줄 수 있어야 하죠.
프로세스가 실행을 멈출 때(예: 인터럽트 등으로 인해), 운영체제는 ‘레지스터 문맥(register context)’이라는 자료구조에 해당 프로세스의 레지스터 값들을 저장합니다. 나중에 이 프로세스를 다시 실행할 때는 저장된 레지스터 값들을 복원함으로써 중단된 지점부터 실행을 재개할 수 있게 됩니다. 이를 ‘문맥 교환(context switch)’이라고 하며, 추후 더 자세히 다루도록 하겠습니다.
프로세스의 상태로는 실행(running), 준비(ready), 대기(blocked) 외에도 몇 가지가 더 있습니다.
어떤 시스템에서는 프로세스가 생성 중일 때의 ‘초기(initial)’ 상태를 별도로 둡니다. 또 프로세스가 종료되었지만 메모리 상에 아직 남아있는 ‘종료(final)’ 상태가 있는데, 유닉스 계열 시스템에서는 이를 ‘좀비(zombie)’ 상태라고 부릅니다.
종료 상태는 해당 프로세스가 성공적으로 실행을 마쳤는지 등을 다른 프로세스(주로 부모 프로세스)가 검사하는 데 활용됩니다. 부모 프로세스는 자식 프로세스의 종료를 기다리는 시스템 콜(예: wait())을 호출하는데, 이를 통해 운영체제는 종료된 프로세스의 자원을 정리할 수 있게 됩니다.