이 포스팅은 <운영체제와 정보 기술의 원리>를 참고하였습니다.
프로그램의 구조와 인터럽트
우리의 컴퓨터 프로그램의 내부적인 구조는 항상 함수들로 구성됩니다. 하나의 함수가 수행될 때 다른 함수가 호출되고, 호출된 함수가 종료되면 원래 호출했던 함수의 위치로 되돌아가서 계속 수행합니다.
프로그램이 수행되기 위해서는 프로그램의 주소 영역이 메모리에 올라가 있어야 하는데, 프로그램의 주소 영역 중 스택 영역이 함수 호출 시에 복귀할 주소 및 데이터를 임시로 저장하는 곳으로 사용되고 있습니다.
우리가 작성한 프로그램은 제일 처음 메인 함수에서 실행하며, 메인 함수에서 다른 함수를 호출하면 CPU가 메인 함수의 코드를 수행하다가 다른 함수의 코드로 수행 위치로 이동합니다. 그러면 프로그램은 새로운 함수 위치로 점프해서 그 함수를 실행합니다. 함수 수행이 완료된 후에는 원래 호출했던 위치로 돌아오게 됩니다. 그 지점을 바로 스택 영역에 저장하게 되는 것입니다. 스택 영역이 반드시 필요한 이유는 함수 호출 시에 해당 영역으로 Jump하기 때문에 다음 명령을 수행하는 메모리 위치가 달라지기 때문입니다.
인터럽트의 동작 원리도 함수 호출과 비슷합니다. 하지만 함수 호출의 경우 프로그램마다의 주소 공간 중 스택 영역에 복귀 주소를 저장하는 데에 반면, 인터럽트의 경우 운영체제 커널 부분에 저장하게 됩니다.
운영체제는 현재 실행 중인 모든 프로그램을 관리하기 위한 자료구조를 가지고 있습니다. 커널 내에 실행 중인 프로그램의 수만큼 프로세스 제어 블록(PCB : Process Control Block)을 만들어 둡니다. 그래서 인터럽트가 발생했을 때 프로그램이 어디까지 수행했는지를 PCB에 저장하게 됩니다.
예를 들면, 프로그램 A가 수행 중에 있는데 인터럽트가 발생했을 때 현재 실행 중이던 지점을 A의 PCB에 저장하고 CPU는 인터럽트 처리 루틴으로 가서 인터럽트 발생과 관련된 일을 처리합니다. 그리고 모두 처리한 후에는 프로그램 A의 PCB에 저장된 주소를 복원시켜 원래 수행하던 일을 재개합니다.
컴퓨터 시스템의 작동 개요
CPU가 어떻게 명령을 수행하는지 알아볼까요? CPU는 현재 수행해야 할 메모리 주소의 명령을 그대로 처리할 뿐입니다. 현재 수행해야 할 메모리 주소의 명령이 담겨져 있는 곳은 레지스터이고, 특히 이 부분을 프로그램 카운터라고 부릅니다. 함수 호출 등에 의한 주소 이동이 없는 이상 프로그램 카운터를 항상 바로 다음 주소의 명령을 가리키게 되고 코드의 순차적인 수행이 이루어집니다.
메모리에는 사용자 프로그램과 운영체제가 같이 올라가 수행됩니다. 이때 CPU는 프로그램 카운터가 가리키는 메모리 위치의 프로그램을 수행하게 됩니다. 그래서 프로그램 카운터가 메모리 주소 중 운영체제가 존재하는 부분을 가리키고 있으면, 현재 운영체제의 코드를 수행 중이라는 것, 즉 커널 모드라는 것을 의미합니다. 반대로 사용자 프로그램이 존재하는 메모리 위치를 가리킨다면 사용자 프로그램이 수행된다는 것, 즉 사용자 모드라는 것을 의미합니다. 통상적으로 명령들은 순차적으로 수행되지만, 함수 호출 등으로 점프해 바로 다음 주소가 아닌 명령을 수행할 수도 있습니다.
CPU가 수행하는 명령 중에는 일반 명령과 특권 명령 있는데, 그 중 특권 명령은 보안이 필요한 명령을 의미합니다. 그래서 특권 명령에 경우에는 운영체제만이 수행할 수 있도록 제한하고 있습니다. 그래서 해당 명령이 수행될 수 있는지 확인하기 위해서 모드 비트를 활용합니다.
사용자 프로그램도 실행 중에 특권 명령의 수행이 필요할 때가 있습니다. 예를 들어 디스크의 파일에 접근해야 하거나 수행 결과를 화면에 출력해야 하는 경우 등입니다. 이 같은 경우에 사용자 프로그램은 스스로 특권 명령을 내릴 수 없기 때문에 운영체제에게 대행을 요청하게 됩니다. 이를 시스템 콜(System call)이라고 합니다. 사용자 프로그램이 시스템 콜을 하게 되면 운영체제는 자신의 커널 영역에서 정의된 시스템 콜 처리 코드를 수행하게 됩니다.
프로그램이 아닌 주변 장치가 CPU에게 서비스를 요청할 때에도 시스템 콜과 비슷한 방식을 사용하는데, CPU는 프로그램 카운터가 가리키는 메모리 위치의 명령만 계속 수행하고 있기 때문에 주변 장치의 상태를 지속적으로 파악할 수가 없습니다. 그래서 주변 장치는 CPU의 도움이 필요할 때마다 인터럽트를 사용해 요청하게 됩니다. 인터럽트를 발생시키기 위해서는 주변 장치는 인터럽트 라인을 세팅합니다. 그래서 CPU는 매번 명령을 수행한 직후에 인터럽트 라인을 체크해 서비스 요청이 들어왔는지를 확인합니다.
프로그램의 실행
프로그램이 실행된다는 것은 두 가지 의미를 가집니다. ➊ 디스크에 존재하던 실행 파일이 메모리에 적재됨을 의미합니다. ➋ 프로그램이 CPU를 할당 받고 기계 명령을 수행하고 있는 상태를 의미합니다. (하나의 CPU는 한 순간에 한 명령만 수행할 수 있지만, 짧은 시간 단위로 여러 프로그램이 CPU를 나누어 쓴다면 여러 프로그램이 메모리에 동시에 적재되기 때문에 여러 프로그램이 동시에 실행된다는 말을 보편적으로 사용할 수 있습니다.)
보통 실행 파일이 메모리에 적재될 때 실행 파일 전체가 메모리에 한꺼번에 올라가는 것이 아닌 일부분만 메모리에 올라가고 나머지는 디스크에 내려가 있는 것이 일반적입니다. 왜냐하면 한정된 메모리 공간을 여러 프로그램이 공유해서 사용하고 있기 때문에 쓸데없는 메모리 낭비를 하는 것은 비효율적이기 때문입니다. 하지만 디스크 자체에 올려놓는 것이 아닌 디스크 중 메모리의 연장 공간인 Swap 영역에 내려놓는 방식을 사용해 효율적으로 관리하고 있습니다.
각 프로그램마다 독자적으로 갖는 주소 공간(코드, 데이터, 스택, 힙 등)을 별도로 가지며, 이를 가상 메모리 또는 논리적 메모리라고 합니다. 실제 물리적 메모리의 주소와 독립적으로 각 프로그램마다 독자적인 주소 공간을 갖기 때문에 지칭하는 용어입니다.
운영체제도 하나의 프로그램으로 운영체제 커널 역시 주소 공간 구성을 가지고 있습니다.
커널의 코드 영역에는 CPU, 메모리 등의 자원을 관리하기 위한 부분과 사용자에게 편리한 인터페이스를 제공하기 위한 부분이 주를 이루고, 시스템 콜 및 인터럽트를 처리하기 위한 부분을 포함합니다.
커널의 데이터 영역에는 각종 자원을 관리하기 위한 자료 구조가 저장됩니다. CPU와 메모리와 같은 하드웨어 자원을 관리하기 위한 자료 구조뿐 아니라 현재 수행 중인 프로그램을 관리하기 위한 자료 구조도 커널의 데이터 영역에 유지됩니다. 이때 현재 수행 중인 프로그램을 프로세스라고 부르고, 커널의 데이터 영역 중엔 각 프로세스의 상태와 CPU 사용 정보, 메모리 사용 정보 등을 유지하기 위한 자료 구조인 PCB(프로세스 제어 블록)을 두고 있습니다. 즉, 하드웨어와 소프트웨어를 포함하는 시스템 내의 모든 자원을 관리하기 위한 자료 구조를 각각 유지하고 있습니다.
커널의 스택 영역은 일반 프로그램의 스택 영역과 마찬가지로 함수 호출 시의 복귀 주소를 저장하기 위한 용도로 사용합니다. 하지만 차이점으로는 현재 수행된 프로세스마다 별도의 스택을 두어 관리합니다. 프로세스가 함수를 호출할 때 자기 주소 영역 내부에 정의된 함수를 호출하면 자식의 스택에 복귀 주소를 저장됩니다. 하지만 프로세스가 특권 명령을 수행하려고 커널에 정의된 시스템 콜을 호출하고 시스템 콜 내부에서 다른 함수를 호출하는 경우 그 복귀 주소는 커널 내의 주소가 되어 사용자 프로그램의 스택과는 별도의 저장 공간이 필요합니다. 그리고 커널은 일종의 공유 코드로서 모든 사용자 프로그램이 시스템 콜을 통해 커널의 함수를 접그할 수 있으므로 일관성 유지를 위해 각 프로세스마다 커널 내에 별도의 스택을 두게 되는 것입니다. 즉, 프로그램이 실행되어 자기 자신의 코드 내에서 함수 호출 및 복귀 주소를 저장하기 위해서는 자기 주소 공간 내의 스택을 사용하고, 시스템 콜 등 커널 내의 함수를 호출하는 경우에는 커널의 주소 공간에 존재하는 커널 스택을 사용하게 된다.
사용자 프로그램이 사용하는 함수
프로그램이 사용하는 함수는 크게 사용자 정의 함수와 라이브러리 함수, 커널 함수 세 가지로 구분해 볼 수 있습니다.
사용자 정의 함수는 프로그래머가 직접 작성한 함수이고, 라이브러리 함수는 프로그래머가 직접 작성하지는 않았지만 누군가가 이미 작성해놓은 함수를 호출만 하여 사용하는 경우를 의미합니다. 이 두 함수는 모두 컴파일하여 실행 파일을 만들게 되면 그 프로그램의 코드 영역에 기계어 명령 형태로 삽입됩니다. 그래서 이 두 함수는 프로그램이 실행될 때에 해당 프로세스의 주소 공간에 포함됩니다. 또한, 함수 호출 시에도 자신의 주소 공간에 있는 스택을 사용하게 됩니다.
커널 함수는 운영체제 커널의 코드에 정의된 함수를 의미합니다. 커널 함수의 종류에는 사용자 프로그램이 운영체제의 서비스를 요청하기 위해 호출하는 시스템 콜 함수와 각종 하드웨어 및 소프트웨어가 CPU의 서비스를 요청하기 위해 발생시키는 인터럽트 처리 함수가 있습니다. 이처럼 커널 함수는 사용자 프로그램의 주소 공간에 코드가 존재하는 것이 아니라 운영체제 커널 주소 공간에 정의되어, 사용자 프로그램은 운영체제에 있는 함수를 호출해서 사용하는 것입니다. printf() 자체로는 라이브러리 함수지만 궁극적으로 특권 명령인 입출력을 수반하므로 printf() 내에서 커널 함수 호출인 시스템 콜을 동반하게 됩니다.
프로세스의 상태
프로세스의 상태는 실행(Running), 준비(Ready), 봉쇄(Blocked, Wait, Sleep) 세 가지로 크게 나눌 수 있습니다. 프로세스의 상태를 나누는 이유는 컴퓨터의 자원을 효율적으로 관리하기 위해서입니다.
실행 상태는 CPU를 할당 받고 기계어 명령을 수행하고 있는 프로세스의 상태입니다. CPU는 하나밖에 없고 CPU에서 실행할 수 있는 프로세스는 매시점 하나밖에 없습니다.
준비 상태는 CPU만 할당 받으면 당장 명령을 수행할 수 있지만, CPU가 하나밖에 없어서 현재 CPU를 할당 받지 못한 프로세스의 상태를 의미합니다.
봉쇄 상태는 CPU를 할당 받더라도 명령을 수행할 수 없는 상태를 의미합니다. 프로세스가 입출력 작업을 진행하고 있는 경우에 봉쇄 상태가 됩니다.
준비 상태에 있던 프로세스가 실행 상태로 변경되는 경우는 실행 상태에 있는 프로세스의 상태가 봉쇄 상태가 되거나 타이머 인터럽트를 통해 CPU 할당 시간이 만료된 경우가 있습니다. 운영체제는 준비 상태에 있는 프로세스들을 줄 세우기 위해 준비 큐(Ready Queue)를 두고 준비 큐의 맨 앞에 있는 프로세스에게 CPU를 할당해줍니다. 줄 세우는 방식은 CPU 스케줄링 방법에 따라 달라집니다.
준비 큐 외에도 운영체제는 특정 자원을 기다리는 프로세스들을 줄 세우기 위해 자원별로 큐를 두고 있습니다. 디스크의 입출력 서비스를 요청한 프로세스들은 디스크 입출력 큐(Disk I/O Queue)에 줄 서게 됩니다. 그러면 디스크 컨트롤러는 디스크 입출력 큐에 줄 서 있는 순서대로 프로세스의 입출력 작업을 수행합니다.
하드제어 자원을 기다리는 프로세스만이 아닌 소프트웨어 자원을 기다리는 경우에도 큐는 필요합니다. 예를 들어 공유 데이터에 대한 접근 권한은 소프트웨어 자원으로 분류될 수 있습니다. 어떤 프로세스가 공유 데이터를 사용하고 있는 도중에 다른 프로세스가 같은 데이터를 접근하면 데이터에 대한 일관성이 훼손될 수 있기 때문에 공유 데이터는 매 시점 하나의 프로세스만이 접근할 수 있도록 해야 합니다.
이때 접근한다는 의미는 반드시 CPU가 해당 데이터를 사용하고 있다는 의미는 아닙니다. 공유 데이터를 접근 중인 프로세스가 준비 상태/봉쇄 상태로 변경된 경우에도 새롭게 CPU를 할당 받은 프로세스가 동일한 데이터를 접근하게 되면 데이터의 일관성이 깨지게 되어 접근을 허락해서는 안 됩니다. 즉, 공유 데이터라는 일종의 소프트웨어 자원을 앞서 접근 중인 프로세스가 다 사용하고 반납할 때까지 다른 프로세스가 CPU를 할당 받았다고 하더라도 접근하지 않고 기다려야 합니다. 여러 프로세스가 공유 데이터를 동시에 접근하려고 할 경우 공유 데이터를 기다리는 큐에 줄 서게 하여 현재 그 데이터를 사용 중인 프로세스가 데이터를 반납하기 전까지 접근을 못하게 하고 반납할 경우에 큐에 줄 서있는 순서대로 데이터의 접근 권한을 주는 방법을 사용하게 됩니다.
프로세스의 상태 관리는 커널의 주소 영역의 데이터 영역에서 다양한 큐를 두어 이루어지게 됩니다. 각 프로세스들이 CPU를 기다리는지, 입출력을 기다리는 지에 대한 정보는 모두 커널에서 총체적으로 관리하고 있다는 의미입니다.
프로세스의 두 가지 실행 상태
하나의 프로세스가 시작되고 수행이 완료되기까지 프로세스 자신의 주소 공간에 있는 코드만이 아닌 커널의 주소 공간에 있는 코드도 실행됩니다. 왜냐하면 프로그램이 사용자 정의 함수나 라이브러리 함수뿐 아니라 입출력 시스템 콜 등을 통해 운영체제 커널의 함수도 호출하여 실행하기 때문입니다.
프로세스가 CPU에서 실행되고 있을 때 두가지 경우로 나눌 수 있습니다. 자신의 주소 공간에 정의된 코드를 실행하는 것을 사용자 모드에서의 실행 상태(User Mode Running), 커널의 시스템 콜 함수를 실행하는 것을 커널 모드에서의 실행 모드(Kernel Mode Running)이라고 합니다. 우리가 시스템 콜을 통해 실행되는 것이 커널의 코드가 수행되는 것이지만, 프로세스가 실행 상태에 있다고 말합니다. 프로세스 입장에서는 CPU를 운영체제 커널에게 빼앗긴 것처럼 보이지만, 사실은 프로세스가 해야 할 일을 대행하여 커널의 코드를 실행하는 것이기 때문에 여전히 프로세스는 실행 상태에 있는 것으로 간주합니다.
'개발 공부' 카테고리의 다른 글
Clean Architecture: Architecture (0) | 2022.03.15 |
---|---|
[자바] 참조 타입 - 2 (0) | 2020.05.04 |
[자바] 참조 타입, JVM, 메모리 영역 (0) | 2020.04.29 |
[프로그래머스] 42897 도둑질 (0) | 2020.04.28 |
[프로그래머스] 42898 등굣길 (0) | 2020.04.28 |