실시간 시스템에서 인터럽트 서비스 루틴(ISR)과 실시간 스레드 간의 효율적이고 안전한 데이터 교환은 필수적이다. ISR은 하드웨어 인터럽트에 대한 빠른 응답을 제공하며, 실시간 스레드는 일정한 주기로 작업을 수행하는 역할을 한다. 이 두 요소 간의 데이터 교환이 제대로 이루어지지 않으면 시스템의 실시간 성능이 저하될 수 있다. 본 섹션에서는 ISR과 실시간 스레드 간의 데이터 교환 메커니즘과 그 구현 방안을 다룬다.

ISR과 실시간 스레드의 역할

ISR은 특정 하드웨어 이벤트가 발생했을 때 호출되는 함수이다. 이 함수는 매우 짧고 신속하게 실행되어야 하며, 필요한 최소한의 작업만 수행한 후 바로 반환해야 한다. 일반적으로 ISR은 긴 시간 동안 실행되지 않으며, 주로 하드웨어 레지스터에서 데이터를 읽거나, 간단한 플래그를 설정하는 등의 작업을 수행한다.

실시간 스레드는 정해진 주기나 이벤트에 따라 실행되는 프로그램의 일부이다. 이러한 스레드는 일정한 응답 시간을 요구하며, 주로 복잡한 계산이나 데이터 처리 작업을 수행한다.

데이터 교환의 문제점

ISR과 실시간 스레드 간의 데이터 교환에는 여러 가지 문제가 따른다. 가장 큰 문제는 동기화 문제로, ISR이 데이터를 업데이트하는 도중 실시간 스레드가 동일한 데이터에 접근하려고 할 때 발생할 수 있다. 이는 데이터의 일관성을 해칠 수 있으며, 예기치 않은 동작을 유발할 수 있다. 또 다른 문제는 ISR이 짧은 시간 내에 여러 번 발생할 경우, 실시간 스레드가 데이터 처리에 뒤처질 수 있다는 점이다.

동기화 메커니즘

동기화 문제를 해결하기 위해 다양한 메커니즘이 사용된다. 대표적으로는 스핀락(spinlock), 뮤텍스(mutex), 세마포어(semaphore) 등이 있다. 각각의 방법은 상황에 맞게 선택하여 사용해야 한다.

원자적 데이터 교환

ISR과 실시간 스레드 간의 데이터 교환에서 데이터의 원자적 접근(atomic access)은 매우 중요하다. 이는 특정 데이터가 중간 상태에 있을 때 다른 스레드가 그 데이터에 접근하지 못하도록 하는 것을 의미한다. 원자적 접근을 보장하기 위해 atomic_t와 같은 특수한 자료형과 관련된 연산이 사용된다.

예를 들어, 카운터 변수를 ISR과 실시간 스레드가 공유한다고 가정해 봅시다. 이 경우, 데이터의 일관성을 유지하기 위해 다음과 같은 형태의 코드가 사용될 수 있다.

atomic_t counter = ATOMIC_INIT(0);

void isr_handler(void) {
    atomic_inc(&counter);
}

void real_time_thread(void) {
    int local_counter;
    local_counter = atomic_read(&counter);
    // 이후 local_counter를 사용한 처리
}

이 코드에서는 atomic_inc() 함수가 counter 변수를 원자적으로 증가시키며, atomic_read() 함수가 해당 값을 안전하게 읽습니다. 이렇게 함으로써 ISR과 실시간 스레드 간의 데이터 교환이 안전하게 이루어진다.

캐시 일관성 문제

멀티코어 시스템에서 캐시 일관성(cache coherence) 문제는 ISR과 실시간 스레드 간의 데이터 교환 시 중요한 고려사항이다. 각각의 코어는 자체 캐시를 가지며, 이 캐시에 저장된 데이터가 다른 코어의 캐시와 일치하지 않을 수 있다. 이를 해결하기 위해 메모리 배리어(memory barrier)를 사용할 수 있다. 메모리 배리어는 프로세서가 특정 명령을 수행하기 전에 모든 읽기 및 쓰기 연산이 완료되도록 보장한다.

메모리 배리어

메모리 배리어는 프로세서가 메모리 접근 순서를 재배치하지 않도록 하여 ISR과 실시간 스레드 간의 데이터 일관성을 유지하는 데 사용된다. 이는 보통 다음과 같은 형태로 사용된다.

void isr_handler(void) {
    data_ready = 1;
    smp_mb();  // Memory barrier to ensure 'data_ready' is updated before the next operation
    wake_up_process(rt_thread);
}

void real_time_thread(void) {
    while (!data_ready)
        cpu_relax();  // Avoid busy waiting

    smp_mb();  // Ensure that 'data_ready' is read before proceeding
    process_data();
}

이 코드에서 smp_mb() 함수는 멀티프로세서 환경에서 메모리 접근 순서를 보장하는 역할을 한다.

버퍼를 통한 데이터 교환

ISR과 실시간 스레드 간의 데이터 교환 시, 버퍼(buffer)를 사용하는 것은 매우 일반적인 방법이다. 버퍼를 사용하면 데이터가 임시 저장되며, ISR이 데이터를 버퍼에 기록하고 실시간 스레드가 해당 데이터를 읽어 처리하는 구조로 동작한다. 이때 버퍼는 큐(queue) 형태로 구성될 수 있으며, 원형 버퍼(circular buffer)가 자주 사용된다.

원형 버퍼

원형 버퍼는 고정된 크기의 배열로 구성되며, 버퍼의 끝이 다시 시작 부분과 연결된 형태를 가지고 있다. 이 구조는 메모리 사용 효율성을 높이고, 데이터가 추가되거나 읽힐 때의 오버헤드를 줄여준다.

원형 버퍼의 주요 변수는 다음과 같다:

원형 버퍼에서 데이터의 추가와 읽기 과정은 다음과 같이 이루어진다:

데이터 추가 (ISR에서):

void isr_handler(void) {
    buffer[head] = new_data;
    head = (head + 1) % size;
}

데이터 읽기 (실시간 스레드에서):

void real_time_thread(void) {
    if (head != tail) {
        process_data(buffer[tail]);
        tail = (tail + 1) % size;
    }
}

이 구조는 데이터를 추가할 때마다 head 포인터를 이동시키고, 데이터를 읽을 때마다 tail 포인터를 이동시킨다. 이로써 버퍼가 꽉 찼는지 또는 비어 있는지를 간단히 판별할 수 있다. 버퍼가 꽉 차면 head 포인터가 tail 포인터를 따라잡아 버퍼 오버플로우가 발생하게 되며, 이 경우 추가적인 오류 처리가 필요할 수 있다.

락프리 데이터 구조

실시간 시스템에서는 락(lock)을 사용하는 동기화 메커니즘이 실시간 성능을 저하시킬 수 있다. 따라서, 락프리(lock-free) 또는 웨이트프리(wait-free) 알고리즘이 선호될 수 있다. 락프리 데이터 구조는 락을 사용하지 않고도 동기화를 유지할 수 있는 구조를 말하며, ISR과 실시간 스레드 간의 데이터 교환에도 유용하게 활용될 수 있다.

락프리 데이터 구조의 대표적인 예는 단일 생산자-소비자 큐(single producer-single consumer queue)이다. 이 큐는 단일 스레드가 데이터를 추가하고, 단일 스레드가 데이터를 소비하는 구조로 동작하며, 락이 필요하지 않는다.

단일 생산자-소비자 큐의 구현

다음은 단일 생산자-소비자 큐를 사용하는 예제이다:

#define QUEUE_SIZE 1024
int buffer[QUEUE_SIZE];
int head = 0;
int tail = 0;

void enqueue(int data) {
    int next_head = (head + 1) % QUEUE_SIZE;
    if (next_head != tail) {
        buffer[head] = data;
        head = next_head;
    }
    // else: 큐가 꽉 찼으므로 데이터 손실 발생 가능
}

int dequeue(void) {
    if (head == tail) {
        return -1; // 큐가 비었음
    } else {
        int data = buffer[tail];
        tail = (tail + 1) % QUEUE_SIZE;
        return data;
    }
}

위의 예제에서 enqueue() 함수는 데이터가 큐에 안전하게 추가될 수 있는지를 확인하고, dequeue() 함수는 큐에서 데이터를 안전하게 제거한다. 이러한 락프리 구조는 실시간 시스템에서 매우 효율적이며, ISR과 실시간 스레드 간의 빠르고 안전한 데이터 교환을 보장한다.

데이터 일관성 보장

ISR과 실시간 스레드 간의 데이터 교환에서 중요한 또 다른 요소는 데이터의 일관성(consistency)이다. 데이터 일관성을 유지하기 위해 여러 가지 기술이 사용되며, 앞서 언급한 동기화 메커니즘과 메모리 배리어가 그 예이다. 그러나, 시스템의 복잡성에 따라 추가적인 일관성 유지 기법이 필요할 수 있다.

예를 들어, 공유 메모리 영역을 사용하는 경우에는 이중 버퍼링(double buffering) 또는 리드-카피-업데이트(Read-Copy-Update, RCU) 같은 기법이 사용될 수 있다. 이중 버퍼링은 데이터의 복사본을 유지하여, 데이터가 업데이트되는 동안 다른 스레드가 안정적으로 데이터를 읽을 수 있도록 한다. RCU는 읽기와 쓰기 연산을 비동기적으로 처리하여, 실시간 시스템에서 높은 성능과 데이터 일관성을 동시에 달성할 수 있도록 도와준다.

이중 버퍼링

이중 버퍼링을 사용할 때는 두 개의 버퍼를 유지하며, 하나의 버퍼는 쓰기용으로, 다른 하나는 읽기용으로 사용된다. 데이터 업데이트가 완료되면 버퍼의 역할이 전환된다.

int buffer1[BUFFER_SIZE];
int buffer2[BUFFER_SIZE];
int *write_buffer = buffer1;
int *read_buffer = buffer2;

void isr_handler(void) {
    update_data(write_buffer);
    swap_buffers();  // read_buffer와 write_buffer를 교환
}

void real_time_thread(void) {
    process_data(read_buffer);
}

이 예제에서 swap_buffers() 함수는 read_bufferwrite_buffer를 교환하여 데이터가 일관된 상태에서 읽힐 수 있도록 보장한다.

성능 고려사항

ISR과 실시간 스레드 간의 데이터 교환 시 성능을 고려하는 것은 매우 중요하다. 특히, 실시간 시스템에서는 데이터 처리 지연(latency)을 최소화하고, ISR이 가능한 한 빨리 반환되도록 설계해야 한다. 따라서, ISR 내에서 복잡한 데이터 처리 작업을 피하고, 이러한 작업은 실시간 스레드에서 수행하는 것이 바람직한다.

이를 위해서는 적절한 데이터 교환 메커니즘을 선택하고, 메모리 배리어와 같은 동기화 기법을 적절히 사용하여 성능을 최적화해야 한다.