운영체제의 기초 - 7. Processes and Threads 4
by jennysgapMultithreading 1
외부에서 입력을 받았을 때 그것을 처리하는 server의 architecture을 살펴보겠습니다.
Server Architecture
- Interative Server: 서버가 깨어나서 메세지큐에서 리퀘스트 받아다가 스스로 처리
- Concurrent Server: 메세지큐에서 리퀘스트를 받으면 그 일을 처리해주는 워커 프로세스(Child)를 생성하여 처리하게 함
Worker Process를 사용해 구현한 Concurrent Server의 단점
처리할 요청의 수 만큼 프로세스를 생성해야 하기 때문에 시스템에 큰 부담이 발생
Multithreading의 목적
Concurrency는 높이면서 Execution Unit을 생성하거나 수행시키는데 드는 부담을 줄임
시스템 전체를 설계하는데 사용하는 디자인 패턴을 'Architecture'라고 한다
Multithreading이 무엇인가?
먼저 Process의 개념으로 돌아가야 함
- Context: 프로세스에게 부여된 자원
- Execution Stream 또는 Thread of Control: 수행의 주체
왜 하나의 프로세스 안에 왜 한개의 Thread of Control만 있어야 하는가?
Thread of Control을 여러개 두어도 되지 않을까?
Thread of Control 부분을 특별이 떼어내서 사람들이 Thread 또는 lightweight process 라고 부르기 시작함
이와 같은 생각의 결과로 하나의 프로세스에 여러 개의 Thread들이 존재하게 됨
프로세스에게 부여된 자원들은 어때요? 걔네들은 1벌만 유지하게 됨
그 프로세스에게 부여된 자원을 이 여러개의 Thread들이 공유하는 형태로 진화하게 됨
그것이 Multithreading임
Thread는 어떻게 구현이 될까? 스택으로 구현이 됨
Multithreading을 하게 되면 그 전 프로세스와 달라진 점은 하나의 프로세스가 여러개의 스택을 갖게 됨
이것으로 충분할까? NO
Thread의 구현을 위해 필요한 자료 구조
- Stack
- Thread ID (각 Thread를 명명할 수 있어야 함)
- Thread Control Block (TCB)
프로세스의 2가지 측면
- Design-time Process: 복잡한 시스템을 Decomposition하여 설계를 하게 되면 독립적으로 수행가능한 entity
- Run-time Process: OS가 자원을 할당하고 수행시키는 주체들
용어정리
- Process:
- Thread:
- Task: Design-time Process를 흔히 Task라고 부름 (설계의 결과물)
MachOS는 Process = Task + Thread 로 정의함
Task: 프로세스에게 부여된 resource들
리눅스의 유저레벨에서는 Process와 Thread로 부르고
커널레벨에서는 Process와 Thread 구분을 안함 (커널이 바라본 수행 entity는 전부 Task라고 부름)
Threads : Multithreading 2
Multithreading이 필요한 이유
1. 적은 비용으로 Concurrency(동시성)를 얻기 위해
- response time을 줄이겠다 (응답의 Agility민첩을 높이겠다는 의미)
2. Massively Parallel Scientific Programming을 할 때 발생하는 오버헤드를 줄이기 위해
오버헤드가 적은 이유
- 프로세스는 놔두고 Thread만 여러개 생성
- 스택만 생성하기 때문에 메모리 요구량이 적다 (프로세스의 경우 여러 가지를 생성해야 함)
- 자기 자신 내부에 Thread만 추가 생성하면 된다 (fork할 필요 없음)
Thread들이 프로세스 할당된 리소스들을 공유하는 장점 (그러나 동기화 문제 발생)
Multithreading의 장점
1. 구현이 쉽고 경제적이다.
- 새로운 Thread 생성 시간도 적게 걸리고
- 메모리도 적게 차지하고
- Thread Context Switching이 훨씬 더 경제적인 Operation임
2. 반응시간을 빠르게 할 수 있다
- 서버를 구현하기 위해 하나의 프로세스만 존재하면 됨
- 그 프로세스는 Input request queue에서 request를 따올 수 있는 Dispatcher Thread를 가짐
- 나머지는 그때그때 필요에 따라 worker Thread를 생성하고 사용
- 이것이 만약 웹페이지다 라고 하면 자주 불러오는 페이지가 있으면 메인메모리(캐시)에 둠 -> 메인메모리(캐시)는 worker Thread들이 공유하는 자료들이 됨
Concurrent Server의 Pseudocode
- Dispatcher는 무한루프형태로 돔 (계속도는 것은 아니고 어느정도 슬립을 하다가 루프를 돈다)
- request를 queue에서 따온다음에 그 request를 처리할 수 있는 worker Thread를 dispatch함
- 그러면 worker thread는 그것을 받아서 캐시에서 데이터를 읽어서 가공하고 클라이언트에게 제공해주는 모습임
Web browser를 Process가 1개 밖에 없는 프로그램으로 구현했다고 하면 어떻게 될까?
어떤 영화를 스트리밍 받기 위해서 다운로드하는 버튼을 눌렀다
다운로드 operation이 스트리밍의 프레임들을 읽어들이는 버퍼링 operation이 되는 동안에
웹브라우저의 다른 어떤 버튼도 입력을 받아들일 수 없음
왜냐하면 Thread of control이 하나밖에 없으니까
그렇기 때문에 multithread로 만들게 됨
지금까지 multithreading이 왜 필요한지. 개념이 무엇인지 배웠음
Threads : Multithreading 3
Thread의 구현 형태 (3가지)
1. User-Level 구현: 커널이 전혀 모르게 살짝 구현하는 방법
2. Kernel-Level 구현: 커널이 100% 알고 구현하는 방법
3. Combined User-Level / Kernel-Level 구현: 두가지 섞어서 구현하는 방법
User-Level Thread 구현
- 여러개의 Thread 생성을 위해 여러개 stack 구현
- TCB(유저레벨에 둔다)를 각 Thread 마다 만든다
- Thread간에 스케쥴링한다 -> user-level에 스케쥴러가 있어야 함
- user-level에 스케쥴러 함수를 구현하고 함수들을 묶어 라이브러리로 만든다 (User-Level Threads library)
- Threads 라이브러리 안에는 Thread 생성/소멸 코드, Thread Context를 저장하는 코드, Treads 간의 커뮤니케이션 함수 존재
User-Level Thread 구현한다는 말은 thread management에 필요한 함수들을 library 구현한다는 의미임
User-Level Threads library를 구현했다고 했을 때 가장 어려워 할 것 같은 동작이 무엇일까?
한 thread에서 다른 thread로 스케쥴링 하는 것
그런데 스케쥴링은 2가지 방법이 있음
1. preemptive
2. Non-preemptive
thread가 Non-preemptive하게 CPU를 넘기는 것 이것은 쉽게 구현이 가능함
왜냐 Non-preemptive 스케쥴링이라고 하는 것은 걔가 library에서 제공해준 CPU yield()를 호출하면 됨
Preemptive Scheduling의 한계가 존재
- 반드시 interrupt가 필요 그렇다면 interrupt를 어떻게 OS이 처리하는지를 생각해 봐야함
- interrupt가 걸리면 handler 뜸 -> service routine 뜸 -> 그 interrupt를 처리할 process를 깨워줌
- interrupt가 오면 처리해야 할 프로세스로 컨트롤을 넘겨줌
- 지금 Thread는 프로세스 내부에 존재 -> interrupt가 프로세스 내부 Thread까지 넘어가야 함
- 그런데 프로세스 내부는 interrupt를 알지 못함 (전달방법이 없음)
read system call 문제
- 어떤 Thread가 수행하다 read호출하면 system call interrupt 발생. 커널로 들어감
- sys read() 수행 -> disk I/O 수행이 끝날 때까지 기다리게 만들어야 할 때 그 프로세스 전체를 블록킹 시킴
- 실제 프로세스 내부에는 read 이외 다른 thread들이 존재할 수 있음 (다른 thread들이 수행할 수 있음에도 블록되는 문제가 발생)
User-Level Thread의 문제
1. 한 Thread가 Blocking System Call을 호출하는 경우 해당 Process의 모든 Thread가 Block 됨
2. 운영체제가 Thread에게 직접 interrupt를 전달할 수 없기 때문에 Preemptive Scheduling을 할 수 없음
multithreading이 처음 나왔을 때 OS를 고치지 않아도 multithreading할 수 있었음
어떻게 보면 반쪽짜리 multithreading임. 그렇다면 이것이 유용할 곳이 있었을까?
있음. 리엑션 없는 시스템 (ex. Scientific Programming: 외부로부터 input 필요없이 계산만 쭉 하기 때문)
커널레벨 multithreading은 매우 간단함
- Task가 생성되고 소멸되는 것을 커널이 다 알아서 해줌
- Thread Creation, Thread termination이 모두 커널함수 system call로 구현이 됨
- TCB 역시 커널이 manipulation(조작) 함
- 그리고 커널이 직접 thread들을 다 스케쥴링하는 그런 entity들이 됨
- interrupt forwarding도 가능해짐
- 앞에서 나온 blocking문제도 다 해결됨
이 기법은 User-Level Thread Implementation의 모든 단점을 해결하지만
Kernel-Level Thread의 문제
Kernel 단의 수행 시간이 증가하면서 추가적인 오버해드가 발생한다.
system call을 하게 되면 system call handler가 수행이 되어야 하고
커널 함수가 dispatcher되야 하고 커널 수행이 되어야 하니까 오버헤드가 많이 들게됨
그렇지만 반응성을 향상시킬 수 있는 장점이 있음
UL도 단점있고 KL도 단점이 있음
그래서 나타난 것이 Combined UL/KL Thread implementation임
- 상당량을 유저레벨에서 처리.
- 단지 인터럽트가 왔을 때 인터럽트를 유저레벨 쓰레드 스케쥴러에게 forward해서 어떤 쓰레드가 깨어나야 하는지 알려주는 기법이 추가 됨
- 어떤 프로세스가 블로킹시스템 콜을 했다 -> 그 쓰레드는 중단시키고 새로운 커널스택을 할당해서 유저프로세스에 붙힘 -> 다시 리턴해주면 유저레벨에 있는 쓰레드 스케쥴러가 다른 쓰레드를 골라서 수행해줌 -> 걔가 다시 커널로 돌아왔을 때 걔한테 보존되어 있는 커널레벨 스택이 있으니 시스템콜 처리도 가능해짐
- 이와같이 thread가 Preemptive Scheduling을 가능하게 하는 기법을 추가한 것이 Combined UL/KL Thread implementation임
Threads : Multithreading 4
User Level Thread 와 Kernel Level Thread를 하나로 연결시켜 관리
Multithreading이 제공해 주는 API를 이해해야 한다. 가장 많이 사용하는 것이 POSIX(파직스)
POSIX (Portable Operating System Interface): 다양한 UNIX 계열 운영체제들의 API를 표준화하기 위해 IEEE가 정의한 인터페이스
pthread_create(): thread를 생성하려면 argument(인수)로 함수의 function 포인터로 전달됨
- thread가 생성이 되면 그 thread의 entry point는 특정 함수가 된다
- entry point의 function 포인터가 pthread_create()에 제공이 된다
pthread_join(): wait system call이랑 비슷
- argument(인수)로 온 thread가 terminate(종료)할 때까지 기다리는 function(기능)
pthread_yield(): CPU를 yield(양도)하는 함수
- Non-preemptive Scheduling, CPU를 포기하는 스케쥴링
위 4가지 함수들을 이용해서 thread가 어떻게 생성이 되서 소멸이 되는지를 알아보겠음
프로세스에는 default로 한개의 Thread를 갖고 있음 그것이 main thread가 됨
pthread_create() 하지 않으면 한개의 thread는 존재하게 됨
main thread가 다른 thread를 스폰할 수 있음 (pthread_create() 이용)
이 경우에 thread가 2개 생겨남
먼저 thread_1은 자기가 수행을 하다가 어떤 이유에선가 CPU를 포기하고 싶음 (pthread_yield() 이용)
yield를 하게 되면 waiting상태로 가거나 ready상태로 가거나 함
언젠가 다시 ready상태가 되서 수행을 하고 자신의 수행을 다 마치게되면 pthread_exit()을 통해서 종료를 하게됨
다른 thread가 수행을 하고 있을 때 main thread는 이 두 thread를 모니터링(종료하는 것을 기다림)하는 일을 함 (pthread_join() 이용)
Q. pthread_yield()를 호출한 쓰레드는 어떤 식으로 수행이 재개되는가?
A. pthread_yield()에 의한 쓰레드의 상태 변화는 운영체제에 의해 관리된다.
User Code 상에서는 pthread_yield()의 호출 지점에서 정지되는 것으로 보이며,
쓰레드가 다시 CPU를 할당받으면 다음 Statement부터 수행을 재개한다.
TaskCode: pthread_create()을 할 때 pthread에 수행코드를 제공해 줄 함수
argument를 받아서 print하고 바로 terminate하는 모습이 됨
main thread를 구현하는 함수
- 제일 먼저하는 일은 process를 스폰하는 일 (pthread_create()를 호출함으로써 thread를 생성)
- 파라미터로 task code라고 하는 앞에서 보여준 function(함수)의 포인터를 제공함
- pthread_create()가 되면 task code와 매핑을 시켜 task code가 수행이 되는 모습이 됨
각 Thread가 생성이 될 때마다 다른 argument를 주고 싶을 때가 있음
그럴 때 thread argument를 여기에 제공해 줄 수 있음
thread argument는 1,2,3,4,5 그냥 sequence number가 됨
그래서 thread가 5번이 생성되고 main thread는 계속 진행하게 됨
main thread가 진행을 할 때 생성된 5개의 thread보다 먼저 수행될까? 알 수 없음
프로그램은 thread와 main thread에 수행의 순서를 control할 수 있음
main thread는 5번 돌면서 pthread_join을 5번 호출함
자기가 생성한 thread 0-4번이 terminate할 때까지 한번씩 기다렸다가 종료하는 모습이 됨
여기서 중요한 점은
User-Level 구현과 Kernel-Level 구현이 어떻게 다른가?
User-Level implementation은 pthread_create() 함수가 user-level library의 함수인 것
이 프로그램이 컴파일 되고 링크될 때 library가 링크 인되어서 하나의 이미지로 수행이 되는 것을 의미
Kernel-Leve implementation은 pthread library가 전부 system call 이라는 것을 의미
출처 - http://snui.snu.ac.kr/ocw/index.php?mode=view&id=641
출처 - http://snui.snu.ac.kr/ocw/index.php?mode=view&id=644
출처 - http://snui.snu.ac.kr/ocw/index.php?mode=view&id=645
출처 - http://snui.snu.ac.kr/ocw/index.php?mode=view&id=646
'BOX' 카테고리의 다른 글
운영체제의 기초 - 8. CPU Scheduling 1 (0) | 2017.03.20 |
---|---|
중간 내용 정리 (0) | 2017.03.20 |
운영체제의 기초 - 6. Processes and Threads 3 (0) | 2017.03.17 |
운영체제의 기초 - 5. Processes and Threads 2 (0) | 2017.03.16 |
운영체제의 기초 - 4. Processes and Threads 1 (0) | 2017.03.16 |
블로그의 정보
jennysgap
jennysgap