그라운드 임베디드 EN 유튜브 구독

노트 / RTOS

펌웨어RTOS2 / 8

태스크와 스케줄러

작업을 각자의 무한 루프를 가진 태스크로 나누고, 스케줄러가 CPU 시간을 나눠 줍니다. 태스크 상태와 틱의 개념.

GROUND EMBEDDED · 정리 노트RTOS 02태스크와 스케줄러 1void imu_task(void *arg) { 2 for (;;) { 3 read_imu(); 4 vTaskDelay(pdMS_TO_TICKS(1));// 1m…

태스크

태스크는 자기만의 스택을 가진 무한 루프 함수입니다.

void imu_task(void *arg) {
    for (;;) {
        read_imu();
        vTaskDelay(pdMS_TO_TICKS(1));   // 1ms 동안 CPU를 양보
    }
}
xTaskCreate(imu_task, "imu", 256, NULL, 3, NULL);

태스크 상태

Blocked기다리며 잠듦Ready차례 기다림RunningCPU 사용 중Blocked다시 기다림
태스크가 오가는 상태. 대부분의 시간은 Blocked에 있어야 합니다.
상태 의미
Running 지금 CPU를 쓰는 중 (코어당 하나)
Ready 실행할 수 있지만 차례를 기다림
Blocked 시간, 큐, 세마포어를 기다리며 잠듦
Suspended 명시적으로 멈춤

대부분의 시간 동안 태스크는 Blocked 상태여야 합니다. 아무 일도 없을 때 CPU를 붙잡고 도는 태스크가 있으면 낮은 우선순위 태스크가 굶습니다.

틱과 컨텍스트 전환

틱태스크 A태스크 B
틱 인터럽트마다 스케줄러가 누구를 실행할지 다시 정합니다. 우선순위가 같으면 번갈아 실행합니다.

SysTick이 1ms마다 인터럽트를 걸어(틱) 스케줄러가 다음에 실행할 태스크를 고릅니다. 태스크를 바꿀 때는 현재 레지스터를 그 태스크 스택에 저장하고 다음 태스크의 레지스터를 복원합니다. Cortex-M에서는 PendSV 예외가 이 일을 합니다.

태스크 나누기

센서 읽기, 제어, 통신, 화면, 로그처럼 주기와 마감이 다른 일끼리 나눕니다. 너무 잘게 나누면 태스크 간 통신이 오히려 복잡해집니다.

태스크의 네 가지 상태

상태 뜻
실행 (Running) 지금 CPU를 쓰는 중. 코어가 하나면 한 번에 하나
준비 (Ready) 실행할 수 있지만 더 급한 태스크에 밀려 대기
대기 (Blocked) 시간이나 사건(큐, 세마포어)을 기다리는 중. CPU를 쓰지 않음
정지 (Suspended) 명시적으로 멈춰 둔 상태

스케줄러는 준비 상태인 태스크 중 우선순위가 가장 높은 것을 실행합니다. 잘 짠 시스템에서는 대부분의 태스크가 대부분의 시간을 대기 상태로 보냅니다.

그림으로 보기: 선점

제어 (높음)통신 (중간)유휴 (최저)깨어남통신 계속깨어남
우선순위가 높은 제어 태스크가 깨어나면 통신 태스크는 그 자리에서 멈추고, 제어 태스크가 끝난 뒤 이어서 실행됩니다.

태스크는 무한 루프다

void sensor_task(void *arg) {
    TickType_t last = xTaskGetTickCount();
    for (;;) {
        read_sensor();
        vTaskDelayUntil(&last, pdMS_TO_TICKS(10));   // 정확히 10 ms 주기
    }
}

xTaskCreate(sensor_task, "sensor", 256, NULL, 3, NULL);   // 스택 256워드, 우선순위 3

vTaskDelay(10)은 "지금부터 10 ms"이고, vTaskDelayUntil은 "지난번 깨어난 시각부터 10 ms"입니다. 주기가 정확해야 하는 일에는 뒤쪽을 씁니다. 앞쪽은 처리 시간만큼 주기가 밀립니다.

3D 실험실여기까지 읽은 내용을 직접 돌려 볼 수 있습니다→

태스크가 CPU를 놓지 않으면

void bad_task(void *arg) {
    for (;;) { if (flag) do_work(); }     // 기다리지 않고 계속 확인
}

이 태스크는 항상 준비 상태라서, 우선순위가 낮은 태스크는 영원히 실행되지 못합니다. 모든 태스크의 루프에는 지연이나 큐·세마포어 대기처럼 CPU를 내놓는 지점이 있어야 합니다.

우선순위 정하는 기준

  • 주기가 짧을수록 높게: 1 ms 제어 루프가 100 ms 로그 출력보다 높아야 합니다.
  • 마감이 엄격할수록 높게: 늦으면 안 되는 일과 조금 늦어도 되는 일을 구분합니다.
  • 오래 걸리는 일은 낮게: 파일 쓰기, 화면 갱신.
  • 단계를 너무 잘게 나누지 않기: 서너 단계면 대부분 충분합니다.

스택 크기

태스크마다 스택이 따로 있고, 이 크기는 만들 때 정합니다. 지역 변수, 함수 호출 깊이, printf 사용 여부에 따라 필요한 양이 크게 달라집니다.

UBaseType_t left = uxTaskGetStackHighWaterMark(NULL);   // 지금까지 남았던 최소 여유

충분히 돌린 뒤 이 값이 전체의 20% 아래면 늘립니다. 스택 넘침 감지 기능(configCHECK_FOR_STACK_OVERFLOW)은 개발 중에 항상 켜 둡니다.

틱과 시간 분해능

스케줄러는 주기적인 틱 인터럽트(보통 1 ms)마다 깨어나 다음 태스크를 정합니다. 지연 시간은 틱 단위로만 지정할 수 있어서, 1 ms보다 정밀한 타이밍은 하드웨어 타이머로 처리합니다.

현업에서 자주 터지는 문제

  • 태스크 함수가 return으로 끝남: 태스크는 끝나면 안 됩니다. 끝내려면 스스로를 삭제해야 합니다.
  • 인터럽트에서 일반 API를 호출: FromISR이 붙은 함수만 씁니다.
  • 높은 우선순위 태스크가 긴 계산을 함: 그동안 아래 태스크가 전부 멈춥니다. 나눠서 하거나 우선순위를 내립니다.
  • 유휴 태스크가 못 돌아감: 삭제된 태스크의 메모리 정리가 유휴 태스크에서 이루어집니다.
3D 실험실에서 직접 돌려 보기값을 바꾸며 이 노트의 원리를 확인합니다→영상으로 이어 보기RC카 만들기 시리즈 6편대기업 임베디드 개발자 Lego RC카 메이킹 시작합니다. 【RC카 개발1】FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →