7 분 소요

연세대학교 차호정 교수님의 운영체제 강의를 듣고 작성한 강의록

프로세스 모델의 한계

Untitled 멀티스레딩을 이야기 하기에 앞서, 프로세스 모델의 특성을 이해하고 이것이 비효율적인 것을 이해해야한다.

전통적인 프로세스 모델은 성능관점, 리소스 사용 관점에서 두 가지 이슈가 있다.

리소스 이슈

예를 들어 웹서버의 예제에서, 요청이 들어올때마다 서버는 fork()를 통해 똑같은, 중복되는 코드의 자식 프로세스들을 통해 처리하게 된다. 사실은 웹 서버는 단일 코드이다. 똑같은 것을 여러개 복제하여 시스템에 똑같은 프로세스를 유지하는 것이 비효율적이다. 프로세스는 만들때 마다 이미지가 있어야하고 실행되어야하기에 제한된 크기의 물리적 메모리에 같은 내용의 프로세스 이미지, 컨텍스트를 올렸다 내렸다 해야한다. 따라서, 기존의 프로세스 모델은 리소스 관점에서 매우 비효율적이다.

멀티 프로세싱 이슈

90년대에, CPU가 싸지고, 병렬 컴퓨팅의 개념이 등장한다. 이 시대에는 Micro processor가 싸고 빨라지기에 여러개의 프로세서를 병렬 연결하는 병렬처리에 대한 시도가 다양하게 이루어졌으며 연구가 이루어졌다. 이런 흐름 속에서 CPU 및 하드웨어의 병렬화를 통해 더 빠른 프로세스 처리가 기대되었다. 하지만 기존의 프로세스 모델에서는 이것이 불가능하다. 프로세스는 각각이 가지고 있는 이미지에 모든 내용이 들어가 있고, 많은 CPU가 있다 한들, 오로지 하나의 CPU에서만 의해서 처리가 가능하다. 즉 프로세스가 한개 있을때, 처리하는 CPU가 2개든 8개든 한 CPU가 하나의 프로세스만 처리할 수 있기 떄문에 병렬화에 따른 성능향상이 불가능한 것이다.(하나의 프로세스를 n배 빠르게는 못함)

프로세스 처리의 다른 방법은 없을까?

Thread Model

Untitled 프로세스는 이미지와 context정보를 포함한 거대한 자료구조이며, 새로이 생성될 때 불필요하게 fork를 통해 중복된 데이터들을 복제해야만 하며 프로세들간 통신 또한 IPC를 사용해야하기에 매우 효용이 떨어진다.

프로세스보다는 가벼운 형태의 모델인 쓰레드가 등장.

Untitled 기존의 프로세스의 구성 요소다. 이미지 + program context + kernel context)

만일 프로세스의 생성을 백 퍼센트 복제하는 것이 아니라 필수적인 요소들만 복제하고 가능한 부분은 공유한다면 어떨까? 그렇다면, 프로세스의 구성요소들을 공유할 수 있다면 어디까지 공유할 수 있을까?

Untitled 당연스럽게도 코드는 공유 가능하다. (Read Only)

global data도 역시 이론적으로 공유 가능하다.(현실적으로는 매우 조심스럽게 설계 및 고민을 해야한다)

이 외에도 힙, 라이브러리 모두 공유가 가능하다.

커널 컨텍스트는 메타정보이기에 이론적으로 공유가 가능하다.

스택은? 로컬 스택은 항상 프로그램 카운터와 밀접한 관련을 맺고 있다. 코드 상에서 어디에서 수행하는지에 따라 run-time stack의 내용이 바뀌게 된다. 특정 run-time 시점에 unique하게 존재하는 녀석이므로 공유가 불가능하다.

마찬가지로 프로그램 컨텍스트도 공유가 안될 것이다. 프로그램 컨텍스트는 특정 시점에 PC의 위치에 따른 register의 snapshot이라고 할 수 있다. 이 역시 runtime에 unique하게 존재하므로 공유가 불가능하다.

이러한 형태로 공유 가능한 데이터와 공유가 불가능한 데이터를 분류했고, 이를 통해 공유 가능한 데이터들을 모아 쓰레드라는 단위로 새로이 정의해냈다.

Untitled 위 예시는 하나의 프로세스에 두개의 쓰레드가 존재하는 상태의 예이다. Kernel context, code,data, global data, heap 영역이 공유된다.

공유 가능하지 않은 영역인 스택, program context는 각 쓰레드마다 유니크 하게 존재한다.

[정리]

가급적 공유 가능한 데이터들을 공유하고 각각의 쓰레드를 가볍게 돌리자. 즉 새로운 프로세스를 생성하기 보다는 최대한 중복되는 부분을 공유하고 최소한의 공유 불가능한 영역만 생성해 내자. 이런 철학으로 쓰레드가 만들어졌다. 기존에 프로세스를 만들어내는 방식보다는 훨씬 처리양이 줄어든다. 이것이 멀티 스레딩 모델이다.

Process VS Thread: address space

Untitled 오른쪽이 스레드 모델의 주소공간이다. 각 스레드마다 별도의 스택공간과 별도의 Program counter가 생겨났다. 각 쓰레드의 program counter가 존재한다는 말은 쓰레드마다 동일한 코드의 다른부분을 각각 실행할 수 있다는 뜻이다. 즉 하나의 프로세스 내에서 여러개의 독립적인 실행 흐름을 가지게 되었다는 뜻이다.

물리적으로 CPU는 program counter는 하나만 가지고 있다. 하지만 논리적으로는 여러개의 program counter 값을 각각 기억해 두기에 각 스레드마다 독립적인 실행흐름이 생성된다.

Process VS Thread : Logical View

Untitled 프로세스는 논리적으로 프로세스간 엄격한 hierachical 구조를 가진다. 모든 프로세스가 부모와 자식을 가지고 있다.(init제외) 이것이 전통적인 프로세스 모델 기반의 멀티 프로세싱이다.

반면에 멀티 스레딩은 쓰레드가 코드와 global datastructure , kernel context와 같은 리소스들을 공유한다.

Process VS Thread: Example

Untitled 앞선 서버-클라이언트 예제를 프로세스/스레드간 코드로 비교해보자.

왼쪽 코드의 프로세스 모델에서 서버는 지속적으로 요청을 듣고 있다가 자기자신을 fork하는 역할을 수행하고 있다. accept를 하면 할수록 리소스를 소모하는 정도가 크다.

반면에 쓰레드 모델은 한 프로세스 내에서 새로운 리소스를 사용하지 않고, 할당된 공간에서 스택과 context만 할당받게 된다. 요청을 듣고 있다가 요청이 들어오면 오직 handle_request라는 메소드를 수행하는 작업 흐름을 새로이 시작하는 것 뿐이다.

멀티스레딩은 정답인것처럼 보인다. 리소스 할당량도 훨씬 줄일 수 있으며 한개의 프로세스의 쓰레드마다 다른 CPU를 배정시켜 병렬 처리에 따른 이점을 살리는 구현도 가능하기 때문이다. 그렇다면, 멀티스레딩의 단점은 없을까??

공유라는 것은 본래 간단치 않은 작업이다. 세개의 쓰레드가 공유 데이터를 읽기만 하면 문제가 없다. 하지만 공유 데이터를 수정하거나 삭제할 시 문제가 된다. 즉 데이터 무결성이 보장이 안되는 것이다. (data integrity) 데이터를 업데이트한 값을 읽어오는 것인지, 업데이트 이전의 값을 읽어 오는 것인지, 더 나아가 무슨 값을 읽는 것이 맞는 것인지 흐름상 unclear하게 된다. → 즉 동기화 이슈가 발생한다.

공유 덕분에 많은 것을 얻었지만, 프로그래머 관점에서는 공유 데이터를 어떤 식으로 handling 할 지를 잘 설계해야하는 책임이 생겼다. 공유 데이터의 무결성을 프로그래머가 보장해야지만 멀티쓰레딩의 이점을 온전히 취할 수 있다.

Process VS Thread : Relationship

Untitled 프로세스와 스레드의 관계를 살펴보자

처음에 스레드가 등장했을때, 스레드를 프로세스 안의 컨테이너 개념으로 설명하고 정의했다. (하지만 컨테이너 개념이 아닌 다른 개념도 존재하며 구현도 있다)

프로세스와 스레드의 공통점 각각의 논리적인 실행 흐름을 가진다.

프로세스와 스레드의 다른점 프로세스는 공유하지 않지만, 스레드는 많은 리소스를 공유하며, switching, 생성, 삭제의 관점에서 프로세 스보다 많은 이점이 있다

Benefits of Threads

Untitled 스레드의 장점.

스레딩을 잘 한다면, 얻을 것이 굉장히 많다

  • concurrency(병렬성)을 극대화 할 수 있다. 하나의 프로세스를 수행하는데 동시에 여러 프로세서가 배정되어 더 병렬처리 성능을 높이기에 용이하다.
  • 프로세스 대비, 쓰레드의 생성, 제거, switch하는 overhead가 훨씬 적다.
  • 프로세스는 IPC 기법(소켓 등) 을 사용해서 커널기능의 개입으로 프로세스간 통신을 복잡하게 구현해야한다. 하지만 쓰레드의 경우 리소스를 공유하기에 쓰레드간 통신에서 커널의 개입이 전무하다.
  • 다중 CPU의 활용 효율(병렬처리성능)이 더 높다.

Untitled 운영체제 관점에서 작업흐름의 발전도.

우리 현대 시대는 오른쪽 아래의 시대이다. 여러개의 프로세스 + 여러개의 쓰레드

Implementing Threads

Untitled 멀티스레딩 모델을 운영체제 관점에서 어떻게 지원을 할 것인가

두가지 접근법이 있다.

  • 멀티스레드의 구현을 철저히 user-level에서 하는 접근법. kernel을 건드리지 않는다. 운영체제, 커널은 관여하지 않고 변함없다.오로지 user-level 에서 멀티스레딩의 이점을 활용하는 느낌으로 다룬다.
  • 커널

User Level Threading

Untitled 위 그림에서 두개의 프로세스가 있고 두개의 스레드가 내부적으로 돌고 있다.

커널 안쪽에서 프로세스 테이블이 있는데, 이는 프로세스의 존재를 커널이 인지하고 있다는 의미다. 즉 PCB라는 자료구조를 가진 테이블을 통해 커널이 프로세스의 존재를 인지하고 관리해준다.(스케줄링의 대상으로 인지)

하지만, 커널은 유저 프로세스 내의 스레드는 전혀 알지 못한다. 모든 프로세스 내에는 run-time system이라는게 존재한다. 이 runtime system안에 있는 스레드 관련 라이브러리로 구현되고, 여기에서 스레드 테이블을 통해 스레드가 유저레벨에서 관리되는 형태이다.

런타임의 존재를 커널은 알 수 없다. 커널이 보기에는 그냥 프로세스가 두개 일 뿐이다. 모든 스레드의 관리 핸들링은 유저 프로세스 안에 있는 런타임 시스템이 관장한다. 커널은 스레드를 스케줄링의 대상으로 삼을 수 없다.

커널을 전혀 건드리지 않고 API의 형태로, 라이브러리를 통해 간편하게 스레드를 사용할 수 있다는 장점이 있다.(빠르게 만들 수 있고 구현이 간단함)

Untitled

이러한 형태의 멀티스레딩을 다른 말로 Many-to-one 모델이라고 하기도 한다.

여러개의 유저 스레드를 하나의 커널 개체로 인지한다. 유저레벨이 사용하는 네개의 스레드가 있는데 이들은 사실은 하나의 스케줄링의 대상이 되는 프로세스의 안에서 존재한다.

장점은 매우 직관적이다. - 구현이 간편함

단점, 문제점들

  • 대표적으로 커널이 인지하지 못하기 때문에 유저 프로세스의 스레드들 중 하나가 IO system call을 하게 되면 나머지 스레드들, 즉 프로세스의 진행이 불가하다. 4개의 스레드들 모두 하나의 개체로 인지되기 때문에 커널에서는 똑같은 IO call로 인지하여 프로세스 자체의 state를 block으로 처리하게된다.
  • 이는 멀티프로세서(다중 cpu)의 병렬 처리의 이점을 활용할 수 없다는 의미이기도 하다. 각각의 스레드들을 각각의 cpu에 할당하는 병렬 처리 자체가 불가능하다. 그 이유는 모든 스레드가 하나의 프로세스로 인지되기 때문이다.

초창기 멀티스레딩의 구현, 연구자들이 빠르게 스레딩 모델을 시연해보려는 시도에서 만들어짐. 현실적인 병렬 처리 효과는 얻을 수 없었다.

Kernel-level Threading

Untitled{ :width=”70%” height=”70%”}{:.aligncenter} 우리가 사용하는 모든 운영체제는 kernel-level multithreading으로 멀티스레딩이 구현되었다.

이제 커널은 프로세스 뿐만 아니라 프로세스 내부의 스레드들을 관리한다. 앞선 user-level 접근법에서 가졌던 이슈들이 모두 해결된다. 커널은 프로세스 내의 스레드들을 모두 인지하기에 이제는 각 스레드들을 스케줄링의 대상으로 인지하게 되고, IO block의 문제도 해결되고 병렬처리도 가능하게 된다.

Individual 하게 스케줄 할 수 있고, 다른 CPU를 각각 할당할 수 있다.

Untitled 이를 다른 말로 one-to-one 모델이라고 한다.

유저의 스레드들을 커널이 스케줄할 수 있다.

단점은 구현이 어렵다는 것이다. 커널에 많은 기능을 추가하고, 수정해야했다. 커널이 복잡해지고 무거워진다.

Untitled 이 와중에 Many-to Many 모델도 존재했다. 대표적으로 솔라리스 운영체제가 시도했다.

더 flexible하게 멀티스레딩 기능을 구현하려 했다. 여러개의 스레드가 여러개의 커널 공간에 맵핑하여 멀티스레딩 환경을 제공한다. (어떤 건지 잘 모르겠다)

결론적으로는 없어지고 One-to- One모델만이 살아남았다

Implementing Threads: Summary

Untitled

Thread Interface

Untitled 멀티스레딩 모델이 등장하고, 각 계열의 운영체제 진영들이 각각의 운영체제에 하루빨리 넣고자 시도했다. 각 진영들마다 각각의 스레딩 구현이 등장.

→ 멀티스레딩 구현체가 운영체제들마다 달랐음. 혼돈의 시기 호환도 안되고 기술들이 너무 다름.

Pthread

표준화의 시도 - Pthread

IEEE라는 단체에서 POSIX standard thread interface를 정의함.

Pthread API

Untitled

Pthread Example

Untitled Untitled

Solaris/Windows Threads

Untitled 두 운영체제의 프로세스/스레드를 구현한 추상적인 개념의 자료구조

둘 다 컨테이너의 형태로 구현. 프로세스 내부에 여러 스레드가 존재. 철저히 컨테이너 개념.

Linux Threads

Untitled 기존의 많은 프로세스들은 컨테이너 개념으로 프로세스 안의 스레드를 구현했었다. 코드가 복잡하고 상당히 무거운 구현이다.

하지만 리눅스는 상당히 획기적인 방법을 사용했다.

리눅스 개발자들은 멀티스레딩 개념의 구현을 위해 기존에 있었던 커널 코드의 핵심을 변경하고 싶지 않았다.

근본적으로 스레드와 프로세스의 차이는 바로 공유 가능성이다.

프로세스의 생성은 공유를 전혀 하지 않은 복제 이미지를 만드는 것이고, 스레드의 생성은 많은 부분을 공유하는 이미지(주소공간)을 만드는 것이다.

스레드는 기존 프로세스에서 많은 것을 공유할수만 있다면 그것이 바로 스레드인 것이다.

fork를 할 적에, 인자를 둬서, 인자에 child와 parent간의 이미지 공유를 직접 정의하여 조절하면? 그것이 바로 스레드인 것이다. 이러한 기능을 담은 시스템 콜인 clone()을 만듬.사용자는 clone()의 파라미터를 통해서 부모와 자식간의 공유 정도를 조절할 수 있다. 이러면 커널 코드를 수정할 필요가 없어진다.

이런 개념을 리눅스에서는 light weight process라고 부른다. 프로세스와 스레드 간의 차이가 없다. clone에 의해 만들어진 자식이 파라미터 값에 따라서, 전통적인 프로세스가 될 수도, 혹은 스레드가 될 수도 있는 것이다. 그래서 미루어 추측을 하자면, linux의 pcb이름이 task_struct인 이유가 이러한 맥락에서 process, thread가 합쳐졌다는 의미로 task라는 용어를 사용한 것 같다.

Pthreads implementation on Linux

Untitled 리눅스 스레드의 발전 과정이다. 결국 오늘날 우리가 쓰는 리눅스 스레딩은 리눅스 커뮤니티에서 선택된 RedHat의 Native Posix Threading Library이다. 위에서 설명했던 one-to-one 모델이자 clone() 시스템 콜을 사용한다.

Untitled NPTL의 구현이 가장 성능이 좋았다.

Thread Summary

Untitled 스레딩 기능을 잘 활용하면 상당한 이점을 얻을 수 있다.

특히 병렬 컴퓨팅 상황에서 매우 효율적이다.

댓글남기기