11주차의 학습 복기

드라이버 작성을 위한 커널 모듈 탑재 그리고 각 장치를 어떻게 리눅스에서 인식하고 사용하는지를 알아보았습니다 .

12주차에는 드라이버의 커널에서 인터럽트를 어떻게 처리하는지와 여러 가지 장치에서의 드라이버 구현 방법에 대해서 알아보았습니다.

그렇다면. 복습도 할 겸 기록을 시작해보도록 하겠습니다!!! 😍

커널 서비스

인터럽트 처리

인터럽트의 처리와 금지는 ISR에서 이루어집니다 .
request_irq() 에서 플래그를 통해 CPU 전체 및 일부에서 인터럽트를 허용하거나 금지할 수 있습니다.

인터럽트의 처리 방식에는 두가지가 있습니다 .

  • TOP Half : 즉각 대응
    • 하드웨어 레지스터의 데이터를 읽거나 인터럽트 완료 신호를 보냄
    • 다른 인터럽트 마스킹 가능
  • Bottom Half : 지연 처리
    • 인터럽트가 허용된 상태에서 실행되므로 시스템의 반응성을 해치지 않음
      "deferred interrupt handling" 
      리눅스 커널에서 인터럽트를 효율적으로 처리하기 위해서 인터럽트 처리 로직을 
      즉시 실행해야하는 짧은 코드와 나중에 실행해도 되는 무거운 코드로 구분  
    • 지연 처리를 위한 3가지 방식
구분SoftirqTaskletWorkqueue
실행 컨텍스트인터럽트 컨텍스트인터럽트 컨텍스트프로세스 컨텍스트
병렬 처리여러 CPU에서 동시 실행 가능동일 Tasklet은 한 CPU만 실행커널 스레드에 의해 병렬 실행
Sleep(잠자기)불가능불가능가능 (Blocking OK)
용도네트워크 블록 디바이스 (고성능)일반적인 드라이버 작업입출력 등 시간이 긴 작업
특징- 정적 생성
-성능이 우수
-커널 소스 수정 요구
동적 생성우리가 필요한 BH는 프로세스적인 특징들이 필요하다

리눅스 커널의 모든 디바이스 드라이버가 하나의 인터럽트 서비스 루틴 내에서 작업을 둘로 쪼개어 진행합니다. 그러면 예시를 통해 Top Half와 Bottom Half를 이해해봅시다 !

NIC(네트워크 인터페이스 카드 )

  • TH(Top Half): ACK 패킷 데이터를 메모리 버퍼로 빠르게 복사 → 이 때는 인터럽트를 금지시킨 상태에서 빠르게 끝낸다
  • BH(Bottom half) : 패킷을 어디로 보낼지 (ip 주소 확인 , 체크썸 계산 등 )은 인터럽트 허용 상태에서 실행 → 지연 처리

workqueue

- 커널 스레드를 생성하여 BH를 구현한다 .
- 프로세스 컨텍스트의 특징을 모두 닺는다
    - 선점 가능
    - 인터럽트 허용
    - 많은 메모리 할당 , 세마포어 획득 , 블로킹 I/O

그렇다면 Workqueue를 사용하여 커널 서비스에서 인터럽트를 어떻게 처리하는지 예제를 보며 확인해보도록 하겠습니다 .

리눅스에는 KWorker라는 일꾼들이 존재합니다 .
이는 지연 처리를 담당하는 일꾼 커널 스레드들로 워크큐에 등록된 task를 하나씩 꺼내어 비동기적으로 처리하는 역할을 담당합니다. 이를 통해 인터럽트 핸들러나 기타 주요 커널 스레드가 오랜 시간 블록되는 것을 방지하고 시스템 응답성을 높일 수 있습니다.

이를 구현하기 위해서는 Global, Custom 의 두가지 워크큐를 활용할 수 있습니다 .

  • 글로벌 큐
    • linux/workqueue.h
      ⇒ 헤더 파일에 정의된 다양한 함수를 통해서 선언, 초기화 및 스케쥴링할 수 있습니다 .
  • Custom workqueue
    • 이는 다양한 옵션을 추가하여 task를 더욱 유연하게 처리하기 위해서 사용됩니다.
    • 생성 및 삭제
      • create_workqueue (const char * name )
      • destroy_workqueue (const char * name )
    • alloc_ workqueue
      • WQ_UNBOUND : 특정 CPU에 종속되지 않고, 스케줄러가 비어 있는 CPU를 찾아 유연하게 할당
      • WQ_FREEABLE : 시스템이 Suspend(절전 모드) 상태로 진입할 때, 해당 큐에 쌓인 작업들을 일시 중단(Freeze)
      • WQ_HIGPRI : 높은 우선순위를 부여합니다. 일반 워크큐보다 먼저 실행되어야 하는 긴급한 지연 처리
      • WQ_MEM_RECLAIM : 메모리 할당이 부족한 상황(OOM 등)에서도 워커 스레드가 반드시 실행되도록 보장 → 메모리 해제 필수

Sample

#include <linux/workqueue.h>

// 1. 실행될 핸들러 함수 정의 (함수명이 매크로보다 먼저 정의되거나 선언되어야 함)
void my_work_handler(struct work_struct *work) {
    // Bottom Half 로직 수행
    pr_info("Custom Workqueue is processing...\n");
}

// 2. DECLARE_WORK 매크로로 선언 및 초기화 (동시에 수행)
// 구조체 변수명(my_work)과 실행할 핸들러(my_work_handler)를 인자로 전달합니다.
DECLARE_WORK(my_work, my_work_handler);

// 3. 워크큐 포인터는 그대로 유지 (워크큐 생성은 런타임에 해야 함)
struct workqueue_struct *my_wq;

/* --- 모듈 초기화 함수 내에서 --- */
int __init my_module_init(void) {
    // 워크큐 생성
    my_wq = alloc_workqueue("my_custom_queue", WQ_HIGPRI | WQ_UNBOUND, 0);
    
    if (!my_wq)
        return -ENOMEM;

    return 0;
}

해당 이미지는 workqueue를 이용한 라즈베리 파이에서 인터럽트 처리를 도식화 한 내용입니다 .

  1. 인터럽트 핸들러
    GPIO Falling edge 감지 → 자동 호출 매우 엄격
    [인터럽트 컨텍스트]
  1. isr_func()
    • printk("(isr) keypad was pressed \n")
    schedule_work() 호출
    • IRQ_HANDLED 반환 매우 짧아야 함 (~수십 μs)
  1. schedule_work()
    인터럽트 → workqueue로 전달
    • work_struct(&gdetect)를 시스템 workqueue에 등록
    • 나중에 여유 있을 때 실행 , 예약 매우 빠름

  2. dev_callback()
    • 실제 "느린 작업" 수행 가능
    • [process context]


Threaded irq

  • IRQ 처리를 irq 핸들러와 kernel 스레드로 나누어 시간차로 처리하는 방식
  • IRQ 핸들러의 실행 시간을 줄이고 시스템 응답성 향상
  • 작동 방식
    • request_threaded_irq() 함수 사용
  • 장점
    • 높은 응답성
    • 코드 작성이 용이
    • Irq 공유 용이성
    • 스케줄링 우선순위를 세밀하게 조정 가능
  • 단점 : 컨텍스트 스위칭 오버헤드
    Sample code
ret = devm_request_threaded_irq(&pdev->dev,         
        key_dev->irq,   // irq no.            
        isr_func,           // top-half            
        isr_func_bh,        // bot-half                        
        IRQF_TRIGGER_FALLING,   // falling trigger              
        "gpio_key_irq",            
        key_dev);                
if (ret) {        
		dev_err(&pdev->dev, "Failed to request IRQ\n");        
		return ret;    
}
platform_set_drvdata(pdev, key_dev);
dev_info(&pdev->dev, "GPIO KEY driver probed\n");

-> "이 인터럽트 처리가 너무 길어서 아예 전용 스레드를 하나 만들어서 처리하겠다"는 인터럽트 중심의 사고방식으로 복잡한 I2C/SPI 통신이 필요한 센서 인터럽트에 주로 사용됩니다.


하드웨어 통신

메모리 매핑과 데이터 전송

리눅스 커널에서는 유저(User)와 커널(Kernel)의 메모리 공간이 분리되어 있으며 포인터를 직접 전달해 읽고 쓸 수 없고, 전용 함수를 사용해야 합니다.

핵심 함수

  • access_ok : 사용자 메모리 공간의 유효성 검사
  • copy_to_user : 커널 메모리 블록의 데이터를 사용자 메모리 블록 데이터에 쓰기
  • copy_from_user : 시용자 메모리 블록 데이터를 커널 메모리 블록에 쓰기
  • get_user: 사용자 공간의 데이터 읽기 . 사이즈는 변수의 바이트 수 만큼(자동)
  • put_user(x,ptr) : 커널 변수의 값을 사용자 공간에 쓰기

ARM 아키텍처(라즈베리 파이 등)는 Memory Mapped I/O 방식을 사용합니다. 장치의 레지스터(SFR)가 일반 시스템 RAM처럼 주소 공간에 배치되어 있어, 특정 주소에 값을 쓰고 읽음으로써 하드웨어를 제어하고 통신할 수 있습니다.

이 때 하드웨어의 실제 주소(Physical Address)에 접근하려면, 포인터로 접근기 보다는 프로세스가 이해할 수 있는 가상 주소(Virtual Address)로 매핑해야 합니다 .

커널 영역에서의 매핑 (ioremap)

디바이스 드라이버 내부에서 하드웨어 레지스터에 접근할 때 사용합니다.

// 예시: 라즈베리 파이 4 GPIO 베이스 주소 매핑
#define GPIO_BASE_PHYS 0xFE200000
#define GPIO_SIZE      0x100

static void __iomem *gpio_base;
gpio_base = ioremap(GPIO_BASE_PHYS, GPIO_SIZE); // 물리 주소를 커널 가상 주소로 매핑

set_reg = gpio_base + GPIO_CLR_OFFSET;	// LED turn ONN

writel(1 << gpio, set_reg); ====> 이처럼 base 주소에 값을 writel를 통해 제어가 가능하다. 

// 매핑 해제
iounmap(gpio_base);

유저 영역에서의 매핑 (mmap)

/dev/mem 활용: 유저 공간에서 커널 도움 없이(직접적으로는 아니지만) 물리 주소에 접근할 수 있게 해주는 특수 파일입니다.

작동 원리: mmap() 호출 시 커널은 vm_area_struct를 통해 가상 메모리 영역(VMA)을 할당하고, 물리 주소와 연결합니다.

    /* GPIO와 mmap */
    gpio_map = mmap(NULL,  /* 0 -> 커널이 알아서 할당해서 반환*/ 
    GPIO_SIZE,   //매핑 영역의 크기 지정  page_size 단위 
    PROT_READ | PROT_WRITE, 
    MAP_SHARED, 
    mem_fd, 
    GPIO_BASE);
    if (gpio_map == MAP_FAILED) {
        printf("[Error] mmap() : %d\n", (int)gpio_map);
        perror -1;
    }
    gpio = (volatile unsigned *)gpio_map; /* 메모리 맵에 대한 포인터 */
    GPIO_OUT(gno); /* 해당 GPIO 핀을 출력으로 설정 */

    GPIO_SET(gno); /* 해당 GPIO 핀에 값 설정 */
  
    munmap(gpio_map, GPIO_SIZE); /* 앞에서 mmap 부분 해제 */


하드웨어 통신 예제

SSD1306 LCD 제어

I2C통신을 활용하여 간단한 라즈베리 파이 모니터 출력 예제를 진행했습니다.
우선 SSD1306의 내부 동작 원리는 해당 블로그에 기록해놓았으니 여기서는 실습 과정 위주로 기록하겠습니다.

 $ tail -c +63 ./<image_name>.bmp > image.bin
	 #헤더 파일을 제외하고 실제 픽셀 값만 추출하기 위해서 사용 
 $ cat <src_code>.c 
 .
 .
 .
 unsigned char reverse_bits(unsigned char byte) {     
	 //바이트를 받아서 endian 변환 후 반환    
	 byte = ((byte & 0xF0) >> 4) | ((byte & 0x0F) << 4);    
	 byte = ((byte & 0xCC) >> 2) | ((byte & 0x33) << 2);    
	 byte = ((byte & 0xAA) >> 1) | ((byte & 0x55) << 1);    
	 return byte;
 }

💡비트 반전을 하는 이유

① 비트 순서(MSB vs LSB)의 차이

  • BMP 파일: 보통 한 바이트 안에서 가장 왼쪽 비트가 이미지의 가장 왼쪽 픽셀
  • SSD1306 OLED: 메모리 구조상 비트가 거꾸로(가장 오른쪽 비트가 첫 번째 픽셀) 인식

② 픽셀 데이터의 상하 반전

  • BMP 파일의 특징: 비트맵 파일은 역사적으로 데이터를 저장할 때 아래쪽 줄부터 위쪽 줄 순서 로 거꾸로 저장
  • OLED: 화면의 왼쪽 위부터 아래쪽 순서로 데이터를 뿌려야 함 ⇒ Byte Swapping & Bit Reversal

Sample code

         printf("mapped addres :  %p\n",  ptrdata );
 
 //      seek data from mono bmp file
         lseek(img, BMP_MONO_DATA_OFFSET, SEEK_SET );    // from offset 62, pixel data begins...
 
         read (img, buf, SSD1306_PIX_BYTECOUNT  );
 
         for(i=0;i<SSD1306_PIX_BYTECOUNT ; i++)
                 buf[i] = change_endian( buf[i] ); // .bmp file has different byte order with SSD1306 so flip them all.
 
         write(dev, buf , SSD1306_PIX_BYTECOUNT );
 
         munmap(ptrdata, MMAP_SIZE);
 
         close(dev);
         close(img);
 
         return 0;
 }

result

  root@rpi:~/../.# ./app ./<image_name>.bmp
  mapped addres :  0x7fa7c52000 
==> 프로세스의 가상 메모리 공간에 할당된 시작 주소 

Bitmap 이미지 출력 예제


작업의 딜레이

HZ : 1초당 발생하는 jiffies ⇒ 100일 때 1초에 100번 Tick 발생

jiffies : 1Tick당 소요되는 시간

Tick : 시스템 타이머 인터럽트가 발생하는 최소 시간 단위 : Tick(sec)=1/HZ

Tickless Kernel (NO_HZ) : 최근의 리눅스 커널로 할 일이 없을 때는 타이머 인터럽트를 멈추고 예약된 작업이 있을 때만 깨어나는 방식

┌─────────────────────────────────────────────────────────────┐
│              하드웨어 타이머 인터럽트 (주기적)                 
│                  (HZ = 250Hz → 4ms마다)                 
├─────────────────────────────────────────────────────────────┤
│ 1. 하드웨어 타이머 만료 → IRQ 발생                          
│    ↓                                                    
│ 2. do_timer_irq() 또는 timer_interrupt() 호출              
│    ↓                                                     
│ 3. update_jiffies() 또는 jiffies_64 업데이트                
│    ↓                                                        
│ 4. jiffies 변수 원자적 증가:                                
│         jiffies = jiffies + 1                               
│    ↓                                                        
│ 5. 관련 구조체 업데이트:                                    
│    - xtime (wall clock 시간)                                
│    - tick_sched (next tick 정보)                            
│    - timekeeper (ntp, skew 보정)                            
│    ↓                                                        
│ 6. 타이머 휠 회전 & 재정렬                                  
│    ↓                                                        
│ 7. timer softirq 예약 → ksoftirqd 실행                      
│    ↓                                                        
│ 8. 하드웨어 타이머 다음 값으로 리로드 (4ms 후)              
└─────────────────────────────────────────────────────────────┘
  • 단기 지연 : 하드웨어 동기화를 맞추기 위해 사용
    • mdelay() : ms

    • udelay() : us

    • ndelay() : ns(나노초)

      ⇒ busy waiting이라 과한 시간 사용은 지양

High Resolution Timer

틱 단위 보다 세밀한 제어가 필요 + 다음 틱 간의 오버 헤드 발생

Sample code

.
#include <linux/hrtimer.h>
#include <linux/ktime.h>

static struct hrtimer my_hrtimer;
static ktime_t kt_period;

// static enum hrtimer_NOrestart // => 이것도 존재 
// 타이머 만료 시 호출되는 콜백 함수
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
    // [ 정밀 작업 수행 ]
    pr_info("hrtimer Callback at jiffies %lu\n", jiffies);

    // 주기적인 반복을 위해 다음 만료 시간 설정
    hrtimer_forward_now(timer, kt_period);
    
    return HRTIMER_RESTART; // 타이머 재시작
}
static int __init hrtimer_example_init(void)
{
    // 100 마이크로초 단위 설정 (0초, 100,000나노초)
    kt_period = ktime_set(0, 100000); 

    // 타이머 초기화: 모노토닉 클럭(절대 시간), 상대 시간 모드
    hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    my_hrtimer.function = &my_hrtimer_callback;
    // 타이머 시작
    hrtimer_start(&my_hrtimer, kt_period, HRTIMER_MODE_REL);
    return 0;
}

디바이스 드라이버 모델

이전까지의 디바이스 드라이버는 커널 모듈 로드를 통한 간단한 예제였습니다.
그렇다면 디바이스 드라이버 모델과 각 장치에 따른 드라이버 구현에 대해서 알아보겠습니다.

드라이버 모델의 필요성

  • 복잡해지는 device topology [ 계층적 연결 지도 : ex) 계층적 허브 구조 ]
  • 기능 추가 필요성
    • 전원 관리 : 시스템에 연결된 장치 연결 탐색 기능
    • 유저 영역 접근
    • 핫 플러그인 : 플러그 앤 플레이
    • 디바이스 클래스 : 연결 방식에 상관 없이 각 디바이스를 기능적인 계층으로 구분

장점

시스템 전체 디바이스들의 연결 트리 구조를 파악 가능 
+ 계층적 전원 관리 가능 

드라이버 모델의 구성 요소

  • Bus (버스): 하드웨어 장치와 드라이버가 만나는 통로

    • acme-bus는 자신에게 연결된 devices와 이를 제어할 drivers 목록을 관리
  • Driver (드라이버):장치를 어떻게 구동할지 알고 있는 소프트웨어

    • acme-keypad-driver는 버스에 등록되어 자신과 매칭 되는 장치를 기다린다
  • Device (장치):실제 하드웨어 인스턴스

    • acme-keypad-device가 버스에 등록되면, 커널은 해당 버스의 드라이버 목록을 뒤져 적절한 드라이버를 찾아 매칭(Binding)
  • Class (클래스):장치를 기능 단위로 묶은 추상화 계층

    • 버스 종류에 상관없이 "키패드"라는 기능을 수행하는 장치들을 acme-keypad-class에서 관리한다.

kobject vs kset vs subsystem

구분kobjectksetsubsystem
무엇인가가장 기본 객체 (하나의 디렉토리)kobject들의 집합 (디렉토리 + 목록)kset + 등록/제거 로직을 가진 큰 단위
주요 역할sysfs에 디렉토리 하나 만듦같은 종류의 kobject들을 관리·정렬전체 서브시스템(예: block, net)을 관리
kset 포함포함 안 함자기 자신도 kobject임내부에 kset을 가짐
대표 예시하나의 디바이스, 하나의 드라이버/sys/class/net/ 아래 모든 net_deviceblock_subsys, net_subsys, power_subsys
사용 예struct device, struct kobj_exampleclass kset, bus ksetsubsys_initcall()로 등록되는 큰 단위

Linux kernel : Procfs vs Sysfs vs Simple Module

리눅스 드라이버를 개발할 때, 커널 내부의 데이터를 유저(User Space)에게 어떻게 보여줄 것인가는 매우 중요한 설계 결정입니다. 각 방식의 차이를 비교해보겠습니다.

sysfs 파일 시스템이란?

sysfs는 하드웨어 장치와 드라이버 정보를 사용자 공간으로 보여주기 위한 가상 파일 시스템(Virtual File System)입니다.

  • 구조화: 커널 내부의 복잡한 장치 계층 구조(Device Topology)를 사용자가 읽고 쓸 수 있는 파일과 디렉터리 형태로 시각화하여 제공합니다.

  • 통합 관리: 단순히 정보를 보여주는 것을 넘어, kobject라는 객체 지향적 구조를 통해 장치의 상태를 관리하고 제어하는 통합 인터페이스 역할을 합니다.

세 가지 방식의 결정적 차이

① 단순 커널 모듈 (Simple Module)

  • 특징: printk로 로그만 남길 뿐, 유저가 직접 접근할 통로가 없습니다.

  • 한계: open(), read(), write() 같은 표준 함수를 쓸 수 없어 실시간 제어가 불가능합니다.

② Procfs 방식 (/proc)

  • 특징: 시스템 정보(CPU, 메모리 통계) 전달이 주 목적

  • 장점: 구현이 매우 빠르고 간단 (Major/Minor 번호 등록이 필요 없음)

  • 용도: 디버깅용 데이터 확인, 일회성 테스트 코드 작성 시 유리

③ Sysfs & Dev 방식 (/dev, 표준)

  • 특징: 실제 하드웨어를 추상화한 표준 디바이스 파일을 생성

  • 장점: udev와 통합되어 장치를 꽂으면 자동으로 /dev/에 파일이 생기며, 권한 설정 및 표준 입출력이 완벽히 지원됩니다

    • 디바이스 번호로 인스턴스를 구분하며 여러 인스턴스를 사용할 수 있습니다.
  • 실제 배포되는 프로덕션 환경의 드라이버 개발 시 필수


디바이스 드라이버의 양이 조금 방대해 12주차 -1 을 여기서 마무리 하고 이어서 여러 디바이스의 구현은 12주차-2 에서 작성하도록 하겠습니다. ....😭😭

profile
세상의 어려운 문제를 해결하자

0개의 댓글