수업 정리/임베디드시스템소프트웨어

[embedded system software/12주차] 이론

gyuun365 2026. 5. 22. 22:25

해당 글은 건국대학교 진현욱 교수님의 임베디드 시스템 소프트웨어 수업 내용을 정리한 글입니다. 

지금까지 배운 리눅스 커널 구조이다. 하드웨어에서 인터럽트가 발생하면 인터럽트 핸들러가 이를 처리하고 드라이버와 VFS를 거쳐 앱까지 전달되는 흐름을 보여준다.

1st situation

여러 프로세스가 하나의 장치 (a FIFO queue)를 공유할 때

동작은 가능하다. 하지만 여러 개의 device가 sensor 등을 다같이 쓰기는 어렵다. 

즉 단순히 인터럽트 핸들러만으로는 여러 개의 데이터를 동시에 효율적으로 처리하기 어렵다.

2st situation

process마다 FIFO queue 하나를 만드는 상황이다.

Demultiplexing을 통해 값을 2개에 동시에 다 넣어주면서 기기들이 각각 동작할 수 있게 만든다.

하지만 TCP/IP나 파일 시스템 처럼 복잡한 처리가 필요할 때, 이걸 모두 ISR(인터럽트 핸들러)안에서 처리하면 시스템이 너무 오랫동안 멈춰있게 된다. 그래서 나중에 처리할 부분을 분리해야 할 필요성이 생긴다. 그렇지 않는다면 overhead가 생기고 responsiveness가 낮아진다.

 

Bottom Half

그래서 ISR을 무겁게 하지 않기 위해서 우리는 ISR을 두 부분으로 나누는 방법에 대해서 배웠었다. 급한 일들은 Top Half라 해서 ISR이 즉각 응답하는 최소한의 작업들로 분류하고 그 외 급하지 않은 나머지 무거운 작업들은 인터럽트를 허용한 후 나중에 안전하게 실행하는 Bottom Half라고 했었다.

 

그래서 ISR을 무겁지 않게 해주는 Bottom Half 라는 방법을 우리는 다른 말로 deferrable function이라고 한다.

그래서 그림처럼 interrupt를 실행한 후에 발동이 되며 우선순위도 interrupt 다음으로 높다.

 

근데 이러한 deferrable Functions 은 Linux에서는 3가지가 존재한다. 보통 Bottom Half랑 같은 말로 쓰이지만 Linux에서는 헷갈릴 수 있으니 주의하자

 

  • BH (Bottom Half): 옛날 방식, 동시 실행 불가.
  • Tasklet: 서로 다른 Tasklet은 여러 CPU에서 동시에 돌 수 있지만, 같은 종류는 순차 실행됨.
    • 이게 왜 문제가 되냐면 I/O 성능이 높을 경우 여러 개의 코어가 다같이 해결해야하는데 Tasklet은 무조건 다른 코어에서 같은 종류를 병렬로 돌릴 수가 없기 때문!
  • Softirq: 가장 빠르고 강력함. 같은 종류라도 여러 CPU에서 동시에 병렬 실행 가능(동기화 주의 필요- 사실 3개 다 해야하긴함).

 

이러한 지연 함수는 언제 실행될까요? 시스템 콜 종료 시, 예외 처리 종료 시, 스케줄러 실행 시 등 커널이 "지금 좀 한가한데?" 하는 시점마다 체크해서 실행된다.

Tasklet

struct tasklet_struct 

defined in <linux/interrupt.h>

 

  • next: 여러 개의 Tasklet을 연결 리스트로 관리하기 위한 포인터이다.
  • state: 현재 Tasklet의 상태를 나타내는 비트마스크이다.
    • TASKLET_STATE_SCHED (1): Tasklet이 이미 스케줄링되어 실행 대기 열에 들어가 있음을 의미한다.
    • TASKLET_STATE_RUN (2): 현재 이 Tasklet이 어느 한 CPU에서 실행 중임을 의미한다. (SMP 환경에서 같은 Tasklet이 동시에 다른 CPU에서 중복 실행되는 것을 방지하는 데 사용됩니다.)
  • count: Tasklet의 활성화 여부를 나타내는 레퍼런스 카운터이다. 이 값이 0일 때만 실행 가능하며, 0보다 크면 비활성화(Disabled) 상태로 간주되어 스케줄링되더라도 실행되지 않고 건너뛴다.
  • func: Tasklet이 스케줄링되었을 때 실제로 커널이 호출해 줄 콜백 함수(함수 포인터)이다.
  • data: func 함수가 실행될 때 인자로 넘겨줄 데이터(unsigned long 타입)이다. 임베디드 드라이버 등에서 특정 디바이스 구조체의 주소를 넘길 때 주로 사용한다.

 

Initialization functions

 

  • 정적 선언 (컴파일 타임/매크로)
    • DECLARE_TASKLET(name, func, data): count를 0으로 초기화하여 생성 즉시 활성화 상태가 된다.
    • DECLARE_TASKLET_DISABLED(name, func, data): count를 1로 초기화하여 생성 시에는 비활성화 상태로 둔다. 나중에 tasklet_enable()을 호출해야만 사용할 수 있다.
  • 동적 선언 (런타임):
    • 코드 실행 중에 동적으로 구조체를 할당받았을 경우,
      tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long),unsigned long data)
      함수를 호출하여 초기화한다. 이 함수는 내부적으로 count를 0(활성화)으로 세팅한다.

 

Scheduling function

Tasklet을 실행해 달라고 커널에 요청할 때는 tasklet_schedule(struct tasklet_struct *t) 함수를 사용한다.

 

  • 상태 체크: 먼저 해당 Tasklet의 state에 TASKLET_STATE_SCHED 비트가 켜져 있는지 확인한다.
  • 중복 요청 방지: 불리기 전에 스케줄링이 중복해서 된다고 하더라도 실행은 1번만 된다.
  • 대기열 추가: tasklet 내부에서도 reschedule을 하는 것 이 가능하다.
  • 돌아가는 중에 scheduled 된다면 완료된 후 또 돌아간다.

 

Tasklet Control Functions

 

  • tasklet_disable(): count 값을 1 증가시킨다. 이 함수가 호출된 이후로는 tasklet_schedule()을 하더라도 실제 함수(func)가 호출되지 않고 대기 상태로 유지된다.
  • tasklet_enable(): count 값을 1 감소시킵니다. 만약 값이 0이 되면 Tasklet은 다시 활성화되어 정상적으로 실행 가능한 상태가 됩니다. tasklet_disable() or DECLARE_TASKLET_DISABLED()에 disable 된 tasklet을 실행시킨다.
  • tasklet_kill(): 대기열에 등록된 Tasklet을 제거한다. 만약 Tasklet이 현재 실행 중이라면 끝날 때까지 기다린 후 대기열에서 완전히 뺀다. 주로 모듈이 언로드(Unload)될 때 안전한 제거를 위해 사용하며, 잠들 수(Sleep) 있는 함수이므로 인터럽트 컨텍스트에서는 절대 호출하면 안된다.

 

더보기
struct tasklet_struct my_tasklet;

struct mydata {
    int year;
    int month;
} my_tasklet_data;

void my_function(unsigned long recvData){
    struct mydata *tempData;
    tempData = (struct mydata*) recvData;
    printk("my_function...\n");
    printk(“Year: %d\n", tempData->year);
    printk(“Month: %d\n", tempData->month);
}

static int __init taskletTester_init(void){
    printk("Tasklet example init\n");
    my_tasklet_data.year = 2020;
    my_tasklet_data.month = 6;
    tasklet_init( &my_tasklet, my_function,
    (unsigned long)&my_tasklet_data );
    /* In general, tasklet_schedule() is called by an ISR */
    tasklet_schedule( &my_tasklet );
    return 0;
}

static void __exit taskletTester_exit(void){
    printk("Tasklet example exit\n");
    tasklet_kill( &my_tasklet );
}

Softirq

Softirq는 리눅스 커널 Bottom Half의 가장 밑바닥에 존재하는 고성능 지연 처리 메커니즘이다. Tasklet도 사실 이 Softirq 위에서 돌아가는 하나의 종류에 불과하다.

정적 결정: Softirq는 코드가 실행되는 도중에 동적으로 생성할 수 없다. 컴파일 타임에 커널 소스(include/linux/interrupt.h)의 enum 상수로 정의된 고정된 개수(보통 10개 내외)만 존재한다.

 

  • HI_SOFTIRQ: 높은 우선순위의 Tasklet 처리용
  • TIMER_SOFTIRQ: 커널 타이머 처리용
  • NET_TX_SOFTIRQ / NET_RX_SOFTIRQ: 네트워크 패킷 송수신용 (가장 고성능이 필요한 영역)
  • BLOCK_SOFTIRQ: 블록 디바이스(스토리지) I/O 처리용
  • TASKLET_SOFTIRQ: 일반 우선순위의 Tasklet 처리용

뭐 이런 것들이 있다.

 

 

Softirq Initialization

등록 (open_softirq): 지정된 Softirq 번호에 실행할 핸들러 함수를 매핑한다. 커널 초기화 단계에서 딱 한 번 실행된다.

  • 예: open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    • 뒤에는 function pointer가 온다.

Scheduling Softirq

호출 (raise_softirq): 하드웨어 인터럽트 핸들러 내에서 지연 처리가 필요할 때, 해당 Softirq를 활성화해 달라고 요청한다. 커널은 내부적으로 해당 CPU의 비트마스크 변수(local_softirq_pending)의 해당 비트를 1로 세팅한다.

더보기
enum{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,
    MY_SOFTIRQ, /* New Entry */
    NR_SOFTIRQS
}; // NR_SOFTIRQS는 Entry의 개수를 세는 것으로 꼭 new entry는 이 위에 와야함

struct mydata {
    int year;
    int month;
} my_softirq_data;

void my_function(struct softirq_action *my_action){
    printk("my_function...\n");
    printk("Year : %d\n", my_softirq_data.year);
    printk(“Month : %d\n", my_softirq_data.month);
}
static int __init softirqTester_init(void){
    printk(“Softirq example init\n");
    my_softirq_data.year = 2020;
    my_softirq_data.month = 6;
    open_softirq(MY_SOFTIRQ, my_function);
    /* In general, raise_softirq() is called by an ISR */
    raise_softirq(MY_SOFTIRQ);
    return 0;
}

static void __exit softirqTester_exit(void){
    printk(“Softirq example exit\n");
    return 0;
}

 

  • interrupt.h 파일의 enum 선언부 마지막에 MY_SOFTIRQ를 추가합니다.
  • 드라이버 초기화 코드에서 open_softirq(MY_SOFTIRQ, my_softirq_handler)를 통해 핸들러 함수를 등록합니다.
  • 원하는 시점(인터럽트 발생 시)에 raise_softirq(MY_SOFTIRQ)를 호출하여 실행을 유도합니다.

 

Tasklet vs Softirq (매우 중요)

이 두 가지는 모두 인터럽트 컨텍스트(Interrupt Context)에서 실행된다는 공통점이 있지만, 멀티코어(SMP) 환경에서의 동작 방식에 치명적인 차이가 있다.

  • 동시성 (Concurrency):
    • Softirq: 같은 종류의 Softirq라도 여러 CPU에서 동시에(병렬로) 실행될 수 있다. 예를 들어, CPU 0과 CPU 1에서 동시에 NET_RX_SOFTIRQ 핸들러가 돌아갈 수 있다. 따라서 핸들러 내부에서 공유 자원에 접근할 때 반드시 락(Lock, 스핀락 등)을 이용한 고도의 동기화 처리를 해야 하므로 개발 난이도가 높다.
    • Tasklet: 동일한 Tasklet은 절대로 여러 CPU에서 동시에 돌 수 없다. 만약 CPU 0에서 my_tasklet이 실행 중인데 CPU 1이 이를 실행하려고 하면, TASKLET_STATE_RUN 비트를 보고 대기하거나 건너뛴다. (단, 서로 다른 Tasklet인 A와 B는 다른 CPU에서 동시에 돌 수 있다.) 이 덕분에 개발자는 내부 공유 자원에 대한 동기화 부담을 크게 덜 수 있어 작성이 훨씬 편리하다.
  • 재진입성 (Reentrancy): Softirq 함수는 반드시 재진입이 가능하도록(Reentrant) 설계되어야 한다.
  • Sleep semantics : 둘 다 interrupt context에서 실행되므로 sleep 는 사용할 수 없다.
  • Preemption : 둘 다 다른 것에 선점되지 않는다. softirq는 오직 event로만 선점 가능하다.
  • Easy of use 
    • tasklet 은 사용하기 쉽다.
    • 하지만 softirq는 사용하기 쉽진 않고 대용량 다룰 때 등의 상황에 좋다.
  • When to use
    • Softirq 사용 기준: 빈도가 매우 높고 성능 효율성이 극한으로 요구되는 네트워크 나 스토리지 드라이버 등에 제한적으로 사용한다.
    • Tasklet 사용 기준: 대부분의 일반적인 디바이스 드라이버 지연 처리에 권장된다.
  • 근본적 한계: Softirq와 Tasklet은 모두 인터럽트 컨텍스트에서 실행되므로, 절대로 잠들 수 없다(Must not sleep). 즉, 핸들러 내부에서 msleep(), copy_from_user(), 무한 루프, 혹은 락을 얻기 위해 대기하는 등 프로세스를 블로킹(Blocking)시키는 그 어떤 함수도 호출해서는 안 된다. 시스템이 그대로 굳어버릴(Freeze) 수 있기 때문이다.

Kernel Threads

그래서 Sleep 이 필요한 작업에는 Kernel Threads를 쓴다. 우선순위는 비록 위 2개에 비해 밀려 응답성도 낮지만 sleep 이 필요한 작업에 유리하다.

 

  • 커널 공간에서만 상주하며 실행되는 백그라운드 프로세스이다.
  • 사용자 공간(User Space)의 메모리를 가지지 않으며, 오직 커널의 함수들만 실행한다.
  • 대표적인 예로 메모리가 부족할 때 가동되는 kswapd, Softirq 오버헤드가 너무 많을 때 이를 대신 처리해 주는 ksoftirqd 등이 있다.
  • 일반 프로세스와 동일하게 커널 스케줄러에 의해 관리되므로, 프로세스 컨텍스트(Process Context)를 가진다. 그래서 실행 중에 언제든 잠들거나 깨어날 수 있는 것이다.

 

Workqueue

Workqueue는 위에서 설명한 커널 스레드를 드라이버 개발자들이 아주 쉽게 사용할 수 있도록 래핑하여 제공하는 지연 처리 API이다.

 

  • 동작 원리:
    1. 개발자가 나중에 실행할 작업 함수를 work_struct(작업 아이템)에 담아 큐(Queue)에 던진다(queue_work).
    2. 그러면 커널 백그라운드에서 돌고 있는 전용 커널 스레드(Worker Thread, 보통 kworker/X:Y라는 이름)가 큐에서 이 작업을 하나씩 꺼내어 실행해 준다.
  • 가장 큰 특징 (핵심 요약):
    • 프로세스 컨텍스트에서 실행: 작업 함수가 실행될 때 인터럽트 모드가 아닌 일반 프로세스 모드로 실행된다.
    • 잠들 수 있음 (May go to sleep): 함수 내부에서 I/O 대기를 하거나, 락을 잡기 위해 대기하거나, msleep() 같은 지연 함수를 자유롭게 호출할 수 있다.
    • 스케줄링 가능: 일반 프로세스처럼 우선순위에 따라 CPU 스케줄러에 의해 제어되며, 실행 도중 다른 프로세스에게 CPU를 양보(Context Switch)할 수 있다.

 

구분 softirq tasklet workqueue
실행 컨텍스트 인터럽트 컨텍스트 인터럽트 컨텍스트 프로세스 컨텍스트
잠들기 가능 여부 (Sleep) 불가 (Absolute No) 불가 (Absolute No) 가능 (Yes)
SMP 동시 실행 (동일 종류) 가능 (여러 CPU에서 동시 실행) 불가 (순차 실행으로 동기화 보장) 가능 (스레드 풀에 의해 처리)
주요 사용처 네트워크, 대용량 스토리지 일반 디바이스 드라이버 디스크 I/O, 블로킹 함수 포함 작업
구현 난이도 매우 높음 (정적 추가 필요) 낮음 / 편리함 보통 / 편리함