본 글은 The Linux Kernel 을 정리한 것이며, 출처를 밝히지 않은 모든 이미지는 원글에 속한 것입니다.
하드웨어 장치의 추상화
- 운영체제는 하드웨어 장치별로 다른 특징을 보이지 않는, 일관된 인터페이스를 사용자에게 제공
- 모든 물리 장치는 각자의 하드웨어 컨트롤러를 가지며, 모든 하드웨어 컨트롤러는 각자의 고유한 제어/상태 레지스터(Control and Status Registers, CSRs)를 포함
- 키보드, 마우스, 직렬포트는 Super I/O 칩이 제어하며, IDE 디스크는 IDE 컨트롤러, SCSI 디스크는 SCSI 컨트롤러가 제어
- CSRs는 장치를 시작 및 중단하고 초기화하며, 문제가 발생했을 때 진단하는 용도로 사용됨
- 커널에는 하드웨어 컨트롤러를 제어하고 관리하기 위해 디바이스 드라이버(Device Driver)가 존재하는데, 이는 커널 모드에서 실행되고 메모리에 존재하며 저수준 하드웨어 처리 루틴을 포함한 공유 라이브러리
- 각 디바이스 드라이버는 각자 관리하는 장치들의 특성들을 처리
- 모든 물리 장치들은 일반 파일처럼 사용자에게 보여지며, 이를 장치 특수 파일(device special file)이라고 하는데, 파일을 다루는 표준 시스템 콜을 이용해서 열고, 닫고, 읽고, 쓸 수 있음
- IDE 디스크는 /dev/hda 로 나타냄
- 네트워크 장치들도 파일로 표시되지만 커널이 네트워크 컨트롤러를 초기화할 때 장치 특수 파일이 생성됨
- 동일한 디바이스 드라이버로 제어되는 모든 장치는 동일한 메이저 장치 번호를 가지며, 각 장치나 컨트롤러를 구분하기 위해 마이너 장치 번호를 사용
- 리눅스는 문자, 블럭, 네트워크 3가지 종류로 하드웨어 장치의 추상화를 제공
- 문자 장치는 버퍼 없이 하나씩 바로 읽고 쓸 수 있음 e.g., 키보드
- 블럭 장치는 일정한 크기(512 또는 1024바이트)의 배수로만 읽고 쓸 수 있음 e.g., 하드 디스크
- 네트워크 장치는 BSD 소켓 인터페이스로 하위 네트워크 계층(TCP/IP)에 접근할 수 있음 e.g., 이더넷
- 커널 디바이스 드라이버의 공통 특성
- 커널 코드의 일부로, 잘못되면 시스템에 큰 피해를 줄 수 있음
- 커널이나 자신이 속한 서브시스템에 표준 인터페이스를 제공
- 터미널 디바이스 드라이버는 커널에 파일 I/O 인터페이스를 제공
- SCSI 디바이스 드라이버는 커널에 파일 I/O와 버퍼 캐시 인터페이스를 제공
- 커널 메커니즘 및 서비스 제공: 메모리 할당, 인터럽트 전달, 대기 큐 같은 커널 서비스 사용
- 대부분 디바이스 드라이버는 필요 시 물리 메모리에 로드하고 필요없으면 언로드 가능
- 커널에 포함된 상태로 함께 컴파일 될 수 있고, 장치는 컴파일 시 설정 가능
- 부팅 후 디바이스 드라이버가 초기화 될 때, 시스템은 제어할 하드웨어 장치를 가지며, 없다하더라도 문제가 되지 않음
폴링(Polling)과 인터럽트(Interrupt)
- 장치에 명려을 전달할 때, 그 명령이 언제 끝났는지 알 수 있는 방법
- 폴링하는 디바이스 드라이버는 시스템 타이머를 이용하여 어느정도 시간이 지나면 커널이 디바이스 드라이버에 있는 한 루틴을 호출. 이 타이머 루틴은 명령이 수행되었는지 상태를 검사 (주기적으로 확인하는 작업)
- 인터럽트를 이용하는 디바이스 드라이버는 제어하는 장치가 작업을 끝냈거나 서비스를 받아야할 때 인터럽트를 발생시킴. 커널은 이 인터럽트를 올바른 디바이스 드라이버로 전달함 (비동기적으로 요청하고 결과를 받음)
- 이더넷 디바이스 드라이버는 네트워크에서 패킷을 받을 때마다 인터럽트를 발생시킴
- 디바이스 드라이버는 인터럽트를 사용하기 위해 인터럽트 처리 루틴의 주소와 사용할 인터럽트 번호를 커널에 등록
- /proc/interrupts 파일을 살펴보면, 디바이스 드라이버가 사용중인 인터럽트를 알 수 있음
- 드라이버가 초기화될 때 인터럽트 자원(제어권)을 커널에 요청
- 시스템의 어떤 인터럽트들은 처음부터 고정되어 있으며, 플로피 디스크 컨트롤러의 경우 항상 6번을 사용. 또는 PCI 장치에서 발생하는 인터럽트들은 부팅 시 동적으로 할당됨
- PCI 디바이스 드라이버는 제어할 장치의 인터럽트 번호(IRQ)를 먼저 알아낸 뒤(즉, 탐사 과정) 인터럽트의 제어권을 요청
- 리눅스는 표준 PCI BIOS 콜백을 지원해서 장치 정보를 제공
- 인터럽트가 PIC에서 CPU로 전달될 때는 인터럽트 모드에서 처리 루틴을 실행하는데, 다른 인터럽트가 발생하지 못하므로 되도록 처리 루틴에서는 적은 일을 하고 인터럽트 전으로 돌아갈 수 있도록 스택에 컨텍스트를 저장하고 복구함
- 작업량이 많으면 나중에 해도 될 일을 커널의 하반부 핸들러나 작업큐에 넣어 처리 (인터럽트 모드를 빨리 벗어나려는 의도)
직접 메모리 접근(Direct Memory Access, DMA)
- 인터럽트를 사용하는 디바이스 드라이버는 데이터의 양이 작을 때는 잘 동작하지만, 디스크 컨트롤러 및 이더넷 장치 같은 데이터 전송률이 높은 장치의 경우 CPU 이용률이 높을 수 밖에 없음
- DMA 컨트롤러는 장치와 시스템 메모리 사이에 CPU 도움 없이 데이터 전송을 가능하게 함
- ISA DMA 컨트롤러는 8개의 DMA 채널을 제공하고, 이 중 7개를 디바이스 드라이버가 사용하는데, 드라이버끼리는 채널을 공유할 수 없음
- 각 DMA 채널은 16비트 주소 레지스터와 16비트 카운터 레지스터로 접근 가능
- 데이터 전송을 초기화하기 위해, 디바이스 드라이버는 DMA 채널의 두 레지스터와 데이터 전송 방향(읽기/쓰기)을 함께 설정
- DMA를 사용해도 좋다는 명령을 보내야 사용 가능하며, 데이터 전송이 완료되면 장치는 인터럽트를 발생시키고, 전송하는 동안 CPU는 다른 일을 할 수 있음
- DMA 컨트롤러는 물리 메모리에 직접 접근하기 때문에, 프로세스의 가상 메모리로 DMA를 바로 사용할 수 없고, DMA에서 사용하는 메모리는 물리 메모리에서 연속된 블럭으로 할당되어야 함
- 사용자는 시스템 콜로 프로세스가 사용하는 물리 페이지에 락을 걸어 DMA 작업 중 스왑 아웃되는 것을 방지할 수 있음
- DMA가 사용하는 메모리는 하부 16MB 로 제한되어 있어 DMA 컨트롤러는 물리 메모리 전체에 접근 불가
- 인터럽트처럼 어떤 장치가 사용하는 DMA 채널이 고정되어 있고, 이더넷 장치의 경우 하드웨어에 점퍼로 설정 가능
- 또는 장치들이 CSRs를 통해 사용할 DMA 채널을 알려주고 디바이스 드라이버가 빈 채널을 사용할 수도 있음
- 리눅스는 DMA 채널 하나당 dma_chan 구조체를 생성하며, 이 구조체의 배열을 이용하여 채널의 사용유무를 추적
- 이 구조체는 2개의 항목을 포함: DMA 채널의 소유자를 나타내는 문자열 및 해당 채널의 할당 유무를 나타내는 플래그
- cat /proc/dma 명령을 실행하면 나오는 것이 dma_chan 구조체의 배열
- DMA가 없을 때와 있을 때의 차이
- DMA 없을 때, CPU가 장치로부터 데이터 읽고 쓰는 작업을 반복해야 되서 CPU 이용률이 증가함
- DMA 있을 때, CPU는 장치에게 DMA 시작 신호 및 DMA 완료 인터럽트만 수신하고, 데이터 전송에는 개입하지 않기 때문에 CPU 이용률이 낮음
디바이스 드라이버가 제공하는 인터페이스
- 디바이스 드라이버의 메모리
- 디바이스 드라이버는 커널 코드의 일부이기 때문에 물리 메모리만 사용 가능
- 인터럽트를 받았거나 하반부 핸들러 또는 작업 큐 핸들러가 스케줄되었을 때, 현재 프로세스는 바뀔 수 있는데, 이는 디바이스 드라이버가 특정 프로세스가 실행될 때 독립적으로 다른 한 켠(백그라운드)에서 실행되기 때문 (커널 코드의 특성)
- 부팅 시 커널에서 사용할 메모리가 할당되고 메모리를 해제하는 루틴을 전달받아, 디바이스 드라이버는 사용할 메모리를 2의 제곱승 단위로 할당 받거나 해제할 수 있음
- 디바이스 드라이버가 할당 받은 메모리를 DMA로 입출력하려면, 그 메모리를 DMA 가능이라고 지정해야 함
- 커널이 모든 종류의 디바이스 드라이버에게 서비스를 요청할 때 사용할 수 있는 공통적인 인터페이스가 존재
- 서로 다른 장치들 또는 디바이스 드라이버들을 일관적이게 다루기 위한 인터페이스
- 디바이스 드라이버는 장치가 없더라도 시스템이 동작할 수 있도록 커널에 의해 초기화될 때 스스로 커널에 등록
- 커널은 등록된 디바이스 드라이버들을 테이블로 관리하며, 이 테이블은 해당 종류의 장치와 인터페이스를 제공하는 함수들의 포인터를 저장하고 있음
- 문자 장치(Character Device)
- 초기화될 때, 이 장치의 디바이스 드라이버는 device_struct 구조체를 생성하여 chrdevs 배열에 추가하는 것으로 커널에 자신을 등록
- /proc/devices 에서 문자 장치에 대한 내용은 모두 chrdevs 배열에서 가져온 것
- 메이저 장치 번호(tty 장치는 4번 같은)는 이 배열의 인덱스로 사용되며, 고정되어 있음
- device_struct 구조체는 2가지 항목을 포함
- 디바이스 드라이버의 등록 이름에 대한 문자열 포인터
- 파일 연산 블럭에 대한 포인터
- 프로세스가 문자 장치를 나타내는 문자 특수 파일(character specific file)을 열면, 커널은 올바른 문자 디바이스 드라이버의 파일 처리 루틴이 호출되도록 설정함
- 각 장치 특수 파일은 VFS inode로 기술되며, VFS inode는 장치의 메이저 식별자와 마이너 식별자를 가짐
- 각 VFS inode는 파일 연산 루틴에 대한 포인터를 가지는데, 생성 시 기본 문자 장치 연산으로 설정됨 (이 연산은 가리키는 파일 시스템 객체에 따라 다름)
- 파일 연산 함수를 실행하면, 장치의 메이저 식별자로 chrdevs 배열에 인덱싱 --> 이 장치에 대한 파일 연산 블럭을 가져옴 --> file 구조체와 파일 연산 포인터가 디바이스 드라이버의 것을 참조하도록 device_struct 구조체를 설정
- 이후 프로세스에서 호출하는 모든 파일 연산은 문자 장치의 파일 연산으로 매핑되어 호출됨
- 블럭 장치(Block Device)
- 초기화될 때, 이 장치의 디바이스 드라이버는 device_struct 구조체를 생성하여 blkdevs 배열에 추가하는 것으로 커널에 자신을 등록
- 문자 장치와 마찬가지로, 메이저 장치 번호는 blkdevs 배열의 인덱스로 사용되며, 다른 점은 장치의 유형을 분류하는 클래스가 존재한다는 것. SCSI 장치와 IDE 장치를 클래스로 구분하며, 각 클래스는 커널에 파일 함수를 제공해야 함
- 각 클래스의 블럭 장치들을 사용하는 디바이스 드라이버는 고유의 클래스 인터페이스를 제공하는데, 예를 들어, SCSI 디바이스 드라이버는 "SCSI 서브시스템이 커널에 파일 함수를 제공하기 위해 사용하는 인터페이스"를 서브시스템에 제공해야 함
- 모든 블럭 디바이스 드라이버는 파일 연산과 함께 버퍼 캐시에 대한 인터페이스를 제공
- 버퍼 캐시에 한 블럭의 데이터를 읽고 쓰기 위한 요청을 보내면, 각 드라이버는 all_requests (정적 리스트)에서 request 구조체를 할당받아 blk_dev_struct 구조체에 있는 request 리스크 (요청 큐)에 할당받은 것을 추가. 이후 요청 큐를 처리하기 위해 blk_dev_struct 구조체에 있는 요청 루틴의 주소로 점프해서 요청을 처리
- blk_dev_struct 구조체는 blk_dev 배열의 원소이며, 메이저 장치 번호로 인덱싱됨
- 각 request 구조체는 하나 이상의 buffer_head 구조체에 대한 포인터를 가지며, 이 구조체는 버퍼 캐시에 의해 락됨
- 이 버퍼로 블럭 연산이 끝날 때까지 프로세스는 대기 상태가 됨
- 요청이 처리되면 드라이버는 request 구조체에서 각 buffer_head 구조체를 제거하고 갱신되었음을 표시한 뒤 락을 해제. 대기 중인 프로세스는 다음 스케줄링에서 실행됨
'Operating System > Linux' 카테고리의 다른 글
리눅스 커널 - 파일 시스템 (0) | 2021.06.06 |
---|---|
리눅스 커널 - 디바이스 드라이버 2 (0) | 2021.06.05 |
리눅스 커널 - 인터럽트와 인터럽트 처리 (0) | 2021.06.05 |
리눅스 커널 - 주변장치 상호연결(PCI) (0) | 2021.06.04 |
리눅스 커널 - 프로세스간 통신(IPC) 메커니즘 (0) | 2021.06.03 |