일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 스택
- DP
- 브루트포스
- 자료구조
- bfs
- 오에스
- 코딩
- 그래프
- 컴공과
- vector
- cs
- 오퍼레이팅시스템
- 북리뷰
- OS
- Stack
- 구현
- Operating System
- Computer science
- 컴공
- 문제풀이
- 컴퓨터공학과
- 알고리즘
- 개발
- 백준
- c++
- 정석
- coding
- 너비우선탐색
- 코테
- 정석학술정보관
- Today
- Total
Little Jay
[CS] Exceptional Control Flow : New Process - Computer System 3rd Edition by Bryant 본문
[CS] Exceptional Control Flow : New Process - Computer System 3rd Edition by Bryant
Jay, Lee 2022. 5. 23. 22:17Creating New Process
새로운 Process를 만드는 데에는 여러가지 방법들이 있다. Directly하게 Process를 생성하는 경우가 있지만, Cloning과 Replacing 기법을 사용해서 새로운 Control Flow를 지니는 Process를 생성한다.
Clonging하는 것은 말 그대로 복제본을 하나 생성하는 것이다. 이때 생성된 Process를 Child Process라고 부르는데, 이 Process는 Parent Process, 즉 자신을 만든 Parent Process와 PID(Process ID)만 빼고 나머지가 같다. 이러한 Child Process를 만드는 것은 fork() system call을 통해 가능하다. 이를 Process Spawning이라고도 하며, Call Once, Return Twice해준다고도 한다.
Replacing은 Process를 바꿔주는 것이다. 이때 Process를 식별해주는 Process Identifier(PID)를 제외한 Machine Code, data, heap, stack 등 모든 것을 싸그리 바꿔주는 것이다. 이는 execve() system call로 구현이 가능하다. 이때 이를 Call Once, Return Nothing이라고 부르기도 한다.
그렇다면 왜 새로운 Process를 Directly하게 만드는 것이 아니라 Fork하고 Exec하는 방식을 통해 새로운 Process를 만드는 것일까? 아래의 그림을 보자. Linux의 내부 구현을 Process로 정리한 Tree 형태의 구조이다. Linux 내부구조라는 책에서 가져온 사진인데, 출처는 아래에 써놓겠다. 여튼, Linux는 init 프로세스로 부터 모든 API가 만들어지는 구조이다. 이때 API도 하나의 Process라고 생각해본다면, 상위 process는 하위 process를 생성하고, 이는 차례대로 다른 process를 생성하여 결과적으로 프로세스 tree를 형성하게 된다.

Linux에서는 위와같은 Fork-Exec Model을 사용해서 Process의 생성에 관여한다. 다시한번 짧게 정리를 해보자면, fork()는 PID 가 완전히 다른 또 하나의 process가 생성해주는 것이고, 반면에 exec()는 이 결과로 생성되는 새로운 process가 없고, exec()를 호출한 process의 PID가 그대로 새로운 process에 적용이 되며, exec()를 호출한 process는 새로운 process에 의해 덮어 쓰여지게 된다. exec는 그 종류도 다양한데, execv, execl, execve, execle, execvp, execlp 등이 있다. fork와 exec는 둘 다 System Call이라는 것을 기억하자.
본격적으로 들어가자면 pit_t pid; pid = fork()를 하게되면 알아서 새로운 child process가 하나 생기게 된다. 이때 child는 parent process와 PID만 다를 뿐 memory, register, stack, heap 등 모든 state는 동일한 상태가 된다. 이때 fork()를 호출한 pid 변수에는 int형이 저장이 되는데, 이때 0이 return되면 child인 것이다.
이 child process는 parent process와 다른 process이기에 서로 독립적이라고 할 수 있다. 따라서 child process에서 수행되는 코드들은 parent process에게 영향을 주지 않는다. 위에서 state를 복제하고 새로운 process를 만드는 것이므로 가히 독립적이라고 할 수 있다. 아래의 코드를 보면 이해가 될 수 있을 것이다.

이렇게 독립적으로 변수 x를 다루게 된다. 이때 child process와 parent process는 스케줄링의 대상이기때문에 실행순서(execution sequence)를 보장하기 못한다. 즉 scheduling 함수에 의해 결정이 되는 것이지 임의로 조정할 수는 없다. 물론 wait을 통해 컨트롤 할 수 있기는 한데 그것은 뒤에서 배우겠다. 다시 코드로 돌아오게 되면 두 process는 x라는 text를 공유하고 있지만 내부적으로는 서로 다른 operation을 수행하여 다른 결과가 나오고 있다. 모든 것을 복제하지만 다른 address space를 지니고 있다.

이번 예시를 보자. 이때 child process는 어떤 결과를 출력하게 될까? 그리고 parent process는 뭘 출력할까?
//child는
there: x=21
here: x=22
//parent는
here: x=21
위와같은 결과가 나온다.
Process Graph
이같은 결과는 생각하기 힘드므로 Process Graph라는 것을 이용한다. Process Graph는 concurrent한 program에서 실행흐름을 파악하는데 용이하게 사용이 된다. 각 vertex는 execution statement 이고 유향간선은 flow를 의미한다. 간선 위에는 파악하고자하는 variable들을 적어놓을 수 있다. 이 그래프는 DAG이기 때문에 위상정렬을 통해 feasible한 순서를 예측할 수 있다. 맨 처음 있는 코드를 Process Graph로 그려보자면, 아래와 같다. 여기서 볼 수 있는 것은 우리가 feasible한 ordering을 예측할 수 있다는 것이다. 아래에 알파벳으로 순서를 써놓았는데 a - b - c - e - d - f 같은 순서는 feasible할 수 있지만 a - b - c - e - f - d 같은 순서는 infeasible하다.

책에는 재미있는 예시들이 많이 나온다. 그 중에 수업에서 소개한 두 가지 코드들을 소개해보자 한다. 저작권에 위배될까봐 배용은 조금 바꾸었다.
void book_fork1() {
printf("HI, \n");
fork();
printf("good \n");
fork();
printf("morning! \n");
}

이런 Process Graph를 그린다면 순서를 쉽게 예측할 수 있다. 따라서 moring! 이 먼저 4번 나오고 good이 2번 나오는 순서는 있을 수 없으며, good이 한번 나오면 morning은 두번 나오는 것이 보장이 되어야 하는등 순서의 흐름을 예측해서 잘못된 결과가 나오지는 않는지 확인할 수 있다. 주목해야 할 점은 fork가 child process 내에서도 호출이 되고 있기 때문에 process의 개수는 2의 n승꼴로 증하가게 된다. 이 점을 주의하길 바란다. 더 자세한 예시는 책을 찾아보고, 답지도 책 뒤에 있기 떄문에 쉽게 확인할 수 있다. 특히 fork가 child에만 nested되면 좀 복잡해지기 때문에 flow를 잘 따라가야한다.
Fork-Exec
앞에서 Linux는 Fork-Exec모델로 실행된다고 언급한 바 있다. Exec는 "Call Once, return Nothing"의 의미를 담고있다. fork는 current process의 복사본을 만드는 반면에 exec는 current process의 code와 address space들을 다른 program을 위해 바꿔놓는 것이다. 이해가 잘 안되겠지만 아래를 보면 쉽게 이해가 될 것이다.

이렇게 exec를 통해서 새로운 program을 실행시킬 수 있다. 실제로 exec를 실행시키면 어떤 것을 return하는 것이 아니기 때문에 child에서 예상이 되는 코드들을 실제로 실행되지 않는다. exec를 실행하기 되면 자체적으로 return이 되기 때문에 호출시점으로부터 그 뒤에 있는 코드들을 dead 된다.
int main() {
fork();
printf("HI, \n");
fork();
printf("good \n");
execl("bin/ls", "ls", (char *)0);
fork();
printf("morning! \n");
}
위에있는 예시를 조금 수정했는데, 이렇게 되면 morning이 printf되지 않는다.
Reference
Randal E. Bryant, & David R. O’Hallaron. (2016). Computer Systems A Programmer’s Perspective Third edition. Carnegie Mellon University: Pearson.