프로세스의 개념과 컨텍스트 스위칭, 그리고 유닉스에서 프로세스를 생성하고 종료하는 방법에 대해 알아봅니다. 추가로 프로세스간 통신에 대해 아주 간략히 알아봅니다. 아래 포스팅을 먼저 읽고 게시물을 읽으면 좋을 것 같습니다.
목차
- 프로세스 개념
- 프로세스와 메모리 레이아웃
- 프로세스 상태
- PCB
- 프로세스 스케쥴링
- 스케쥴링큐
- 스케쥴러
- 컨텍스트 스위칭
- Operation on process
- 프로세스 통신
프로세스 개념
1. 프로세스와 메모리 레이아웃
프로세스 정의
프로세스는 실행 중인 프로그램입니다. 프로그램은 디스크에 저장돼 있는 명령어들의 모임이고 프로세스는 디스크에서 메모리에 로딩된 후 실행되는 프로그램입니다. 프로세스를 이해하려면 컨택스트와 프로세스의 상태에 대해 이해해야합니다.
컨텍스트 (context)
프로세스의 문맥은 프로세스가 생성된 후 무엇을 어떻게 실행했는지, 현재 어떤 상태를 가지는지 나타내는데 사용되는 개념입니다. 컨텍스트가 왜 필요한지 알아보기 전에 프로세스가 어떻게 실행되는지 수행 과정을 간단하게 살펴보겠습니다. 프로세스가 CPU를 잡으면 program counter가 실행할 명령어를 가리킵니다. 매 순간 기계어를 한줄씩 읽어서 레지스터에 필요한 값을 넣고, 그 값을 가지고 CPU에서 연산을 하고, 그 결과를 레지스터 혹은 외부 메모리에 저장합니다.
프로세스가 계속 진행될 수도 있겠지만 작업이 끝나지 않더라도 언젠가는 CPU를 다른 프로세스에게 넘겨야 합니다. 다른 프로세스에게 CPU를 양보하고 다시 CPU를 받았을 때, 처음부터 다시 실행해야할까요? 처음부터 다시 실행하지 않고 이어서 프로세스를 실행하려면 컨텍스트를 저장해야 합니다. 즉 컨텍스트는 프로세스를 진행하다가 어느시점까지 프로세스가 진행됐는지 규명하는데 필요한 요소입니다. 특정 시점에 프로세스의 컨텍스트를 나타내기 위해서는 program counter가 어디를 가르키고 있는지 이 프로세스의 메모리는 어떤 내용을 담고 있는지, 함수를 실행했다면 스택에 어떤 내용이 어디까지 쌓여있는지, 데이터 변수의 값을 사용하고 바꿨다면 변수의 값은 무엇인지 레지스터에 어떤 값을 넣어놨는지를 알아야합니다.
크게 세 가지로 컨텍스트의 요소를 설명할 수 있습니다.
- CPU와 관련된 하드웨어 문맥
- program counter : 명령어를 어디까지 수행했는지
- 레지스터 : 레지스터에 어떤 값이 저장됐는지
- 메모리와 관련된 프로세스의 주소 공간
- 코드 : 프로세스가 실행할 코드가 저장된 여역
- 데이터 : 전역변수와 스태틱변수가 저장돼있는 영역
- 스택 : 함수 호출시 생성되는 지역변수, 매개변수가 저장된 영역
- 커널 관련 자료구조
- 프로세스를 관리하는 역할은 운영체제가 합니다. 운영체제는 자신의 데이터 영역에 PCB라는 자료구조를 두고 관리하는데, 뒤에서 자세히 작성하겠습니다.
- 커널 스택 : 각 프로세스는 자신이 실행할 수 있는 함수는 각자 자신의 스택에서 관리합니다. 그런데 자신이 처리할 수 없는 함수 가령 메모리에 직접 접근하거나, I/O 처리에 대한 함수는 운영체제가 대신 처리합니다. 커널 코드는 여러 프로세스들이 공유하는 코드지만, 그 코드가 어떤 프로세스에게 호출을 받고 실행하는지는 매번 다릅니다. 그래서 커널에게 함수호출이 이뤄져서 스택에 정보를 쌓을 때는, 프로세스마다 커널 스택을 별도로 생성하고 관리합니다. 따라서 프로세스의 컨텍스트를 규명하려면 유저 스택 뿐만 아니라 각자의 커널 스택에 어떤 내용을 쌓고 있는지를 알아야합니다.
프로세스가 하나만 실행된다면 컨텍스트에 대해 굳이 알 필요 없지만, 대부분 시스템은 멀티태스킹으로 프로세스들이 CPU를 번갈아가면서 사용하는 구조입니다. 만약 레지스터에 어떤 값을 저장했는데 백업해두지 않으면, 즉 프로세스의 컨텍스트를 알지 못하면 다시 CPU를 잡았을 때 어디서부터 프로세스를 진행할지 알 수 없습니다.
2. 프로세스 상태
- Running : CPU가 하나인 상황을 가정하면 CPU를 잡고 있는 프로세스는 매 순간 하나입니다. CPU를 잡고 명령어를 실행 중인 상태를 Running이라고 부릅니다.
- Ready : 기다리고 있는 상태입니다. CPU를 실행할 수 있는 모든 조건이 만족된 상태이고, CPU만 가지고 있지 못한 상태입니다. 실행에 필요한 모든 부분이 메모리에 이미 올라와 있고 CPU만 얻으면 바로 시리행이 가능한 상태입니다.
- Blocked (wait, sleep) :CPU를 당장 잡아도 실행하지 못하는 프로세스입니다. 예를 들면 I/O 작업을 요청하고 기다리고 있는 상태, 혹은 다른 프로세스가 끝나기를 기다리고 있는 상태입니다
- 그 밖에 프로세스가 생성중인 상태인 new, 수행이 끝나고 종료된 상태인 terminate가 있습니다.
프로세스의 상태도
프로세스가 하나 생성되고 메모리가 할당되면 new에서 admitted되어 ready 상태가 됩니다.
CPU를 얻고 Running 상태가 됩니다. 긴 I/O작업을 요청하거나 할당된 시간을 전부 사용하면 인터럽트가 들어와서 ready 상태가 될 수 있습니다.
3. PCB
PCB(process control block)은 운영체제가 각 프로세스를 관리하기 위해 프로세스 당 유지하는 정보입니다.
- 프로세스 상태 : new, ready, running, waiting, halted 등 앞서 설명한 상태
- Program Counter, 그 외 레지스터 : 프로세스에서 다음에 실행할 명령문의 주소와 그 밖에 레지스터에 저장 된 값
- CPU scheduling information : 프로세스 id, 프로세스 우선순위 정보, 스케쥴링 큐를 가르키는 포인터
- Memory-management information : 메모리와 관련된 base, limit register
- Accounting information : CPU 사용시간, 시간 제한, 프로세스 넘버
- I/O status information : 프로세스에 할당된 I/O device
프로세스 스케쥴링
멀티프로그램의 목적은 CPU 사용 효율을 최대화입니다. 한 CPU 코어당 하나의 프로세스를 실행할 수 있는데, time sharing은 CPU에 할당된 프로세스를 빈번하게 교체해서 CPU가 쉬는 시간을 최소로 만듭니다.
1. 스케쥴링 큐
Ready queue
프로세스가 시스템에 들어가면 CPU 코어를 할당받기 위해 레디큐에 저장됩니다. 레디큐는 연결리스트로 head가 처음 실행될 프로세스의 PCB를 가르킨다. 그리고 각 PCB는 다음 실행될 프로세스의 PCB를 가리키는 포인터를 가지고 있습니다.
Waiting queue
I/O device에서 입출력을 처리하는 것보다 CPU에서 프로세스를 처리하는 것이 훨씬 빠르기 때문에, 프로세스가 I/O request를 기다릴 때 CPU를 점유하고 있는 것은 비효율적입니다. 프로세스가 종료된 것은 아니지만 재실행을 기다리고 있고 당장은 CPU를 점유받을 수 있는 상태는 아닐 때 waiting queue에 들어갑니다. I/O 요청작업이 끝나면 레디큐에 들어갈 수 있습니다.
2. 스케쥴러
프로세스의 실행 순서를 정합니다. 스케쥴러는 각각의 자원별로 어떤 프로세스를 실행하고, 또 그 다음에 어떤 프로세스를 실행하고 얼만큼 실행시간을 할당할지를 정합니다.
short-term scheduler
cpu 스케쥴러입니다. 짧은 시간 단위로 스케쥴링하기 때문에 short term scheduler라고 부릅니다. 밀리세컨드 단위로 변환이 굉장히 잦고, 어떤 프로세스에게 CPU를 건네줄지 결정하는 스케쥴러입니다.
Long-term scheduler
어떤 프로세스에게 얼마만큼 메모리를 할당할지 결정하는 스케쥴러입니다. 다른말로 job 스케쥴러라고 부릅니다. 프로세스가 시작할 때 new 상태에서 ready상태로 넘어간다고 설명했습니다. 상태도에서 admit이라는 던어가 등장했는데, 이는 프로세스를 메모리에 올려서 레디큐에 옮겨 CPU를 얻을 수 있는 상태로 만드는 것을 의미합니다. long term scheduler가 결정하는 일인데, 시작 프로세스 중 어떤 프로세스를 레디큐에 보낼지에 대한 문제를 다룹니다.
degree of multiprogramming. 메모리에 프로그램이 몇개 올라가 있는가를 나타내는 지표입니다. 메모리에 만약 너무 많은 프로그램이 올라가면 전체적인 성능이 안 좋아집니다. 각 프로그램에 필요한 부분이 메모리에 올라가 있지 않을 수도 있어서 I/O 요청이 너무 빈번하게 일어날 수 있습니다. 하지만 너무 적은 프로그램이 올라가도 성능이 안 좋아질 수 있는데 만약 극단적으로 프로세스가 하나만 올라가 있다고 한다면 그 프로세스가 I/O 작업을 요청하면 CPU가 놀고 있는 상태가 될 수 있습니다. 그래서 degree of multiprogramming을 제어하는 것이 중요한데 사실은 실제로 대부분 시스템에서 long term scheduler를 사용하고 있지는 않습니다. 사실 100개의 프로그램을 실행하면 100개가 메모리에 올라가는 것이 대부분입니다.
Medium-term scheduler
프로그램을 실행시키는대로 메모리에 올리면 문제가 될 수 있어서 swapper가 조절합니다. swapper는 일부 프로그램을 골라서 메모리에서 통째로 쫓아내는 방식으로 프로세스의 수를 조절합니다. 어쨌든 일단 메모리에 프로그램을 올려놓고 너무 많은 프로세스가 올라와서 성능이 안좋아질 것 같으면 그때 medium term scheduler가 프로세스를 쫓아내는 방식으로 degree of multiprogramming을 조절하는 것입니다.
3. 컨텍스트 스위칭(Context Switching)
CPU는 굉장히 빠른 자원이기 때문에 프로세스가 CPU를 장악해서 계속 쓰는 것이 아니라 짧은 시간 간격으로 CPU를 얻었다가 뺐었다가 프로세들끼리 경쟁합니다. 그 때 CPU를 뺒기고 다시 CPU를 얻으면 처음부터 실행하는 것이 아니라 컨텍스트를 기억해놨다가 그 시점부터 다시 실행할 수 있습니다.
즉 CPU가 사용자 프로세스 하나로부터 또 다른 사용자 프로세스에게 넘어가는 과정을 컨텍스트 스위칭(context switching)이라고 부릅니다.
어떤 프로세스가 인터럽트로 CPU를 빼앗기면 여러 프로세스에 대한 정보를 지우는 것이 아니라 다음에 다시 CPU를 얻었을 때 그 컨텍스트부터 실행을 재기하기 위해 레지스터에 저장돼 있던 값을 그 프로세스의 PCb에 저장합니다. PCB는 커널의 주소 공간에 저장하고, 프로세스 B로 CPU를 넘길 때는 커널의 주소 공간에 저장된 프로세스의 PCB를 하드웨어에 복원하고 CPU를 넘깁니다.
프로세스를 교체하는데 걸리는 시간은 그 동안 시스템에서 유용한 일을 하지 않기 때문에 순수하게 오버헤드입니다. 지연되는 시간은 하드웨어에 따라 다르지만 레지스터 셋이 많을수록 오버헤드가 작습니다.
register set일 때 $P_{0}$에서 $P_{1}$으로 컨텍스트 스위칭을 하는 상황을 가정합니다. 이 때, 인터럽트가 발생하면, $P_{0}$의 PCB0을 메모리에 복사하고, $P_{1}$의 PCB1을 register set에 복사합니다. 그리고 $P_{1}$이 종료되면 $P_{0}$를 메모리에서 다시 register set으로 복사합니다.
register set이 여러개인 경우, 위 같은 메모리에 복사하는 시간을 생략하고 단순히 포인터를 실행하고자하는 프로세스로 바꿔주면 됩니다.
Operation on process
1. 프로세스 생성
프로세스 생성에 여러 모델이 있는데 가장 흔한 방법은 보통 복제 생성입니다. 부모 프로세스가 자식 프로세스를 만드는데, 원칙적으로 자원을 복사할뿐이고 자원을 복사하지 않습니다. 자식프로세스를 만들고 나면 별도의 프로세스기 때문에 서로 경쟁하는 사이가 돼서 각자 cpu와 메모리를 더 많이 경쟁하려합니다.
유닉스의 예시로는 fork() 시스템콜을 사용해서 새로운 프로세스를 생성합니다. 부모 프로세스를 그대로 복사하고 주소 공간을 할당합니다. 복제 생성한 뒤 그대로 사용할 수도 있고 exec() 시스템콜을 통해 완전히 새로운 프로그램을 실행할 수도 있습니다.
fork() 시스템콜 예시
#include <stdio.h>
#include <unistd.h>
#include <wait.h>
int main()
{
int pid;
pid = fork();
if (pid == 0)
printf("I am a child \n");
else if (pid > 0)
printf("Hello, I am parent!\n");
}
fork()를 통해 운영체제가 프로세스를 복제하면 부모 프로세스와 자식 프로세스를 구분할 수 있어야 합니다. fork()의 리턴 값으로 구분할 수 있는데 자식 프로세스의 pid를 반환합니다. 그래서 부모 프로세스의 fork()의 결과값은 양수, 자식의 fork()는 결과값으로 0을 받습니다.
자식 프로세스는 fork() 이후로 실행합니다. 왜냐하면 fork()를 통한 복제 생성은 부모 프로세스의 컨텍스트를 그대로 복사하기 때문인데, fork()이후 부모 프로세스의 PC는 fork()다음 문장을 가르키고 있었을 것이고 자식프로세스의 PC 또한 마찬가지로 fork()다음 문장을 가르키고 있을것입니다.
아래는 fork() 이외 다른 시스템콜입니다.
exec()
프로세스를 완전히 새로운 프로세스로 바꿉니다.
wait()
프로세스 A가 wait() 시스템 콜을 호출하면 커널은 자식 프로세스가 종료될 때까지 프로세스 A를 block시킵니다. 자식 프로세스가 종료되면 커널이 프로세스 A를 다시 깨우고 ready 상태로 만듭니다.
exit()
프로세스를 종료시킵니다. exit()을 통해 자발적으로 종료할 수 있습니다. 프로그램에 명시적으로 적어주지 않아도 main함수가 리턴되는 위치에 컴파일러가 자동으로 넣어줍니다. 비자발적인 종료로는 부모 프로세스가 자식 프로세스를 강제로 종료시키는 경우가 있는데, 자식 프로세스가 한계치를 넘는 자원을 요청하거나, 자식에게 할당된 프로세스가 필요하지 않게된 경우입니다.
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
printf("%d",pid);
if (pid < 0) {
fprintf(stderr, "Fork Failed");
return 1;
}
else if (pid == 0) {
execlp("/bin/ls", "ls", NULL);
}
else {
wait(NULL);
printf("Child Complete\n");
}
return 0;
}
위 코드를 실행하면 해당 폴더에서 ls를 실행하고, Child Complete가 출력됩니다.
pid = fork();에서 자식 프로세스가 생성되는데 이 때 pid는 0이상 값이므로 else문이 실행되고 wait(NULL)에서 부모 프로세스는 waiting queue로 들어갑니다.
다시 pid=fork(); 이후로 자식 프로세스가 실행되는데, 이 때 pid는 0이므로 exclp()가 실행됩니다. bin/ls의 ls가 실행되고 자식 프로세스는 종료됩니다.
그리고 부모 프로세스가 재실행돼서 Child Complete를 출력하고 부모 프로세스도 종료됩니다.
프로세스 통신
프로세스는 각자의 주소 공간을 가지고 수행하기 때문에 원칙적으로는 하나의 프로세스는 또 다른 프로세스에 영향을 주지 못합니다. 하지만 여러가지 협력 메커니즘을 통해 하나의 프로세스가 다른 프로세스에 영향을 미칠 수도 있습니다. 그러한 프로세스 간 협력 메코니즘을 IPC (Interprocess Communication)이라고 부릅니다. IPC에는 대표적으로 shared memory와 message passing이 있습니다.
message passing
프로세스끼리 메모리나 변수를 일체 공유하지 않고 커널이 메신저 역할을 해줍니다. 통신하려는 프로세스의 이름을 명시적으로 표현할 수도 있고, 같이 공유하는 mailbox라는 message queue에 메세지를 읽고 쓸 수도 있습니다.
shared memory
원래는 프로세스가 독자적인 메모리 공간을 가지고 있어서 자기 메모리에만 접근할 수 있는데, 일부 공간을 두 프로세스가 공유할 수 있게 두기도 합니다. 위 그림과 같이 물리적인 메모리에 커널과 프로세스 A와 B가 올라가 있고 A와 B는 공유하는 영역이 있습니다.
Reference
2. 쉬운코드
3. https://www.tutorialspoint.com/operating_system/os_process_scheduling.html
4. Operating System Concepts 10th edition
'CS > 운영체제' 카테고리의 다른 글
[OS] 인터럽트, 시스템 콜, 커널모드, 사용자모드 (0) | 2023.02.03 |
---|---|
[OS] 뮤텍스, 세마포어, 모니터 (0) | 2023.01.18 |
[OS] 동기화 문제, Peterson's solution, 하드웨어 명령어 (0) | 2023.01.15 |
[OS] CPU scheduling (0) | 2023.01.11 |
[OS] Threads & Concurrency (0) | 2023.01.11 |