[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

💻 CS/운영체제

[운영체제] 쓰레드

inu 2020. 4. 7. 17:29
반응형

쓰레드의 개요

  • fork()를 사용하면 사용자 공간 상 text 영역은 부모프로세스와 공유하고, data,bss,heap,stack 영역은 복사되어 별도로 할당된다. 이렇게 생성된 프로세스가 전통적 프로세스이며, heavy weight process이다.
  • 이러한 heavy weight process는 data영역이 별도로 할당되기 때문에 프로세스간의 공유변수를 갖기 어렵다. (운영체제가 제공하는 특수 구조체나 공유파일(pipe, shared mem., signal, socket 등)을 사용해야 공유가 가능해진다.) 또한 영역을 복사하는 과정이 시간적 오버헤드와 메모리 낭비를 초래할 수 있다.
  • 반면 light weight process인 쓰레드는 text와 data 모두를 공유하고 stack만 따로 갖는 형태이다.
  • 따라서 전역 변수를 쓰레드간 자료교환 수단으로 활용할 수 있다. 또한 영역은 복사하는 양이 적어 생성이 빠르고 자원 점유도 적다. 하나의 프로세스가 여러 쓰레드를 가질 수 있다. (heavy weight process는 하나의 쓰레드로 구성된 것으로 볼 수 있음)

쓰레드

  • 프로세스를 밧줄이라고 본다면, 쓰레드는 그를 구성하는 실이라고 할 수 있다.
  • 사용자 프로세스 입장에서는 하나의 프로세스가 여러 가상 CPU를 가지고 작업을 수행 중인 것으로 보인다.
  • 응답성, 자원공유, 경제성, 다중처리기 구조활용가능 의 장점이 있다.
#include <stdio.h>
#include <pthread.h>
void consumer (void); // thread function prototype
char buffer[n]; // circula queue
int n, in = 0, out = 0;

int main () {
    char nextp; int i;
    pthread_t tid;
    pthread_create (&tid, NULL, consumer,NULL); // consumer thread 의 생성
    // main thread는 producer의 역할을 한다. // consumer thread와 병행으로 수행된다.
    for (i = 0; i < 500; i++) {
    	...
        produce an item in nextp
        ...
        while ( (in+1) % n == out) ; // 소비자에서 소비하고 out이 1 더 커지길 대기
        buffer[in] = nextp; 
        in++; 
        in %= n;
    }
    pthread_join (tid);
}

void consumer(void) {
    char nextc;
    for (i = 0; i < 500; i++) {
        while (in == out) ; // 생산자에서 생성하고 in이 1 더 커지길 대기
        nextc = buff[out];
        out++; 
        out %= n;
        ...
        consume the item in nextc;
    }
}
  • 전역변수 buffer : main함수 측은 계속해서 이 곳에 데이터를 집어넣는 프로듀서 역할을 하고, consumer함수는 이 곳에서 데이터를 빼가서 활용하는 소비자역할을 한다. 이 때 이 전역변수 buffer를 데이터를 계속해서 넣고 빼는 장소로 활용한다는 의미에서 circula queue라고도 부른다.
  • 프로듀서(main)와 소비자(consumer)의 코드 흐름이 하나로 되어 있지 않기 때문에 이런 버퍼(circula queue)의 활용은 필수적이다. 데이터가 하나의 흐름으로 들어오고 나가지 않아도 정상적으로 작동하도록 하는 것이다.
  • main함수는 기본적으로 하나의 쓰레드에 의해 수행이 시작된다. 주로 이를 메인 쓰레드라고 한다.
  • pthread_create에서 consumer 함수를 불러서 consumer 쓰레드를 시작한다.
  • 그 후 메인 쓰레드는 자신의 작업을 계속해서 진행한다. 위 코드에서는 반복문을 돌면서 버퍼에 데이터를 넣는 작업을 한다.
  • comsumer 쓰레드에서는 마찬가지로 반복문을 돌면서 버퍼로부터 데이터를 빼간다.
  • pthread_join 함수는 프로세스에서 wait와 같은 역할을 하여 consumer 쓰레드가 종료되도록 기다리도록 한다.
  • 리턴 시 쓰레드는 소멸된다.
  • 프로세스로 이를 구현할 경우 파이프같은 자료구조를 따로 이용해야 한다는 번거로움이 있다. (전역변수를 공유할 수 없기 때문)

쓰레드의 레지스터 문맥과 스택

  • 한 프로세스 내에서 여러 프로세스가 동시에 동작되도록 하려면, 시분할의 단위를 프로세스가 아닌 쓰레드로 해야한다.
  • 이를 위해서 커널 수준 문맥 중 특수 레지스터와 범용 레지스터의 내용을 합친 '레지스터 문맥'을 쓰레드 별로 관리하면서 이 레지스터 문맥을 대상으로 문맥 교환을 하도록 한다.
  • text와 data를 공유하고 stack 부분만 별도로 관리한다. 따라서 쓰레드끼리 전역변수를 서로 공유할 수 있다.
  • 단, 이들을 공유하는 만큼 서로간의 간섭이 있을 수 있다. 따라서 이에 대한 제어가 필요하다. (이는 mutex나 semaphore 등으로 이루어지는데, 이들에 대해서는 추후 자세히 다루도록 하겠다.)
  • 어쨌든 결국 쓰레드의 정체는 레지스터 문맥과 스택이다.

TCB(Thread Control Block)

  • PCB는 쓰레드도 보유할 수 있도록 내부적으로 TCB 공간을 가진다.
  • 스택 부분과 레지스터 문맥(PC,SP,PSR 및 범용레지스터 공간)을 한 PCB가 여러개 가지는 형태이다.

커널 쓰레드 vs 사용자 쓰레드

  • 커널 쓰레드 : 커널은 쓰레드별로 TCB를 만들어서 문맥을 관리한다. 이처럼 커널 자체적으로 쓰레드를 만들어 관리하는 것을 '커널 쓰레드'라고 부른다. 운영체제가 직접 지원하며, 쓰레드의 생성 및 관리가 커널에서 직접 이루여져 속도는 조금 느리다.
  • 사용자 쓰레드 : 응용프로그램의 라이브러리에 의해 관리하는 것은 '사용자 쓰레드'라고 부른다. 응용프로세스가 받은 타임 슬라이스를 쓰레드 라이브러리(threadlib)를 이용하여 다시 한 번 시간을 분할해서 각 쓰레드를 수행시킬 수 있도록 하는 것이다. 따라서 커널 입장에서는 프로세스에 해당하는 쓰레드 하나만 있을 뿐, 사용자 쓰레드는 인지되지 않는다. 다만 커널에 의해 관리될 필요가 없어 생성과 관리가 빠르다.
  • 사용자 프로그램 입장에서는 사용자 쓰레드든, 커널 쓰레드든 별 차이가 없다. 그러나 입출력에 의한 대기상태에 있어서는 차이가 있다. 사용자 쓰레드의 경우엔 처음엔 하나의 쓰레드로 시작했으므로 입출력 요청 시 프로세스 전체가 대기 상태가 된다(사용자 공간에서 쓰레드 형태를 갖춘 다른 쓰레드들도 작동을 멈춘다). 하지만 커널 쓰레드는 애초에 쓰레드가 나누어져 구성되므로, 입출력 요청 시 해당 쓰레드만 대기 상태가 된다(다른 쓰레드들은 정상적으로 작업을 진행한다).
  • 두 가지 형태를 모두 지원하는 혼합형 쓰레드(다중 쓰레드 모델)도 존재한다. 이는 커널 쓰레드와 사용자 쓰레드를 일대일, 일대다, 대다대 모델로 다양하게 구성해준다. 대표적인 예로 Solaris 2 Threads에서 사용하는 LWP가 있다.

쓰레드 스캐쥴링 범주

  • LWP를 중점으로 본다면, 임의의 LWP(Light Weight Process)에 묶여있는 사용자 레벨 쓰레드들이 사용자 라이브러리에 의해서 스캐쥴링 되는 범주가 있다. 이를 PCS(Process-Contention Scope)라고 한다.
  • CPU를 중점으로 본다면, 시스템 내 모든 커널 쓰레드들이 CPU를 할당받기 위해 스케쥴링되는 범주가 있다. 이를 SCS(System-Contention Scope)라고 한다.
반응형