[Operating System] Process Termination / Thread
CPU 에러, signal, 부모 프로세스 관련 문제 등으로 프로세스가 동작을 마치면 exit System Call을 호출해 terminated 상태로 변한다.
이 때 release는 프로세스가 실행될 때 사용했던 컴퓨팅 리소스를 반환함을 의미한다.
terminated 상태인 프로세스는 아직 제거된 상태는 아니고, wait System Call이 호출될 때 제거된다.
사용량 및 통계 정보를 저장하는 PCB만 남아있는 상태이다.
exit System Call을 호출하면 해당 프로세스는 User Mode에서 Kernel Mode로 변하게 된다.
이 때 PCB를 제외한 test.c의 모든 컴퓨팅 자원을 회수하는데, 이 PCB를 통해 해당 프로세스가 terminated 상태임을 확인한다.
PCB의 State 블럭에 terminated 상태를 기록하고, Accounting Information에 프로세스가 사용한 컴퓨팅 리소스를 기록한다.
이후 자식 프로세스는 부모 프로세스에게 death-of-child signal을 보내고, 부모 프로세스가 wait System Call을 호출하게 되면 terminated 상태인 자식 프로세스가 가진 Accounting Information을 부모 프로세스가 읽고 남겨진 PCB 정보도 제거한다.
리눅스에서는 termintate 되는 프로세스가 자식 프로세스를 가지는 경우 남겨진 자식 프로세스는 init 프로세스의 자식이 된다.
기본적으로 운영체제는 보안을 위해 한 프로세스가 다른 프로세스의 정보를 변경할 수 없도록 방지한다.
그럼에도 프로세스들이 서로 협력해서 기능을 구현해야 할 때가 있는데, 이 때 Inter-Process Communication을 사용한다.
Message Passing
프로세스 A가 B에게 메세지를 보낼 때 메세지를 커널에게 보내는 System Call을 호출해 커널에게 메세지를 보낸다.
먼저 운영체제에게 메세지 큐를 만들어달라고 요청하고 자식 프로세스를 생성한다.
자식 프로세스는 부모 프로세스의 리소스를 그대로 가져가니 부모 프로세스가 만든 메세지 큐를 자식 프로세스도 같이 사용할 수 있다.
send System Call은 실제로 메세지를 보내는 작업을 수행하고, receive System Call은 자신에게 도착한 메세지를 확인한다.
프로세스 A가 커널을 통해 B에게 메세지를 전달하면, 이후 프로세스 B가 실행될 때 커널을 통해 자신에게 도착한 메세지를 받아서 처리한다.
구현은 간단하지만.. 여러 번의 Mode Change가 필요해 실행 시간이 길어진다.
Shared Memory
프로세스 A가 운영체제에게 A와 B가 함께 공유하는 공간을 만들어달라고 요청하고 자식 프로세스를 생성한다.
A가 B에게 메세지를 전달할 때, 커널에 요청하지 않고 Shared Memory에 메세지를 작성한다.
커널을 거치지 않아 실행 속도가 빠르지만, Shared Memory에서 Conflict가 발생할 수 있으니 동기화 문제를 해결해 줘야 한다.
IPC의 장점은 MSA와 유사한 점이 많다.
분산된 프로세스들이 특정 목적을 위해 협력할 수 있고, 병렬 처리를 통해 성능을 향상시킬 수 있다.
OS 레벨에서 데이터를 공유할 때는 IPC를, 아키텍처 레벨에서 데이터를 공유할 때는 MSA를 사용한다.
프로세스가 다른 프로세스에게 이벤트가 발생했음을 알릴 때 signal을 사용한다.
시그널을 받은 경우.. 프로세스를 끝내거나, 중단하거나, 무시하거나, 사용자가 지정한 핸들러를 수행하게 된다.
Signal - 프로세스가 다른 프로세스에게 이벤트 발생을 전달
Interrupt - 하드웨어 장치들이 운영체제에게 이벤트 발생을 전달
System Call - 프로세스가 운영체제에게 특정 작업을 요청
쓰레드는 프로세스의 실행 단위이고, 실행 단위는 CPU 명령어의 집합으로 생각하면 된다.
프로세스가 실행된다는건 프로세스 내부의 쓰레드가 실행됨을 의미한다.
쓰레드가 실행되려면 여러 컴퓨팅 리소스가 필요한데, 프로세스는 리소스를 운영체제로부터 할당받은 껍데기라고 생각하자.
실제로 명령어를 실행하는 매체는 쓰레드이다.
모든 프로세스는 내부에 쓰레드를 하나 이상 가지고, 필요한 경우 프로세스가 운영체제에게 요청해 쓰레드를 두 개 이상 가질 수 있다.
쓰레드는 각 요소마다 Stack과 Thread Control Block을 자체적으로 가지고 있다.
하나의 쓰레드를 실행하다가 다른 쓰레드로 옮겨 가서 작업하는 경우가 있는데, 이 때 기존 쓰레드가 사용하던 값을 Register에 저장해 이어서 작업할 수 있도록 한다.
프로세스는 files를 가진다. 어떤 프로그램을 실행할 때 기본적으로 세 개의 파일이 프로세스에게 주어지는데, 이 파일은 프로세스의 입출력 장치로 사용된다. (stdin, stdout, stderr)
각 쓰레드는 data code files 영역을 공유해 전역변수를 함께 사용할 수 있다.
프로세스를 새로 만들 때는 컴퓨팅 리소스를 모두 할당해야 해서 할 일이 많지만.. 쓰레드를 새로 만들 때는 Register와 Stack 부분만 새로 할당하면 되기에 메모리도 적게 사용하고 생성 속도도 빠르다.
새로 만들 때, 종료할 때, 스위치 할 때, 통신할 때.. 무슨 작업을 하더라도 프로세스보다는 쓰레드를 다루는 편이 더 가볍다.
프로세스 하나에 여러 쓰레드를 실행시키는 방식을 멀티쓰레딩이라고 부른다.
멀티쓰레드 환경에서는 프로세스 전체가 Block되지 않고 해당 쓰레드만 Block 된다.
(User Level Thread에서는 위의 예시처럼 실행할 수 없다)
각 쓰레드는 독립적으로 CPU를 할당받아 스케쥴링된다.
CPU 코어가 여러 개인 경우, 운영체제가 쓰레드 A와 쓰레드 B를 병렬로 실행할 수 있어 좀 더 빠르게 처리할 수 있다.
쓰레드 스케쥴러가 이를 관리하고, 선택한 쓰레드를 실행한다.
단일 쓰레드 프로세스 3개를 실행할 때는 서로 다른 메모리 공간을 가져 데이터 공유가 어렵다.
IPC를 통해 통신해야 하지만, 각 프로세스가 독립적이니 하나의 프로세스가 고장나더라도 다른 프로세스는 영향받지 않는다.
반면 프로세스 하나에 3개의 쓰레드를 실행할 때는 데이터 공유가 편하고 좀 더 적은 비용으로 프로그램을 실행할 수 있다.
쓰레드 간 동기화 문제가 발생할 수있고, 쓰레드에서 발생한 오류가 전체 프로세스까지 전파될 수 있다.
싱글 프로세스에 싱글 쓰레드 기반으로 모든 작업을 처리하면 CPU를 비효율적으로 사용하게 되니.. 효과적인 방법을 선택해보자.
쓰레드를 만드는 작업이 커널 바깥에서 이루어지는 경우를 User Level Thread 라고 부른다.
쓰레드를 만들고 관리하는 시간이 Kernel Level Thread보다 빠르고, 커널 입장에서는 프로세스 하나에 쓰레드가 하나만 있는 것으로 인식해 쓰레드 하나에서 커널로 요청한 입출력 요청이 Block 된 경우, 커널은 프로세스 전체를 Block 하게 된다.
쓰레드를 만드는 작업이 커널 내부에서 이루어지는 경우는 Kernel Level Thread 라고 부른다.
커널 내부에 쓰레드를 만들어 Mode Change가 발생하고 시스템 관리 관련 작업을 처리하게 돼 속도는 느리지만, 커널 입장에서도 멀티쓰레드 환경으로 인식할 수 있다.
쓰레드 모델 중 M:N 모델으로, KLT안에서 여러 ULT를 생성해서 사용하는 것도 가능하다.
운영체제는 KLT만 스케쥴링하고, 그 안에서 어떤 ULT를 언제 실행할지는 라이브러리 레벨에서 사용자가 직접 관리한다.
성능 최적화는 잘 되지만.. 구현 복잡도가 높다. OS는 대부분 1:1 모델을 사용한다. (Go나 Rust에서 M:N 모델 사용)
Tomcat은 Kernel Level Thread를 사용한다.
자바의 쓰레드 풀 기반으로 요청을 처리하는데, 자바의 쓰레드는 기본적으로 Kernel Level Thread를 사용한다.
만약 User Level Thread를 사용했다면... 커널이 쓰레드를 인식하지 못해 전체 프로세스가 마비될 수 있다.
Tomcat은 I/O 작업이 많으니 Kernel Level Thread를 사용하는 편이 합리적이다.
스프링을 보면.. CGI -> 서블릿 -> 스프링 순서로 발전했는데, CGI 방식에서는 요청이 올 때 마다 새로운 프로세스를 fork()해서 프로그램을 실행하는 방식으로, 동적 리소스 요청을 멀티프로세스 방식으로 처리했다.
이후 서블릿이 도입되면서 멀티쓰레드 방식으로 요청을 처리하는데, Tomcat이 한 번 서블릿 객체를 생성한 후 클라이언트의 요청마다 새로운 쓰레드로 service() 메서드를 호출해 하나의 JVM 내부에서 쓰레드로 요청을 처리한다.
어떤 방법을 쓰든 프로세스를 만드는 것 보다는 훨씬 빠르다.
Concurrent한 작업을 수행해야 할 때는 Kernel Level Thread 방식을 사용하고, 굳이 그럴 필요 없이 빨리 동작하는게 중요한 경우 User Level Thread 방식을 사용한다.
(Parent Child 보다는 First Second 표현이 더 적합하다)
리눅스에서 프로세스가 생성되면 task_struct 구조체를 할당한다.
이 자료구조는 User Context를 저장하는 자료구조를 가지고 있는데, 이 상황에서 두 번째 쓰레드를 만들면 clone System Call을 호출하게 된다.
커널은 pid 12인 task_struct를 새로 할당하고 code와 data는 공유해서 사용하도록 설계한다.
멀티쓰레드 환경에서는 각 쓰레드끼리 전역변수를 공유하고, 멀티프로세스 환경에서는 각 프로세스끼리 전역변수를 공유할 수 없다.
'Computer Science > Operating System' 카테고리의 다른 글
[Operating System] Semaphore (0) | 2025.04.08 |
---|---|
[Operating System] 프로세스와 동시성 제어 (0) | 2025.03.28 |
[Operating System] Process (Context, Creation, Switch) (0) | 2025.03.17 |
[Operating System] 커널 구조와 프로세스 (0) | 2025.03.12 |
[Operating System] 운영체제 개요 (0) | 2025.03.10 |
댓글
이 글 공유하기
다른 글
-
[Operating System] Semaphore
[Operating System] Semaphore
2025.04.08 -
[Operating System] 프로세스와 동시성 제어
[Operating System] 프로세스와 동시성 제어
2025.03.28 -
[Operating System] Process (Context, Creation, Switch)
[Operating System] Process (Context, Creation, Switch)
2025.03.17 -
[Operating System] 커널 구조와 프로세스
[Operating System] 커널 구조와 프로세스
2025.03.12