- C 언어의
setenv()
및 unsetenv()
함수는 스레드를 사용하는 프로그램에서 안전하게 사용할 수 없음
- 이 함수들은 전역 상태를 수정하며, 다른 스레드에서
getenv()
를 호출할 때 충돌을 일으킬 수 있음
- Go의
os.Setenv
및 Rust의 std::env::set_var()
과 같이 C 표준 라이브러리 함수를 사용하는 다른 언어에서도 충돌 발생
- Go 프로그램에서 관련 문제를 추적하고 버그를 보고하는 데 2일이 소요됨
- Go의 DNS 리졸버 내부에서 getaddrinfo()를 사용하는데 이게 getenv()를 호출하기 때문
- 근데 이 문제는 굉장히 오래 되었음. 2017년에도 관련 글이 있고, 글 하단에 5년뒤 2022년에 만나요! 라고 써있는데 2023년에 다시 만난 것
- 이는 POSIX 표준의 결함으로, C 표준을 확장하여 환경 변수를 수정할 수 있도록 한 것
- 가장 화가 나는 부분은, 표준에 영향을 미치거나 C 라이브러리를 유지 관리할 수 있는 많은 사람들이 이를 문제라고 생각하지 않는다는 것
- 그 이유는 사양에 스레드와 함께 setenv()를 사용할 수 없다고 명확하게 명시되어 있기 때문
- 따라서 누군가가 이 작업을 수행하면 충돌은 그들의 잘못임
- 그러니 우리는 "모든 함수의 사양을 주의 깊게 읽고, 다른 사람이 작성한 소프트웨어를 사용하지 말고, 스레드를 사용하지 말아야 함"
- 하지만 현대 소프트웨어에서 이것은 비현실적인 가정임
- 이보다는 망치기 어렵고 생태계 변화에 따라 진화하는 API를 만들기 위해 노력해야 한다고 생각함
- C 언어와 표준 라이브러리는 대부분의 소프트웨어의 기반에서 계속해서 중요한 역할을 하고 있으니, 이를 개선할 방법을 찾거나 아니면 버릴 방법을 찾아야 함
왜 setenv()는 Thread-Safe 하지 않은가
-
getenv()
는 char*
을 반환하며, 애플리케이션은 나중에 이를 해제할 필요가 없음
- 한 스레드가 이 포인터를 사용하는 동안 다른 스레드가
setenv()
또는 unsetenv()
를 사용하여 동일한 환경 변수를 변경할 수 있음
- C 표준에는
getenv()
만 포함되어 있으나, 대부분의 구현체는 POSIX 표준을 따르며 환경을 수정하는 함수를 포함함
-
putenv()
는 환경 변수 집합에 char*
를 추가하며, 애플리케이션이 putenv()
반환 후 메모리를 수정하면 환경 변수도 수정됨
-
environ
은 애플리케이션이 읽고 할당할 수 있는 NULL로 종료되는 포인터 배열(char**
)이며, 이 배열에 대한 접근은 스레드 안전하지 않음
환경 변수 구현 방식
- 애플리케이션이 기존 변수를 덮어쓸 때 구현체는 어떻게 처리할지 결정해야 함
- glibc와 Solaris/Illumos는 환경 변수를 절대 해제하지 않으며,
getenv()
에서 반환된 값은 불변이고 스레드에 안전하게 사용될 수 있음
- musl과 FreeBSD/Apple은 환경 변수를 해제하며, 다른 스레드가
setenv()
를 호출한 후 getenv()
에서 반환된 포인터를 사용하면 충돌이 발생할 수 있음
- 환경 변수 집합이 스레드 안전하게 업데이트되는 것을 보장하는 것이 두 번째 문제이며, 이로 인해 glibc에서 충돌이 발생함
프로그램이 환경 변수를 사용하는 이유
- 환경 변수는 다른 프로그램에 포함된 공유 라이브러리나 언어 런타임을 구성하는 데 유용함
- 사용자가 프로그램 작성자가 명시적으로 구성을 전달할 필요 없이 구성을 변경할 수 있음
- 많은 라이브러리가
getenv()
를 호출하며, 프로그램은 사용하는 라이브러리를 구성하기 위해 이 변수들을 변경해야 함
이 문제를 해결해야 하며, 다음과 같이 할 수 있음
- 내 생각에 이것이 오랫동안 알려진 문제였다는 것은 말도 안 되는 일임
- 문제를 디버깅하거나 해결 방법을 논의하느라 수천 시간을 낭비함
- 문제를 해결할 방법
- Illumos/Solaris와 같이 스레드에 안전한 구현을 만들기
- 이것은 약간 한계가 있음. setenv()에서 메모리가 누수되고 프로그램이 putenv() 또는 환경 변수를 사용하는 경우 여전히 안전하지 않음
- 하지만 이는 현재 Linux 및 Apple 구현에 비해 개선된 것
- 두 번째로는 Microsoft의 getenv_s()와 같이 설계상 스레드에 안전한 모든 환경 변수를 가져오는 새로운 API를 추가하는 것
- 내가 선호하는 해결책은 두 가지를 모두 사용하는 것
- 기존 프로그램과 라이브러리에 대한 문제 발생 가능성을 줄이고, Go와 Rust와 같은 새로운 코드나 언어에 대한 문제를 완전히 피할 수 있는 경로를 제공함
- getenv_s()와 유사하게 하나의 환경 변수를 사용자가 지정한 버퍼에 복사하는 함수를 추가
- 모든 환경 변수를 반복하거나 모든 변수를 복사할 수 있는 스레드 안전 API를 추가
- getenv()를 더 이상 사용되지 않는 것으로 표시하고 대신 스레드에 안전한 새로운 getenv() 함수를 권장
- putenv()를 더 이상 사용되지 않는 것으로 표시하고 대신 setenv()를 권장
- environ을 사용 중단된 것으로 표시하고 대신 환경 변수 함수를 권장
- 스레드에 안전하도록 환경 변수 구현을 업데이트