트랩
고친 사람 github-actions[bot]
트랩은 도는 프로그램을 그 명령에서 멈추고 미리 등록해 둔 처리기로 제어를 넘깁니다. 처리기는 운영체제가 원인마다 걸어 둔, 그 일을 대신 맡는 코드입니다. 프로그램이 스스로 일으키기도 합니다. 명령을 끝까지 실행할 수 없는 프로세서가 일으키기도 합니다.
쉽고 빠른 이해
트랩은 프로그램이 혼자 못 하는 일을 운영체제에 넘기는 통로입니다. 파일을 읽는 함수를 부르면 그 안에서 트랩이 걸려 운영체제로 넘어갑니다.
이게 없으면 프로그램이 디스크와 메모리를 직접 만져야 합니다. 그러면 한 프로그램의 실수가 기계를 통째로 멈춥니다. 권한을 갈라 놓아도 넘나들 길은 있어야 합니다. 그 길이 트랩입니다.
이렇게 돕니다:
- 하던 명령을 멈추고, 돌아올 주소와 원인 번호를 적어 둡니다
- 원인 번호로 표를 찾아 정해진 처리기로 건너뜁니다
- 처리기가 일을 마치면 적어 둔 주소로 돌아옵니다
대가는 시간입니다. 오갈 때마다 레지스터 값을 저장하고 되돌려 놓아야 합니다. 잦은 트랩은 그 시간이 쌓입니다.
상세
선수가 교체를 청할 때도, 넘어진 선수가 일어나지 못할 때도, 관중석에서 무언가 날아들 때도, 심판의 휘슬 한 번에 경기는 그 자리에서 멈춥니다. 다음에 무엇을 할지는 선수가 아니라 규정집이 미리 정해 둡니다. 심판이 그 절차를 마치면 경기는 멈춘 자리에서 다시 이어집니다.
트랩은 그 멈춤에 해당합니다. 도는 프로그램이 명령 하나를 처리하다 멈추고, 제어가 운영체제의 핵심부인 커널로 넘어가는 일입니다. 파일을 읽거나 소켓에 쓰는 순간이 그런 때입니다. 커널이 그 요청을 대신 처리하고 프로그램을 다시 이어 줍니다.
이런 통로가 따로 있어야 하는 까닭은 권한 때문입니다. 프로그램에 디스크와 메모리를 직접 만질 권한을 주면 한 프로그램의 실수가 기계 전체를 무너뜨립니다. 그래서 권한을 갈라 놓습니다.
그러면 프로그램 혼자서는 할 수 없는 일이 생깁니다. 트랩은 그 선을 지키면서도 건너갈 수 있게 열어 둔 문입니다.
도는 순서
프로세서, 곧 CPU(Central Processing Unit, 중앙 처리 장치)는 명령을 한 줄씩 꺼내 실행합니다. 트랩이 걸리면 이 흐름이 한 명령 앞에서 끊깁니다. 끊긴 흐름을 나중에 이어 붙이려면 두 가지를 먼저 적어 둬야 합니다. 어디로 돌아올지와 왜 끊겼는지입니다.
sequenceDiagram
participant 프로그램
participant CPU
participant 커널
프로그램->>CPU: 트랩을 부르는 명령
CPU->>CPU: 돌아올 주소와 원인 번호를 적는다
CPU->>커널: 원인 번호가 가리키는 처리기로 넘긴다
커널->>커널: 요청을 대신 처리한다
커널-->>프로그램: 적어 둔 주소로 돌려보낸다
그림에서 볼 것은 제어가 정해진 길로만 옮겨 간다는 점입니다. 여기 그린 것은 프로그램이 스스로 트랩을 부르는 경우입니다. 나머지 두 원인은 다음 소절에서 봅니다.
프로그램은 어느 처리기로 갈지 못 고릅니다. 갈 곳은 커널이 미리 등록해 둔 표가 정합니다. 그래서 프로그램이 트랩을 일으켜도 커널 코드의 아무 데나 뛰어들 수는 없습니다.
넘어갈 때 실행 권한도 함께 바뀝니다. 프로그램은 장치와 메모리를 직접 만질 수 없는 낮은 권한으로 돕니다. 트랩이 걸리면 그 권한이 높은 쪽으로 올라가고, 돌아올 때 다시 내려옵니다. 사용자 공간과 커널을 가르는 선이 이 대목에서 지켜집니다.
트랩을 일으키는 세 원인
원인은 셋입니다. 셋 다 같은 통로를 쓰지만, 무엇이 흐름을 끊었는지가 다릅니다. 끊은 것이 다르면 돌아올 때 어디부터 이어야 하는지도 달라집니다.
| 원인 | 언제 | 돌아온 뒤 |
|---|---|---|
| 일부러 부른다 | 커널의 기능을 쓸 때 | 다음 명령부터 이어 간다 |
| 명령을 끝낼 수 없다 | 0으로 나누기 · 없는 메모리 읽기 | 고쳤으면 그 명령을 다시 돈다 |
| 바깥에서 신호가 온다 | 키보드 입력 · 타이머 | 끊겼던 명령부터 이어 간다 |
첫째는 프로그램이 스스로 부르는 것입니다. 파일을 읽거나 소켓에 쓰는 일은 프로그램 혼자 못 합니다. 전용 명령으로 트랩을 일으켜 커널에 부탁하고, 답을 받아 이어 갑니다. 이렇게 커널의 기능을 부르는 통로가 시스템 콜입니다.
둘째는 예외입니다. 실행 중인 명령 자체에서 생긴 이상입니다. 프로그램이 쓰려는 메모리가 아직 안 올라와 있어 생기는 페이지 폴트가 대표입니다. 커널이 그 내용을 채워 넣으면 프로그램은 아무 일도 없었던 듯 이어 갑니다.
못 고치는 예외는 프로그램에 전해집니다. 커널이 정해진 번호의 알림인 시그널을 보냅니다. 받아 둔 처리가 없으면 프로그램은 거기서 끝납니다.
flowchart TD
E["예외가 생긴다"] --> Q1{"고칠 수 있나"}
Q1 -->|예| K["커널이 채워 넣고 그 명령을 다시 실행한다"]
Q1 -->|아니오| S["커널이 시그널을 보낸다"]
S --> Q2{"받아 둔 처리가 있나"}
Q2 -->|있다| H["처리기를 실행한다"]
Q2 -->|없다| X["프로그램이 끝난다"]
같은 예외라도 어느 갈래로 빠지느냐에 따라 결말이 셋으로 갈립니다. 페이지 폴트는 고칠 수 있는 쪽이라 아무 일도 없었던 듯 이어집니다. 잘못된 주소를 건드렸을 때 나는 세그멘테이션 폴트는 못 고치는 쪽이고, 받아 둔 처리가 없으면 프로그램이 거기서 끝납니다.
셋째는 인터럽트입니다. 지금 무슨 명령을 돌고 있었는지와 상관없이 바깥 장치가 보냅니다. 앞의 둘과 달리 프로그램 잘못이 아닙니다. 처리가 끝나면 끊겼던 흐름이 그대로 이어집니다.
넓은 뜻과 좁은 뜻
여기까지가 넓은 뜻입니다. 넓게 쓰면 위 셋을 다 담습니다. 원인이 무엇이든 흐름이 끊기고 정해진 처리기로 넘어가는 일이면 전부 트랩입니다. 명령어 집합을 설명하는 문서와 운영체제 문서가 대개 이 뜻으로 씁니다.
좁게 쓰면 예외의 한 갈래만 가리킵니다. 예외를 돌아오는 방식으로 셋으로 가르는 분류가 있습니다. 그중 하나의 이름이 트랩입니다.
| 갈래 | 무엇이 일으키나 | 돌아오는 곳 |
|---|---|---|
| 트랩 | 프로그램이 일부러 | 다음 명령 |
| 폴트 | 고칠 수 있는 이상 | 끊긴 그 명령 |
| 어보트(abort) | 고칠 수 없는 이상 | 안 돌아온다 |
어보트는 하드웨어가 고장 났을 때처럼 무엇이 어긋났는지조차 짚을 수 없는 경우입니다. 끊긴 프로그램을 이어 갈 방법이 없어 그대로 끝냅니다.
flowchart TD
subgraph wide["트랩 · 넓은 뜻"]
SC["시스템 콜"]
IR["인터럽트"]
subgraph exc["예외"]
TR["트랩 · 좁은 뜻"]
FA["폴트"]
AB["어보트"]
end
end
같은 낱말이 큰 묶음의 이름입니다. 그 안의 한 갈래 이름이기도 합니다.
어느 뜻인지는 글이 무엇을 다루는지를 보고 가릅니다. 예외를 갈래별로 나누는 표가 함께 나오면 좁은 뜻입니다. 그런 표가 없이 흐름이 커널로 넘어가는 이야기만 하면 넓은 뜻입니다.
오갈 때 치르는 비용
트랩은 함수를 부르는 것보다 비용이 더 듭니다. 오가는 길에 해야 하는 뒷정리가 있기 때문입니다. 끊길 때 레지스터에 들어 있던 값을 밀어 둡니다. 돌아올 때 그 값을 되돌려 놓아야 합니다.
다른 코드로 건너뛰는 데에도 비용이 듭니다. 프로세서는 곧 실행할 명령을 미리 준비해 두고 돕니다. 흐름이 다른 코드로 건너뛰면 그 준비가 버려집니다. 한 번은 짧지만 횟수가 늘면 그만큼 쌓입니다.
그래서 트랩을 줄이는 쪽으로 만든 장치가 여럿 있습니다. 여러 요청을 한 번에 모아 보내는 배치 처리가 그렇습니다. 커널에 안 들어가고도 답할 수 있는 몇몇 호출을 사용자 공간에서 처리하는 vDSO도 그렇습니다.
기계 전체를 흉내 내는 가상화에서는 트랩이 더 잦습니다. 가상 기계 안에서 도는 운영체제가 장치를 만지는 명령을 실행하면 그때마다 트랩이 걸립니다. 가상 기계를 관리하는 하이퍼바이저가 그 명령을 대신 처리합니다. 그래서 이 비용을 줄이는 것이 성능의 큰 몫을 차지합니다.
서버 코드에서 트랩을 만나는 순간
서버 코드에는 트랩을 부르는 명령이 직접 안 보입니다. 표준 라이브러리가 감싸 두었기 때문입니다. read()로 파일을 읽고 write()로 소켓에 쓰는 함수들이 그 안에서 트랩을 일으킵니다.
드러나는 것은 횟수입니다. 요청 하나가 작은 쓰기를 수백 번 하면 트랩도 수백 번 걸립니다. 같은 양을 모아서 한 번에 쓰면 트랩은 한 번입니다.
flowchart TD
subgraph split["나눠 보낼 때"]
A1["같은 양을 네 번 나눠 쓴다"] --> A2["트랩 네 번"]
end
subgraph batch["모아 보낼 때"]
B1["같은 양을 버퍼에 모은다"] --> B2["한 번에 쓴다"]
B2 --> B3["트랩 한 번"]
end
두 갈래가 옮기는 데이터 양은 같습니다. 갈리는 것은 트랩 횟수뿐입니다. 버퍼를 두는 것과 요청을 묶어 보내는 것이 여기서 비용을 아낍니다.
프로그램을 밖에서 들여다보는 도구도 이 통로를 씁니다. 어떤 시스템 콜을 몇 번 불렀는지 찍어 주는 strace가 그렇습니다. 디버거가 지정한 줄에서 실행을 멈추는 것도 마찬가지입니다. 그 줄에 트랩을 일으키는 명령을 몰래 심어 두는 방식입니다.
이름만 같은 다른 트랩
낱말이 같아도 뜻이 다른 쓰임이 둘 더 있습니다. 둘 다 프로세서와 상관이 없습니다.
하나는 망 관리입니다. SNMP(Simple Network Management Protocol, 간이 망 관리 프로토콜)에서 장비가 관리 서버에 먼저 보내는 알림을 트랩이라고 부릅니다. 서버가 묻기를 기다리지 않고 사건이 생긴 쪽에서 밀어 보낸다는 점이 프로세서 트랩과 겹칩니다.
다른 하나는 셸입니다. 셸 스크립트에서 trap 은 특정 시그널을 받았을 때 실행할 명령을 미리 걸어 두는 내장 명령입니다. 스크립트가 중간에 끊겨도 임시 파일을 지우고 나가게 만들 때 씁니다.
셋을 관통하는 뼈대는 같습니다. 사건이 생기면 미리 등록해 둔 곳으로 흐름이 넘어갑니다. 다른 것은 무엇이 그 흐름을 넘기느냐입니다. 프로세서가 넘기느냐, 장비가 넘기느냐, 셸이 넘기느냐의 차이입니다.
관련 항목
트랩과 같은 통로로 들어오는 사건
예외 · 인터럽트 · 시스템 콜 · 페이지 폴트 · 세그멘테이션 폴트 · 시그널 · 하드웨어 예외 · 소프트웨어 인터럽트 · 런타임 오류
트랩이 제어를 넘겨주는 상대와 그 경계
커널 · 운영체제 · 사용자 공간 · 커널 모드 · 특권 모드 · 커널 모드와 유저 모드
트랩이 건너뛸 곳을 정하는 장치
트랩 벡터 · 벡터 테이블 · 인터럽트 디스크립터 테이블 · 예외 처리기 · 프로그램 카운터 · 레지스터
트랩을 일으키고 받아 내는 하드웨어
CPU · 명령어 집합 · x86 · RISC-V · 특권 명령 · 실행
트랩이 오갈 때 치르는 비용과 그 비용을 줄이는 방법
문맥 교환 · 모드 전환 · vDSO · 버퍼링 · 배치 처리
트랩을 가로채 기계를 흉내 내는 기술
가상화 · 하이퍼바이저 · 트랩과 에뮬레이션 · 가상 머신 · 반가상화
트랩을 도구로 쓰는 프로그램
디버거 · 중단점 · ptrace · strace
이름만 같고 뜻이 다른 트랩
다른 이름: trap · 트랩 명령