제6부 하드웨어 인터페이스
6.6 실시간 루프와 ros2_control의 제약
6.6.1 이 장에서 배우는 것 / 선수 지식
6.1부터 "실시간"이라는 말을 계속 써 왔고, 6.1.6에서 "ros2_control을 쓰면 실시간이 보장된다는 것은 오해" 라고 명시했다. 이 장은 그 문장을 정확하게 만든다.
무엇이 보장되고 무엇이 아닌가. 제어 루프에서 정확히 무엇이 금지되며 그것을 어떻게 검증하는가. 그리고 실시간이 정말 필요한지 — 이 질문이 의외로 중요하다. 많은 로봇이 실시간 없이도 잘 동작하며, 실시간 시스템의 비용은 크다.
배우는 것:
- ros2_control이 보장하는 것과 아닌 것 — 정확한 경계.
- 제어 루프의 금지 목록 — 왜 각각이 문제인가.
realtime_tools전체 — 각 도구의 용도와 원리.- 주기 선택 — 통신·계산·제어 요구에서 도출하기.
- 지터 측정 — 실제로 얼마나 흔들리는지 아는 법.
- 실시간이 정말 필요한가 — 판단 기준.
- 실시간 시스템 구축 순서 — 무엇부터.
선수 지식: 6.1~6.5(ros2_control), 2.5(타이머 정밀도·드리프트), 2.6(메모리·할당), 2.9(스레드 안전·우선순위 역전). RT 커널·스케줄링은 12.2, 무할당 경로는 12.3에서 심화한다.
정직: 실시간 성능 수치는 인용하지 않는다(3.6.1의 방침). 하드웨어·커널·부하에 따라 자릿수가 달라지기 때문이다. 이 장은 측정 방법과 지배 요인을 가르친다.
realtime_toolsAPI·ros2_control_node의 RT 설정 지원 여부는 배포판 의존이며(확인 필요)로 표시했다(목차 3).(→ 168차 확인:realtime_tools의 네 도구 전부와 지터 측정 방법론(스레드 수·chrt·pgrep·topic hz)을 Jazzy에서 실측했다 — 6.6.4·6.6.6 실행 확인(168차) 참고.ros2_control_node의 RT 스케줄링 CLI 지원 자체(12.2이 다루는 영역)는 이번에도 시도하지 않았다.)(→ 169차 확인:RealtimePublisher는 168차엔 헤더만 확인했었는데, 이번에 실제ament_cmake패키지로 컴파일·실행하고 독립적으로 DDS 발행까지 확인해 네 도구 전부의 실제 실행 확인을 완결했다 — 6.6.4.2 실행 확인(169차) 참고.)
6.6.2 무엇이 보장되고 무엇이 아닌가
| 항목 | ros2_control | 필요한 추가 조치 |
|---|---|---|
| 루프 구조 | ✅ 제공 | — |
| 실행 순서 결정성 | ✅ 제공 | — |
| ROS 통신과의 분리 | ✅ 도구 제공 | 올바르게 사용해야(6.6.4) |
| 스케줄링 우선순위 | ❌ | RT 우선순위 설정(12.2) |
| 커널 선점성 | ❌ | PREEMPT_RT(12.2) |
| CPU 격리 | ❌ | isolcpus·affinity(12.2) |
| 메모리 잠금·무할당 | ❌ | mlockall·코드 규율(12.3) |
| 통신 지연 | ❌ | 하드웨어·프로토콜 선택(6.7) |
| 최악 지연 상한 | ❌ | 위 전부 + 측정 |
*정확한 진술: ros2_control은 실시간을 가능하게 하는 구조를 제공하지만, 실시간성 자체는 시스템 전체가 만든다.*
그리고 사용자 코드가 그것을 깨뜨릴 수 있다. update() 안에서 std::string을 만드는 순간 무할당이 깨지고, 로그 한 줄이 락을 획득한다(6.2.7·6.4.4).
견해: "프레임워크를 썼으니 실시간"이라는 믿음이 가장 위험하다. 측정하지 않으면 지터가 있는지도 모르고, 문제가 생겼을 때 원인을 프레임워크 밖에서 찾게 된다. 6.6.6의 측정을 반드시 하라.
6.6.3 금지 목록과 그 이유
제어 스레드(read()·update()·write())에서 금지되는 것과 근거:
| 금지 | 왜 문제인가 | 최악 영향 |
|---|---|---|
| 동적 할당 | malloc이 락을 획득하고, 힙 파편화 시 시간이 길어지며, 커널로 내려가 페이지를 요청할 수 있다 | 수십 µs ~ ms |
| 뮤텍스 대기 | 낮은 우선순위 스레드가 락을 쥐면 우선순위 역전(2.9.5) | 무제한 |
| 로깅 | 위 둘 + I/O + DDS 발행(/rosout — 1.11.3) | ms 단위 |
| ROS 통신 | 직렬화·네트워크·블로킹 가능 | 예측 불가 |
| 블로킹 I/O | 무한 대기 가능 | 무제한 |
| 페이지 폴트 | 커널이 페이지를 매핑 | 수십 µs ~ ms |
| 예외 던지기 | 스택 언와인딩·RTTI, 구현에 따라 락 | 예측 불가 |
| 시스템 호출(불필요한) | 컨텍스트 스위치 | µs 단위 |
"무제한"이 붙은 둘이 특히 위험하다. 뮤텍스 대기와 블로킹 I/O는 상한이 없다 — 최악의 경우 영원히 기다린다. 실시간 시스템에서 이것은 곧 제어 상실이다.
대안 표:
| 하고 싶은 것 | 실시간 안전한 방법 |
|---|---|
| 상태 알리기 | 원자적 카운터 + 별도 진단(1.11.7·6.9) |
| 명령 받기 | RealtimeBuffer(6.6.4) |
| 데이터 발행 | RealtimePublisher(6.6.4) |
| 버퍼 필요 | on_configure에서 사전 할당 |
| 오류 표시 | 정수 코드 + 상태 인터페이스 |
| 문자열 | 고정 크기 배열·정수 ID |
| 조건부 처리 | 분기는 괜찮음(예측 가능) |
할당을 만드는 숨은 것들:
// ❌ 전부 할당을 유발할 수 있다
std::string msg = "error " + std::to_string(code); // 문자열
vec.push_back(x); // 벡터 성장
std::function<void()> f = [captured]() {...}; // 큰 캡처
auto ptr = std::make_shared<T>(); // 제어 블록
std::vector<double> tmp(n); // 지역 벡터
sstream << value; // 스트림
// ✅ 안전
std::array<double, 8> tmp{}; // 스택
error_code_.store(code, std::memory_order_relaxed); // 원자
preallocated_vec_[i] = x; // 사전 할당
함정:
std::function에 큰 객체를 캡처하면 힙 할당이 일어난다. 콜백을 실시간 경로에 두는 설계는 이 점을 확인해야 한다. 작은 캡처는 SBO(small buffer optimization)로 스택에 들어가지만 보장되지 않는다.
6.6.4 realtime_tools 상세
6.6.4.1 RealtimeBuffer — 비실시간 → 실시간
realtime_tools::RealtimeBuffer<Command> buffer_;
// 비실시간(구독 콜백)
buffer_.writeFromNonRT(cmd);
// 실시간(update)
const Command * cmd = buffer_.readFromRT();
원리: 이중 버퍼 + 원자적 포인터 교체(2.9.4의 패턴).
| 성질 | 설명 |
|---|---|
| 읽기 측 | 절대 블로킹되지 않는다 |
| 쓰기 측 | 락을 쓸 수 있다(비실시간이므로 허용) |
| 값 | 항상 최신 완결된 값 |
| 손실 | 빠르게 여러 번 쓰면 중간값은 유실 |
함정:
Command타입이 힙을 쓰면(std::vector멤버) 복사 시 할당이 일어난다.writeFromNonRT는 비실시간이라 괜찮지만, 읽기 측이 값을 복사하면 실시간에서 할당이 된다.readFromRT()가 포인터를 반환하는 이유이며, 복사하지 말고 포인터로 읽으라.
실행 확인(168차, Jazzy) — 위
writeFromNonRT/readFromRT패턴을 실제로 컴파일·실행해 핵심 주장(비블로킹·값 완결성)을 정량으로 확인했다. 비실시간 쓰기 스레드(50µs 간격)와 '실시간' 읽기 스레드(블로킹 없이 최대한 자주 읽음)를 2초간 동시 실행했다 — 쓰기 13,152회, 읽기 23,771,005회(초당 약 1,200만 회 — "절대 블로킹되지 않는다"는 표의 주장과 일치), 읽기 최대 지연 74.6µs(버퍼 자체가 아니라 OS 스케줄링 변동으로 추정), 찢어지거나 역전된 값 0건(2,300만 번 넘는 동시 읽기 동안 단 한 번도 불완전한 값을 못 봄) — "값은 항상 최신 완결된 값"이라는 표의 주장이 정량으로 확인됐다. 원본:tools/gap_harness/gap168/rt_buffer_test/(test_realtime_buffer.cpp·run_result.log).
6.6.4.2 RealtimePublisher — 실시간 → 비실시간
auto pub = node->create_publisher<Msg>("~/state", qos);
auto rt_pub = std::make_shared<realtime_tools::RealtimePublisher<Msg>>(pub);
// on_configure — 크기 미리 확보 (6.4.5)
rt_pub->msg_.data.resize(n);
// update() — 실시간
if (rt_pub->trylock()) {
rt_pub->msg_.data[0] = value;
rt_pub->unlockAndPublish();
}
원리: 별도 스레드가 실제 발행을 담당한다. update()는 버퍼에 쓰고 조건변수로 신호만 보낸다.
함정:
trylock()없이lock()을 쓰면 블로킹된다. 실시간 경로에서는 항상trylock()이며, 실패하면 건너뛴다. 상태 발행 한 번을 놓치는 것이 주기를 놓치는 것보다 낫다(6.4.4).
함정:
msg_의 크기를update()에서 바꾸면 할당이다.resize·push_back을 실시간에서 하면RealtimePublisher를 쓰는 의미가 없어진다.
실행 확인(169차, Jazzy) — 위
trylock/unlockAndPublish패턴을 실제ament_cmake패키지로 빌드해 실행하고, 독립적으로 DDS 발행까지 확인했다.rclcpp::Node+SingleThreadedExecutor로 실제 스핀시키며 5kHz 흉내 루프로 3초간 실행한 결과 trylock 성공 11,728~11,845회, 실패 13~31회(성공률 99.7~99.9%)였다 — "실패하면 건너뛴다" 설계와 일치한다. 별도 프로세스에서ros2 topic hz로 독립 확인한 결과 평균 3880.249Hz로 실제 DDS 메시지가 발행되고 있었다 — "별도 스레드가 실제 발행을 담당한다"는 원리 설명이 확인됐다. 원본:tools/gap_harness/gap169/rt_pub_test/(test_realtime_publisher.cpp·run_result2.log·topic_hz_during_run2.log).
6.6.4.3 RealtimeBox — 객체 교환
포인터나 객체를 실시간·비실시간 사이에서 안전하게 교환한다(확인 필요: Jazzy에서의 API 형태)(→ 168차 확인: RealtimeBox는 Jazzy에서 폐기(deprecated)됐고 현재 이름은 RealtimeThreadSafeBox다 — 아래 실행 확인(168차) 참고).
| 도구 | 방향 | 용도 |
|---|---|---|
RealtimeBuffer | 비RT → RT | 명령·파라미터 |
RealtimePublisher | RT → 비RT | 상태 발행 |
RealtimeBox | 양방향 | 객체 참조 교환 |
RealtimeServerGoalHandle | RT ↔ 액션 | 액션 목표 처리(확인 필요)(→ 168차 확인: 실재하며 폐기 표시 없음 — 아래 실행 확인(168차) 참고) |
견해: 이 도구들은 "실시간 안전"을 이름에 걸고 있지만, 잘못 쓰면 안전하지 않다. 담는 타입이 힙을 쓰거나, 크기를 실시간에서 바꾸거나,
lock()을 쓰면 깨진다. 도구를 쓰는 것과 실시간 안전한 것은 다르다 — 측정으로 확인해야 한다(6.6.6).
실행 확인(168차, Jazzy) —
RealtimeBox·RealtimeServerGoalHandle의 정확한 API 형태를 소스로 확인했다.realtime_box.hpp를 직접 읽으니,RealtimeBox는 여전히 쓸 수 있지만 컴파일 시#pragma message로 명시적 폐기 경고가 출력된다 —"'RealtimeBox' is deprecated. Please update your code to use 'realtime_thread_safe_box.hpp' header, and class name 'RealtimeThreadSafeBox' instead."내부적으로RealtimeBox<T> = RealtimeBoxStandard<T> = RealtimeThreadSafeBox<T, std::mutex>다.RealtimeBoxBestEffort도 별도로 폐기돼 있는데(RealtimeBox대신 쓰라고 안내), 그RealtimeBox자체도 폐기라 이중 폐기 체인이다 — Jazzy에서RealtimeBox라는 이름은 여전히 동작하지만 폐기됐고, 현재 권장 이름은RealtimeThreadSafeBox다.realtime_server_goal_handle.hpp에서는template <class Action> class RealtimeServerGoalHandle을 확인했다 —setAborted·setCanceled·setSucceeded·setFeedback·execute·valid·runNonRealtime메서드가 실재하고 폐기 표시가 없다. 원본:tools/gap_harness/gap168/rt_tools_test/(header_list.log·realtime_box_hpp.log·realtime_server_goal_handle_hpp.log).
6.6.5 주기 선택
6.3.7의 부등식을 다시 본다:
$$T_{\text{주기}} > t_{\text{read}} + t_{\text{update}} + t_{\text{write}} + t_{\text{여유}}$$
각 항의 지배 요인:
| 항 | 지배 요인 | 전형적 크기 |
|---|---|---|
| $t_{\text{read}}$·$t_{\text{write}}$ | 하드웨어 통신 | 프로토콜·버스 속도 의존(6.7) |
| $t_{\text{update}}$ | 컨트롤러 계산 | 대개 작음(관절 수에 비례) |
| $t_{\text{여유}}$ | OS 지터·기타 | 시스템 구성 의존 |
대부분의 경우 통신이 지배한다. 시리얼 1 Mbps로 관절 6개의 상태를 읽고 명령을 쓰면 프레임 크기와 왕복 지연이 주기를 정한다.
제어 요구에서 도출하기:
| 대상 | 필요 주기(대략) |
|---|---|
| 이동 로봇 속도 제어 | 20~100 Hz |
| 관절 위치 제어(하드웨어가 서보) | 50~250 Hz |
| 관절 토크 제어 | 500 Hz ~ 수 kHz |
| 힘 제어·접촉 작업 | 1 kHz 이상 |
| 시각 서보 | 카메라 주기(30~60 Hz) |
정직: 위 수치는 일반적으로 알려진 범위이며 시스템에 따라 크게 다르다
(확인 필요). 제어 대역폭 요구는 로봇 동역학·관절 유연성·목표 성능에서 나오며, 이론적 배경은 자매서robotics(RB-63·RB-66)가 다룬다.
절차:
견해: 통신 시간을 먼저 측정하라(6.3.7의 반복). "1 kHz로 하고 싶다"에서 시작하면 안 되는 이유는, 통신이 그것을 허용하지 않을 수 있기 때문이다. 하드웨어 선택 단계에서 이미 주기 상한이 정해진다 — 이것이 6.7에서 버스 선택이 중요한 이유다.
6.6.6 지터 측정
설정값이 아니라 실측이 진실이다(2.5.6의 반복).
① 컴포넌트 안에서 측정:
// read()/write() 소요 시간 — 실시간 안전하게
hardware_interface::return_type MyRobotHardware::read(
const rclcpp::Time &, const rclcpp::Duration & period)
{
const auto t0 = std::chrono::steady_clock::now(); // 1.10.2
// ... 실제 읽기 ...
const auto dt_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - t0).count();
// 원자적 갱신만 — 할당·로깅 없이
read_time_ns_.store(dt_ns, std::memory_order_relaxed);
if (dt_ns > read_max_ns_.load(std::memory_order_relaxed)) {
read_max_ns_.store(dt_ns, std::memory_order_relaxed);
}
// 주기 지터도 기록
const double period_ms = period.seconds() * 1000.0;
period_hist_.record(period_ms); // 락프리 히스토그램 (사전 할당)
return hardware_interface::return_type::OK;
}
② 진단으로 내보내기(6.9):
| 지표 | 의미 |
|---|---|
| 주기 p50·p99·p99.9·max | 지터 분포(2.5.6) |
read() 최대 시간 | 통신 최악값 |
write() 최대 시간 | 동일 |
| 주기 초과 횟수 | 지연된 횟수 |
③ 외부 측정:
# 제어 루프 주기의 대리 지표
ros2 topic hz /joint_states --window 1000
# 스레드 확인 (프로세스 이름으로 정확히 — 아래 함정 참고)
PID=$(pgrep -x ros2_control_node)
ps -T -p $PID
top -H -p $PID
# 스케줄링 정책·우선순위 확인 — 스레드마다 다를 수 있다
chrt -p $PID # 메인 스레드만
for t in /proc/$PID/task/*; do chrt -p $(basename $t); done # 전 스레드
ps -eLo pid,tid,cls,rtprio,comm | grep ros2_control
함정:
chrt -p <PID>는 메인 스레드 하나만 보여 준다. ROS 2 노드는 DDS 송수신·트레이싱까지 스레드가 열 개를 훌쩍 넘고(실측 사례는 15개, 12.2.11.2), 정책이 스레드마다 다를 수 있다./proc/<PID>/task를 순회해 전부 확인하지 않으면 "RT로 설정했다"는 착각이 남는다.
함정:
pgrep -f(전체 명령줄 매칭)는 자기 자신까지 매칭한다. 이 책의 다른 진단 예시도-f를 쓰지만, 정확히는pgrep -x가 안전하다 — 이유와 대안은 11.1.13에 정리했다.(→ 168차 정정: 이 절이 예시로 쓰는 바로 그 프로세스 이름ros2_control_node(17자)에서pgrep -x가 실제로 실패함을 확인했다 — 아래 실행 확인(168차) 참고. 리눅스 커널의comm필드가 15자로 잘리기 때문이며,pgrep도 스스로 경고를 낸다.)
실행 확인(168차, Jazzy) — 스레드 수·
chrt·pgrep·topic hz를 실제ros2_control_node로 확인했다.mock_components/GenericSystem+joint_state_broadcaster로 실제 노드를 실행하고 측정했다.
- 스레드 수: 37개(
/proc/PID/task기준) — 책이 인용한 "실측 사례 15개"(12.2.11.2)보다도 많다. "열 개를 훌쩍 넘는다"는 정성적 주장이 실측으로 확인됐다.chrt -p <메인PID>는 정확히 1개 스레드만 보여 줬고, 37개 전체를 보려면/proc/PID/task/*를 순회해야 했다 — 위 함정 그대로 재현됐다(이 미조정 상태에서는 37개 전부SCHED_OTHER, 우선순위 0으로 동일).pgrep -x ros2_control_node가 조용히 매칭 0건이었다.ros2_control_node는 17자인데 리눅스 커널의comm필드는 15자로 잘린다(ros2_control_no).pgrep -x는 잘린comm과 정확히 일치해야 매칭하므로, 원래 이름으로 호출하면 매칭이 안 된다(pgrep자신도 경고를 출력한다:"pattern... longer than 15 characters will result in zero matches"). 대안 셋을 비교했다 —pgrep -x ros2_control_no(잘린 이름, 동작),pidof ros2_control_node(정상 동작),pgrep -f '[r]os2_control_node'(대괄호 트릭, 동작하지만 래퍼 프로세스까지 4개가 매칭됨). 위 함정의 "정확히는pgrep -x가 안전하다"는 조언은, 이 절 자신이 예시로 쓰는 프로세스 이름에서는 성립하지 않는다 — 15자를 넘는 이름에는pidof나 잘린 이름을 쓰거나 대괄호 트릭을 써야 한다.ros2 topic hz /joint_states --window 200: 평균 99.99Hz(설정한update_rate: 100과 정확히 일치, std dev 0.09~0.47ms) — 제어 루프 주기의 대리 지표라는 주장을 정량으로 확인했다.컨테이너
gap168j. 원본:tools/gap_harness/gap168/thread_test/(ps_T_result.log·chrt_all_threads.log·topic_hz_result.log).
④ 시스템 수준 지터 측정:
# 커널 스케줄링 지연 (RT 시스템 표준 도구)
sudo cyclictest -t1 -p 80 -n -i 1000 -l 100000
# (rt-tests 패키지 — 12.2)
cyclictest가 시스템의 실시간 능력 상한을 알려준다. 이 값보다 좋은 지터는 애플리케이션에서 얻을 수 없다. 시스템이 1 ms 지터를 보이면 500 µs 제어 주기는 불가능하다.
견해: 순서가 중요하다. ①
cyclictest로 시스템 능력 확인 → ②read/write시간 측정 → ③ 주기 결정 → ④ 실제 분포 검증. 이 순서를 건너뛰고 주기부터 정하면 나중에 되돌아온다.
⑤ 트레이싱(11.6):
ros2_tracing으로 제어 루프의 각 단계를 커널 수준에서 추적할 수 있다. 측정 코드가 대상을 방해하지 않는 장점이 있다.
6.6.7 실시간이 정말 필요한가
이 절이 이 장에서 가장 실무적일 수 있다.
실시간이 불필요한 경우:
| 상황 | 이유 |
|---|---|
| 하드웨어가 자체 서보 제어 | 상위는 목표만 보내면 됨 |
| 이동 로봇 속도 제어(20~50 Hz) | 여유가 크다 |
| 위치 명령 전달(하드웨어가 보간) | 타이밍 민감도 낮음 |
| 시각 서보(카메라가 병목) | 30 Hz가 상한 |
실시간이 필요한 경우:
| 상황 | 이유 |
|---|---|
| 토크·전류 제어 | 높은 대역폭 |
| 힘 제어·접촉 | 불안정화 위험 |
| 안전 감시 루프 | 최악 지연 상한이 요구 |
| EtherCAT 분산 클럭 | 프로토콜이 요구(6.7) |
| 고속 동역학 로봇 | 안정성 |
견해: "실시간이면 좋겠지"로 도입하지 말라. RT 커널 유지·CPU 격리·무할당 코드 검증은 실제 비용이고, 개발 속도를 떨어뜨린다. 요구가 명확할 때만 지불하라. 많은 이동 로봇과 협동 로봇 응용이 일반 커널에서 잘 동작한다.
견해: 반대로 안전 기능이 그 루프에 있다면 실시간은 협상 대상이 아니다. "평균적으로 빠르다"는 안전 논증이 아니다 — 최악 지연에 상한이 있어야 하며, 그것을 보장하려면 RT 구성과 측정이 필요하다(14.10).
중간 지대: 제어 루프는 일반 커널에서 실행하되, 안전 감시만 하드웨어나 별도 MCU로 분리하는 구성이 실용적이다(6.8). 비상 정지·한계 감시를 소프트웨어 실시간에 의존하지 않는 것이다.
6.6.8 구축 순서
실시간 시스템을 만들기로 했다면 순서가 있다(2.6.8의 확장).
1·2번이 먼저인 이유: 요구를 모르면 언제 멈출지 모르고, 시스템 능력을 모르면 달성 불가능한 목표를 세운다.
5번(코드 규율)이 6~8번보다 먼저인 이유: RT 커널을 켜도 update()에서 할당하면 소용없다. 애플리케이션 코드가 먼저 정리돼 있어야 시스템 튜닝의 효과가 보인다(2.6.8의 순서 논리와 같다).
9번 검증:
| 확인 | 방법 |
|---|---|
| 주기 분포 | p99.9·max(6.6.6) |
| 할당 여부 | 페이지 폴트 카운터(2.6.7) |
| 우선순위 적용 | chrt -p |
| CPU 격리 | taskset -p·/proc/*/status |
| 장시간 안정성 | 수 시간 이상 연속 측정 |
견해: 장시간 측정이 중요하다. 몇 분 실행해서는 안 나타나는 문제가 있다 — 메모리 파편화 누적, 로그 회전, 주기적 시스템 작업(cron·업데이트 확인). 최소 몇 시간, 가능하면 하루 이상 실행해 최악값을 보라.
운영 중 상시 감시:
// 주기 초과를 진단으로 (6.9)
if (period.seconds() > period_limit_) {
++overrun_count_; // 원자적
}
// 별도 진단 타이머가 overrun_count_ 를 보고
현장에서 지터가 악화되는 것을 조기에 아는 것이 목적이다. 부하 증가·하드웨어 열화·설정 변경이 원인일 수 있다.
6.6.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | "프레임워크 = 실시간"으로 믿음 | 지터를 모른 채 운영 | 측정(6.6.6) |
| 2 | update()에서 문자열·벡터 성장 | 할당 지터 | 사전 할당·정수 코드 |
| 3 | RealtimeBuffer 값 복사 | 실시간에서 할당 | 포인터로 읽기 |
| 4 | RealtimePublisher lock() 사용 | 블로킹 | trylock() |
| 5 | msg_ 크기를 실시간에서 변경 | 할당 | on_configure에서 |
| 6 | 뮤텍스 대기 | 우선순위 역전(무제한) | 락프리·try_lock |
| 7 | 블로킹 I/O | 무한 대기 가능 | 타임아웃·논블로킹 |
| 8 | 주기부터 정하고 통신 맞춤 | 루프 밀림 | 측정 후 결정 |
| 9 | 시스템 능력 미확인 | 달성 불가 목표 | cyclictest 먼저 |
| 10 | 코드 규율 없이 RT 커널 | 효과 없음 | 순서 준수(6.6.8) |
| 11 | 짧게 측정하고 확정 | 드문 최악값 놓침 | 장시간 측정 |
| 12 | 필요 없는데 실시간 도입 | 비용만 증가 | 요구 판단(6.6.7) |
| 13 | 안전 기능을 소프트 실시간에 의존 | 안전 논증 불가 | 하드웨어·MCU 분리 |
| 14 | 운영 중 감시 없음 | 열화를 모름 | 주기 초과 진단 |
| 15 | chrt -p <PID> 만 확인 | 메인 스레드만 보여 RT인 줄 착각 | /proc/<PID>/task 순회(12.2.11.2) |
| 16 | pgrep -x로 15자 넘는 프로세스명 검색(168차 실측 발견) | 조용히 매칭 0건 | pidof, 잘린 이름, 또는 pgrep -f '[문자]나머지' |
6.6.10 체크포인트
- ros2_control이 제공하는 것 네 가지와 제공하지 않는 것 네 가지를 구분할 수 있는가.
- "프레임워크를 썼으니 실시간"이 왜 위험한 믿음인가.
- 제어 루프 금지 목록에서 "무제한" 영향을 갖는 둘은 무엇이며 왜인가.
- 할당을 유발하는 숨은 코드 패턴 다섯 가지를 들 수 있는가.
RealtimeBuffer의 읽기 측이 포인터를 반환하는 이유는?RealtimePublisher에서trylock()을 쓰고 실패 시 건너뛰는 것이 왜 옳은가.- "도구를 쓰는 것과 실시간 안전한 것은 다르다"의 의미를 예로 설명할 수 있는가.
- 제어 주기 부등식의 각 항과 지배 요인을 말할 수 있는가. 대부분의 경우 무엇이 지배하는가.
- 주기 결정 절차 다섯 단계를 순서대로 말할 수 있는가. 왜 통신 측정이 먼저인가.
cyclictest가 알려주는 것과, 그것이 왜 첫 번째 측정인가.- 실시간이 불필요한 경우와 필요한 경우를 각각 세 가지 들 수 있는가. 안전 기능이 있으면 왜 협상 대상이 아닌가.
- 실시간 구축 순서에서 코드 규율이 RT 커널보다 먼저인 이유는?
- 장시간 측정이 필요한 이유와, 운영 중 상시 감시의 목적은?
다음 장 6.7는 이 루프의 지배 요인이었던 하드웨어 통신 자체를 다룬다 — 시리얼·CAN·EtherCAT의 특성과 드라이버 작성 원칙.