[전공 CS]/[컴퓨터 구조론][운영체제]

[운영체제] 04 스레드와 멀티 스레드

우당탕탕 개발 일지 2025. 10. 21. 00:00
728x90

안녕하세요. 우당탕탕 개발일지 입니다. 중간 D-3 레츠고 

 

 

 

출제) 프로세스와 스레드 차이점 
: 프로세스는 운영체제가 응용프로그램으로 적재하는 단위 & 스레드는 운영체제의 실행단위. 

예상) 환경 컨텍스트, 실행 컨텍스트 차이 

 

 

 

CH 4-1 프로세스의 문제점. 

 

( 운영체제 실행단위 : 스레드 /  멀티 테스킹의 기본단위 : 프로세스) 

 

프로세스를 실행단위로 하는 멀티 테스킹의 문제점  : 커널에 많은 시간적, 공간적 부담 ▶ 시스템 전체 속도 저하

 

1. 프로세스 생성의 큰 오버헤드 

2.  프로세스 컨텍스트 스위칭의 큰 오버헤드 

3. 프로세스 사이의 통신이 어려움. 

 

**오버헤드 : 추가로 드는 부담, 비용, 시간 

** 컨텍스트 스위칭 : cpu가 다른 프로세스로 바꿔서 실행할 떄, 이전 상태 저장 + 새 상태 불러오기 

 

프로세스가 다른 프로세스의 메모리에 접근 불가 

→ 공유 메모리나 소켓 등등 이용 : 코딩이 어렵고 실행 속도가 느리고  운영체제 호환성이 부족함. 

 

 

 


CH 4-2 스레드 개념 

 

멀티 태스킹의 문제점 : 커널에 많은 시간, 공간 부담 ⇒ 시스템 전체 속도 저하

 

해결방안 ) 스레드 출현 4가지 이유 : 새로운 실행단위 

  1. 프로세스보다 크기가 작음.
  2. 프로세스보다 생성 및 소멸이 빠름
  3. 컨텍스트 스위칭이 빠름
  4. 통신이 쉬운, 실행 단위 필요.

스레드는 운영체제에게 실행단위이고 스테줄링 단위

 

  • 스레드는 코드, 데이터, 립 , 스택을 가진 실체
  • 스레드 마다 스레드 정보를 저장하는 구조체 TCB (Thread Control Block)있음.

 

프로세스는 스레드들의 컨테이너

 

프로세스 =하나의 아파트 

스레드 = 안에 사는 사람들 

공유 자원 = 수도, 전기 , 엘리베이터 등등 

컨테이너 = 아파트 단지 

 

프로세스는 반드시 하나 이상의 스레드로 구성.

 

PCB가 여러개의 TCB를 관리함.

 

 

PCB(Process Control Block) : 프로세스 전체의 정보를 관리하는 자료구조.  

TCB(Thread Control Block) :  프로세스 안에서 각각의 스레드 정보를 관리하는 자료구조.

 

 

 

프로세스는 스레드의 공유 공간(환경) 제공

모든 스레드는 프로세스의 코드, 데이터, 힙 공유  + 스택 공간을 나누어 사용.

공유할때 한번에 공유하면 병목 현상 발생 ⚠️   해결) 동기화 

 

스레드가 실행할 작업은 함수로 작성 

즉. 스택 영역은 공유하지 않고 각 스레드 마다 자신의 스택을 가짐. 

 

 

 

스레드의 생명과 프로세스의 생명

(스레드의 종료와 프로세스 종료를 분리해서 생각하기! )

 

스레드 종료 

  • 스레드로 만든 함수 종료 →스레드 종료 (함수내 지역 변수, 스택이 사라짐)
  • 스레드 종료 → 스레드 관련 정보 모두 제거됨 ( TCB제거)

프로세스 종료

  • 프로세스에 속한 모든 스레드 종료 [더 이상 종료할 프로세스가 없을때]  → 프로세스 종료 
  • 프로세스 종료 ( TCB제거, 4계층 메모리 해제 , PCB 삭제) →스레드 종료 

 

#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 스레드 생성 1️⃣
        // 스레드가 생성된 수 커널에 의해 언젠가 스케줄되어 실행

        pthread_join(tid, NULL); // tid 번호의 스레드 종료를 기다림
        printf("calcThread 스레드가 종료하였습니다.\n");4️⃣
        printf("sum = %d\n", sum);5️⃣
}

void* calcThread(void *param) { // param에 "100" 전달 받음💡
        printf("calcThread 스레드가 실행을 시작합니다.\n");2️⃣
        int to =  atoi(param); // to = 100
        int i;

        for(i=1; i<=to; i++) // 1에서 to까지 합 계산
                sum += i; // 전역 변수 sum에 저장3️⃣💡
}

 

더보기

$ gcc -o makethread makethread.c -lpthread

$ ./makethread

calcThread 스레드가 실행을 시작합니다.

calcThread 스레드가 종료하였습니다.

sum = 5050

 

 

총 스레드 수 2개 : 1) 메인 스레드  2) 새로 만든 스레드 

 

  • 프로세스 시작  : 메인 스레드 자동 생성 
  • pthread_create 호출  : 새 스레드 생성

 

TCB 생성 과 SUM 값 변화 위주로 보기

 

 

 

참고만 스레드가 3개면 TCB가 3개 생성.

 

 

→  각 프로세스 안에 스레드가 각각 있으니까 오버헤드가 덜 발생. (스레드 사용의 목적!) 

 

함수는 다른 함수에 의해 호출되어 실행되지만, 스레드 함수의 코드는 CPU가 바로 실행하도록 커널에 의해 제어됨

→ TCB는 커널에 의해 생성되고 관리가 됨!!.

 

 

 

멀티 스레딩 

 

*동시성 : 3개의 스레드가  1개의  CPU에 의해 동시 실행. 

동시성

 

* 병렬성  : 3개의 스레드가 3개의 cpu에 의해 동시 실행.

 


CH 4-3 스레드 주소 공간과 컨텍스트 

 

 

스레드 주소 공간 : 스레드가 실행 중에 사용하는 메모리 공간. 

 

스레드 주소 공간은  프로세스 주소 공간 내에 존재한다. 

 

 

 

 

🦄사적 공간 (스택, TLS) vs 👻공유 공간 (코드, 데이터 공간, 힙영역)

 

 

<사용자 공간 >

스레드 코드영역 👻 : 함수 

스레드 데이터 영역 👻 : 1) 전역 변수들  2) 개별 스레드의 전용 변수 공간. (TLS)

스레드 힙 👻 : 위에서 아래로 쌓임.

스레드  (사용자) 스택 🦄 : 아래에서 위로 쌓임 |  스레드가 생성될때  사용자 스택할당. & 종료시 반환.

 

<커널 공간 >

스레드 (커널) 스택 🦄 :

  •  스레드가 생성될때 커널 공간에 스레드 마다 스택할당.  & 종료시 반환
  • 스레드가 시스템 호출로 커널에 진입할 때, 커널 스택 활용. 

 

※ 스레드가 생성될때 마다 모든 스레드에 동일한 크기의 공간 할당.

static __thread int sum = 5;

 


 

 

TLS ( Thread Local Storage) : 스레드마다 안전하게 다루고자 하는 데이터를 저장하기 위한 별도의 영역

 

  • 대체로 힙이나 스택 에 할당. 언어마다 운영체제마다 할당 방법이나 위치가 다름. 
  •  TLS ↔ 프로세스의 데이터 영역은 모든 스레드의 공용 공간임. 
  • 스레드 삭제할 떄 TLS도 같이 사라짐  ( 내 개인적인 의견으로는 그냥 지역 변수를 떠올리면 될것 같음 ) 

 

 

#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);
}

 

 Q1  출력되는 순서대로 써보기 

더보기

$ gcc -o mtTLS makethreadwithTLS.c lpthread

$ ./mtTLS

calcThread 스레드가 생성되었습니다.

스레드 생성 초기 tsum = 5

calcThread 스레드가 생성되었습니다.

계산 후 tsum = 5050

스레드 생성 초기 tsum = 5

계산 후 tsum = 20100

calcThread 스레드가 종료하였습니다.

calcThread 스레드가 종료하였습니다.

total = 25150

$


 

 Q2 TLS, 코드 , 데이터, 힙, 스택에 들어가는 거 그려보기  + 총 몇개의 스레드가 생성되는지 알아보기 

 

그려보기

총 스레드는 3개. tsum의 값 변화 위주로 보기 

 


 

Q3  tsum을 어떤 변수라고 부르는가? 그리고 total 변수와의 차이점은 무엇인가?

더보기

A3. tsumTLS 변수로 불린다. tsum은 각 스레드의 TLS 영역에 스레드마다 생기고 스레드에 의해 사적으로 사용.

total프로세스의 전역변수로서 프로세스 전체에 하나만 생기고 프로세스에 속한 모든 스레드에 의해 공유

 


 

스레드 일생 

스레드 상태는 TCB에 저장.

 

 

 

 

Ready : 스레드가 스케줄 되기를 기다리는 상태 

Running : 스레드가 CPU에 의해 실행 중인 상태

Blocked : 스레드가 입출력을 요청하거나 sleep() 같은 시스템 호출로 인해 중단된 상태 

Terminated : 스레드가 종료한 상태.

 

 

 

스레드 운영 

 

1) 스레드 생성 

자동으로 main스레드 &  시스템 호출

 

2) 스레드 종료

프로세스 종료 / 스레드 종료 

 

3) 스레드 조인 

스레드가 다른 스레드 종료할 때 까지 대기 

( ex 부모가 자식 종료할때까지 대기 )

 

4) 스레드 양보

스레드가 자발적으로 yield()와 같은 함수 호출을 통해 스스로 실행을 중단하고 다른 스레드를 스케줄하도록 요청

 

 

 


 

스레드 컨텍스트 : 실행중인 cpu의 상태 정보 

  •  TCB에 저장됨. 
  • 컨텍스트 정보 : PC,SP,  다른 레지스터 등등
  • PC : 프로세스 코드  & SP : 프로세스 스택에 저장. 

 

스레드 컨텍스트 스위칭 

: 현재 실행중인 스레드 저장  - 새로운 스레드 실행  -이전에 실행하던 스레드 불러옴.

:스레드 스케줄링 후 ⇒ 실행중인 스레드를 중간 시키고 선택된 스레드에게 cpu 할당.

 

 

스레드 (컨텍스트) 스위칭 발생 경우 

→ 상태 전이도를 생각하면서 4가지 경우 생각해보기⭐️⭐️⭐️

  1.  스레드 양보 
  2. 시스템 호출 → wait() 이나 blocked() 상태 
  3.  스레드 타임 슬라이드를 소진한 경우 (TTS)
  4. 인터럽트 I/O

 스레드 스위칭이 이루어지는 위치 : 커널

1. 시스템 호출 중  : 커널 모드  (자발적) 

2. 인터럽트 발생시  : 커널 모드 (강제적)

 

 

스레드 스위칭 과정 

  • 원본 TCB → 준비 리스트(Ready List) 또는 블록 리스트(Block List) 로 이동
  • 인터럽스 TCB→ 준비 리스트에서 분리되어 실행(Running) 상태로 변경

 

컨텍스트 스위칭 오버헤드 

: 컨텍스트 스위칭은 모두 cpu 작업이기 떄문에 컨텍스트 스위칭의 시간이 길거나 잦은 경우 발생.

동일 프로세스 내 스레드 스위칭 CPU 레지스터 저장/복원, TCB 수정, 캐시 일부 변경 오버헤드 작음
다른 프로세스 간 스레드 스위칭 PCB + TCB 교체, 페이지 테이블 교체, TLB 초기화, 캐시 전체 무효화 오버헤드 큼

 

 

 

TCB 

= 스레드 엔터티 or 스케줄링 엔터티라고 불림 

: 스레드를 실행 단위로 다루기 위해 스레드에 관한 정보를 담은 구조체 

 

PCB--TCB1-TCB2

 

 

스레드 정보  tid
state
컨텍스트  PC
SP
다른 레지스터들
스테줄링⭐️ 우선순위 
CPU 사용시간

 

 

 

 

PCB와 TCB의 관계 

 

PCB와 나머지 TCB들이 포인터(pointer)로 연결됨.

 

TCB와 TCB는 연결 리스트(linked list)로 되어 있음 

→이유 : 스레드의 갯수가 가변적이고 삽입/삭제가 잦기 때문. 

PCB 와 TCB가 논리적으로 연결되어 있는 걸 나타내는 그림 .

→ 누가 먼저 실행될지는 알수 없고 커널이 cpu 스케줄링 시 선택해서 실행됨.

 

 

준비 리스트 / 블록 리스트 

  • CPU를 기다리면 (Ready)→ 준비 리스트(Ready List)
  •  입출력(I/O)을 기다리면 (Blocked , waiting)→ 블록 리스트(Blocked List)

 

 

 


CH 4-4 커널 레벨 스레드와 사용자 레벨 스레드  ( 출제 가능성 낮음) 

 

스케줄링 주체에 따라 2종류의 스레드로 구분. 

  • 커널 레벨 스레드 : 커널에 의해 스케줄링
  • 사용자 레벨 스레드 : 스레드 라이브러리에 의해 스케줄링

 프로세스당 하나의 커널 레벨 스레드(=메인 스레드 ) 자동 생성.

 

 

순수 커널 레벨 스레드 :운영체제 부팅 시 커널이 직접 생성한 스레드

  • 커널 모드에서만 사용되는거고 사용자가 조정하는게 아님.
  • ex) usb 같은거 연결할때, 마우스 클릭

 

  사용자 레벨 스레드  커널 레벨 스레드 
스레드 스위칭 사용자모드에서 커널 모드에서 
경향 잘 안씀  많이씀
컨텍스트 스위칭 속도 빠름 느림
병렬성 동시성 병렬성

 


CH 4-5 멀티 스레드 구현  ( 출제 가능성 낮음) 

 

개발자가 멀티 스레드 구현 방법 3가지

  1. 1:1   (현재 사용)
  2. N:1   
  3. N:M

 

1:1 (멀티코어 병렬 처리 완벽 지원.& 구현이 쉬움)

[사용자 공간]                                                [커널 공간]
U-TCB1 ──────────────▶ K-TCB1 ─▶ CPU 코어1
U-TCB2 ──────────────▶ K-TCB2 ─▶ CPU 코어2
U-TCB3 ──────────────▶ K-TCB3 ─▶ CPU 코어3

 

N:1  병렬처리가 안되서 비효율적임.

[사용자 공간]     [커널 공간]
U-TCB1 ─┐
U-TCB2 ├─▶   K-TCB1 ─▶ CPU 코어1
U-TCB3 ┘

 

M:N 구현이 복잡하고, 유지보수가 어려움

[사용자 공간]  [커널 공간]
U-TCB1 ─┐
U-TCB2 ├─▶ K-TCB1 ─▶ CPU 코어1
U-TCB3 ┘

U-TCB4 ─▶ K-TCB2 ─▶ CPU 코어2
U-TCB5 ─▶ K-TCB3 ─▶ CPU 코어3

 

 

 


CH 4-6 멀티 스레딩에 관한 이슈  ( 출제 가능성 낮음) 

 

4장 요약 

  • 프로세스는 스레드들의 공유 공간(컨테이너) :  모든 스레드의 주소 공간이 프로세스 주소 공간 내에 형성되고 공유
  • 프로세스는 운영체제가 응용프로그램을 적재하는 단위이고, 스레드실행 단위
  • PCB에 저장된 정보는 환경 컨텍스트, TCB에 저장된 정보는 실행 컨텍스트
  • 다른 프로세스에 속한 스레드로의 스위칭보다 동일한 프로세스에 속한 스레드 스위칭 속도가 빠름
  • 프로세스에 속한 모든 스레드가 종료할 때 프로세스 종료

 

멀티 스레딩 장점 ) 

  • 높은 실행 성능 = 병렬 실행
  • 사용자에 대한 우수한 응답성 : 사용자와의 입출력 가능
  • 서버 프로그램의 우수한 응답성 : 동시에 많은 사용자들의 접근 가능.
  • 효율성 높음 ) 스레드 >>> 프로세스
  • 응용프로그램 구조의 단순화
  • 작성이 쉽고 효율적인 통신

 

멀티 스레딩시 주의할 점)

  • 여러 개의 스레드 중 한 스레드만 fork()를 호출하는 경우 :  새 프로세스에 한 스레드만 복사되어 ‘불완전한 복제’가 발생
  • 멀티스레드 상태에서 하나의  exec()를 호출하는 것. :  현재 프로세스의 모든 스레드가 사라짐
  •  스레드 사이의 동기화 문제 : 여러 스레드가 공유 데이터를 동시에 수정하면 데이터 손상 발생

 

(+ 규약) 멀티스레드 프로그램에서 fork()를 호출한 후, 자식 프로세스에서는 즉시 exec()만 호출해야 하며, 다른 함수 호출은 하지 말 것.

.