[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

💻 CS/운영체제

[운영체제] 입출력시스템

inu 2020. 3. 26. 21:50
반응형


입출력 시스템의 일반적 구성

  • 입출력 장치는 '시스템 버스'와 연결되어야 하는데, 물리적인 입출력장치가 바로 시스템버스에 접속되어 있는 것은 아니고 '제어기'를 통해서 연결이 된다. 즉, 장치 제어기가 시스템 버스와 연결되어 있다.
  • 응용 프로그램이 시스템 콜을 통해 커널의 파일 시스템 및 디바이스 드라이버에 연결되고, 커널의 다바이스 드라이버가 장치 제어기를 통해 입출력 장치 자체와 연결된다.
  • 즉, 응용 프로그램이 시스템 콜을 호출하면 이 호출이 커널의 디바이스 드라이버에게 전달되고, 디바이스 드라이버는 하드웨어인 장치 제어기와 명령 및 정보를 주고받으며 작업을 수행하고, 이러한 작업을 통해 입출력 장치가 통제되게 되는 것이다.

디바이스 드라이버와 장치제어기의 상호작용 (하드웨어 인터페이스)

  • 디바이스 드라이버의 장치 제어기와 소통은 장치 제어기 내부에 있는 특정 레지스터들의 값을 디바이스 드라이버가 읽거나 쓰는 방식으로 수행된다. 이러한 레지스터에는 명령 레지스터, 작업 레지스터, 자료 레지스터가 있다.
  • 명령 레지스터는 장치에게 쓰거나 읽는 동작을 명령하기 위한 명령 코드를 적재하는 곳이다.
  • 상태 레지스터는 해당장치에 오류가 있는지, 작업이 진행중인지, 작업을 끝내고 쉬고 있는지 등 장치의 상태를 체크하는 용도로 사용된다. busy/done 등의 플래그나 오류코드로 표현된다.
  • 자료 레지스터는 명령 레지스터를 통해 명령한 동작을 실행하는데 필요한 데이터를 읽고 쓰기 위한 버퍼같은 용도로 사용된다.

커널과 디바이스 드라이버의 상호작용

  • 디바이스 드라이버가 마치 커널과 하나인 것처럼 이야기했지만 사실 조금 다르다.
  • 디바이스 드라이버라는 것은 커널이 배포된 이후에도 업체들이 창의적으로 새로운 장치를 만들고 이를 제어할 수 있도록 구성한 것이다. 즉, 커널 개발자는 커널의 소스코드를 변경하지 않고도 새로운 장치의 다바이스 드라이버를 플러그를 꽂고 뽑듯이 커널에 인스톨할 수 있도록 해야 한다.
  • 커널과 디바이스 드라이버 사이의 인터페이스에는 특정 드라이버에 필요한 함수들을 저장하는 테이블같은 자료구조가 존재한다. 응용프로그램에서 해당 장치를 사용하고자 특정 시스템 콜을 하면 해당 신호가 트랩을 통해 커널 안으로 진입하고, 앞서 언급한 테이블에서 해당 드라이버내의 함수를 수행하도록 한다. 이러한 과정을 통해 커널과 디바이스 트라이버간의 인터페이스가 동작하는 것이다.
  • 참고로 디바이스 드라이버는 커널 모드에서 커널과 함께 동작하기 때문에 커널 함수를 직접 호출하거나 커널 변수에 직접 접근할 수도 있다. 따라서 이를 활용한다면 커널의 기능을 효과적으로 활용하는 디바이스 드라이버를 만들 수도 있을 것이다.

레지스터 접근 방식

  • 입출력장치의 레지스터를 접근하는 방식은 2가지가 있다.
  • 격리형 입출력 (Isolated I/O, I/O mapped I/O) : 메모리와는 별도의 공간을 가지는 방식이다. 메모리 주소 지정을 위해 사용하는 주소 버스와 별도로 입출력 장치만을 위한 주소 라인을 따로 사용한다. 따라서 주소값이 메모리 주소인지 입출력장치 주소인지 구분하는 제어 라인이 따로 필요한 하드웨어를 이룬다. 이러한 하드웨어적 특성을 이용하기 위해 반드시 특수한 입출력 명령어를 사용해야 한다. 입출력이 메모리 공간의 사용에 영향을 주지 않는다는 장점이 있지만, 별도의 명령어를 사용해야 하므로 프로그래밍의 일관성이나 이식성이 떨어진다는 단점이 있다.
  • 메모리 사상형 입출력 (Memory-Mapped I/O) : 메모리 주소 지정을 위한 주소 버스와 입출력 제어 라인을 공유한다. 즉, 메모리에 할당된 주소 일부를 장치 제어기의 레지스터를 접근하는 주소로서 활용한다. (이를 메모리 주소 공간에 제어기 레지스터를 '사상한다'라고 한다.) 메모리 주소와 입출력 주소가 단일 공간에 존재하여 별도의 특수명령어 없이 LOAD나 STORE같은 메모리 접근을 위한 명령어를 사용할 수 있다. 따라서 명령어 개수가 줄어들어 프로그래밍이 용이하고 일관성과 이식이 뛰어나다는 장점이 있다. 하지만 입출력을위하여 메모리의 일부를 사용하므로 메모리 공간에 영향을 준다는 단점이 있다.

자료 이동 방식

  • CPU가 장치제어기의 자료를 관리하는 방식이다.
  • 직접 입출력 : CPU가 장치제어기 내부 자료 레지스터의 자료 이동을 직접 관장하는 것이다. 매번 입출력을 할 때마다 인터럽트를 걸어야하므로 부담이 될 수 있다. 따라서 입출력 발생의 시간 간격이 비교적 큰 문자 장치(키보드)에 주로 활용되고, 단위가 큰 블록 장치(하드 디스크)에서는 활용이 어렵다. 블록의 단위를 512 바이트라고 한다면 512 번의 인터럽트가 일어나야 하는데 이는 사실상 불가능하기 때문이다.
  • DMA(Direct Memory Access) : 이러한 한계를 극복하기 위해 개선된 방식이다. 입출력 장치가 CPU의 도움없이 독자적으로 메모리에 접근하도록 하여 하나의 입출력 명령만으로 블록 단위의 입출력이 가능하다. 하지만 CPU도 독립적으로 메모리에 접근하고 DMA도 독립적으로 메모리에 접근하면 서로 간에 충돌이 발생할 수 있다. 이러한 문제는 싸이클 스틸링 방식으로 해결한다. (CPU와 DMA가 동시에 메모리 접근을 요구하면 DMA에게 우선권을 주고 CPU는 한싸이클을 쉰다.) 어쨌든 이러한 DMA는 한 블록의 입출력 완료시에 한번의 인터럽트만 발생하도록 하여 CPU에 부담을 줄여주고 블록 단위의 고속 입출력도 가능하게 한다.

폴링 방식과 인터럽트 방식

  • 장치 제어기의 상태를 CPU가 알게 하는 방식은 먼저 지금까지 보았던 인터럽트 방식, 그리고 폴링방식 두가지가 존재한다. 먼저 폴링방식에 대해 알아보자.
  • 폴링 방식 : CPU가 상태 레지스터를 반복적으로 체크하면서 목표한 상태로 바뀌었는지 체크하는 방식이다. 디바이스 드라이버는 응용 프로세스로부터 요청이 들어오면, 장치 제어기의 명령 레지스터에 관련 명령어를 적재하고 이로 인해 장치가 가동된다. 그리고 상태 레지스터의 값을 체크하며 Busy 상태가 Done상태로 바뀔 때까지 조건문을 반복적으로 수행한다. 그러다 장치가 명령의 수행을 마쳐 Done 상태를 띄면 조건문을 만족하면 장치 제어기의 자료 레지스터의 내용을 가져오는 등의 요청받은 일을 수행한다. 이러한 방법은 반복문을 무한히 돌게되어 CPU를 낭비할 수 있다. (Busy Wating)
  • 인터럽트 방식 : 폴링 방식에서의 CPU에 대한 부담을 줄이기 위해 고안된 방법이다. CPU가 다바이스 드라이버 내의 코드를 수행함으로서 입출력 명령을 장치 제어기에 전송하고 CPU는 다른 프로세스를 수행하고 있다가 장치 제어기가 이를 알림방식으로 CPU에게 알려주는 것이다. 하드웨어가 인터럽트 처리 체계를 갖추고 있다는 것을 전제로 한다. 인터럽트가 들어오면 CPU는 현재 진행중인 프로세스 또는 하위 ISR 수행을 중단하고 (중단) 프로그램 카운터(PC) 및 CPU 레지스터 값을 보존한다. (문맥보존) 그리고 현재 인터럽트에 해당하는 마스크를 설정하여 자신보다 하위 인터럽트는 처리되지 않도록 한다. (마스크설정) 마지막으로 현재 인터럽트에 해당하는 ISR로 점프하여 해당 과정을 실시한다. (ISR진입) 인터럽트 처리가 끝나면 인터럽트 당한 프로세스 혹은 하위 ISR을 실행시키기 위해 레지스터 내용을 복구한다. 특히 프로세스를 복구하는 경우 어떤 프로세스를 복구하여 진행시킬지 스케쥴링에 따라 결정한다.

다수준 인터럽트와 인터럽트 마스크

  • 지금까지 자연스럽게 사용하던 '하위 인터럽트'라는 말에서 유추할 수 있듯이 인터럽트는 한 가지만 있는 것이 아니다. 다양한 인터럽트 장치에서 다양한 인터럽트가 들어온다. 따라서 이들의 충돌을 방지하기 위해 인터럽트 우선순위(Interrupt priority level)가 존재한다. 이에 따라 인터럽트에는 여러가지 수준이 존재할 것이므로 이를 다수준 인터럽트라고도 부른다.
  • 인터럽트 마스크는 순위를 비교하는 용도로 사용된다. 어떤 인터럽트가 발생하면 그보다 낮은 수준의 인터럽트는 통과될 수 없도록 한다. 물론 그보다 높은 수준의 인터럽트는 마스크를 무시하고 통과할 수 있다. (이는 AND 연산자를 통해 처리된다. 새로 들어온 인터럽트 번호와 현재 수행중인 인터럽트 마스크 번호가 AND연산 처리된다. AND연산 후 값이 하나라도 남으면 지금 수행중인 인터럽트를 계속 처리하고 그 후에 새로 들어온 인터럽트를 처리한다.)
  • cf. Fast Interrupt Handler : 특별히 빠른 처리 빠른 반응을 위해 다른 인터럽트 처리를 disable하고 짧은 시간안에 처리를 마무리하는 것. time critical한 측면이 있는 clock interrupt같은 상황에서만 사용하는 것이 좋다. (되도록 다수준 인터럽트를 사용) 참고로 clock interrupt는 많은 작업을 동반하기 때문에 이를 모두 해결하면 다음 작업을 처리하지 못할 수 있다. 따라서 순수 인터럽트 처리 부분은 top-half로, 나머지를 bottm-half로 구분한다. 그리고 top-half는 fast interrupt 모드로, 나머지는 bottm-half에서 다수준 인터럽트로 처리하기도 한다.

입출력 시스템 정리

Read()함수를 예로 입출력 시스템의 메커니즘을 정리해보겠다.

  1. 부팅을 하면 커널에 필요한 함수들이 적재된다. 이에는 ISR이 실행될 수 있도록 돕는 Interrupt Descript Table와 디바이스 드라이버 등이 포함된다.
  2. 응용 프로그램에서 시스템 호출 함수인 Read()를 부를 때는 입출력장치에서 읽어온 데이터를 저장하기 위한 공간이 필요하다. 따라서 사용자 메모리 공간에 버퍼공간을 할당하여 해당 주소를 Read()의 파라미터로 활용한다.
  3. 함수를 실행하면 해당 신호가 트랩을 거쳐 커널의 디바이스 드라이버로 향하게된다. 그리고 해당 디바이스 드라이버에 존재하는, 실질적으로 입출력장치 제어에 필요한 함수인 'sys_read 함수'까지 이른다.
  4. sys_read 함수는 장치제어기로 부터 데이터를 받아야하므로 커널 공간내에 새로운 버퍼공간을 만든다. (사용자 메모리 공간과 커널 공간이 다르기 때문에 기존에 만들었던 버퍼공간을 활용할 수 없기 때문이다.)
  5. 장치제어기의 명령레지스터에 Read() 명령어를 기록한다. (이는 메모리 사상 입출력 방식을 사용하고 있기 때문에 가능한 일이다. 아니라면 별도의 특수한 명령어를 통해 동작했을 것이다.) 이 때 커널 공간내에 만들었던 버퍼공간의 주소도 함께 넘겨주는데, 이는 입출력 장치가 DMA를 통해 그 주소로 직접 접근할 수 있도록 시작주소를 알려주기 위함이다.
  6. 그리고 장치제어기로부터 데이터를 받기 전까지 Sleep()을 통해 해당 프로세스는 잠들고 Sched()를 실행해 CPU가 다른 프로세스를 진행하고 있도록 한다.
  7. 장치제어기는 명령어를 받았으니 자료레지스터에 응용 프로그램이 읽고자 했던 데이터를 받아온다. 데이터를 모두 받으면 커널 공간내에 할당했던 버퍼공간에 해당 데이터를 넘겨준다. 이는 CPU의 간섭없이 DMA를 통해 전달된다.
  8. 장치제어기가 CPU에게 인터럽트를 걸어 작업이 마무리됐음을 알린다. 해당 인터럽트는 인터럽트 마스크 등이 동반되는 다수준 인터럽트 처리과정을 거쳐 IDT에서 해당 ISR함수 주소를 찾아 실행한다. 그럼 다시 스케쥴링을 발생시키고 다시 우리가 사용하던 프로세스가 깨어난다.
  9. 그럼 sys_read함수의 sleep()다음 라인부터 코드가 진행된다. 커널 공간에 할당된 버퍼에 존재하는 데이터를 기존 사용자 메모리 데이터의 버퍼로 카피시켜주고, sys_read함수는 리턴된다.
  10. 그럼 read()함수는 리턴되고, 처음 할당했던 사용자 메모리 버퍼에 원하는 데이터를 읽어오게 된다.
반응형