제12부 실시간·성능
12.2 RT 커널·스케줄링·CPU 격리
12.2.1 이 장에서 배우는 것 / 선수 지식
12.1에서 "전 경로를 다뤄야 한다"고 했다. 이 장은 그중 OS 층이다 — 가장 아래이자, 여기가 깨지면 위의 모든 노력이 무의미해지는 층.
핵심 질문은 하나다. "내 스레드가 실행되어야 할 때 실제로 실행되는가?" 일반 Linux는 이것을 보장하지 않는다. 커널이 선점 불가능한 구간에 들어가 있거나, 우선순위가 낮은 스레드가 CPU를 점유하고 있거나, 인터럽트가 개입하면 지연된다.
배우는 것:
- PREEMPT_RT — 무엇을 바꾸는가.
- 스케줄링 정책 —
SCHED_FIFO·RR·DEADLINE. - 우선순위 설계 — 그리고 우선순위 역전.
- ROS 2에서 설정 — 실제 코드·설정.
- CPU 격리 — 전용 코어 확보.
- 인터럽트 친화도 — IRQ를 치우기.
- 전원 관리 — 절전이 지터를 만든다.
- 측정 —
cyclictest와 판단. - 설정 경로·옵션 실측(12.2.12) —
/sys·/proc·패키지로 닫을 수 있는 것과 없는 것.
선수 지식: 12.1(실시간 정의), 2.2.6(실행기 스레드), 6.6(실시간 제어), 11.7(측정).
정직: 커널 인터페이스·스케줄링 정책·권한 설정은 Jazzy 컨테이너에서 실제로 확인했다(실행 확인 — 12.2.11). 다만 이 환경은 PREEMPT_RT 커널이 아니어서(
PREEMPT_DYNAMIC), RT 커널의 지연 특성·부트 파라미터 효과는 검증하지 못했다 — 그 부분은 표기를 그대로 두었다. 여기 제시하는 것은 개념과 전형적 형태이며, 자신의 시스템 문서로 확인해야 한다. 지연 수치는 하드웨어에 극히 의존하므로 제시하지 않는다(3.6.1). 이 환경에서 검증하지 않았다(목차 3). 커널 설정 변경은 시스템을 부팅 불가로 만들 수 있으니 복구 수단을 확보하고 진행하라.(→ 169차 확인: PI 뮤텍스 API를 컴파일·실행했고,--cap-add SYS_NICE로 실제SCHED_FIFO스케줄링을 확보해 우선순위 역전이 실제로 완화되는지까지 정량 재현했다 — 12.2.4 실행 확인(169차) 참고. 이 재현은 PREEMPT_RT가 아닌 일반 커널에서도 가능했다(PI는 POSIX 스레드 기능이지 RT 커널 전용 기능이 아니다).)(→ 170차 확인: 우선순위 천장(PTHREAD_PRIO_PROTECT)도 같은 방식으로 재현해 PI와 사실상 동일한 효과(H 대기 ~40ms)를 확인했다 — 12.2.4 실행 확인(169차) 블록의 170차 추가분 참고.)
12.2.2 PREEMPT_RT
일반 Linux의 문제는 "커널이 선점되지 않는 구간"이다.
| PREEMPT_RT가 바꾸는 것 | 설명 |
|---|---|
| 스핀락 → 뮤텍스 | 대부분의 커널 락을 선점 가능하게 |
| 인터럽트 핸들러 → 스레드 | 우선순위를 가진 커널 스레드로 |
| 우선순위 상속 | 우선순위 역전 완화(12.2.5) |
| 고해상도 타이머 | 정밀한 깨우기 |
| 대가 | 설명 |
|---|---|
| 처리량 감소 | 문맥 전환 증가 |
| 일부 드라이버 비호환 | 확인 필요 |
| GPU·특수 드라이버 | 문제 가능성(확인 필요) |
| 커널 선택 제약 | 배포판 지원 |
# RT 커널 여부 확인
uname -a | grep -i rt
cat /sys/kernel/realtime 2>/dev/null # PREEMPT_RT 커널에만 있는 파일 (실행 확인)
정직: PREEMPT_RT의 주요 부분이 메인라인 커널에 병합되어 왔으며, 배포판별 제공 형태가 다르다
(확인 필요). Ubuntu 24.04에서의 RT 커널 제공 방식·라이선스 조건은 배포판 문서를 확인해야 한다.
견해: RT 커널은 "필요조건이지 충분조건이 아니다"(12.1.4의 반복). 커널을 바꿔도 애플리케이션이 할당을 하거나 우선순위 설정을 안 하면 지터는 그대로다. RT 커널만 설치하고 끝내는 것이 흔한 오해다.
함정: RT 커널에서 GPU·특수 하드웨어 드라이버가 문제를 일으킬 수 있다. GPU 가속 인지(7.8)를 쓰는 시스템에서는 호환성을 먼저 확인해야 하며, 이것이 RT 도입을 막는 실무적 장벽이 되기도 한다.
12.2.3 스케줄링 정책
| 정책 | 특성 | 용도 |
|---|---|---|
SCHED_OTHER(CFS) | 공정 분배·nice | 기본 — 대부분의 노드 |
SCHED_FIFO | 우선순위 고정·양보까지 실행 | RT 스레드 |
SCHED_RR | FIFO + 같은 우선순위 간 타임슬라이스 | RT |
SCHED_DEADLINE | 주기·실행시간·데드라인 명시 | 고급 RT — 실재 확인(chrt -d, 12.2.11.3) |
SCHED_BATCH | 선점을 덜 받는 일괄 작업용 | 배경 계산 |
SCHED_IDLE | 유휴 시만 | 배경 작업 |
chrt가 받는 정책은 이 여섯이다(실행 확인). 우선순위 숫자를 갖는 것은 SCHED_FIFO·SCHED_RR(1~99)뿐이고, 나머지는 0이다 — SCHED_DEADLINE이 0인 것은 미지원이 아니라 우선순위 대신 세 값으로 스케줄링하기 때문이다(12.2.11.3).
# 확인
chrt -p <PID>
ps -eo pid,cls,rtprio,ni,comm | grep my_node
# 설정 (실행 시)
chrt -f 80 ./my_node # SCHED_FIFO 우선순위 80
함정:
SCHED_FIFO스레드가 무한 루프에 빠지면 시스템이 멈춘다. 우선순위가 높아 아무것도 선점하지 못한다. RT 스로틀링(sched_rt_runtime_us)이 기본으로 이를 완화한다 — 값을 실측하니 정확히 950000 / 1000000 = 95%였다(실행 확인). 그것을 끄면 복구가 어려워진다. 개발 중에는 SSH 세션을 하나 더 열어 두는 것이 실용적 안전장치다.
# RT 스로틀링 확인 (실행 확인 — 경로·기본값 모두 아래 그대로다)
cat /proc/sys/kernel/sched_rt_runtime_us # 기본 950000 (=95%)
cat /proc/sys/kernel/sched_rt_period_us # 1000000
견해: 스로틀링을 끄지 말라. 이론적으로는 RT 스레드가 100% 쓸 수 있게 하는 것이 맞지만, 실수 하나로 시스템이 잠겨 물리적 재부팅이 필요해진다. 95% 제한이 실무적으로 합리적이며, 그 5%를 못 견디는 설계라면 다른 문제가 있는 것이다.
12.2.4 우선순위 설계
모든 것을 높이면 아무것도 높지 않다.
| 원칙 | 설명 |
|---|---|
| 주기가 짧을수록 높게(RMS) | 비율 단조 스케줄링 |
| 최소한만 RT로 | 필요한 스레드만 |
| 최고 우선순위 회피 | 99는 시스템용 |
| IRQ 스레드와의 관계 고려 | 데이터 원천보다 소비자를 낮게 |
| 문서화 | 우선순위 표를 유지 |
견해: 우선순위 표를 문서로 유지하라. 여러 프로세스가 각자 우선순위를 정하면 충돌하고, 나중에 왜 그 값인지 아무도 모른다. 시스템 전체의 우선순위 배치를 한 곳에 적어 두면 변경 시 영향을 판단할 수 있다.
함정: RT 스레드를 너무 많이 만들면 서로 경쟁해 의미가 없어진다. "중요하니까 RT로"를 반복하면 결국 모두가 RT이고, 그중 낮은 것은 일반 스레드보다 나을 것이 없다. 정말 데드라인이 있는 것만 RT로 둔다.
우선순위 역전:
| 대책 | 설명 |
|---|---|
| 우선순위 상속(PI 뮤텍스) | L을 임시로 H 수준으로 |
| RT 경로에서 락 회피 | 락프리 자료구조(6.5.6) |
| 우선순위 천장 | 진입 시 상승(→ 170차 확인: PTHREAD_PRIO_PROTECT로 실제 재현 — 아래 실행 확인(169차) 블록의 170차 추가분 참고) |
// PI 뮤텍스 (개념 — 확인 필요: 정확한 사용)(→ 169차 확인: 아래 코드 그대로 컴파일·실행해 정상 동작을 확인했다 — 아래 실행 확인(169차) 참고)
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&m, &attr);
견해: RT 경로에서 락을 전혀 쓰지 않는 것이 가장 확실하다.
realtime_tools의 락프리 버퍼(6.5.6)가 그 목적으로 존재한다. 락이 불가피하면 PI 뮤텍스를 쓰되, 표준 라이브러리 컨테이너·std::mutex가 내부적으로 무엇을 하는지 확인해야 한다.
실행 확인(169차, Jazzy) — PI 뮤텍스 API를 컴파일·실행했고, 실제 우선순위 역전 완화 효과까지 정량으로 재현했다.
- API 확인. 위 코드를 그대로 컴파일·실행하니
pthread_mutexattr_setprotocol·pthread_mutex_init·pthread_mutex_lock/unlock전부rc=0(성공)이었고,pthread_mutexattr_getprotocol로 실제PTHREAD_PRIO_INHERIT(값 1)가 설정됐음을 확인했다 — 정확한 사용법에 대한 미확인 표시를 이걸로 닫는다.- 실제 우선순위 역전 재현(핵심 발견). 저(10)·중(50)·고(90) 세
SCHED_FIFO스레드를 CPU 0에 고정해 만들었다(--cap-add SYS_NICE로 컨테이너에서도 실제 RT 스케줄링을 확보) — L은 뮤텍스를 획득하고 50ms 임계구역을 실행, M은 뮤텍스 없이 300ms 동안 CPU를 독점하는 바쁜 루프, H는 L이 획득한 뮤텍스를 요청하고 대기 시간을 측정한다. 일반 뮤텍스: H 대기 305.159/305.231/305.214ms(3회 반복, 매우 일관) — M이 L을 선점해 L이 임계구역을 못 끝내므로 H가 사실상 M이 끝날 때까지 기다리는 교과서적 우선순위 역전이 재현됐다. PI 뮤텍스: H 대기 39.837/39.844/39.823ms(3회 반복, 매우 일관) — H가 대기를 시작하는 순간 L의 우선순위가 H 수준으로 상속돼 M을 선점하고 임계구역을 빠르게 끝낸다. 약 7.7배 개선(305ms → 40ms)이 두 조건 모두 3/3 반복에서 0.1ms 이내로 수렴할 만큼 재현성 높게 확인됐다. 이 재현은 PREEMPT_RT가 아닌 일반 커널(PREEMPT_DYNAMIC)에서도 가능했다 — PI는 POSIX 스레드 기능이지 RT 커널 전용 기능이 아니기 때문이다.컨테이너
gap169j(ros:jazzy-ros-base,--cap-add SYS_NICE --ulimit rtprio=99). 원본:tools/gap_harness/gap169/pi_mutex_test/(test_pi_mutex.c·run_result.log),tools/gap_harness/gap169/pi_demo/(pi_inversion_demo.c·no_pi_result.log·with_pi_result.log).170차 추가 — 우선순위 천장(
PTHREAD_PRIO_PROTECT)도 같은 방식으로 재현해 PI와 비교했다. 같은 저(10)·중(50)·고(90) 3-스레드 설계에pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_PROTECT)+pthread_mutexattr_setprioceiling(&attr, 90)을 적용했다. 결과: H 대기 39.873/39.865/39.879ms(3회 반복) — 169차의 PI 결과(39.837/39.844/39.823ms)와 사실상 동일했다. 이 시나리오(단일 뮤텍스, 중첩 없음)에서는 두 프로토콜이 동등하게 효과적임을 정량으로 확인했다. 원본:tools/gap_harness/gap170/pc_demo/(pc_ceiling_demo.c·pc_protect_result.log·pc_none_result.log).
12.2.5 ROS 2에서 설정
#include <pthread.h>
#include <sched.h>
bool set_realtime_priority(int priority)
{
struct sched_param param;
param.sched_priority = priority;
if (pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m) != 0) {
// 권한 부족일 가능성 (12.2.9)
return false;
}
return true;
}
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
set_realtime_priority(80); // 메인 스레드
// 메모리 잠금도 함께 (12.3.3)
auto node = std::make_shared<MyRtNode>();
rclcpp::spin(node); // 이 스레드가 RT
}
실측 결과는 "전부 상속한다"이다(실행 확인 — 12.2.11.2). 그리고 그것이 문제다.
chrt -f 50으로 노드를 실행하면 워커 스레드뿐 아니라 DDS 내부 송수신 스레드까지 전부SCHED_FIFO 50이 된다. 의도한 "제어 루프만 RT"가 되지 않는다.
# 스레드별 정책·우선순위 확인
ps -eLo pid,tid,cls,rtprio,pri,comm | grep my_node
권한 설정:
# /etc/security/limits.conf (실행 확인 — 이 형식으로 반영된다)
@realtime - rtprio 99
@realtime - memlock unlimited
@realtime - nice -20
# 사용자를 그룹에 추가 후 재로그인
sudo usermod -aG realtime $USER
함정: 권한이 없으면
pthread_setschedparam이 조용히 실패한다. 반환값을 확인하지 않으면 RT라고 믿으면서 일반 스케줄링으로 실행되는 상태가 된다. 반드시 반환값을 검사하고 실패 시 명확히 로그를 남기라.
견해: 실패 시 어떻게 할지 정하라. 개발 환경에서는 경고 후 계속 진행하는 것이 편리하지만, 운영에서 RT가 필수라면 시작을 거부하는 것이 안전하다. 조용히 성능이 저하된 상태로 실행되는 것이 최악이다.
ros2_control에서:
확정됨(실행 확인):
ros2_control의 컨트롤러 매니저는thread_priority파라미터로 RT 우선순위를 받고 기본값은 50이다. 자세한 동작과 함정은 12.2.12.4. 6.2·6.6에서 다룬 구조 위에서, 실제 지원 여부는 문서로 확인해야 한다.
12.2.6 CPU 격리
RT 스레드에 전용 코어를 준다.
# 부트 파라미터 (GRUB — 확인 필요: 정확한 옵션 조합)
isolcpus=3 nohz_full=3 rcu_nocbs=3 irqaffinity=0-2
| 파라미터 | 효과 |
|---|---|
isolcpus | 스케줄러가 그 코어에 일반 작업 배치 안 함 |
nohz_full | 타이머 틱 제거 — 인터럽트 감소 |
rcu_nocbs | RCU 콜백을 다른 코어로 |
irqaffinity | 인터럽트를 다른 코어로 |
# 실행 시 코어 지정
taskset -c 3 ./my_rt_node
# 코드에서
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(3, &set);
pthread_setaffinity_np(pthread_self(), sizeof(set), &set);
견해: CPU 격리가 지터 감소에 가장 효과적인 조치 중 하나다. 다른 작업과 캐시·실행 자원을 경합하지 않으면 최악값이 크게 안정된다. 다만 코어 하나 전체를 내주는 대가가 있으므로, 코어 수가 적은 임베디드에서는 신중해야 한다.
함정: 격리한 코어에 아무도 배치하지 않으면 유휴 코어가 된다.
isolcpus는 "자동 배치를 막는" 것이지 "RT 스레드를 옮기는" 것이 아니다.taskset이나 코드로 명시적으로 배치해야 한다.
함정:
nohz_full은 조건이 까다롭다 — 해당 코어에 실행 가능한 태스크가 하나뿐일 때만 틱이 제거된다(확인 필요). 여러 스레드를 승격하면 효과가 사라진다.
하이퍼스레딩:
견해: 하이퍼스레딩(SMT)은 실시간에 불리할 수 있다. 같은 물리 코어의 두 논리 코어가 실행 자원을 공유하므로, 옆 스레드의 부하가 지터를 만든다. RT 코어의 짝 논리 코어를 비워 두거나 SMT를 끄는 것이 흔한 조치이며, 처리량과의 절충이다.
12.2.7 인터럽트와 전원 관리
인터럽트 친화도:
# 현재 IRQ 분포
cat /proc/interrupts
# 특정 IRQ를 CPU 0-2로 (실행 확인 — 16진 비트마스크)
echo 7 > /proc/irq/<N>/smp_affinity # 0x7 = 0b0111 = CPU 0,1,2
# 더 읽기 쉬운 목록 형식도 있다 (실행 확인)
echo 0-2 > /proc/irq/<N>/smp_affinity_list
# irqbalance가 되돌릴 수 있으니 주의
sudo systemctl status irqbalance
함정:
irqbalance데몬이 설정한 친화도를 되돌린다(기본 10초마다 재분배). RT 시스템에서는 비활성화하거나 격리 코어를 제외해야 하며, 설정 파일은/etc/default/irqbalance다 —IRQBALANCE_BANNED_CPULIST=0-2형태가 가장 읽기 쉽다(실행 확인, 12.2.12.2).
견해: 네트워크·USB 인터럽트가 지터의 흔한 원천이다. 대용량 센서 데이터가 들어오는 인터페이스의 IRQ가 RT 코어에 고정돼 있으면, 데이터가 올 때마다 제어 루프가 방해받는다.
/proc/interrupts로 분포를 확인하고 옮긴다.
전원 관리 — 절전이 지터를 만든다:
| 기능 | 문제 |
|---|---|
| C-state(절전 상태) | 깊은 절전에서 깨어나는 지연 |
| 주파수 스케일링 | 클럭이 낮을 때 실행 시간 증가 |
| 터보 부스트 | 변동성 |
| 열 스로틀링 | 예측 불가 |
# CPU 거버너 (실행 확인 — 경로 맞음)
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors # 고를 수 있는 값
sudo cpupower frequency-set -g performance
# C-state 제한 (부트 파라미터)
# intel_idle.max_cstate=1 processor.max_cstate=1 idle=poll
견해: 전원 관리 설정이 의외로 큰 지터 원인이다. RT 커널·격리를 다 하고도 지터가 남는다면 C-state와 거버너를 확인하라.
idle=poll은 극단적이며 전력·발열을 크게 늘리므로, 배터리 로봇에서는 신중해야 한다 — 실시간성과 주행 시간의 절충이다.
함정: 열 스로틀링은 시간이 지나야 나타난다. 짧은 시험에서는 문제없다가 장시간 운전에서 성능이 떨어진다. 장시간·고부하 조건에서 측정해야 한다(11.7.7).
12.2.8 측정
cyclictest가 표준 도구다.
# 기본 (실행 확인 — 아래 옵션 전부 실재)
sudo cyclictest -m -p 80 -i 1000 -h 400 -q -D 1h
# -m: 메모리 잠금 -p: 우선순위 -i: 주기(us)
# -h: 히스토그램 -D: 지속 시간
보는 것: 최댓값이다.
| 지표 | 의미 |
|---|---|
| Avg | 참고 |
| Max | 핵심 — 최악 깨우기 지연 |
| 히스토그램 | 분포·꼬리 |
견해:
cyclictest는 "OS가 스레드를 제때 깨우는가"만 측정한다. 애플리케이션의 실제 지연은 여기에 처리 시간·통신·실행기가 더해진다.cyclictest값은 달성 가능한 하한이지 애플리케이션 성능이 아니다.
부하를 가하고 측정해야 한다:
# 부하 생성 (예)
stress-ng --cpu 4 --io 2 --vm 2 &
# 또는 실제 시스템 전체를 기동한 상태
sudo cyclictest -m -p 80 -i 1000 -D 1h
함정: 무부하
cyclictest는 낙관적이다(11.7.8의 반복). 실제로는 인지·항법·기록이 동시에 실행되며 캐시와 메모리 대역을 경합한다. 실제 워크로드 아래에서 측정해야 의미가 있다.
견해: 장시간 측정이 필수다. 드문 사건(열 스로틀링·주기적 커널 작업·로그 회전)은 짧은 시험에서 안 나타난다. 몇 시간~며칠 실행해 최댓값을 본다.
애플리케이션 수준 측정(11.7.5):
| 측정 | 방법 |
|---|---|
| 콜백 주기 지터 | 간격 분포 |
| 콜백 지속 | steady_clock |
| 데드라인 초과 횟수 | 카운터 → 진단(6.9) |
| 종단 지연 | header.stamp |
견해: 데드라인 초과를 카운터로 세어 진단으로 내보내라(6.9.5). 운영 중에도 실시간성이 유지되는지 알 수 있고, 열화를 조기에 발견한다. "실시간 시스템인데 아무도 위반을 세지 않는" 상태가 흔하다.
12.2.11 커널 인터페이스와 스레드 상속 — 실측
이 장은 커널·부트 파라미터에 의존해 (확인 필요)가 18건으로 이 책에서 가장 많았다. 컨테이너에서 확인되는 부분과 안 되는 부분을 나눠 실제로 실행해 봤다(실행 확인, 일반 Ubuntu 24.04 커널 · privileged 컨테이너).
12.2.11.1 커널이 RT인지 확인하는 법
먼저 확인하지 못한 것을 밝힌다 — 이 환경은 PREEMPT_RT 커널이 아니다. 따라서 아래는 "RT가 아닌 커널에서 무엇이 보이는가"의 기록이며, RT 커널의 지연 특성은 검증하지 않았다.
| 확인 방법 | 이 환경의 결과 | 해석 |
|---|---|---|
cat /sys/kernel/realtime | 파일 없음 | PREEMPT_RT에만 존재한다 |
uname -v | #28~24.04.1-Ubuntu SMP **PREEMPT_DYNAMIC** … | 일반 커널 |
PREEMPT_DYNAMIC을 PREEMPT_RT로 오독하지 마라. 둘 다 PREEMPT로 시작해 uname -a | grep -i rt류 검사에 걸리기 쉽다. PREEMPT_DYNAMIC은 부팅 시 선점 수준을 고를 수 있다는 뜻이지 실시간 커널이라는 뜻이 아니다.
확실한 판별은
/sys/kernel/realtime의 존재 여부다 — 파일이 전혀 없으면 RT 커널이 아니다.
12.2.11.2 스레드는 정책을 전부 상속한다 — 그리고 그것이 문제다
이 절의 핵심 실측이다. chrt -f 50으로 ROS 2 노드를 실행하고 /proc/<pid>/task의 스레드를 전수 조사했다:
| tid | 정책 | 우선순위 | 스레드 이름 |
|---|---|---|---|
| 300 | SCHED_FIFO | 50 | talker(메인) |
| 304·305 | SCHED_FIFO | 50 | talker-ust(LTTng 계측, 11.6) |
| 306·307·317 | SCHED_FIFO | 50 | talker(rclcpp 내부) |
| 308 | SCHED_FIFO | 50 | dds.shm.wdog |
| 309 | SCHED_FIFO | 50 | dds.ev.0 |
| 310·311·313·316 | SCHED_FIFO | 50 | dds.udp.7410 · dds.udp.7400 · dds.udp.7411 · dds.udp.7000 |
| 312·315 | SCHED_FIFO | 50 | dds.shm.7411 · dds.shm.7000 |
| 314 | SCHED_FIFO | 50 | dds.asyn.0.0 |
열다섯 개 스레드가 전부 SCHED_FIFO 50이다. 리눅스에서 스레드는 생성 시 부모의 스케줄링 속성을 물려받으므로, chrt로 프로세스를 기동하면 그 뒤에 만들어지는 모든 스레드가 RT가 된다.
이것이 왜 문제인가.
- 의도한 설계가 아니다. "제어 루프만 RT, 나머지는 보통"으로 나누려 했는데 DDS 송수신·공유메모리 감시·트레이싱 스레드까지 같은 우선순위를 받는다.
- RT 스로틀링에 함께 걸린다. 95% 한도(12.2.5)는 RT 클래스 전체에 적용되므로, 통신 스레드가 쓰는 시간까지 제어 루프의 예산을 잠식한다.
- 우선순위가 같으면 선점이 안 된다.
SCHED_FIFO는 같은 우선순위끼리 양보 없이 순서대로 실행되므로, DDS 스레드가 길게 점유하면 제어 콜백이 기다린다.
견해: 프로세스 전체를
chrt로 기동하는 방식은 시작점일 뿐 최종 형태가 아니다. 제대로 하려면 코드 안에서 필요한 스레드만pthread_setschedparam으로 승격하고 나머지는SCHED_OTHER로 두어야 한다. 위 표의 스레드 이름이 그 작업의 지도가 된다 —dds.*는 통신,*-ust는 트레이싱이므로, 대개 제어 루프보다 낮은 RT 우선순위나 보통 정책이 맞다.
함정: 스레드 이름은 벤더에 따라 다르다. 위 이름들은 기본 RMW(Fast DDS)의 것이다. Cyclone DDS로 바꾸면(3.5) 이름 체계가 달라지므로, RMW를 교체하면 우선순위 설계를 다시 확인해야 한다.
확인 명령은 두 가지다:
ps -eLo pid,tid,cls,rtprio,comm | grep my_node # cls 가 FF 면 SCHED_FIFO
for t in /proc/<PID>/task/*; do chrt -p $(basename $t); done
12.2.11.3 우선순위 범위와 권한
chrt -m으로 정책별 우선순위 범위를 확인했다(실행 확인):
| 정책 | 우선순위 범위 |
|---|---|
SCHED_FIFO · SCHED_RR | 1 ~ 99 |
SCHED_OTHER · SCHED_BATCH · SCHED_IDLE | 0 / 0 |
SCHED_DEADLINE | 0 / 0 |
SCHED_DEADLINE이 0/0인 것은 미지원이라는 뜻이 아니다. 이 정책은 우선순위 숫자가 아니라 주기·실행시간·데드라인 세 값으로 스케줄링하므로 우선순위 범위가 의미를 갖지 않는다(chrt -d로 설정한다). (확인 필요)였던 SCHED_DEADLINE의 실재는 이것으로 확인된다.
권한은 rtprio 한도가 가른다(실행 확인):
| 상황 | ulimit -r | chrt -f 50 결과 |
|---|---|---|
| root | (제한 없음) | 성공 |
| 일반 사용자(기본) | 0 | Operation not permitted |
limits.conf에 rtprio 99 부여 후 로그인 셸 | 99 | 성공 |
기본값이 0이라 일반 사용자는 RT를 못 준다. 실패가 조용하지 않고 명확한 오류라는 점은 다행이다.
# /etc/security/limits.conf
tester - rtprio 99
tester - memlock unlimited
함정:
limits.conf는 로그인 세션에만 반영된다.su tester처럼 로그인 셸을 거치지 않으면ulimit -r이 그대로 0이다(실행 확인 —su - tester로는 99가 됐다). systemd 서비스도limits.conf를 보지 않으므로 유닛 파일에LimitRTPRIO=·LimitMEMLOCK=을 직접 적어야 한다(14.8).
참고:
memlock기본 한도도 낮다 — 이 환경에서 8192 KB였다.mlockall(12.3)로 전체 메모리를 잠그려면 이 한도부터 상향해야 하며, 상향하지 않으면 잠금이 부분적으로만 걸린다.
12.2.12 설정 경로·옵션·파라미터를 실제로 확인했다 — 실측
이 장은 커널·부트 파라미터 의존이 커서 (확인 필요)가 가장 많은 장이었다. 그중 컨테이너와 호스트의 /sys·/proc·패키지만으로 닫을 수 있는 것을 골라 한꺼번에 확인했다.
확인한 환경: Ubuntu 24.04 호스트(20코어,
intel_pstate),ros:jazzy-ros-base컨테이너. 부트 파라미터가 필요한 항목(isolcpus·nohz_full·C-state)은 재부팅 없이 확인할 수 없어 그대로 남겨 두었다.
12.2.12.1 IRQ 친화도 — 16진 비트마스크, 그리고 더 쉬운 길
$ cat /proc/irq/0/smp_affinity
fffff
$ cat /proc/irq/0/smp_affinity_list
0-19
smp_affinity는 16진 비트마스크다 — 20코어 머신에서 전 코어 허용이 fffff(20비트). 본문 예시의 echo 7은 0b0111로 맞다.
그런데 smp_affinity_list가 같은 디렉터리에 있다. 0-2 같은 목록을 그대로 쓰므로 코어 수가 많을 때 실수가 훨씬 적다. 32코어에서 "0~3번만"을 비트마스크로 쓰면 0000000f인데, 자릿수를 하나 틀리면 조용히 잘못된 코어에 할당된다.
견해: 스크립트에는
smp_affinity_list를 쓰라. 비트마스크는 사람이 검토하기 어렵고, 검토되지 않는 설정은 틀린 채로 오래 유지된다.
12.2.12.2 irqbalance — 설정 파일과 네 개의 변수
패키지를 설치해 기본 설정 파일을 확인했다(/etc/default/irqbalance, systemd가 EnvironmentFile로 읽는다).
| 변수 | 뜻 |
|---|---|
IRQBALANCE_BANNED_CPULIST | IRQ를 할당하지 않을 코어 목록 — 0-2·0,3,5-7 형식 |
IRQBALANCE_BANNED_CPUS | 같은 뜻의 64비트 비트마스크(읽기 어렵다) |
IRQBALANCE_ONESHOT | 시작 10초 뒤 한 번만 균형 잡고 종료 |
IRQBALANCE_ARGS | 데몬에 넘길 추가 인자 |
파일의 설명에 "기본은 10초마다 재분배"라고 적혀 있다 — 수동으로 설정한 친화도가 10초 뒤에 되돌아가는 이유다.
RT 코어를 격리했다면
IRQBALANCE_BANNED_CPULIST에 같은 코어를 적어라.isolcpus로 스케줄러에서 빼도 IRQ는 그것과 별개로 배정된다 — 격리했다고 믿는 코어에 네트워크 인터럽트가 계속 떨어지는 것이 흔한 함정이다.
12.2.12.3 거버너 — 경로는 맞았지만 선택지가 둘뿐이었다
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver
intel_pstate
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
performance powersave
경로는 본문 그대로 맞다. 다만 intel_pstate 드라이버에서는 거버너가 performance와 powersave 둘뿐이다 — ondemand·conservative·schedutil은 목록에 없다. 다른 문서에서 본 거버너 이름을 그대로 넣으면 실패한다.
대신 이 드라이버에는 에너지·성능 선호도(EPP) 라는 별도 손잡이가 있다.
$ cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences
default performance balance_performance balance_power power
함정:
scaling_available_governors를 먼저 읽어라. 드라이버(intel_pstate·acpi-cpufreq·cpufreq-dt)에 따라 선택지가 완전히 다르다. 로봇의 ARM SoC에서는 또 다른 목록이 나온다(확인 필요: ARM 플랫폼별 목록).
12.2.12.4 controller_manager의 RT 우선순위 — thread_priority, 기본 50
본문이 "파라미터로 지원하는지·이름이 무엇인지 버전 의존"이라고 남겼던 항목이다. 확정했다.
[INFO] Spawning controller_manager RT thread with scheduler priority: 50
[WARN] Could not enable FIFO RT scheduling policy: with error number <1>(Operation not permitted).
권한을 주면 성공한다.
$ docker run --cap-add SYS_NICE …
[INFO] Spawning controller_manager RT thread with scheduler priority: 50
[INFO] Successful set up FIFO RT scheduling policy with priority 50.
| 항목 | 확인된 값 |
|---|---|
| 파라미터 이름 | thread_priority |
| 기본값 | 50 |
| 정책 | SCHED_FIFO |
| 권한 없을 때 | 경고만 내고 계속 실행된다 |
-p thread_priority:=80으로 실행하면 로그가 priority: 80으로 바뀌는 것까지 확인했다.
함정 둘이 겹쳐 있다. ① 권한이 없으면 조용히 격하된다. 예외도 종료도 없이 일반 스케줄링으로 실행된다 — 로그 한 줄을 놓치면 "RT로 실행되고 있다"고 믿은 채 운영하게 된다. 컨테이너 배포라면
--cap-add SYS_NICE(또는ulimit -r)가 필요하다(14.8). ② 파라미터 이름을 틀리면 조용히 무시된다.rt_priority:=80으로 줘 봤더니 경고 없이 기본값 50으로 실행됐다 — 11.1.14.3에서 본 것과 같은 방식의 실패다.
확인 순서는 로그다. ros2_control_node를 실행하고 Successful set up FIFO가 출력되는지 보라. 그 한 줄이 RT 설정 전체의 성패를 말해 준다.
12.2.12.5 cyclictest 옵션은 본문 그대로였다
rt-tests 패키지를 설치해 --help와 대조했다 — 본문의 여섯 옵션이 모두 실재하고 뜻도 같다.
| 옵션 | 긴 이름 | 뜻 |
|---|---|---|
-m | --mlockall | 현재·미래 메모리 잠금 |
-p | --priority | 최고 우선순위 스레드의 우선순위 |
-i | --interval | 주기(µs), 기본 1000 |
-h | --histogram | 종료 후 지연 히스토그램 출력 |
-q | --quiet | 종료 시 요약만 |
-D | --duration | 시험 지속 시간 |
추가로 알아 둘 것: -t(--threads)는 코어마다 한 스레드를, -S(--smp)는 표준 SMP 시험(-a -t + 동일 우선순위)을 뜻한다. 다중 코어 로봇에서는 -S가 출발점으로 낫다.
12.2.12.6 닫지 못한 것
정직하게 남긴다 — 부팅이 필요한 항목은 이 방법으로 확인할 수 없다.
| 항목 | 왜 못 닫았나 |
|---|---|
isolcpus·nohz_full의 실제 효과 | 부트 파라미터 변경 + 재부팅 필요 |
| C-state 제한 | 동일 |
| Ubuntu 24.04의 RT 커널 제공 형태 | 구독(Ubuntu Pro) 상태에 따라 다름 |
| GPU·특수 드라이버와 RT의 상성 | 해당 하드웨어 필요 |
12.2.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | RT 커널만 설치하고 끝 | 지터 그대로 | 전 층 조치 |
| 2 | 권한 부족 · 반환값 미검사 | RT인 줄 알지만 아님 | 검사·명시적 실패 |
| 3 | 프로세스 전체를 chrt | DDS 스레드까지 전부 RT가 되어 의미 상실 | 필요한 스레드만 코드에서 승격(12.2.11.2) |
| 4 | SCHED_FIFO 무한 루프 | 시스템 잠김 | 스로틀링 유지·여분 세션 |
| 5 | RT 스로틀링 해제 | 복구 불가 위험 | 유지 |
| 6 | 우선순위 역전 미고려 | 무한 대기 | PI 뮤텍스·락 회피 |
| 7 | isolcpus만 하고 미배치 | 노는 코어 | taskset |
| 8 | irqbalance 방치 | 친화도 원복 | 비활성·설정 |
| 9 | IRQ가 RT 코어에 | 지터 | 친화도 이동 |
| 10 | C-state·거버너 방치 | 큰 지터 | performance·제한 |
| 11 | idle=poll을 배터리 로봇에 | 전력·발열 | 절충 판단 |
| 12 | 하이퍼스레딩 무시 | 짝 스레드 간섭 | 비우기·SMT off |
| 13 | 무부하 측정 | 낙관적 | 실제 부하 |
| 14 | 짧은 측정 | 드문 사건 놓침 | 장시간 |
| 15 | GPU 드라이버 호환 미확인 | RT 도입 실패 | 사전 확인 |
| 16 | 데드라인 위반 미집계 | 열화 모름 | 카운터·진단 |
| 17 | uname | grep -i rt 로 RT 판별 | PREEMPT_DYNAMIC을 RT로 오독 | /sys/kernel/realtime 존재 확인(12.2.11.1) |
| 18 | systemd 서비스에 limits.conf 기대 | ulimit -r이 0 그대로 | 유닛에 LimitRTPRIO= 명시(12.2.11.3) |
| 19 | RT 권한 없이 controller_manager 운영 | 경고만 내고 일반 스케줄링 | 로그의 Successful set up FIFO 확인(12.2.12.4) |
| 20 | 격리 코어에 IRQ가 계속 배정됨 | isolcpus와 IRQ는 별개 | IRQBALANCE_BANNED_CPULIST(12.2.12.2) |
| 21 | 다른 문서의 거버너 이름을 그대로 사용 | intel_pstate엔 둘뿐 | scaling_available_governors 먼저(12.2.12.3) |
12.2.10 체크포인트
- 일반 Linux에서 RT 스레드가 지연되는 근본 원인은?
- PREEMPT_RT가 바꾸는 네 가지와, 그 대가 네 가지는?
- "RT 커널은 필요조건이지 충분조건이 아니다"의 의미는?
- 스케줄링 정책 여섯 가지와 각각의 용도를 말할 수 있는가. 우선순위 숫자를 갖는 것은 어느 둘인가.
- RT 스로틀링이 무엇이며, 끄지 말라는 조언의 근거는?
- 우선순위 설계 원칙 다섯 가지와, "모두 RT로"의 문제는?
- 우선순위 역전의 발생 경로와 대책 셋을 설명할 수 있는가.
- ROS 2에서 RT 우선순위를 설정하는 방법과, 반드시 확인해야 할 두 가지는? 프로세스 전체를
chrt로 기동하면 어떤 스레드까지 RT가 되는가. - 권한 실패 시 개발과 운영에서 다르게 처리해야 하는 이유는?
- CPU 격리 네 파라미터의 효과와,
isolcpus만으로 부족한 이유는? - 하이퍼스레딩이 실시간에 불리한 이유와 대처는?
irqbalance가 왜 문제이며, IRQ 친화도가 왜 중요한가.- 전원 관리의 네 가지 지터 원인과,
idle=poll의 절충은? - 열 스로틀링이 짧은 시험에서 안 보이는 이유는?
cyclictest가 측정하는 것과 측정하지 않는 것을 구분할 수 있는가.- 데드라인 위반을 세어 진단으로 내보내라는 조언의 근거는?
- 프로세스를
chrt로 기동하면 어느 스레드까지 RT가 되는가.PREEMPT_RT여부를 확실히 판별하는 방법은(12.2.11). controller_manager의 RT 우선순위 파라미터 이름과 기본값은? 권한이 없으면 어떻게 되는가(12.2.12.4).- IRQ 친화도를 지정하는 두 가지 파일과 각각의 형식은. 어느 쪽을 스크립트에 쓸 것인가(12.2.12.1).
- 코어를
isolcpus로 격리하면 IRQ도 함께 빠지는가(12.2.12.2).
다음 장 12.3는 두 번째 층 — 메모리다. 동적 할당이 왜 실시간의 적이며, 어떻게 피하는가.