AEGLOSLAB ROS 2 Jazzy 대전

제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: 대부분 예 RT 스레드가 깨어나야 할 시각 커널이 선점 가능? 임계 구역 종료까지 대기 = 지연 즉시 선점 → 실행
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_RRFIFO + 같은 우선순위 간 타임슬라이스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 우선순위 설계

모든 것을 높이면 아무것도 높지 않다.

우선순위 (높음 → 낮음) 99: 예약 · 커널 워치독 80~90: 하드 제어 루프 60~79: 센서 IRQ 스레드 · 통신 40~59: 보조 RT SCHED_OTHER: 일반 노드
원칙설명
주기가 짧을수록 높게(RMS)비율 단조 스케줄링
최소한만 RT로필요한 스레드만
최고 우선순위 회피99는 시스템용
IRQ 스레드와의 관계 고려데이터 원천보다 소비자를 낮게
문서화우선순위 표를 유지

견해: 우선순위 표를 문서로 유지하라. 여러 프로세스가 각자 우선순위를 정하면 충돌하고, 나중에 왜 그 값인지 아무도 모른다. 시스템 전체의 우선순위 배치를 한 곳에 적어 두면 변경 시 영향을 판단할 수 있다.

함정: RT 스레드를 너무 많이 만들면 서로 경쟁해 의미가 없어진다. "중요하니까 RT로"를 반복하면 결국 모두가 RT이고, 그중 낮은 것은 일반 스레드보다 나을 것이 없다. 정말 데드라인이 있는 것만 RT로 둔다.

우선순위 역전:

낮은 우선순위 L이 락 보유 높은 우선순위 H가 같은 락 요청 H가 대기 중간 우선순위 M이 L을 선점 ★ H가 M 때문에 무한정 대기
대책설명
우선순위 상속(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, &param) != 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 스레드에 전용 코어를 준다.

4코어 시스템 CPU 0-2: 일반 OS · ROS 노드 · 로깅 CPU 3: 격리 RT 제어 루프만 인터럽트
# 부트 파라미터 (GRUB — 확인 필요: 정확한 옵션 조합)
isolcpus=3 nohz_full=3 rcu_nocbs=3 irqaffinity=0-2
파라미터효과
isolcpus스케줄러가 그 코어에 일반 작업 배치 안 함
nohz_full타이머 틱 제거 — 인터럽트 감소
rcu_nocbsRCU 콜백을 다른 코어로
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정책우선순위스레드 이름
300SCHED_FIFO50talker(메인)
304·305SCHED_FIFO50talker-ust(LTTng 계측, 11.6)
306·307·317SCHED_FIFO50talker(rclcpp 내부)
308SCHED_FIFO50dds.shm.wdog
309SCHED_FIFO50dds.ev.0
310·311·313·316SCHED_FIFO50dds.udp.7410 · dds.udp.7400 · dds.udp.7411 · dds.udp.7000
312·315SCHED_FIFO50dds.shm.7411 · dds.shm.7000
314SCHED_FIFO50dds.asyn.0.0

열다섯 개 스레드가 전부 SCHED_FIFO 50이다. 리눅스에서 스레드는 생성 시 부모의 스케줄링 속성을 물려받으므로, chrt로 프로세스를 기동하면 그 뒤에 만들어지는 모든 스레드가 RT가 된다.

이것이 왜 문제인가.

  1. 의도한 설계가 아니다. "제어 루프만 RT, 나머지는 보통"으로 나누려 했는데 DDS 송수신·공유메모리 감시·트레이싱 스레드까지 같은 우선순위를 받는다.
  2. RT 스로틀링에 함께 걸린다. 95% 한도(12.2.5)는 RT 클래스 전체에 적용되므로, 통신 스레드가 쓰는 시간까지 제어 루프의 예산을 잠식한다.
  3. 우선순위가 같으면 선점이 안 된다. 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_RR1 ~ 99
SCHED_OTHER · SCHED_BATCH · SCHED_IDLE0 / 0
SCHED_DEADLINE0 / 0

SCHED_DEADLINE이 0/0인 것은 미지원이라는 뜻이 아니다. 이 정책은 우선순위 숫자가 아니라 주기·실행시간·데드라인 세 값으로 스케줄링하므로 우선순위 범위가 의미를 갖지 않는다(chrt -d로 설정한다). (확인 필요)였던 SCHED_DEADLINE의 실재는 이것으로 확인된다.

권한은 rtprio 한도가 가른다(실행 확인):

상황ulimit -rchrt -f 50 결과
root(제한 없음)성공
일반 사용자(기본)0Operation 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_CPULISTIRQ를 할당하지 않을 코어 목록 — 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 함정 카탈로그

#함정증상대처
1RT 커널만 설치하고 끝지터 그대로전 층 조치
2권한 부족 · 반환값 미검사RT인 줄 알지만 아님검사·명시적 실패
3프로세스 전체를 chrtDDS 스레드까지 전부 RT가 되어 의미 상실필요한 스레드만 코드에서 승격(12.2.11.2)
4SCHED_FIFO 무한 루프시스템 잠김스로틀링 유지·여분 세션
5RT 스로틀링 해제복구 불가 위험유지
6우선순위 역전 미고려무한 대기PI 뮤텍스·락 회피
7isolcpus만 하고 미배치노는 코어taskset
8irqbalance 방치친화도 원복비활성·설정
9IRQ가 RT 코어에지터친화도 이동
10C-state·거버너 방치큰 지터performance·제한
11idle=poll을 배터리 로봇에전력·발열절충 판단
12하이퍼스레딩 무시짝 스레드 간섭비우기·SMT off
13무부하 측정낙관적실제 부하
14짧은 측정드문 사건 놓침장시간
15GPU 드라이버 호환 미확인RT 도입 실패사전 확인
16데드라인 위반 미집계열화 모름카운터·진단
17uname | grep -i rt 로 RT 판별PREEMPT_DYNAMIC을 RT로 오독/sys/kernel/realtime 존재 확인(12.2.11.1)
18systemd 서비스에 limits.conf 기대ulimit -r이 0 그대로유닛에 LimitRTPRIO= 명시(12.2.11.3)
19RT 권한 없이 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는 두 번째 층 — 메모리다. 동적 할당이 왜 실시간의 적이며, 어떻게 피하는가.