임베디드 소스코드를 보다 보면 가끔 변수 타입 앞에 단어 volatile 하나가 더 붙어 있는 경우를 볼 수 있습니다. 뭐 특별한 기능을 하고 있는 것 같지도 않은데 말이죠.
1. volatile의 정의
영어 단어로서 volatile 의미는 휘발성의, 불안정한, 휘발하는 뜻을 가지고 있습니다. 이런 의미로 프로그래밍에서 volatile은 컴파일러에게 특정 변수의 값이 프로그램 흐름에 의해 예측할 수 없으므로, 항상 메모리에서 직접 읽고 쓰도록 지시하는 키워드입니다.
최적화 과정에서 해당 변수의 값이 캐시 되지 않도록 보장하고 주로 하드웨어 레지스터, 멀티스레드 환경의 공유 변수, 인터럽트 핸들러에서 사용되는 변수에 적용됩니다.
2. 하드웨어 코드 예시
변수의 값을 최적화하지 말고 메모리에서 직접 읽고 쓰도록 컴파일러에 지시하는 키워드입니다.
이는 하드웨어 레지스터나 멀티스레드 환경에서 공유 자원에 접근하는 경우 유용합니다.
volatile int* port = (volatile int*)0x4000; // 하드웨어 포트 주소
void writePort(int value) {
*port = value; // 하드웨어 포트에 값 쓰기
}
int readPort() {
return *port; // 하드웨어 포트에서 값 읽기
}
3. 컴파일러가 최적화하는 문제
위에서 본 예제처럼 단순하게 작업한다면 대략 그렇겠구나 생각할 수 있지만, 조금 다른 코드를 보겠습니다.
// A라는 IO 장치가 Memory-Mapped 주소 0x4000에 매핑되어 있다고 가정하고 값을 쓸 경우,
*(unsigned int*)0x4000 = 0;
*(unsigned int*)0x4000 = 1;
*(unsigned int*)0x4000 = 2;
위의 코드는 개발자가 의도한 데로 작동할 수도 있고 아닐 수도 있습니다. 컴파일러가 코드최적화 단계에 따라서 다를 수 있는데, 컴파일러 입장에서 메모리에 순차적으로 대입하는 것이지만 위의 2줄의 코드는 의미 없는 코드가 됩니다. 동일한 메모리 주소에 결국 2를 쓰겠다는 의미가 되기 때문입니다.
그러나 개발자는 A라는 장치에 0, 1, 2가 순차적으로 값이 들어와야 초기화되는 장치라고 한다면 의도한 것과 다르게 작동할 것입니다. 여기서 개발자는 나는 분명 코드를 정상적으로 작성했다고 할 것이고, 컴퓨터가 이상하다. 혹은 다른 컴퓨터에서는 컴파일하면 된다는 등의 이야기도 할 수 있습니다.
결론을 말하자면 변수를 volatile 타입으로 지정하면 최적화를 수행하지 않고 모든 메모리 쓰기를 지정한 순서대로 수행한다는 것입니다. 아래와 같이 코드를 바꾸면 의도한 제어가 가능하게 됩니다.
*(volatile unsigned int*)0x4000 = 0;
*(volatile unsigned int*)0x4000 = 1;
*(volatile unsigned int*)0x4000 = 2;
참고로, 컴파일러가 최적화하지 않더라도 입출력 장치로부터 데이터를 제대로 얻어오지 못하는 경우도 있습니다. 바로 캐시 메모리 때문인데요. 우리가 알고 있는 임시로 저장해 두는 캐시 메모리 맞습니다. 계속 똑같은 주소를 읽으면 프로세서가 실제 메모리에 있는 값이 아닌 캐시 메모리 값을 전달해 줍니다. 모든 경우는 아니고 캐시가 개입하게 될 경우 외부장치에 의해 메모리 값이 변화한 경우 이전의 값을 읽을 수도 있는 상황도 생기게 되는 것입니다.
그렇기 때문에 I/O영역의 경우 노캐시(Non-cache) 메모리 주소를 사용하거나 캐시를 초기화해 주고 다시 읽어오는 작업을 하기도 합니다.
4. 멀티스레드 환경에서 플래그 변수로
추가적으로, 멀티스레드 환경에서도 사용할 수 있는데 그 중하는 공유 플래그로 사용할 때가 있습니다.
아래의 stopFlag는 메인 스레드와 작업 스레드가 공유하는 변수입니다. volatile 없이는 stopFlag를 작업 스레드에서 캐시 하고, 메인 스레드에서 변경된 값이 즉시 반영되지 않을 수 있습니다. 그런데 volatile로 선언하면, stopFlag는 항상 메모리에서 직접 읽히므로 값 변경이 즉시 감지됩니다.
volatile int stopFlag = 0; // 스레드 간 공유 플래그
void workerThread() {
while (stopFlag == 0) {
// 작업 수행
}
// 스레드 종료
}
void stopWorker() {
stopFlag = 1; // 작업 중지 요청
}
주의할 점은 volatile은 원자성(atomicity)을 보장하지 않습니다. 즉, 여러 스레드에서 동시에 값을 수정하는 경우 경쟁상태(race condition)가 발생할 수 있습니다. 이를 위해서 std::atomic 또는 뮤텍스(mutex) 같은 동기화 메커니즘을 사용해야 합니다.
다른 내용으로 이어지기 때문에 여기서 마무리하도록 하겠습니다.
결론, volatile을 사용하면, 매번 실제 메모리에서 읽고 쓰므로 정확성이 보장된다.
감사합니다.
<참고 자료>
1. [C] 임베디드 시스템에서 volatile 키워드를 사용하는 이유
https://luna-archive.tistory.com/2
2. [도서] 임베디드 소프트웨어 베이직, 5.6 메모리맵드(Memory-Mapped) I/O
'Embedded' 카테고리의 다른 글
[Jetson Nano] Jetson Nano에 OpenCV 빌드 및 설치 확인해 보기 (2) | 2023.10.18 |
---|---|
[Jetson Nano] docker에 MQTT(mosqitto) 설치해서 외부에서 작동확인 해보기 (0) | 2023.10.09 |