[OS] 프로세스와 쓰레드의 비교, pthreads, Signal handling

2024. 4. 11. 21:12CS/Operating System

Processes vs Threads

  • 쓰레드는 하나의 프로세스에 담겨있다.
  • 반면에, 프로세스는 멀티플 쓰레드를 가질 수 있다.
  • 쓰레드끼리는 같은 address 내에 있으니 싸게 데이터를 공유할 수 있다.
  • 쓰레드는 운영체제가 관리하는 스케줄링의 단위가 된다.
  • 프로세스는 쓰레드를 수용하기 위한 컨테이너로 생각하면 된다.

프로세스와 쓰레드의 유사한 점

  • 각자의 로지컬한 control flow를 가질 수 있다.
  • 각자 동시에 다른 것들과 실행될 수 있다.
  • 컨텍스트 스위치

프로세스와 쓰레드의 차이점

  • 쓰레드는 코드와 데이터를 공유한다.
    • 그러나, 프로세스는 그렇지 않다. 같은 코드와 데이터를 복제해 온다.
  • 쓰레드가 프로세스보다 훨씬 저렴하다.
    • 리눅스에선 거의 2배 차이. address space를 새로 만들지 않아도 되고, 하드웨어 상태만 관리해 주면 되니까 훨씬 쉽다.

address space 관점에서의 쓰레드

하나의 프로세스에 하나의 쓰레드

하나의 프로세스에 세 개의 쓰레드

sp, pc가 세 개가 필요한 건 아니고 저장했다가 꺼내오는 방식의 컨텍스트 스위치를 거치는 것이다.

하나의 어드레스 스페이스 안에서 여러 개의 쓰레드가 동작하므로 T1은 T2의 스택 영역을 건드릴 수 있다. 물론 시스템에 따라서는 막아놓기도 한다.


쓰레드 인터페이스, pthreads

pthreads 라이브러리를 많이 사용한다. POSIX(유닉스 계열)의 표준이다.

pthreads를 사용하여 작성된 코드는 gcc ex.c -lpthread 옵션을 줘야지만 컴파일할 수 있다.

  • pthread_create(pthread_t *tid, pthread_attr_t *attr, void *(start_routine)(void *), void *arg)
    쓰레드를 만든다. *attr 파라미터로 환경 변수 같은 걸 줄 수 있다. 쓰레드가 만들어지면 threadfunc를 실행하게 할 수  있다.
  • pthread_join(pthread_t tid, void **thread_return)
    쓰레드가 끝날 때 까지 대기한다.

쓰레드 이슈

fork(), exec(), exit()

  • 쓰레드가 fork()를 부르면?
    • 새로운 프로세스가 만들어진다. 새로운 프로세스에도 멀티쓰레드가 생기게 될텐데 제는 쓰레드가 fork()를 부르게 되면 모든 쓰레드를 다 복제하게되는가? 이다. fork()를 부른 쓰레드만 복제해서 새로운 프로세스에게 싱글 쓰레드로 만들어 줄 수도 있고, 모든 쓰레드를 다 복제할 수도 있다.
    • phthreads에서는 기본적으로 싱글 쓰레드의  프로세스를 만들어 준다.
    • 유닉스에서는 fork(), fork1()이 있어서 선택할 수 있다.
  • exec()를 부르면?
    • 하나의 쓰레드로 아예 대체된다. 새로 만들어진 프로그램에 따라 싱글 쓰레드로 처음부터 돌게 된다.
  • exit()를 부르면?
    • 프로세스를 종료시키는 게 exit()의 일이므로 전체 프로세스가 종료된다. 따라서 return으로 종료하는 게 안전하다.
  • child 쓰레드가 종료되기 전에 parent 쓰레드가 종료되면 어떻게 될까?
    • main 함수에서 return으로 종료시키면 프로세스 전체가 종료된다. 그래서 대부분은 pthread_join을 사용한다.

쓰레드 종료

  • 쓰레드를 중간에 취소하면?
    • Asynchronous cancellation : 언제든 취소할 수 있다. 타겟 쓰레드가 파일을 열어놓고 작업을 한다던가 소켓으로 통신하던 중이던가 등의 중요한 작업 중에 취소되면 문제가 생길 수 있다.
    • Deferred cancellation : 즉시 죽이는 게 아니라, 죽어야 한다고 알려주는 것이다. 타겟이 중요한 작업을 하고 있으면 프로그래머의 의도대로 동작한 후 종료한다. 가능하면 이 방법을 쓰는 게 더 좋다.
    pthreads에서는 두 방법 모두 지원한다.

Signal handling

  • Signal handling
    • 시그널은 커널이 프로세스에 보내는 것이다.
    • 쓰레드에게 시그널을 보내면 어떻게되는지 생각을 좀 해봐야 한다. 기본적으로 운영체제는 프로세스에게 시그널을 보내는 것이지, 쓰레드에게 보내는 것은 아니다.
      그렇다면 시그널을 보냈을 때 어떤 쓰레드에게 전달해야 하는지가 문제가 된다.
    • 시그널에 해당하는 쓰레드만 시그널을 받을 수 있게 할 수 있다.
    • 혹은 모든 쓰레드에게 시그널을 다 전달하는 방법도 사용할 수 있다.
    • 혹은 특정 쓰레드를 정해놓고, 무조건 그 쓰레드에게 시그널을 전달하고, 그 내부에서 알아서 시그널을 전달하는 등의 방법을 사용할 수 있다.
    • pthreads는 시그널 마스크라는 걸 둬서, 쓰레드 내부에 bit mask로 필요한 쓰레드에게만 시그널을 줄 수 있다. 이게 일반적인 방법이다.
    • 시그널을 어떻게 전달하는가에 대한 문제는 쓰레드의 구현, 운영체제의 구현에 달려있고 선택의 문제이다.

글로벌 변수의 공유

  • 쓰레드는 글로벌 변수를 공유해서 사용한다. 쓰레드들이 특정 글로벌 변수에 접근하면 조심할 필요가 있다. 동기화에서 문제가 생길 수 있고, 라이브러리에 포함된 변수를 사용할 때 조심해야 한다.
    • <errno.h> 헤더파일에 errno이라는 글로벌 변수를 생각해 보자. Thread1이 특정 작업을 성공해서 errno를 0으로 세팅하고, 컨텍스트 스위칭이 일어나면서 Thread2가 뭔가가 실패해서 errno를 1로 세팅하게 된다. 다시 Thread1으로 돌아오면 errno이 1이므로 문제가 생길 수 있다.

  • 따라서 라이브러리는 내부에 글로벌 변수가 있는지 없는지 안보이기에 조심해야 한다.
  • errno이나 strtok() 같은 걸 조심해야 한다. strtok()는 어디서 시작해서 어디까지 잘라야 하는지 위치를 기억하는 포인터가 있고, 그 포인터는 글로벌 변수이다. errno도 내부에 글로벌 변수로 값을 가지고 있다.
  • 라이브러리 내부를 잘 보면 Multithread-safe 한지 아닌지 적혀있다. 여러 쓰레드가 동시에 safe 하지 않은 함수를 부르면 문제가 생길 수 있다고 적혀있다. 따라서 멀티쓰레드 프로그래밍을 할 때는 Multithread-safe 한 라이브러리만 가지고 프로그래밍하는 게 좋다. 혹은 명확하게 errno 가 바뀔 수 있구나 생각하고 프로그래밍하면 된다.
  • 해결책으로 글로벌 변수를 쓰레드별로 영역을 나눠서 따로 가지게끔 운영체제가 지원할 수 있다. 하지만 그렇게 하는 운영체제는 거의 없다. 쓰레드의 목적 중 하나가 글로벌 변수의 공유이기 때문이다.