● 프로세스 생성, 실행, 종료
프로세스는 복제 생성을 하며, 부모 프로세스가 자식 프로세스를 생성해야 하는 구조로 프로세스의 계층은 트리 형태이다.
프로세스가 만들어지면 그 프로세스만의 독자적인 주소 공간(Code, Data, Stack)이 생긴다. fork() 함수를 통해 부모 프로세스를 복사하면 이 주소 공간을 Binary 통째로 복사한다. Program Counter까지 모두 복사되니까 당연히 자식 프로세스는 부모 프로세스가 실행한 부분 (fork() 함수의 다음 줄)부터 실행하게 된다. 이렇게 되면 시스템의 프로세스는 모두 똑같이 동작하게 된다. 그래서 exec()라는 System Call을 통해 프로그램을 덮어 씌워야 한다. 프로세스가 자발적으로 종료될 때에는 exit() System Call을 통해 부모 프로세스에게 종료가 되었음을 알려주고 모든 자원을 반납 후 종료한다.
- fork()함수를 이용해서 부모 프로세스 복제 후, exec()로 프로그램을 덮어 씌운다. 종료시에는 exit()함수를 호출한다.
- fork()와 exec()는 서로 독립적이기 때문에 복제 후 덮어 씌우지 않을 수도 있고, 복제를 하지 않고 기존 프로그램을 exec()로 덮어 쓸 수도 있다.
- fork() 함수의 시스템 콜이 발생해 커널 모드에서 시스템 콜 번호에 해당하는 시스템 콜 핸들러 함수(sys_clone())가 호출된다.
✓ 프로세스는 자원을 필요로 함
- 운영체제로 받는다
- 부모와 공유
✓ 자원의 공유
- 부모와 자식에 모든 자원을 공유하는 모델
- 일부를 공유하는 모델
- 전혀 공유하지 않는 모델
✓ 수행 (execution)
- 부모와 자식은 공존하며 수행되는 모델
- 자식이 종료(terminate)될 때까지 부모가 기다리는 (wait) 모델
● 프로세스 종료
exit()라는 System Call을 하면 프로세스가 종료된다. main() 함수가 끝나면 프로그래머가 exit() 호출을 하지 않더라도 컴파일러가 자동으로 exit() 함수를 System Call 해준다.
프로세스가 종료될 때에는 자식 프로세스가 부모 프로세스에게 알리고 모든 자원을 반납한 후 종료가 된다. 프로세스의 세계에서는 자식 프로세스가 먼저 죽고 부모가 죽어야 한다.
프로세스의 종료에는 자발적으로 종료되는 경우가 있고 비자발적으로 종료되는 경우가 있다.
자발적으로 종료가 될 때에는 exit() System Call을 해주고, 비자발적인 종료가 될 때에는 abort() System Call을 한다.
✓ 비정상적인 종료
- 어떤 자원의 할당치를 넘어선 일을 했을 경우
- 자식 프로세스를 생성한 이유는 일을 시키려고 생성했는데 더이상 시킬 일이 없는 경우
- 부모 프로세스가 종료되는 경우
● 프로세스 System Call 함수
fork()
int main()
{
int pid;
pid=fork();
if(pid==0) // 자식 프로세스이면 ..
printf("\n Hello, I am child!\n");
else if(pid>0) // 부모 프로세스이면 ..
printf("\n Hello, I am parent!\n");
}
운영체제에게 새로운 프로세스를 만들어 달라는 요청을 하기 위해 fork()라는 함수를 사용한다. 보통 함수를 호출하게 되면 그 함수에 해당하는 기능을 생성하고 결과 값을 반환하며 그다음 코드를 수행한다. fork() 시스템 함수도 마찬가지이지만, 기존 순차적인 함수 호출과 다른 점은 fork() 함수가 실행되는 순간 위의 코드와 똑같은 코드, 문맥을 가진 프로세스가 하나 더 생기는 것이다. 문맥 (Context)까지 복사하기 때문에 자식 프로세스도 fork() 다음 코드부터 수행한다. 즉, 부모 프로세스는 fork()를 통해 프로세스를 생성 후 그 아래 코드를 순차적으로 실행하지만, 자식 프로세스는 main()의 시작부터 실행하는 것이 아니라 fork()를 실행한 그 이후 시점부터 실행하게 되는 것이다. 정리하자면, fork()를 통한 복제 생성은 부모 프로세스의 문맥을 그대로 복사하는 것이고, Program Counter가 fork()가 끝난 시점을 가리키기 때문에 자식 프로세스도 fork()가 끝난 시점을 가리키기 때문에 그다음 코드 부분을 실행하게 되는 것이다. 같은 말을 반복하는 것 같긴 하군 ㅎ
fork() System Call을 이용해서 운영체제가 프로세스 복제를 할 때, 자식과 부모를 구분하기 위해서 return value를 다르게 반환한다. 부모 프로세스라면 양수가 얻어지고, 자식 프로세스라면 리턴 값이 0으로 원본과 복제본을 구분할 수 있다.
exec()
fork()를 이용해 복사를 하게 되면 같은 코드를 가지는 프로그램만 존재할 것이다. 실제로는 다른 프로그램들이 실행되어야 하기 때문에 exec() System Call 함수로 프로그램을 덮어 씌워준다.
int main()
{
int pid;
pid=fork();
if(pid==0) // 자식 프로세스이면 ..
{
printf("\n Hello, I am child! Now I'll run date \n");
execlp("/bin/date", "/bin/date", (char*)0); // 새로운 프로그램으로 덮어 씌움
printf("\n Hello, I am parent!\n");
}
else if(pid>0) // 부모 프로세스이면 ..
{
printf("\n Hello, I am parent!\n");
}
}
첫 번째 printf()를 화면에 출력한 후, exec() System Call 함수를 호출해 프로그램을 /bin/date라는 새로운 프로그램으로 덮어 씌운다. date라는 프로그램의 main() 함수를 처음부터 끝까지 실행하기 때문에 exec() 함수 다음에 나오는 printf() 문은 영원히 실행이 불가능하다.
echo라는 명령어는 뒤에 나오는 argument를 그대로 화면에 출력하는 Command이다. c에서의 printf()와 유사하다.
int main()
{
printf("1");
execlp("echo", "echo", "3", (char*)0);
printf("2");
}
첫 번째 printf()인 "1"이 먼저 출력되고 echo라는 프로그램으로 덮어 씌워서 "3"은 출력이 되지만 "2"는 영원히 출력되지 않는다.
wait()
프로세스를 Blocked 상태로 만든다. 오래 걸리는 이벤트를 기다리다가 실행이 완료가 되면 다시 CPU를 얻을 수 있는 Ready 상태로 돌아가게 되는 것이다. 프로세스 A가 wait() System Call을 호출하면 커널은 자식 프로세스가 종료될 때까지 프로세스 A를 sleep 시킨다. 그리고 자식 프로세스가 종료되면 커널은 프로세스 A를 깨워 Ready 상태로 만든다.
부모 프로세스에서 fork()로 자식 프로세스를 생성하면 자식 프로세스도 주소 공간이 만들어지는데 fork()의 결과값이 0이면 자식프로세스가 실행하는 코드이고 0이 아니면 부모 프로세스가 실행을 한다. 부모 프로세스가 실행하는 위치에다가 wait() System Call을 실행시키면 부모프로세스는 sleep상태가 된다. Blocked 상태가 되어 CPU를 얻지 못하고 기다리게 되며, 자식 프로세스가 종료가 되고 나서야 wait() 함수의 다음 코드를 실행한다. 자식이 종료될 때까지 기다리는 모델로 리눅스 터미널에서 어떤 프로그램을 실행시킬 때 &를 사용하지 않고 그냥 실행시켰을 경우에 해당한다.
exit()
프로세스를 종료시키는 과정은 자발적 종료와 비자발적 종료 두 가지가 있다. 프로그램 본인이 스스로 종료시키는 경우가 자발적 종료에 해당한다. 마지막 statement 수행 후 exit() System Call을 통해 종료가 되거나, 프로그램에 명시적으로 적어주지 않아도 main() 함수가 return 되는 위치에 컴파일러가 exit() System Call을 넣어준다. 비자발적 종료는 프로그램 본인은 코드의 어딘가를 실행하고 싶지만 밖에서 프로그램을 죽인 경우로 세 가지로 나눌 수 있다. 첫 번째 경우는 자식 프로세스가 한계치를 넘어서는 자원을 요청하거나 자식에게 할당된 Task가 더 이상 필요하지 않아 부모 프로세스가 자식 프로세스를 강제로 종료시키는 경우이다. 두 번째 경우는 사용자가 키보드로 Kill 명령어나 Ctrl + C와 같은 커맨드를 입력했을 경우이다. 마지막은 부모 프로세스 본인이 종료되는 경우이다. 프로세스의 세계에서는 부모 프로세스보다 자식 프로세스가 먼저 종료되어야 하기 때문에 부모 프로세스가 종료해야 하는 상황이 있다면 자식 프로세스의 흐름과 관계없이 강제적으로 자식 프로세스를 종료하게 된다.
프로세스와 관련한 시스템 콜 4가지
- fork() : 자식 프로세스 복제
- exec() : 새로운 프로세스를 덮어 씌움
- wati() : 자식 프로세스가 종료될 때까지 blocked 상태로 잠들어 있음
- exit() : 모든 자원을 반납한 후, 부모 프로세스에게 알림
● 프로세스 간 협력
원칙적으로 프로세스는 서로 경쟁하는 관계이기 때문에 굉장히 독립적이다. 즉, 하나의 프로세스가 다른 프로세스에 영향을 끼칠 수 없다는 말이다. 하지만 경우에 따라 프로세스들이 협력을 해야지만 더 효율적으로 실행할 수 있는 경우가 있다. 이러한 메커니즘을 Interprocess Communication (IPC)라고 한다. IPC는 Message passing과 Shraed Memory 두 가지 방법이 있다.
- 독립적 프로세스 (Independent process)
프로세스는 각자의 주소 공간을 가지고 수행되므로 원칙적으로 하나의 프로세스는 다른 프로세스의 수행에 영향을 미치지 못한다.
- 협력 프로세스 (Cooperating process)
프로세스 협력 메커니즘을 통해 하나의 프로세스가 다른 프로세스의 수행에 영향을 미칠 수 있다.
프로세스 간 협력 메커니즘 (IPC : Interprocess Communication)
- Massage passing : 커널을 통해 메시지를 전달하는 방법이다.
- Shared Memory : 서로 다른 프로세스 간에도 일부 주소 공간을 공유하게 하는 메커니즘으로 주소 공간을 공유한다.
- Thread : 사실상 하나의 프로세스이므로 프로세스 간 협력으로 보기는 어렵지만 동일한 프로세스를 구성하는 Thread들 간에는 주소 공간을 공유하므로 협력이 가능하다.
Message passing
Message passing 은 프로세스 A가 프로세스 B에게 메시지를 전달하고 그 메시지를 받아 프로세스 B가 실행된다. 프로세스는 독립적이기 때문에 자기 자신의 메모리 주소 공간만 볼 수 있고 원칙적으로는 프로세스가 메시지를 직접 전달할 수 있는 방법이 없다. 이 역할을 운영체제 커널이 해주는 것이다. 커널이 메신저 역할을 해 메시지를 전달해 준다.
Message passing 방법으로 두 가지가 있다. 메시지를 받아 볼 프로세스 명을 명시하냐 명시하지 않느냐로 나뉜다.
Shared memory
프로세스는 원칙상 자기 자신의 주소 공간에만 접근할 수가 있다. 그럼에도 불구하고 Shared memory는 일부 주소 공간을 공유하는 방법이다. 프로세스 A와 프로세스 B를 물리적 메모리에 매핑할 때, 일부 데이터는 공유가 가능하도록 매핑하는 것이다. Shared memory를 하려면 커널한테 Shared memory를 한다는 System Call을 해서 매핑을 해줘야 한다. 커널이 한 번만 진행해주면 그 후로는 사용자 프로세스 둘이서 작업을 한다.