1. Thread란 무엇인가?
프로세스와 스레드 사이의 차이점이 분명히 존재하고, 그것이 무엇인지 알아둘 필요가 있다.
https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html
1) 프로세스란?
: 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램으로, 메모리에 올라와 실행되고 있는 프로그램의 인스턴스이다. => 운영체제로부터 시스템 자원을 할당받는 작업의 단위로, CPU 시간, address space, code, heap, stack. heap등의 구조로 되어 있는 독립적인 메모리 영역을 할당받게 된다.
2) Thread란?
: 프로세스 내에서 실행되는 여러 흐름의 단위이다. 이때 stack과 register만 따로 할당 받으며 code, data, file등의 시템 자원들은 같은 프로세스 내에 스레드끼리 공유하게 된다. (프로세스는 메모리 자원 공유 불가능-> IPC 사용)
=> 각각의 스레드들은 별도의 레지스터와 스택을 갖고 있지만, heap memory는 서로 읽고 쓸 수 있다.
-> 스레드를 사용하는 이유로 web server를 들 수 있다.
: 서버에서는 많은 요청들이 지속적으로 도착하고, 도착할 때마다 fork 호출 후에 해당 task를 실행하게 된다.
-> 그렇지만 이때마다 프로세스를 생성하기 위해서 fork를 실행하면 메모리 양도 많이 필요하고, computation 양도 너무 커지게 된다.
+) Recap : 프로세스의 생성 과정
1) fork 함수를 실행하여 새로운 프로세스를 생성한다.
2) 새로운 pid와 PCB를 생성하고, address space를 duplicate 하고 새로운 프로그램을 로딩한다.
3) child을 running state로 돌아가게 한다. 이 때 parent process는 wait 함수를 실행하여 동기화한다.
4) 이때, 프로세스 끼리 소통하기 위해서는 IPC mechanism이 필요하다.
: Shared memory & Message passing
=> fork 함수 자체가 굉장히 resource를 많이 소요하고, 시간이 오래걸린다. 따라서 task가 생길때마다 프로세스 생성하는 비효율적이므로, 프로세스 내에서 스레드를 생성해서 overhead양을 줄일 수가 있다.
<Single vs. Multi-Threading processes>
: 스레드는 프로세스 내에서 실행되는 object인데, 이때 스레드가 하나만 생성될 수도, 여러개가 생성될 수도 있다.
-> multi-threading 일 경우에는 여러개의 실행 객체가 concurrently 하게 실행 가능하다.
: 따라서 멀티 프로세싱 보다 멀티 스레딩을 사용하는 이유는, 프로그램을 여러개 키는 것 보다 하나의 프로그램 내에서 여러가지 작업을 실행하는 것이 효율적이다. 따라서 10개의 task가 도착했다고 프로세스를 10개 만들 필요가 없고, 하나의 프로세스만 있어도 스레드를 여러개 생성해서 클라이언트의 요청을 실행할 수 있다.
-> multithreaded process 에서는 register와 stack과 같이 가변적으로 계속 변하는 실행 개체는 공유가 불가능하고,
code, data, files과 같은 자원은 shared resources로 공유하게 된다.
- 멀티 스레딩의 장점
1) Responsiveness : 응답성
: 여러개의 스레드가 존재해서 특정 스레드가 block 되더라도 남은 스레드에서 task 수행이 가능하다.
싱글스레딩해서는 하나만 disk- I/O 를 신청해도 전체가 block된다는 단점이 있다.
2) Resource sharing
: binary code, data, file 등의 자원을 나눠서 사용한다.
3) Economic
: Process creation 과 Process context switch 보다 훨씬 경제적이다. 시간도 적게 걸린다.
4) Utilization of Multi-processor architechture
: 멀티코어 프로세서를 적용할 수 있다. 하나의 스레드당 하나의 CPU에 할당이 가능하므로, 노는 CPU가 없다.
- Fundamental concept
: 프로세스는 여러개가 혼합된 entity이다
: A set of threads : control point를 결정하는 dynamic object이다.
: A collection of resources: Code, data, files, address space 등의 자원 부분
<Multicore programming>
: 여러개의 스레드가 동시 수행 가능할때, 프로그래밍을 잘해야 여러개 코어를 효과적으로 이용 가능하다.
이때 멀티코어 프로그래밍을 하면서 parellelism의 타입이 두가지가 존재한다.
1. Data parellelism
: 동일한 프로그램을 수행한다. 그렇지만 들어가는 data가 다르다. 이것을 data parellelism이라고 한다.
ex) cake를 먹는 것은 동일하지만, 다른 사람에게 먹힘.
2. Task parellelism
: 각각의 코어들이 본인만의 unique한 operation을 실행하는 것이다.
ex) 케이크를 만들면서 초콜렛을 뎁히고, 밀가루와 계란을 섞는 일을 parellel하게 시행할 수 있다.
<Concurrent Execution vs Parellelism>
1. Concurrent
: 여러개의 프로세스가 동시에 실행되는 것 처럼 보인다. 사실 single core가 여러가지 task를 번갈아가며 수행한다.
<한 시점에서 하나의 task 만이 수행된다.> : Time sharing을 통해 프로세스가 cpu를 나눠서 수행하면서 사용자가 여러개의 프로세스가 동시에 실행되는 것처럼 느끼게 된다.
: CASE: Mutex, Deadlock
2. Parellelism
: multi-core system으로. 실제 여러개의 CPU에서 동시에 같은일 이 수행된다.
-> Multi Core programming이 어려운 이유는?
1.Identifying task
2. Balance: 특정 코어만 일을 몰아 줘서는 안된다.
3. Data splitting : 데이터를 어떻게 분배할 것인가?
4. Data dependency : 서로 다른 코어에서도 해당 data 사용할 수 있는지?
5. Test and debugging
=> 새로운 접근 방식이 필요하다.
<Multi-Threading Model>
1. Multi threading이란?
: 하나의 응용 프로그램을 스레드로 구성하고 스레드로 하여금 하나의 작업을 맡아서 수행하도록 한다.
- 이때, 두가지의 Thread가 존재하게 된다.
1) Kernel Thread
: Kernel 내부에서 생성되고 파괴되는 스레드를 의미하고, OS에서 관리하게 된다.
2) User Thread
: user space의 Thread Library에서 생성되는 스레드이다. 커널과는 전혀 관련이 없다.
=> 커널 스레드와 user thread가 매핑이 되어야지만 kernel이 수행중일 때 user thread가 실행 가능하다.
특징이 세가지 존재한다.
1. Scheduler는 User thread의 존재를 인지하지 못한다. 오직 Kernel Thread만 스케줄링한다.
2. Kernel thread ka가 블락되었다면, 해당 커널 스레드에 매핑된 user thread역시 모두 blocking 된다.
3. 각각 커널 스레드가 다른 CPU에 할당 가능하다.
=> 2. User thread 가 I/O를 수행 요청하면 kernel thread도 block된다. 반대도 마찬가지.
=> 하나의 커널 스레드는 하나의 CPU에 할당될 수 있다.
<MultiThreading model>
멀티스레딩 모델에는 세가지가 있다.
1. Many-to-one model
2. one-to-one model
3. many-to-many model
=> 멀티스레딩 할 때 세가지 원칙이 존재한다.
1) 스케줄러는 user thread의 존재를 모른다 ( 오직 kernel thread만 스케줄링함)
2) 커널스레드가 block 되면, 연관된 user thread역시 block된다.
: kernel thread와 매핑된 user thread에서 disk i/o와 같은 커널 스레드르를 waiting 상태로 전환시킬 수 있는 system call 호출 시에 그 커널 스레드와 매핑된 모든 user thread를 waiting 상태로 간주한다.
3) 각 cpu들은 kernel thread에게 각각 매핑된다. (user thread는 cpu 할당 x)
1. Many-to-one model
: 하나의 커널 스레드에 여러개 user thread가 매핑 된 경우이다. kernel thread가 생성될때는 상당히 overhead가 크지만,
user thread만 여러개 생성하면 overhead도 적고, 커널에서 개입할 필요가 없으므로 수행 시간이 매우 빠르다.
- 만약 user thread중 하나라도 disk i/o 등의 system call을 호출한다면, kernel thread가 disk io 했다고 간주하여 kernel thread 와 연관있는 모든 다른 user thread 또한 waiting 상태로 빠지게 된다.
- 하나의 cpu 만 kernel thread에 할당하고, 나머지 cpu는 일하지 않는 idle한 state에 빠져있다.
2. One-to-One model
- 하나의 user thread는 각각 하나의 kernel thread에 매핑된다.
- 각각 cpu들은 네개의 kernel thread에 매핑된다- 진정한 parellelism 을 실행하게 된다.
- 하나 user thread가 멈춰도 다른 스레드가 돌아간다. ( 여러가지 스레드가 돌아가는 것을 허용한다)
- user thread 생성할 때 마다 kernel thread를 생성해야 하므로 overhead가 상당히 크다.
3. Many- to- Many model
=> 다수의 user thread를 더 적거나 동일한 개수의 kernel thread에 multiplexing한다. (다중 송신: 매핑 결정하는 법)
: 어떤 kernel thread에 매핑할 것인지가 관건이다.
=> One-to-one model ( kernel/user thread 생성 개수 제한) 과 many-to-one 모델( multi processing 효과 X) 을 병합한 hybrid model이라고 할 수 있다.
# Many-to-Many model의 장점
1. 개발자들은 최대한 많은 user thread를 개발할 수 있다.
2. kernel thread들은 각각 parellel 하게 multiprocessor 에서 돌아갈 수 있다.
3. thread가 blocking system call 을 호출했을때 (disk i/o), 커널은 다른 thread를 스케줄링 할 수 있다. (하나 막힌다고 다른것까지 줄줄이 막힐 필요가 없다는 말!)
# two-level model
: many-to-many model+ one-to-one model: 두 종류의 모델을 동시에 사용한다.
-> user thread를 bound thread라고 한다.
# Linux와 windows는 one-to-one model 사용한다.
=> Linux에서의 kernel thread는 앞서 설명한 multithreading model의 kernel 과는 차이가 존재한다.
: 리눅스에서의 kernel thread는 커널에서 해야할 일을 하는 thread이다. 리눅스에서는 kernel thread가 user thread와 따로 매핑되는 개념이 아니라 커널에서 해야할 일을 background에서 실행하는 개체이다.
-> 각각 thread생성 시에 별도의 PCB가 생기기 때문에 one-to-one model 이라고 볼 수 있다.
=> process 하나만 생성될 때는 fork, 여러개를 생성할때는 clone (리눅스), pthread_create( all)을 사용하여 스레드를 생성하고, 각 스레드 생성 시에 task_struct 타입의 PCB를 thread 마다 할당한다. 따라서 해당 thread가 별도로 스케줄링 되는 개체라는 것을 의미하고, 이를 kernel thread에 비유할 수 있다.
=> 따라서 다중 스레드가 존재할때, 하나의 스레드가 block되어도 옆 스레드는 연관이 없이 잘 돌아가게 된다.
Thread Libraries
+) 참고 문서
- 프로그래밍을 위해서는 thread library 함수를 호출하게 된다. thread 생성, 삭제, 관리,.. 각종 함수를 제공하게 된다.
: 구현 가능한 형태로는 두가지가 있다.
l 커널의 지원없이 완전히 사용자 공간에만 라이브러리를 제공하는 것. 라이브러리를 위한 모든 코드와 자료구조는 사용자공간에 존재한다. 라이브러리의 함수를 호출하는 것은 시스템 호출이 아닌 사용자 공간의 지역 함수를 호출하게된다.
(시간 덜 걸리고 덜 번거로움. system call 까지 굳이 안가도 되기 때문이다.)
l 운영체제에 의해 지원되는 커널 수준의 라이브러리를 구현. 라이브러리를 위한 코드와 자료구조는 커널 공간에 존재한다. 라이브러리 API를 호출하는 것은 커널 시스템 호출을 사용한다.
=> 리눅스에서 PCB를 생성해야 하므로, system call 형태로 변환시켜서 커널 스레드 생성이 가능하다.
-그리고 세개의 중요한 라이브러리가 존재한다.
1) Posix Pthreads
: user-level 또는 kernel-level library로 제공된다.
2) Win32 thread
: kernel-level library로 제공된다.
3) Java threads
: 해당 스레드 라이브러리가 어떤 host system에서 구현되는지가 영향을 끼친다.
=> java thread는 windows에서 win32 api를 사용한다.
#Threading issues
: 멀티 스레딩 형태일때 발생되는 이슈에는 어떤 것이 있는가?
=> Fork() 와 Exec() 함수에서 발생될 수 있는 이슈
: 하나의 프로세스 안에 여러개의 스레드가 존재하고, 그중 하나의 스레드가 fork함수를 호출한다.
- Fork() 함수에서는 1. 모든 스레드를 복제하는가? or 2. fork 함수를 호출하는 스레드만 복제하는가?
둘 중에 하나를 선택해야 한다.
이때,
1) exec() 함수가 바로 호출된다면, 오직 호출한 thread만 duplicate된다.
2) exec() 함수가 호출이 안된다면, 모든 스레드들이 duplicate된다.
-> 세개의 스레드가 존재하고, 한 스레드가 fork system call을 호출했을때,
1) exec이 호출되지 않았다면
: parent process를 그대로 수행하는 목적이 존재하므로, 모든 thread를 copy한다.
2) exec을 호출했다면
=> exec family의 함수들은 형재 실행되는 프로세스를 다른 프로세스 이미지로 교체한다.
: overwrite되기 때문에 세개 다 copy할 필요가 없으며, 호출한 thread만 duplicate된다.
#Thread cancellation
:thread가 끝나기 전에 종료를 하려고 한다. 웹 브라우저 안에 여러개의 스레드들이 수행되고 있을때,한 스레드를 X 버튼을 눌러 종료했을때, cancel되고자 하는 스레드인 target thread는 바로 종료되는가, 아니면 지연 후에 종료되는가?
1) Asynchronous cancellation: 바로 target thread를 모두 종료한다.
2) Deferred cancellation: target thread가 terminate 가능한지 확인 후에 안전할 때 까지 지연 시킨후에 하나하나씩 terminate한다.
#Signal Handling
: signal은 unix system에서 특정한 event가 발생했을때 프로세스에게 인지시켜주는 역할을 하고, OS에서 사용하는 매우 인기있는 메카니즘이다.
1) Synchronus signal
: 프로그래밍 상에서 에러 발생시에 해당 exception 핸들러가 발생한다고 볼 수 있다.
ex) Illegal memory access, divide by zero
2) Asynchronus signal
: 실행되고 있는 프로세스 외에서의 event
=> 해당 signal은 exception과 비슷하며, 이를 해결할 수 있어야 한다.따라서 Signal 핸들러가 존재한다.
Signal handler에는 두가지 종류가 존재한다.
1) A default signal handler : 많은 경우에 호출된다.
2) User-defined signal handler : 필요에 의해 사용자가 signal handler를 함수로 작성해야 한다.
=> signal은 어디로 전송되어야 하는가?
1. signal이 발생될 스레드.
2. 프로세스의 스레드 전체에 signal 전송
3. 프로세스의 특정 스레드에 signal 전송
4. signal을 받을 특정 스레드를 할당하는것이 가능하다.
ex) Division by zero 발생시 -> 적용되는 스레드에게만 (맨 왼쪽 스레드에게만 signal 전송)
<control> <c> 버튼 눌렀을시-> 전체 process kill : pthread_kill (모두 signal 전송)
#Thread Pools
: multiprogramming의 스레드 개수가 많아지면 시스템 수행 시간이 느려진다. 따라서 스레드 생성을 제한하여 시스템을 안정적으로 동작할 수 있도록 지원하는것이 스레드 풀의 main idea이다.
=> 따라서 시스템에서 오직 한정된 양의 스레드를 미리 생성할 수 있게 하고, 이 미리 생성된 스레드를 할당하여 정책을 사용하여, system을 안정적으로 동작할 수 있게 하며, overhead또한 적어져서 빠르게 일 처리가 가능하다.
=> request가 발생할때마다 미리 생성된 세개의 쓰레드를 갖다가 사용하면 된다. request가 한정된 숫자보다 더 많이 들어와도 thread를 새로 생성하는 것이 불가능하며, request가 실행되지 않거나 다른 request가 끝날 때 까지 기다려야 한다.
+) Thread- specific data
: 전역변수를 특정한 스레드에게 쓰고자 한다. -> 모든 스레드간에 공유가 가능하고, library에서 이와 같은 기능을 지원해줘야 쓸 수 있다,
#Thread-programming API
1. pthread_create
-> 새로운 thread creation할 때 사용한다. 그리고 이 스레드에서 thread routine f를 실행시키고, arg를 input argument로 해서 thread routine함수를 실행시킨다.
=> pthread_create함수가 return되면, tid 에서는 새롭게 생성된 thead의 id를 담게 된다.
#include<pthread.h>
int pthread_create(pthread_t* tid, pthread_attr_t *attr, (void*) f, void *arg)
https://bitsoul.tistory.com/157
2. pthread_join
int pthread_join (pthread_t tid, void *thread_return)
=> 특정 tid를 갖는 스레드가 종료되기를 기다린다.=> fork()의 wait()함수와 비슷하다.
=> 하지만 이는 특정 thread가 terminate되기만 기다릴수 있다는 점이 약간 다르다.
3. pthread_exit
int pthread_exit(void *thread_return)
-> thread를 terminate하고, thread_value라는 값을 리턴하여 pthread_join으로 연결되게 된다.
따라서 peer thread가 종료되면 main thread로 이동되어서 나머지 코드가 실행된다.
pthread가 create되면 peer thread로 이동하여 실행되고, 잠시 main thread는 pthread_join으로 기다리고 있는다.
peer thread가 종료되고 나서는 pthread_exit() 가 실행되고, 이 리턴값이 pthread_join()으로 리턴되어서 나머지 main thread 코드가 실행되게 된다.
<Thread 실행 과정>
#define HAVE_STRUCT_TIMESPEC 1
#include <stdio.h>
#include<pthread.h>
#include <io.h>
#include<stdlib.h>
int sum; //스레드 사이에 함께 사용되는 전역변수
void runner(void *param); // thread routine
int main(int argc, char *argv[]) {
pthread_t tid; //스레드 변수
//해당 스레드를 create하여 돌린다.
pthread_create(&tid, NULL, runner, argv[0]);
//tid 스레드가 terminate될때까지 기다린다.
pthread_join(tid, NULL);
//terminate되면 나머지 main thread코드를 실행한다.
printf("sum= %d \n", sum);
}
// Thread function
void runner(void *param) {
int upper = atoi(param);
sum = 0;
if (upper > 0) {
for (int i=1 ; i <= upper; i++) {
sum += i;
}
}
//thread가 terminate되고, join함수에 값을 리턴한다.
pthread_exit(0);
}
'Computer Science > Operating system' 카테고리의 다른 글
Ch 5-3) Process scheduling (0) | 2020.05.31 |
---|---|
Ch 5-(1),(2) Process Scheduling (0) | 2020.05.16 |
ch.3-2,3 프로세스 생성, 종료 & IPC (0) | 2020.05.04 |
Operating system- chapter 1 (0) | 2020.04.10 |
3 little things- Limited Direct Execution 정리+ 번역 (0) | 2020.04.07 |