[운영체제] 5. I/O(입출력) - 1

I/O 장치는 굉장히 많고 장치마다 특성이 다르다 -> 운영체제는 크게 두가지로 나눠서 관리

1. 블록 디바이스

- 빠름 -> 읽기쓰기가 블록 단위 -> 블록마다 주소 부여

- 오퍼레이션 : 읽기/쓰기, 탐색 등

2. 캐릭터 디바이스

- 입출력하는 펜/키보드/마우스 등의 장치 : 입출력 단위가 문자

-> 주소가 아닌 입출력 순서대로 처리.

 

이런 형태에 따라 드라이버 작성 규칙이 정해져 있음.

운영체제의 일부인 디바이스 드라이버가 컨트롤러를 작동시켜 입출력을 수행

- 프로세스에서 전송한 데이터를 컨트롤러를 통해 디바이스에 전송(버스를 통해 비트스트림으로)

-> 비트스트림을 블록장치라면 블록 단위로 변환. 깨지는걸 방지하기 위해 ECC 코드를 추가로 붙여서 전송

-> 오퍼레이션 : 컨트롤 레지스터에 기록된 데이터를 보고 신호로 변환 -> 구동

-> 디바이스는  CPU로 인터럽트 전송 -> 인터럽트 핸들러가 디바이스 버퍼의 데이터를 메모리로 전송

 

디바이스 컨트롤러 내에도 메모리가 존재 -> 제어 레지스터, 데이터 레지스터, DRAM 등

 

디바이스 메모리에도 메모리 주소를 할당해줘야함

a : 디바이스 내의 레지스터에 0번지 부터 메모리 주소를 할당 : 버스에 I/O 메모리 어드레스인지 시스템 메모리 어드레스인지 선택할수 있는 핀 존재

b :  시스템 메모리 영역의 일부를 디바이스에 있는 메모리에다 매핑

c : 짬뽕 -> 펜티엄

 

 Bus의 핵심이 CPU 메모리 I/O인데.

CPU에서 I/O의 데이터 읽어오려면?

CPU에서 수행하는 프로그래밍 I/O요청 -> cpu는 제어레지스터에 데이터 읽어와라 명령 -> i/o컨트롤러는 디바이스 컨트롤러의 dram버퍼에서 데이터 읽어오고 -> 메모리에 복사 -> CPU의 레지스터로 데이터 가져와서 수행

 

버스는 어드레스 버스, 컨트롤 버스가 있음.

a : CPU/메모리가 느리니 하나의 버스에 물림

b : CPU/메모리가 빨라져서 전용 인터페이스(버스) 존재 -> 이 인터페이스와 I/O의 버스와 연결하려면 브릿지 필요

 

I/O 요청 -> cpu의 프로그램이 dma컨트롤러에 주소, 읽을 양(count), 읽을건지 쓸건지 세팅 -> cpu는 다른 프로그램 실행.

DMA컨트롤러는 I/O장치에 요청 -> 디스크 컨트롤러 내의 버퍼메모리에서 메인메모리쪽으로 데이터 전송(플라이바이모드), 이 때 디스크 컨트롤러에서 전송 기능이 없으면 DMA로 가져오고 DMA에서 다시 메인메모리로 전송. -> 끝났다고 인터럽트

 

 

 

Q. CPU와 DMA가 동시에 메모리에 접근하면?

-> DMA는 I/O처리. 메모리간 데이터 복사를 함. 왜? 프로그램에서 해당 데이터가 필요하기 때문

-> DMA가 우선임 : cycle stealing(DMA가 뺏어서 했다는 의미)

 

DMA의 전송모드

1. 플라이 바이 모드 : I/O장치에서 메모리로 데이터를 직접 옮기는 것. 이 모드를 지원 안하면 dma컨트롤러로 데이터 가져와서 다시 메모리에 복사

2. 버스트 모드 : 시작주소와 카운트를 가지고 여기서부터 저기까지 복사. 일반적. 이게 아니면 뭐 바이트 단위 등으로 복사.

 

우리가 사용하는 디바이스는 인터럽트 방식으로 동작.

디바이스 컨트롤러 : 장치들의 인터럽트를 인터럽트 컨트롤러로 우선순위 따져서 전달

-> CPU는 인터럽트를 받아 핸들러 구동 -> 핸들러가 인터럽트 컨트롤러로 ACK신호 -> 디바이스로 애크 전송 -> 인터럽트 컨트롤러에서 다음 인터럽트 발생.

 

인터럽트 발생 시점에, 실행시키는 명령어를 어떻게 처리할거냐에 따라

Imprecise 인터럽트 : CPU내부에서 명령어를 한 시점에 하나만 실행하지 않음. b상태에서 인터럽트가 발생하면 실행중인 명령어들의 status를 전부 백업해야함 -> 오버헤드가 커짐

 

Precise 인터럽트 : PC가 지정되면 그 이후에는 실행되지 않게 만든다음 인터럽트 처리

속성

1. PC는 프로그램 스택에 저장.

2,3. PC가 가리키는 어드레스의 이전 상태는 전부 실행 상태, 이후의 인스트럭션은 실행되지 않은 상태

4. PC는 실행되지 않은 상태. 

-> 사실은 b처럼 실행되는데, a처럼 만들어서 실행함(?)

 

 

I/O 소프트웨어 : I/O 디바이스마다 드라이버 존재. 운영체제 내에 드라이버를 관리하는 소트프웨어.

i/o 소프트웨어 목표

1. 독립성 : 디바이스마다 맞춰서 프로그래밍이 어려워서, 소프트웨어에 맞춰서 디바이스 드라이버를 개발하게 함.(USB HDD SDD 형태가 다르지만, 사용자 입장에서는 동일한 파일시스템으로 마운팅)

2. 네이밍 : 디바이스나 파일 이름은 스트링+문자의 조합 -> 루트파일시스템 하위에 마운트 하여 path를 통해 장치나 파일 지정.

3. 에러 핸들링 : 디바이스와 i/o 모두에서 동작

 

I/O 소프트웨어 : 인터럽트 드리븐 방식으로 동작 :  운영체제는 비동기적 -> i/o디바이스 구동시키고 CPU는 다른일 수행.

그러나 우리가 작성한 프로세스는 실행이 멈춤(Block됨) : 해당 프로그램은 싱크로너스

-> read 끝나면 실행되는 것으로 보이는데, 실질적으로는 그렇지 않다.

 

버퍼링 : 여러 디바이스의 입출력 데이터의 단위도 다르기 때문에 필요, 성능상의 이유로도 필요.

1. 노 버피링 : 유저 프로세스의 버퍼로 데이터를 직접 가져옴

커널모드와 유저모드는 메모리가 분리되어있음 -> 리스케쥴 가능성, 피지컬 메모리가 모자라 아웃 가능성, 프로텍션  비트 세팅도 필요

-> 고속 네트워크 디바이스는 사용 : 커널에서의 버퍼링이 느리기 때문(복사를 많이 해야 해서)

2. 싱글 버퍼링 : I/O디바이스에서 버퍼로 읽은다음 프로세스로 복사

3. 더블 버퍼링 : 버퍼-> 프로세스로 복사하는 동안 디바이스 -> 버퍼로 복사가 안되니까 두개 사용

 

공유 : 디스크는 여러 프로세스가 공유, 테잎 장치는 그럴 수 없음. 하나의 프로세스에 독립적으로 매핑.

 

 

ver programmed I/O : 사용자 프로세스에서 동작하는 프로그램이 출력프린트에게 출력 요청 -> 사용자 버퍼의 데이터를 커널로 복사 -> 한 문자씩 출력. 복사 한 다음 유저스페이스의 프로세스는 계속 수행. 카운트만큼 루프를 돎.

: CPU가 실행시키는 커널코드에서 프린터 컨트롤러에게 한 문자씩 전달(스페이스 레지스터 확인 후 데이터 레지스터에 한 문자씩 작성) -> CPU가 놀게 됨(프린터가 세월아네월아 해서)

ver Interrupt-Driven I/O : 디바이스 구동 부분과 인터럽트 핸들러 부분으로 이원화.

디바이스 구동(출력부분) : 커널버퍼로 복사 후 인터럽트를 발생 -> 프린터레지스터가 준비되면 한 바이트 보내고 스케쥴링 -> cpu에서는 다른 프로세스 구동 -> 한 바이트 출력 끝나면 인터럽트 발생 -> 인터럽트 핸들러 실행되어서 반복

인터럽트 핸들러 : 인터럽트가 왔다는건 한 바이트 출력했다는 의미. 카운트 줄이고 i 버퍼옵셋 +1. 컨트롤러에 ack로 알려주고 인터럽트 루틴에서 빠져나옴. : 자원이 효율적으로 쓰이게 됨

ver DMA : 커널버퍼로 데이터 복사하고 DMA 컨트롤러에 주소, 카운트, 컨트롤 세팅

스케쥴러 호출 -> 다른 프로세스 실행

CPU는 다른거 실행하다가 다 끝났다는 프린터로부터 인터럽트 오면 핸들러 실행

핸들러가 ack를 컨트롤러로 전송 -> 컨트롤러가 또 인터럽트 받은상태가 되어 프로세스 wakeup -> 핸들러에서 빠져나옴 -> 끝났으면 또 다시 다른프로세스 실행

 

 

 

소프트웨어 layer를 살펴보자.

I/O 요청하는 프로그램에서 운영체제의 devide-independent software쪽으로 요청을 전달

-> 하드웨어 동작 후 인터럽트 발생하면 핸들러에서 그걸 처리. 인터럽트 핸들러는 디바이스 드라이버의 일부분임.

핸들러는 디바이스 구동시점엔 개입 X -> 완료 후 인터럽트를 컨트롤러나 CPU로 보내면 그 시점에 핸들러가 enable.

 

 

CPU는 Program Counter 지정 후에 그 이전의 인터럽트를 전부 실행 후에 해당 인터럽트를 찾아서 로드하고, 서비스 루틴으로 점프.

1. CPU에서 백업 받지 않은 인터럽트를 백업.

2. 서비스루틴 동작을 위한 것들 set up

3. 스택을 set up.

3. 인터럽트 발생했으니까 CPU도 동작모드가 커널모드로 바뀌었으므로 커널 스택으로 스위칭. -> 이제서야 인터럽트 컨트롤러에게 인터럽트 받았다는 신호를 줌 -> 인터럽트를 또 발생시킬 준비가 됨.

5. 3번과 관련있는데, 프로세스마다 각각 유저스택 커널스택을 가지면 필요 없지만, 커널스택을 공유한다면 백업해줘야함.

6. 핸들러 수행을 위한 셋업을 다 했으니 핸들러 실행

7. 끝나면 CPU는 유저모드로 돌아가서 다음에 실행시킬 프로세스 결정(스케쥴링 알고리즘으로 어디로 돌아갈지 결정)

8. 프로세스마다의 가상주소를 피지컬 주소로 바꿔야 access할 수 있으니 해당 프로세스의 MMU컨텍스트를 복원

9. 새로운 프로세스 레지스터를 복구한 다음

10. 실행 재개

-> 이게 총 인터럽트 서비스 루틴임.

 

디바이스 드라이버 : 컨트롤러를 통해 디바이스를 제어.

이 드라이버 위에 I/O 소프트웨어 = Rest of the operating system이 있음.

이 아이오 소프트웨어는 위와 같은 일을 합니다. 어떤 만들어져야할 것들 정하고, 리소스 할당, 버퍼링, 에러 리포트, i/o와 단위 맞춰주는 것 까지.

 

디스크도 인터페이스가 다 다름. sata, ide, scsi 등.. -> 디바이스 드라이브를 만들어서 연결해줘야해 타입마다.

일정한 형태의 규격 존재, 규격에 따라 드라이버를 만들어서 배포해야함 -> 운영체제는 인터페이스를 일정하게 맞춰주기 위한  io소프트웨어 제공

 

a : 버퍼링 안함 b : 사용자 레벨 버퍼링  -> 잘 안쓰고, 고속 네트워크 인터페이스는 b

c : 싱글, d : 더블 버퍼링

네트워크 카드에서 사용자레벨 버퍼링을 쓰는 이유는, 두 개 컴퓨터에서 동작하는 프로세스 간 네트워크 전송을 보면 복사가 너무 많이 일어남(유저->커널->네트워크컨트롤러->상대 네트워크 인터페이스 카드 -> 커널 -> 유저)

-> 네트워크 컨트롤러(=카드) 간 복사는 어쩔 수 없지만, 유저-커널 간 복사를 줄이려고 b형태 사용.

 

I/O 소프트웨어 동작 순서

1. 사용자 프로세스에서 해당 디바이스 쪽으로 i/o요청 

2. device-indipendent software에서 어떤 디바이스인지 확인 = 이름 찾아서 어떤 드라이버 구동시키는지 찾고, 요청한 유저 프로세스가 해당 디바이스 구동 권한이 있는지 확인 후에 유저 프로세스 블록

-> 데이터를 버퍼링하고, 디바이스를 사용할 수 있도록 할당 후 디바이스 드라이버로 감

3. 드라이버는 레지스터 세팅하고 하드웨어를 구동, 디바이스가 실제 I/O 실행하고 끝나면 인터럽트 보내서 핸들러가 후처리하고 드라이버로 감. 드라이버에서 다시 소프트웨어로, 유저 프로세스로 감.

 

헤드를 떼어놓으면 구동시키기 힘들어서 뭉쳐놨음.

메커니컬 장치가 이 암 부분이고, 이것 때문에 optimization(최적화) 하기가 어려웠음.

디스크 지오메트리를 보면,

넘버 오브 실린더는 트랙의 개수가 되겠지.

트랙 퍼 실린더는 헤드의 개수. 뭐 이런것들로 디스크의 지오메트리 정보 구성.

중요한건 시크타임, 로테이션 타임, 트랜스퍼 타임.

회전속도가 빠르고, 내부 버퍼가 클 수록 좋은 것.

 

마그네틱 디스크의 내부를 보면, 내/외부의 트랙 길이 차이가 있음. 그래서 존으로 나누어서 트랙당 섹터의 개수가 다른 형태가 됨. 왼쪽 형태로 디스크가 구현,

프로그래밍하기 어려워서 오른쪽처럼 논리적으로 트랙 당 같은 섹터 개수를 가진다고 가정하여 프로그래밍.

 

HDD는 고질적인 문제가 느리다+에러. RAID 잘 알아두세요.

레벨 0a는 4k단위 io쓰기 요청이 옴. 디스크 4개를 묶어서, 하나의 디스크처럼 보이게 함 -> 이게 raid의 역할.

RAID 디바이스가 4k짜리 write 요청 받으면 디스크가 4개니까 1k씩 쪼개면, 속도가 4배 빨리지지. 근데 안정성은..? 예를들어 디스크 하나에 fail이 10%가 나면 4배로 페일이 늘어나는거니까. 그래서 제안된게 b

 

레벨 1 b는 세트를 2개 만들어서, 데이터를 똑같이 씀. 여유있는데서 i/o요청이 가져감. 그러나 비싸겠지

레벨 2 c는 데이터를 비트 단위로 쪼개서 비트에 대한 에러 정정코드를 만들어서 각 디스크에 저장. 하나가 fail되면, 그 디스크에 저장된 데이터를 전부 복구 -> b에서 디스크 겨우 하나 줄어듦.

 

 

네트워크에서는 전송할 때 4비트 당 3비트(위치,어떻게 일어나는지 검출,수정)짜리 ECC를 붙였어. 근데 네트워크는 어떤 bit가 fail될 지 잘 모르는 반면 디스크는 딱 고장나면 컨트롤러가 동작 안하니까 위치가 정해져 있음->위치검출필요x

레벨 3 d는 수정을 위한 코드만 있으면 돼. 근데 비트 단위로 쪼개니까 효율이 떨어짐

레벨 4 e는 비트 단위로 쪼개는게 일이라서 다시 그래서 이걸 strip단위로 묶음. 가로로 한 줄 묶어서 stripe라고 하고, 스트라이프를 구성하는게 5개 디스크라면 strip의 크기는 블록사이즈거나 그의 n배거나. 아무튼 strip단위로 에러 보정 코드를 만듦 = parity, 이를 패리티 디스크에 저장하고 4개 strip에 대해서 XOR 해서 만들어진 코드가 parity.

-> d보다 성능이 훨씬 좋음. 

 

레벨4 살펴보면, fail나면 패리티 디스크를 통해 데이터를 복원함(reconstructuon) -> i/o요청이 올 때 마다 parity disk 다시 써야함. -> 얘가 병목이 남

레벨 5 f : 패리티를 디스크마다 분산 BUT 페일이 2개 동시에 나면 복구 못하지? 그래서 패리티 2개쓰면 레벨 6.

 

 

CD롬은 스파이럴 구조, 섹터의 크기는 2k. 레이저가 통과하는 구멍이 피트, 못하는게 랜드. 이게 1 0이 됨.

섹터는 ECC가 288비트나 있음 -> 에러가 많이 남.

Preamble은 특정 목적으로 존재, HDD에서는 CHS(트랙마다 섹터 개수 다르니까 디바이스 파티셔닝을 위해) 정보 저장, 헤드 조정, 펌웨어에 따라 이런저런 특정 목적으로 갖고있기도 함.

레코더블CD : 피트를 만들어서 빛이 통과되어서 Reflective gold layer에 반사되면 기록 된 것.

DVD는 용량을 키움. 한 면에 레이어가 더블 -> 두 면 다 사용까지.

Cd DVD는 이런게 있었다만 기억.

 

 

각 트랙의 0번 섹터 위치가 다름. 헤드는 고정하고 디스크가 회전하면서 read함. 다음 트랙으로 가는 동안에도 디스크는 회전하기 때문에 헤드가 이동하고 0번이 헤드 밑으로 오기까지의 시간을 고려하여 위치를 틀어놓은 것.

-> 씰린더 스큐~

 

트랙 내에 섹터 배치할 때 interleaving도 함. 중간중간 띄워놓음. 전송하는 시간인 transfer 타임 고려한 것.

-> 세월이 가면서 트랙버퍼를 트랙사이즈만큼 줘서 걍 다 읽어버림. 그래서 이젠 안씀.