하드디스크 드라이버는 하드디스크라는 I/O 디바이스를 구동하는 시스템 소프트웨어(펌웨어)입니다. 드라이버는 OS의 한 부분으로 애플리케이션과 I/O 디바이스 사이에 존재하며 애플리케이션이 I/O 디바이스를 사용할 수 있게 도와줍니다.
그래서 디바이스 드라이버는 I/O 컨트롤러에 있는 레지스터를 읽고 쓰는 코드로 이루어져 있습니다. 추가적으로 I/O 컨트롤러가 I/O 오퍼레이션이 종료되었을 때 CPU하게 이슈하는 인터럽트 서비스 루틴도 들어있습니다.
UNIX OS를 통해서 어떻게 I/O 디바이스을 구별하고 디바이스 드라이버 루틴을 찾아 호출할 수 있는지 알아보겠습니다. 우선 I/O 디바이스를 파일 형태로 이름을 붙여 구별을 합니다.
이 디바이스 파일에 어떤 정보가 들어있는지 알아보기 위해서 dev 디렉터리에 있는 몇몇 디바이스 파일을 시각화해봤습니다. 디바이스 파일의 첫 번째 부분에는 해당 디바이스가 블록 디바이스인지 캐릭터 디바이스인지 이렇게 어떤 타입에 해당하는지 적혀있습니다.
그다음에 메이저 넘버와 마이너 넘버가 적혀있는데 메이저 넘버는 디바이스 타입마다 부여되는 고유번호이고 마이저 넘버는 디바이스 타입에 따른 인스턴스를 구별하기 위해 부여된 이름입니다. (메이저 넘버 = 터미널일 경우 터미널에 부여된 고유 번호가 적힘, 마이너 넘버 = 여러 개의 터미널을 구별하기 위해서 터미널마다 가지는 아이디)
그래서 메이저 넘버만 안다면 OS가 해당 디바이스 타입에 따른 디바이스 드라이버 루틴을 모두 액세스할 수 있습니다. 이렇게 넘버를 통해서 정보를 얻기 위해서는 테이블이 필요합니다.
여기서는 스위치 테이블에 이런 디바이스 정보들이 잘 정리되어 있습니다. 스위치 테이블은 특정 디바이스 타입에 대한 디바이스 드라이버 루틴을 가리키는 포인터들로 구성되어 있습니다. (인덱스 넘버가 메이저 넘버)
터미널의 메이저 넘버가 1일 때 유저가 터미널을 이용하는 경우를 통해 설명해보겠습니다. 터미널은 캐릭터 디바이스로 캐릭터 디바이스 스위치 테이블로 이동합니다. 그 이후 1번 인덱스를 액세스 하면 어떤 data structure에 대한 포인터를 얻을 수 있습니다. 이 포인터로 이동하면 디바이스 네임과 디바이스 드라이브 루틴들에 대한 function point도 얻을 수 있습니다.
캐릭터 디바이스 드라이버 루틴에는 open, read, write, close, I/O control 이 존재합니다. open은 특정 디바이스를 초기화 시킵니다. close는 반대로 그 디바이스가 사용하던 리소스를 다 반납하고 디바이스를 시스템에서 detach 시킵니다. read/write는 디바이스에 데이터를 보내고 읽어들이는 오퍼레이션입니다.
마지막 I/O control은 그 디바이스의 컨트롤러에 있는 특정 레지스터 값을 읽어오거나 특정 레지스터 값을 변경시키는 역할을 합니다.
좀 더 자세히 알아보면 open 루틴의 경우 디바이스에 data structure를 잡아주고 디바이스 스위치 테이블의 entry를 채워줍니다. 그리고 인터럽트 서비스 루틴도 등록시키며 몇 번 open 되었는지 나타내주는 reference counter를 증가시킵니다. close 루틴의 경우 reference counter를 1 감소시키며 만약 0이라면 그 디바이스를 위해서 할당된 리소스를 반환시킵니다. 그다음 인터럽트 핸들러도 unregister 시킵니다.
Polling I/O의 경우 CPU에서 I/O 컨트롤러를 모니터링하면서 I/O 오퍼레이션이 종료될 때까지 무한루프를 돌면서 수행됩니다. interrupt drive I/O는 모니터링했을 때 I/O 컨트롤러가 아직 I/O 오퍼레이션을 수행할 준비가 안 돼있다면 sleep 합니다. 인터럽트가 발생했을 때 깨어나 I/O 오퍼레이션의 후반부를 처리합니다.
read/write는 실질적인 I/O 오퍼레이션을 진행하는 루틴으로 blocking I/O와 non blocking I/O로 나뉩니다. blocking I/O는 poliing이든 interrupt든 I/O를 하기 위해서 I/O 컨트롤러를 모니터링합니다. 이때 I/O 디바이스가 I/O 서비스를 진행할 준비가 안되어 있다면 polling의 경우 무한 루프를 돌고, interrupt의 경우 sleep을 시킵니다.
그 반면에 non-blocking I/O는 I/O 오퍼레이션을 하기 위해서 I/O 컨트롤러를 모니터링했을 때 준비가 안되어 있다면 거기서 수행을 끝내버립니다.
블록 디바이스의 경우도 비슷합니다. 우선 똑같이 블락 디바이스 스위치 테이블을 가지고 있고 블록 디바이스들의 메이저 넘버가 인덱스로 사용됩니다. 특정 인덱스에 가면 그 블록 디바이스에 대한 드라이버 루틴들의 주소를 담고 있는 구조체를 얻을 수 있습니다.
캐릭터 디바이스와 블록 디바이스의 차이는 블록 디바이스의 경우 data transfer의 단위가 byte가 아니라 block인 것입니다. 그래서 블록 디바이스 드라이버의 경우 스토리지에서 데이터를 읽어왔을 때 메인 메모리에 저장한 후 연산이 끝나더라도 바로 버리지 않습니다. (다른 프로그램에서 액세스 할 때 다시 전달해 주기 위해서 캐싱을 하는 것)
그래서 블록 디바이스의 경우 I/O 오퍼레이션인 read/write가 발생했을 때 인메모리 캐시에서 처리할 수 있다면 하드디스크 드라이브 액세스로 이루어지지 않습니다. (I/O 오퍼레이션이 발생되지 않음)
블록 디아비스가 스토리지 디바이스에 있는 블록을 읽어오면 커널이 관리하는 메인 메모리에 저장합니다. 이 과정을 buffer cache/page cache라고 부릅니다. 이렇게 캐시를 유지하고 있을 때 block read/write 오퍼레이션이 들어오면 메이저 넘버를 통해서 블록 디바이스 스위치 테이블의 entry를 찾아온 다음 캐시에 존재하는 구조체를 통해 디바이스 드라이버의 루틴을 호출하게 됩니다.
이번에는 bottom half에 대해서 알아보겠습니다. 프로세스가 실행 중일 때 자기와 무관한 I/O 오퍼레이션이 끝나서 I/O 컨트롤러가 인터럽트를 걸어줬습니다. 인터럽트 핸들러가 인터럽트를 disable 시키고 수행할 때 동일한 I/O 컨트롤러가 또 인터럽트를 발생시킨다면 첫 번째 인터럽트 핸들러가 끝나자마자 두 번째 인터럽트 핸들러가 enable 되어서 실행이 됩니다.
인터럽트 1과 2에 대한 핸들러가 두 번 도는 것이 아니라 bottom half가 한 번만 돌아서 두 개의 인터럽트를 처리하는 방식입니다. 인터럽트 서비스를 인터럽트 컨텍스트로 실행되는 인터럽트 핸들러 부분과 일반 스레드 텍스트로 실행되는 bottom half로 나눠서 프로세싱하는 것이 리눅스의 대표적인 인터럽트 핸들러 방식입니다.
'CS > Operating System' 카테고리의 다른 글
26. File System (0) | 2024.02.03 |
---|---|
25. Files and Directories (0) | 2024.02.03 |
23. I/O Device 관리 (1) | 2024.01.30 |
22. Trends in Memory Management (1) | 2024.01.29 |
21. Trashing and Working set (1) | 2024.01.27 |