Study/컴퓨터 밑바닥의 비밀

[컴퓨터 밑바닥의 비밀] 6. 입출력

sw_develop 2025. 3. 24. 00:00

CPU는 어떻게 입출력 작업을 처리할까?

  • 입출력 작업을 어떻게 요청하고 결과를 어떻게 전달받을까?
  • CPU 내부에 레지스터가 있는 것과 마찬가지로, 장치에도 자체적인 레지스터인 장치 레지스터(device register)가 있음
    • 장치 레지스터는 주로 장치에 관련된 일부 정보를 저장하고, 다음 두 가지 레지스터가 있음
      • 데이터를 저장하는 레지스터
        • ex) 사용자가 키보드의 키를 누르면 그 정보는 이런 레지스터에 저장됨
      • 제어 정보와 상태 정보를 저장하는 레지스터
        • 레지스터를 읽고 쓰는 작업을 이용해 장치를 제어하거나 장치 상태를 볼 수 있음
  • 장치에서 생성된 데이터를 얻거나 장치를 제어하는 작업은 모두 이런 레지스터를 읽고 쓰는 것으로 함

 

Q. 어떻게 장치 레지스터를 읽고 쓸까?

  • 아래 두 가지 방식을 사용함
    1. 특정 입출력 기계 명령어를 사용한다.
    2. 메모리의 읽기와 쓰기 명령어를 함께 사용하지만 주소 공간의 일부분을 장치에 할당한다.
  • 입출력 기계 명령어
    • 전문적으로 장치 레지스터를 읽고 쓰는 특정한 기계 명령어를 설계
    • 이러한 특정한 기계 명령어를 입출력 명령어(input and output instruction)이라고 함
  • 메모리 매핑 입출력
    • 주소 공간의 일부분을 장치에 할당하여, 메모리를 읽고 쓰는 것처럼 장치를 제어하는 방법을 '메모리 매핑 입출력'이라고 함
    • 장치마다 고유한 주소가 부여되며, 입출력 명령어에 장치 주소를 지정
    • 이것으로 CPU가 입출력 명령어를 실행하면 하드웨어 회로가 어떤 장치 레지스터를 읽고 써야 하는지 알 수 있음

 

Q. 장치에 데이터가 입력된 것을 어떻게 알까?

  • 인터럽트 구동식 입출력(interrupt driven input and output)을 사용
    • CPU가 외부 장치로부터 데이터 입력이 올 때까지 동기 방식으로 기다리지 않음
    • CPU는 다른 스레드의 작업을 처리하고 외부 장치에 데이터가 입력되면 외부 장치가 인터럽트 신호를 보냄
    • CPU는 실행 중인 현재 작업과 인터럽트 요청의 우선순위를 비교해 처리를 수행
    • 이와 같이 입출력을 비동기로 처리하는 방법을 '인터럽트 구동식 입출력'이라고 함
  • 인터럽트를 처리할 때는 먼저 중단된 작업 상태를 보존해야 함
  • 이어서 CPU는 인터럽트 처리 함수의 시작 위치로 점프하여 인터럽트 처리 함수의 명령어를 실행한 후 처리가 끝나면 다시 원래 자리로 점프하여 중단되었던 작업을 계속 실행함

 

디스크가 입출력을 처리할 때 CPU가 하는 일은 무엇일까?

  • 디스크가 입출력을 처리할 때 CPU 개입이 필요하지 않음
  • 디스크가 입출력 요청을 처리하는 동안 OS는 CPU가 다른 작업을 수행하도록 스케줄링
    • CPU는 다른 스레드를 실행하거나 커널 모드에서 커널 프로그램을 실행하거나 아니면 유휴 상태일 수 있음

 

Q. 디스크가 입출력 요청을 처리하는 전체 과정에서 왜 CPU 개입이 필요하지 않을까?

  • 이를 이해하려면, 장치 제어기, 직접 메모리 접근(DMA), 인터럽트의 관계를 이해해야 함

 

장치 제어기

  • 입출력 장치의 전자 부분을 '장치 제어기(device controller)'라고 함
    • 디스크와 같은 입출력 장치는 대체로 두 부분으로 나눌 수 있음
      • 기계 부분
        • 디스크의 경우 헤드, 실린더 등 눈에 보이고 만질 수 있는 부분들을 포함
        • 입출력 요청이 들어왔을 때 읽어야 하는 데이터의 위치를 찾기 위해 헤드가 특정 트랙으로 이동해야 함
          • 이 과정을 탐색(seek)이라고 하며, 이 작업은 디스크 I/O 중 매우 많은 시간을 소모하는 작업에 해당함
          • 기계 장치이기 때문에 CPU 속도와 비교하면 느릴 수 밖에 없음
      • 전자 부분
        • 기계 부분을 제외한 나머지 부분
        • 전자 부품으로 구성되어 있으며, 이를 '장치 제어기'라고 함
  • 마이크로 컴퓨터 시스템으로 발전하여 전자 부분은 자체적인 프로세서와 펌웨어(firmware)를 갖추고 있음
  • 따라서 CPU가 직접 도와주지 않는 상황에서도 복잡한 작업을 할 수 있고, 동시에 자신만의 버퍼나 레지스터를 갖추고 있어 장치에서 읽은 데이터나 장치에 저장할 데이터를 저장할 수 있음

 

추가) 장치 제어기와 장치 드라이버

  • 장치 드라이버(device driver)는 OS에 속한 코드
  • 장치 제어기는 장치 드라이버에서 명령을 받아 외부 장치를 제어하는 하드웨어에 해당
  • 즉, 장치 제어기는 OS에 해당하는 장치 드라이버와 외부 장치를 연결하는 다리 역할 수행
  • 장치 제어기가 점점 복잡해지는 목적 중 하나가 바로 CPU를 해방시키기 위함임

 

직접 메모리 접근(DMA)

  • 데이터는 항상 장치와 메모리 사이에 전송되어야 하므로, 누군가는 이 작업을 수행해줘야 함
  • CPU 개입 없이 장치와 메모리 사이에 직접 데이터를 전송할 수 있는 작동 방식을 설계하였고, 이 작동 방식을 '직접 메모리 접근(direct memory access)'라고 함

  • DMA의 동작 과정
    • CPU가 직접 데이터를 복사하지는 않지만, CPU는 반드시 어떻게 데이터를 복사할지 알려주는 명령어를 DMA에 전달해야 함
      • 메모리에서 장치로 데이터를 기록할지 or 장치에서 메모리로 기록할지, 데이터를 얼마나 기록할지, 어느 메모리 위치에서 데이터를 읽어야 하는지, 어떤 장치에서 데이터를 읽고 쓸지 등
    • DMA는 CPU로부터 전달받은 정보로 자신의 작업 목표를 명확히 하고, 버스 중재(bus arbitration) 수행함
      • 버스의 사용 권한을 요청한 후 이어서 장치를 작동시킴
      • 참고) DMA의 버스 중재란?
        • DMA 컨트롤러가 메모리와 데이터를 주고받기 위해서는 시스템 버스(주소 버스, 데이터 버스, 제어 버스)를 사용해야 함
        • 해당 버스는 CPU와 DMA 컨트롤러가 공유하기 때문에 누가 언제 버스를 사용할지를 조정하는 과정이 필요함
        • 이 과정이 '버스 중재(bus arbitration)'임
    • 디스크에서 데이터를 읽는 경우, 장치 제어기의 버퍼에서 데이터를 읽으면 DMA가 지정된 메모리 주소에 데이터를 쓰는 방식으로 데이터 복사가 수행됨
      • 즉, 원래 CPU가 해야 할 작업 일부를 대신함

 

추가) DMA 사용으로 인한 문제와 해결

더보기
  • 가상 메모리 지원 시스템
    • OS가 DMA에 필요한 가상 주소와 물리 메모리 주소 사이의 매핑 정보를 제공함
    • 이를 통해 DMA가 가상 주소를 기반으로 직접 데이터를 전송할 수 있게 됨
  • 캐시 지원 시스템
    • 동일한 데이터가 캐시와 메모리에 각각 저장되어 있어 데이터 일관성 문제가 생길 수 있음
    • 만약 DMA가 a 변수를 메모리에서 읽어 장치에 저장해야 한다면, 상응하는 캐시의 데이터를 즉시 메모리에 갱신하여 일관성 문제가 나타나지 않도록 함
  • CPU는 데이터 전송이 완료되었는지 어떻게 알 수 있을까?
    • DMA가 데이터 전송을 완료하면 인터럽트 작동 방식을 사용하여 CPU에 알려줌

 

파일을 읽을 때 프로그램에는 어떤 일이 발생할까?

  • 메모리 관점에서 입출력은 단순히 데이터의 복사일 뿐이다.
    • 입력 : 데이터가 외부 장치에서 메모리로 복사되는 것
    • 출력 : 데이터가 메모리에서 외부 장치로 복사되는 것

 

순서

더보기
  • 단일 코어 CPU 시스템에 프로세스 A와 B가 있고, 프로세스 A가 현재 실행중
  • 프로세스 A에는 파일을 읽는 코드가 있음
  • 일반적으로 먼저 데이터를 저장하는 버퍼를 정의한 후 다음과 같이 read 계열의 함수를 호출함
char buffer[LEN];

read(buffer);
  • 해당 함수는 저수준 계층에서 시스템 호출을 이용해 OS에 파일 읽기 요청을 보냄
  • 이 요청은 커널에서 디스크가 이해할 수 있는 명령어로 변환되어 디스크로 전송됨
  • OS는 현재 프로세스의 실행을 일시 중지하고 입출력 블로킹 대기열에 넣음
  • OS는 이미 디스크에 입출력 요청을 보낸 상태이고, 디스크는 DMA 작동 방식을 사용해 데이터를 특정 메모리 영역으로 복사하는 작업을 시작함
    • 이 메모리 영역이 read 함수 호출할 때 지정했던 buffer
  • OS에는 블로킹 대기열 외에도 준비 완료 대기열이 존재함
    • 준비 완료 대기열은 대기열 안의 프로세스가 다시 실행되는 조건이 준비되었음을 의미함
  • OS는 프로세스 B를 준비 완료 대기열에서 꺼내 CPU를 이 프로세스에 할당함
  • 프로세스 B는 CPU가 실행하고 있고, 디스크는 프로세스 A의 메모리 공간에 데이터를 쓰고 있음
  • 디스크가 데이터를 모두 프로세스 A의 메모리에 복사하는 작업이 완료되면, 디스크는 CPU에 인터럽트 신호를 보냄
  • CPU는 인터럽트 신호를 받은 후 처리가 중단되었던 함수로 점프하며, 디스크의 입출력 처리가 완료되어 프로세스 A가 계속 실행될 수 있는 자격을 다시 얻었음을 알 수 있게 됨
  • OS는 프로세스 A를 입출력 블로킹 대기열에서 꺼내 다시 준비 완료 대기열에 넣음
  • 이후 OS는 CPU를 프로세스 A에 할당할지 프로세스 B에 할당할지 결정한 후 실행을 이어나감

 

참고)

  • 실제로는 입출력 데이터는 먼저 OS 내부로 복사되며 이후 OS가 프로세스의 주소 공간으로 복사함
  • 즉 OS를 통한 복사라는 하나의 계층이 더 존재함
  • OS를 우회하여 직접 데이터를 프로세스 주소 공간에 복사할 수도 있는데, 이를 zero-copy 기법이라고 함

 

높은 동시성 제공을 위한 입출력 다중화

  • 모든 입출력 장치는 파일이라는 개념으로 추상화됨 (모든 것이 파일)
    • 디스크, 네트워크 데이터, 터미널, 프로세스 간 통신 동구인 파이프까지 모두 파일로 취급됨
  • 모든 입출력 작업은 파일 읽기와 쓰기로 구현할 수 있음
    • 이러한 추상화를 이용해 일련의 인터페이스로 모든 외부 장치를 사용할 수 있음
    • open 함수를 사용해 파일을 열고, read 함수와 write 함수를 사용해 파일을 읽고 쓰며, seek 함수를 사용해 파일을 읽고 쓰는 위치(file pointer)를 변경하고, close를 사용해 파일을 닫음

 

파일 서술자(file descriptor)

  • 파일을 열 때 커널은 파일 서술자를 반환하며, 파일 작업을 실행할 때 해당 파일 서술자를 커널에 전달해 주어야
  • 커널은 이 숫자를 얻은 후 해당 숫자에 대응하는 파일에 관련된 모든 정보를 찾아 파일 작업을 완료할 수 있음
    • 파일 서술자는 숫자에 불과함
  • 파일 서술자를 사용하면 프로세스는 파일에 대해 아무것도 몰라도 됨
    • 파일이 디스크에 저장되어 있는지, 디스크의 어느 위치에 저장되어 있는지, 지금 읽고 있는 위치는 어디인지 등의 정보를 OS가 대신 처리함
    • 프로그래머는 파일 서술자만 프로그래밍하면 됨
char buffer[LEN];
int fd = open(file_name); // 파일 서술자 얻기

read(fd, buffer); // fd에 해당하는 파일에서 데이터를 읽어와 buffer에 복사하는 것

 

입출력 다중화(input/output multiplexing)

  • 프로그램이 동시에 많은 수의 파일 서술자를 다룰 수 있는 방법 (동시에 여러 파일에 읽고 쓰기를 가능하게 하는 방법)
    • 컨셉
      • 커널에 계속 입출력 장치가 읽을 수 있는 상태인지 쓸 수 있는 상태인지 물어보는 대신 필요할 때 커널이 응용 프로그램에 통지하도록 하는 것
      • 관심 대상인 파일 서술자를 커널에 알려주고, 커널에 '여기 파일 서술자 10,000개 있으니 대신 감시하다가 읽고 쓸 수 있는 파일 서술자가 있을 때 알려주면 처리하겠다'라고 이야기 하는 것
  • 입출력 다중화는 다음과 같은 과정을 의미함
    1. 파일 서술자를 획득한다. 이때 서술자 종류는 어떤 입출력 장치의 파일 서술자든 간에 상관없다.
    2. 특정 함수를 호출하여 커널에 다음과 같이 알린다. '이 함수를 먼저 반환하는 대신, 이 파일 서술자를 감시하다 읽거나 쓸 수 있는 파일 서술자가 나타날 때 반환해 주세요.'
    3. 해당 함수가 반환되면 읽고 쓸 수 있는 조건이 준비된 파일 서술자를 획득할 수 있으며, 이를 통해 상응하는 처리를 할 수 있음
  • 리눅스에서 입출력 다중화 기술을 사용하는 방법 select, poll, epoll 세 가지가 있음
    • 세 가지 모두 동기 입출력 다중화 기술
      • 함수들이 호출될 때 감시해야 하는 파일 서술자에서 읽기 가능 또는 쓰기 가능 같은 관심 대상 이벤트가 발생하지 않으면 호출된 스레드가 블로킹되어 일시 중지되고, 파일 서술자가 해당 이벤트를 생성할 때까지 함수는 반환되지 않음 (Blocking I/O)
    • select
      • 감시할 수 있는 파일 서술자 묶음에 제한이 있음 (일반적으로 1024개 초과 불가)
      • select가 호출될 때 대응하는 프로세스 또는 스레드는 감시 대상인 파일의 대기열에 배치되므로 select 호출로 블로킹되며 일시 중지됨
      • 파일 서술자 중 하나라도 읽기 가능 또는 쓰기 가능 이벤트가 나타나면 해당 프로세스 또는 스레드가 다시 깨어남
      • 여기서 문제는 어떤 파일 서술자가 읽고 쓸 수 있는지 알 수 없어, 어떤 파일 서술자가 준비 완료 상태인지 알기 위해 파일 서술자 묶음을 처음부터 끝까지 다시 확인해야 함
      • 이것이 select가 대량의 파일 서술자를 감시할 때 효율이 매우 떨어지는 근본적인 원인임
    • poll
      • select와 매우 유사
      • 달라진 점은 감시 가능한 파일 서술자 수의 제한을 없앤 것
      • poll도 마찬가지로 감시해야 하는 파일 서술자 수가 늘어날수록 성능이 저하되는 문제가 있으므로, 높은 동시성을 요구하는 상황에 제대로 대응이 불가능함
    • epoll
      • 대량의 파일 서술자 감시할 때 select와 poll에서 발생할 수 있는 성능 저하 문제를 해결함
      • 커널에 필요한 데이터 구조를 생성함, 해당 데이터 구조에서 중요한 것은 '준비 완료된 파일 서술자 목록'임
      • 감시되고 있는 파일 서술자에서 관심 이벤트가 발생하면 해당 프로세스나 스레드를 깨우면서 준비 완료된 파일 서술자를 준비 완료 목록에 추가
      • 프로세스나 스레드는 모든 파일 서술자를 처음부터 끝까지 확인할 필요없이 준비 완료된 파일 서술자를 직접 획득할 수 있어 효율적