 |
|
난이도 : 중급 Vikram Shukla, 소프트웨어 엔지니어, IBM
옮긴이: 박재호 이해영 dwkorea@kr.ibm.com
2008 년 10 월 14 일 LinuxThreads 프로젝트는 원래 리눅스(Linux®)에서 멀티스레딩 기능을 제공했지만, POSIX 스레딩 표준을 준수하지는 않았습니다. 좀 더 최근에 NPTL(Native POSIX Thread Library)이 이런 간극을 좀 더 메워 나가고는 있지만, 다른 문제점이 여전히 남아 있습니다. 이 기사에서는 두 가지 리눅스 모델에 대한 몇 가지 차이점을 기술해 응용 프로그램을 LinuxThreads에서 NPTL로 이식하기를 원하거나 단순히 차이점이 무엇인지 살펴보기를 원하는 개발자를 지원합니다.
리눅스가 처음 개발되었을 때 커널에서 진짜 스레드를 지원하지 않았다. 하지만 리눅스는 clone() 시스템 호출을 통해 스케쥴 가능한 엔티티로 프로세스를 지원했다. 이 호출은 호출하는 프로세스의 복사본을 생성해 호출자의 주소 영역에서 복사 공유가 가능하게 만들었다. LinuxThreads 프로젝트는 이런 시스템 호출을 사용해 사용자 영역에서 스레드 지원을 완벽하게 흉내내었다. 불행하게도, 이런 접근 방식에는 특히 신호 처리, 스케줄링, 프로세스 간 동기화 부문에서 몇 가지 단점이 있었다. 또한 스레드 모델이 POSIX 요구사항을 준수하지 못했다.
LinuxThreads를 개선하기 위해, 몇 가지 커널 지원과 새로 작성한 스레드 라이브러리가 필요함이 명백해졌다. 이런 요구 사항을 충족하기 위해 두 가지 경쟁 관계에 있는 프로젝트가 출발했다. IBM에서 나온 팀은 NGPT(Next-Generation POSIX Threads)를 만들었다. 반면 레드햇에서 나온 팀은 NPTL을 만들고 있었다. NGPT는 2003년 중반에 좌초되었으며 NPTL만 살아남았다.
LinuxThreads를 대신한 NPTL 선택은 필연적인 결론처럼 보이지만, 늙어가는 리눅스 배포판을 위한 응용을 유지보수하고 있으며 조만간 판올림을 계획하고 있다면, NPTL 이주는 프로세스 이식 과정에서 아주 중요한 주제가 될 것이다. 이주의 대안으로, 이런 차이점을 파악해 예전 기술과 새 기술을 동시에 수용하도록 응용 프로그램을 설계할 수도 있다.
이 기사는 어떤 배포판에서 어떤 스레드 모델이 구현되었는지 세부 사항을 다룬다.
LinuxThreads 설계 명세
스레드는 프로그램을 쪼개어 동시에 동작하는 태스크를 하나 이상으로 나눈다. 단일 프로세스의 상태 정보를 공유하며, 다른 자원과 메모리를 직접 공유한다는 점에서 스레드는 전통적인 멀티태스킹에서 말하는 프로세스와 다르다. 동일 프로세스에 존재하는 스레드 사이에서 일어나는 문맥 전환은 일반적으로 프로세스 사이에서 일어나는 문맥 전환보다 훨씬 더 빠르다. 따라서 멀티스레드 프로그램은 멀티프로세스 응용보다 더 빠르다는 장점이 있다. 또한 스레드를 사용하면 동시에 처리하도록 구현할 수 있다. 이런 상대적인 장점이 프로세스 기반 접근 방식을 벗어나 LinuxThreads 구현을 이끌었다.
LinuxThreads 초기 설계는 관련된 프로세스 사이에서 일어나는 문맥 전환이 충분히 빨라서 개별 커널 스레드가 대응하는 사용자 수준 스레드를 처리할 수 있다는 믿음으로 출발했다. 이는 1대1 스레드 모델이라는 혁신을 이끌었다.
LinuxThreads 설계 명세에 대한 핵심을 검토해 보자.
LinuxThreads와 제약점
일반적으로 LinuxThreads 설계는 멋지게 동작한다. 하지만 과도한 응용이 부하를 주면 성능, 확장성, 사용성에 문제를 일으킨다. LinuxThreads 설계에서 몇 가지 제약을 살펴보자.
- LinuxThreads는 각 프로세스가 소유한 모든 스레드 사이에서 생성과 조율을 위해 관리자 스레드를 사용한다. 이는 스레드 생성과 파괴에 부하를 높인다.
- 관리자 스레드 방식 설계 때문에 문맥 전환이 상당히 많이 일어나며, 확장성과 성능에 잠재적인 장애를 일으킨다.
- 관리자 스레드는 단지 CPU 하나에서만 동작하므로, 동기화 수행은 SMP나 NUMA 시스템에서 확장성 문제를 초래할 수 있다.
- 스레드 관리 방식과 각 스레드마다 다른 프로세스 ID 할당으로 인해, LinuxThreads는 다른 POSIX 관련 스레드 라이브러리와 호환되지 않는다.
- 시그널은 동기화 요소를 구현하는 데 사용되었다. 이는 연산의 반응 속력에 영향을 미쳤다. 또한 메인 프로세스로 시그널 전송 개념이 존재하지 않는다. 따라서 시그널 처리 과정에서 POSIX 방식을 따르지 않는다.
- LinuxThreads 내부에서 시그널 처리는 각 스레드가 독자적인 프로세스 ID를 할당받았기에 프로세스 기반이 아니라 스레드 기반으로 수행한다. 시그널은 지정 스레드로 보내지기 때문에 시그널이 직렬화된다. 다시 말해 시그널은 이 스레드를 통해 다른 스레드로 전달된다. 이는 시그널 병렬 처리를 위한 POSIX 표준 요구 사항과 대조적이다. 예를 들어, LinuxThreads 하에서,
kill()로 보낸 시그널은 전체 프로세스가 아니라 개별 스레드에 전송될 것이다. 이는 스레드가 시그널을 차단하고 있다면 LinuxThreads는 시그널을 차단하지 않는 다른 스레드가 핸들러를 즉시 수행하는 대신 단순히 이 스레드에 대한 큐에 시그널을 밀어 넣어서 스레드가 시그널 차단을 해제할 때 핸들러를 실행한다.
- LinuxThreads에서 개별 스레드는 프로세스이므로, 사용자와 그룹 ID 정보가 단일 프로세스에서 모든 스레드에 공통으로 작용하지 않을 것이다. 따라서 멀티스레드
setuid()/setgid() 결과는 스레드마다 달라질 수 있다.
- 생성된 멀티스레드 코어 덤프는 모든 스레드 정보를 포함하고 있지 않다. 다시 말해, 이런 행동 방식은 각 스레드가 프로세스이기 때문에 일어나는 결과다. 비정상 종료가 스레드 중 하나에서 발생하면, 시스템 코어 파일에서 해당 스레드만 보인다. 하지만 이런 행동 양식은 LinuxThreads 구현 중에서 주로 예전 버전에 해당된다.
- 각 스레드는 독자적인 프로세스이므로, /proc 디렉터리는 이상적으로 스레드가 되어야 할 수 많은 프로세스 항목으로 가득 차버린다.
- 각 스레드는 프로세스이므로 응용을 위해 생성 가능한 스레드 숫자에 제한이 있다. 예를 들어 IA32 시스템에서 생성 가능한 스레드 숫자이자 생성 가능한 프로세스 숫자는 4090개다.
- 스레드 지역 자료 계산 방법이 스택 주소 위치에 기반을 두므로 자료 접근이 아주 느리다. 또 다른 단점으로 사용자는 스택 크기를 명확히 지시하지 못한다. 사용자는 스택 영역을 다른 목적으로 사용되는 영역에 우연히 사상할 수도 있기 때문이다. 요청에 따라 자라나는 개념(부동 스택 개념이라고도 부른다)은 리눅스 커널 2.4.10 이후에 구현되었다. 이 버전 이전에는 LinuxThreads가 정적 스택을 사용했다.
 |

|
NPTL에 대해
NPTL(Native POSIX Thread Library)은 LinuxThreads의 단점을 극복하기 위한 새로운 구현으로 POSIX 요구사항 또한 충족한다. NPTL은 성능과 확장성 측면에서 LinuxThreads보다 강력한 개선 사항을 제공한다. LinuxThreads와 같이 NPTL은 1대1 모델을 구현한다.
Ulrich Drepper와 Ingo Molnar는 NPTL 설계에 참여한 레드햇 직원이다. 전반적인 설계 목표 중 몇 가지는 다음과 같다.
- 새로운 스레드 라이브러리는 POSIX를 준수해야 한다.
- 스레드 구현은 대규모 프로세서를 탑재한 시스템에서도 잘 동작해야 한다.
- 심지어 작은 작업을 위해 새로운 스레드를 생성하더라도 시작 비용이 낮아야 한다.
- NPTL 스레드 라이브러리는 LinuxThreads와 이진 호환이 가능해야 한다. 이런 목적으로
LD_ASSUME_KERNEL을 사용할 수 있다. 이 기사 뒷부분에 다루겠다.
- 새로운 스레드 라이브러리는 NUMA 지원을 활용할 수 있어야 한다.
NPTL의 장점
NPTL은 LinuxThreads에 비해 여러 가지 장점이 있다.
- NPTL은 관리자 스레드를 사용하지 않는다. 프로세스의 일부로 모든 스레드에 치명적인 시그널을 보내는 등 관리자 스레드에서 필요한 몇 가지 요구 사항이 존재하지 않는다. 커널 자체가 이런 작업을 신경쓸 수 있기 때문이다. 커널은 또한 각 스레드 스택이 사용한 메모리를 할당 해제한다. 심지어 어버이 스레드를 정리하기 앞서 기다리고 있는 모든 스레드 종료를 관리하므로 좀비를 막을 수 있다.
- 관리자 스레드를 사용하지 않기 때문에, NPTL 스레드 모델은 NUMA와 SMP 시스템에서 좀 더 나은 확장성과 동기화 메커니즘을 제공한다.
- 새로운 커널 구현과 함께 NPTL 스레드 라이브러리는 시그널을 사용한 스레드 동기화 기법을 피한다. 이런 목적으로 NPTL은 퓨텍스(futex)라는 새로운 메커니즘을 도입했다. 퓨텍스는 공유 메모리 영역에서 동작하므로 프로세스 사이에 공유가 가능하므로 프로세스 간 POSIX 동기화를 제공한다. 또한 프로세스 사이에서 퓨텍스를 공유할 수도 있다. 이런 행동 양식은 프로세스 간 동기화를 현실로 만든다. 실제로 NPTL은
PTHREAD_PROCESS_SHARED라는 매크로를 포함해 개발자에게 다른 프로세스의 스레드 사이에 뮤텍스를 사용자 수준 프로세스에서 공유하기 위한 핸들을 제공한다.
- NPTL이 POSIX 규약을 따르므로 NPTL은 프로세스 단위로 시그널을 처리한다.
getpid()는 모든 스레드에서 똑같은 프로세스 ID를 반환한다. 예를 들어 시그널 SIGSTOP을 보내면 전체 프로세스가 멈춘다. LinuxThreads에서는 이 시그널을 받은 스레드만 멈춘다. 이는 NPTL 기반 응용에서 GDB와 같은 디버그 지원을 강화한다.
- NPTL에서 모든 스레드에는 어버이 프로세스 하나만 존재하므로, 어버이에게 보고되는 자원 사용(CPU나 메모리 퍼센트와 같은)은 스레드 하나가 아니라 전체 프로세스에 보고된다.
- NPTL 스레드 라이브러리에 도입된 중요한 특징 중 하나는 ABI(Application Binary Interface) 지원이다. 이는 LinuxThreads와 하위 호환이 가능하도록 돕는다. 다음에 다룰
LD_ASSUME_KERNEL의 도움을 받아 ABI 지원을 처리한다.
 |

|
LD_ASSUME_KERNEL 환경 변수
앞서 설명한 바에 따르면, ABI 도입은 코드에서 NPTL과 LinuxThreads 모델을 둘 다 지원할 수 있게 만든다. 기본적으로 어떤 런타임 스레드 라이브러리를 동적으로 링크할지 결정하는 작업은 ld(동적 링커/로드) 몫이다.
예를 들어, WebSphere® 응용 서버가 사용하는 몇 가지 공통 변수 설정은 다음과 같다. 요구사항에 맞춰 적절히 시도해보자.
LD_ASSUME_KERNEL=2.4.19: 이는 NPTL 구현을 덮어쓴다. 이 구현은 일반적으로 부동 스택 기능을 활성화한 표준 LinuxThreads 모델을 의미한다.
LD_ASSUME_KERNEL=2.2.5: 이는 NPTL 구현을 덮어쓴다. 이 구현은 일반적으로 고정 스택 기능을 활성화한 LinuxThreads 모델을 의미한다.
다음과 같은 명령으로 이 환경 변수를 설정해보자.
export LD_ASSUME_KERNEL=2.4.19
LD_ASSUME_KERNEL 설정을 위한 지원은 스레드 라이브러리를 위해 현재 지원하는 ABI 버전에 의존한다. 예를 들어, 스레드 라이브러리가 ABI 버전 2.2.5를 지원하지 않으면, 사용자는 LD_ASSUME_KERNEL을 2.2.5로 설정할 수 없다. 일반적으로 NPTL은 2.4.20을 요구하며, LinuxThreads는 2.4.1을 요구한다.
이 모든 설정은 일반적으로 NPTL 활성 리눅스 배포판을 돌리지만 응용이 LinuxThreads 모델을 기초로 설계되어 있을 경우에 사용한다.
GNU_LIBPTHREAD_VERSION 매크로
대다수 현대적인 리눅스 배포판은 LinuxThreads와 NPTL 양쪽을 모두 포함하며, 양쪽 사이에 전환이 가능한 방법을 제공한다. 현재 시스템에서 사용 중인 스레드 라이브러리 버전을 확인하려면 다음 명령을 내린다.
$ getconf GNU_LIBPTHREAD_VERSION
출력은 다음과 같다.
NPTL 0.34
또는 다음과 같다.
linuxthreads-0.10
리눅스 배포판에 따른 스레드 모델, glibc 버전, 커널 버전
표 1은 인기 있는 몇 가지 리눅스 배포판을 보여준다. 스레드 구현 유형, glibc 라이브러리, 커널 버전을 담고 있다.
표 1. 리눅스 배포판과 스레드 구현
| 스레드 구현 | C 라이브러리 | 배포판 | 커널 |
|---|
| LinuxThreads 0.7, 0.71 (for libc5) | libc 5.x | Red Hat 4.2 | | | LinuxThreads 0.7, 0.71 (for glibc 2) | glibc 2.0.x | Red Hat 5.x | | | LinuxThreads 0.8 | glibc 2.1.1 | Red Hat 6.0 | | | LinuxThreads 0.8 | glibc 2.1.2 | Red Hat 6.1 and 6.2 | | | LinuxThreads 0.9 | | Red Hat 7.2 | 2.4.7 | | LinuxThreads 0.9 | glibc 2.2.4 | Red Hat 2.1 AS | 2.4.9 | | LinuxThreads 0.10 | glibc 2.2.93 | Red Hat 8.0 | 2.4.18 | | NPTL 0.6 | glibc 2.3 | Red Hat 9.0 | 2.4.20 | | NPTL 0.61 | glibc 2.3.2 | Red Hat 3.0 EL | 2.4.21 | | NPTL 2.3.4 | glibc 2.3.4 | Red Hat 4.0 | 2.6.9 | | LinuxThreads 0.9 | glibc 2.2 | SUSE Linux Enterprise Server 7.1 | 2.4.18 | | LinuxThreads 0.9 | glibc 2.2.5 | SUSE Linux Enterprise Server 8 | 2.4.21 | | LinuxThreads 0.9 | glibc 2.2.5 | United Linux | 2.4.21 | | NPTL 2.3.5 | glibc 2.3.3 | SUSE Linux Enterprise Server 9 | 2.6.5 |
커널 2.6.x과 glibc 2.3.3 이후에는 NPTL 버전 번호 관례가 바뀐듯이 보인다. 이제 스레드 라이브러리는 사용 중인 glibc 버전과 일치한다.
JVM(Java™ Virtual Machine) 지원은 버전에 따라 달라질 수 있다. IBM이 이식한 JVM은 glibc 2.1 이상을 탑재한 표 1에 나온 대다수 배포판을 지원한다.
결론
LinuxThreads의 한계는 나중에 나온 LinuxThreads 버전은 물론이고 NPTL로 극복해왔다. 예를 들어, 최신 LinuxThreads 구현은 스레드 지역 자료 위치를 지정하기 위해 스레드 레지스터를 사용한다. 예를 들어, 인텔(Intel®) 프로세서에서 LinuxThreads는 %fs와 %gs 세그먼트 레지스터를 사용해 스레드 지역 자료에 접근하는 가상 주소를 지정한다. LinuxThreads에서 변경 결과 개선이 있었지만, 좀 더 높은 부하와 스트레스 테스트에서 여전히 문제점이 고개를 내민다. 관리자 스레드에 대한 과도한 의존, 시그널 처리와 같은 요인 때문이다.
LinuxThreads로 라이브러리를 만드는 동안에 -D_REENTRANT 컴파일 플래그를 잊어버리지 말자. 이는 라이브러리를 스레드 안전한 상태로 만든다.
마지막으로 아마도 가장 중요한 힌트로, LinuxThreads는 프로젝트 주창자가 NPTL을 대안으로 생각하기에 더 이상 활발하게 갱신하지 않는다는 사실을 기억하자.
LinuxThreads의 단점으로 NPTL이 오류가 없다는 식으로 받아들여서는 곤란하다. SMP 위주 설계로 만들어진 NPTL 역시 단점이 있다. 최근 레드햇 커널에서 간단한 스레드 응용 프로그램이 단일 프로세스 기계에서는 정상으로 동작했지만 SMP에서는 얼어버리는 상황을 목격했다. 하이엔드 응용을 만족시키기 위해 확장성을 높이려면 리눅스에서 해야 할 일이 여전히 많다는 생각이다.
참고자료 교육
제품 및 기술 얻기
- LinuxThreads README는 LinuxThreads에 대한 일반적인 설명을 담고 있다.
- SEK for Linux 주문: 두 장짜리 DVD 세트를 주문하자. DB2®, Lotus®, Rational®, Tivoli®, WebSphere®와 같은 리눅스용 IBM 최신 평가판 소프트웨어를 포함하고 있다.
-
With IBM 평가판 소프트웨어: developerWorks에서 직접 내려받아, 다음번 리눅스 개발 프로젝트에 활용하자.
토론
필자소개  | |  | Vikram Shukla는 객체지향 언어 개발과 설계에 6년 이상 경력을 쌓았고 현재 인도 뱅갈로르에 있는 자바 기술 센터에 소프트웨어 엔지니어로 일한다. 리눅스에서 IBM JVM 지원을 맡고 있다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|