버스·핸드셰이크·중재
주소·데이터·제어 신호로 역할을 나눈 버스의 이론 대역폭을 계산하고, 공통 클록에 맞추는 동기 전송과 요청·응답 신호로 준비를 확인하는 4단계 핸드셰이크를 비교합니다. 여러 장치가 동시에 요청할 때 하나를 고르는 고정 우선순위와 라운드 로빈 중재를 시뮬레이션합니다.
CPU와 DMA 컨트롤러가 같은 순간에 메모리에 접근하려 하면, 누구의 요청을 먼저 들어줄지 정해야 합니다. 여러 부품이 하나의 통로를 함께 쓰려면, 무엇을 어디로 언제 보낼지에 대한 약속과 차례를 정하는 규칙이 필요합니다. 이렇게 여러 부품이 데이터를 주고받는 통로와 그 규칙을 버스(bus)라고 합니다. 이 글에서는 I/O와 DMA의 전송이 실제로 지나가는 버스의 구성과 규칙을 다룹니다.
주소, 데이터, 제어
교재에서 흔히 쓰는 시스템 버스 모델은 신호를 세 무리로 나눕니다. 주소 버스는 어느 위치에 접근할지를, 데이터 버스는 주고받는 값을, 제어 버스는 읽기인지 쓰기인지, 전송이 끝났는지 같은 신호를 나릅니다. 주소 버스의 폭은 가리킬 수 있는 위치의 수를, 데이터 버스의 폭은 한 번에 옮기는 양을 정하므로, "32비트 버스"라는 말만으로는 둘 가운데 무엇이 32비트인지 알 수 없습니다.
데이터 버스가 32비트, 곧 4바이트이고 1초에 1억 번 전송한다면 이론상 1초에 바이트, 곧 400 MB를 옮길 수 있습니다. 이것을 이론 대역폭이라고 합니다. 실제로 쓸 수 있는 유효 대역폭은 주소를 보내고 응답을 기다리는 시간, 다른 장치와 다투는 시간 때문에 이보다 작습니다.
동기 전송과 핸드셰이크
동기 전송(synchronous transfer)은 모든 부품이 같은 클록을 보고, 정해진 클록 주기에 맞추어 신호를 보내고 받습니다. 규칙이 단순하고 빠르지만, 모든 부품이 그 클록의 속도를 따라갈 수 있어야 하고, 선이 길어지면 클록 신호가 도착하는 시간이 어긋나기 쉽습니다.
비동기 전송(asynchronous transfer)은 공통 클록 대신, 보내는 쪽과 받는 쪽이 신호를 주고받으며 서로 준비되었는지 확인하는 핸드셰이크(handshake)를 씁니다. 대표적인 4단계 핸드셰이크는 요청 신호 REQ와 응답 신호 ACK로 다음과 같이 진행됩니다.
| 단계 | 하는 일 | REQ | ACK |
|---|---|---|---|
| 1 | 보내는 쪽이 데이터를 안정시키고 REQ를 올린다 | 1 | 0 |
| 2 | 받는 쪽이 데이터를 받아 두고 ACK를 올린다 | 1 | 1 |
| 3 | 보내는 쪽이 ACK를 보고 REQ를 내린다 | 0 | 1 |
| 4 | 받는 쪽이 REQ를 보고 ACK를 내린다 | 0 | 0 |
네 단계가 끝나 두 신호가 모두 0으로 돌아와야 다음 전송을 시작합니다. 보내는 쪽은 ACK가 올라올 때까지 데이터를 바꾸지 않으므로, 받는 쪽이 느리더라도 데이터를 놓치지 않습니다. 대신 전송마다 신호가 네 번 오가야 하므로 동기 전송보다 느립니다. 신호만 한 번 보내고 받았는지 확인하지 않는 단순한 방식도 있지만, 그런 방식은 받는 쪽이 실제로 받았는지 알 수 없습니다.
누구에게 버스를 줄까
동시에 여러 부품이 버스를 쓰겠다고 요청하면, 그 가운데 하나를 골라 사용권을 주어야 합니다. 이 일을 중재(arbitration)라 하고, 버스를 쓰겠다고 요청하는 부품을 버스 마스터(bus master)라고 합니다. 고정 우선순위(fixed priority) 방식은 미리 정한 순서에서 요청한 것 가운데 가장 앞선 것을 고릅니다. 단순하지만, 우선순위가 높은 부품이 계속 요청하면 낮은 부품은 끝없이 기다리는 기아(starvation)가 생길 수 있습니다.
라운드 로빈(round-robin) 방식은 지난번에 고른 부품의 다음 차례부터 요청한 것을 찾으므로, 계속 요청하는 부품들이 돌아가며 사용권을 얻습니다. 세 부품 A, B, C가 여섯 주기 동안 요청한 목록으로 두 방식을 비교해 보겠습니다.
def fixed_priority(requests, order="ABC"):
return next((name for name in order if name in requests), None)
def round_robin(requests, last, order="ABC"):
start = order.index(last) + 1 if last else 0
for i in range(len(order)):
name = order[(start + i) % len(order)]
if name in requests:
return name
return None
cycles = ["AB", "AB", "BC", "ABC", "C", "AB"]
print([fixed_priority(r) for r in cycles])
last, picks = None, []
for requests in cycles:
last = round_robin(requests, last)
picks.append(last)
print(picks)
print(4 * 100_000_000)['A', 'A', 'B', 'A', 'C', 'A']
['A', 'B', 'C', 'A', 'C', 'A']
400000000cycles의 문자열은 그 주기에 요청한 부품들입니다. 고정 우선순위는 A가 요청한 주기마다 A를 골라, 첫 두 주기에 함께 요청한 B가 셋째 주기에야 사용권을 얻습니다. 라운드 로빈은 지난번에 고른 부품의 다음부터 찾으므로 둘째 주기에 B를, 셋째 주기에 C를 고르고, 다섯째 주기처럼 C만 요청하면 C를 고릅니다. 마지막 줄은 앞에서 계산한 32비트 버스의 이론 대역폭입니다. 라운드 로빈이 차례를 고르게 나누어 주더라도 전송 하나의 길이가 다르면 각 부품이 기다리는 시간은 여전히 다를 수 있습니다.
중재를 회로로 만드는 방법도 여러 가지입니다. 데이지 체인(daisy chain) 방식은 사용권 신호를 부품들에 차례로 흘려보내, 요청한 첫 부품이 그 신호를 가로채게 합니다. 배선은 간단하지만 연결된 순서가 곧 우선순위가 됩니다. 한 곳의 중재기가 모든 요청을 받아 결정하는 중앙 집중식 중재와, 부품들이 서로 신호를 주고받아 결정하는 분산 중재로 나누기도 합니다.
버스의 계층과 점대점 연결
모든 부품을 하나의 버스에 매달면 가장 느린 장치가 버스를 오래 붙잡아 빠른 CPU와 메모리까지 기다리게 됩니다. 그래서 컴퓨터는 CPU와 메모리를 잇는 빠른 버스와, 주변 장치를 잇는 느린 버스를 나누고, 그 사이를 브리지(bridge)로 잇는 계층 구조를 씁니다. 브리지는 서로 다른 속도와 규칙을 가진 버스 사이에서 전송을 전달합니다.
오늘날의 PCIe(PCI Express)처럼 빠른 연결은 여러 장치가 한 통로를 나누어 쓰는 공유 버스가 아니라, 두 장치만 잇는 점대점(point-to-point) 연결을 여러 개 두고 스위치로 묶는 방식입니다. 그래서 옛 공유 버스의 그림을 그대로 오늘날의 연결에 옮기면 안 됩니다. 다만 주소, 데이터, 제어를 나누어 생각하는 방식과, 준비를 확인하고 차례를 정하는 문제는 연결 방식이 바뀌어도 여전히 남아 있습니다.
정리
- 버스는 주소, 데이터, 제어 신호로 역할을 나누며, 주소의 폭과 데이터의 폭은 서로 다른 값입니다.
- 이론 대역폭은 한 번에 옮기는 바이트 수 × 1초에 전송하는 횟수이고, 유효 대역폭은 대기와 경쟁 때문에 이보다 작습니다.
- 동기 전송은 공통 클록에 맞추고, 비동기 전송은 REQ와 ACK의 4단계 핸드셰이크로 서로 준비되었는지 확인합니다.
- 중재는 여러 요청 가운데 하나를 고르며, 고정 우선순위는 기아가 생길 수 있고 라운드 로빈은 차례를 돌아가며 나눕니다.
- 빠른 버스와 느린 버스를 브리지로 나누는 계층 구조를 쓰며, PCIe 같은 연결은 공유 버스가 아니라 점대점 연결입니다.
연습 문제
1. 계속 요청하는 세 장치
A, B, C가 매 주기 모두 요청하고 한 주기에 하나만 고를 수 있습니다. 고정 우선순위 A, B, C 순서와, A부터 시작하는 라운드 로빈의 첫 여섯 선택을 쓰세요.
해설 보기
고정 우선순위는 언제나 요청한 것 가운데 가장 앞선 A를 고르므로 A, A, A, A, A, A이고, B와 C는 A가 요청을 멈추지 않는 한 끝없이 기다립니다. 라운드 로빈은 지난번 선택의 다음부터 찾으므로 A, B, C, A, B, C로 세 장치가 번갈아 사용권을 얻습니다. 본문의 함수에 "ABC"를 여섯 번 넣으면 같은 결과가 나옵니다. 고정 우선순위를 쓰더라도 높은 우선순위 장치가 쉬는 시간이 충분하다면 문제가 없지만, 그렇지 않다면 기아를 막는 장치가 필요합니다.
2. 버스의 이론 대역폭
데이터 폭이 64비트이고 200 MHz 클록의 한 주기에 두 번 전송하는 버스의 이론 대역폭은 얼마인가요? 대기와 경쟁으로 전송 기회의 25%를 쓰지 못한다면 유효 대역폭은 얼마인가요?
해설 보기
64비트는 8바이트이고, 1초에 번 전송하므로 이론 대역폭은 바이트, 곧 초당 3.2 GB입니다. 전송 기회의 25%를 쓰지 못하면 75%만 쓰므로 유효 대역폭은 초당 2.4 GB입니다. 클록의 오르는 순간과 내리는 순간에 모두 전송하는 방식은 DDR 메모리에서 쓰이며, 클록 주파수만 보고 대역폭을 계산하면 절반으로 잘못 셀 수 있습니다.
3. 핸드셰이크를 지키지 않으면
보내는 쪽이 REQ를 올린 뒤, ACK가 올라오기 전에 다음 데이터로 바꾸었습니다. 받는 쪽에 어떤 문제가 생길 수 있나요?
해설 보기
받는 쪽은 REQ가 올라온 것을 보고 데이터를 읽는데, 그 순간 데이터가 이미 다음 값으로 바뀌었거나 바뀌는 중일 수 있습니다. 그러면 첫 데이터를 놓치거나, 두 데이터가 섞인 값을 받거나, 같은 데이터를 두 번 받을 수 있습니다. 보내는 쪽은 ACK로 받는 쪽이 데이터를 받았다는 확인을 받은 뒤에만 데이터를 바꾸어야 하고, 두 신호가 모두 0으로 돌아온 뒤에 다음 전송을 시작해야 합니다. 핸드셰이크의 각 단계는 데이터가 언제까지 안정되어야 하는지를 정한 것입니다.
버스의 문제를 따질 때는 무엇을 보내는지, 언제 받았다고 볼지, 누가 먼저 쓸지의 세 가지를 따로 적어 보세요. 다음 장의 인터럽트와 예외에서는 전송 완료 같은 사건이 CPU의 실행 흐름에 끼어드는 과정을 다룹니다.