인터럽트는 CPU가 작업을 하는 도중 예기치 않은 상황이 발생할 때, 현재 정상적으로 작업중인 프로세스를 잠시 중단하고, 해당 상황에 대한 우선 처리를 한 후 원래 작업중인 프로세스로 복귀하는 것을 말합니다. 이를 통해 시스템이 예기치 않은 상황에 대해 대응을 할 수 있습니다.
여기서 예기치 않은 상황이란 것은 뭘까요?
● 외부 인터럽트, 내부 인터럽트
예기치 않은 상황이란 말을 듣게되면 당연히 에러부터 생각하실 겁니다. 하지만 CPU의 입장에선 여러 가지 경우가 있습니다. 단순히 분류하자면 소프트웨어 측에서 나는 예외 상황이 있고, 하드웨어 측에서 나는 예외 상황이 있습니다.
소프트웨어 측에서 발생한 인터럽트를 내부 인터럽트(혹은 소프트웨어 인터럽트)라고 하고,
하드웨어 측에서 발생한 인터럽트를 외부 인터럽트(혹은 하드웨어 인터럽트)라고 합니다.
● 내부 인터럽트 (Internal Interrupt, 또는 Exception)
내부 인터럽트(소프트웨어 인터럽트)란 CPU 내부에서 발생하는 예외 상황이나 오류에 대한 인터럽트를 말합니다. 내부 인터럽트는 주로 소프트웨어 또는 하드웨어의 잘못된 동작으로 인해 발생하며, 일반적으로 CPU가 스스로 감지해냅니다.
내부 인터럽트의 주요 예시를 살펴보자면,
디바이드 바이 제로(0 나누기):프로그램이 숫자를 0으로 나누려고 할 때 발생하는 예외.
페이지 폴트:메모리 관리에서 잘못된 메모리 주소에 접근할 때 발생하는 예외.
잘못된 명령어:CPU가 알 수 없는 명령어를 처리하려고 할 때 발생.
오버플로우:산술 연산이 CPU 레지스터의 최대값을 초과할 때 발생.
시스템 콜(System Call):운영체제의 기능을 호출할 때 발생하는 인터럽트. 사용자가 커널 모드로 진입하기 위해 주로 사용.
● 외부 인터럽트 (External Interrupt)
외부 인터럽트(하드웨어 인터럽트)란 CPU 외부를 통해 발생하는 이벤트에 대한 인터럽트를 말합니다. 외부 인터럽트는 주로 입출력 장치나, 타이머 등의 외부장치가 특정 조건을 충족할 때 발생하는 이벤트를 통해 발생합니다.
외부 인터럽트의 주요 예시를 살펴보자면,
주요 외부 인터럽트 예시:
키보드 입력: 사용자가 키보드를 눌렀을 때 발생하는 인터럽트.
마우스 클릭: 마우스 클릭 등의 입력이 있을 때 발생.
타이머 인터럽트: 타이머가 설정된 시간이 다 되었을 때 발생. 보통 운영체제에서 시간 기반의 스케줄링에 사용.
I/O 인터럽트: 네트워크 카드, 하드 드라이브 등에서 데이터 전송이 완료되었을 때 발생.
전원 관리 인터럽트: 배터리 수준이 낮거나 전원이 꺼지기 직전에 발생.
●인터럽트의 처리 과정
인터럽트는 어떤 과정을 통해 처리를 할까요?
1. 인터럽트가 발생할 시, CPU는 현재 상태를 저장하고 실행 중인 작업을 잠시 중단합니다.
2. 인터럽트 벡터 테이블을 통해 해당 인터럽트를 확인하고, 인터럽트를 처리하는 핸들러로 점프해 예외 처리를 합니다.
3. 예외 처리를 다 하면 저장해놓은 상태를 참조해 원래 작업으로 복귀합니다.
인터럽트 벡터 테이블 이란?
인터럽트 벡터 테이블은 인터럽트가 발생했을 때, 어떤 인터럽트가 발생했는지 식별하고, 해당 인터럽트에 맞는 처리 코드(인터럽트 핸들러)의 주소를 저장해놓은 테이블입니다.
● 내부 인터럽트와 외부 인터럽트의 주요 차이점
내부 인터럽트와 외부 인터럽트의 주요 차이점을 보고 마무리 해보겠습니다.
발생 원인
CPU 내부에서 발생 (프로그램 실행 중)
외부 하드웨어 장치에서 발생
예시
디바이드 바이 제로, 페이지 폴트 등
키보드 입력, 타이머, 네트워크 카드 등
발생 시점
예측 불가능한 오류나 특정 조건이 발생할 때
장치에서 특정 이벤트가 발생할 때
CPU 개입 여부
CPU 스스로 감지
외부 장치에 의해 발생
인터럽트들은 시스템이 다양한 작업을 동시에 처리하고, 예기치 않은 상황에 대처할 수 있도록 하기 위해 만들어진 매커니즘입니다.
따라서 하나의 코어에서 여러 개의 프로세스들을 짧은 시간동안 처리하려면 시분할로 작업을 처리해야 하는데요.
여러 프로세스를 효율적으로 처리하려면 각 프로세스를 언제, 얼마나 작업을 처리할지 신중히 선택해야합니다. (저희가 해야하냐고요? 아뇨 커널이 해줍니다 ㅋ)
커널은 각 프로세스마다 형평성과 효율성에 맞게 작업 순서와 시간을 배분하는데요. 이를 CPU 스케줄링이라고 말합니다.
CPU 스케줄링의 주요 목표
CPU 스케줄링의 목표는 다음과 같습니다.
공정성 : 각 프로세스나 스레드가 공정하게 CPU를 사용할 수 있어야 합니다.
응답 시간 최소화 : 프로세스의 요청에 대한 응답 시간을 줄여야 합니다.
처리량 최적화 : 주어진 시간 동안 최대한 많은 작업을 처리하는 것이 중요합니다.
대기 시간 최소화 : 각 프로세스의 작업이 대기하는 시간을 줄여야 합니다.
CPU사용율 최적화 : 유휴 상태에 빠진 CPU가 없이 최대한 많이 사용되도록 관리해야 합니다.
프로세스의 우선순위
CPU 스케줄링은 프로세스의 우선순위를 바탕으로 작업 배분을 하게 됩니다.
그렇다면 프로세스의 우선순위는 어떠한 기준으로 정해질까요?
커널 프로세스 vs 일반 프로세스 : 커널 프로세스는 운영체제의 중요한 작업을 처리하기 때문에, 일반 프로세스보다 우선순위가 높습니다.
CPU 집중 프로세스 vs 입출력 집중 프로세스 : CPU를 많이 사용하는 작업을 CPU 집중 프로세스, 입출력(I/O)작업을 많이 수행하는 작업을 입출력 집중 프로세스라고 합니다. 입출력 집중 프로세스는 빠르게 입출력 대기 상태로 전환되기 때문에 시스템 효율성을 높일 수 있어 CPU 집중 프로세스보다 우선순위가 높습니다. 이를 사이클 훔치기라고 합니다.
전면 프로세스 vs 후면 프로세스 : 전면 프로세스는 사용자와 상호작용하는 프로세스, 후면 프로세스는 사용자와 상호작용이 없는 프로세스입니다. 전면 프로세스는 사용자와 상호작용을 통해 즉각적인 응답을 요구하므로 후면 프로세스보다 우선순위가 높습니다.
프로세스 상태의 다중 큐
프로세스의 우선순위는 프로세스 제어 블록(PCB)에 표시됩니다.
CPU스케줄러는 프로세스의 제어 블록을 탐색해서 우선순위가 가장 높은 프로세스를 작업하게 되는데요, 하지만 매번 모든 프로세스를 탐색하기는 부담이 상당히 많이 갑니다.
때문에 다중 큐 (Multiple Queue) 라는 것을 사용하게 되었습니다.
● 준비 상태의 다중 큐
왼쪽 그림은 우선순위 별로 정렬하지 않은 상태입니다.
이러면 우선순위가 높은 프로세스를 찾기 위해 일일히 찾아야 해서 비용이 큽니다 ㅠ.
때문에 오른쪽 그림처럼 우선순위 별로 큐를 만든 후, 우선순위 단위로 큐를 처리하면 편합니다.
준비 상태의 다중 큐는 프로세스 우선순위를 배정하는 방식이 2가지가 존재하는데, 바로 고정 우선순위 방식과 동적 우선순위 방식입니다.
고정 우선순위란, 프로세스가 생성될 때 우선순위가 결정되며, 실행 중에 바꿀 수 없는 방식입니다. 보통 시스템이 자동으로 설정하거나 사용자가 지정합니다.
ㄴ ex) 운영체제의 중요한 작업이나 시스템 프로세스는 높은 우선순위를 가지고 실행됩니다.
동적 우선순위란, 프로세스가 실행되는 도중에 우선순위가 변동될 수 있는 방식입니다. 우선순위가 낮은 프로세스가 우선순위가 높은 프로세스에 밀려 실행이 밀리는 기아 문제 (Starvation)현상을 방지하기 위해 사용합니다.
ㄴ ex) 오래 대기하는 프로세스의 우선순위를 점점 높이는 에이징(Aging) 기법을 사용할 때 자주 씁니다.
●대기 상태의 다중 큐
대기 상태의 다중 큐란, 위 사진처럼 각각의 입출력에 대해 대기 중인 프로세스들의 큐를 말합니다. 대기 큐는 여러 Pcb를 동시에 꺼내어 준비 상태로 옮기는 역할을 합니다.
입출력 장치들이 동시에 많이 처리될 경우 여러 개의 인터럽트를 한번에 처리해야 하는데, 이를 위해 인터럽트 벡터를 사용합니다.
인터럽트 벡터란, 인터럽트가 발생했을 때 어떤 인터럽트가 발생했는지 식별하고, 해당 인터럽트에 맞는 처리를 하기 위한 일종의 테이블입니다.
대기 큐는 사실 이름과 달리 무조건 선입선출이 아닙니다. 작업 속도를 높이기 위해 작업 순서를 뒤바꾸는 경우가 있기 때문입니다.
선점형 스케줄링과 비선점형 스케줄링
CPU 스케줄러는 스케줄링을 작성할 때 2가지 방식을 고려합니다. 바로 선점형 스케줄링과 비선점형 스케줄링인데요.
선점형 스케줄링이란, 어떤 프로세스가 작업중일 때 다른 프로세스가 작업을 위해 현재 프로세스의 작업을 중단하고 해당 프로세스의 작업을 할당받는 방식입니다. 즉 한 코어가 여러 프로세스를 작업하는 방식입니다.
비선점형 스케줄링이란, 선점형 스케줄링의 반대로 어떤 프로세스가 작업중일 때 다른 프로세스가 작업을 할당받지 못하도록, 즉 한 프로세스가 한 코어의 작업을 독점하는 방식입니다.
스케줄링 알고리즘
CPU 스케줄러는 스케줄링을 효율적으로 작성하기 위해 여러 알고리즘을 사용한다.
당연히 효율적인 알고리즘을 사용하기 위해선 평가 기준이 있어야한다.
●스케줄링 알고리즘 평가 기준
CPU 스케줄러의 알고리즘 평가(목표) 기준은 다음과 같다.
대기 시간 (Waiting Time) : 각 프로세스가 CPU할당을 기다리는 시간을 최소화 하는 것.
반환 시간 (Turnaround Time) : 프로세스가 시작부터 끝날 때까지 걸리는 시간을 최소화 하는 것.
응답 시간 (Response Time) : 사용자가 요청을 시작한 후 첫 번째 응답을 받는 시간을 최소화하는 것.
CPU 이용률 (CPU Utilization) : CPU가 유휴없이 최대한 많은 시간을 작업 처리에 사용하도록 하는 것.
문맥 전환 오버헤드(Context Switching Overhead) : 문맥 전환에 소모되는 자원을 최소화하는 것.
공정성(Fairness) : 기아(Starvation) 문제 없이 모든 프로세스가 공평하게 CPU를 할당받는 것.
처리량(Throughput) : 단위 시간당 처리할 수 있는 프로세스의 수를 최대화하는 것.
우선순위(priority handling) : 우선순위가 높은 프로세스에 더 많은 CPU 자원을 할당하는 것.
●스케줄링 알고리즘 종류
스케줄링 알고리즘에는 다양한 종류가 있지만, 크게 선점형 알고리즘과 비선점형 알고리즘을 사용하는지에 따라 나눌 수 있다.
●비선점형 알고리즘
1. FCFS 스케줄링(First Come First Served)
준비 큐에 도착한 순서대로 CPU에 작업을 할당하는 방식입니다. 준비 큐가 하나라 모든 프로세스의 우선순위가 동일합니다. 때문에 처리 시간이 긴 프로세스가 CPU를 차지하면 다른 프로세스가 작업을 못하기 때문에 효율성이 낮아진다.
2. SJF 스케줄링(Shortest Job first)
준비 큐에 있는 프로세스 중에 실행 시간이 가장 짧은 작업부터 처리하는 방식입니다. 최단 작업 우선 스케줄링이라고도 합니다. 하지만 현대의 프로세스는 사용자와 빈번하게 상호 작용하기 때문에 프로그램의 실행 시간을 파악하기가 어려워 이 알고리즘을 사용하기 힘듭니다. 작은 작업부터 처리하기 때문에 효율적일 수 있으나, 실행 시간이 긴 프로세스의 CPU할당이 연기될 수 있는 기아 문제가 발생할 수 있으므로 공평성에 위배된다. 이 때, 대기 시간이 길수록 우선 순위를 높이는 에이징 방식으로 완화할 수 있다.
3. HRN 스케줄링(Highest Response Ratio Next)
SJF 스케줄링에서 발생할 수 있는 기아 상태를 기아 상태를 해결하기 위해 만들어진 알고리즘입니다. 최고 응답률 우선 스케줄링이라고도 합니다. SJF의 우선순위 판단 기준이 프로세스의 실행 시간이었다면, HRN은 대기 시간과 사용 시간에 대한 연산을 통하여 우선순위를 정하는 방식입니다.
●선점형 알고리즘
1. 라운드 로빈 스케줄링(Round Robin)
한 프로세스가 할당받은 시간(타임 슬라이스)동안 작업을 하다가 완료하지 못하면 다시 큐에 집어 넣습니다. 우선순위를 적용하지 않는 단순한 방식의 알고리즘입니다.
2. SRT 스케줄링(Shortest Remaining Time)
SJF 스케줄링과 라운드 로빈 스케줄링을 혼합한 방식의 알고리즘입니다. 최소 잔류 시간 우선 스케줄링이라고도 합니다.
라운드 로빈 스케줄링의 방식을 기본적으로 쓰되, CPU가 다음 프로세스를 할당할 때, 남은 작업 시간이 가장 적은 프로세스를 선택합니다. 하지만 이 또한 기아 문제때문에 잘 사용하지 않습니다.
3. 다단계 큐 스케줄링(Multi-Level Queue)
우선순위에 따라 준비 큐를 여러 개 사용하는 방식입니다. OS로부터 부여받은 우선순위에 따라 각 우선순위에 맞는 큐에 삽입됩니다. 각 우선순위 큐는 라운드 로빈 알고리즘 방식으로 운영되며, 고정 우선순위 방식을 사용합니다. 제일 상위 우선순위 큐의 모든 프로세스 작업이 끝나야 다음 우선순위 큐 작업이 시작됩니다. 때문에 하위 우선순위 큐의 작업들이 연기되는 문제가 있습니다.
3. 다단계 피드백 큐 스케줄링(Multi-Level Feedback Queue)
다단계 큐 스케줄링에서 에이징 기법을 적용해 단점을 보완한 방식입니다.
다단계 피드백 큐 방식의 특징은, CPU 할당이 끝난 프로세스가 원래 우선순위의 큐에 되돌아 가지 않고 한 단계 아래의 우선순위 큐에 들어가게 됩니다. 때문에 변동 우선순위 방식을 사용합니다. 또한 우선순위에 따라 할당하는 시간(타임 슬라이스)가 다릅니다. 우선순위가 낮은 큐의 프로세스는 CPU를 좀 더 사용할 수 있도록 타임 슬라이스를 크게 부여합니다.
사진을 자세히 보시면 레지스터>캐시(L1, L2, L3)>주 기억장치(RAM)>보조 기억장치(HDD)순으로 속도가 빠르다고 나와있는데요. 이는 CPU와의 물리적 거리가 해당 순서대로 가깝기 때문입니다.또한 고속 메모리를 만들기 위한 설계와 비용적 제약으로 인해 처리속도와 저장용량이 반비례하게 됩니다.
2. 휘발성 메모리와, 비휘발성 메모리로 나뉜다.
레지스터, 캐시, RAM은 휘발성 메모리, HDD는 비휘발성 메모리입니다.
여기서 휘발성 메모리와 비휘발성 메모리는 데이터 유지 특성에 따라 구분하는데요.
- 휘발성 메모리 : 전원이 공급될 때만 데이터를 유지하는 메모리입니다. CPU에 접근해 빠른 속도로 데이터를 읽고 쓰기가 가능해 작업 중인 데이터를 임시로 저장하는 용도로 사용합니다.
- 비휘발성 메모리 : 전원이 꺼져도 데이터를 유지하는 메모리입니다. CPU에 직접 접근할 수 없고 커널(파일 시스템)을 통해 RAM에 로드하기 때문에 속도가 느립니다. 주로 영구 저장용 메모리로 사용됩니다.
이런 특징들로 인해 각자의 역할이 나뉘어져 있습니다. 하나씩 차근차근 알아보겠습니다.
레지스터(Register)
레지스터는 CPU에서 가장 가까운 위치에 있는 메모리입니다.
1. 레지스터의 특징
레지스터의 주요 특징을 살펴보겠습니다.
CPU가 연산 등의 요청을 처리하는데 필요한데이터를 일시적으로 저장합니다.
속도가 매우 빠릅니다. (CPU와 가장 가까운 메모리이기 때문)
휘발성 메모리입니다.
용량이 매우 작습니다.
2. 레지스터가 데이터를 읽고 쓰는 과정
사실 로우레벨 프로그래밍을 하지 않으면 접할 일이 거의 없을겁니다.
굳이 볼 수 있긴한데, 디버그 중VS의 디스어셈블리 창을 열어보시면 해당 사진과 비슷한 광경이 나옵니다.
그 전에 먼저, CPU에는산술연산장치(ALU)라고 CPU로부터 들어온 연산요청을 처리하는 장치가 있습니다.
해당 연산에 사용할 데이터를 주로 레지스터에서 꺼내오게 되는데요. 연산을 수행한 후, 결과를 다시 레지스터나 RAM에 저장하게 됩니다.
#define COUNT 10000000
double Process1(std::vector<int>& vec)
{
auto start = std::chrono::steady_clock::now();
for (auto o : vec)
{
++o;
}
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
return elapsed_seconds.count();
}
double Process2(std::list<int>& list)
{
auto start = std::chrono::steady_clock::now();
for (auto o : list)
{
++o;
}
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
return elapsed_seconds.count();
}
int main()
{
std::vector<int> vec;
std::list<int> list;
for (int i = 0; i < COUNT; i++)
{
vec.push_back(i);
list.push_back(i);
}
std::cout << "vector : " << Process1(vec) << '\n';
std::cout << "list : " << Process2(list) << '\n';
}
(막상 해보니까 차이가 너무 많이나는데...머지...)
어쨋든 차이가 심합니다. 캐시 적중률을 높이는 것은 개발자에 있어서 숙명이라고 할 수 있겠습니다.
6. 캐시 일관성
CPU는 각기마다 개별 캐시 메모리를 소유하고 있습니다.
때문에 멀티코어 시스템 환경에선 각 코어가 캐시를 사용하는 동안, 서로 동일한 메모리 주소에 대한 데이터가 일관되게 유지되도록 해야합니다. 이를 캐시 일관성이라고 하는데요.
어떤 상황에서 캐시 일관성 문제가 발생할까요?
CPU1과 CPU2가 있다고 가정해봅시다.
CPU1은 메모리 주소 X에 저장된 값을 자신의 캐시에 저장했습니다.
CPU2도 메모리 주소 X에 저장된 값을 자신의 캐시에 저장했습니다.
CPU1이 캐시에서 값을 읽어와 변경하고, 메모리에 쓰지 않았습니다.
CPU1이 캐시에서 값을 변경한 후 메모리에 쓰지 않았기 때문에 CPU2는 여전히 이전 값을 자신의 캐시에 유지하게 됩니다.
이 경우 CPU1과 CPU2는 동일한 메모리 주소 X에 대한 서로 다른 값을 가지게 됩니다.
이러한 문제가 발생할 시 데이터 무결성에 문제가 생깁니다.
이를 방지하기 위해 캐시 일관성 프로토콜이 사용된다는데요? 나중에 알아봅시다.
메인 메모리(RAM)
프로세스의 주소공간에 할당되는 메모리는 메인 메모리에 해당됩니다.
1. 메인 메모리의 특징
메인 메모리의 주요 특징에 대해 살펴보겠습니다.
프로그램이 실행 중일 때 필요한 데이터와 명령어를 임시로 저장합니다.
CPU에게 처리할 데이터를 전달해줍니다.
레지스터, 캐시보단 속도가 느리지만, HDD보다 빠릅니다.
휘발성 메모리입니다.
HDD에 비해 상대적으로 작은 용량을 가지고 있습니다. (통상 8~32GB)
2. RAM과 HDD의 관계
RAM은 HDD의 데이터를 로드해 CPU가 빠르게 데이터를 처리할 수 있도록 도와줍니다. RAM이 부족할 경우, 가상 메모리나 페이지 파일을 통해 HDD를 메모리처럼 사용할 수 있습니다.
예를 들자면, 운영체제나 프로그램을 실행할 때, HDD에서 RAM으로 데이터를 로드하여 프로그램이 원할하게 실행되도록 해줍니다.
하드 디스크(HDD, SSD)
메모리 계층 구조에서 CPU가 데이터를 접근하는 데 가장 느린 장치입니다.
1. 하드 디스크의 특징
하드 디스크의 주요 특징에 대해 살펴보겠습니다.
장기적인 데이터 저장을 위해 사용합니다.
속도가 느립니다.
비휘발성 메모리입니다.
대용량 저장이 가능합니다.
2. HDD vs SSD
HDD와 SSD는 다들 들어보셨을겁니다.
용량은 HDD가 SSD보다 많지만, 속도는 SSD가 훨 빠르다는 사실을 대부분 아실겁니다. 그 이유가 뭘까요?
1. HDD와 SSD의 구조적 차이
HDD : 데이터를 읽고 쓰기 위해서 기계적인 부품(플래터)와 읽기/쓰기 헤드를 사용합니다. 데이터를 읽거나 쓰기 위해서는 플래터가 특정 위치로 회전해야 하고, 읽기/쓰기 헤드가 해당 위치로 이동해야 합니다. 이 과정에서 데이터 접근 시간이 길어집니다. 또한 데이터를 병렬적으로 처리하지 못하기 때문에 동시에 여러 데이터에 접근하기 어렵습니다.
SSD : 데이터를 전자적으로 처리하는 플래시 메모리 칩을 사용하여 데이터를 읽고 씁니다. 기계적 부품이 없기 때문에 데이터에 접근하는 속도가 매우 빠릅니다. 또한 내부에서 병렬 처리가 가능하기 때문에 여러 데이터에 빠르게 접근할 수 있습니다.