태스크와 스케줄러
작업을 각자의 무한 루프를 가진 태스크로 나누고, 스케줄러가 CPU 시간을 나눠 줍니다. 태스크 상태와 틱의 개념.
태스크
태스크는 자기만의 스택을 가진 무한 루프 함수입니다.
void imu_task(void *arg) {
for (;;) {
read_imu();
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms 동안 CPU를 양보
}
}
xTaskCreate(imu_task, "imu", 256, NULL, 3, NULL);
태스크 상태
| 상태 | 의미 |
|---|---|
| Running | 지금 CPU를 쓰는 중 (코어당 하나) |
| Ready | 실행할 수 있지만 차례를 기다림 |
| Blocked | 시간, 큐, 세마포어를 기다리며 잠듦 |
| Suspended | 명시적으로 멈춤 |
대부분의 시간 동안 태스크는 Blocked 상태여야 합니다. 아무 일도 없을 때 CPU를 붙잡고 도는 태스크가 있으면 낮은 우선순위 태스크가 굶습니다.
틱과 컨텍스트 전환
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"입니다. 주기가 정확해야 하는 일에는 뒤쪽을 씁니다. 앞쪽은 처리 시간만큼 주기가 밀립니다.
태스크가 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이 붙은 함수만 씁니다. - 높은 우선순위 태스크가 긴 계산을 함: 그동안 아래 태스크가 전부 멈춥니다. 나눠서 하거나 우선순위를 내립니다.
- 유휴 태스크가 못 돌아감: 삭제된 태스크의 메모리 정리가 유휴 태스크에서 이루어집니다.
영상으로 이어 보기RC카 만들기 시리즈 6편대기업 임베디드 개발자 Lego RC카 메이킹 시작합니다. 【RC카 개발1】FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →