Proven C Book←↑→

11 멀티태스킹과 인터럽트 — 번갈아 도는 기계

먼저 알아야 할 것

9장 스트림의 기원 · 장치와 통(버퍼)의 그림
10장 프로그램과 프로세스 · 프로세스가 저마다 혼자 도는 것처럼 보인다는 것

돌아보기

10장에서 프로세스가 여럿이라도 저마다 혼자 도는 것처럼 보인다고 했다. 그런데 계산하는 곳이 하나뿐인 기계에서 그 「동시에」는 사실일 수 없다. 실제로 벌어지는 일은 하나뿐이다 — 코어 하나가 이 일을 조금 하다 멈추고, 저 일을 조금 하다 멈추기를 아주 빠르게 되풀이한다.

그러면 곧바로 세 가지가 궁금해진다. 그 번갈아 하기를 누가 정하는가? 언제 끊는가 — 명령을 몇 개 실행할 때마다? 프로그램이 부탁할 때? 아니면 정해진 시간마다? 그리고 그 「이제 끊을 때다」를 어떻게 코어에게 알리는가?

답. 이 세 물음의 답이 이 장의 뼈대다.

누가 — 운영체제 안의 스케줄러가 정한다. 어느 일에게 얼마나 줄지 정하는 정책이 거기 있다.

언제 — 두 가지 계기가 있다. 하나는 프로그램이 스스로 멈추는 때다(디스크를 기다리거나 입력을 기다리면 더 할 일이 없다). 다른 하나는 정해진 시간이 다 됐을 때다. 명령을 몇 개 세는 방식은 쓰지 않는다 — 그 이유는 이 장에서 본다.

어떻게 — 시계 장치가 정해진 간격마다 코어를 불러 세운다. 그 부름이 바로 인터럽트다. 곧 멀티태스킹과 인터럽트는 서로 다른 물건이지만, 정책이 하드웨어를 부리는 이 자리에서 만난다.

이 장의 필요성과 맥락

먼저 솔직하게 밝혀 둔다. 인터럽트는 C 프로그램에서 직접 쓰는 물건이 아니다. 표준 C 에는 인터럽트를 걸거나 받는 문법이 없고, 이 책의 나머지에서도 인터럽트를 직접 다룰 일은 없다. 그러니 「쓸모」를 기준으로 보면 이 장은 건너뛰어도 된다.

그런데도 한 장을 들이는 까닭이 셋이다. 첫째, 아예 무관하지는 않다 — C 표준의 신호(81장)가 이 구조를 그대로 물려받았다. 둘째, CPU 안에서 벌어지는 경합과 작업 사이의 전환을 이해하는 데 이만한 열쇠가 없다. 셋째, 한때 이것이 개인용 컴퓨터의 거의 모든 동작을 처리하는 기본이었다 — 도스 시절 이야기다.

그리고 이 장은 두 가지를 나란히 놓는다. 멀티태스킹은 「시간을 어떻게 나눠 쓸 것인가」라는 정책이고, 인터럽트는 「바깥의 사건을 코어에게 알리는」 장치다. 목적이 분명히 다르지만, 정책이 그 장치를 부려야만 성립하기 때문에 둘을 갈라서 이해하고 이어서 이해해야 한다. 10장가 「프로세스란 무엇인가」를 말했다면, 이 장은 그 프로세스들이 어떻게 번갈아 도는가를 말한다.

그러니 이 장은 「써먹으려고」가 아니라 컴퓨터의 구조를 더 잘 이해하려고 읽는 자리다. 그 정도로 들어 두면 충분하다.

이 장이 끝나면

코어 하나로 여러 일을 번갈아 하는 방법 — 그 전환을 누가·언제·어떻게 하는지를 알게 된다. 멀티태스킹(multitasking)의 갈래(한 번에 하나만 돌리는 태스크 스위처, 협력형, 선점형, 시분할)와 각 방식이 앓는 병을 본다. 전환할 때 무엇을 옮겨야 하는지(문맥)도 짚는다. 그리고 그 전환의 시점을 알리는 인터럽트(interrupt) — 바깥이 언제든 내 흐름을 멈춰 세울 수 있다는 사실 — 의 원리를, 장치를 잇는 배선부터 인터럽트가 겹칠 때 코드가 견디는 법까지 본다. 마지막으로 도스 시절의 개인용 컴퓨터가 어떻게 이것 위에 통째로 서 있었는지를 본다.

이 장에서 답할 질문

  1. 그러면 인터럽트가 아예 없어도 되지 않나? 어차피 물어봐야 한다면?
  2. 그러면 인터럽트를 아예 못 오게 막을 수는 없는가?

11.1 한 코어로 여럿을 — 번갈아 하기라는 발상#

계산하는 곳이 하나뿐인 기계에서 프로그램 둘을 「동시에」 돌리는 방법은 하나뿐이다. 아주 잘게 번갈아 하는 것이다. 눈에 안 보일 만큼 빠르게 오가면 사람에게는 둘이 함께 도는 것처럼 보인다.

이것을 멀티태스킹(multitasking)이라 한다. 그런데 「번갈아 한다」는 말에는 정해야 할 것이 셋 숨어 있다.

정해야 할 것은 셋이다. 누가 정하는가 — 다음에 어느 일을 돌릴지 고르는 주체이고, 답은 운영체제 안의 스케줄러다. 언제 끊는가 — 지금 도는 일을 멈추는 계기이고, 답은 「스스로 멈출 때」와 「시간이 다 됐을 때」 둘이다. 그리고 어떻게 알리는가 — 그 계기를 코어에게 전하는 방법이고, 답은 시계 장치의 인터럽트다.

세 번째 줄이 이 장에서 멀티태스킹과 인터럽트가 만나는 자리다. 앞의 둘은 정책이고, 마지막 하나는 장치다.

11.2 언제 끊을 것인가 — 세 가지 후보#

「언제 끊는가」에 대한 답은 여러 가지가 있을 수 있다. 후보를 늘어놓고 하나씩 지워 보면 오늘의 방식이 왜 이런지 보인다.

후보어떻게 되는가쓰는가왜
명령을 정해진 개수만큼 실행하면코어가 명령을 세다가 정해진 수에 이르면 멈춘다쓰지 않는다셀 수는 있다. 다만 명령마다 걸리는 시간이 달라 공평하지 않다
프로그램이 스스로 내놓을 때「나는 이제 기다린다」고 말하면 그때 넘긴다쓴다입출력을 기다리는 동안 코어를 놀릴 이유가 없다 — 다만 이것만으로는 부족하다
정해진 시간이 지나면시계가 일정 간격으로 코어를 불러 세운다쓴다내놓지 않는 프로그램에게서도 되찾을 수 있다 — 이것이 있어야 한 프로그램이 기계를 독점하지 못한다
더 급한 일이 깨어나면기다리던 일이 준비되면 지금 도는 것을 밀어낸다쓴다반응이 중요한 일(입력·소리)이 뒤에서 기다리지 않게 한다

표 11.1 — 「언제 끊을 것인가」의 후보들

★ 첫 줄을 지우는 까닭이 중요하다. 흔히 「하드웨어가 명령을 세어 주지 않아서」라고들 하지만 그렇지 않다. 요즘 코어에는 「성능 감시 장치(performance monitoring unit)」가 있어 실제로 물러난 명령을 세고, 세다가 넘치면 인터럽트까지 일으킬 수 있다.

그런데도 쓰지 않는 까닭은 따로 있다. 첫째, 명령 수와 흐른 시간이 비례하지 않는다 (기억을 기다리는 명령은 수십 배 느리다 — 12장). 그래서 「명령 천 개」가 사람에게 뜻하는 시간이 그때그때 다르다. 둘째, 그 계수기는 수가 정해진 귀한 자원이라 아무 때나 꺼내 쓰는 일반 시계가 아니다. 셋째, 운영체제가 지키려는 것은 공평함과 응답인데 그 눈금은 명령이 아니라 시간이다. 그래서 시간으로 끊는다.

11.2.1 스스로 내놓는 것만으로는 왜 부족한가#

둘째 줄만 쓰는 방식도 실제로 있었다. 프로그램이 「나는 여기서 잠깐 물러난다」고 말할 때만 전환하는 것이다. 이것을 협력형(cooperative) 또는 비선점형이라 한다.

문제는 「협력」이라는 말에 그대로 들어 있다. 한 프로그램이 협력하지 않으면 아무도 못 돈다. 계산이 길어지거나, 무한 고리에 빠지거나, 그냥 양보를 잊으면 — 기계 전체가 멈춘다. 마우스도 안 움직인다.

그래서 셋째 줄이 필요하다. 프로그램의 선의와 무관하게 바깥에서 되찾아 오는 장치. 그 방식을 선점형(preemptive)이라 한다.

11.3 멀티태스킹의 갈래#

이제 갈래를 정리할 수 있다. 역사가 대체로 이 순서로 흘렀다.

방식한 번에 도는 일누가 전환을 결정하나장점앓는 병
태스크 스위처하나사람이 — 화면을 바꿀 때단순하다. 기억이 모자라도 된다(재우고 꺼낸다)뒤로 보낸 일은 멈춘다 — 내려받기도 멈춘다
협력형(비선점)여럿도는 프로그램이 스스로 양보할 때전환 자리가 정해져 있어 자료가 안전하다하나가 양보를 안 하면 전부 멈춘다
선점형여럿운영체제가 — 시간이 다 되면 빼앗는다한 프로그램이 기계를 독점하지 못한다언제든 끊기므로 자료를 지켜야 한다
시분할여럿(사람도 여럿)운영체제가 — 사람마다 공평하게여럿이 한 기계를 나눠 쓴다전환이 잦아 값이 든다

표 11.2 — 일을 번갈아 하는 방식들

★ 첫 줄의 태스크 스위처(task switcher)는 멀티태스킹이 아니다. 여러 프로그램을 띄워 둘 수는 있지만 도는 것은 언제나 하나다. 전환할 때 뒤로 가는 프로그램은 통째로 재워지고 (때로는 디스크에 내려놓고), 앞으로 오는 것이 깨어난다. 기억이 귀하던 시절의 타협이다. 지금도 「전환은 되는데 뒤에서는 아무 일도 진행되지 않는」 물건을 만나면 그것이 이 방식이다.

실제 사례. 양보하지 않는 프로그램 하나가 기계를 세운다

협력형이 널리 쓰이던 시절에는 이런 일이 일상이었다. 어떤 프로그램이 긴 계산에 들어가면 화면이 그대로 굳는다. 다른 창을 눌러도 반응이 없고, 시계도 멈추고, 마우스 커서만 모래시계로 바뀐 채 아무 일도 일어나지 않는다. 그 프로그램이 양보해야만 다른 것이 돌 수 있기 때문이다.

선점형으로 옮겨 가면서 이 증상이 사라졌다. 오늘도 한 프로그램이 무한 고리에 빠질 수는 있지만, 그것 때문에 운영체제까지 멈추지는 않는다 — 시계가 되찾아 오기 때문이다.

11.4 전환할 때 무엇을 옮기는가 — 문맥#

일을 바꾼다는 것은 코어의 지금 상태를 통째로 갈아 끼우는 일이다. 그 상태를 문맥(context)이라 하고, 갈아 끼우는 일을 문맥 전환이라 한다.

무엇왜 옮겨야 하나안 옮기면
범용 레지스터계산 도중의 값이 거기 있다돌아왔을 때 남의 값으로 계산한다
프로그램 계수기어디까지 실행했는지어디로 돌아갈지 모른다
스택 포인터제 스택의 꼭대기남의 스택을 제 것으로 쓴다
상태 비트비교 결과 같은 것바로 뒤의 분기가 엉뚱하게 갈린다
주소 공간의 뿌리어느 쪽 표로 주소를 번역할지(10장)남의 기억이 보인다

표 11.3 — 전환할 때 옮겨야 하는 것

여기에 표에 안 적히는 값이 하나 더 붙는다. 전환한 뒤에는 캐시와 번역표에 앞사람이 쓰던 것이 들어 있어서, 새로 온 일은 한동안 느리게 시작한다. 저장하고 복원하는 일 자체보다 이 「식은 캐시」가 더 비쌀 때도 많다(12장).

★ 그래서 전환 간격에는 맞바꿈이 있다. 짧게 끊으면 반응이 빨라지지만 전환에 쓰는 몫이 커지고, 길게 끊으면 그 반대다. 운영체제마다 이 값을 다르게 잡고, 같은 운영체제도 일의 성격에 따라 다르게 준다.

11.5 그 시점을 누가 알리는가 — 시계 인터럽트#

이제 마지막 물음이다. 「시간이 다 됐다」를 코어에게 누가 알리는가?

코어는 스스로 시계를 보지 않는다. 대신 시계 장치가 정해진 간격마다 코어를 불러 세운다. 그 부름이 이 장의 나머지 절반, 곧 인터럽트다.

차례무슨 일이 벌어지나누가 하는가
1정해진 간격이 지나 시계 장치가 인터럽트를 건다하드웨어
2코어가 지금 명령까지만 끝내고 운영체제의 처리 코드로 뛴다하드웨어
3시각을 세고, 이 일의 시간이 다 됐는지 본다운영체제
4다 됐으면 문맥을 갈아 끼워 다음 일로 넘긴다운영체제(스케줄러)
5아니면 하던 일로 그대로 돌아간다운영체제

표 11.4 — 시계 인터럽트가 하는 일

★ 표의 3~5 줄이 정책이고 1~2 줄이 장치다. 같은 시계 인터럽트 위에 어떤 정책을 얹느냐에 따라 협력형도 선점형도 될 수 있다 — 실제로 도스 시절에는 응용 프로그램이 이 인터럽트를 가로채 제 나름의 정책을 얹기도 했다(이 장의 뒷부분에서 본다).

지금까지가 「번갈아 하기」의 이야기다. 여기서부터는 그 마지막 고리인 인터럽트 자체를 본다 — 시계만이 아니라 키보드도, 디스크도, 통신선도 같은 방식으로 말을 걸어온다.

11.6 장치와 잇는 선 — 줄지어 보내기와 한꺼번에 보내기#

9장에서 본 카드 판독기도, 라인프린터도, 텔레타이프도 결국 선으로 컴퓨터에 붙어 있었다. 그 선을 어떻게 놓느냐에 따라 장치의 성격이 갈린다.

줄지어 보내기(직렬)한꺼번에 보내기(병렬)
선의 수한 가닥여덟 가닥(바이트 하나면)
한 박자에1비트8비트
필요한 약속「여기서 시작」과 「여기서 끝」을 표시해야 한다「지금 값이 유효하다」를 알리는 선이 따로 있다
어려운 점빠르게 보내려면 양쪽의 시계가 잘 맞아야 한다여덟 줄이 똑같은 순간에 도착해야 한다

표 11.5 — 바이트 하나를 보내는 두 가지 방법

텔레타이프는 앞쪽이었다. 선 한 가닥에 비트를 줄지어 흘려보내고, 받는 쪽은 「시작」 표시를 보고 시계를 맞춘 뒤 정해진 간격으로 값을 집어 간다. 프린터를 잇던 굵은 케이블은 뒤쪽이었다 — 여덟 가닥에 한 바이트를 한꺼번에 싣고, 「지금 유효하다」는 신호를 따로 보냈다.

직관과 달리, 빨라질수록 이기는 쪽은 직렬이다. 줄이 여덟이면 줄마다 도착 시각이 조금씩 어긋나는데, 속도를 올릴수록 그 어긋남이 한 비트의 시간보다 커져 버린다. 그래서 오늘날 빠른 연결은 대개 직렬 한 가닥을 아주 빠르게 만들고 그것을 여러 벌 묶는 쪽으로 갔다.

한 가지만 더 짚어 둔다. 작은 기계에서는 바깥으로 난 선 하나가 입력이 될 수도 있고 출력이 될 수도 있다. 어느 쪽으로 쓸지는 프로그램이 정해진 자리에 값을 써서 알려 준다. 이 「정해진 자리에 값을 써서 장치를 부린다」가 바깥을 다루는 기본 방식이고, 그 자리는 주소로 가리키지만 기억이 아니다 — 읽으면 지금 상태가 나오고, 쓰면 무슨 일이 벌어진다. 이 구분이 나중에 volatile이라는 낱말을 낳는다 (14장).

그런데 선을 어떻게 놓든 남는 문제가 둘 있다. 하나는 양쪽의 속도가 다르다는 것이고 (그래서 9장에서 본 통을 사이에 둔다), 다른 하나가 이 장의 주제다 — 바깥이 말을 걸어오는 시점을 누가 정하는가.

11.7 바깥이 말을 걸어오는 시점#

9장에서 들어오는 쪽에 통이 있다고 했다. 그러면 그 통에 글자를 담는 것은 누구인가? 키가 눌린 것을 프로그램은 어떻게 알아채는가?

방법이 둘이다.

물어보기(폴링)인터럽트
누가 움직이나내 프로그램이 계속 확인한다장치가 CPU 를 불러 세운다
일이 없을 때묻느라 시간을 쓴다아무 값도 안 든다
일이 생겼을 때다음 물음까지 늦어진다곧바로 알아챈다
코드의 모양순서대로 흐른다흐름이 끊겼다 이어진다

표 11.6 — 바깥의 일을 알아채는 두 가지 방법

폴링(polling)은 프로그램이 「왔니? 왔니?」 하고 되풀이해 묻는 방식이다. 단순하지만, 묻는 사이에 일이 생기면 다음 물음까지 기다려야 하고, 아무 일이 없어도 계속 물어야 한다.

인터럽트(interrupt)는 반대다. 장치가 「일이 생겼다」고 CPU 를 불러 세운다. 그러면 CPU 는 하던 일을 그 자리에서 멈추고, 미리 등록해 둔 처리 코드로 뛰어가 그 일을 처리한 뒤, 멈췄던 자리로 돌아온다.

차례무슨 일이 벌어지나프로그램이 아는가
1장치가 「일이 생겼다」고 알린다모른다
2CPU 가 지금 명령까지만 끝내고 멈춘다모른다
3돌아올 자리를 어딘가에 적어 둔다모른다
4미리 정해진 처리 코드로 뛴다모른다
5처리가 끝나면 적어 둔 자리로 돌아온다아무 일도 없었던 것처럼 이어진다

표 11.7 — 인터럽트가 일어나는 차례

곧 입력 버퍼에 글자가 쌓이는 일은 대개 이렇게 일어난다. 내 프로그램이 다른 일을 하고 있는 동안, 키보드가 끼어들어 통에 한 글자를 넣고 물러난다. 그래서 나중에 읽으러 갔을 때 이미 거기 있는 것이다.

여기서 한 가지가 분명해진다. 인터럽트는 내 코드의 어느 줄 사이에서든 일어날 수 있고, 내 코드는 그것을 알아채지 못한다.

흔한 오해. 내가 쓴 프로그램은 내가 쓴 순서대로, 끊기지 않고 흐른다

순서는 지켜진다. 그러나 끊기지 않는다는 것은 사실이 아니다. 두 줄 사이에, 심지어 한 줄이 여러 걸음으로 나뉜다면 그 걸음 사이에도 다른 코드가 끼어들어 돌 수 있다. 대개는 아무 문제가 없다 — 돌아올 때 원래대로 되돌려 놓기 때문이다. 문제가 되는 것은 끼어든 쪽과 내가 같은 것을 만질 때다.

11.7.1 인터럽트가 여럿이면#

바깥 장치는 하나가 아니다. 타이머도, 키보드도, 통신선도 저마다 「일이 생겼다」고 부른다. 그러면 끼어드는 것들끼리도 부딪친다.

무엇어떤 상황인가그래서 기계는
겹침처리하는 도중에 다른 것이 또 부른다누구를 먼저 받을지 우선순위를 정해 둔다
중첩급한 것이 처리 중인 것을 다시 가로챈다허용하거나 금지한다 — 기계마다 다르다
기다림지금은 못 받으니 「부탁이 있었다」만 적어 둔다받을 수 있게 되면 그때 처리한다
놓침같은 장치가 처리되기 전에 또 부른다대개 부탁이 있었다는 사실만 남고 횟수는 사라진다
늦어짐급한 일이 덜 급한 일 뒤에서 기다린다그 늦어짐이 곧 이 기계의 응답 한계가 된다

표 11.8 — 인터럽트가 여럿일 때 생기는 일

★ 넷째 줄이 특히 낯설다. 인터럽트는 세어지지 않는다. 「부탁이 있었다」는 사실은 켜짐과 꺼짐 하나로 적히는 것이 보통이라, 처리되기 전에 열 번이 더 와도 처리는 한 번 일어난다. 그래서 바깥을 다루는 코드는 「몇 번 불렸나」를 세는 대신 장치에게 무슨 일이 있었는지를 물어본다.

11.7.2 「물어본다」는 것이 무슨 뜻인가#

방금 「장치에게 물어본다」고 했다. 그런데 어떻게 묻는가? 편지를 보내는가? 무슨 함수를 부르는가?

정해진 주소를 읽는 것이 곧 묻는 것이다. 새 문법도, 특별한 함수도 없다.

앞에서 바깥 장치는 「정해진 자리에 값을 쓰고 읽는 방식으로 다룬다」고 했다. 그 자리를 장치의 창구라고 생각하면 된다. 창구는 주소로 가리키지만 기억이 아니다. CPU 가 그 주소를 선에 실으면, 그 주소로 배선된 장치가 지금 자기 상태를 값으로 실어 보낸다. 읽기 한 번이 질문이고, 그때 실려 오는 값이 대답이다 — 주고받는 절차가 따로 있는 것이 아니다.

장치가 여는 창구는 대개 이런 것들이다.

창구읽으면쓰면무엇을 묻고 답하나
상태지금의 사정이 비트로 나온다대개 못 쓴다「받을 것이 있나?」 「보낼 자리가 났나?」 「무언가 잘못됐나?」
자료들어온 값 하나가 나온다내보낼 값 하나를 넣는다「그 값 줘」 「이거 내보내」
제어설정을 되읽는다장치의 동작을 바꾼다「이 속도로 해」 「인터럽트를 걸어 줘」
개수쌓여 있는 개수가 나온다대개 못 쓴다「몇 개나 와 있나?」 — 이런 창구를 두는 장치도 있다

표 11.9 — 장치가 여는 창구들

예를 들어 보자. 문자를 받는 장치(9장의 그 통에 글자를 넣어 주는 쪽)에 인터럽트가 걸렸다. 그런데 그 사이에 바이트가 셋 도착해 있었다면?

「몇 번 불렸나」를 세는 코드는 한 번 불렸으니 하나만 가져가고, 나머지 둘을 잃는다. 물어보는 코드는 이렇게 한다.

void uart_isr(void)                          /* 인터럽트가 걸리면 불린다 */
{
    while (*UART_STATUS & RX_READY)          /* 「받을 것이 있나?」 --- 묻는다 */
        buffer_push(*UART_DATA);             /* 「이거」 --- 읽으면 한 바이트가 나온다 */
}

*UART_STATUS 는 상태 창구를 읽는 것이고, 그 값의 한 비트가 「받을 것이 있다」를 뜻한다. 있으면 자료 창구를 읽어 한 바이트를 가져오고, 다시 묻는다. 셋이 와 있었다면 고리를 세 번 돌고, 없으면 한 번도 안 돌고 끝난다. 몇 번 불렸는지는 아무 상관이 없다.

여기에 처음 보면 놀라운 성질이 하나 붙는다. 읽는 행위가 장치의 상태를 바꾼다. 자료 창구를 읽으면 그 바이트는 장치의 큐에서 빠지고, 더 남은 것이 없으면 상태 창구의 「받을 것이 있다」 비트도 저절로 내려간다. 그래서 위의 고리는 저절로 멈춘다.

흔한 오해. 읽기는 값을 가져올 뿐 아무것도 바꾸지 않는다

기억이라면 그렇다. 장치의 창구는 다르다. 읽는 순간 큐에서 값이 빠지고, 상태 비트가 지워지고, 때로는 인터럽트 요청 자체가 취소된다. 그래서 「값을 두 번 읽어 확인하자」 같은 습관이 장치 앞에서는 사고가 된다 — 두 번째 읽기는 다음 바이트를 가져가 버린다.

거꾸로, 이 성질을 써야만 하는 자리도 있다. 어떤 장치는 상태 창구를 읽어 주어야 인터럽트 요청이 내려간다. 처리기가 그 읽기를 빠뜨리면 요청이 계속 걸려 있어, 처리기가 끝나자마자 다시 불리고 또 불린다.

문. 그러면 인터럽트가 아예 없어도 되지 않나? 어차피 물어봐야 한다면?

답. 둘은 하는 일이 다르다. 인터럽트는 초인종이고, 물어보기는 문을 열어 누가 왔는지 보는 일이다. 초인종이 없으면 계속 문을 열어 봐야 하고(폴링), 초인종만 있고 문을 안 열면 누가 왔는지 모른다. 그래서 실제 코드는 둘을 함께 쓴다 — 언제 볼지는 인터럽트가 알려 주고, 무엇이 왔는지는 물어서 안다.

문. 그러면 인터럽트를 아예 못 오게 막을 수는 없는가?

답. 잠깐은 막을 수 있다. 「지금은 받지 않겠다」고 표시해 두면 그동안 온 부탁은 기다린다. 그런데 그동안 급한 일도 함께 늦어지고, 오래 막으면 놓치는 것도 생긴다. 그래서 막는 구간은 짧게, 그리고 막기 전의 상태를 기억했다가 원래대로 되돌려 놓는 것이 규율이다.

11.7.3 처리하는 도중에 또 오면 — 기존 코드는 이렇게 견딘다#

앞 표의 「중첩」이 실무에서 가장 성가신 줄이다. 처리기가 도는 도중에 다른 인터럽트가 또 들어오면, 같은 코드가 자기 자신이 끝나기 전에 다시 시작될 수도 있다.

그래서 기계와 코드가 함께 대처한다. 먼저 기계 쪽 — 대부분의 칩은 처리기로 뛰어드는 순간 인터럽트를 자동으로 막아 둔다. 우선순위를 두는 기계라면 「같은 급 이하만 막고, 더 급한 것은 들어오게」 한다. 그래서 중첩은 아무 때나 일어나는 것이 아니라 더 급한 것에 한해 일어난다.

그 위에 코드 쪽의 규율이 얹힌다. 오래된 코드부터 요즘 코드까지 되풀이되는 것은 다섯 가지다.

방법어떻게 하나무엇을 치르나
처리기를 짧게 유지한다처리기는 사실만 적고 나온다 — 「눌렸다」를 적어 두고, 실제 일은 주 흐름이 나중에 한다처리가 조금 미뤄진다
다시 들어와도 괜찮게 짠다처리기 안에서는 지역 변수만 쓰고, 밖과 나눠 쓰는 자료는 최소로 — 이것을 재진입 가능하다고 한다처리기 안에서 쓸 수 있는 함수가 줄어든다
나눠 쓰는 값은 한 걸음으로주 흐름과 처리기가 함께 보는 값은 한 번에 읽고 쓸 수 있는 작은 값으로 두고, 그렇지 못하면 잠깐 막고 만진다막는 동안 다른 일이 늦어진다
잠깐 막고, 원래대로 되돌린다만지는 구간만 인터럽트를 막되, 막기 전 상태를 기억했다가 그대로 되돌린다「무조건 켜기」로 끝내면 남의 규약을 깨뜨린다
겹칠 깊이를 미리 센다중첩이 몇 겹까지 가능한지 세어 스택을 그만큼 잡아 둔다기억을 미리 떼어 두어야 한다

표 11.10 — 인터럽트가 겹칠 때 코드가 쓰는 다섯 가지 방법

★ 첫 줄이 나머지를 거의 다 덮는다. 처리기가 짧으면 겹칠 일도 줄고, 겹쳐도 금방 풀린다. 그래서 오래된 장치 코드일수록 처리기가 놀랄 만큼 짧다 — 한 바이트를 통에 옮기고, 「할 일이 생겼다」는 표시를 하나 세우고, 그것으로 끝이다.

운영체제 위에서도 같은 얼개가 반복된다. 신호 처리기가 도는 동안 같은 신호는 대개 막혀 있고, 처리기 안에서 할 수 있는 일이 아주 좁게 정해져 있다. 왜 그런 규칙이 생겼는지는 81장에서 규칙과 함께 본다.

11.8 둘은 어떻게 다른가 — 인터럽트와 멀티태스킹#

여기까지 오면 두 가지가 한 자리에서 만나는 것이 보인다. 그래서 오히려 갈라 두어야 한다. 비슷해 보이지만 목적이 분명히 다르다.

인터럽트멀티태스킹
무엇인가바깥의 사건을 코어에게 알리는 장치시간을 나눠 쓰는 정책
목적늦지 않게 알아채는 것공평하게, 그리고 놀리지 않게 나누는 것
계기바깥에서 온다 — 키·시계·디스크정책이 정한다 — 시간이 다 됐거나 스스로 멈췄거나
끝난 뒤하던 일로 돌아간다다른 일로 넘어간다
없으면계속 물어봐야 한다(폴링)한 프로그램이 기계를 독점한다
층하드웨어가 정한 규약운영체제가 정한 규칙

표 11.11 — 인터럽트와 멀티태스킹

★ 넷째 줄이 둘을 가르는 가장 또렷한 자리다. 인터럽트는 원래 하던 일로 돌아가는 것이 기본이고, 전환은 그 위에 운영체제가 얹은 선택이다. 시계 인터럽트가 걸렸다고 해서 반드시 다른 일로 넘어가는 것이 아니다 — 시간이 남았으면 그대로 돌아간다.

거꾸로 멀티태스킹은 인터럽트 없이도 성립할 수 있다. 협력형이 그렇다 — 프로그램이 스스로 양보하는 자리에서만 전환하면 시계가 없어도 된다. 다만 그 방식이 어떤 병을 앓는지는 앞에서 보았다.

같은 순간에 둘이 각각 무엇을 정하는지 갈라 보면 이렇다. 시계가 인터럽트를 걸면 하드웨어가 지금 명령까지 끝내고 처리기로 뛰며, 처리기가 도는 동안 인터럽트를 잠시 막아 둔다. 그 사이 운영체제는 시각을 세고 이 일의 시간이 다 됐는지 본다. 그리고 처리기가 끝날 때 하드웨어는 문맥을 원래대로 되돌리는데 — 운영체제는 그 문맥을 다른 것으로 바꿔 놓을 수 있다.

셋째 줄의 마지막 칸이 「전환」이 실제로 일어나는 자리다. 운영체제는 인터럽트에서 돌아갈 때 돌아갈 자리를 바꿔치기한다. 하드웨어가 보기에는 그저 처리기에서 돌아가는 것인데, 돌아가 보니 다른 프로그램이 돌고 있다.

11.9 한때는 이것이 개인용 컴퓨터의 거의 전부였다#

지금은 인터럽트가 커널 아래로 내려가 잘 보이지 않는다. 그러나 개인용 컴퓨터의 첫 시대에는 이것이 사용자 가까이 있었다. 도스 시절 이야기다.

그 시절의 기계에는 인터럽트 번호마다 「이 번호가 오면 여기로 뛰어라」를 적어 두는 표가 있었고, 그 표는 기억의 맨 앞에 놓여 있었다(4장에서 0번지 부근이 왜 특별한지 이야기했다). 놀라운 것은 그 표에 응용 프로그램이 자기 주소를 적어 넣을 수 있었다는 점이다.

무엇어떻게지금은
운영체제를 부르는 일파일을 열려면 정해진 번호를 「던졌다」 — 함수 호출이 아니라 인터럽트였다시스템 호출로 남았다(방식은 비슷하다)
키 입력키를 누르면 인터럽트가 걸리고, 그 처리기가 통에 글자를 넣었다그대로다 — 다만 커널 안에 있다
시계일정한 간격으로 인터럽트가 걸려 시각을 세었다그대로다 — 그 위에 작업 전환이 얹혔다
상주 프로그램표의 한 칸을 가로채 자기 코드를 끼워 넣고 물러나 있었다사라졌다 — 운영체제가 막는다
여러 프로그램 돌리기시계 인터럽트로 실행을 잘게 끊어 번갈아 나눠 주었다운영체제의 기본 기능이 되었다

표 11.12 — 도스 시절, 인터럽트가 하던 일

도스에는 태스크 스위처도 있었다. 프로그램 여럿을 띄워 두고 화면을 바꿔 가며 쓰되, 뒤로 간 것은 통째로 재워 두는 방식이다 — 앞에서 본 갈래표의 첫 줄이 그것이다. 기억이 640 KiB 남짓이던 기계에서는 그것만으로도 큰 편의였다.

★ 넷째 줄과 다섯째 줄이 특히 이 시대를 말해 준다. 도스는 한 번에 한 프로그램만 돌리는 물건이었는데, 응용 프로그램이 시계 인터럽트를 가로채 실행을 잘게 나누면 여러 프로그램을 번갈아 돌릴 수 있었다. 실제로 그런 제품이 있었고, 운영체제가 아니라 응용 프로그램 하나가 그 일을 해냈다.

실제 사례. 표의 한 칸을 가로챈다는 것

상주 프로그램(TSR)은 이렇게 살았다. 실행되면 인터럽트 표에서 자기가 노리는 칸의 값을 기억해 두고, 그 자리에 자기 코드의 주소를 적은 뒤, 기억에 남은 채로 물러난다. 그 인터럽트가 걸리면 자기 코드가 먼저 돌고, 할 일이 끝나면 기억해 둔 원래 주소로 넘긴다.

이 방식이 강력했던 만큼 위태롭기도 했다. 둘 이상이 같은 칸을 가로채면 순서가 사슬처럼 얽히고, 하나라도 「원래 주소로 넘기기」를 빠뜨리면 그 뒤가 전부 죽었다. 오늘날 운영체제가 이 자리를 응용 프로그램에게 내주지 않는 이유가 여기 있다.

11.10 오늘 이 이야기가 어디로 이어지는가#

도스는 갔지만 구조는 남았다. 이 장에서 본 것이 어디로 이어지는지만 짚어 둔다.

이 장의 것어디로 이어지나어느 장에서
흐름이 가로채인다운영체제가 프로세스를 번갈아 돌린다10장에서 본 그 전환의 밑바탕
처리기 안에서 할 수 있는 일이 좁다신호 처리기의 좁은 규칙81장
한 줄이 나눌 수 없는가원자적 연산과 메모리 순서85장
값이 언제든 바뀐다volatile 이라는 낱말14장
바깥을 직접 다루는 코드운영체제 없는 기계의 프로그래밍104장

표 11.13 — 이 장의 개념이 이어지는 곳

이 장에서 가져갈 것은 세 문장이다. 바깥 장치는 정해진 자리에 값을 쓰고 읽는 방식으로 다룬다. 인터럽트는 언제든 올 수 있고, 여럿이 오면 서로 부딪친다. 그리고 그 위에서 「여러 프로그램이 동시에 도는 것처럼 보이는 일」이 성립한다.

쓸 일은 없을 것이다. 그러나 기계가 어떻게 여러 일을 번갈아 하는지, 왜 어떤 코드는 「언제든 끼어들 수 있다」는 가정 아래 짜여야 하는지 — 그 감각은 남는다.