Processes vs Threads, as the Linux Kernel Sees Them
리눅스 커널에서 프로세스와 스레드는 어떻게 다를까요
리눅스 커널은 프로세스와 스레드를 모두 CPU에 배치하는 task로 다루며, 둘의 큰 차이는 주소 공간을 공유하는지에 있습니다. 공유 메모리는 데이터 복사를 줄이지만 경쟁 상태와 장애 전파를 낳으므로, 작업의 데이터 접근 방식과 실패 격리 필요성을 기준으로 선택해야 합니다.
- 주제
에디터 노트
스레드의 성능 이점은 메모리 공유에서 나오고, 같은 구조가 경쟁 상태와 장애 전파의 원인이 됩니다. 글은 copy-on-write를 설명해 fork()가 처음부터 모든 메모리를 복사한다는 오해를 바로잡고, 카운터 실험과 세그멘테이션 오류 사례로 비용을 구체화합니다. 다만 clone()의 차이를 CLONE_VM 하나로 요약한 표현은 개념을 설명하기 위한 단순화입니다. 실제 호출에는 글도 보여주듯 여러 플래그가 관여합니다. 댓글은 기초 원리를 다시 살펴보는 데 공감하지만, 기술적 주장에 대한 검증이나 반론을 보태지는 않습니다.
AI 요약
프로세스와 스레드를 흔히 무거운 실행 단위와 가벼운 실행 단위로 구분하지만, 이 설명만으로는 메모리 공유와 장애 범위가 어떻게 달라지는지 알기 어렵습니다. 글은 리눅스 커널의 관점에서 두 실행 단위를 비교하고, 주소 공간 공유가 성능과 위험을 함께 만든다고 설명합니다.
프로세스마다 따로 쓰는 가상 주소 공간
프로세스마다 별도의 가상 주소 공간이 있습니다. 물리 메모리를 실제로 덩어리째 나눠 주는 것은 아닙니다. 커널이 관리하는 페이지 테이블과 CPU의 메모리 관리 장치(MMU)가 가상 주소를 물리 메모리로 변환합니다. 따라서 두 프로세스가 같은 주소를 사용해도 서로 다른 메모리를 가리킬 수 있습니다. 매핑되지 않은 주소에 접근하면 하드웨어가 오류를 일으키고, 커널은 보통 SIGSEGV로 처리합니다. 프로세스 사이의 이 격리 덕분에 한 프로그램이 다른 프로그램의 메모리를 직접 읽거나 손상시키지 못합니다.
스레드는 주소 공간을 공유합니다
리눅스 스케줄러는 프로세스 자체가 아니라 task를 CPU에 배치합니다. 스레드도 task이며, 기존 주소 공간을 함께 쓰도록 만들어집니다. 글은 리눅스에서 clone()과 CLONE_VM 플래그가 이 차이를 보여준다고 설명합니다. fork()로 자식 프로세스를 만들면 별도 task와 페이지 테이블이 생기지만, 메모리 페이지를 즉시 전부 복사하지는 않습니다. copy-on-write 방식으로 페이지를 공유하다가 한쪽이 쓰기를 시도할 때 해당 페이지를 복사합니다.
글의 예제는 전역 변수 brightness를 자식 프로세스와 스레드가 각각 100으로 바꾸게 합니다. 자식 프로세스에서 바꾼 값은 부모에게 반영되지 않지만, 스레드가 바꾼 값은 공유 주소 공간에 기록되어 부모에서도 100으로 보입니다. Ubuntu 24.04에서 실행한 결과를 제시하고, strace로 확인한 pthread_create() 호출에는 CLONE_VM 외에도 파일 디스크립터 공유를 나타내는 CLONE_FILES, 시그널 처리기 공유를 나타내는 CLONE_SIGHAND, 같은 스레드 그룹에 속하게 하는 CLONE_THREAD 등이 포함됐다고 설명합니다. 각 스레드는 자기 스택과 레지스터, 스레드 로컬 저장소를 따로 씁니다. 힙과 전역 변수, 열린 파일 등은 공유합니다. 컨테이너의 네임스페이스도 clone()에 CLONE_NEW* 플래그를 지정하는 방식으로 만든다고 덧붙입니다.
공유 메모리가 줄이는 비용과 늘리는 위험
같은 사진을 처리하는 예에서는 네 스레드가 이미지의 서로 다른 구역을 맡습니다. 이미지 데이터를 복사하거나 결과를 다시 합치지 않아도 됩니다. 반대로 네 프로세스가 각각 데이터를 수정하면, 실제로 수정하는 페이지가 copy-on-write로 복사될 수 있고 결과를 모으는 과정도 필요합니다. 글은 이를 스레드의 기본 공유와 프로세스의 기본 격리 차이로 정리합니다.
공유 데이터에 동기화 없이 접근하면 경쟁 상태가 생깁니다. 두 스레드가 값 50을 읽고 각각 10을 더한 뒤 60을 쓰면, 기대한 70 대신 60이 남습니다. 글은 네 스레드가 공유 카운터를 각각 백만 번 증가시키는 실험도 제시합니다. 잠금이 없을 때 기대값 4,000,000 대신 3,966,879가 나왔고, 뮤텍스를 사용했을 때는 4,000,000이 나왔습니다. 잠금은 결과를 보호하지만 대기 시간을 만들 수 있습니다. 작업 데이터를 스레드별로 나눠 서로 같은 데이터를 거의 건드리지 않게 하면 잠금 사용을 줄일 수 있다고 설명합니다.
장애 범위도 다릅니다. 스레드 하나가 세그멘테이션 오류를 일으키면 기본 신호 처리에서는 프로세스 전체가 종료됩니다. 한 스레드의 버퍼 오버플로가 다른 스레드가 쓰는 메모리를 손상시킬 수도 있습니다. 별도 프로세스는 메모리 격리로 이런 장애가 다른 프로세스까지 번지는 것을 막습니다. 글은 Chrome이 사이트별 프로세스를 두는 사례와 PostgreSQL이 연결마다 프로세스를 두는 사례를 들며 격리와 메모리 비용의 선택을 설명합니다. 큰 공유 데이터를 여러 코어에서 처리할 때는 스레드가 유리할 수 있고, 파이썬 표준 빌드에서는 GIL 때문에 파이썬 바이트코드가 여러 스레드에서 병렬 실행되지 않는다는 점은 커널이 아니라 언어 구현의 문제라고 구분합니다.
선택 기준
실패를 격리해야 하거나 작업자가 많은 데이터를 빠르게 공유할 필요가 없다면 프로세스를 선택할 수 있습니다. 공유 데이터가 많고 작업을 서로 겹치지 않게 나눌 수 있다면 스레드가 적합합니다. 데이터를 나누기 어렵다면 스레드와 잠금을 함께 쓰되 동기화와 디버깅 비용을 고려해야 한다고 글은 정리합니다.
dev.to 반응
- @mridul_it_is — 정말 읽을 가치가 있습니다. 편리해진 도구 때문에 잊거나 건너뛰는 개념이 많다고 생각합니다. 밑바탕의 원리를 깊이 이해하는 건 역시 좋습니다.♥️
원문: dev.to / 번역·요약: Trawling