1일 1커밋🌱
프로세스와 쓰레드
1. 프로그램과 프로세스
프로그램
- 하드디스크 등과 같은 저장 장치에 저장된 명령문의 집합체를 말한다.
- 애플리케이션이나 앱이라고도 불리고, 윈도우즈 운영체제에서는 exe 파일의 모습을 하고 있다.
- 프로그램은 컴퓨터 관점에서 하드디스크, 즉 저장장치만 사용되는 수동적인 존재이다.
프로세스
- 프로세스를 최대한 간단하게 정의하면 실행 중인 프로그램이라고 할 수 있다.
- 실행 중인 프로그램이란 하드디스크에 저장된 프로그램이 메모리에 올라갔을 때를 말한다.
- 프로세스는 메모리도 사용하고 운영체제의 CPU 스케줄링 알고리즘에 따라 CPU도 사용하고 필요에 따라 입출력도 하기 때문에 능동적인 존재라고 할 수 있다.
- 프로세스는 코드 영역, 데이터 영역, 스택 영역, 힙 영역으로 이루어져 있다. 코드 영역은 자신을 실행하는 코드가 저장되어 있고, 데이터 영역은 전역 변수와 스태틱 변수가 저장되어 있다. 스택 영역에는 지역 변수와 함수 호출을 했을 때 필요한 정보들이 저장된다. 힙 영역은 런타임시 동적으로 메모리를 할당하는데 쓰인다.
- 프로세스는 운영체제에 의해 관리된다.
2. 멀티프로그래밍과 멀티프로세싱
유닛 프로그래밍
- 메모리에 오직 하나의 프로세스가 올라온 것을 말한다.
멀티 프로그래밍
- 메모리에 여러 개의 프로세스가 올라온 것을 말한다.
멀티 프로세싱
- CPU가 여러 개의 프로세스를 처리하는 것을 말한다. 시분할 처리를 통해 CPU가 각각의 프로세스를 짧은 시간 동안 교대로 실행한다.
유닛 프로그래밍과 멀티 프로그래밍이 메모리의 관점에서 정의된 것이라면, 멀티 프로세싱은 CPU 관점에서 정의된 것이다. 오늘날의 OS는 멀티 프로그래밍과 멀티 프로세싱 두 개가 공존한다.
스와핑
- 과거에는 메모리의 크기가 작아서 멀티 프로그래밍이 불가능했고 이때는 유닛 프로그래밍을 하면서 멀티 프로세싱을 이용했다. 메모리에 프로세스를 올려서 CPU로 처리하고, 이 프로세스를 다른 저장 장치에 저장한다. 그리고 다른 저장 장치에 있던 프로세스를 메모리에 올려서 CPU로 처리하는 것이다. 이때 메모리에 있는 데이터를 다른 저장 장치로 보내고, 다른 저장 장치에서 메모리에 올리는 것을 스와핑이라고 한다.
참고 : 멀티프로세스와 멀티쓰레드
3. PCB(Process Control Block)
프로세스가 만들어지면 운영체제는 해당 프로세스의 정보를 가지고 있는 PCB를 만들고 저장한다. PCB들은 연결 리스트라는 자료 구조로 저장된다. 연결 리스는 각각의 데이터가 다음 데이터를 연결하는 구조로 되어 있는 자료 구조이다. 운영체제는 프로세스가 종료되면 연결 리스트에서 해당 프로세스의 PCB를 제거한다.
위의 사진은 PCB의 구조이다.
포인터
- 부모 프로세스에 대한 포인터, 자식 프로세스에 대한 포인터, 프로세스가 위치한 메모리 주소에 대한 포인터, 할당된 자원에 대한 포인터, 프로세스의 한 상태에서 다른 상태로 전환될 때 저장하는 포인터 등을 가지고 있다.
프로세스 상태
- 프로세스 상태는 현재 프로세스의 5가지 상태 즉, 생성, 준비, 실행, 대기, 완료를 나타낸다.
프로세스 아이디
- 프로세스를 식별하기 위한 숫자가 저장된다.
프로그램 카운터
-
다음에 실행될 명령어의 주소를 저장한다.
-
시분할 처리로 인해 어떤 프로세스가 실행되다가 다른 프로세스에게 CPU를 뺏기고 다시 실행될 때, 원래 실행하던 명령어가 실행되어야 하기 때문에 프로그램 카운터가 반드시 있어야 한다.
레지스터
- 프로세스가 실행될 때 사용했던 레지스터 값들이 저장된다.
- 프로그램 카운터와 마찬가지로 CPU를 뺏기고 다시 실행될 때 이전에 사용하던 값을 복구하기 위한 용도이다.
메모리 제한
- 해당 프로세스의 메모리 위치 정보나 메모리 침범을 막기 위한 경계 레지스터 값 등이 저장된다.
CPU 스케줄링 정보
- CPU 스케줄링에 필요한 우선순위, 최종 실행 시간, CPU 점유 시간 등이 저장된다.
4. 프로세스 상태
사용자가 프로그램을 실행시키면 메모리에 올라가며 프로세스가 생성된다. 오늘날의 운영체제에는 동시에 수많은 프로세스가 실행된다. 작업 관리자를 확인해보면 수많은 프로세스가 실행중인 것을 확인할 수 있다. 시분할 시스템을 사용하는 운영체제에서는 여러 개의 프로세스를 돌아가면서 실행한다. CPU가 여러 개의 프로세스를 동시에 실행한다는 말이 아니라 한순간에는 하나의 프로세스밖에 처리하지 못한다는 말이다. 다만 속도가 매우 빨라서 사람이 보기에는 그냥 동시에 실행되는 것처럼 보이는 것이다.
프로세스는 시분할 처리를 위한 5가지 상태를 가지고 있다. 바로 생성 상태, 준비 상태, 실행 상태, 대기 상태, 완료 상태가 있다.
생성 상태
- PCB를 생성하고 메모리의 프로그램 적재를 요청한 상태이다.
- 메모리의 프로그램 적재를 승인받으면 준비 상태로 넘어간다.
준비 상태
- CPU를 사용하기 위해 기다리고 있는 상태이다.
- 준비 상태에 있는 프로세스는 CPU 스케줄러에 의해 CPU가 할당된다.
- 대부분의 프로세스는 이 준비 상태에 있다.
실행 상태
- 준비 상태에 있는 프로세스가 CPU 스케줄러에 의해 CPU를 할당받아 실행되는 상태이다.
-
실행 상태에 있는 프로세스의 수는 CPU의 개수만큼이다. CPU가 1개라면 실행 상태의 프로세스는 최대 1개이다.
-
실행 상태에 있는 프로세스도 CPU를 무한정 쓸 수 있는 것이 아니라 부여된 시간만큼만 사용할 수 있다.
- CPU 스케줄러는 부여된 시간을 초과하면 할당된 CPU를 강제로 빼앗고 프로세스는 다시 준비 상태로 돌아간다.
대기 상태
- CPU는 굉장히 빠른 장치이지만 입출력 장치는 상당히 느린 장치이다. 그래서 특정 프로세스가 입출력 요청을 하면 입출력이 완료될 때까지 CPU를 기다리게 하는 것은 굉장히 비효율적이다.
- 따라서 입출력 요청을 한 프로세스를 대기 상태로 두고 다른 프로세스에게 CPU를 할당한다. 그러다가 시간이 지나서 입출력 작업이 완료되면 대기 상태에 있던 프로세스에게 CPU 할당 기회를 준다.
완료 상태
- 프로세스가 종료된 상태이다.
- 프로세스가 사용했던 데이터를 메모리에서 제거하고 생성된 PCB도 제거한다.
5. 컨텍스트 스위칭(Context Switching)
컨텍스트 스위칭이란 프로세스를 실행하는 중에 다른 프로세스를 실행하기 위해 실행 중인 프로세스의 상태를 저장하고, 다른 프로세스의 상태값으로 교체하는 작업이다. 컨텍스트 스위칭이 일어날 때, PCB의 내용이 변경된다. 실행 중인 프로세스의 작업 내용을 PCB에 저장하고, 실행될 프로세스의 PCB 내용대로 CPU가 다시 세팅된다. 컨텍스트 스위칭이 일어날 때, PCB에 변경하는 값들로는 프로세스 상태, 다음 실행할 명령어의 주소를 담고 있는 프로그램 카운터, 각종 레지스터 값이 있다.
프로세스 2개가 컨텍스트 스위칭을 하는 상황을 살펴보자. 프로세스 A가 실행 중인데, CPU 점유 시간을 초과했다. 운영체제는 프로세스 A가 CPU를 너무 오래 사용했다고 판단하고 인터럽트를 발생시킨다. 프로세스 A는 하던 일을 멈추고, 나중에 현재 상태에서 시작되어야 하기 때문에 현재 CPU의 레지스터 값 등을 PCB-A에 저장한다. 이제 PCB-B를 참조해서 PCB-B에 있는 값으로 CPU 레지스터값을 설정한다. 여기에는 다음 실행할 명령어의 주소를 가지고 있는 프로그램 카운터를 가지고 있기 때문에 바로 프로세스 B의 명령어를 실행시킬 수 있다. 프로세스 B가 점유 시간 동안 CPU를 사용하다가 점유 시간이 다 되면, 운영체제는 다시 인터럽트를 발생시킨다. 그리고 프로세스 B의 현재 상태를 PCB-B에 저장하고, PCB-A에서 프로세스 A의 상태를 가져오고 다시 프로세스 A를 실행시킨다.
이런 식으로 메모리에 있는 모든 프로세스들은 컨텍스트 스위칭을 한다. 컨텍스트 스위칭이 발생하는 이유는 CPU 점유 시간이 다 되거나, 입출력 요청이 있거나, 다른 종류의 인터럽트가 있는 등 다양하다.
6. 프로그램 생성과 종료
일반적으로 프로세스가 생성될 때는 다음과 같은 방법으로 생성된다.
- exe 파일을 더블 클릭으로 실행하면 운영체제는 해당 프로그램의 코드 영역과 데이터 영역을 메모리에 로드하고 빈 스택과 빈 힙을 만들어 공간을 확보한다.
- 이 프로세스를 관리하기 위한 PCB를 만들어 값을 초기화해준다.
위의 프로세스 생성 과정은 운영 체제가 부팅되고 0번 프로세스가 생성될 때 딱 한 번만 실행된다. 이제 나머지 모든 프로세스는 새로 생성하지 않고 0번 프로세스를 복사해서 쓰게 된다. 복사는 fork() 함수를 이용한다. 이렇게 하는 이유는 새로 생성하는 것보다 복사를 하는 게 더 빠르기 때문이다. 0번 프로세스를 복사해서 생성되는 프로세스는 자식 프로세스라고 하고 이 자식 프로세스의 입장에서 0번 프로세스는 부모 프로세스가 된다. 자식 프로세스는 부모 프로세스의 코드 영역, 데이터 영역, PCB의 내용을 전부 복사한다.
그러나 0번 프로세스의 코드와 데이터를 모두 복사해서 실행하면 0번 프로세스와 똑같이 실행되는 거 아닌가는 의문이 들 수도 있다. 그렇다면 자기가 원하는 코드는 어떻게 실행시킬까? 바로 exec() 함수를 이용하는 것이다. fork() 함수로 프로세스를 복사한 후 exec() 함수를 실행시키면 부모를 복사한 자식 프로세스의 코드와 데이터 영역을 원하는 값으로 덮어쓰게 된다. 그럼 이때부터 자식 프로세스는 부모 프로세스와 완전히 다르게 동작하게 된다.
fork() 함수를 호출하면 운영체제는 이 프로세스와 동일한 프로세스를 복사하고 fork() 함수의 반환값으로 부모 프로세스에게는 0이 아닌 값을, 자식 프로세스에게는 0을 반환한다. 이제 이 두개의 프로세스는 CPU 스케줄링에 따라 실행되는데, 어떤 프로세스가 먼저 실행될지는 운영체제의 결정에 따른다.
#include <studio.h>
#include <unistd.h>
int main()
{
int pid;
pid = fork();
if(pid == 0) // 자식 프로세스
{
execlp("InternetBrowser", "0", NULL);
exit(0);
}
else // 부모 프로세스
{
wait(NULL);
printf("인터넷 브라우저 닫힘");
exit(0);
}
}
부모 프로세스가 먼저 실행된다고 가정해보자. 부모 프로세스는 fork() 함수의 반환값으로 1을 받았다. wait() 함수는 자식 프로세스에게서 exit() 신호가 올 때까지 기다리는 시스템 함수이다. 컨텍스트 스위칭을 거쳐 부모 프로세스에게 CPU가 할당되어도 자식 프로세스의 exit() 신호가 오기 전까지는 다른 코드를 실행하지 않는다.
이제 CPU 스케줄링으로 자식 프로세스가 실행된 상황이다. 자식 프로세스는 fork() 함수의 반환값으로 0을 받았다. execlp() 함수로 InternetBrowser 프로그램을 실행시킨다. 그럼 InternetBrowser 프로그램에서 코드와 데이터 영역을 가져와 자식 프로세스를 덮어쓰게 된다. InternetBrowser를 사용하다가 종료가 되면 exit() 함수를 호출하고 프로세르 종료를 알린다.
이제 CPU 스케줄링으로 다시 부모 프로세스가 실행되게 된다. wait() 함수로 자식 프로세스의 종료를 기다리고 있었는데 자식 프로세스가 종료되었다. 부모 프로세스는 자식 프로세스를 완전히 종료시킨다. 그리고 exit() 함수로 프로세스 종료를 알린다.
위의 예제에서처럼 exit() 함수는 자식 프로세스가 부모 프로세스에게 정상 종료를 알리는 함수이다. 부모 프로세스는 자식 프로세스의 Exit Status = 0을 읽고 자식 프로세스를 정리한다.
만약 부모 프로세스가 자식 프로세스보다 먼저 종료되거나 자식 프로세스가 비정상적으로 종료되어 exit() 신호를 주지 못해서 Exit Status를 읽지 못해 메모리에 계속 살아있는 상태를 좀비 프로세스라고 부른다. 컴퓨터를 오래 켜두면 느려지는 현상이 발생하곤 하는데 이는 여러 프로세스가 메모리에 올라온 것도 있고 좀비 프로세스가 많아져서 메모리를 차지하는 경우가 있다. 컴퓨터를 켰다 키면 메모리가 초기화되기 때문에 다시 빨라진다.
7. 쓰레드
- 운영체제가 작업을 처리하는 단위는 프로세스이다.
- 프로세스의 수가 많아지면 프로세스의 수만큼 PCB를 생성하고 메모리에 코드, 데이터, 스택, 힙 영역을 만들어 줘야 하기 때문에 너무 무거워진다.
- 프로세스끼리의 통신을 위해서는 IPC를 이용해야 하는데 IPC는 통신의 비용이 상대적으로 많이 든다.
- 컴퓨터 시스템 개발자들은 이 문제를 해결하기 위해 쓰레드라는 것을 고안한다.
- 한 프로세스 내의 쓰레드들은 그 프로세스의 PCB, 코드, 데이터, 힙 영역을 공유하며 스택 영역은 공유하지 않고 쓰레드마나 하나씩 가지고 있다.
- 프로세스 내의 여러 개의 쓰레드를 구분하기 위해 쓰레드 아이디와 쓰레드를 관리하기 위한 쓰레드 컨트롤 블록(TCP)가 필요하다.
- 구글의 크롬 브라우저는 탭 1개에 1개의 프로세스가 생성된다. 반면 파이어폭스 브라우저는 처음 4개의 탭에서만 프로세스가 생성되고 추가적인 탭은 생성된 프로세스 내에 쓰레드를 추가하는 식으로 동작한다.
- 프로세스는 서로 독립적이기 때문에 하나의 프로세스가 문제가 생기더라도 다른 프로세스가 영향을 받지 않는다. 반면 쓰레드는 하나의 프로세스 내에 존재하기 때문에 해당 프로세스에 문제가 생기면 그 안에 있는 모든 쓰레드에 문제가 생겨 안정성 측면에서는 프로세스가 우수하다.
- 각각의 프로세스는 서로 고유한 자원을 가지고 있다. 코드, 데이터, 스택, 힙 영역을 전부 따로 두고 있고 프로세스 간에 통신을 하려면 IPC를 이용해야 해서 오버헤드가 크다. 반면 쓰레드는 한 프로세스 내에서 스택 영역을 제외한 영역은 모두 공유하기 때문에 오버헤드가 굉장히 작다. 쓰레드 간의 통신은 데이터를 공유할 수 있기 때문에 프로세스 간의 통신보다는 상대적으로 쉽다. 따라서 속도와 자원 측면에서는 쓰레드가 우수하다.
참고
댓글남기기