floatFirstTOC: right
🖥️ 시작하며🔍 프로세스의 문제점📌 개선된 프로세스 관리 방법🔍 스레드📌 멀티스레드의 이점🔍 스레드 이슈📌 fork() 호출 시 문제점📌 UNIX 시스템의 두 가지 fork() 버전💡 exec()는 일반적으로 주소 공간을 날려버리므로 스레드도 날라갑니다.📌 exit() 호출 시 문제점💡 Signal handling의 문제⚙️ 라이브러리는 내부에 독립적인 버전의 변수를 가질 수 있습니다. (ex: errno)📌 Multithread-safe (MT-safe)🔍 가장 빠른 함수는?📌 Kernel-level Threads VS User-level Threads📌 User-level Threads⚙️ User-level Thread의 Context Switch📌 User-level Threads의 한계⚙️ Non-preemptive, 뺏지 않는 스케줄링⚙️ Preemptive, 뺏는 스케줄링
🖥️ 시작하며
왜 스레드를 사용하는지, 스레드를 사용하는 데 있어 문제는 무엇인지에 대해 알아보려고 합니다.
🔍 프로세스의 문제점
결국은, 너무 무겁다.
- 프로세스의 구성 요소:
- 주소 공간: 모든 코드와 데이터 페이지를 포함합니다.
- OS 자원: 열린 파일 등과 계정 정보가 포함됩니다.
- 하드웨어 실행 상태: 프로그램 카운터(PC), 스택 포인터(SP), 레지스터 등을 포함합니다.
- 새로운 프로세스를 생성하는 것은 모든 데이터 구조를 할당하고 초기화해야 하기 때문에 매우 비용이 많이 듭니다.
- 예: Linux에서는 task_struct에 100개 이상의 필드가 있습니다.
- 프로세스 간 통신은 주로 OS를 통해 이루어져야 하기 때문에 비용이 많이 듭니다.
- 시스템 호출 및 데이터 복사의 오버헤드가 발생합니다.
📌 개선된 프로세스 관리 방법
- 공간 문제:
- PCB, 페이지 테이블 등의 자원 소모가 큽니다.
- 시간 문제:
- OS 구조를 생성하고,
fork
및 주소 공간을 복사하는 데 많은 시간이 소요됩니다.
- 해결 방법:
- 여러 프로세스를 병렬로 실행해야 합니다.
- 각 프로세스가 동일한 주소 공간을 매핑하여 데이터를 공유해야 합니다.
- 예: 공유 메모리
- OS가 이러한 프로세스를 병렬로 스케줄링해야 합니다.
유사점 | 차이점 |
주소 공간: 동일한 코드와 데이터를 사용합니다. | 하드웨어 실행 상태: 각 프로세스는 고유의 PC, 레지스터, SP 및 스택을 가집니다. |
권한: 동일한 권한을 사용합니다. | ㅤ |
자원: 파일, 소켓 등 동일한 자원을 사용합니다. | ㅤ |
→ 결국 실행 상태를 분리하면 됩니다.
🔍 스레드
프로그램 내에서 실행되는 명령어의 연속적인 흐름, 하드웨어의 수행 상태
- 구성 요소:
- 프로그램 카운터 및 일반 레지스터
- 지역 변수와 반환 주소를 추적하기 위한 스택
- 스레드는 프로세스의 명령어와 대부분의 데이터를 공유합니다.
- 하나의 스레드가 공유 데이터를 변경하면, 다른 스레드도 그 변경사항을 볼 수 있습니다.
- 스레드는 또한 프로세스의 대부분의 OS 상태를 공유합니다. → 자원 공유!
📌 멀티스레드의 이점
- 병행성 생성 비용이 적음: 스레드는 프로세스를 생성하는 것보다 적은 시간과 메모리를 소모합니다.
- 프로그램 구조 개선: 스레드를 사용하면 코드 구조가 더 나아지고, 복잡한 작업을 분리하여 관리하기 쉬워집니다.
- 높은 처리량: 스레드 간에 작업을 분산시켜 I/O 작업과 계산 작업을 동시에 수행할 수 있어 처리량이 증가합니다.
- 더 나은 응답성: 사용자 인터페이스와 서버 응답성이 향상됩니다. 예를 들어, 웹 서버는 여러 요청을 동시에 처리할 수 있습니다.
- 자원 공유 용이: 스레드는 동일한 주소 공간을 공유하므로 자원 관리가 효율적입니다.
- 다중 프로세서 아키텍처 활용: 스레드를 통해 병렬 처리가 가능해지며, 멀티프로세서 시스템에서 성능을 극대화할 수 있습니다.
ㅤ | 프로세스 (Process) | 스레드 (Thread) |
연결 관계 | 하나 이상의 스레드를 포함할 수 있음 | 단일 프로세스에 종속됨 |
데이터 공유 비용 | 프로세스 간 데이터 공유는 비용이 큼 | 스레드 간 데이터 공유는 저렴함 |
주소 공간 | 각 프로세스는 고유한 주소 공간을 가짐 | 동일한 주소 공간을 공유 |
스케줄링 단위 | 전통적으로 프로세스가 스케줄링 단위였음 | 스레드가 스케줄링 단위가 됨 |
실행 환경 | 프로세스는 스레드가 실행되는 컨테이너 역할을 함 | 프로세스 내에서 실행됨 |
자원 관리 | 프로세스마다 독립적으로 자원을 관리 | 자원 관리는 프로세스 수준에서 이루어짐 |
오버헤드 | 프로세스 생성과 전환 시 오버헤드가 큼 | 스레드 생성과 전환 시 오버헤드가 적음 |
보안 및 안정성 | 각 프로세스는 서로 독립적이어서 보안 및 안정성이 높음 | 스레드 간의 충돌이 있을 수 있으며 보안에 취약할 수 있음 |
🔍 스레드 이슈
📌 fork() 호출 시 문제점
- 새 프로세스가 모든 스레드를 복제하는가?
- 새 프로세스가 단일 스레드인가?
📌 UNIX 시스템의 두 가지 fork() 버전
- pthreads에서:
- fork()는 호출한 스레드만 복제합니다.
- Unix에서:
- fork()는 부모 프로세스의 모든 스레드를 자식 프로세스에서 복제합니다.
- fork1()은 호출한 스레드만 복제합니다.
💡 exec()는 일반적으로 주소 공간을 날려버리므로 스레드도 날라갑니다.
📌 exit() 호출 시 문제점
- 스레드가 exit()를 호출하면?
- 프로세스 내의 모든 스레드를 종료시킵니다.
- 메인 스레드가 자식 스레드보다 먼저 종료(return, exit())하면?
- 대부분의 경우, 메인 스레드가 종료되면 전체 프로세스가 종료되므로 자식 스레드도 함께 종료됩니다.
→ 멀티 스레드는 exit로 종료하면 안 됩니다. 무조건 return으로 종료해야 합니다.
취소 방식 | 설명 | 문제점 및 고려사항 |
비동기 취소 (Asynchronous Cancellation) | - 대상 스레드를 즉시 종료합니다. | - 대상 스레드가 자원을 보유하고 있거나, 공유 자원을 업데이트 중인 경우 문제가 발생할 수 있습니다. |
지연 취소 (Deferred Cancellation) | - 대상 스레드는 취소 지점에서 종료됩니다. | - 대상 스레드는 주기적으로 자신이 취소되어야 하는지 확인해야 합니다. |
💡 Signal handling의 문제
운영체제가 시그널을 보낼 때, 기본적으로 커널이 프로세스에게 보냅니다.
- 스레드를 사용할 때 어떤 스레드가 시그널을 받아야 하는지 정해야 합니다.
- 모든 스레드
bit mask
를 사용해 특정 스레드가 시그널을 받을지
⚙️ 라이브러리는 내부에 독립적인 버전의 변수를 가질 수 있습니다. (ex: errno)
📌 Multithread-safe (MT-safe)
라이브러리에 이 라이브러리가 멀티스레드에서 안전한지 명시되어 있음
전역 변수를 수정하는 경우 멀티스레드에서 안전한 상태로 만들어야 합니다. 읽기만 하면 괜찮습니다.
지금까지의 스레드는 커널 스레드에 대해 이야기했습니다. 지금부터는 유저 레벨 스레드에 대해 이야기합니다.
🔍 가장 빠른 함수는?
ㅤ | Local Function | User-Level Thread | System call | Kernel-level Thread |
속도 | 가장 빠름 | 빠름 | 느림 | 매우 느림 |
이유 | Local Stack에서 해결 가능 | OS 관여 없이 라이브러리에서 처리 | 일반적으로 사용자 모드에서 커널 모드로, 그리고 다시 사용자 모드로의 단일 전환을 포함 | 커널로 컨텍스트 스위칭이 필요하므로 오버헤드가 심함, 스케줄링과 동기화도 필요함
커널 수준 스레드는 커널이 관리해야 할 추가적인 자원과 상태 정보를 필요 |
📌 Kernel-level Threads VS User-level Threads
ㅤ | Kernel-level | User-level |
생성/관리 | System call → 오버헤드 증가 | local 함수나 라이브러리 |
특징 | OS 수준에서 스레드를 알 수 있음 | OS는 스레드를 볼 수 없음 |
스케줄링 | OS가 가능 | 라이브러리가 수행 |
병렬성 | OS 수준에서 스케줄링, 컨텍스트 스위칭 수행하므로 병렬성 좋음 | OS가 직접 스레드를 스케줄링 할 수 없으므로 병렬성 낮음 |
비용 | 매우 고비용 (커널 모드 전환) | 상대적 저비용 |
리소스 관리 | 커널에서 스레드 조각 생성 제한 (25만개로 제한) | 이론적 제한 X |
- 유저 레벨 스레드가 가능한 이유
- 같은 주소 공간을 공유함
- 스레드끼리는 대략 Hardware context만 상이함
→ 사용자 수준 프로세스 자체에서 조작 가능
📌 User-level Threads
- 더 싸고, 빠른 스레드가 필요했음
- User-level Threads는 런타임 시스템(사용자 수준 라이브러리)에서 전적으로 관리해서 이식성이 좋음
- 각 스레드는 Hardware context와 작은 TCB를 가짐
- 스레드 생성, 전환, 동기화는 프로시저 호출을 통해 수행됨 (커널 개입 없음)
프로시저 호출은 프로그램의 흐름을 특정 작업을 수행하는 코드 블록으로 전달하는 것입니다. 이 과정에서 현재의 실행 상태(예: 프로그램 카운터, 레지스터 값 등)를 저장하고, 호출된 프로시저(함수, 메서드)로 제어를 넘긴 다음, 해당 프로시저가 실행을 완료하면 원래의 실행 위치로 돌아오는 과정을 포함합니다.
⚙️ User-level Thread의 Context Switch
- 커널이 할 일을 라이브러리가 하므로 매우 간단함
- 각 스레드는 TCB의 일부로써 스택을 가지고, push로 저장하고 pop으로 상태 복원 수행
📌 User-level Threads의 한계
- User-level Threads는 OS에게서 보이지 않으므로 context switch시 좋지 않은 결정을 내릴 수 있음
- I/O 중인데 CPU 할당
- I/O 시작한 스레드를 Block
- 뺏으면 안되는 상황에 뺏음
🙋 다른 상황에서는 프로세스의 instruction이 사용 중이라도, Timer 하드웨어를 통해 interrupt를 주어 context switch를 하게 됨 → User-level은?
⚙️ Non-preemptive, 뺏지 않는 스케줄링
- 평화롭게 해결, 스스로 CPU를 내놓음
→ 절대 CPU를 내놓지 않으면 문제가 생김
⚙️ Preemptive, 뺏는 스케줄링
- 런타임의 스케줄러가 Time interrupt 요청 → 시그널로 받게 됨
- 소프트웨어 interrupt지만 HW→SW가 아닌 OS→SW임
댓글