운영체제 기본/프로세스와 스레드: 두 판 사이의 차이
프로세스 간 통신 총론 연결과 문서 순서·둘러보기 갱신 |
프로세스와 스레드의 차이, 공유 자원과 실행 상태, 단일·다중 스레드 예시 보강 |
||
| 5번째 줄: | 5번째 줄: | ||
}} | }} | ||
프로그램을 실행하려면 명령어뿐 아니라 데이터를 보관할 메모리, 현재 실행 위치, 열린 파일, CPU를 사용할 기회가 필요하다. '''프로세스(Process)'''는 프로그램의 실행에 필요한 주소 공간과 자원을 묶는 단위이고, '''스레드(Thread)'''는 그 안에서 명령을 순서대로 실행하는 흐름이다. 하나의 프로세스에는 하나 이상의 스레드가 있으며, 여러 스레드가 같은 실행 환경을 공유할 수 있다. | |||
이 | == 프로그램과 프로세스 == | ||
디스크에 저장된 실행 파일은 코드와 초기 데이터를 담는다. 이 파일을 실행하면 운영체제는 메모리에 실행 환경을 마련하고, 파일을 읽거나 장치를 사용하는 데 필요한 상태를 관리한다. 실행 파일이 같더라도 실행할 때마다 별도의 프로세스가 만들어질 수 있다. | |||
== 실행 | 같은 편집기를 두 번 실행하여 서로 다른 문서를 열었다고 하자. 두 프로세스는 같은 프로그램의 코드를 실행하지만, 편집 중인 내용과 열린 파일, 메모리 할당 상태는 따로 관리한다. 한쪽에서 바꾼 일반 변수의 값이 다른 쪽에 그대로 반영되지 않는다. 각 프로세스가 별도의 '''가상 주소 공간(Virtual Address Space)'''을 갖기 때문이다. 같은 숫자의 주소라도 서로 다른 메모리를 가리킬 수 있다.<ref name="process-model">[https://mit-pdos.github.io/xv6-riscv-book/first.html MIT PDOS, xv6: Operating system organization], 프로세스의 실행 환경과 격리.</ref> | ||
실행 | |||
프로세스에는 주소 공간 외에도 열린 파일을 찾는 정보, 실행에 사용하는 각종 자원과 관리 정보가 있다. 운영체제는 이 묶음을 이용해 자원을 회수하고 다른 프로세스와 분리한다. 어떤 작업을 지금 CPU에서 실행할지는 그 프로세스에 속한 스레드의 상태를 보고 결정한다. | |||
== 스레드와 실행 상태 == | |||
스레드 하나로 실행되는 프로그램에서는 입력 처리, 계산, 결과 출력이 하나의 실행 흐름을 따라 이어진다. 함수를 호출하면 호출된 함수로 진행하고, 반환하면 호출한 위치로 돌아온다. 함수가 많다는 이유만으로 스레드가 여러 개 생기지는 않는다. | |||
CPU가 이 흐름을 멈췄다가 이어가려면 다음 명령의 위치와 계산 중이던 값, 함수 호출 관계를 알아야 한다. 이를 위해 스레드마다 다음과 같은 '''실행 문맥(Execution Context)'''을 관리한다.<ref name="thread-context">[https://learn.microsoft.com/en-us/windows/win32/procthread/about-processes-and-threads Microsoft Learn, About Processes and Threads], 프로세스 자원과 스레드 실행 문맥.</ref> | |||
* '''프로그램 카운터(Program Counter, PC)''': 이어서 실행할 명령의 위치. | |||
* '''레지스터(Register)''': 연산 중인 값과 함수 인자 등 CPU의 실행 상태. | |||
* '''스택(Stack)과 스택 포인터(Stack Pointer, SP)''': 함수 호출과 반환, 호출별 지역 변수 등을 위한 메모리와 그 위치. | |||
* '''스케줄링 상태''': 실행 중인지, 실행 가능한지, 입출력이나 다른 조건을 기다리는지에 관한 정보. | |||
실행 중인 스레드의 레지스터 값은 CPU에 있고, 다른 스레드를 실행할 때는 다시 복원할 수 있도록 보관한다. 스레드마다 PC와 스택이 있으므로 같은 함수를 실행하더라도 서로 다른 위치에 있을 수 있고, 함수 인자와 지역 변수의 값도 다를 수 있다. | |||
== 공유 자원과 스레드별 상태 == | |||
여러 스레드를 사용하는 프로세스는 실행 흐름을 여러 개 두되, 대부분의 실행 환경을 함께 사용한다. 같은 프로세스의 스레드는 코드, 전역 데이터, 힙을 포함하는 주소 공간을 공유한다. Unix 계열에서는 열린 파일을 찾는 파일 디스크립터 테이블도 공유하므로 한 스레드가 연 파일을 다른 스레드가 사용할 수 있다.<ref name="source-3">[https://man7.org/linux/man-pages/man7/pthreads.7.html pthreads(7)], 공유하는 프로세스 자원과 스레드별 상태.</ref> | |||
<pre> | |||
프로세스 P | |||
├─ 공유 주소 공간 | |||
│ ├─ 코드 · 전역 데이터 · 힙 | |||
│ ├─ 스레드 A의 스택 | |||
│ └─ 스레드 B의 스택 | |||
├─ 공유 파일 디스크립터 테이블 | |||
├─ 스레드 A: PC · SP · 레지스터 · 실행/대기 상태 | |||
└─ 스레드 B: PC · SP · 레지스터 · 실행/대기 상태 | |||
프로세스 Q | |||
├─ 별도 주소 공간 · 파일 디스크립터 테이블 | |||
└─ 스레드 C: PC · SP · 레지스터 · 실행/대기 상태 | |||
</pre> | |||
'''스택을 따로 사용한다는 것과 메모리 접근이 서로 차단된다는 것은 다르다.''' A와 B의 스택도 같은 주소 공간 안에 있다. 유효한 포인터가 있으면 다른 스레드의 스택에 놓인 객체에도 접근할 수 있으므로, 객체의 수명과 동기화를 지켜야 한다. 반면 별도 프로세스 Q의 사적인 메모리는 보통 P의 포인터만으로 접근할 수 없다. 프로세스 사이에 같은 메모리를 쓰려면 공유 매핑 등 별도의 연결이 필요하다. | |||
{| class="wikitable" | |||
! 구분 !! 서로 다른 프로세스 !! 같은 프로세스의 여러 스레드 | |||
|- | |||
| 주소 공간 || 기본적으로 분리 || 공유 | |||
| 실행 위치와 레지스터 || 각 프로세스의 스레드마다 보유 || 스레드마다 보유 | |||
| 함수 호출 스택 || 각 스레드가 별도로 사용 || 각 스레드가 별도로 사용하지만 주소 공간은 공유 | |||
| 데이터 전달 || 통신이나 명시적 공유가 필요 || 같은 객체에 직접 접근 가능 | |||
| 잘못된 메모리 쓰기의 영향 || 다른 프로세스의 사적 메모리와 격리 || 같은 주소 공간의 다른 스레드에 전파 가능 | |||
|} | |||
== 단일 스레드와 다중 스레드 == | |||
문서 편집기가 입력을 처리하다가 오래 걸리는 검색 작업을 수행한다고 하자. 단일 스레드가 검색 루프를 실행하는 동안에는 같은 흐름에서 입력을 처리할 수 없다. 작업을 잘게 나누어 번갈아 처리하는 설계도 가능하지만, 입력 담당 스레드와 검색 담당 스레드를 나누면 각각의 진행 위치를 독립적으로 유지할 수 있다. | |||
새 스레드는 정해진 함수에서 실행을 시작하고, 기존 스레드는 자신의 실행을 계속한다. 보통 함수 호출처럼 새 작업이 반환할 때까지 반드시 기다리는 구조가 아니다. 결과가 필요한 지점에서는 완료를 기다리거나 통지를 받아야 한다.<ref name="thread-create">[https://man7.org/linux/man-pages/man3/pthread_create.3.html pthread_create(3)], 새 스레드의 시작과 실행 순서.</ref> | |||
예를 들어 입력 스레드가 문서 내용을 수정하고 검색 스레드가 같은 내용을 읽는다면, 두 스레드가 무엇을 언제 볼지 정해야 한다. 락으로 접근을 조정하거나 검색용 사본을 만드는 방법이 있다. 실행 흐름을 나누는 편리함에는 공유 데이터 관리 비용이 따라온다. | |||
<pre> | |||
단일 스레드: 입력 처리 → 검색 작업 → 다음 입력 처리 | |||
다중 스레드: 입력 스레드 → 입력 처리 → 입력 처리 → ... | |||
검색 스레드 → 검색 작업 ─────────→ 완료 통지 | |||
(문서 데이터 공유에는 동기화 필요) | |||
</pre> | |||
스레드가 둘이라고 항상 두 작업이 동시에 CPU에서 실행되는 것은 아니다. 논리 CPU 하나에서는 번갈아 진행하는 '''동시성(Concurrency)'''을 얻고, 여러 논리 CPU에 배치되면 같은 시각에 실행하는 '''병렬성(Parallelism)'''도 얻을 수 있다. 스레드를 늘려도 CPU 수나 공유 락, 입출력 장치가 병목이면 처리량이 늘지 않을 수 있다. | |||
== 실행 파일과 실행 문맥 == | |||
실행 문맥을 새로 만드는 일과 실행할 프로그램을 바꾸는 일도 구분된다. Unix 계열에서는 프로세스를 생성하는 연산과 기존 프로세스의 실행 이미지를 교체하는 연산이 분리되어 있다. 생성과 실행을 나누면 새 프로그램을 시작하기 전에 입출력 연결이나 실행 환경을 구성할 수 있다. 대신 어떤 상태를 상속하고 어떤 상태를 초기화할지 명확한 규칙이 필요하다. <ref name="source-1">[https://man7.org/linux/man-pages/man2/fork.2.html fork(2)].</ref><ref name="source-2">[https://man7.org/linux/man-pages/man2/execve.2.html execve(2)].</ref> | 실행 문맥을 새로 만드는 일과 실행할 프로그램을 바꾸는 일도 구분된다. Unix 계열에서는 프로세스를 생성하는 연산과 기존 프로세스의 실행 이미지를 교체하는 연산이 분리되어 있다. 생성과 실행을 나누면 새 프로그램을 시작하기 전에 입출력 연결이나 실행 환경을 구성할 수 있다. 대신 어떤 상태를 상속하고 어떤 상태를 초기화할지 명확한 규칙이 필요하다. <ref name="source-1">[https://man7.org/linux/man-pages/man2/fork.2.html fork(2)].</ref><ref name="source-2">[https://man7.org/linux/man-pages/man2/execve.2.html execve(2)].</ref> | ||
| 21번째 줄: | 81번째 줄: | ||
대신 결과나 공통 데이터를 주고받을 [[운영체제 기본/프로세스 간 통신|통신 경로]]가 필요하다. 메시지에 담을 데이터 표현을 정해야 하고, 구현에 따라 복사와 직렬화, 커널 경계 통과 비용이 발생한다. 공유 메모리로 복사를 줄일 수 있지만, 그 영역에는 다시 접근 규칙과 동기화가 필요하다. | 대신 결과나 공통 데이터를 주고받을 [[운영체제 기본/프로세스 간 통신|통신 경로]]가 필요하다. 메시지에 담을 데이터 표현을 정해야 하고, 구현에 따라 복사와 직렬화, 커널 경계 통과 비용이 발생한다. 공유 메모리로 복사를 줄일 수 있지만, 그 영역에는 다시 접근 규칙과 동기화가 필요하다. | ||
같은 프로세스 안에서 스레드를 늘리면 주소 공간과 열린 파일 등을 공유하므로 포인터로 같은 객체를 다룰 수 있다. 연결과 전달이 간단해지는 대신, 잘못된 쓰기와 공유 상태의 오염도 다른 스레드에 영향을 준다. 스레드별 스택은 독립적인 실행 상태이지, 서로 접근할 수 없는 보호 경계는 아니다. <ref name="source-3" | 같은 프로세스 안에서 스레드를 늘리면 주소 공간과 열린 파일 등을 공유하므로 포인터로 같은 객체를 다룰 수 있다. 연결과 전달이 간단해지는 대신, 잘못된 쓰기와 공유 상태의 오염도 다른 스레드에 영향을 준다. 스레드별 스택은 독립적인 실행 상태이지, 서로 접근할 수 없는 보호 경계는 아니다. <ref name="source-3"/> | ||
따라서 프로세스와 스레드의 선택은 생성 비용만으로 결정되지 않는다. 공유할 데이터의 양, 장애를 격리할 범위, 통신 비용, 동기화 복잡도를 함께 고려해야 한다. 별도 프로세스도 같은 파일이나 공유 메모리를 변경한다면 경쟁 문제가 생긴다. | 따라서 프로세스와 스레드의 선택은 생성 비용만으로 결정되지 않는다. 공유할 데이터의 양, 장애를 격리할 범위, 통신 비용, 동기화 복잡도를 함께 고려해야 한다. 별도 프로세스도 같은 파일이나 공유 메모리를 변경한다면 경쟁 문제가 생긴다. | ||