본론(영속성)에 들어가기에 앞서 먼저입력/출력 장치의 개념을 소개하고 운영체제가 이 장치들과 상호 작용하는 방법을 알아보자.
핵심 질문 : 어떻게 I/O를 시스템에 통합할까
- 시스템에 I/O를 어떻게 통합해야 하는가? 일반적인 방법은 무엇인가?
- 어떻게 효율적으로 통합할 수 있을까?
논의를 시작하기 위해 일반적인 시스템 구조를 살펴보자.
가상의 표준 장치를 살펴보고 이 장치를 효율적으로 활용하기 위해 필요한 것은 무엇인지 알아보자.
위에서 보인 단순화된 디바이스의 인터페이스는 세 개의 레지스터로 구성되어 있다.
이 레지스터들을 읽거나 쓰는 것을 통해 운영체제는 디바이스의 동작을 제어할 수 있다.
이번에는 디바이스가 운영체제를 대신하여 특정 동작을 할 때에 운영체제와 디바이스 간에 일어날 수 있는 상호 동작의 과정을 살펴보자. 이 경우 다음과 같은 방식을 따른다.
while (STATUS == BUSY)
; // 디바이스가 바쁜 상태가 아닐 때까지 대기
데이터를 DATA 레지스터에 쓰기
명령어를 COMMAND 레지스터에 쓰기
(그러면 디바이스가 명령어를 실행한다)
while (STATUS == BUSY)
; // 요청을 처리하여 완료할 때까지 대기
이 방식은 잘 동작하지만 폴링을 사용한다는 점에서 매우 비효율적이다.
폴링의 단점
핵심 질문 : 폴링 사용 비용을 어떻게 피하는가
어떻게 하면 자주 폴링을 하지 않으면서 운영체제가 디바이스의 상태를 확인할 수 있고, 디바이스를 관리하는 CPU의 오버헤드를 줄일 수 있을까?
인터럽트
팁 : 인터럽트가 폴링보다 항상 좋은 것은 아니다
하이브리드 방식
병합(coalescing) 방식
많은 양의 데이터를 디스크로 전달하기 위해 programmed I/O(PIO)를 사용하면 CPU가 또 다시 단순 작업 처리에 소모된다.
핵심 질문 : 어떻게 PIO의 오버헤드를 줄이는가
PIO를 사용하면 CPU는 너무 많은 시간을 데이터를 디스크에서 또는 디스크로 이동하는 데 사용한다. 이 작업을 어떻게 줄일 수 있으며 CPU를 더 효율적으로 활용하는 방법은 무엇인가?-> DMA 방식을 사용하자
메모리 상의 데이터 위치
, 전송할 데이터의 크기
, 대상 디바이스
와 같은 정보를 DMA 엔진에 넘겨준다.디바이스와 운영체제가 실제로 어떻게 정보를 교환하는지 알아보자.
in
과 out
명령어를 사용하여 디바이스들과 통신근데 두 방식 모두 대단히 큰 장점은 없고, 둘다 현재도 쓰인다고 한다...
최종적으로 다룰 문제는 서로 다른 인터페이스를 갖는 장치들과 운영체제를 연결시키는 가능한 일반적인 방법을 찾는 것이다.
핵심 질문 : 어떻게 장치 중립적인 운영체제를 만드는가
어떻게 하면 운영체제를 장치 중립적으로 만들고, 장치와의 상호작용을 위한 상세 내용을 운영체제로부터 숨길 수 있을까?
디바이스 드라이버를 이용한 추상화
read/write
요청을 할 뿐이다.참고) 운영체제 코드의 70%가 디바이스 드라이버를 위한 코드이다.
- 어떤 디바이스를 시스템에 연결하든, 디바이스 드라이버가 필요하다.
- 때문에 시간이 흐름에 따라 디바이스 드라이버 코드가 커널 코드의 대부분을 차지하게 되었다.
IDE 디스크 드라이브를 살펴보자.
IDE 디스크는 시스템에 다음과 같은 4개의 레지스터로 이루어진 단순한 인터페이스를 제공한다.
Control
, Command block
, Status
, Error
in
과 out
I/O 명령어를 사용하여 특정 "I/O 주소들"(아래의 0x3F6
와 같은)을 읽거나 씀으로써 접근 가능하다.// IDE 인터페이스
Control Register:
Address 0x3F6 = 0x80 (0000 1RE0): R=reset , E=0 means "enable interrupt"
Command Block Registers:
Address 0x1F0 = Data Port
Address 0x1F1 = Error
Address 0x1F2 = Sector Count
Address 0x1F3 = LBA low byte
Address 0x1F4 = LBA mid byte
Address 0x1F5 = LBA hi byte
Address 0x1F6 = 1B1D TOP4LBA: B=LBA , D=drive
Address 0x1F7 = Command/status
Status Register (Address 0x1F7):
7 6 5 4 3 2 1 0
BUSY READY FAULT SEEK DRQ CORR IDDEX ERROR
Error Register (Address 0x1F1): (check when Status ERROR==1)
7 6 5 4 3 2 1 0
BBK UNC MC IDNF MCR ABRT T0NF AMNF
BBK = Bad Block
UNC = Uncorrectable data error
MC = Media Changed
IDNF = ID mark Not Found
MCR = Media Change Requested
ABRT = Command aborted
T0NF = Track 0 Not Found
AMNF = Address Mark Not Found
READY
상태가 될 때까지 Status 레지스터 (0x1F7
) 를 읽는다.0x1F2
-0x1F6
)에 기록한다.0x00
, 슬레이브인 경우0x10
이다.0x1F7
).대부분의 프로토콜은 xv6 IDE 드라이버에 나타나 있으며, (초기화 이후에) 네 개의 기본 함수를 통하여 동작한다.
// xv6 IDE 디스크 드라이버 (단순화한 버전)
static int ide_wait_ready() {
while (((int r = inb(0x1f7)) & IDE_BSY) || !(r & IDE_DRDY))
; // 드라이브가 바쁘지 않을 때까지 반복문 수행
}
static void ide_start_request(struct buf *b) {
ide_wait_ready();
outb(0x3f6 , 0); // 인터럽트 발생
outb(0x1f2 , 1); // 섹터는 몇 개?
outb(0x1f3 , b−>sector & 0xff); // 여기에 LBA 기록. . .
outb(0x1f4 , (b−>sector >> 8) & 0xff); // . . . 여기도
outb(0x1f5 , (b−>sector >> 16) & 0xff); // . . . 여기도!
outb(0x1f6 , 0xe0 | ((b−>dev&1)<<4) | ((b−>sector>>24)&0x0f));
if(b−>flags & B_DIRTY){
outb(0x1f7 , IDE_CMD_WRITE); // 이것이 WRITE 명령어
outsl(0x1f0 , b−>data , 512/4); // 데이터도 전송!
} else {
outb(0x1f7 , IDE_CMD_READ); // 이것이 READ (데이터 없음)
}
}
void ide_rw(struct buf *b) {
acquire(&ide_lock);
for (struct buf **pp = &ide_queue; *pp; pp=&(*pp)−>qnext)
; // 큐를 순회
*pp = b; // 요청을 맨 뒤에 추가
if (ide_queue == b) // q가 비었다면
ide_start_request(b); // 디스크에 req를 보냄
while ((b−>flags & (B_VALID|B_DIRTY)) != B_VALID)
sleep(b , &ide_lock); // 완료를 대기
release(&ide_lock);
}
void ide_intr() {
struct buf *b;
acquire(&ide_lock);
if ( ! ( b−>flags & B_DIRTY) && ide_wait_ready() >= 0)
insl(0x1f0 , b−>data , 512/4); // READ 라면: 데이터를 가져오기
b−>flags |= B_VALID;
b−>flags &= ~B_DIRTY;
wakeup(b); // 대기중은 프로세스를 깨우기
if ((ide_queue = b−>qnext) != 0) // 다음의 요청을 시작
ide_start_request(ide_queue); // (존재한다면)
release(&ide_lock);
}
동작을 위한 4개의 기본 함수
ide_rw()
ide_start_request()
를 통해). ide_start_request()
in
과 out
명령어가 장치 레지스터를 읽거나 쓰는 데 각각 사용된다.ide_wait_ready()
ide_intr()
ide_start_request()
를 이용하여 다음 요청 처리를 시작한다.DMA를 어느 기계가 먼저 도입했는가?
근데 사실 누가 먼저 도입했는가는 중요하지 않고, 이 초기 기계를 만들 때부터 I/O 지원이 필요해졌다는 것이 더 중요하다.
인터럽트와 DMA 그리고 관련된 개념들은 빠른 CPU와 느린 장치들의 특성을 활용한 결과물이다.