[OS] 프로세스와 쓰레드의 비교, pthreads, Signal handling
2024. 4. 11. 21:12ㆍCS/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 : 즉시 죽이는 게 아니라, 죽어야 한다고 알려주는 것이다. 타겟이 중요한 작업을 하고 있으면 프로그래머의 의도대로 동작한 후 종료한다. 가능하면 이 방법을 쓰는 게 더 좋다.
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 가 바뀔 수 있구나 생각하고 프로그래밍하면 된다.
- 해결책으로 글로벌 변수를 쓰레드별로 영역을 나눠서 따로 가지게끔 운영체제가 지원할 수 있다. 하지만 그렇게 하는 운영체제는 거의 없다. 쓰레드의 목적 중 하나가 글로벌 변수의 공유이기 때문이다.
'CS > Operating System' 카테고리의 다른 글
[OS] Synchronization & Critical sections & Synchronization Problem (0) | 2024.04.13 |
---|---|
[OS] User-level 쓰레드 VS 커널 쓰레드, Go Language (1) | 2024.04.12 |
[OS] 프로세스의 단점과 쓰레드의 도입, 멀티 쓰레딩의 장점 (0) | 2024.04.09 |
[OS] Zombie & Orphan process와 PCB, Context Switch, fork (1) | 2024.04.03 |
[OS] 프로세스의 개념과 생성, fork 시스템 콜 (0) | 2024.04.02 |