개발자는 기록이 답이다

혼공 컴퓨터구조 + 운영체제 10. 프로세스와 스레드 본문

CS/운영체제

혼공 컴퓨터구조 + 운영체제 10. 프로세스와 스레드

slow-walker 2024. 1. 28. 22:21

 

1. 프로세스 개요

 

'실행 중인 프로그램' = '프로세스'

 

프로그램은 실행되기 전까지는 그저 보조기억장치에 있는 데이터 덩어리일 뿐이지만, 보조기억 장치에 저장된 프로그램을 메모리에 적재하고 실행하는 순간 그 프로그램은 프로세스가 된다. 그 리고 이 과정을 '프로세스를 생성한다'라고 표현한다.

 

프로세스 직접 확인하기

 

컴퓨터가 부팅되는 순간부터 수많은 프로세스들이 실행된다. 윈도우에서는 작업 관리자의 [프로세스 탭]에서 확인할 수 있고, 유닉스 체계의 운영체제에서는 ps 명령어로 확인할 수 있다.

 

 

실제로 컴퓨터를 켜고 확인해 실행한 프로세스 외에도 알 수 없는 여러 프로세스가 실행되고 있는 것을 볼 수 있다. 그중에는 사용자가 볼 수 있는 공간에서 실행되는 프로세스도 있지만, 보이지 않는 공간에서 실행되는 프로세스도 있다. 

  • 포그라운드 프로세스 foreground process : 사용자가 볼 수 있는 공간에서 실행되는 프로세스
  • 백그라운드 프로세스 background process : 사용자가 보지못하는 공간에서 실행되는 프로스

백그라운드 프로세스 중에는 사용자와 직접 상호작용할 수 있는 백그라운드 프로세스도 있지만, 사용자와 상호작용하지 않고 그저 묵묵히 정해진 일만 수행하는 백그라운드 프로세스도 있다. 이러한 백그라운드 프로세스를 유닉스 체계의 운영체제에서는 데몬daemon이라고 부르고, 윈도우 운영체제에서는 서비스 service라고 부른다.

 

프로세스 제어 블록


모든 프로세스는 실행을 위해 CPU를 필요로 하지만, CPU 자원은 한정되어 있다. 즉 모든 프로 세스가 CPU를 동시에 사용할 수는 없다. 그렇기에 프로세스들은 차례대로 돌아가며 한정된 시간 만큼만 CPU를 이용한다. 자신의 차례가 되면 정해진 시간만큼 CPU를 이용하고, 시간이 끝났음 을 알리는 인터럽트(타이머 인터럽트)가 발생하면 자신의 차례를 양보하고 다음 차례가 올 때까지 기다린다.

 

※ 타이머 인터럽트는 클럭 신호를 발생시키는 장치에 의해 주기적으로 발생하는 하드웨어 인터럽트이다. 타임아웃 인터럽트라고도 부른다.

 

 

운영체제는 빠르게 번갈아 수행되는 프로세스의 실행 순서를 관리하고, 프로세스에 CPU를 비롯한 자원을 배분한다. 이를 위해 운영체제는 프로세스 제어 블록PCB Process Control Block (이하 PCB)을 이용한다.

 

프로세스 제어 블록은 프로세스와 관련된 정보를 저장하는 자료 구조다. (마치 상품에 달린 태그와도 같다)

옷이나 가전제품에 달려 있는 태그에 해당 제품을 식별하기 위한 정보가 있는 것처 프로세스 제어 블록에는 해당 프로세스를 식별하기 위해 꼭 필요한 정보들이 저장된다.

 

메모리는 커널 영역과 사용자 영역으로 나누어져 있다는데, PCB는 커널 영역에 생성된다.

옷가게 점원이 수많은 옷들 사이에서 태그로 특정 옷을 식별하고 해당 옷과 관련된 정보를 판단하는 것처럼 운영체제도 수많은 프로세스들 사이에서 PCB로 특정 프로세스를 식별하고 해당 프로세스를 처리하는 데 필요한 정보를 판단한다.

 

  • '새로운 프로세스가 생성되었다' = '운영체제가 PCB를 생성했다'
  • '프로세스가 종료되었다' = '운영체제가 해당 PCB를 폐기했다'

PCB에 담기는 정보는 운영체제마다 차이가 있지만, 대표적으로 담기는 정보는 아래와 같다.

 

1) 프로세스 ID

 

프로세스 ID (PID: Proces ID)는 특정 프로세스를 식별하기 위해 부여하는 고유한 번호

  • 같은 일을 수행하는 프로그램이라 할지라도 두 번 실행하면 PID가 다른 두 개의 프로세스가 생성된다.
  • 윈도우 작업 관리자에서 PID를 확인할 수 있다.

 

 

 

2) 레지스터 값

 

  • 프로세스는 자신의 실행 차례가 돌아오면 이전까지 사용했던 레지스터의 중간값들을 모두 복원한다.
  • 그래야만 이전까지 진행했던 작업들을 그대로 이어 실행할 수 있다.
  • 그래서 PCB 안에는 해당 프로세스가 실행하며 사용했던 프로그램 카운터를 비롯한 레지스터 값들이 담긴다.

 

3) 프로세스 상태

 

  • 현재 프로세스가 어떤 상태인지도 PCB에 기록되어야 한다.
  • 현재 프로세스가 입출력장치를 사용하기 위해 기다리고 있는 상태인지, CPU를 사용하기 위해 기다리고 있는 상태인지, 아니면 CPU를 이용하고 있는 상태인지 등의 프로세스 상태 정보가 PCB에 저장된다.

4) CPU 스케줄링 정보

 

  • 프로세스가 언제, 어떤 순서로 CPU를 할당받을지에 대한 정보도 PCB에 기록된다.

 

5) 메모리 관리 정보

  • 프로세스마다 메모리에 저장된 위치가 다르기 때문에 PCB에는 프로세스가 어느 주소에 저장되어 있는지에 대한 정보가 있어야 한다.
  • PCB에는 베이스 레지스터, 한계 레지스터 값과 같은 정보들이 담긴다.
  • 또한 프로세스의 주소를 알기 위한 또 다른 중요 정보 중 하나인 페이지 테이블 정보도 PCB에 담긴다.

※ 페이지 테이블은 14장에서 다룬다. 지금은 'PCB에는 프로세스의 메모리 주소를 알 수 있는 정보 들이 담기는구나' 정도로 이해하자

 

6) 사용한 파일과 입출력장치 목록

  • 프로세스가 실행 과정에서 특정 입출력장치나 파일을 사용하면 PCB에 해당 내용이 명시된다.
  • 즉, 어떤 입출력장치가 이 프로세스에 할당되었는지, 어떤 파일들을 열었는지에 대한 정보들이 PCB에 기록된다.

 

문맥 교환(컨텍스트 스위칭)


하나의 프로세스에서 다른 프로세스로 실행 순서가 넘어가면 어떤 일이 일어날까?

 

▶ 가령 프로세스 A가 운영체제로부터 CPU를 할당받아 실행되다가 시간이 다 되어 프로세스 B에 CPU 사용을 양보한다고 가정해보자.
이런 상황에서 바로 직전까지 실행되던 프로세스 A는 프로그램 카운터를 비롯한 각종 레지스터 값 메모리 정보, 실행을 위해 열었던 파일이나 사용한 입출력장치 등 지금까지의 중간 정보를 백업해야 한다. 그래야만 다음 차례가 왔을 때 이전까지 실행했던 내용에 이어 다시 실행을 재개할 수 있다.


이러한 중간 정보, 즉 하나의 프로세스 수행을 재개하기 위해 기억해야 할 정보를 문맥 context이라고 한다.

  • 하나의 프로세스 문맥은 해당 프로세스의 PCB에 표현되어 있다.
  • PCB에 기록되는 정보들을 문맥이라고 봐도 무방하다.

실행 문맥을 잘 기억해 두면 언제든 해당 프로세스의 실행을 재 개할 수 있기 때문에 프로세스가 CPU를 사용할 수 있는 시간이 다 되거나 예기치 못한 상황이 발생 하여 인터럽트가 발생하면 운영체제는 해당 프로세스의 PCB에 문맥을 백업한다. 그리고 뒤이어 실행할 프로세스 B의 문맥을 복구한다. 이렇게 자연스럽게 실행되는 프로세스가 바뀐다.

 

이처럼 기존 프로세스의 문맥을 PCB에 백업하고, 새로운 프로세스를 실행하기 위해 문맥을 PCB로부터 복구하여 새로운 프로세스를 실행하는 것을 문맥 교환context switching이라고 한다.

 

문맥 교환은 여러 프로세스가 끊임없이 빠르게 번갈아 가며 실행되는 원리다. 문맥 교환이 자주 일어나면 프로세스는 그만큼 빨리 번갈아 가며 수행되기 때문에 여러분의 눈에는 프로세스들이 동시에 실행되는 것처럼 보인다.


※ 문맥 교환을 너무 자주 하면 오버헤드가 발생할 수 있기 때문에 문맥 교환이 자주 일어난다고 해서 반드시 좋은 건 아니다.

 

프로세스의 메모리 영역

 

프로세스가 생성되면 커널 영역에 PCB가 생성된다고 했다. 그렇다면 사용자 영역에는 프로세스가 어떻게 배치될까?

 

하나의 프로세스는 사용자 영역에 크게 코드 영역, 데이터 영역, 힙 영역 스택 영역으로 나뉘어 저장된다. 

1) 코드 영역

  • 코드 영역 (code segmen)은 텍스트 영역 (text segment)이라고도 부른다.
  • 말 그대로 실행할 수 있는 코드, 즉 기계어로 이루어진 명령어가 저장된다.
  • 데이터가 아닌 CPU가 실행할 명령어가 담겨 있기 때문에 쓰기가 금지되어 있다.
  • 다시 말해 코드 영역은 읽기 전용 read-only 공간이다.


2) 데이터 영역

  • 데이터 영역(data segment) 은 잠깐 썼다가 없앨 데이터가 아닌 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간이다.
  • 이런 데이터로는 전역 변수(global variable) 가 대표적입니다.
    •  '프로그램이 실행되는 동안 유지되며, 프로그램 전체에서 접근할 수 있는 변수'
코드 영역과 데이터 영역은 그 크기가 변하지 않는다. 
프로그램을 구성하는 명령어들이 갑자기 바뀔 일이 없으니 코드 영역의 크기가 변할 리 없고, 
데이터 영역에 저장될 내용은 프로그램이 실행되 는 동안에만 유지될 데이터니까.

그래서 코드 영역과 데이터 영역은 '크기가 고정된 영역'이라는 점 에서 정적 할당 영역이라고도 부른다. 
힙 영역과 스택 영역은 프로세스 실행 과정에서 그 크기가 변할 수 있는 영역이라서 동적 할당 영역이라고도 부른다.

 

3) 힙 영역

 

  • 힙 영역(heapsegment) 프로그램을 만드는 사용자, 즉 프로그래머가 직접 할당할 수 있는 저장 공간이다.
  • 프로그래밍 과정에서 힙 영역에 메모리 공간을 할당했다면 언젠가는 해당 공간을 반환해야 한다.
  • 메모리 공간을 반환한다는 의미는 '더 이상 해당 메모리 공간을 사용하지 않겠다'라고 운영체제에 말해주는 것과 같다.메모리 공간을 반환하지 않는다면 할당한 공간은 메모리 내에 계속 남아 메모리 낭비를 초래한다.
  • 이런 문제를 메모리 누수memory leak라고 한다. (많은 개발자가 자주 마주치는 문제 중 하나)
_693848=ERROR: LeakSanitizer: detected memory leaks - 888바이트의 메모리가 낭비되었습니다.

Direct leak of 888 byte(s) in 1 object(s) allocated from:
#0 0xffff8222ea30 in __interceptor_malloc (/lib/aarch64-linux-gnu/libasan.so.5+0xeda30) 
#1 0xaaaacfbea224 in load_symtab /home/ubuntu/leak/uftrace/utils/symbol.c:410
#2 0xaaaacfbf6234 in load_module_symbol /home/ubuntu/leak/uftrace/utils/symbol.c:1060 
#3 0xaaaacfbf68f4 in load_module_symtab /home/ubuntu/leak/uftrace/utils/symbol.c:1096 
#4 0xaaaacfbf6fe8 in load_module_symtabs /home/ubuntu/leak/uftrace/utils/symbol.c:1172 
#5 0xaaaacfafb3d4 in load_session_symbols /home/ubuntu/leak/uftrace/cmds/record.c:1469 
#6 0xaaaacfb03570 in write_symbol_files /home/ubuntu/leak/uftrace/cmds/record.c:1990 
#7 0xaaaacfb04ee4 in do_main_loop /home/ubuntu/leak/uftrace/cmds/record.c:2094 
#8 0xaaaacfb06028 in command_record /home/ubuntu/leak/uftrace/cmds/record.c:2209 
#9 0xaaaacfa674d0 in main /home/ubuntu/leak/uftrace/uftrace.c:1369
#100xffff812ea08 in #11 0xaaaacfa5a660 (/home/ubuntu/leak/uftrace/uftrace+0x2a7660)
##....

SUMMARY: AddressSanitizer: 888 byte(s) leaked in 1 allocation(s).

 

4) 스택 영역

  • 스택 영역(stack segment) 은 데이터를 일시적으로 저장하는 공간이다.
  • 데이터 영역에 담기는 값과는 달리 잠깐 쓰다가 말 값들이 저장되는 공간이다.
  • 이런 데이터로 는 함수의 실행이 끝나면 사라지는 매개 변수, 지역 변수가 대표적이다.
  • 일시적으로 저장할 데이터는 스택 영역에 PUSH되고, 더 이상 필요하지 않은 데이터는 POP됨으로써 스택 영역에서 사라진다.


힙 영역과 스택 영역은 실시간으로 그 크기가 변할 수 있기 때문에 동적 할당 영역이라고 불러서 일반적으로 힙 영역은 메모리의 낮은 주소에서 높은 주소로 할당되고, 스택 영역은 높은 주소 에서 낮은 주소로 할당된다. 그래야만 힙 영역과 스택 영역에 데이터가 쌓여도 새롭게 할당되는 주소가 겹칠 일이 없다.

 

2. 프로세스 상태와 계층 구조

 

 

 

프로세스는 모두 저마다의 상태가 있다. 운영체제는 이런 프로세스의 상태를 PCB에 기록하여 관리한다. 그리고 많은 운영체제는 이처럼 동시에 실행되는 수많은 프로세스를 계층적으로 관리한다.

 

프로세스 상태


컴퓨터를 사용할 때 여러 프로세스들이 빠르게 번갈아 가면서 실행된다고 했다.

그 과정에서 하나의 프로세스는 여러 상태를 거치며 실행된다. 그리고 운영체제는 프로세스의 상태를 PCB를 통해 인식하고 관리한다. 프로세스의 상태를 표현하는 방식은 운영체제마다 조금씩 차이 가 있지만, 프로세스가 가질 수 있는 대표적인 상태는 아래와 같다.


1) 생성 상태

 

  • 프로세스를 생성 중인 상태를 생성 상태(new)라고 한다.
  • 이제 막 메모리에 적재되어 PCB를 할 당받은 상태
  • 생성 상태를 거쳐 실행할 준비가 완료된 프로세스는 곧바로 실행되지 않고 준 비 상태가 되어 CPU의 할당을 기다린다.

 

2) 준비 상태

 

  • 준비 상태 (ready)는 당장이라도 CPU를 할당받아 실행할 수 있지만, 아직 자신의 차례가 아니기에 기다리고 있는 상태 
  • 준비 상태 프로세스는 차례가 되면 CPU를 할당받아 실행 상태가 된다

※ 준비 상태인 프로세스가 실행 상태로 전환되는 것을 디스패치(dispatch)라고 한다.

 

3) 실행 상태

  • 실행 상태 (running)는 CPU를 할당받아 실행 중인 상태
  • 실행 상태인 프로세스는 할당된 일정 시간 동안만 CPU를 사용할 수 있습니다.
  • 이때 프로세스가 할당된 시간을 모두 사용한다.
  • 이때 프로세스가 할당된 시간을 모두 사용한다면(타이머 인터럽트가 발생하면) 다시 준비 상태가 되고, 실행 도중 입출력장치를 사용하여 입출력 장치의 작업이 끝날 때까지 기다려야 한다면 대기 상태가 된다.

 

4) 대기 상태

  • 프로세스는 실행 도중 입출력장치를 사용하는 경우가 있다.
  • 입출력 작업은 CPU에 비해 처리 속 도가 느리기에, 입출력 작업을 요청한 프로세스는 입출력장치가 입출력을 끝낼 때까지(입출력 완료 인터럽트를 받을 때까지) 기다려야 한다.
  • 이렇게 입출력장치의 작업을 기다리는 상태를 대기 상태 (blocked)라고 힌다.
  • 입출력 작업이 완료되면 해당 프로세스는 다시 준비 상태로 CPU 할당을 기다립니다.

 

5) 종료 상태

  • 종료 상태(terminated)는 프로세스가 종료된 상태
  • 프로세스가 종료되면 운영체제는 PCB 와 프로세스가 사용한 메모리를 정리한다.

프로세스 상태 도표

 

위와 같은 도표를 프로세스 상태 다이어그램 process state diagram이라고 한다. 이처럼 컴퓨터 내의 여러 프로세스는 생성, 준비, 실행, 대기, 종료 상태를 거치며 실행된다. 운영체제는 이 상태를 PCB에 기록하며 프로세스들을 관리하는 것이다.

 

대기 상태의 일반적인 정의

 

프로세스가 대기 상태가 되는 이유에 입출력 작업만 있는 것은 아니다.

조금 더 일반적으로 표현하자면 특정 이벤트가 일어나길 기다릴 때 프로세스는 대기 상태가 된다.

다만, 프로세스가 대기 상태가 되는 대부분의 원인이 입출력 작업이 기 때문에 '프로세스가 입출력 작업을 하면 대기 상태가 된다'고 생각해도 무방하다.

 

프로세스 계층 구조


프로세스는 실행 도중 시스템 호출을 통해 다른 프로세스를 생성할 수 있다.

  • 부모 프로세스 parent process : 새 프로세스를 생성한 프로세스
  • 자식 프로세스 child process : 부모 프로세스에 의해 생성된 프로세스

 

부모 프로세스와 자식 프로세스는 엄연히 다른 프로세스이기에 각기 다른 PID를 가진다. 일부 운영체제에서는 자식 프로세스의 PCB에 부모 프로세스의 PID인 PPID Parent PID가 기록되기도 한다.


부모 프로세스로부터 생성된 자식 프로세스는 실행 과정에서 또 다른 자식 프로세스를 생성할 수 있고, 그 자식 프로세스는 실행 과정에서 또 다른 자식 프로세스를 생성할 수 있다. 많은 운영체제는 이처럼 프로세스가 프로세스를 낳는 계층적인 구조로써 프로세스들을 관리합니다.

 

컴퓨터가 부팅될 때 실행되는 최초의 프로세스가 자식 프로세스들을 생성하고, 생성된 자식 프로세스들이 새로운프로세스들을 낳는 형식으로 여러 프로세스가 동시에 실행된다. 이 과정을 도표로 그리면 아래와 같은 트리 구조를 띄는데, 이를 프로세스 계층 구조라고 한다.

 

 

▶ 예시 : 사용자가 컴퓨터를 켜고 로그인 창을 통해 성공적으로 로그인해서 bash 셸(사용자 인터 페이스)로 Vim이라는 문서 편집기 프로그램을 실행했다고 가정해 봅자.

이 경우 ① 사용자가 컴퓨터를 켠 순간에 생성된 최초 프로세스는 로그인을 담당하는 프로세스를 자 식 프로세스로 생성한 것이고, ② 로그인 프로세스는 사용자 인터페이스(bash 셸) 프로세스를 자식 프로세스로 생성한 것이고, 사용자 인터페이스 프로세스는 Vim 프로세스를 생성한 셈이다다. 이 를 도표로 그리면 아래와 같다.

 

※ 데몬이나 서비스 또한 최초 프로세스의 자식 프로세스이다.

 

최초의 프로세스

 

모든 프로세스의 가장 위에 있는 최초의 프로세스는 무엇일까?

 

최초의 프로세스는 유닉스 운영체제에서는 init, 리눅스 운영체제에서는 systemd, macOS에서는 launchd라고 한다.

최초의 프로세스 PID는 항상 1번이며, 모든 프로세스 최상단에 있는 부모 프로세스이다.


pstree 명령어는 프로세스 계층 구조를 보여주는 명령어이다.

 

리눅스에서 pstree 명령어를 입력하면 systemd가 최상단에 있다는 것을 확인할 수 있고, macOS에서 pstree 명령어를 입력하면 launchd가 최상단에 있는 것을 확인할 수 있다.

 

 

프로세스 생성 기법


부모 프로세스가 자식 프로세스를 어떻게 만들어 내고, 자식 프로세스는 어떻게 자신만의 코드를 실행할까?

 

결론부터 말하면, 부모 프로세스를 통해 생성된 자식 프로세스들은 복제와 옷 갈아입기를 통해 실행된다.

조금 더 정확하게, 부모 프로세스는 fork를 통해 자신의 복사본을 자식 프로세스로 생성해 내고, 만들어진 복사본(자식 프로세스)은 exec를 통해 자신의 메모리 공간을 다른 프로그램으로 교체한다.

 

fork와 exec는 시스템 호출이다.

 

  • 부모 프로세스는 fork 시스템 호출을 통해 자신의 복사본을 자식 프로세스로 생성한다.
    • 즉, fork는 자기 자신 프로세스의 복사본을 만드는 시스템 호출이다.
  • 자식 프로세스는 부모 프로세스의 복사본이기 때문에 부모 프로세스의 자원들, 이를테면 메모리 내 의 내용, 열린 파일의 목록 등이 자식 프로세스에 상속된다(복사된 자식 프로세스라 할지라도 PID 값이나 저장된 메모리 위치는 다릅니다).

 

  • fork를 통해 복사본이 만들어진 뒤에 자식 프로세스는 exec 시스템 호출을 통해 새로운 프로그램으 로 전환된다.
    • exec는 자신의 메모리 공간을 새로운 프로그램으로 덮어쓰는 시스템 호출이다.
    • 다시 말해 새로운 프로그램 내용으로 전환하여 실행하는 시스템 호출이다.


메모리 공간에 새로운 프로그램 내용이 덮어 써진다는 점에서 이는 자식 프로세스가 새로운 옷으로 갈아입었다고도 볼 수 있다.

exec를 호출하면 코드 영역과 데이터 영역의 내용이 실행할 프로그램의 내용으로 바뀌고, 나머지 영역은 초기화된다.


▶ 예시 : 사용자가 bash 셸에서 Is라는 명령어를 쳤다고 가정해보자.

 

  • 셀 프로세스는 fork를 통해 자신과 동일한 프로세스를 생성하고, 그로부터 탄생한 자식 프로세스(셸의 복제 프로세스)는 exec를 통해 Is 명령어를 실행하기 위한 프로세스로 전환되어 실행된다.
  • 그렇게 셸의 복사본으로 탄생한 자식 프로세스는 Is 명령어를 실행하기 위한 프로세스로 바뀌고, 메모리 공간에는 Is 명령어를 실행하기 위한 내용들이 채워진다.

정리

  1. 부모가 자식 프로세스를 실행하며 프로세스 계층 구조를 이루는 과정은 fork exec가 반복되는 과정이라 볼 수 있다.
  2. 쉽게 말해 부모 프로세스로부터 자식 프로세스가 복사되고, 자식 프로세스는 새로운 프로그램으로 옷을 갈아입고, 또 그 자식 프로세스로부터 자식 프로세스가 복사 되고 옷을 갈아입는 방식으로 여러 프로세스가 계층적으로 실행되는 것이다.
  3. 부모 프로세스가 자식 프로세스를 fork한 뒤에 부모 프로세스, 자식 프로세스 누구도 exec를 호출 하지 않는 경우도 있다.
  4. 이 경우 부모 프로세스와 자식 프로세스는 같은 코드를 병행하여 실행하는 프로세스가 된다.

 

지금까지 배운 내용들 을 간단한 소스 코드로 학습해자.

하드웨어의 큰 그림을 그리고 작동 원리를 학습했었던 컴퓨터 구조 편과는 달리, 운영체제 편은 코드와 맞닿아 있는 부분이 많다.

프로그래밍 언어 입문서만 가볍게 학습한다면 놓치기 쉬운 중요한 내용들도 많이 포함되어 있다.

 

C/C++, Python, Java 등의 프로그래밍 언어로 프로세스를 다루는 예제들은 아래 링크 process 항목에 첨부되어있다.

예제 코드 다운로드하기

 

3. 스레드

 

스레드(thread) : 프로세스를 구성하는 실행의 흐름 단위

  • 하나의 프로세스는 여러 개의 스레드를 가질 수 있다
  • 스레드를 이용하면 하나의 프로세스에서 여러 부분을 동시에 실행할 수 있다.

 

 

프로세스와 스레드

 

전통적인 관점에서 보면 하나의 프로세스는 한 번에 하나의 일만을 처리했다.

앞선 절에서도 한 번에 하나의 작업을 처리하는 프로세스를 얘기했었다.

 

예를 들어, 웹 브라우저, 게임, 워드 프로세서 프 로세스가 있을 때 이 모든 프로세스가 하나의 실행 흐름을 가지고 한 번에 하나의 부분만 실행되는 프로세스를 가정했다.

 

'실행의 흐름 단위가 하나'라는 점에서 왼쪽 그림처럼 실행되는 프로세스들은 단일 스레드 프로세스라고 볼 수 있다.

 

 

하지만 스레드라는 개념이 도입되면서 하나의 프로세스가 한 번에 여러 일을 동시에 처리할 수 있게 되다. 

즉, 프로세스를 구성하는 여러 명령어를 동시에 실행할 수 있게 된 것이다.

 

이런 점에서 볼 때 스레드는 '프로세스를 구성하는 실행 단위'라고 볼 수 있다.

이 말이 조금 추상적으로 들릴 수도 있겠지만, 스레드의 구성 요소를 파악하면 조금 더 분명히 와닿을 것이다.

 

스레드는 프로세스 내에서 각기 다른 스레드 ID, 프로그램 카운터 값을 비롯한 레지스터 값, 스택으로 구성된다.

각자 프로그램 카운터 값을 비롯한 레지스터 값, 스택을 가지고 있기에 스레드마다 각기 다른 코드를 실행할 수 있습니다.

 

여기서 중요한 점은 프로세스의 스레드들은 실행에 필요한 최소한의 정보 (프로그램 카운터를 포함 한 레지스터, 스택)만을 유지한 채 프로세스 자원을 공유하며 실행된다는 점이다.

 

프로세스의 자원을 공유한다는 것이 스레드의 핵심이다.

위 그림의 예를 보면 스레드 1만의 코드/데이터/힙 영역 이 있고, 스레드 2만의 코드/데이터/힙 영역이 있는 게 아니라는 의미다.


정리

  1. 프로세스가 실행되는 프로그램이라면 스레드는 프로세스를 구성하는 실행의 흐름 단위다.
  2. 실제로 최근 많은 운영체제는 CPU에 처리할 작업을 전달할 때 프로세스가 아닌 스레드 단위로 전달한다.
  3. 그리고 스레드는 프로세스 자원을 공유한 채 실행에 필요한 최소한의 정보만으로 실행된다.

 

리눅스 운영체제에서 프로세스 vs 스레드

 

많은 운영체제가 프로세스와 스레드를 구분하지만, 프로세스와 스레드 간에 명확한 구분을 짓지 않는 운영체제도 있다.

대표적으로 리눅스가 그러하다.

리눅스는 프로세스와 스레드 모두 실행의 문맥(context of execution)이라는 점 에서 동등하다고 간주하고 이 둘을 크게 구분 짓지 않는다.

 

프로세스와 스레드라는 말 대신 태스크(task)라는 이름으로통일하여 명명한다.


아래 그림은 "프로세스와 스레드의 개념을 조금 더 분명히 구분 지을 필요가 있다"는 말에 대한 리눅스 운영체제 창시자 리누스 토르발스(Linus Torvalds)의 반응이다. 이번 장에서 배운 프로세스와 스레드를 바라보는 운영체제 창시자의 철 학을 엿볼 수 있는 흥미로운 읽을거리이니 전문을 읽어 보고 싶다면 여기를 찾아가보자.

 

 

멀티프로세스와 멀티스레드


하나의 프로세스에 여러 스레드가 있을 수 있다는 말을 조금 더 자세히 알아보자

 

컴퓨터는 실행 과정에서 여러 프로세스가 동시에 실행될 수 있고, 그 프로세스를 이루는 스레드는 여러 개 있을 수 있다고 했다.

이때 여러 프로세스를 동시에 실행하는 것을 멀티프로세스(multiprocess), 그리고 여러 스레드로 프로세스를 동시에 실행하는 것을 멀티스레드(multithread)라고 한다.

 

동일한 작업을 수행하는 단일 스레드 프로세스 여러 개를 실행하는 것과 하나의 프로세스를 여러 스레드로 실행하는 것은 무엇이 다를까?

 

▶ 예시 : "hello, os"를 화면에 출력하는 간단한 프로그램이 있다고 해보자.

 

이 프로그램을 세번 fork하여 실행하면 화면에는 "hello, os"가 세 번 출력된다.

이 프로그램 내에 "hello, os"를 출력하는 스레드를 세 개 만들어 실행해도 화면에는 "hello, os"가 세 번 출력됩니다.

 

"hello, os"가 세 번 출력된다는 결과는 같은데 뭐가 다를까?

 

여기에는 큰 차이가 있다.

 

프로세스끼리는 기본적으로 자원을 공유하지 않지만, 스레드끼리는 같은 프로세스 내의 자원을 공유한다는 점이다.


프로세스를 fork하여 같은 작업을 하는 동일한 프로세스 두 개를 동시에 실행하면 코드 영역, 데이터 영역, 힙 영역 등을 비롯한 모든 자원이 복제되어 메모리에 적재된다. 한 마디로 PID, 저장된 메모 리 주소를 제외하면 모든 것이 동일한 프로세스 두 개가 통째로 메모리에 적재되는 것이다. fork를 세 번, 네 번 하면 마찬가지로 메모리에는 같은 프로세스가 통째로 세 개, 네 개 적재된다. 같은 프로그램을 실행하기 위해 메모리에 동일한 내용들이 중복해서 존재하기때문에 낭비다.

 

※ fork를 한 직후 같은 프로세스를 통째로 메모리에 중복 저장하지 않으면서 동시에 프로세스끼리 자원을 공유하지 않는 방법도 있다. 이를 쓰기 시 복사(copy on write) 기법이라고 하는데, 이는 14장에서 나온다.

 

이에 반해 스레드들은 각기 다른 스레드 ID, 프로그램 카운터 값을 포함한 레지스터 값, 스택을 가질 뿐 프로세스가 가지고 있는 자원을 공유한다. 즉, 같은 프로세스 내의 모든 스레드는 그림처럼 동일한 주소 공간의 코드, 데이터, 힙 영역을 공유하고, 열린 파일과 같은 프로세스 자원을 공유한다. 여러 프로세스를 병행 실행하는 것보다 메모리를 더 효율적으로 사용할 수 있다. 또한 서로 다른 프로세스들은 기본적으로 자원을 공유하지 않기 때문에 서로가 남남처럼 독립적으로 실행되는 반면, 스레드는 프로세스의 자원을 공유하기 때문에 서로 협력과 통신에 유리하다.

 

프로세스의 자원을 공유한다는 특성은 때론 단점이 될 수도 있다.

 

멀티프로세스 환경에서는 하나의 프로세스에 문제가 생겨도 다른 프로세스에는 지장이 적거나 없지만, 멀티스레드 환경에서 는 하나의 스레드에 문제가 생기면 프로세스 전체에 문제가 생길 수 있다. 모든 스레드는 프로세스의 자원을 공유하고, 하나의 스레드에 문제가 생기면 다른 스레드도 영향을 받기 때문이다.

 

프로세스 간 통신

 

프로세스끼리는 '기본적으로' 자원을 공유하지 않지만, 프로세스끼리도 충분히 자원을 공유하고 데이터를 주고받을 수 있다. 프로세스 간의 자원을 공유하고 데이터를 주고받는 것을 프로세스간 통신(IPC; Inter-Process Communication)이라고 부른다.

 

‘통신'이라는 말을 들으면 네트워크를 통해 데이터를 주고 받는 방식만을 떠올리기 쉽지만, 같은 컴퓨터 내의 서로 다른 프 로세스나 스레드끼리 데이터를 주고받는 것도 통신으로 간주한다.

  • 프로세스 A : 'hello.txt' 파일에 새로운 값을 쓰는 프로세스
  • 프로세스 B :'hello.txt' 파일을 읽는 프로세스

두 프로세스는 'hello.txt' 파일 속 데이터를 주고받으므로 프로세스 간의 통신이 이루어져야 한다. 이는 파일을 통한 프로세스 간 통신으로 볼 수 있다.


또 프로세스들은 서로 공유하는 메모리 영역을 두어 데이터를 주고받을 수 있다. 프로세스들이 공유할 수 있는 메모리 영역을 공유 메모리(shared memory)라고 한다.

 



가령 프로세스 A와 B가 공유하는 메모리 영역 내에 'name'이라는 전역 변 수가 있다고 가정해보자. 프로세스 A가 name 안에 값을 저장한 뒤, 프로세스 B가 name 변수 값을 읽어들인다면 두 프로세스는 전역 변수 name을 통해 서로 값을 주고받았다고 볼 수 있다.

 

이 외에도 프로세스들은 소켓, 파이프 등을 통해 통신할 수 있습니다. 즉, 프로세스들끼리 데이터를 교환하는 것은 모든 자원을 처음부터 공유하고 있는 스레드에 비하면 다소 까다로운 것일 뿐 불가능한 것은 아니다.

 

지금까지 배운 내용들을 간단한 소스 코드로 학습해보자.

예제 코드 다운로드하기