본문 바로가기

computer security/RISC-V

Chapter 5. Interrupts and device drivers

장치 드라이버는 특정 하드웨어 장치를 관리하는 운영체제의 코드이다.

장치를 설정하고, I/O 작업을 시작하며, 작업 완료 시 발생하는 인터럽트를 처리하고, 장치의 I/O를 기다리는 프로세스와 상호작용한다.

드라이버는 하드웨어와 병렬적으로 실행되기 때문에 동기화가 중요하다.

장치에서 인터럽트가 발생하면, 커널은 드라이버의 interrupt handler를 호출한다.

대부분의 드라이버는 두 가지 context에서 실행된다:

1) Top half : 시스템 호출(read, write 등)을 통해 실행되며, 장치에 I/O 작업을 요청한다.

2) Bottom half : 장치에서 작업이 완료되어 interrupt가 발생했을 때 실행되며, 대기 중인 프로세스를 깨우고 다음 작업을 준비한다.

 

 

Code : Console input

1. 콘솔 입력 드라이버

x86의 콘솔 드라이버는 드라이버 구조를 이해하기 위한 간단한 예시이다.

이 드라이버는 사용자가 키보드로 입력한 문자를 UART(유니버셜 비동기 수신 송신기)를 통해 처리한다.

 

하드웨어 세부 사항 : 

- 콘솔 드라이버는 QEMU가 에뮬레이션하는 16650 UART 칩과 상호작용한다.

- UART는 메모리 맵 I/O로 접근하며, 관련 레지스터는 0x10000000(즉, UART0)에서 시작한다.

- 주요 UART 레지스터 : LSR(Line Status Register, 입력 문자가 대기중인지 나타냄), RHR(Receiver Holding Register, 수신된  문자 저장), THR((Transmitter Holding Register, 전송할 문자 저장)   

 

2. 초기화 : consoleinit(kernel/console.c)은 UART를 설정하여 입력 문자를 수신했을 때, 문자 전송이 완료되었을 때 인터럽트를 발생시키도록 만든다.

 

3. 데이터 흐름

1) 사용자 입력

사용자가 키보드를 통해 입력하면, QEMU는 해당 키 입력은 에뮬레이트된 UART를 통해 xv6로 전달한다.

UART는 interrupt를 발생시키며, 이는 devintr(kernel/trap.c)에서 처리된다.

devintr는 PLIC(Platform-Level Interrupt Controller)을 통해 어떤 장치가 인터럽트를 발생시켰는지 확인한 후, UART가 원인이라면 uartintr를 호출한다.

2) 인터럽트 처리

uartintr(kernel/uart.c)은 UART에서 입력된 문자를 읽고 이를 consoleintr(kernel/console.c)에 전달한다.

consoleintr는 입력된 문자를 버퍼(cons.buf)에 저장하며, 줄바꿈 문자(\n)가 입력되면 이를 한 줄로 처리한다.

이후, 대기 중인 프로세스(ex. shell)를 깨운다.

3) 입력 읽기

쉘은 파일 디스크립터를 통해 read system call을 사용하여 입력을 읽는다.

read 호출은 커널의 consoleread(kernel/console.c)로 전달된다.

consoleread는 sleep을 통해 입력이 올 때까지 대기하며, 완전한 줄이 입력되면 이를 사용자 공간으로 복사하여 반환한다.

 

 

Code : Console output

1. 콘솔에 출력하는 과정

1) write system call -> uartputc로 전달

프로세스가 파일 디스크립터를 통해 콘솔에 출력하기 위해 write system call을 하면, 이 호출은 결국 uartputc에 도달한다. 

uartputc는 UART 장치로 데이터를 보내는 역할을 한다.

2) 출력 버퍼에 데이터 저장

uarputc는 출력 데이터를 즉시 UART로 보내는 대신, uart_tx_buf라는 출력 버퍼에 데이터를 추가한다.

그런 다음, uartstart를 호출하여 UART 장치가 전송을 시작하도록 한다.

이 방식 덕분에 프로세스는 UART가 전송을 끝낼 때까지 기다릴 필요 없이 곧바로 반환되어 다음 작업을 계속할 수 있다.

다만, 버퍼가 가득 찬 경우에만 uarputc는 UART가 데이터를 전송할 때까지 대기하게 된다.

3) UART가 바이트 전송 완료 -> 인터럽트 발생

UART가 한 바이트 전송을 완료할 때마다 인터럽트를 발생시킨다.

커널은 인터럽트를 처리하기 위해 uartintr(UART 인터럽트 핸들러)를 호출한다.

uartintr는 다시 uartstart를 호출하여 버퍼에서 다음 바이트를 꺼내 UART로 보낸다.

따라서, 첫 번째 바이트는 uartputc에서 직접 전송되고, 나머지 바이트는 UART의 인터럽트가 발생할 때마다 uartstart가 처리하여 전송된다.

 

2. 버퍼링과 인터럽트를 통한 동작 분리

입력과 출력의 분리 : 콘솔 드라이버는 프로세스의 입력/출력 활동과 장치의 실제 동작을 버퍼링과 인터럽트를 통해 분리한다.

입력 처리 : 프로세스가 입력을 기다리지 않아도 콘솔 드라이버는 UART로부터 입력을 받아 버퍼에 저장하고, 나중에 프로세스가 read 호출을 통해 읽을 수 있다.

출력 처리 : 프로세스가 출력할 때 UART가 전송을 마칠 때까지 기다릴 필요 없이 출력 버퍼에 데이터를 추가하고 곧바로 반환된다. UART는 인터럽트를 통해 버퍼에 남아 있는 데이터를 순차적으로 전송한다. 

이러한 I/O concurrency는 장치가 느리거나 즉각적인 처리가 필요한 경우 성능을 향상시키는 중요한 기법이다.

 

Concurrency in drivers

1. 동시성 문제

드라이버는 여러 프로세스와 하드웨어가 동시에 접근할 수 있기 때문에 동시성 문제가 발생할 수 있다.

1) 두 프로세스가 서로 다른. CPU에서 동시에 consoleread를 호출하는 경우 : 두 프로세스가 동시에 콘솔에서 입력을 읽으려고 할 때, 버퍼 관리가 꼬일 위험이 있다.

2) consoleread 실행 중에 UART 인터럽트가 발생하는 경우 : 인터럽트 핸들러가 버퍼에 데이터를 추가하려고 하면서 race condition이 발생할 수 있다.

3) 다른 CPU에서 UART 인터럽트가 발생하는 경우 : 동기화 문제가 발생할 수 있다.

 

2. lock을 사용한 동기화

이러한 동시성 문제를 해결하기 위해 acquire와 같은 lock을 사용하여 공유 데이터(버퍼)에 대한 접근을 보호한다.

consoleread와 consoleintr에서 락을 사용하여 데이터를 안전하게 처리하도록 한다. 

 

3. 인터럽트 핸들러의 제한

인터럽트 어느 프로세스가 실행 중이든 상관없이 발생할 수 있기 때문에, 인터럽트 핸들러는 현재 실행 중인 프로세스에 의존하지 않고 작동해야 한다.

예를 들어 인터럽트 핸들러가 copyout과 같이 현재 프로세스의 페이지 테이블에 의존하는 작업을 수행하는 것은 안전하지 않다.

따라서, 인터럽트 핸들러는 최소한의 작업만 수행하고, 나머지 작업은 상위 코드가 처리하도록 설계되어 있다.

 

 

Timer Interrupts

1. 타이머 인터럽트의 역할

xv6는 타이머 인터럽트를 사용하여 시스템의 clock을 유지하고, 프로세스 간 CPU 스케줄링을 수행한다.

타이머 인터럽트는 RISC-V CPU에 연결된 clock 하드웨어에 의해 주기적으로 발생한다.

xv6는 각 CPU에 연결된 clock 하드웨어를 설정하여 주기적으로 인터럽트를 발생시키도록 한다.

 

2. RISC-V에서의 타이머 인터럽트 처리

RISC-V는 타이머 인터럽트를 머신 모드에서만 처리하도록 요구한다. 

머신모드는 페이징 없이 실행되며 별도의 제어 레지스터 세트를 사용하므로, 일반적인 xv6 커널 코드를 실행하기 적합하지 않다.

이 때문에 xv6는 타이머 인터럽트를 trap 메커니즘과는 별도로 처리한다.

 

3. 타이머 인터럽트 초기 설정

start.c에서 xv6는 타이머 인터럽트를 받을 수 있도록 초기 설정을 수행한다.

- CLINT(Core-Local Interruptor) 하드웨어를 설정하여 일정 시간 후 인터럽트를 발생시키도록 한다.

- scratch 영역을 설정하여, 레지스터를 저장하고 CLINT 레지스터의 주소를 관리한다.

- mtvec 레지스터를 설정하여 타이머 인터럽트가 발생할 때 timervec을 호출하도록 한다.

 

4. 타이머 인터럽트 처리 흐름

1) 타이머 인터럽트가 발생하면 RISC-V는 timervec을 호출한다.

2) timervec는 몇 개의 레지스터를 scratch 영역에 저장하고, CLINT가 다음 인터럽트가 발생할 시점을 설정한 후, RISC-V에 소프트웨어 인터럽트를 요청한다.

3) RISC-V는 소프트웨어 인터럽트를 커널에 전달하며, 커널은 일반적인 트랩 메커니즘을 통해 이를 처리한다.

4) devintr에서 소프트웨어 인터럽트를 처리하고, yield를 호출하여 스케줄러가 프로세스를 전환할 수 있도록 한다.

 

 

Real World

1. 타이머 인터럽트와 장치 인터럽트 처리

xv6는 사용자 모드뿐만 아니라 커널 모드에서도 타이머 인터럽트와 장치 인터럽트를 허용한다.

타이머 인터럽트는 커널 코드가 실행 중이더라도 발생할 수 있으며, 인터럽트가 발생하면 yield를 호출하여 커널 스레드 간에도 스케줄링이 이루어진다.

이는 커널 코드가 오랜 시간 동안 실행될 때 CPU가 특정 스레드에 독정되지 않도록 하는 데 유용하다.

하지만 이로 인해 커널 코드가 타이머 인터럽트에 의해 중단되었다가 다른 CPU에서 재개될 수 있도록, 동시성 문제에 대한 고려가 필요하다.

 

2. 장치 드라이버 구현의 복잡성

실제 운영체제에서 모든 장치를 지원하려면 많은 양의 드라이버 코드가 필요하다.

각 장치는 다양한 기능을 제공하며, 장치와 드라이버 간의 프로토콜이 복잡하거나 문서화가 제대로 되어 있지 않은 경우가 많다.

일반적으로 운영체제의 드라이버 코드가 커널 코어 코드보다 훨씬 많다.

 

3. 프로그래밍된 I/O

xv6의 UART 드라이버는 프로그래밍된 I/O 방식을 사용하여 UART 제어 레지스터를 통해 한 번에 한 바이트씩 데이터를 읽고 쓴다.

프로그래밍된 I/O는 단순하지만, 데이터 전송 속도가 느린 장치에만 적합하다. 빠른 속도로 많은 데이터를 전송해야 하는 장치(ex. 디스크, 네트워크)에는 적합하지 않다.

 

4. DMA(Direct Memory Access)

고속 데이터 전송이 필요한 장치는 DMA(직접 메모리 접근) 방식을 사용한다.

- DMA는 장치가 CPU의 개입 없이 RAM에 직접 데이터를 읽고 쓰는 방식이다.

- 드라이버는 데이터를 RAM에 준비한 후, 제어 레지스터에 명령을 써서 장치가 준비된 데이터를 처리하도록 지시한다.

현대의 디스크 및 네트워크 장치는 대부분 DMA 방식을 사용한다.

 

5. 인터럽트와 폴링(Polling)

인터럽트는 장치가 예측할 수 없는 시점에 주기적으로 CPU의 주의를 필요로 할 때 적합하다.

- 하지만 인터럽트는 CPU 오버헤드가 크기 때문에, 고속 장치에서는 과도한 인터럽트를 피하기 위한 기법이 필요하다.

고속 장치에서 사용하는 기법

1) 배치처리(batch processing) : 여러 요청을 한꺼번에 처리한 후, 단일 인터럽트를 발생시킨다.

2) 폴링(polling) : 드라이버가 주기적으로 장치를 확인하여 처리할 작업이 있는지 확인하는 방식이다. 장치가 매우 빠르게 동작할 때 적합하지만, 장치가 주로 유휴 상태일 때는 CPU 시간을 낭비하게 된다.

3) 동적 전환 : 장치의 부하에 따라 폴링과 인터럽트 방식을 동적으로 전환하는 방식도 사용된다.

 

6. 데이터 복사 문제

xv6의 UART 드라이버는 데이터를 커널 버퍼에 한 번 복사한 후, 사용자 공간으로 다시 복사한다.

이런 방식은 저속 장치에는 적합하지만, 고속 장치에서는 성능을 저하시킬 수 있다.

일부 운영체제는 DMA를 활용하여 데이터를 사용자 공간 버퍼와 장치 간에 직접 전송하여 이러한 문제를 해결한다.

 

7. ioctl 시스템 호출

표준 파일 시스템 호출(read, write) 만으로는 모든 장치 제어 기능을 처리할 수 없다.

예를 들어, 콘솔 드라이버에서 라인 버퍼링 설정을 제어하려면 추가적인 명령이 필요하다.

Unix 계열 운영체제에서는 이러한 장치 제어를 위해 ioctl 시스템 호출을 제공한다.

 

8. 실시간 시스템과 xv6

일부 시스템은 정해진 시간 내에 반드시 응답해야 하는 실시간 특성이 필요하다.

예를 들어, 안전과 관련된 시스템에서는 일정 시간 내에 응답하지 못하면 치명적인 문제가 발생할 수 있다.

xv6는 하드 실시간 시스템에 적합하지 않다. 하드 실시간 운영체제는 일정 시간 내에 응답할 수 있도록 최악의 응답 시간을 분석할 수 있어야 하지만, xv6는 이런 구조를 갖추고 있지 않다.

xv6는 또한 소프트 실시간 시스템에도 적합하지 않다. xv6의 스케줄러가 단순하고, 인터럽트를 오랜 시간 비활성화하는 커널 코드가 존재하기 때문이다. 

 

 

'computer security > RISC-V' 카테고리의 다른 글

chapter 7. Scheduling  (8) 2025.01.16
Chapter 6. Locking  (7) 2025.01.14
Chapter 4. Traps and system calls  (4) 2025.01.09
Chapter 3. Page tables  (6) 2025.01.07
chapter 2. Operating system organization  (7) 2025.01.06