정리
- 프로세스는 자원을 따로 복사해 무겁고 안전하지만 비용이 크다.
- 쓰레드는 자원을 공유해 가볍고 빠르지만 공유 데이터 관리가 필요하다.
- 생성 비용이 낮고 응답성이 좋아 병렬 처리와 I/O 중첩에 유리하다.
- ULT는 빠르지만 블록 문제가 있고, KLT는 진정한 병렬성을 제공하지만 오버헤드가 있다.
- 멀티코어에서의 성능 향상은 Amdahl의 법칙과 오버헤드에 의해 제한된다.
Intro.
1. 모듈 간 통신 방법
프로젝트를 모듈 단위로 설계할 때, 모듈의 위치에 따라 통신 방법이 달라진다.
-
하나의 프로세스 내부
- 함수를 직접 호출
- (메모리를 공유하여 별도의 통신 기법 불필요)
-
(같은 컴퓨터) 여러 프로세스
- 프로세스는 메모리를 공유하지 않으므로 OS가 제공하는 IPC(Inter-Process Communication) 필요
- Shared Memory: 메모리 영역 공유 → 빠르지만 동기화 문제
- Message Passing: 메시지 송수신 → 안전하고 단순
- Semaphore: 통신이 아닌 동기화 도구 (공유 자원 접근 제어)
- Pipe: 파일처럼 동작하는 단방향 채널
- 프로세스는 메모리를 공유하지 않으므로 OS가 제공하는 IPC(Inter-Process Communication) 필요
-
다른 노드(컴퓨터)에 있는 프로세스
- OS가 IPC 중재 불가 → 네트워크 API 사용
- Socket: 저수준 통신, IP+포트 연결, TCP/UDP 선택
- RPC: 원격 함수를 로컬 함수처럼 호출 (내부적으로 소켓 사용)
- OS가 IPC 중재 불가 → 네트워크 API 사용
2. 소프트웨어 개발 3요소
- 기능 정확도 (입문 단계)
- 성능 최적화 (경쟁력)
- Secure Coding
3. 프로세스 vs 스레드
-
멀티 프로세스 (
fork()+exec())- 각 프로세스가 독립된 메모리 공간 보유
- 통신 시 IPC 필요 → 무거움
-
멀티 스레드 (
pthread_create())- 같은 프로세스 내에서 메모리 공유
- Process Context 오버헤드 없음 → 가볍고 빠름
-
핵심 포인트
- 한 프로세스 안에서 동시 작업이 필요할 때,
- 무거운 멀티 프로세스 대신 메모리를 공유하는 스레드를 사용하면 효율적이다.
1. 프로세스와 쓰레드
-
프로세스는
- 실행 중인 프로그램(a program in execution)이다.
-
프로세스는 두 가지 역할을 동시에 가진다.
- 메모리·파일·IPC·시그널 같은 자원을 소유하는 자원 소유권(resource ownership) 역할
- 다른 하나는 CPU 레지스터와 스케줄링 정보를 바탕으로 실제 실행되는 수행/스케줄링 역할
-
현대 운영체제는 이 두 역할을 분리해서,
- 자원을 담는 그릇은 태스크(task),
- 실제 제어 흐름은 쓰레드(thread)로 본다. 그래서 쓰레드를 경량 프로세스(lightweight process)라고 부르기도 한다.
1.1. 프로세스 이미지
-
프로세스 이미지는
- 위에서부터 PCB, 텍스트(코드), 데이터(전역변수), 힙(동적 할당), free area, 스택(지역변수·매개변수) 순으로 구성된다.
- 힙은 위로, 스택은 아래에서 위로 자라며 가운데 빈 공간을 공유한다.
-
스레드별로 저장해야 하는 정보
- 스레드별 스택
- 함수 호출, 지역 변수, 매개변수 등
- 스레드 제어 블록(TCB; Thread Control Block)
- 스레드 ID, PC (Program Counter), 레지스터 값 등
- 스레드별 스택
1.2. 쓰레드 모델
-
스레드 생성 및 처리 흐름
- creating program: 메인 프로그램에서 이벤트(클라이언트 연결 등) 감지
pthread_create(fn()): 해당 이벤트를 처리할 새 스레드 생성- created thread: 스레드 생성 완료
processfd(): 생성된 스레드 내에서 소켓(File Descriptor)을 실제로 처리
-
쓰레드 모델은 4가지로 구분된다.
- 단일 프로세스/단일 쓰레드
- 단일 프로세스/멀티 쓰레드
- 멀티 프로세스/프로세스당 단일 쓰레드
- 멀티 프로세스/프로세스당 멀티 쓰레드
-
일반 함수 호출 vs. 쓰레드 함수 호출의 차이가 핵심이다.
- 일반 함수 호출은 호출한 쪽이 멈추고 함수가 끝나야 돌아온다. (단일 실행 흐름)
pthread_create()로 만든 쓰레드는 생성 즉시 별개의 실행 흐름으로 갈라져 동시에 진행된다.
-
멀티쓰레딩 환경의 공유 관계:
- 프로세스(태스크) 단위로 공유되는 것: 가상 주소 공간, 파일, I/O 자원, 다른 프로세스에 대한 접근 제어
- 쓰레드마다 별개로 갖는 것: 실행 상태(수행/준비/블록), CPU 레지스터 문맥, 실행 스택, 지역변수 저장 공간
- 즉 한 프로세스 내 모든 쓰레드는 코드·데이터·파일을 공유하지만, 레지스터와 스택은 각자 따로 가진다. (단일 쓰레드 모델은 이 모든 것을 하나만 가짐)
cf. Creation Overhead
| 방식 | 비용 | (100개 작업) 총 비용 |
|---|---|---|
| 일반 | 1000 / 1 / 500 | 1500 × 100 = 150,000 |
| 쓰레드 | 1000 / (100 / 1 / 50) | 1,000 + 150 × 100 = 15,100 |
| 5개 쓰레드 풀 | 1000 / (100 / 1 / 50) | 1,000 + 5 × 150 + 100 × 1 = 1,850 |
(5개 쓰레드 풀, 100개 작업 기준) → 생성/종료 비용이 0에 수렴
-
비용 비교 (생성/종료 오버헤드)
- 프로세스 방식: 1개 생성/종료 비용이 1,500이며, 100번 반복 시 총 150,000 소모
- 쓰레드 방식: 프로세스 초기 비용(1,000) + 쓰레드 생성/종료(150) × 100 = 15,100
- 결론: 쓰레드가 프로세스보다 약 10배 저렴하지만, 여전히 매번 생성/종료하는 비용이 누적됨
-
풀(Pool)의 등장 배경
- “100번의 작업 동안 생성/종료를 아예 하지 않는다면?”이라는 질문에서 출발
- 미리 만들어 두고 재사용하면 생성/종료 비용을 거의 제거할 수 있음
-
풀의 종류
- 프로세스 풀(Process-pool): 독립된 메모리 공간을 가진 프로세스들을 미리 생성
- 쓰레드 풀(Thread-pool): 하나의 프로세스 내에서 자원을 공유하는 쓰레드들을 미리 생성
-
풀의 동작 3단계
- 초기화: 시작 시 미리 생성 (생성 비용은 이때 한 번만)
- 작업 할당: 들어온 일을 Idle 상태의 자원에 배정
- 반환: 작업 완료 후 종료하지 않고 풀에 반납 → 재사용
1.3. 쓰레드의 장점
-
쓰레드의 장점:
- 프로세스보다 생성/종료 비용이 훨씬 저렴함
- 병렬처리에 적합 → 멀티프로세서 효율 활용, 병행 서버 구현 (요청 수신 쓰레드 + 응답 쓰레드)
- 응답시간 향상 → I/O 작업과 계산 작업을 동시 진행 (워드 프로세서, 스프레드시트 등)
-
생성 비용 차이는 표(12쪽)에서 극적으로 드러난다.
- 5만 번 생성 기준으로
fork()는 수십~100초대인데 pthread_create()는 1~2초 수준으로, 쓰레드가 훨씬 가볍다.
- 5만 번 생성 기준으로
2. 프로세스/쓰레드 생성 함수
2.1. fork() 시스템 호출
- fork() (프로세스 생성):
- 한 번 호출되지만 부모와 자식 양쪽에서 반환된다.
- 부모에게는 자식의 PID(0이 아닌 값), 자식에게는 0이 반환되므로 이 반환값으로 분기한다.
if (pid == 0) { /* 자식 코드 */ }
else { /* 부모 코드 */ }(1) 예제
/* test_fork2.c: 같은 변수 접근 */
#include <sys/types.h>
#include <unistd.h>
// 함수 원형: pid_t fork(void);
int glob = 6;
char buf[] = "a write to stdout\n";
int main(void) {
int var = 88;
pid_t pid;
if (write(STDOUT_FILENO, buf, sizeof(buf)) != sizeof(buf)) {
perror(“write error”);
exit(1);
}
printf("before fork\n"); /* we don't flush stdout */
if ( (pid = fork()) < 0) {
perror(“fork error”);
exit(1);
} else if (pid == 0) { // **자식 프로세스**: 0 반환
/* modify variables */
glob++;
var++;
} else { // **부모 프로세스**: 자식 프로세스의 PID 반환 (non-zero)
sleep(2);
}
printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var
// 부모는 glob=6, var=88
// 자식은 glob=7, var=89
exit(0);
}- 예제(
test_fork2.c)의 핵심은- fork 후 부모·자식이 변수를 복사해서 따로 가진다는 점이다.
- 자식이
glob++; var++를 해도 부모 값은 그대로이다. - 실행 결과에서 자식은
glob=7, var=89, 부모는glob=6, var=88로 서로 독립적임이 확인된다.
2.2. pthread_create()
-
pthread (POSIX Thread): IEEE POSIX 1003.1c 표준
pthread.h를 include하고 컴파일 시-lpthread옵션을 붙인다. (gcc -o test test.c -lpthread)- 리눅스의 pthread는
clone()시스템 콜로 구현된 커널 수준 쓰레드이다. - 생성은
pthread_create(&tid, NULL, 함수, 인자), 종료는pthread_exit(NULL)로 한다.
-
여기서 fork와 결정적으로 다른 점이 나온다.
- pthread 예제에서는 전역변수
glob을 모든 쓰레드가 공유하므로 6→7→8→9→10→11로 누적 증가한다. - 반면 지역변수
var는 각 쓰레드 스택에 따로 있어 항상 89로 동일하다. - 앞의 fork 예제와 비교하면 “프로세스=복사 / 쓰레드=공유”의 차이가 명확해진다.
- pthread 예제에서는 전역변수
(1) 예제
#include <pthread.h> // gcc test.c -o test -lpthread
#define NUM_THREADS 5
void *PrintHello(void *threadid) {
printf("Hello World! It’s me, thread #%ld \n", threadid);
pthread_exit(NULL);
}
int main (int argc, char *argv[]) {
pthread_t threads[NUM_THREADS];
int rc, t;
for (t=0;t < NUM_THREADS;t++) {
printf("In main: creating thread %d\n", t);
// 쓰레드 생성 (&thread, NULL, 함수명, 인자)
rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);
if (rc) {
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
}
pthread_exit(NULL); // 쓰레드 종료
}2.3. 활용 사례
-
웹 서버
- Dispatcher thread가 요청을 받아 Worker thread에 분배하고,
- Worker는 캐시 확인 후 페이지를 반환
-
RPC(원격 프로시저 호출)
- 여러 쓰레드가 각각 다른 서버에 RPC 요청을 보내 응답 대기 시간을 중첩
cf. TCP 소켓 통신의 서버/클라이언트 흐름 예
- 윈도우 서버 프로그래밍 발전사
- 초기: WSAAsyncSelect (메시지 기반)
- 중기: WSAAsyncEvent (이벤트 신호 기반)
- 발전: Overlapped I/O (비동기 결과 통보)
- Event 방식 (I), Completion Routine 방식 (II)
- 완성: IOCP(I/O Completion Port) = Thread Pool + 비동기 I/O
흐름 과정에서 bind()가 명시적/암묵적으로 어떻게 호출되는지
- 서버의 accept()는 내부적으로 새로운 소켓(nsd)을 만들어 클라이언트와 1:1 통신을 담당시킨다.
- 클라이언트는 명시적으로 bind()를 호출하지 않지만, connect() 내부에서 자동으로 임시 포트가 bind된다.
// ===== Server =====
sd = socket(AF_INET, SOCK_STREAM, 0); // TCP 소켓 생성 (listening socket)
bind(sd, IP, Port); // 서버의 IP/Port에 결합
listen(sd, backlog); // 연결 요청 대기 큐 생성
while (1) {
nsd = accept(sd, &client_addr, &len); // 클라이언트 연결 수락
// 내부적으로 새 소켓 nsd 생성:
// nsd = socket() → 통신 전용 소켓
// bind(nsd, server_IP, same_Port) → 서버 주소에 자동 결합
// (즉, listening socket과 동일한 포트를 공유하되,
// 4-tuple [src IP/Port, dst IP/Port]로 구분됨)
if (fork() == 0) { // 또는 pthread_create()
close(sd); // 자식은 listening socket 불필요
svc(nsd); // 클라이언트 서비스 처리
close(nsd);
exit(0);
}
close(nsd); // 부모는 통신 소켓 불필요, 다시 accept 대기
}// ===== Client =====
sd = socket(AF_INET, SOCK_STREAM, 0); // TCP 소켓 생성
connect(sd, server_IP, server_Port); // 서버에 연결 요청
// 실질적으로 connect() 안에서 자동으로:
// bind(sd, INADDR_ANY, 0) → OS가 임시 포트(ephemeral port) 자동 할당
// (클라이언트는 굳이 자기 포트를 지정할 필요가 없으므로)
svc(sd); // 서버와 통신
close(sd);-
서버 측: accept()의 동작
sd(listening socket)는 연결을 받기만 하는 전용 소켓accept()는 새 연결이 들어올 때마다 새로운 소켓nsd를 생성하여 반환nsd는 해당 클라이언트와의 1:1 통신 전용 소켓- 즉, 같은 포트(예: 80)에 여러 클라이언트가 붙어도 각각 별도의 nsd로 구분됨 (4-tuple로 식별)
-
클라이언트 측: 암묵적 bind
- 클라이언트는 자신의 포트 번호를 신경 쓸 필요가 없음
connect()호출 시 OS가 사용 가능한 임시 포트(ephemeral port)를 자동 할당하여 내부적으로 bind- 그래서 클라이언트 코드에는 명시적 bind()가 없음
-
멀티쓰레딩/멀티프로세싱과의 연결
svc(nsd)를 처리하는 부분에서 보통 fork() 또는 pthread_create()를 사용- 한 서버가 동시에 여러 클라이언트를 처리할 수 있는 이유 = 새 nsd마다 새로운 쓰레드/프로세스 배정
- 여기서 앞서 정리한 쓰레드 풀(Thread Pool) 개념이 활용됨 (매번 생성/종료 대신 미리 만들어둔 풀에서 꺼내 씀)
3. 쓰레드의 유형
| 구분 | 사용자 수준 쓰레드(ULT) | 커널 수준 쓰레드(KLT) |
|---|---|---|
| 관리 주체 | 응용 프로그램이 쓰레드 라이브러리를 통해 | 커널이 직접 |
| 커널 인지 여부 | 커널은 쓰레드 존재를 모름 | 커널이 문맥·스케줄링 관리 |
| 예 | cthread, pthread | Windows |
3.1. 사용자 수준 쓰레드 (ULT)
-
사용자 수준 쓰레드 장점:
- 쓰레드 전환 시 커널 모드 전환이 필요 없어 오버헤드 절약,
- 응용에 맞는 스케줄링 가능,
- 모든 OS에서 동작
-
사용자 수준 쓰레드 단점:
- 한 쓰레드가 블로킹 시스템 호출을 하면 같은 프로세스의 모든 쓰레드가 함께 블록됨,
- 멀티프로세서 이점을 살리지 못함,
- 커널 루틴 자체는 멀티쓰레딩 불가
-
성능 표(30쪽)를 보면
- Null Fork 연산이 ULT 34, KLT 948, 프로세스 11,300(μs)로,
- 사용자 수준 쓰레드가 가장 빠르고 프로세스 생성이 압도적으로 느리다.
3.2. 커널 수준 쓰레드 (KLT)
-
장점:
- 다중처리기 활용 가능
-
단점:
- 모드 전환 오버헤드 발생
3.3. 결합 방식
-
결합 방식 (Combined)
- ULT와 KLT를 혼합한 방식
- 예: Solaris
-
단점:
- 구현 복잡
3.4. 쓰레드와 프로세스의 관계
- 1:1: 대부분의 UNIX
- M:1: Windows NT, Solaris, Linux, MACH (한 프로세스에 여러 쓰레드)
- 1:M: Ra(Clouds), Emerald (쓰레드가 프로세스 간 이동 가능)
- M:N: TRIX
4. 멀티코어와 멀티쓰레딩
-
Amdahl의 법칙으로 병렬화 성능 한계를 설명한다.
- 여기서 f는 병렬화 가능한 코드 비율, (1−f)는 순차적으로만 실행되는 코드 비율, N은 프로세서 개수이다.
- 순차 부분(1−f)이 전체 속도향상의 상한을 결정하기 때문에, 병렬 비율이 낮으면 코어를 아무리 늘려도 효과가 제한된다.
-
또한 실제로는 멀티프로세서 간 통신, 작업 분배, 캐시 일관성 유지 같은 오버헤드 때문에, 코어 수를 늘릴수록 속도향상이 계속 오르지 않고 어느 지점에서 정점을 찍고 오히려 꺾인다. (34쪽 그래프 (b))
cf. Linux Process Context
- 프로세스 문맥이란
- 운영체제가 프로세스를 관리하고 실행하는 데 필요한 모든 정보로,
- 시스템 문맥 · 하드웨어 문맥 · 메모리 문맥 세 영역으로 구성된다.
1. 시스템 문맥 (System Context)
커널이 프로세스 관리를 위해 유지하는 자료구조들이다. Task Structure가 중심이 되어 다른 구조들을 포인터로 연결한다.
- Task Structure: 프로세스 핵심 정보를 담고 다른 자료구조를 가리키는 중심 구조체
- File Table & fd: 열린 파일 관리 (task structure → fd 배열 → file table 참조)
- Segment Table: 메모리 세그먼트 정보, page table을 가리킴
- Page Table: 가상 주소 → 물리 주소 변환 정보
2. 하드웨어 문맥 (Hardware Context)
CPU 레지스터 값을 저장하는 영역으로 **TSS(Task State Segment)**라 불린다. 문맥 교환 시 실행 상태를 보존·복원하며, Task Structure와 양방향으로 연결된다.
- 주요 레지스터는
eip(다음 명령어 주소),sp(스택 포인터),eflags(CPU 상태 플래그),eax(범용 레지스터),cs(코드 세그먼트)이다.
3. 메모리 문맥 (Memory Context)
- 프로세스가 실제 사용하는 메모리 공간이다.
- Memory(주기억장치): page table로 매핑된 물리 메모리, 코드·데이터 적재
- Disk(swap / a.out): 메모리에 없는 부분이 보관되는 곳, 필요 시 메모리로 적재