프로세스 생성과 컨텍스트 스위칭의 큰 오버헤드
프로세스 사이의 통신 어려움
프로세스를 사용하는 문제점 해결을 위해
-> 프로세스보다 더 작은 스케줄링(실행)단위가 필요
-> 프로세스의 생성 및 소멸에 따른 오버헤드 저감
-> 빠른 컨텍스트 스위칭
-> 프로세스사이 통신에 대한 어려움 해소
스레드는 실행단위이며 스케줄링 단위이다.
프로세스마다 PCB를 두고 프로세스 정보를 관리하듯이
스레드마다 TCB(Thread Control Block) 구조체를 두고
스레드 ID, 스케줄링 우선순위 등 스레드 정보를 관리하며 생성,소멸,스케줄링 등 스레드를 독립적인 단위로 다룬다 -> 운영체제는 TCB를 통해 스레드의 존재를 인식한다.
프로세스는 스레드들의 컨테이너이다.
프로세스를 생성할 때 커널은 자동으로 프로세스 내에 1개의 스레드를 생성하는데, 이 스레드를 메인 스레드라고 부른다. 프로세스가 실행을 시작한다는 것은 메인 스레드가 실행을 시작하는 것이다. 프로세스마다 PCB가 생성되고, 스레드마다 스레드 정보를 담는 TCB가 생성된다.
프로세스는 스레드의 공유 공간을 제공한다.
프로세스의 주소 공간 내에 스레드의 코드들이 적재되고
스레드들은 프로세스 내에 작성된 함수들을 호출할 수 있고 프로세스 내에 선언된 전역변수들을 액세스할 수 있으며 프로세스의 힙을 공유한다.
스레드가 동적 할당 받는 곳이 프로세스의 힙이고, 한 스레드가 동적 할당받은 메모리를 다른 스레드가 액세스할 수 있다.
스레드는 함수로 작성한다
- 응용 프로그램에서 스레드가 실행할 작업을 함수로 작성
스레드의 생명과 프로세스의 생명
- 스레드로 만든 함수가 종료하면 스레드 종료
main()함수: 스레드 코드.
calcThread() 함수
전역 변수 sum
#include <pthread.h> // pthread 라이브러리를 사용하기 위해 필요한 헤더 파일
#include <stdio.h>
#include <stdlib.h>
void* calcThread(void *param); // 스레드로 작동할 코드(함수)
int sum = 0; // main 스레드와 calcThread가 공유하는 전역 변수
int main() {
pthread_t tid; // 스레드의 id를 저장할 정수형 변수
pthread_attr_t attr; // 스레드 정보를 담을 구조체
pthread_attr_init(&attr); // 디폴트 값으로 attr 초기화
pthread_create(&tid, &attr, calcThread, "100"); // calcThread 스레드 생성
// 스레드가 생성된 수 커널에 의해 언젠가 스케줄되어 실행
pthread_join(tid, NULL); // tid 번호의 스레드 종료를 기다림
printf("calcThread 스레드가 종료하였습니다.\n");
printf("sum = %d\n", sum);
}
void* calcThread(void *param) { // param에 "100" 전달 받음
printf("calcThread 스레드가 실행을 시작합니다.\n");
int to = atoi(param); // to = 100
int i;
for(i=1; i<=to; i++) // 1에서 to까지 합 계산
sum += i; // 전역 변수 sum에 저장
}
즉,
테트리스가 3개의 스레드로 구성된다는 것 = 3개의 TCB가 존재하고, 3개의 함수 주소가 각각 저장되어 있다.
일반적으로 함수는 다른 함수에 의해 호출되어 실행되지만 스레드 함수의 코드는 커널에 의해 직접 CPU가 실행하도록 제어된다.
concurrency(동시성): 1개의 CPU에서 2개 이상의 스레드가 동시에 실행 중인 상태, 타임 슬라이스 단위로 CPU를 사용하도록 번갈아 스레드 실행
parallelism(병렬성): 2개 이상의 스레드가 다른 CPU에서 같은 시간에 동시 실행
스레드가 실행 중에 사용하는 메모리 공간
스레드의 코드, 데이터, 힙, 스택 영역
스레드 코드 영역:
- 프로세스의 코드 영역 사용
스레드 데이터 영역:
- 스레드가 사용할 수 있는 데이터 공간
2개의 공간으로 구분
스레드 힙
- 모든 스레드가 동적 할당받는 공간, 프로세스의 힙 공간 사용
- 스레드에서 malloc()를 호출하면 프로세스의 힙 공간에서 할당받음
스레드 스택
- 스레드가 생성될 때마다 프로세스의 사용자 스택의 일부분을 할당받음
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void printsum(); // 모든 스레드에 의해 호출되는 함수
void* calcThread(void *param); // 스레드 코드(함수)
static __thread int tsum = 5; // 스레드 로컬 스토리지(TLS)에 tsum 변수 선언
int total = 0; // 프로세스의 전역 변수, 모든 스레드에 의해 공유
int main() {
char *p[2] = {"100", "200"};
int i;
pthread_t tid[2]; // 스레드의 id를 저장할 정수 배열
pthread_attr_t attr[2]; // 스레드 정보를 담을 구조체
// 2개의 스레드 생성
for(i=0; i<2; i++) {
pthread_attr_init(&attr[i]); // 구조체 초기화
pthread_create(&tid[i], &attr[i], calcThread, p[i]); // 스레드 생성
printf("calcThread 스레드가 생성되었습니다.\n");
}
// 2개 스레드의 종료를 기다린 후에 total 값 출력
for(i=0; i<2; i++) {
pthread_join(tid[i], NULL); // 스레드 tid[i]의 종료대기
printf("calcThread 스레드가 종료하였습니다.\n");
}
printf("total = %d\n", total); // 2개 스레드의 합이 누적된 total 출력
return 0;
}
void* calcThread(void *param) { // 스레드 코드
printf("스레드 생성 초기 tsum = %d\n", tsum); // TLS 변수 tsum의 초기값 출력
int i, sum = 0; // 지역 변수
for(i=1; i<= atoi(param); i++) sum += i; // 1~param까지 더하기
tsum = sum; // TLS 변수 tsum에 합 저장
printsum();
total+=sum; // 전역 변수 total에 합 누적
}
void printsum() { // 모든 스레드가 호출할 수 있는 공유 함수
printf("계산 후 tsum = %d\n", tsum);
}
Q. 이 프로그램이 실행되는 동안 총 몇개의 스레드가 실행되는가?
A. main스레드 포함 총 3개가 실행된다.
Q. tsum을 어떤 변수라고 부르는가? 그리고 total 변수와의 차이점은 무엇인가?
A.Tsum은 각 스레드의 TLS 영역에 스레드마다 생기고 스레드에 의해 사적으로 사용. Total은 프로세스 전체에 하나만 생기고 프로세스에 속한
모든 스레드에 의해 공유
스레드 생성
스레드 종료: 프로세스 종료와 스레드 종료의 구분 필요
스레드 종료
스레드 조인
- 스레드가 다른 스레드가 종료할 때까지 대기
스레드 양보
- 스레드가 자발적으로 yield()와 같은 함수 호출을 통해 스스로 실행을 중단하고 다른 스레드를 스케줄하도록 요청
스레드의 실행중인 상태 정보
CPU 레지스터들의 값(PC, SP, 데이터/상태 레지스터 등)
스레드를 실행 단위로 다루기 위해 스레드에 관한 정보를 담은 구조체(스레드 엔티티(thread entity), 스케줄링 엔티티(scheduling entity))
커널 영역에 만들어지고, 커널에 의해 관리( Context switching 시 TCB 수정)
준비 리스트
블록 리스트
- 현재 실행중인 스레드를 중단시키고, 다른 스레드에게 CPU 할당
- 현재 CPU 컨텍스트를 TCB에 저장하고, 다른 TCB에 저장된 컨텍스트를 CPU에 적재
동일한 프로세스 내 다른 스레드로 스위칭되는 경우
1. 컨텍스트 저장 및 복귀
다른 프로세스의 스레드로 스위칭하는 경우
- 다른 프로세스로 교체되면, CPU가 실행하는 주소 공간이 바뀌는 큰 변화로 인한 추가적
인 오버헤드 발생
1. 추가적인 메모리 오버헤드
스레드 스케줄링 주체에 따라 2 종류의 스레드로 구분
- 커널 레벨 스레드: 커널에 의해 스케줄링되는 스레드
- 사용자 레벨 스레드: 스레드 라이브러리에 의해 스케줄링되는 스레드
커널 레벨 스레드
- 응용프로그램이 시스템 호출을 통해 커널 레벨 스레드 생성
- TCB는 커널 공간에 생성되며 커널에 의해 소유됨
- 커널에 의해 스케줄
- 스레드 주소 공간(스레드 코드와 데이터) : 사용자 공간 혹은 커널 공간에 존재
사용자 레벨 스레드
- 응용프로그램이 라이브러리 함수를 호출하여 사용자 레벨 스레드 생성
- 스레드 라이브러리가 스레드 정보(U-TCB)를 사용자 공간에 생성하고 소유
- 스레드 라이브러리는 사용자 공간에 존재
- 커널은 사용자 레벨 스레드의 존재에 대해 알 수 없음
프로세스 2개, 커널 레벨 스레드 4개, 사용자 레벨 스레드 3개일 때,
2개의 순수 커널 레벨 스레드: TCB1, TCB2
2개의 커널 레벨 스레드
3개의 사용자 레벨 스레드
프로세스2에서의 사용자 스레드 스케줄링
- 스레드 라이브러리가 3개의 사용자 스레드 스케줄
예) main() 함수가 스레드 라이브러리의 yield() 함수를 호출하면 이 함수는 현재 대기중인 U-TCB2, U-TCB3 중에서 하나를 선택한다. 만일 U-TCB3이 선택되었다면, U-TCB1에 현재 실행 주소 등을 저장해 두고, U-TCB3에 저장된 실행시작 주소(스레드 코드 3)로 점프하여 실행 시작 -> U-TCB3이 스케줄 되었음
응용프로그램에서 작성한 스레드가 시스템에서 실행되도록 구현하는 방법
N:1 매핑(N개의 사용자 레벨 스레드를 1개의 커널 레벨 스레드로 매핑)
3개의 사용자 레벨 스레드가 1개의 커널 레벨 스레드 TCB4 공유
장점: 단일 코어 CPU에서 멀티스레드 응용프로그램의 실행 속도가 전반적으로 빠름.
스레드 생성, 스케줄, 동기화 등에 있어 커널로 진입없이 사용자 공간에서 이루어짐
단점: 비효율적(프로세스에 속한 여러 사용자 레벨 스레드들의 병렬 처리 안됨), 하나의 사용자 레벨 스레드가 blocked되면 프로세스 전체 블록(프로세스 내 다른 사용자 레벨 스레드로 스위칭되지 못함)
1:1 매핑(1개의 사용자 레벨 스레드를 1개의 커널 레벨 스레드로 매핑, 사용자 레벨 스레드는 매핑된 커널 레벨 스레드가 스케줄되면 실행)
장점: 개념이 단순하여 구현이 용이함. 멀티 코어 CPU에서 멀티스레드 응용프로그램에게 높은 병렬성 제공. 하나의 사용자 레벨 스레드가 블록되어도 응용프로그램 전체가 블록되지 않음
단점: 사용자 레벨 스레드가 많아지면 모두 커널의 부담
N:M 매핑(N개의 사용자 레벨 스레드를 M개의 커널 레벨 스레드로 매핑. 2단계 스케줄링)
장점: 1:1 매핑에 비해 커널 엔티티 개수가 작아 커널의 부담이 적음
단점: 구현하기 복잡하여 현대의 운영체제에서는 거의 사용되지 않음