📣
해당 포스트는 운영체제 공룡책과 고건 교수님의 OLC 강의 등을 참고하여
작성되었습니다.
이 포스트는 시스템 콜을 이해하고 있다는 것을 바탕으로 설명되기 때문에
시스템 콜이 뭔지 모르신다면 꼭 보고 오시는 걸 추천드려요!
[운영체제] System Call(시스템콜)을 파보자!
📣 해당 포스트는 운영체제 공룡책과 고건 교수님의 OLC 강의 등을 참고하여 제 나름대로 작성되었습니다. 시스템콜을 한마디로 정리하면 커널에 접근하기 위한 인터페이스입니다. 하지만 시스
hasensprung.tistory.com
우리가 아래 영상처럼 달력 프로그램에서 이벤트 생성을 눌러 하나의 팝업창을 띄워도,
그 이전 화면인 달력을 움직일 수 있습니다.
이는 하나의 프로그램이 아마 여러개의 프로세스로 돌아가기 때문이에요.
(아이폰이었다면 멀티 스레드 앱이라는게 확실하지만..)
하나의 프로세스만 돌아간다면, 저 창에서만 반응하고 저 밖의 창은 반응하지 않았을거에요.
그 하나의 프로세스가 저 약속 생성 작업만을 처리하고 있을테니까요.
[새로운 이벤트] 버튼을 누르는 순간 현재 프로세스에서 새로운 프로세스를 생성한다면,
부모 프로세스는 저 캘린더의 작업을 담당하고, 자식 프로세스는 약속 생성 작업을 담당할 수있겠죠?

1. PCB (Process Control Block)
fork()를 이해하기 위해서는 먼저 PCB에 대해서 알고 있어야 해요!!
PCB란 무엇이냐!!
한마디로 정리하면
프로세스에 대한 메타데이터가 들어있는 자료구조입니다!
어떤 메타데이터가 들어있을까요??
PID, 우선순위, 상태, 메모리 어디에 올라와있는지, 실행 시간 등등
현재 프로세스에 대한 대부분의 정보들이 다 들어있어요!
저번 포스트에서 시스템 콜을 다루면서,
프로세스는 언제든 CPU를 커널에게 뺏길 수 있다는 걸 봤죠?
시스템콜 인터럽트 때문에 바로 커널로 빼앗기고 CPU ready list로 가게 되잖아요?
만약에 더 높은 우선 순위가 다른 프로세스에게 있다면, 다른 프로세스에게 CPU를 뺏기게 됩니다!!
CPU가 프로세스에서 다른 프로세스로 바로 넘어가는 건 불가능합니다! 나중에 설명할 프로세스 스케줄링을 커널이 하기 때문이죠!
반드시 커널로 넘어가고 커널은 우선순위에 따라서 CPU를 다른 프로세스에게 넘겨줍니다!
CPU를 넘겨준다, 프로세스가 CPU를 받는다 모두 앞으로 프로세스가 CPU로 디스패치된다고 할게요!
근데 CPU를 뺏길때 프로세스가 어디까지 실행했었는지 뭘 하고 있었는지 등등의 환경과 리소스들을 기록해줘야겠죠?
그래야 다음에 다시 차례가 되었을 때 마치 CPU를 빼앗기지 않았던 것처럼 실행할 수 있잖아요???
그 마지막 상태를 커널에 있는 해당 프로세스의 PCB에 저장하는 겁니다!!
PCB에는 요런 정보들이 들어간다고 합니다!
참고로 읽어보셔용~

자 이제 PCB에 대해서 아셨으면, fork()를 뽀개버릴 준비가 다 되신겁니다!

2. fork 시스템 콜 (Child Process 생성)
Unix와 Window의 자식 프로세스 생성에 대해서 알아볼게요.
먼저 운영체제에서 새로운 자식 프로세스를 생성하기 위해서는 fork()라는 시스템 콜을 사용합니다.
fork()는 자식 프로세스를 생성해주는 시스템콜입니다.
시스템 콜을 만나게 되면, 커널모드로 넘어간다고 했죠??
커널에서는
"앗! 새로운 자식 프로세스를 만드라는 시스템콜이다!!"
이러면서 새로운 프로세스인 자식 프로세의 PCB 공간을 할당해주고,
부모 프로세스의 PCB를 그대로 Copy해서 넣어줍니다!
어 왜 새로운 프로세스인데 부모 프로세스를 그대로 copy하지???
이건 좀 이따 설명할게요! 일단 넘어가죠ㅎㅎ
커널에서 만들어진 자식 프로세스는 CPU를 얻기 위해 Read List에 줄을 섭니다.
CPU는 커널 작업을 처리했지만, 바로 부모 프로세스로 돌아와서 하던 작업을 마무리하게 되고요!!
부모 프로세스가 종료되거나, 혹은 프로세스 우선순위에 밀려 뺏기게 된 이후에 자식 프로세스에게 디스패치 차례가 오는거죠!
자 이제 코드를 보면서 한번 정리해볼게요!
이런 코드가 있습니다. 어떤 순서로 printf문이 실행되게 될지 한번 생각해보세요!!
#include <stdio.h>
#include <unistd.h>
int main(void) {
int pid;
printf("start!\n");
pid = fork();
printf("who are you?\n");
if (pid == 0) { // 자식 프로세스라는 뜻
printf("I am child\n");
/* 자식이 할 일을 한다 */
} else { // 부모 프로세스라는 뜻
printf("I am parent!\n");
/* 부모는 이제 다른 일을 한다. */
}
}
정답은...!
start!
who are you?
I am parent!
who are you?
I am child
입니당!!
도대체 어떻게 돌아가는건지 당연히 아직 모르실 수 있어요! 왜냐면 설명을 다 안했거든요^^
그래서 저 코드를 바탕으로 하나씩 설명해드릴게요!
먼저 start!가 출력되겠죠?? 이건 프로세스 생성과 상관없이 당연히 출력이 되는거고
fork() 부터 자식프로세스를 만드러 커널을 다녀오게 됩니다! 다시 말하지만 시스템콜이니깐!
그리고 부모 프로세스로 돌아와서 마저 일을 하게 되죠
다음 줄인 who are you? 를 출력합니다.
그리고 다음 줄에서는 분기처리되어서 I am parent! 를 출력하고 부모 프로세스는 이제 더 이상 할 일이 없으니까 종료됩니다!
자 그다음에 커널은 자식 프로세스를 CPU 디스패치합니다.
근데 아까 뭐라고했죠?? 자식 프로세스는 부모의 PCB를 그대로 카피한다고 했죠?
프로그램 카운터도 그대로 똑같이 가지는거죠!!
그 말인즉슨, 부모 프로세스가 자식프로세스를 생성했을 때의 마지막 실행문인 fork()의 다음 줄 부터 실행하는겁니다.
그래서 who are you?를 실행하게 되는거죠!!
그 다음은 분기처리로 들어가서 I am child를 출력합니다. 그리고 자식 프로세스도 종료가 되겠죠?
그 후에는 부모 프로세스에 나 끝났어요! 하는 시그널을 보냅니다.
근데 보통... 자식 프로세스는 만들어진 다음에 exec()를 실행해서 다른 프로그램을 실행합니다..ㅎㅎ
아까 달력 프로그램에서는 약속을 생성하는 프로그램을 실행하는거죠!
다음 코드 처럼요!
다음 프로그램으로 넘어감! 부분을 보시면 됩니다.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int pid;
printf("start!\n");
pid = fork();
printf("who are you?\n");
if (pid == 0) {
printf("I am child\n");
execlp(); // 다른 프로그램으로 넘어감
} else {
printf("I am parent!\n");
/* 부모는 이제 다른 일을 한다. */
}
}
이런 경우에는 기존에 자식 프로세스가 가지고 있던 이미지가 덮어씌워집니다. 자식 프로세스가 그 새로운 프로그램으로 탈바꿈하는거죠!
운영체제에서 이미지는 프로그램을 실행하기 위한 코드, 데이터, 리소스를 포함한 스냅샷을 의미합니다.
즉, 상태값을 가지고 있어서 언제든지 바로 실행될 수 있는 준비된 상태인것이죠!
자 정리하자면,
step1 - PCB 공간을 만들어주고 초기값을 주어야 한다. 초기값은 Parent를 copy해온다.
이를 통해 parent의 실행환경을 복사하고, 사용하던 리소스 (터미널, 키보드, 스크린 등)을 쉐어하는 것이다.
step2 - 자식 프로세스가 사용할 메모리 공간 확보, 그리고 그 공간을 초기화 한다. parent 이미지를 그대로 copy한다.
즉, parent와 child는 똑같은 코드 (이미지)를 가지게 된다.
step3 - child를 Ready List에 대기시킨다. (cpu에 줄서있으라고 하는거다.. CPU는 여전히 parent가 돌아와서 쓰고 있다.)
step4 - 언젠가 child 프로세스가 CPU로 디스패치 됩니다.
만약 exec(디스크로부터 새 이미지를 읽어오는 것)를 실행한다면?
step5 - 그 새로운 Image를 가지고 와서 덮어씌운다. 이제 child 프로세스는 그 이미지인 것이다.
step6 - 자식이 할일을 다 마치고 종료되면, 부모에게 시그널을 보낸다!
만약 exec(디스크로부터 새 이미지를 읽어오는 것)를 실행하지 않는다면?
step5 - 그냥 쭉 부모와 같은 코드를 실행한다. (물론 pid로 분기처리되서 자식은 다른 일을 할 수 있다!)
step6 - 자식이 할일을 다 마치고 종료되면, 부모에게 시그널을 보낸다!
이미지가 덮어씌워진다는게 여전히 헷갈리실 수도 있어요!
만약에...!
exec()를 한 이후에 다른 일이 더 있으면 어떻게 될까요??
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int pid;
pid = fork();
if (pid == 0) {
execlp(); // 다른 프로그램으로 넘어감
printf("exec 이후에 다른 일~~\n");
} else {
/* 부모는 이제 다른 일을 한다. */
}
}
저 exec 이후에 다른일이라는 친구는 실행될까요??
정답은 NO! 입니다!!
exec를 사용하여 이미지가 덮어씌워지면 아예 그 덮어씌워진 프로그램이 되어버리는거에요!!ㅎㅎ
3. Wait 시스템 콜
근데 아까... 자식 프로세스가 끝날 때 자기가 끝난다고 부모 프로세스한테 시그널을 보낸다고 했죠?
좀 이상하지 않아요..?
부모 프로세스는 먼저 실행되서 이미 끝나고 없는데 ....? (공포)

이런 경우에 자식 프로세스는 고아 프로세스가 됩니다.
사실 부모 프로세스가 종료되면, 당연히 자식 프로세스도 종료되어야 하잖아요??
달력 프로그램이 종료되면 약속 생성하는 창도 종료되어야 하는 것처럼요!!!
만약 종료되지 않고 계속 실행되고 있으면, 메모리 누수나 리소스 누수,
혹은 필요없는 프로세스로서 시스템 자원을 잡아먹고 있는 문제아(..ㅎㅎ)가 될 수 있습니다!
그래서 부모 프로세스는 자식 프로세스가 끝나기를 기다려야 합니다!
그때 사용하는 시스템콜이 wait() 이고요!!
wait 시스템 콜을 사용하면, 여타 시스템 콜처럼 당연히 CPU가 커널로 넘어갑니다!
그리고 커널은 wait을 사용한 부모 프로세스를 Sleep 시키고 CPU에게 ready list에 있는 다른 작업들을 맡깁니다.
Sleep은 자식 프로세스가 끝나서 시그널을 날릴 때까지 유지되고,
그 이후에는 cpu ready list에서 차례가 돌아오길 대기하게 됩니다!!
마지막으로 정리해볼게요!!
아주 설명이 잘된 그림이 있어서 가져왔습니다. (고건 교수님 강의 ppt에요!)
한단계씩 설명할게요! 그림과 함께 보시면서 따라오시면 됩니다.

1. fork 시스템콜을 통해, 커널로 가서 fork 시스템콜을 처리한다.
2. 커널은 pcb 공간을 만들고 자식 프로세스를 생성한 후 부모의 pcb를 복사해서 넣어준다.
3. 부모 프로세스가 다시 CPU로 디스패치되어서 작업을 마저 진행한다
4. wait시스템콜을 통해 커널로 간다. 커널에서 wait 시스템콜을 처리한다.
5. wait 시스템콜 터리를 하면서 context switch를 한다. 왜? 이제 저 프로세스는 자식이 끝나기 전까지 잠들어있어야 하니까!
context_switch 는 세 단계로 나뉩니다!
1. 현재 CPU state vector를 해당 PCB에 저장합니다
2. 기존 프로세스를 Sleep합니다.
3. 새롭게 돌아갈 프로세스의 PCB를 load합니다.
6. 그리고 자식 프로세스는 차례가 오면 CPU로 디스패치된다.
7. 여기서는 exec를 실행했다. ls라는 binary 파일을 실행했다! exec 역시 시스템 콜이기 때문에 커널로 들어간다.
8. 디스크에서 ls 파일을 찾아서 가져온다.
이제 다음 페이지로 넘어갈게요~!!

9. 자식프로세스의 이미지가 교체된다. 이제 자식프로세스는 ls 를 실행하는 프로세스가 되었다!
10. 이제 커널에서 자식프로세스로 넘어간다. 넘어가고 났더니 ls 파일의 main부터 시작된다.
11. ls 작업을 수행한다.
12. 다 하면 exit을 하고
13. 부모프로세스에게 알린다.
14. 그럼 부모프로세스는 다시 대기열에 들어가서 cpu를 받은 후 종료한다!
휴! 여기까지 깔끔하죠?? 이제 조금 더 나아가볼까요??
4. 자식 프로세스를 생성할 때 오버헤드
제목이 좀 거창하죠?
별건 아니고 바로 위 정리한 순서에서 2번 있잖아요??
2. 커널은 pcb 공간을 만들고 자식 프로세스를 생성한 후 부모의 pcb를 복사해서 넣어준다.
이게 어떤 방식으로 되는지, 그리고 왜 그런 방식으로 되는지 설명하려고 해요!
부모의 PCB 가 복사한다고 했죠?
그리고 부모의 코드와 리소스, 데이터. 즉 이미지 전체 가 복사가 됩니다.
당연히 PCB를 복사하는 오버헤드보다 이미지 전체를 복사하는 오버헤드가 더 크겠죠??
근데 아까 대부분 자식프로세스에서 다른 프로그램을 실행하는 걸로 쓰이고,
그 새로운 프로그램의 이미지가 자식 프로세스에 덮어쓰워져서 기존의 것이 하나도 안남는다고 했죠??
대부분 자식 프로세스의 대부분이 새 프로그램을 실행시킨다면 왜 굳이 오버헤드를 감당하면서 이미지 전체를 복사하는 것일까요??
대부분이라고 했지 전체라곤 안했으니까...ㅎㅎ

새로운 프로그램을 실행하지 않고, 기존의 프로그램에서 자식프로세스로 똑같은 프로그램을 원하는 경우도 많습니다!
예를 들면, 커널에서 네트워크를 처리하기 위한 (자식프로세스인) 데몬을 만든다던지,
MS워드를 쓰는데 사용자가 입력한 글자의 맞춤법 검사를 즉시 즉시 해내야 한다던지 같은 경우에요!
모두 해당 프로그램 그대로, 그 위에서 다른 역할을 수행하는거죠? 다른 프로그램 실행 없이요!!
사실 Linux는 Clone을 통해 이걸 좀 더 멋지게 해결하지만..! 나중에 다루도록 할게요!
왜 이미지 복사를 하는지는 이해하셨죠??
그럼 이제 PCB 복사를 볼텐데!! 이 친구가 아주 재밌습니다ㅎㅎㅎ
일단 Unix는 PCB를 그냥 그 자체를 복사합니다!
근데 자식프로세스가 만들어지면 바로 복사하느냐!
아닙니다!ㅎㅎ
엥?? 아까 복사한다면서..!
근데 사실 바로 복사하지 않고 일단 부모의 PCB를 그대로 공유해서 사용해요..!!
왜냐면 자식 프로그램이 다른 프로그램으로 바로 넘어가버릴 수도 있으니까!!
그러면 굳이 PCB를 또 복사해줄 필요가 있을 필요가 있나 싶은거죠! 어차피 다른 프로그램으로 넘어갈거!!
그리고 다른 프로그램이 없더라도 만약에 부모코드와 똑같이 작업하고 바뀔게 없는 코드라면, 굳이 PCB를 바꿔주지 않아도 되겠죠??
이런 상황들 때문에 자식 프로세스가 생성될 때는 PCB를 복사하지 않고,
부모 프로세스나 자식 프로세스에서 write작업이 실행될 때 그 때 복사를 해서 각각 자신의 PCB를 가지게 합니다.
이걸 COW(Copy of Write) 라고 합니다...!!!
오 근데... 책을 읽다보니까 부모 프로세스에서 write 작업을 수행할 때마다 COW가 된다고 하는데...
이게 왜 이렇게 되는지 도저히 모르겠더라고요...! 아무리 찾아도 명쾌한 답이 나오지 않네용ㅜㅜ
한번 write되서 COW 하면, 부모와 자식은 다른 복사본을 가져서 그 이후부터는 각각의 PCB를 가지고 있는 프로그램처럼 사용하면 되는거 아닌가요??
왜 write마다 COW가 일어나는지...! 아시는 분은 댓글 부탁드릴게요!!
결론적으로 프로세스는 COW 기법을 통해 PCB 복사에 대한 오버헤드를 줄일 수 있게 됩니다ㅎㅎ
이렇게 해서 프로세스 생성에 대한 이야기를 마쳤는데,
리눅스의 clone()은 PCB를 다 복사하지 않고 바뀐 부분만 복사하고 나머지는 부모의 PCB를 포인터로 가리키고 (LWP)
자식이 다른 프로그램을 실행하는 것에 대한 대응도 부모보다 자식을 먼저 실행시키는 방식을 이용해서 더 효율적으로 돌아가요!
즉, 시스템 수준에서 Clone이라는 명령어를 통해 스레드(LWP, thread)를 만드는 것을 지원하는거죠!
Clone 명령어에 대해서는 다음에 시간이 좀 생기면 포스팅 하도록 하겠습니다!!

긴 글 읽으시느라 수고하셨어요!!!
<참고 자료>
운영체제 10판
운영체제 - YES24
운영체제
www.yes24.com
고건 교수님 OLC
OLC CENTER
olc.kr
'운영체제' 카테고리의 다른 글
[운영체제] 인터럽트(Interrupt) 핸들링을 파보자! (0) | 2023.02.09 |
---|---|
[운영체제] Linux의 CPU 스케줄링 (CFS 알고리즘)을 파보자! (4) | 2023.02.08 |
[운영체제] System Call(시스템콜)을 파보자! (7) | 2023.02.04 |