OS는 virtualize storage interface를 user process에게 제공해야 하는데 그 interface가 file이다.
I/O devices에서는 다양한 device를 어떻게 handle하는지 아는 것이 main이다.
다양한 device에게 standard interface, standard protocol을 제공해야하고 이것을 다른 말로 canonical interface, canonical protocol이라고 한다.
CPU나 memory에 저장된 data는 power failure가 발생하면 data를 maintain할 수 없기 때문에 persistence를 유지하기 위해서는 permanent storage에 data를 저장해야 한다.
I/O가 없으면 컴퓨터는 쓸모가 없다. I/O가 없으면 OS가 print, show, store등을 할 수 없기 때문이다.
뇌는 있는데 손, 발, 입 이런게 아무것도 없는거지..
하지만 수 많은 device는 다 조금씩 다르다.
이 device들에게 어떻게 standardize된 interface를 제공할 수 있을까?
OS의 역할은 hardware resource를 virtualize해서 process에게 abstraction을 제공하는 것이다.
Data granularity: Bytes vs. Block
• CPU와 device가 data를 transfer하거나 communication할 때 사용할 data의 unit을 말한다.
• 몇몇 device는 single byte를 보내고 다른 것들은 whole block을 보낸다.
• 보통 storage device가 block 단위를 사용함.
Access pattern: Sequential vs. Random
• Device에 access할 때 어떤 방법을 사용하는지를 의미한다.
• Sequential은 tape같은 방식이다. tape의 맨 끝을 보고 싶다면 처음부터 시작해야 한다.
• Random은 원하는 곳에 그냥 access할 수 있는 것이다. hdd나 ssd같은건 random access를 허용한다.
Transfer mechanism: Programmed I/O and DMA
• Programmed I/O는 int, out과 같은 special instruction을 사용한다.
• DMA는 direct access memory mechanism을 사용한다.
I/O subsystem의 목적은 다양한 devices에게 uniform interface를 제공하는 것이다.
Provide uniform interfaces, despite wide range of different devices!
아래의 코드는 많은 서로 다른 devices에서 동작한다.
FILE fd = fopen(“/dev/something”,”rw”);
for (int i = 0; i < 10; i++) {
fprintf(fd,”Count %d\n”,i);
}
close(fd);
왜냐하면 devices(device driver)를 control하는 code는 standard interface를 implement하기 때문이다!
Block Devices: disk drive, tape drive, DVD-ROM 같은 것들
• data의 block을 access한다.
• open()
, read()
, write()
, seek()
등의 command가 있다.
• Raw I/O or file-system access
• Memory-mapped file access 가능
Character Devices: 키보드, 마우스, serial ports, USB devices같은 것들
• byte단위이다. Single characters at a time
• get()
, put()
과 같은 command가 있다.
• Libraries layered on top allow line editing
Blocking Interface: "Wait"
• Read나 write같은 system call이 끝날 때까지 OS가 기다린다.
• Data를 request할 때는(ex. read()
) data가 ready되기 전까지 process가 sleep하도록 한다.
• Data를 write할 때는(ex. write()
) device가 그 data에 대해 ready될 때까지 process가 sleep하도록 한다.
Non-Blocking Interface: "Don't Wait"
• User process가 system call이나 function을 호출해도 OS가 기다리지 않는다. 그래서 실행이 끝났는지 아닌지 확인할 수 없다.
• Device가 엄청 느리거나 해야할 일이 엄청 많거나 할 때는 periodically하게 device가 실행중인지 체크할 수 있다.
• Read나 write request가 몇 개의 bytes가 성공적으로 transfer되면 빠르게 return된다.
• Read는 아무것도 return하지 않을 수 있고 write는 아무것도 write하지 않았을 수 있다.
Asynchronous Interface: "Tell Me Later"
• Non-blocking과 비슷하지만 asynchronous는 check를 하지 않고 device의 실행이 끝날 때까지 기다리지도 않지만 device한테 실행이 끝나면 알려달라고 하는 것이다. 그럼 나중에 확인을 한다.
• Data를 request할 때 user의 buffer의 pointer를 가져와서 즉시 return한다. 나중에 kernel이 buffer를 채우고 user에게 알려준다.
• Data를 send할 때 user의 buffer의 pointer를 가져와서 즉시 return한다. 나중에 kernel이 data를 가져오고 user에게 알려준다.
Buses
CPU, RAM, I/O device 사이에 information이 움직일 수 있게 해주는 data path이다.
I/O bus
CPU와 I/O device 사이에 연결된 data path이다.
I/O 버스는 3개의 하드웨어 구성 요소에 의해 I/O 장치에 연결된다: I/O ports, interfaces, device controller이다.
Canonical devices는 두 개의 중요한 component가 있다.
Canonical interface는 canonical device, canonical protocol로 이루어져 있다.
Canonical device는 hardware interface, internal로 이루어져 있다.
Device vendor가 canonical interface를 따르기만 한다면 OS는 언제나 각 device에 맞는 새로운 code 없이도 그 device와 communicate할 수 있다.
어떤 type의 device이든 computer의 main board에 연결되고 싶다면 이 세 종류의 register가 define되어야 한다.
Status register: device의 status를 나타낸다.
See the current status of the device
Command register: OS로부터 command를 가져오는 레지스터이다.
Tell the device to perform a certain task
Data register: memory로부터 device memory로 data를 가져오는 레지스터이다.
Pass data to the device, or get data from the device
위의 세 register를 read하고 write하는 것을 통해 OS는 device의 behavior를 control할 수 있다.
❓OS가 어떻게 device와 communicate할 수 있을까?
OS는 canonical protocol을 통해 device와 communicate한다.
Canonical protocol은 OS와 device가 communicate하기 위해 만들어진 standarize된 protocol이다.
I/O instruction은 OS가 특정 device register에게 data를 send하는 방법이다.
x86에는 in
, out
instruction이 있다.
Memory-mapped I/O는 device memory가 physical DRAM에 mapping 되어있는 것이다. 그래서 마치 memory location에 존재하는 것처럼 사용할 수 있다.
Physical memory의 그 영역을 read/write하면 자동으로 device에 redirection된다.
Memory-mapped I/O를 위해서는 I/O, MMU, bus architecture의 도움이 필요하다.
OS는 load
나 store
를 통해 device에 접근할 수 있다.
❓OS는 어떻게 different specific interface와 소통할 수 있을까?
SCSI disk에도 동작하고 IDE disk에도 동작하고 USB keychain driver에도 동작하는 file system을 만들고 싶다..
이것의 해결책은 abstraction이다.
Abstraction은 device interaction의 모든 세부사항을 encapsulation한다.
Code는 어떻게 device와 소통할까?
Register를 virtual address space에 map하는 것이다.
이것을 memory-mapped I/O라고 한다.
Memory-mapped I/O는 memory의 특정 영역을 read/write하면 자동으로 I/O device에게 redirect된다.
Data는 memory bus를 통해 움직이고 page table entry의 몇 개의 bit으로 protect한다.
OS+MMU+장치가 mapping을 구성한다.
inb()
는 in byte이다.
특정 port number를 받아서 그 port number를 가진 device로부터 1 byte를 읽어온다.
Device status register를 읽는 것이다.
Memory mapped I/O에서는 memory의 특정 영역을 device에 연결하는 데에 성공하면 statue register value를 device로부터 읽어올 필요가 없고 physical memory를 그냥 읽으면 된다.
Programmed I/O
• Special instruction이 필요하다. in
, out
같은 것들..
• Device에 대한 전용 hardware interface가 필요할 수 있다.
• Instruction에 대한 접근은 kernel mode를 통한 protection이 강제된다.
• Virtualization이 어렵다.
Memory-Mapped I/O
• Standard load/store instruction을 재사용한다.
• Standard memory hardware interface를 재사용한다.
• 일반적인 memory protection scheme에 따라 protection한다.
• 일반적인 memory virtualization scheme으로 virtualization할 수 있다.
Programmed I/O
• 각 byte가 processor의 in/out이나 load/store로 transfer된다.
• 장점 : hardware가 simple하고 programming하기 쉽다.
• 단점 : data size에 비례해서 processor cycle을 소모한다.
Direct Memory Access
• Memory bus에게 controller access를 준다.
• Device가 memory와 direct하게 data block을 주고 받는다.
DMA는 Memory-mapped I/O와는 다르다. Memory-mapped I/O의 일종이라고 볼 수도 있겠지만 살짝 다르다!
DMA는 device가 DRAM을 directly하게 access할 수 있도록 허용한다.
Programmed I/O는 a few byte를 보낼 때는 매우 효율적이지만 data를 transfer하기 위해서 CPU cycle을 소모해야 한다.
하지만 DMA는 device가 자신이 access해야 할 memory region(physical memory의 starting address)을 알려주면 device가 직접 memory에 접근해서 자신의 hardware circuit을 이용해 data를 보낸다.
그동안 CPU는 다른 job을 한다.
Device driver에서 starting address, length를 정하고 command를 send하면 device가 memory에 direct access해서 CPU cycle의 소모 없이 read/write를 수행한다.
Programmed I/O를 사용했을 때 CPU가 large chunk of data를 memory로부터 device로 copy하기 위해 많은 시간을 낭비하였다.
반면 DMA를 사용하면 memory의 어디에 data가 있고 얼마나 copy해야 하는지만 알면 CPU cycle 소모 없이 data를 copy 할 수 있다.
Copy가 끝나면 DMA가 interrupt를 발생시켜서 disk에 I/O가 시작된다.
OS는 다음과 같은 상황을 알아야 한다:
이 상황을 알아내는 방법에는 2가지가 있다.
Interrupt
• Device가 done하면 CPU에 직접 signal을 보내서 CPU가 I/O completion을 위한 interrupt handler를 call한다.
• Device가 service가 필요할 때 generation한다. Asynchronous하고 CPU는 다른 일을 할 수 있다.
• 장점 : unpredictable한 event를 잘 다룰 수 있다.
• 단점 : interrupt는 상대적으로 overhead가 크다. mode switch(최악의 경우 context switch)를 해야하기 때문이다.
Polling
• Device가 끝날 때까지 OS가 device를 계속 확인해야 한다.
• OS가 periodically하게 device-specific statue register를 확인한다. I/O device는 실행을 마치면 completion information을 status register에 올린다.
• 장점 : 오버헤드가 낮다. Device가 done하면 바로 알 수 있다. Low latency.
• 단점 : infrequent하거나 unpredictable I/O operation인 경우 polling에 많은 cycle을 낭비할 수 있다.
OS는 반복적으로 status register를 읽어서 device가 ready될 때까지 기다린다.
좋은 점은 간단하게 작동한다는 점이다.
하지만 device를 wait하면서 CPU time을 낭비한다.
CPU를 utilize하는 데에는 다른 ready process로 switch하는 것이 낫다.
I/O request를 한 process를 sleep 상태로 놓고 다른 process로 switch한다.
이를 통해 disk와 device가 자신의 job을 수행할 때 다른 job을 수행할 수 있다.
만약 device의 실행이 끝나면 I/O를 기다리고 있던 process를 interrupt로 깨운다.
CPU와 disk가 적절하게 utilize된다는 장점이 있다.
하지만 interupt가 항상 best solution인 것은 아니다.
만약 device가 굉장히 빨리 수행된다면 interrupt는 system을 느리게 만들 수 있다.
Context switch를 하는 비용이 expensive하기 때문이다 (아주 짧은 I/O waiting을 위해 매번 context switching을 수행해야 함).
If a device is fast -> polling is best.
It a device is slow -> interrupts is better.
Polling과 interrupt의 차이점을 명확히 숙지하도록 하자.
OS가 실제로 device와 communicate하는 방법은 abstraction이다.
ioctl()
system call을 통해 지원된다.Device driver는 전형적으로 두 부분으로 나뉘어져 있다.
Top half: system call의 call path에서 access된다.
OS에 의해 사용되는 부분이다. OS가 process로부터 request를 받아서 data나 command를 device에게 보낼 수 있도록 software를 호출한다.
• open()
, close()
, read()
, write()
, ioctl()
등과 같은 standard, cross-device calls의 set을 implement한다.
• Device driver에 대한 kernel의 interface이다.
• Top half는 device에 대한 I/O를 시작하고 완료될 때까지 thread를 sleep 상태로 전환할 수 있다.
Bottom half: interrupt routine으로 사용된다.
Device의 실행이 끝나면 interrupt가 발생하고 interrupt handler를 call해야한다.
Device vendor가 interrupt handler를 제공해야하고, 이 interrupt handler가 bottom half이다.
• Input을 가져오거나 output의 다음 block을 transfer한다.
• I/O가 완료되면 sleep 상태였던 thread를 깨울 수 있다.
User program은 I/O request를 받으면 system call을 호출한다.
Kernel I/O subsystem은 어떤 device에 access해야 하는지 확인하고 그 device의 top half device driver를 호출한다.
이후 device가 끝나고 I/O가 완료되면 interrupt를 generate한다.
Interrupt signal이 CPU에 도달하면 mode switch를 해서 interrupt handler를 호출하는데 그 interrupt handler가 bottom half of device driver이다.