Study/컴퓨터 밑바닥의 비밀
[컴퓨터 밑바닥의 비밀] 6. 입출력
sw_develop
2025. 3. 24. 00:00
CPU는 어떻게 입출력 작업을 처리할까?
- 입출력 작업을 어떻게 요청하고 결과를 어떻게 전달받을까?
- CPU 내부에 레지스터가 있는 것과 마찬가지로, 장치에도 자체적인 레지스터인 장치 레지스터(device register)가 있음
- 장치 레지스터는 주로 장치에 관련된 일부 정보를 저장하고, 다음 두 가지 레지스터가 있음
- 데이터를 저장하는 레지스터
- ex) 사용자가 키보드의 키를 누르면 그 정보는 이런 레지스터에 저장됨
- 제어 정보와 상태 정보를 저장하는 레지스터
- 레지스터를 읽고 쓰는 작업을 이용해 장치를 제어하거나 장치 상태를 볼 수 있음
- 데이터를 저장하는 레지스터
- 장치 레지스터는 주로 장치에 관련된 일부 정보를 저장하고, 다음 두 가지 레지스터가 있음
- 장치에서 생성된 데이터를 얻거나 장치를 제어하는 작업은 모두 이런 레지스터를 읽고 쓰는 것으로 함
Q. 어떻게 장치 레지스터를 읽고 쓸까?
- 아래 두 가지 방식을 사용함
- 특정 입출력 기계 명령어를 사용한다.
- 메모리의 읽기와 쓰기 명령어를 함께 사용하지만 주소 공간의 일부분을 장치에 할당한다.
- 입출력 기계 명령어
- 전문적으로 장치 레지스터를 읽고 쓰는 특정한 기계 명령어를 설계함
- 이러한 특정한 기계 명령어를 입출력 명령어(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가 해야 할 작업 일부를 대신함
- CPU가 직접 데이터를 복사하지는 않지만, CPU는 반드시 어떻게 데이터를 복사할지 알려주는 명령어를 DMA에 전달해야 함
추가) 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개 있으니 대신 감시하다가 읽고 쓸 수 있는 파일 서술자가 있을 때 알려주면 처리하겠다'라고 이야기 하는 것
- 컨셉
- 입출력 다중화는 다음과 같은 과정을 의미함
- 파일 서술자를 획득한다. 이때 서술자 종류는 어떤 입출력 장치의 파일 서술자든 간에 상관없다.
- 특정 함수를 호출하여 커널에 다음과 같이 알린다. '이 함수를 먼저 반환하는 대신, 이 파일 서술자를 감시하다 읽거나 쓸 수 있는 파일 서술자가 나타날 때 반환해 주세요.'
- 해당 함수가 반환되면 읽고 쓸 수 있는 조건이 준비된 파일 서술자를 획득할 수 있으며, 이를 통해 상응하는 처리를 할 수 있음
- 리눅스에서 입출력 다중화 기술을 사용하는 방법은 select, poll, epoll 세 가지가 있음
- 세 가지 모두 동기 입출력 다중화 기술임
- 함수들이 호출될 때 감시해야 하는 파일 서술자에서 읽기 가능 또는 쓰기 가능 같은 관심 대상 이벤트가 발생하지 않으면 호출된 스레드가 블로킹되어 일시 중지되고, 파일 서술자가 해당 이벤트를 생성할 때까지 함수는 반환되지 않음 (Blocking I/O)
- select
- 감시할 수 있는 파일 서술자 묶음에 제한이 있음 (일반적으로 1024개 초과 불가)
- select가 호출될 때 대응하는 프로세스 또는 스레드는 감시 대상인 파일의 대기열에 배치되므로 select 호출로 블로킹되며 일시 중지됨
- 파일 서술자 중 하나라도 읽기 가능 또는 쓰기 가능 이벤트가 나타나면 해당 프로세스 또는 스레드가 다시 깨어남
- 여기서 문제는 어떤 파일 서술자가 읽고 쓸 수 있는지 알 수 없어, 어떤 파일 서술자가 준비 완료 상태인지 알기 위해 파일 서술자 묶음을 처음부터 끝까지 다시 확인해야 함
- 이것이 select가 대량의 파일 서술자를 감시할 때 효율이 매우 떨어지는 근본적인 원인임
- poll
- select와 매우 유사
- 달라진 점은 감시 가능한 파일 서술자 수의 제한을 없앤 것
- poll도 마찬가지로 감시해야 하는 파일 서술자 수가 늘어날수록 성능이 저하되는 문제가 있으므로, 높은 동시성을 요구하는 상황에 제대로 대응이 불가능함
- epoll
- 대량의 파일 서술자 감시할 때 select와 poll에서 발생할 수 있는 성능 저하 문제를 해결함
- 커널에 필요한 데이터 구조를 생성함, 해당 데이터 구조에서 중요한 것은 '준비 완료된 파일 서술자 목록'임
- 감시되고 있는 파일 서술자에서 관심 이벤트가 발생하면 해당 프로세스나 스레드를 깨우면서 준비 완료된 파일 서술자를 준비 완료 목록에 추가함
- 프로세스나 스레드는 모든 파일 서술자를 처음부터 끝까지 확인할 필요없이 준비 완료된 파일 서술자를 직접 획득할 수 있어 효율적임
- 세 가지 모두 동기 입출력 다중화 기술임