프로세스는 부모프로세스가 자식프로세스를 생성함으로써 만들어지게 된다. 보통의 경우 복제생성이며 부모는 여러 자식을 만들 수 있어 트리 계층 구조를 형성하게 된다.
프로세스는 운영체제로부터 받는 자원을 필요로 하는데, 자원을 부모와 공유하는 모델도 있고, 그렇지 않은 모델도 있다.
원칙적으로는 자원을 공유하지 않고 부모가 자식을 생성하면 서로 CPU를 두고 경쟁하는 관계가 된다.
프로세스가 종료된다. 프로세스가 마지막 명령을 수행한 후 운영체제에게 종료된다는 것을 알려준다.
C언어 프로그램에서도 메인 함수 리턴 부분에 컴파일러가 exit()
시스템 콜을 자동으로 넣어주게 된다. (비명시적인 경우이다.)
또한 프로세스가 종료될 때 부모 프로세스에게 output data를 보내고 프로세스의 각종 자원들을 운영체제에게 반납한다.
부모 프로세스가 자식의 수행을 종료시키는 경우이다.
1. 자식이 할당 자원의 한계치를 넘어서는 경우(자식 프로세스가 자원을 펑펑 쓴다)
2. 자식에게 할당된 태스크가 더 이상 필요없는 경우
3. 부모프로세스가 종료(exit)되는 경우
1,2,3 과 같은 경우에 abort
된다.
프로세스는 복제 생성된다. 부모 프로세스가 자신과 똑같은 자식 프로세스를 생성하게 된다.
여기서 복제 생성이란? 프로세스의 문맥을 모두 복사하는 것을 말한다. 부모프로세스의 주소공간인 code, data, stack과 Program Counter를 그대로 복사하여 자식 프로세스를 만든다.
프로세스 하나가 만들어지게 되면 부모 프로세스와 별개로 독립적인 프로세스이기 때문에 자원을 차지하기 위해 서로 경쟁하는 것이 원칙이다. 그러나 경우에 따라 부모와 자식 프로세스가 자원을 공유하는 경우도 있다.
리눅스와 같이 효율적인 운영체제에서는 가능한 공유할 수 있는 것들을 공유하려한다. 이와 같은 경우 자식은 부모의 주소공간을 공유하고 PC만 copy(복제 생성)해서 가지고 있는다. 이렇게 처음부터 모든 프로세스의 문맥을 다 copy하기 보다는 write(원래 있던 내용이 바뀌는 상황. write가 일어나기 전에는 내용이 같아서 주소공간을 그대로 공유해도 문제가 되지 않는다.)가 발생했을 때 그 때 필요한 부분만을 copy한다.
보통 자식 프로세스는 복제 생성된다. 자식은 부모의 공간을 복사한다. (binary and OS data(운영체제에 있는 데이터들 : PCB, 자원 등))
일단 복제된 곳에 새로운 프로그램을 덮어씌워 올릴 수 있다.
단, 운영체제를 통해서만 자식 생성이 가능하다. (따라서 앞으로 등장할 fork, exec도 시스템콜이다)
유닉스의 예를 들어보자면,
다음부턴 Process Management 관련 각종 시스템 콜에 대해 알아보자.
부모 프로세스를 복제 생성하여 자식 프로세스를 만든다.
완전히 새로운 프로세스로 덮어씌운다.
자식이 종료될 때까지 부모가 기다리게 한다.
종료할 때 쓰는 시스템 콜로, 모든 자원을 반납하고 부모에게 죽는다는 것을 알려준다.
각각에 대해 자세히 알아보자.
fork()
는 운영체제에게 새로운 프로세스를 하나 만들어달라는 요청이다.
위 그림은 fork()
를 사용해서 사용자 프로그램이 프로세스를 만드는 코드이다.
위와 같이 자식 프로세스인 경우 pid가 0, 부모 프로세스인 경우 pid가 양수로, pid가 다르기 때문에 부모 프로세스와 자식 프로세스가 각각 다른 일을 하도록 하게 할 수 있다.
따라서 위의 코드에서 부모 프로세스는 "Hello, I am parent!"
를 출력하고, 자식 프로세스는 "Hello, I am child!"
를 출력하게 된다.
위의 코드에서는 어떻게 동작할까? 부모프로세스는 "Hello, I am child!"
와 Hello, I am parent!
를 순서대로 출력하게 된다. 하지만 자식 프로세스의 경우 "Hello, I am child!"
만 출력하게 된다.
그 이유는, 자식 프로세스는 부모의 PC값을 복제하기 때문에 pid=fork();
이후부터 실행하게 되기 때문이다.
exec()
시스템 콜은 어떤 프로그램을 완전히 새로운 프로세스로 거듭나게 하는 역할을 해준다.
위의 코드에서 execlp
는 일종의 함수인데, exec() 시스템콜을 하게 한다.
exec 시스템콜을 만나게 되면 지금까지 수행했던 기억을 잊고 새로운 프로그램으로 덮어 씌워진다. (새 인생!👶🏻)
단, 한번 exec하면 되돌아갈 수 없다. 새로 덮어씌워진 프로그램이 끝나면 프로세스는 종료되지, 덮어씌워지기 이전의 상태로 돌아가지 않는다.
꼭 자식을 만들어서 exec해야 하는 것은 아니고, 위의 코드와 같이 자기 자신에서 exec() 시스템콜을 수행할 수도 있다. 이때 위에서 설명했듯 이전의 상태로 돌아갈 수 없기 때문에 위의 코드에서는 execlp
이후의 "Hello, I am parent!"
를 출력하는 코드는 영원히 실행할 수 없다.
💡 execlp() 사용법
exelp("실행시킬 프로그램 이름", "실행시킬 프로그램 이름", "전달할 arg(여러 개일 수 있음, 콤마로 구분)", (char*) 0);
이 코드에서는 1이 출력되고 hello 3이 출력되고 종료된다.
wait()
시스템콜은 보통 자식 프로세스를 만든 다음에 하게 되는데, 자식 프로세스가 종료되기를 기다리면서 blocked되는 것이다.
프로세스 A가 wait() 시스템 콜을 호출하면 커널은 child가 종료될 때까지 프로세스 A를 sleep 시킨다(blocked
상태)
자식 프로세스가 종료되면 커널은 프로세스 A를 깨운다.(ready
상태)
wait 시스템콜을 사용하게 되면 자식과 부모가 경쟁하면서 공존하며 수행되는 모델이 아니라, 자식이 종료될 때까지 부모가 기다리는 모델이 해당된다.(위에서 설명했다)
wait 시스템콜을 하는 대표적인 예는 리눅스에서 프로그램 이름을 입력받는 경우가 있다.
프로세스를 종료시킬 때 호출하는 시스템콜이다.
위 코드에서 상단에 위치한 Hello, I am child!를 출력한 후 exit() 코드로 인해 종료된다.
위와 같이 명시적으로 exit()을 하는 경우도 있고 프로그램이 끝나는 경우 컴파일러가 자동으로 넣어주는 경우도 있다.
exit() 시스템콜은 보통 자발적 종료에 의해 실행된다. 명시적으로 적어주는 경우와 그렇지 않더라도 리턴 위치에서 컴파일러가 넣어주는 경우도 이에 해당한다.
외부에서 프로세스를 강제로 죽여버리는 경우이다.
원칙적으로 프로세스는 독립적이다(Independent process). 서로 자원을 두고 경쟁하는 것이 원칙이다. 프로세스는 각자의 주소 공간을 가지고 수행되므로 원칙적으로 하나의 프로세스는 다른 프로세스의 수행에 영향을 미치지 못한다.
그렇지만 경우에 따라 프로세스 간 협력을 해야 효율적으로 수행할 수 있는 경우가 있다. 협력
이란 서로 정보를 주고 받는 것을 말한다. 이런 프로세스 간 협력 메커니즘(즉, 프로세스 간 정보를 주고받을 수 있는 방법)을 IPC(Interprocess Communication)
이라고 한다.
IPC에는 크게 Message Passing
과 Shared Memory
두가지 방법이 있다.
메시지를 전달하여 정보를 전달하는 방법이다. 하지만 프로세스는 워낙 독립적이기 때문에 메시지를 직접 전달할 수 없다. 따라서 중간에 커널에 메신저 역할을 하여 커널을 통해 메시지를 전달하게 된다.
메시지 패싱을 하는 방법은 또 다시 두가지로 나뉜다.
통신하려는 프로세스의 이름을 명시적으로 표시한다.
Mailbox(또는 port)
를 통해 메시지를 간접 전달한다.
누구에게 보내는지 명시하지 않기 때문에 경우에 따라 아무에게나 전달할 수 있다.
이렇게 Message Passing이 두가지로 나뉘지만 사용자 프로세스끼리 무언가를 전달하는 것은 불가능하고 커널이 필요한 것은 같다. 통신하는 프로세스의 이름을 명시적으로 표시하느냐 아니냐의 차이만 존재한다.
원칙적으로 프로세스들은 주소공간(code, data, stack)을 각각 갖고 있지만 shared memory
의 경우 일부 공간을 공유한다. 물리적인 메모리에 매핑할 때 일부 메모리를 공유하도록 하는 것이다. A가 공유된 공간에 어떤 내용을 적으면 B는 자신의 주소공간에도 포함되어 있기 때문에 바로 사용할 수 있다.
이때도 커널에게 shared memory를 사용하겠다는 시스템콜을 하여 매핑을 해서 share를 하게 하는 과정이 필요하다.
처음에는 커널에게 도움을 받지만 일단 한번 커널이 해주면 사용자 프로세스끼리 정보를 공유할 수 있는데, 그렇기 때문에 두 프로세스는 서로 신뢰할 수 있는 관계여야만 한다.
cf) Thread 간에 협력
Thread의 경우 사실 상 하나의 프로세스 내에서 이뤄지기 때문에 프로세스 간 협력으로 보기는 어렵지만 동일한 프로세스를 구성하는 스레드들 간에는 주소 공간을 공유하므로 협력이 가능하다.
프로그램이 수행된다는 것은 위 그림과 같이 CPU만 사용하는 단계(CPU burst)와 I/O를 실행하는 단계(I/O burst)를 번갈아가면서 수행하는 것을 말한다.
빈도나 길이의 차이는 프로그램마다 다르다. 사람과 interaction하는 프로그램(interactive)의 경우 I/O burst가 빈번히 일어난다(I/O bound job
) (many short CPU bursts).
반면 과학 계산 프로그램처럼 계산 위주의 프로그램의 경우 복잡한 연산을 하게 되는 경우 연산 과정동안 CPU를 오래 쓰기 때문에 I/O burst가 잘 일어나지 않는다(CPU bound job
) (few very long CPU burst).
이런 식으로 컴퓨터 안에는 I/O bound job과 CPU bound job이 섞여있다. 그렇기 때문에 자원을 적절히 쓰기 위해 CPU 스케줄링
이 필요하다. 특히 I/O bound job의 경우 사람과 interaction을 하는 job이기 때문에 CPU를 CPU bound job만 잡고 있으면 I/O bound job과 interaction하는 사용자에게 불편함을 주게 된다. 따라서 CPU를 효율적이고 공평하게 사용하게 하기 위해 CPU 스케줄링이 필요한 것이다.
CPU 스케줄링에는 누구에게 우선해서 줄 것이냐, 그리고 언제 뺏을 것이냐의 이슈들이 중요하다.
누구에게 CPU를 줄지 결정하는 일을 한다. Ready 상태의 프로세스 중에서 이번에 CPU를 줄 프로세스를 고른다.
CPU scheduler는 독립적인 하드웨어나 소프트웨어가 아니라 운영체제 안의 CPU scheduling을 하는 코드이다.
CPU scheduler가 누구에게 넘겨줄지 결정하면 Dispatcher가 그 프로세스에게 CPU를 넘겨준다. 이 과정을 문맥교환(context switch)
라고 한다.
마찬가지로 운영체제 안의 코드이다.
여기서 1, 4는 강제로 빼앗지 않고 자진반납하는 경우(nonpreemptive
)이고 2,3은 계속 쓰고 싶은데 강제로 빼앗기는 경우(preemptive
)이다.
nonpreemptive, preemptive는 자주 등장하는 용어이기 때문에 이 용어를 정확히 알아둘 필요가 있다.
http://www.kocw.net/home/cview.do?lid=b988d89cb0bc07b3
http://www.kocw.net/home/cview.do?lid=3a5437eaa6c9e5b0