본 글은 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 파일을 살펴보면, 디바이스 드라이버가 사용중인 인터럽트를 알 수 있음
    • 드라이버가 초기화될 때 인터럽트 자원(제어권)을 커널에 요청

/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 이용률이 낮음

http://jake.dothome.co.kr/dma-1/

디바이스 드라이버가 제공하는 인터페이스

  • 디바이스 드라이버의 메모리
    • 디바이스 드라이버는 커널 코드의 일부이기 때문에 물리 메모리만 사용 가능
    • 인터럽트를 받았거나 하반부 핸들러 또는 작업 큐 핸들러가 스케줄되었을 때, 현재 프로세스는 바뀔 수 있는데, 이는 디바이스 드라이버가 특정 프로세스가 실행될 때 독립적으로 다른 한 켠(백그라운드)에서 실행되기 때문 (커널 코드의 특성)
    • 부팅 시 커널에서 사용할 메모리가 할당되고 메모리를 해제하는 루틴을 전달받아, 디바이스 드라이버는 사용할 메모리를 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 구조체를 제거하고 갱신되었음을 표시한 뒤 락을 해제. 대기 중인 프로세스는 다음 스케줄링에서 실행됨

blk_dev 구조체
https://lwn.net/Kernel/LDD2/ch12.lwn
https://lwn.net/Kernel/LDD2/ch12.lwn

 

본 글은 The Linux Kernel 을 정리한 것이며, 출처를 밝히지 않은 모든 이미지는 원글에 속한 것입니다.


인터럽트(Interrupt)

  • 커널은 장치에 요청을 한 뒤 기다리지 않고 다른 작업을 하면서 요청한 작업이 끝나면 장치로부터 인터럽트를 받음
    • 리눅스는 여러 하드웨어 장치를 사용하는데, 동기적으로 구동할 경우 요청을 완료할 때까지 기다려야하는 문제가 있음
  • 인터럽트를 사용할 경우 여러 장치에 동시에 작업을 요청하는 것이 가능
  • 커널이 인터럽트를 처리하는데 일반적인 메커니즘과 인터페이스 표준이 존재하지만 세부 내용은 아키텍처마다 상이함
    • 장치가 인터럽트를 전달하는 것은 하드웨어에서 지원해야 함
  • 보통 CPU의 특정한 핀의 전압이 바뀌면(예를 들어 +5볼트에서 -5볼트), CPU는 하던 일을 멈추고 인터럽트 처리 코드를 실행
    • 어떤 핀은 간격 타이머에 의해 1000분의 1초마다 인터럽트를 받기도 함

https://www.embien.com/blog/interrupt-handling-in-embedded-software/

  • 인터럽트 컨트롤러(Interrupt Controller)
    • 대체로 시스템은 CPU의 인터럽트 핀으로 1:1로 인터럽트를 전달하지 않고, 컨트롤러를 사용하여 장치 인터럽트들을 그룹화
    • 인터럽트 컨트롤러에는 인터럽트를 조정하는 마스크 레지스터와 상태 레지스터가 존재
    • 마스크 레지스터의 비트들을 1 또는 0으로 설정하여 인터럽트를 가능하게 하거나 불가능하게 만들 수 있음
    • 상태 레지스터는 시스템에 현재 발생한 인터럽트를 가짐
    • 시스템의 일부 인터럽트는 하드웨어적으로 연결되어 있는데, 실시간 간격 타이머가 이에 해당되며, 각 시스템은 독자적인 인터럽트 전달 방식을 제공
  • 인터럽트 모드(Interrupt Mode)
    • 인터럽트가 발생하면 CPU는 지금 실행중인 명령어를 잠시 중단하고 인터럽트 처리 코드(Interrupt Handler)가 있거나 해당 코드로 분기하는 명령어의 메모리 주소로 점프(jump)하는데, 여기서 인터럽트 모드에 진입한다고 함
    • 보통 이 모드에서는 다른 인터럽트가 발생할 수 없으며, 예외로 인터럽트에 우선순위를 매기는 CPU의 경우, 높은 인터럽트가 발생하는 것을 허용
    • 인터럽트 처리 코드는 인터럽트 처리하기 전에 CPU의 수행 상태(즉, CPU의 레지스터 등을 포함한 컨텍스트)를 자신의 스택에 저장하고, 인터럽트를 처리하고 나면 CPU의 수행 상태가 인터럽트 처리 전으로 복구되고 인터럽트는 해제됨
    • 이후 CPU는 인터럽트가 발생하기 전에 수행하던 명령어를 재개
  • 프로그램 가능 인터럽트 컨트롤러(Programmable Interrupt Controller, PIC)
    • ISA 주소 공간에 있는 컨트롤러의 레지스터를 이용해 프로그램할 수 있는 컨트롤러
      • 이 레지스터의 위치는 고정되어 이미 알려진 것
    • 인텔에 기반하지 않은 시스템은 위와 같은 구조적 제약에서 자유로우며, 대개 다른 인터럽트 컨트롤러를 사용
    • PIC는 마스크 레지스터와 상태 레지스터를 포함하는데, 마스크 레지스터는 특정 비트를 1 또는 0으로 설정하여 특정 인터럽트(N번 비트면 N번 인터럽트)의 사용 유무를 결정
    •  마스크 레지스터는 쓸 수만 있으며, 써 놓은 값을 읽어오려면 설정 값을 별도로 복사해야 함
    • 인터럽트 허용 상태의 루틴과 인터럽트 금지 상태의 루틴에서, 마스크 레지스터의 복사본을 변경하고 매번 레지스터에 변경된 마스크 값을 씀
    •  인터럽트가 발생하면 인터럽트 처리 코드는 2개의 인터럽트 상태 레지스터(Interrupt Status Register, ISR)를 읽어서 처리

(출처를 못찾겠..) 2개의 PIC 로 장치를 연결

인터럽트 관련 자료구조

인터럽트 처리 커널 자료구조

  • 디바이스 드라이버들이 시스템의 인터럽트에 대한 제어권을 요청하면서 자료구조를 초기화
  • 각 디바이스 드라이버는 인터럽트를 요청해서 켜거나 끄거나 하는 루틴들을 호출해 각자의 인터럽트 처리 루틴의 주소를 커널 자료구조에 저장(등록 과정)
  • PCI 디바이스 드라이버는 장치가 어떤 인터럽트를 사용하는지 알고 있으나, ISA 디바이스 드라이버는 알 방법이 없으므로 커널은 사용할 인터럽트를 탐사(probe)할 기회를 제공
    • 탐사 과정: 디바이스 드라이버는 장치에 인터럽트를 발생하게 한 다음, 다른 장치에 할당되지 않은 모든 인터럽트를 켜서, 처음에 발생시켰던 장치의 인터럽트가 PIC를 통해 커널로 전달되면, 커널은 ISR을 읽어 디바이스 드라이버에게 값을 알려줌. 만약 그 값이 0이 아니라면 탐사 중에 하나 이상의 인터럽트가 발생한 것을 의미. 탐사 종료 후 다른 장치에 할당되지 않은 인터럽트를 모두 끄기
  • ISA 장치가 사용하는 인터럽트 핀은 대개 장치 위에 있는 점퍼를 사용해 설정하고 디바이스 드라이버는 지정된 값을 사용하지만,  PCI 장치가 사용할 인터럽트는 시스템이 부팅 중 PCI 초기화 과정에서 PCI BIOS나 PCI 서브시스템에 의해 할당됨
  • 인텔 칩을 사용하는 PC에서는 시스템 부팅 시 BIOS 에서 셋업 코드를 실행하나, BIOS가 없는 경우 리눅스 커널이 셋업을 수행
    • PCI 셋업 코드는 각 장치별로 인터럽트 컨트롤러의 핀 번호를 PCI 설정 헤더에 쓰고, 장치가 사용하는 PCI 슬롯 번호와 PCI 인터럽트 핀 번호 및 PCI 인터럽트 전달 구조를 이용하여 인터럽트 핀(또는 IRQ)을 결정
    • 디바이스 드라이버는 셋업 코드가 위 정보를 저장한 인터럽트 라인 항목을 읽어서 인터럽트 제어권을 커널에 요청할 때 사용
  • PCI-PCI 브릿지를 사용할 때와 같이 시스템에 PCI 인터럽트를 일으키는 장치가 많은 경우, PCI 장치는 인터럽트를 공유하여 여러 장치의 인터럽트가 하나의 핀에 발생하게 함
    • 공유 인터럽트를 사용하려면 커널에게 인터럽트 제어권을 요청할 때 해당 디바이스 드라이버가 인터럽트 공유 유무를 전달
    • irq_action 배열에 irqaction 구조체를 여러 개 저장하여 인터럽트를 공유
  • 공유 인터럽트가 발생하면 커널은 그 인터럽트 핀을 사용하는 장치의 모든 인터럽트 핸들러를 호출하므로, 모든 PCI 디바이스 드라이버는 서비스할 인터럽트가 없더라도 무시 등의 동작으로 호출을 대비해야함

인터럽트 처리(Interrupt Handling)

https://www.embien.com/blog/interrupt-handling-in-embedded-software/

  • 리눅스에서 인터럽트 처리 서브시스템은 인터럽트를 올바른 핸들러로 전달하기 위해 인터럽트 처리 루틴의 주소를 저장하고 있는 구조체에 대한 포인터를 사용
  • 이 루틴들은 해당 디바이스 드라이버에 있는 것이며, 드라이버가 초기화 될 때 각자 사용할 인터럽트의 제어권을 커널에 요청하면서 루틴의 주소를 알려주게 됨
  • 각 irqaction 구조체는 인터럽트 처리 루틴의 주소를 포함하는데, 핸들러에 필요한 정보도 가짐
    • irq_action 배열의 크기는 인터럽트가 발생하는 장치의 수
  • 인터럽트의 수와 이들이 처리되는 방법은 아키텍처마다 다르기 때문에 리눅스의 인터럽트 핸들러는 아키텍처에 종속적
  • 인터럽트 처리 과정
    • 커널은 시스템에 있는 PIC의 인터럽트 상태 레지스터(ISR)을 읽어 어느 장치가 인터럽트가 발생했는지 파악하고 irq_action 배열의 오프셋을 계산
    • irqaction 구조체에 접근하여 저장된 핸들러가 없다면 커널은 오류를 기록하고, 핸들러가 있다면 이 인터럽트가 발생하는 모든 장치에 대해 irqaction 구조체에 있는 핸들러를 호출
    • 디바이스 드라이버는 인터럽트 원인(오류/요청 작업 완료)을 파악하기 위해 해당 장치의 상태 레지스터를 읽어 인터럽트를 처리
  • 디바이스 드라이버는 인터럽트를 처리할 때 작업량이 많을 수 있는데, 커널에서는 CPU가 오랫동안 인터럽트 모드에 있는 것을 피하기 위해 작업을 인터럽트 처리 후로 미루는 메커니즘을 지원
  • 예를 들어, 터미널에 출력하기 위해 인터럽트가 발생(어셈블리어 명령 int가 인터럽트 처리 루틴을 호출)

https://stackoverflow.com/questions/29656136/is-there-a-system-call-service-routine-in-the-interrupt-vector

 

+ Recent posts