한 프로그램이 디스크를 기다리는 동안 CPU는 쉬는 걸까요, 다른 일을 하러 가는 걸까요? 창구에 앉은 사람이 바뀔 때는 어디까지 했는지 적어 두고 다음 사람 서류를 꺼내 오는 시간이 듭니다.
기초에서 먼저 보기컴퓨터는 기다리는 동안 다른 일을 할 수 있을까?많이 받는 질문 두 가지에 답하는 단원입니다. "IO를 만나면 CPU는 쉬나요, 아니면 다른 일을 하러 가나요?" 그리고 "코어가 하나인데 어떻게 여러 작업을 나눠서 하고, 대체 언제 쉬나요?" 결론부터 말하면 CPU는 거의 쉬지 않습니다. 아래에서 프로그램 A가 파일을 읽으러 간 순간부터, B와 C가 실행되고, CPU가 잠들었다 다시 깨어나 A가 이어서 실행되기까지를 한 단계씩 따라갑니다.
우리가 짠 코드는 사용자 모드에서 돕니다. 여기서는 디스크에 직접 명령하거나 다른 프로그램의 메모리를 건드릴 수 없습니다. 그런 일이 필요하면 운영체제에게 부탁해야 하고, 그 부탁이 시스템 콜입니다. 시스템 콜이 일어나면 CPU는 커널 모드로 바뀌고 운영체제 코드를 실행합니다. 중요한 점은, 이때도 CPU는 쉬는 게 아니라 그냥 다른 코드(운영체제)를 실행하고 있다는 것입니다.
실행 중인 프로그램의 "현재 상태"는 사실 CPU 레지스터 몇 개에 들어 있습니다 — 어디까지 실행했는지(PC), 계산 중이던 값들, 스택의 현재 위치. 이 한 묶음을 문맥이라고 부릅니다. CPU는 한 번에 한 문맥만 들 수 있으므로, 다른 프로그램을 실행하려면 지금 문맥을 어딘가에 적어두고 새 문맥을 불러와야 합니다. 이 저장과 복원 한 쌍이 컨텍스트 스위치입니다. 애니메이션에서 노란 칸이 저장되는 순간, 파란 칸이 복원되는 순간입니다.
첫째는 자발적 양보입니다. IO를 기다려야 하는 프로그램은 스스로 시스템 콜을 불러 커널에 제어권을 넘기고 Waiting 상태로 내려갑니다. 둘째는 강제 회수, 즉 선점입니다. 계산만 계속하며 양보하지 않는 프로그램을 위해, 하드웨어 타이머가 몇 밀리초마다 인터럽트를 걸어 CPU를 강제로 커널에게 되돌립니다. 이 두 가지 덕분에 코어 하나로도 모두가 조금씩 진행할 수 있습니다.
디스크는 데이터를 다 읽으면 CPU에게 인터럽트라는 신호를 보냅니다. 그러면 CPU는 하던 일을 잠깐 멈추고 커널의 처리 코드로 점프해, 기다리던 프로그램을 Waiting에서 Ready로 옮깁니다. 즉 CPU가 디스크를 계속 쳐다보며 기다리는 게 아니라, 다 되면 장치가 알려주는 구조입니다.
실행할 수 있는(Ready) 프로그램이 하나도 없을 때뿐입니다. 그때 커널은 idle 태스크를 실행하고, HLT 같은 명령으로 CPU를 저전력 상태로 재웁니다. 그리고 인터럽트가 들어오면 깨어납니다. 노트북 배터리가 오래 가는 것도, 서버 CPU 사용률이 5%로 찍히는 것도 이 구간 덕분입니다. 아래 타임라인에서 빗금 친 구간이 그 "진짜 쉬는 시간"입니다.
CPU는 지금 프로그램 A의 코드를 실행 중입니다. CPU 안의 레지스터에는 A의 PC와 값들이 올라와 있습니다 — CPU는 한 번에 한 프로그램의 문맥(context)만 들고 있습니다. B와 C는 자기 문맥을 보관함에 넣어둔 채 차례를 기다립니다.
시뮬레이터에서 해보기
이론을 봤으니 실제로 돌려봅시다. 아래에는 성격이 다른 프로그램 세 개가 있습니다. A는 함수를 계속 호출하는 계산 프로그램, B는 무거운 계산만 하는 프로그램, C는 데이터베이스를 부르는 서버입니다. 셋이 CPU 하나를 나눠 씁니다. 초록 테두리가 지금 CPU를 잡은 프로그램이고, 각 코드의 PC 표시가 어디까지 실행됐는지를 보여줍니다. 특히 이것을 확인해 보세요 — A가 tax()를 호출하고 돌아오는 동안 CPU는 다른 프로그램으로 넘어가지 않습니다(함수 호출은 그냥 점프일 뿐입니다). 반면 C는 db.query() 줄에서 스스로 양보하고, B는 양보를 안 하니 타임슬라이스가 다 되면 강제로 끌려 나옵니다. 아래 로그가 매번 그 이유를 알려줍니다. 타임슬라이스를 짧게/길게 바꿔 보면 교대가 잦아지고 커널 비중이 늘어나는 것도 볼 수 있습니다.
이 단원의 용어
왜 이렇게 발전했나
만약 프로그램의 양심에만 맡겼다면(협력적 스케줄링), 무한 루프 하나가 컴퓨터 전체를 멈춰 세울 수 있습니다. 실제로 초기 운영체제들이 그랬습니다. 타이머 인터럽트를 이용한 선점이 도입되면서 비로소 한 프로그램의 폭주가 시스템 전체를 죽이지 않게 되었습니다. 이 차이는 11단원에서 이벤트루프를 볼 때 다시 등장합니다 — 루프에는 이 안전장치가 없습니다.
CPU는 기다릴 때 쉬지 않는다. 커널을 거쳐 다른 일로 간다 — 진짜 쉬는 건 아무도 Ready가 아닐 때뿐.
남은 문제
그런데 프로세스는 만들고 갈아타기 비쌌습니다(5단원). 한 프로그램 안에서 여러 흐름을 돌리는 더 가벼운 방법은 없을까요? →