문맥 교환
문맥 교환은 프로세서가 돌리던 실행 흐름을 다른 흐름으로 갈아 끼우는 일입니다. 돌던 흐름이 어디까지 갔는지를 통째로 저장해 둡니다. 그리고 다음 흐름이 저장해 뒀던 것을 되살립니다. 그래서 프로세서 하나로 여러 흐름을 번갈아 돌릴 수 있습니다.
상세
리모컨을 쥔 사람이 보던 것을 아무 대목에서나 멈춰 세우고 다른 것을 틉니다. 멈춘 지점을 기억해 두었다가 그 지점부터 다시 트는 쪽도 리모컨을 쥔 사람입니다. 화면 속 인물은 하다 만 낱말부터 이어서 말할 뿐, 끊겨 있던 동안이 있었다는 것을 알지 못합니다.
돌던 실행 흐름이 어디까지 갔는지를 담은 상태 한 벌을 문맥이라고 부릅니다. 나중에 이어서 돌리려면 있어야 하는 것 전부입니다. 프로세서는 저장해 둔 것 말고는 아무것도 기억하지 못합니다. 다음에 실행할 차례인 명령이 어디인지, 계산 중이던 값들이 무엇이었는지, 어느 메모리를 자기 것으로 보고 있었는지가 여기 들어갑니다.
문맥 교환은 그 한 벌을 통째로 저장하고, 다른 흐름의 것을 복원하는 일입니다. 저장과 복원, 이 둘이 본체입니다.
그래서 프로세서 하나를 여러 흐름이 번갈아 씁니다. 각 흐름은 자기가 내내 돌고 있었다고 여깁니다. 중간에 멈춰 있었다는 사실을 흐름 자신은 알아채지 못합니다. 저장해 뒀던 것이 그대로 되살아나기 때문입니다.
이 일이 실제로 몇 번 일어났는지 보이는 자리는 아래 예시에서 봅니다. 무엇을 얼마나 저장하고 되살리는지는 동작에서 봅니다. 그 값이 어디로 새는지는 대가에서 봅니다.
배경
프로세서는 한 번에 한 흐름밖에 못 돌립니다. 그런데 한 기계에 여러 프로그램을 겹쳐 올리고 싶었습니다. 특히 입출력을 기다리는 동안 프로세서가 놀았습니다. 기다리는 흐름을 잠시 치우고 다른 일을 시킬 수 있어야 했습니다.
그러려면 돌던 것을 정확히 그 자리에서 되살릴 수 있어야 합니다. 1962년의 실험적 시분할 시스템 논문이 이 요구를 절차로 적어 놓았습니다. 감독 프로그램이 사용자 프로그램을 정해진 계산량 뒤에 끊을 수 있어야 한다고 못 박습니다. 끊는 계기는 셋입니다. 계산 도중 버퍼가 차는 경우입니다. 아직 준비되지 않은 입력을 프로그램이 더 요청하는 경우입니다. 정해진 시간 몫을 다 쓰는 경우입니다. 그러면 인터럽트를 통해 감독 프로그램으로 들어갑니다. 필요한 감독 작업을 마친 뒤 차례가 다음 사용자에게 넘어간다고 적습니다. 차례를 정하는 방식은 대개 단순한 라운드 로빈이었습니다.
넘겨받는 쪽이 무엇인지는 조금 뒤에 이름을 얻습니다. 1970년의 다중 프로그래밍 커널 논문은 프로그램과 내부 프로세스를 날카롭게 가릅니다. 프로그램은 계산 과정을 서술한 명령어의 모음이고, 내부 프로세스는 주어진 저장 영역에서 그 명령어들을 실제로 실행하는 것입니다. 내부 프로세스는 고유한 프로세스 이름으로 식별됩니다. 돌고 있는 실행을 프로그램과 따로 부르게 된 자리입니다.
같은 논문은 프로그램 적재와 스와핑을 커널에 속하지 않는 개념이라고 못 박습니다. A 에서 B 로 갈아타는 일은 부모 프로세스 안에서 구현할 수 있다고 적습니다. stop(A) · output(A) · input(B) · start(B) 네 마디입니다. 실행 한 벌을 저장했다 되살리는 절차가 커널 밖에서 이렇게 적힌 자리입니다.
예시
getrusage(2) 의 두 계수기
리눅스의 자원 사용량 구조체는 문맥 교환 횟수를 자발과 비자발로 갈라 셉니다.
struct rusage {
...
long ru_nvcsw; /* voluntary context switches */
long ru_nivcsw; /* involuntary context switches */
};
ru_nvcsw 는 시간 몫을 다 쓰기 전에 프로세스가 자발적으로 프로세서를 포기해 문맥 교환이
일어난 횟수입니다. ru_nivcsw 는 더 높은 우선순위 프로세스가 실행 가능해졌거나 지금 프로세스가
시간 몫을 초과해 일어난 횟수입니다. 둘 다 리눅스 2.6 부터 있습니다.
/proc/[pid]/status 의 두 줄
같은 두 값이 프로세스별 상태 파일에도 그대로 나옵니다.
voluntary_ctxt_switches: 150
nonvoluntary_ctxt_switches: 545
리눅스 2.6.23 부터 있는 줄입니다.
vmstat 의 cs 열
기계 전체를 보는 자리에서는 cs 라는 한 글자짜리 열 이름을 씁니다.
System
in: The number of interrupts per second, including the clock.
cs: The number of context switches per second.
이 관례는 리눅스만의 것이 아닙니다. FreeBSD 공식 매뉴얼도 같은 자리에 cs 를 두고 cpu context
switches 라고 적습니다. 그리고 vmstat 이 3BSD(Berkeley Software Distribution 의 3판)에서
처음 나왔다고 밝힙니다.
perf 의 소프트웨어 이벤트
성능 계측 도구는 문맥 교환을 하드웨어 이벤트가 아니라 소프트웨어 이벤트로 분류합니다. 이벤트
목록을 sw 또는 software 로 걸러 내면 문맥 교환 같은 소프트웨어 이벤트가 나온다고 매뉴얼이
적습니다.
Windows 의 CONTEXT 와 GetThreadContext
문맥이 자료형으로 노출되는 자리도 있습니다. Windows 는 특정 스레드의 문맥을 읽는 호출을 내놓습니다.
BOOL GetThreadContext(
[in] HANDLE hThread,
[in, out] LPCONTEXT lpContext
);
CONTEXT 구조체의 ContextFlags 칸 값에 따라 문맥의 일부만 골라 가져옵니다. 다만 제약이
붙습니다. 문서는 돌고 있는 스레드에서는 유효한 문맥을 얻을 수 없다고 못 박습니다. 먼저
SuspendThread 로 스레드를 멈춘 다음에 불러야 합니다.
동작
계기가 생기면 돌던 흐름의 문맥을 저장하고, 다음에 돌릴 흐름을 고르고, 그 흐름의 문맥을 복원해 넘깁니다.
sequenceDiagram
participant A as 돌던 흐름
participant K as 커널
participant B as 다음 흐름
A->>K: 계기 — 스스로 양보하거나 선점당함
Note over K: 돌던 흐름의 문맥을 저장
K->>K: 다음에 돌릴 흐름을 고른다
Note over K: 다음 흐름의 문맥을 복원
K->>B: 멈췄던 자리부터 이어 감
고르는 단계는 이 표제어가 맡는 자리가 아닙니다. 누구에게 넘길지 정하는 일은 스케줄링이 맡고, 문맥 교환은 그 결정이 끝난 뒤에 벌어집니다. 여기서는 다음 흐름이 이미 정해졌다고 보고 넘어갑니다.
저장되는 것
POSIX(Portable Operating System Interface) 계열이 규정한 사용자 문맥 자료형이 무엇을 담는지를
그대로 보여 줍니다. ucontext_t 는 적어도 네 칸을 가집니다. 이 문맥이 끝났을 때 이어서 돌
문맥을 가리키는 uc_link, 이 문맥에서 막아 둔 시그널 집합인 uc_sigmask, 이 문맥이 쓰는
스택인 uc_stack, 그리고 저장된 문맥의 기계별 표현인 uc_mcontext 입니다. 마지막 칸에 대해
문서는 부르는 쪽 스레드의 기계 레지스터들이 들어간다고 적습니다.
Windows 쪽 자료형도 같은 것을 담습니다. CONTEXT 구조체는 프로세서별 레지스터 데이터를
담는다고 문서가 적고, 실제 칸 이름으로 세그먼트 레지스터와 플래그 레지스터, 범용 레지스터,
스택 포인터, 베이스 포인터, 명령 포인터, 부동소수점 저장 영역이 나열됩니다.
저장 범위는 무엇이 바뀌느냐에 따라 갈립니다.
| 무엇이 바뀌나 | 무엇까지 갈아 끼우나 |
|---|---|
| 같은 주소 공간 안의 흐름끼리 | 프로세서 레지스터 한 벌 — 명령 위치 · 스택 위치 · 범용 레지스터 · 상태 플래그 |
| 주소 공간까지 바뀔 때 | 위의 것에 더해 페이지 테이블을 가리키는 레지스터 |
리눅스 커널 문서는 뒤쪽을 CR3(제어 레지스터 3) 조작으로 적습니다. 페이지 테이블을 바꾸는 일이 문맥 교환에서, 그리고 커널로 들어가고 나올 때 일어난다고 적습니다.
모드 전환과 가르는 자리
시스템 콜이나 인터럽트로 커널에 들어가는 일은 문맥 교환과 갈립니다. Windows 드라이버 문서는 프로세서가 실행 중인 코드의 종류에 따라 사용자 모드와 커널 모드를 오간다고 적습니다. 응용은 사용자 모드에서 돌고 핵심 운영체제 구성 요소는 커널 모드에서 돕니다. 바뀌는 것은 어느 특권으로 도느냐이지 어느 흐름이 도느냐가 아닙니다. 그래서 모드 전환만으로는 저장해 둔 다른 흐름의 문맥을 되살리는 일이 일어나지 않습니다.
자발과 강제
계기는 둘로 갈립니다. 하나는 흐름이 스스로 프로세서를 내놓는 쪽입니다. 리눅스 문서는 시간 몫을 다 쓰기 전에 프로세스가 자발적으로 프로세서를 포기하는 경우를 이렇게 세고, 보통 어떤 자원이 쓸 수 있게 되기를 기다리려는 경우라고 적습니다. 다른 하나는 빼앗기는 쪽입니다. 더 높은 우선순위의 프로세스가 실행 가능해졌거나, 지금 프로세스가 자기 시간 몫을 초과한 경우입니다.
사용자 공간에서 직접 갈아 끼우기
문맥 교환이 커널만의 일은 아닙니다. swapcontext() 는 현재 문맥을 oucp 가 가리키는 구조체에
저장한 다음, ucp 가 가리키는 문맥을 활성화합니다. 문서는 이 함수가 성공했을 때 반환하지
않는다고 적습니다. 나중에 oucp 가 활성화되면 그때는 이 호출이 0 을 돌려준 것처럼 보인다고
덧붙입니다. 표준 자리는 옮겨 다녔습니다. SUSv2(Single UNIX Specification version 2)와
POSIX.1-2001 에 있었습니다. POSIX.1-2008 에서는 이식성 문제를 이유로 빠졌습니다. 대신 응용은
POSIX 스레드를 쓰도록 다시 쓰라는 권고가 붙었습니다.
대가
직접 치르는 값은 저장하고 되살리는 데 드는 시간입니다. 그 값을 정하는 것은 하드웨어가 무엇을 요구하느냐입니다. 1970년의 커널 논문은 RC 4000 기계에서 잰 전형적인 실행 시간을 표로 남겼습니다. 프로세스를 시작하는 데 26밀리초, 멈추는 데 4밀리초, 만드는 데 3밀리초, 없애는 데 30밀리초입니다. 논문은 시작과 제거가 이렇게 큰 이유를 그 기계 특유의 저장 보호 방식으로 돌립니다. 프로세스가 쓰는 모든 저장 워드마다 보호 키를 설정해야 했기 때문입니다.
주소 공간까지 바뀌면 페이지 테이블을 가리키는 레지스터를 건드려야 합니다. 리눅스 커널의 x86 페이지 테이블 격리 문서는 그 기능을 켠 조건에서 CR3 조작이 백 사이클 남짓이고 커널에 들어가고 나올 때마다 필요하다고 적습니다.
간접으로 새는 값은 주소 변환 버퍼(TLB, Translation Lookaside Buffer)와 얽혀 있습니다. 커널 문서는 메모리 구간의 매핑을 풀거나 속성을 바꿀 때 커널에게 두 선택지가 있다고 적습니다. 하나는 명령 두 개짜리 절차로 주소 변환 버퍼를 통째로 비우는 것입니다. 짧게 끝나는 동작이지만 부수 피해를 남깁니다. 비우려던 영역이 아닌 다른 영역의 항목까지 파괴되고, 나중에 얼마간의 값을 치러 다시 채워야 합니다. 다른 하나는 한 페이지씩 무효화하는 것입니다. 명령 수는 훨씬 많이 들 수 있지만 훨씬 정밀해서 다른 항목에 부수 피해를 주지 않습니다.
같은 문서 계열은 격리 기능을 켰을 때 전역 페이지가 어디서 꺼지는지를 적습니다. 커널 페이지 테이블과 사용자 공간 페이지 테이블 양쪽에 매핑되지 않은 커널 구조체가 그 대상입니다. 전역 페이지는 서로 다른 프로세스가 커널을 가리키는 주소 변환 항목을 공유하게 해 주는 기능입니다. 이 혜택을 잃으면 문맥 교환 뒤에 주소 변환 버퍼 미스가 늘어납니다. 다만 문서는 실제 성능 손실이 아주 작고 1퍼센트를 넘는 일은 없다고 적습니다.
프로세서 쪽에서 이 값을 깎으려고 붙인 장치도 있습니다. PCID(Process-Context Identifier, 프로세스 문맥 식별자)는 페이지 테이블이 바뀔 때 CR3 의 특별한 비트를 세워, 주소 변환 버퍼를 통째로 비우는 일을 건너뛰게 해 주는 기능입니다. 그래서 문맥 교환이나 커널 진입과 이탈에서 페이지 테이블 전환이 더 싸집니다. 대신 조건이 붙습니다. PCID 를 지원하는 시스템에서는 문맥 교환 코드가 사용자 항목과 커널 항목을 둘 다 주소 변환 버퍼에서 비워야 합니다. 사용자 쪽 무효화는 사용자 공간으로 나갈 때까지 미뤄 비용을 최소화한다고 문서는 적습니다. 지원이 없는 시스템에서는 CR3 에 쓸 때마다 주소 변환 버퍼 전체가 비워집니다. 그래서 시스템 콜과 인터럽트와 예외 하나하나가 주소 변환 버퍼를 비우는 일이 됩니다.
관련 항목
문맥 교환이 속하는 상위 분류
문맥 교환과 헷갈리는 이웃
문맥을 넘겨받는 실행 주체
프로세스 · 스레드 · POSIX 스레드
문맥 교환이 거치는 앞뒤 단계
저장하고 복원하는 문맥의 조각
스택 · 세그먼트 · 페이지 테이블 · CR3 · 시그널
문맥 교환을 정의하는 표준과 자료형
POSIX · SUSv2 · ucontext_t · CONTEXT · 프로세스 제어 블록(Process Control Block, PCB)
문맥을 부르거나 다루는 함수 호출
getcontext · setcontext · makecontext · swapcontext · GetThreadContext · sched_yield
문맥 교환 횟수를 재는 지표와 도구
getrusage · vmstat · perf · 성능 · 계측
문맥 교환을 다루는 다른 운영체제 계보
문맥 교환에서 새는 비용과 터지는 장애
TLB · 무효화 · 전역 페이지 · 스래싱 · 문맥 교환 폭주
비용을 정하는 아키텍처 장치
x86 · PCID · ASID(Address Space Identifier, 주소 공간 식별자) · 격리 · RC 4000
문맥 교환 횟수를 줄이는 대안
코루틴 · 이벤트 루프 · CPU 고정
다른 이름: context switch · context switching · 컨텍스트 스위치 · 컨텍스트 스위칭