버스: 데이터나 명령어가 움직이는 공간. - 다양한 종류가 있음. - GPU 같은 거 연결하는 PCIe bus - USB나 키보드 등 비교적 느린 디바이스을 연결할 때 사용하는 expansion bus - SAS 컨트롤러는 디스크를 연결하기 위한 컨트롤러. 디스크를 연결하는 버스의 느낌.
컨트롤러: 포트, 버스, 디바이스들을 동작시키는 장치.
이전에 배웠듯, 디바이스 드라이버는 OS에 속해있는SW이고, 얘네가 각자의 디바이스 컨트롤러(HW)랑 연결되어 있음
호스트가 명령 내용에 따라 커맨드 레지스터(command register)의 write bit를 1로 설정함. - 그리고 실제 write할 데이터를 data out 레지스터에 복사해놓음.
호스트가 command-ready bit를 1로 설정함. - command-ready bit는 디바이스가 명령을 받았는지 체크하는 비트임
컨트롤러가 busy bit를 1로 설정하고, 전송을 실행함 - 컨트롤러가 커맨드 레지스터를 보고 자신에게 온 커맨드가 write임을 판독함 - 실제로 데이터가 기록이 된 data out 레지스터를 읽어서, bit정보를 가져와서 디바이스에 출력함.
전송이 완료되면 컨트롤러가 command-ready bit를 0으로 지움 - 그리고 입출력 성공을 알리기 위해 상태 레지스터의 busy bit, error bit도 0으로 지움
* 주의할 점은, 이 방법은 Busy Waiting 방식이라는 것.
1번 과정에서 장치로부터 입출력 완료를 대기하는 과정에서 busy waiting이 발생
따라서 입출력 장치가 느리다면, 비효율성을 초래함 → Controller가 CPU에게 통보하는Interrupts방식이 더 효과적!
◆ Interrupt
- CPU는 프로세스의 각 instruction을 실행할 때마다 매번 interrupt-request line(CPU의 핀들 중에 전기적 interrupt 시그널을 받을 수 있는 라인)을 체크함. - 만약 인터럽트 발생한 경우, 현재 상태를 저장한 후 인터럽트 벡터(인터럽트 핸들러의 주소를 가리키고있는 벡터)로 점프해서 인터럽트 핸들러 루틴 실행. (각 인터럽트에 대해 인터럽트 핸들러 갖고 있음)
대부분의 CPU는 두 개의 Interrupt request line을 갖고 있음.
이들은 아래 두 종류의 인터럽트를 받는 각각의 라인임
Nonmaskable 인터럽트: 메모리 에러같은 거 발생했을 때 나는 인터럽트 → 즉시 처리되어야 함!!!
Maskable 인터럽트: 일반적인 인터럽트. I/O 디바이스 따위에서 에서 오는 인터럽트 → 무시되거나 연기될 수 있음.
인터럽트 벡터 (Interrupt vector) : 인터럽트를 올바른 인터럽트 핸들러로 디스패치함
특정 인터럽트 핸들러의 주소를 저장하고 있음
한 인터럽트 넘버가 서로 다른 디바이스를 처리하는 경우가 있기도 함ㅇㅇ 이때 실제로 필요한 디바이스가 뭔지 확인해서 처리하는 것을 Interrupt chaining 이라고 함.
OS는 대응하는 인터럽트 핸들러를 부트 시에 설치함.
인터럽트는 사실 굉장히 많은 상황에서 쓰임
Exception (0으로 나누기 등)
Page fault (메모리 접근 에러)
System call (trap을 통한 커널 호출)
etc...
멀티 프로세서 시스템의 경우 각 CPU가 인터럽트를 concurrently하게 처리할 수도 있음. (물론 운체가 지원해주는 경우에.)
폴링은 busy waiting이라 하던 일을 중단하지 못하지만, 인터럽트는 잠시 중단하고 실행하는 것임 → 시간에 민감한 프로세스에 사용됨.
◆ Direct Memory Access (DMA)
디스크가 CPU를 거치지 않고 바로 메모리에 접근하는 방식
용량이 큰 데이터의 이동을 위하여 programmed I/O 방식을 회피하는 것
※ Programmed I/O (one byte at a time) : 상태 비트를 감시하고, 한번에 1바이트씩 controller register에 옮기는 것
지금까지는 CPU를 거쳐서 (Directy I/O나 Memory mapped 같은 방식을 사용해서) 메모리 접근했음.
그러나 DMA 방식은 프로세서와 관련 없이, 디스크에서 메모리로 데이터를 바로 올리는 것. → I/O디바이스 사용할 때 보다는 효율적 접근 가능
프로세서는 디스크에서 메모리로 데이터를 올리는 사이에 다른 작업을 할 수 있음.
데이터 전송이 완료되면 CPU가 곧바로 데이터에 접근할 수 있음.
보통 대용량 데이터를 옮길 때 사용함 CPU가 "DMA사용해서큰 데이터옮겨놔~"한 다음에다른 작업처리하러 가면 됨.
DMA 컨트롤러가 있어야 함.
◆ Kernel I/O Structure
커널은 I/O 시스템 콜을 통해 실제 디바이스의 동작들을 캡슐화 함 → 사용자가 불법적인 입/출력을 못하게 함.
디바이스 드라이버 계층이 여러 I/O 하드웨어들의 차이점을 숨기고, 이들을 간단한 표준 인터페이스들로 보이도록 포장함. → I/O subsystem이 하드웨어와 독립적으로 되어서 OS 개발자의 작업을 간단하게 해줌.
◆ Block and Character Devices
어떤 단위로 움직이느냐에 따른 I/O 디바이스의 두 가지 유형이 있다.
Block Device : 블록 단위로 움직이는 I/O 디바이스. (ex: 디스크) - (블록 내에서) 읽고(read()), 쓰고(write()), 위치를 옮기고(seek()).. 이런 커맨드들을 제공함 - 직접적으로 디바이스에 접근할 수도 있고, 파일 시스템을 통해서 접근할 수도 있음. 보통은 파일 시스템 인터페이스를 통해서 디스크에 접근함
Character Device : 캐릭터 단위로 움직이는 I/O 디바이스. (ex: 키보드. 키보드 누르면 캐릭터 하나가 움직임) - get(), put() 등 하나의 데이터를 주고받을 때 사용하는 커맨드를 제공함.
◆ Clocks and Timers
시간과 관련한 I/O 디바이스로는 Clock과 Timer가 있다.
Clock : 1, 0이 반복되는 주기적인 signal을 의미함.
CPU가 동작할 때 Clock에 맞추어 동작한다.
Clock이 1→0으로 움직이는 걸 한 사이클이라 함.
한 Clock 사이클 동안 여러 instruction을 처리할 수도 있음 ㅇㅇ
Timer : 고정된 시간마다 어떤 Task를 해야 할 때 사용됨. (타이머 인터럽트; 주기적인 인터럽트)
하드웨어로 구현된 타이머 중에 Programmable interval tiemer라는게 있음 : Clock 한 칸마다 count value(초기값은 0)를 올리는데, 이때 어떤 임계점을 정해놓고 임계점에 도달했을 때 interrupt를 발생하도록 하는 것.
◆ Nonblocking and Asynchronous I/O
I/O 요청을 한 후에 어떻게 동작할지에 관한 이야기.
다음 세 방식 중 하나로 동작함.
Blocking: I/O 요청한 다음에 I/O 데이터가 준비될 때까지 기다림
Nonblocking: I/O 요청해보고, I/O 데이터가 없으면 안 기다리고 리턴함
Asynchronous: 인터럽트 처럼, I/O요청한 다음에 다른 거 하고 있고, I/O 데이터가 준비가 되면 알림을 받는 방식.
◆ I/O Request Life Cycle
일반적인 I/O 요청 생명 주기.
기본적으로 유저 리퀘스트 시스템 콜을 통해 커널에게 I/O 요청함.
→ 커널이 시스템 콜 핸들링. 이 요청을 바로 처리할 수 있는지 아닌지 확인함.
→ (Yes) 만약 (커널이 미리 캐싱해놔서) 바로 처리할 수 있다면? 처리 결과를 시스템 콜에 대한 응답으로 바로 알려줌
→ (No)그게 아니라면, 디바이스 드라이버한테 요청을 전달하고, 얘가 디바이스 컨트롤러한테 데이터 달라고 요청 함.
→ 그러면 디바이스 컨트롤러는 실제로 I/O를 수행하고, 수행이 종료되면 다 했다고 인터럽트 보냄