CS 공부/운영체제

OS 정리 3- 프로세스 & 쓰레드 (Process and Thread)

1. Process 정의

프로세스 : 수행중인 프로그램 

  • 텍스트, program counter와 register의 값, stack, data section, heap section을 포함한다.

* 프로그램은 어떤 작업을 하기 위한 명령어 목록과 데이터를 묶어 놓은 파일이며, 보조 기억장치에 저장되어 있다.

 

간단하게 비유하면 프로그램은 레시피, 프로세스는 요리라고 보면 편하다. 

 

프로세스의 상태 

  • 생성 상태(new) : 프로그램을 메모리에 가져와 실행 준비가 완료된 상태.
  • 준비 상태(ready) : 실행을 기다리는 모든 프로세스가 자기 차례를 기다리는 상태
  • 실행 상태(running) : 선택된 프로세스가 CPU를 사용하는 상태(실행되는 상태)
  • 대기 상태(waiting) : 실행 상태에 있는 프로세스가 입출력을 요청하면 입출력이 완료될 때 까지 기다리는 상태
  • 완료 상태(terminated) : 프로세스가 종료된 상태.

 

프로세스 제어 블록(PCB; Process Control Block)

특정한 프로세스를 관리할 필요가 있는 정보를 포함하는 운영체제 커널의 자료 구조이다. 운영체제가 프로세스를 다룰 때 프로세스 제어 블록을 이용해서 다루게 된다. 여기에 담기는 정보는 운영체제별로 다르다. 일반적으로 아래와 같은 것들이 저장된다.

  • Process State : 프로세스의 상태.
  • Program Counter : 다음 명령어의 주소가 저장된다.
  • CPU Registers : 누산기, 인덱스 레지스터, 스택 포인터, 범용 레지스터 등 컴퓨터 구조에 따라 다르다.
  • CPU-Scheduling Information : 프로세스 우선순위, 스케쥴링 큐의 포인터 등을 일컫는다.
  • Memory-Management Information : 페이지 테이블, 세그먼트 테이블 등 메모리 시스템에 따라 다르다.
  • Accounting Information : CPU 시간의 총량, 실제 사용된 시간, 시간 제한 등에 대한 정보
  • I/O Status Information : 프로세스에 할당된 입출력 상태 목록

 

Process Scheduling Queue

  • Job queue : 시스템에서 실행중
  • Ready queue : 메인메모리에서 실행 준비 혹은 대기 (실행 이전시점)
  • Device queue : I/O 기기에서 대기
  1. 프로세스가 시스템에 들어오면 ready queue에 연결리스트 형태로 올라감
  2. 프로세스는 실행되기 위해 할당될 때까지 기다림
  3. 프로세스가 CPU core에 할당되면 아래 중에 하나가 발생함
    • 프로세스가 I/O를 요청하여 I/O 대기 큐에 넣음
    • 프로세스가 자식 프로세스를 만들고 본인은 대기 큐에서 그 자식프로세스가 종료될 때 까지 기다림
    • 프로세스가 인터럽트나 할당 시간이 만료되어 cpu 코어로부터 강제적으로 remove되면 준비 큐에 다시 들어감
    처음 두 경우는 대기->준비 상태로 바뀌고 ready queue에 넣음. 프로세스는 이 과정을 종료될 때 까지 반복하며 종료되면 모든 큐에서 삭제되고 해당 프로세스의 PCB와 자원 할당을 해제함

 

Context Switch

프로세스가 인터럽트나 시스템 콜에 의해서 실행중인 프로세스를 잠깐 냅두고 다른 프로세스를 처리하고 오는 것

  1. 프로세스 p1이 프로세스 p2에 의해 인터럽트 당함
  2. 운영체제는 p1에 대한 정보를 PCB1에 저장
  3. 그리고 PCB2에서 p2의 정보를 reload함
  4. p2를 실행
  5. p2가 끝나거나 p1에 의해 인터럽트/시스템 콜로 호출당하면 p2에 대한 정보를 PCB2에 저장
  6. PCB1에서 p1에 대한 정보 reload
  7. p1 실행
  • 문맥 데이터는 PCB 에 저장된다.
  • 문맥 교환 시간은 아무런 처리를 못 하는 오버헤드 작업이다
    → 자주 하는 경우에는 시스템 저하를 일으킨다.
  • context switching 하는 비용 (저장하고 리로드)도 있어서 너무 자주하면 프로세스가 처리하는 일보다 context switching하는 비용이 큼

 

Process 생성과 종료

프로세스는 프로세스에 의해 만들어진다. (프로세스는 사람이랑 비슷하게 봐도 된다고 한다.) 컴퓨터가 부팅이 되었을 시 메모리에 OS가 올라가고, 최초의 프로세스를 생성하게 된다. 이렇게 만든 프로세스를 만들고, 그 프로세스가 또 다른 프로세스를 만들게 된다.

이러한 이미지로, tree와 같은 모습이 나타난다.

이렇게 생성된 프로세스는 각각 고유의 번호를 갖는데 이를 PID(Process Identifirer)라고 한다. PID는 일반적으로 정수형(integer)으로 표현한다. PPID는 부모의 PID를 말한다.

 

부모와 자식 프로세스간의 자원 공유나 실행은 다양하게 나타날 수 있다.

 

프로세스 종료의 경우 일반적으로 부모 프로세스에게 정수의 형태인 Status value를 반환하고 (wait()을 통해) 물리적/가상 메모리, 입출력 버퍼 등을 포함한 모든 자원은 운영체제에 의해 할당 해제된다.

 

또한 다음과 같은 경우에서 부모는 자식 프로세스의 실행을 멈출 수 있다. 

  • 자식 프로세스가 자신에게 허용된 자원을 초과하여 사용한 경우 (이를 위해서는 부모 프로세스가 자식 프로세스의 상태를 모니터링하는 메커니즘이 존재해야 함)
  • 자식 프로세스에게 할당된 일이 더 이상 필요하지 않은 경우
  • 운영체제에서 부모 프로세스가 종료되었을 때 자식 프로세스가 계속 실행되는 것을 허용하지 않을 경우 ( =Cascading Termination)

 

UNIX에서는 다음과 같은 방식으로 프로세스의 생성과 종료를 진행한다.

  • fork : 새로운 프로세스 생성
    • 생성된 프로세스는 부모 프로세스와 동일한 address space를 가진다. 이는 부모 프로세스와 자식 프로세스가 수월하게 통신(communicate)할 수 있도록 해준다. 
    • 부모, 자식 프로세스 모두 fork() 호출 이후 실행을 계속하지만, 자식 프로세스는 0을 반환하고 부모 프로세스는 non-zero 값을 반환한다.
    •  자식 프로세스는 부모 프로세스의 fork() 호출 이후 내용부터 실행된다.
  • exec : 프로세스에서 새로운 파일 실행.
    • 일반적으로 fork() 호출 이후에 호출된다.
    •  프로세스의 메모리 공간을 새 프로그램으로 교체한다. 이를 통해 자식 프로세스가 부모 프로세스와 다른 일을 하도록 할 수 있다.
  • exit: 프로세스 종료 

 

IPC

운영체제 내에서 동시에 실행되고 있는 프로세스들은 서로 독립적이거나, cooperating 관계에 있다. 어떤 프로세스가 다른 프로세스에 영향을 주지 않고, 받지도 않을 때 두 프로세스는 독립적(Independent)이라 한다.  그렇지 않은 프로세스는 cooperating이라 한다.

 

Cooperating 프로세스들은 IPC(InterProcess Communication) 메커니즘을 필요로 한다. IPC는 프로세스 간 데이터 교환을 가능하게 해 주며, 크게 두 가지 유형으로 나뉜다.

 

공유 메모리(Shared Memory)

 - 운영체제에 의해 메모리의 특정 구간을 공유

 - 프로세스들은 단순히 메모리를 읽고 쓰기만 한다.

 - 공유 메모리에 대한 갱신은 다른 모든 프로세스들에게 즉시 보여진다.

 

메시지 패싱(Message Passing)

 - 데이터는 메시지 형식으로 전달된다.

 - 분산형 시스템에 더 적합하다.

 

 

2. 스레드

스레드(Thread)는 실행(Execution)의 가장 작은 단위로, 한 프로세스는 다수의 스레드들을 포함한다. 간단하게 말하면, 쓰레드는 프로그램 내부의 흐름(맥)이라고 볼 수 있다.

int main(void)
{
  int n = 0;
  int m = 10;
  printf("%d\n", n * m);
  while(n < m)
    n++;
  printf("END\n");
}

이러한 코드가 있다고 했을 때, 위의 코드 역시 하나의 흐름을 가지고 있고, 이를 쓰레드라고 부르게 된다. 일반적으로 하나의 프로그램은 하나 이상의 쓰레드를 갖는다. 

 

스레드의 장점

1. 응답성(Responsiveness)

멀티스레드 환경으로 동작하는 프로그램은 프로그램이 block되거나, 긴 작업을 수행하는 동안에도 계속 돌아갈(running) 수 있다. 이로써 사용자에게 향상된 응답성을 제공할 수 있다.

 

ex) 웹 브라우저에서 웹 페이지의 이미지가 로딩되는 도중에도 웹 페이지의 기능을 사용할 수 있음.

 

2. 자원의 공유(Resource Sharing)

프로세스들은 IPC 기술(Shared Memory/Message Passing)을 이용해야만 서로의 메모리를 공유할 수 있다. 이러한 기술들은 순전히 프로그래머들의 몫이다.

하지만 스레드들은 자신들이 포함된 프로세스의 자원을 기본적으로 공유할 수 있다. 코드와 데이터를 공유할 수 있다는 것은 한 프로그램이 서로 다른 활동의 스레드들을 동일한 주소 공간에 담을 수 있다는 것을 의미한다.

 

3. 경제성(Economy)

프로세스의 생성을 통해 메모리 및 자원을 할당하는 것은 비용이 든다.

하지만 스레드는 자신이 속한 프로세스의 자원을 공유하므로, context-switch 스레드를 생성하는 것은 훨씬 경제적이다.

 

4. 확장성(Scalability)

멀티스레딩의 이점은 멀티프로세서 구조에서 훨씬 확연히 나타난다.

멀티프로세서 구조에서는 스레드들이 각자 다른 프로세서 위에서 병렬적으로 실행되기 때문이다. 즉, 다중 CPU 환경에서 병렬성이 크게 증대된다.

 

멀티스레드

멀티 스레드는 위에서 이야기한 스레드가 한 프로그램 내에서 진행되는 것이며, 스레드가 스위칭되기 때문에 이렇게 두여러개가 한번에 돌아가는 것 처럼 느낄 수 있다. 스레드를 실제로 사용하기 위해서는 커널 수준 또는 사용자 수준에서 스레드를 사용할 수 있도록 지원이 필요하다.

 

커널 스레드(Kernel Thread) : 커널 수준에서 사용하는 스레드. 운영체제에 의해 직접 관리된다.

유저 스레드(User Thread): 사용자 수준에서 사용하는 스레드. 유저 스레드는 커널의 지원 없이 관리된다.

 

Threading Issues

fork(), exec() system call

그렇다면 멀티스레드 프로그램에서 fork()의 호출로 복제된 새 프로세스는 원본의 스레드도 모두 복사할까? 아니면 하나의 단일 스레드를 가진 새 프로세스가 생성될까?

 

일부 UNIX 시스템에서는 두 가지 경우를 선택할 수 있도록 fork() 시스템 콜을 지원한다.

반면 exec() 시스템 콜은 프로세스에서의 호출과 동일하게 작동한다. 만약 한 스레드가 exec()을 호출한다면, 그 프로세스는 모든 자신의 스레드를 포함하여 다른 것으로 교체된다.

 

스레드 취소(cancellation)

스레드의 취소는 스레드가 완료되기 전에 하던 일을 종료시키는 것을 말한다. 이때 취소의 대상이 되는 스레드를 타겟 스레드(Target Thread)라고 한다. 

 

비동기 취소 (Asynchronous Cancellation) 한 스레드가 타겟 스레드를 즉시 종료시킨다.

지연 취소 (Deffered Cancellation) : 타겟 스레드는 앞 순서의 스레드가 종료되었는지 주기적으로 확인하며, 이를 통해 일련의 순서대로 종료시킬 수 있다.

 

시그널 핸들링

시그널(Signal)은 UNIX 시스템에서 특정 이벤트가 발생했을 때 프로세스에게 이를 알리는 데 주로 사용되는 것으로, 시그널이 발생한 이유에 따라 동기(ex. division by 0) 또는 비동기(ex. 실행중인 프로세스 외부의 이벤트)로 처리될 수 있다.

 

동기/비동기 여부에 관계없이, 시그널은 다음과 같은 패턴을 따른다.

  1.  특정 이벤트에 의해 시그널이 생성된다.
  2.  생성된 시그널이 프로세스로 전달된다.
  3.  전달되었다면, 시그널은 Handling된다. (해당하는 액션을 취한다.)

여러 스레드를 가진 프로세스가 전달하는 시그널의 선택지는 다음과 같다.

  •  시그널이 적용되는 스레드에게 전달
  •  프로세스 내의 모든 스레드에게 전달
  •  프로세스 내의 특정 스레드에게 전달
  •  모든 스레드를 전담하는 특별한 스레드를 할당

 

스레드 풀

각각의 요청마다 스레드를 생성하는 것은 프로세스를 새로 만드는 것보다 더 좋은 방법이지만, 이러한 멀티스레드 서버는 잠재적인 문제가 존재한다.

요청을 수행하기 위해 생성된 스레드는 요청을 모두 수행하고 버려진다는 사실과, 시스템 내에서 동시적으로 돌아가는 스레드 갯수에 대한 상한선을 정하지 않았다는 것이다. 제한이 없는 스레드는 시스템의 자원을 모두 고갈시킬 것이다.

이러한 문제로 인해 스레드 풀을 사용하는 것이 해결책으로 등장하였다.

 

스레드 풀의 기본적인 아이디어는 프로세스가 시작할 때 미리 특정 수의 스레드를 만들어, 이를 pool 의 형태로 두는 것이다. 요청을 받으면, 이 pool로부터 사용 가능한 스레드 하나를 선택하여 요청을 수행하도록 한다.

만약 선택된 스레드가 요청을 모두 수행했다면 그 스레드는 다시 pool로 돌아가 자신에게 요청이 들어오는 것을 대기하게 된다.

 

장점

  • 이미 존재하는 스레드를 사용하는 것이 스레드의 생성을 기다리는 것보다 보통 더 빠르다.
  • 스레드의 숫자에 대한 상한선이 지정되어 있기 때문에 자원을 더 효율적으로 사용할 수 있다.

 

참고자료
https://4legs-study.tistory.com/39
https://velog.io/@codemcd/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-7.-%EC%93%B0%EB%A0%88%EB%93%9CThread
http://www.kocw.net/home/search/kemView.do?kemId=978503