본문 바로가기
학부공부/OS_운영체제

14. Process(3)

by sonpang 2021. 11. 17.
반응형

안녕하세요. 오늘은 process에 대해 알아보는 3번째 시간입니다. 저번 시간에는 Context switch를 중심으로 알아보았는데요. System call도 문맥교환이 필요한 것인지, CISC와 RISC의 Context switch등 추가적인 이슈도 알아보았습니다. 마지막으로 Process state에 대해 알아보며 글을 마무리 하였는데요.

2021.11.15 - [학부공부/OS_운영체제] - 12. Process(2)

 

12. Process(2)

안녕하세요. 오늘은 process에 대해 알아보는 2번째 시간입니다. 저번 포스팅에서는 프로그램과 프로세스, linking에 대해 알아보았는데요. 2021.11.12 - [학부공부/운영체제] - 10. Process(1) 10. Process(1) 안

ku320121.tistory.com

 

이번 포스팅에서는 실제로 프로세스가 어떻게 생성되고 종료되는지에 대해 알아보겠습니다. 프로세스가 프로그램으로부터 어떻게 만들어지는 가에 대해서 기억이 잘 나지 않으신다면 아래 포스팅을 읽어보시는 것을 추천드립니다.

2021.11.12 - [학부공부/OS_운영체제] - 10. Process(1)

 

10. Process(1)

안녕하세요. 오늘은 process에 대해서 알아보는 시간을 가질까 합니다. 저번 포스팅에선 운영체제 내용을 이해하는데 필요한 기초적인 컴퓨터과목 내용을 소개하는 시간을 가졌는데요. 2021.11.08 -

ku320121.tistory.com

 

 

14.01. Process creation

프로세스를 직접 만들어볼까요? 

막막할 것입니다. 말 그대로 한땀한땀 만들어야 하겠죠. 하지만 이미 존재하는 프로세스를 복제할 수도 있을 겁니다. 일종의 양식같은 것을 만들어두고 사용하는 것이죠. 이러한 접근방법이 유효한 것은 PCB와 같이 프로세스를 나타내는 data structure가 정해져있기 때문입니다. 

 

fork() : System call

 

init이라는 프로세스에 대해서 들어보신 적 있으신가요? UNIX기반 운영체제에서 init은 booting 과정 중 최초의 프로세스인데요. 시스템이 종료될 때가지 실행하는 일종의 daemon 프로세스입니다. 다른 모든 프로세스의 직간접적 부모 프로세스이기도 하죠. 여기서 fork()를 호출하는 프로세스는 부모 프로세스가 되고 호출당하여 복제(duplicate)된 프로세스는 자식 프로세스가 됩니다. 여기까지 읽으시고 계층적구조가 떠오르셨다면 훌륭합니다.

 

Daemon process

Multitasking OS에서 사용자가 직접적으로 제어하지 않고, 백그라운드에서 여러 작업을 수행하는 프로그램을 의미한다.

 

Process Creation Hierarchy in Linux

[그림 1] Process Hierarchy in Linux

PC를 booting하면 init이 기다리고 있다가 사용자가 사용할 프로세스를 띄우게 됩니다.(Linux는 shall을 window는 window를 띄우고 사용자가 필요한 GUI op를 할 수 있게 해주겠죠.) 여기서 pagedaemon, swapper는 커널 프로세스이고요. Booting과정에서 이런 커널 프로세스를 다 띄우고 init을 만듭니다. init 프로세스는 어떻게보면 대부분 프로세스의 시조라고 볼 수 있죠. 이렇게 만들어진 부모와 자식은 프로세스 그룹을 형성합니다.(Container에 대해 알고계신 분들은 익숙하실 수도 있겠군요.) 또 부모와 자식 프로세스는 독립적으로 동작하나, 자식이 terminate 될 때 부모는 notification (SIGCHLD)을 받습니다. 사실 부모는 자식이 terminate 될 때까지 wait할 수 있습니다. 어떤 resources는 공유하기도 하죠.(PCB가 처음에 복사되기 때문입니다.) 

 

부모 프로세스가 먼저 exit한다면요?

부모 프로세스가 종료되면 자식 프로세스도 terminate되기 때문에 일반적으로 자식 프로세스가 있다면 부모 프로세스가 기다립니다. 그래서 자식 프로세스가 terminate될 때 부모 프로세스가 notification을 받는 것이죠.

 

init이 하는 일은요?

제가 앞서서 양식(init)을 복사하면 편하다라고 말씀드렸기 때문에 init 프로세스가 빈 프로세스라고 생각하실 수도 있습니다. 정확히는 init도 하는 일이 있습니다. kernel code에 들어가서 보실 수 있고요. 백그라운드 서비스 실행, 시스템 서비스 실행, 네트워크와 파일 시스템 등 초기화, 프로세스 상태 모니터링, 사용자 권한 관리, 알람(log를 모아 저널링), 부모 프로세스가 죽어 고아가 된 프로세스 부모(프로세스가 종료되어 좀비(?)상태가 되면 해당 프로세스가 가진 자원을 반환시키는 역할도 한다는군요.) 등의 역할을 수행한다. 물론 혼자서 수행하진 않고 시스템 서비스들의 도움을 받아서 수행하죠.

 

Hierarchy가 바뀔수도 있나요?

No. 기본적으로 부모 프로세스가 자식 프로세스를 fork 해서 exec했다는 사실은 바뀔 수 없습니다. 다만 init 프로세스를 설명드리면서 말씀드렸던 case와 같이 아주 예외적인 case는 있을 수 있습니다.

 

 

부모 프로세스가 어떻게 자식 프로세스를 만드는지 한눈에 보여주는 그림자료를 가져와 보았습니다.

[그림 2] Child process

먼저 fork()라는 시스템 콜을 이용해 새로운 프로세스를 만듭니다. 이때 새로운 프로세스(자식)는 original 프로세스(부모)의 메모리 복사본으로 구성되죠. 틀을 복사하는 겁니다. 그럼 내용은 새로 채워야겠죠. exec()이라는 시스템 콜을 통해 메모리는 새로운 프로그램으로 대체됩니다. 지난 포스팅에서 언급했던 text, data, bss size등을 바꾸는 것입니다. 이때 heap과 stack은 pointer를 초기화하기 때문에 메모리 자체를 초기화시키진 않습니다.(사실 어떤 내용이 들어있어도 상관이 없습니다. 초기화된 pointer로부터 heap과 stack이 만들어질테니까요) 이제쯤 fork()가 memory management와 관련이 있다고 감을 잡으셨을 겁니다. 참고로 Windows에서는 시스템 콜 이름은 다를 수도 있지만 기본 철학은 같습니다.

 

최종적으로 fork()는 parent process를 복사하는 것이고 exec()는 text segment, data loding, C언어 같은 경우 pc는 main으로 줘버리기, stack, heap pointer 초기화하기를 수행하여 새로운 프로그램을 loading한다고 정리할 수 있을 것 같습니다. 여기서 보안분야에 관심이 많으신 분들은 눈을 반짝이실 수도 있겠네요.(parent process를 복사한다는 것은 결국 호출된 함수와 argument를 볼 수 있게 된다는 거죠. 이는 보안관점에서 약점이 될 수도 있습니다.)

 

14.02. Process creation in Linux

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, char* argv[]){
    int counter = 0;
    pid_t pid;
    printf("Creating Child Process\n");
    pid = fork();
    if(pid < 0){ // Error in fork
    	fprintf(stderr, "fork faild, errno: %d\n", errno);
    	exit(EXIT_FAILURE);
    }
    else if(pid > 0){ // This is Parents Process
        int i;
        printf("Parents(%d) made Child(%d)\n", getpid(), pid);
        for(i=0; i<10; i++){
        	printf("Counter: %d\n", counter++);
      	}
    }
    else if(pid == 0){ // This is Child Process
        int i;
        printf("I am Child Process %d!\n", getpid());
        execl("/bin/ls", "ls", "-l", NULL); // Run 'ls -l' at /bin/ls
        for(i=0; i<10; i++){ // Cannot be run
            printf("Counter: %d\n", counter++);
        }
    }
    wait(NULL); //wait for child termination
    return EXIT_SUCCESS;
}

fork()는 argument가 없습니다. pid는 process identifier(프로세스 식별자)의 약자입니다. 이 pid는 프로세스가 만들어지면서 증가합니다. init의 pid가 바로 1이죠. 음수가 나오는 경우는 시스템에 메모리가 부족한 경우입니다. 항상 시스템 콜을 호출한다면 return 값을 꼭 체크하는 것이 중요합니다.(framework, System call, 제 3자가 작성한 function을 호출 할 때 return 값을 확인하는 것은 프로그래밍을 업으로 하는 사람이라면 중요한 덕목 중 하나입니다.) fork하는 순간 프로세스가 생성되는 것이고 특별한 조치를 하지 않으면 parent와 동일한 code가 수행됩니다. exec()에는 여러 방식이 있는데요. 궁금하신 분들은 추가적으로 검색하거나 궁금하신 부분이 있으시다면 댓글 남겨주시기 바랍니다.(전달하는 인자에 따라 execl, execv, execle, execve, execlp, execvp로 나뉩니다.)

 

참고로 자식 프로세스가 스스로 child인 줄은 pid를 보고 알 수 있습니다. fork()라는 시스템 콜의 semantics가 return 값이 child인지 parent인지 구별합니다. 또한 커널이 어느 프로세스가 복제되는지 알고 return 값을 커널이 결정합니다. 마지막으로 fork()는 PCB를 copy하기 때문에 굉장히 heavy한 operation이라고 볼 수 있습니다. 

 


추가로 설명드리자면 PCB에 프로세스의 처리에 필요한 다양한 resource가 존재하고 크게 6개 구조로 나눌 수 있습니다. 다음 구분선이 나올 때까지 추가설명입니다. 이 부분에 대한 이해가 없더라도 큰 맥락에 대해 이해하는데는 무리가 없을 것입니다.

[그림 3] PCB
[그림 4] task_struct

실제 PCB 정보에 대한 구조체는 [그림 4]와 같습니다. 6개의 구조로 나누는 이유는 일부분은 부모와 자식 프로세스가 동일하게 사용하여 resource를 복사하지 않고 공유하여 사용하는 부분도 있기 때문입니다.(PCB일부를 공유하여 overhead를 줄이는 거죠) 나중에 thread에 대해 배우신다면 이 내용에 대한 이해도가 깊어질 것이라 생각합니다.

[그림 5] Reduce Process Creation

일반적이라면 커널모드의 동작이 끝난 후 유저모드로 돌아가지만 wait() 시스템 콜은 원래의 유저모드로 돌아가지 않습니다. CPU를 다른 프로세스에게 주죠. 커널모드에서 메모리 접근이 가능하기 때문에 다른 프로세스의 PCB를 가져와 PC register에 담긴 정보를 토대로 jump하게 됩니다. 이 code에서 부모 프로세스는 wait()을 호출하면 sleep 상태로 가고 CPU는 자식 프로세스가 가지게 됩니다. 자식이 프로세스 수행을 종료하고 나서 signal을 보내면서 부모 프로세스는 다시 ready queue로 이동하게 되죠. 다른 자료에서 좋은 그림이 있어 가져와봤습니다.

[그림 6] Context switch

아래의 예는 linux terminal에서 ls 명령어를 입력하였을 때 내부 동작 과정입니다.

[그림 7] ls_1
[그림 8] ls_2


 

 

Code의 실행결과

fork() 실패

  • pid에 -1값이 return.

fork() 성공

  • 성공하는 순간 프로세스는 2개가 됨(부모 프로세스 + 자식 프로세스)

여기서부터 2개의 프로세스가 동작하는 것이고 각각 return 값을 가지게 됩니다.

  • return 0 : 자식 프로세스
  • return 양수 : 부모 프로세스(부모 프로세스는 자식 프로세스의 pid 값을 반환받죠.)
  • getpid()는 자기 프로세스의 pid값을 반환해줍니다.
  • execl의 /bin/ls는 executable의 path입니다.

 

Windows도 철학은 같다고 말씀드렸는데요. Code는 찾아보시면 아시겠지만 argument 수가 많은 CreateProcess를 사용합니다. 하나의 시스템 콜만 가지고 해당하는 executagle을 바로 호출하겠다는 것이죠. 다만 Object, handle 등을 도입하여 wait하기 때문에 조금 복잡해 보이실수도 있습니다. 

 

 

14.03. Process termination

프로세스 생성에 대해 알아보았다면 termination에 대해서도 알아보아야겠죠. 왜 termination이 중요할까요?

커널이 사용하고 있는 자원을 반납하는 것이 중요하기 때문입니다. Calling process C 프로그래밍을 하면서 core dump를 많이 마주치신 분들은 더더욱 공감하실 겁니다.(Core dump는 디버깅을 할 수 있게 해주는 소중한 것이죠.)

 

exit()이라는 시스템 콜을 통해 커널에 삭제하도록 요청합니다. 프로세스의 resources는 커널에 의해 할당이 해제되죠. 만약 호출되지 않는다면? Abort function은 비정상적인 프로세스 종료를 유발합니다. SIGABRT라는 signal이 calling process로 전송됩니다.(Signal에 대해서는 곧 IPC관련 포스팅에서 다룰 예정입니다.)

 

Core dump란?

컴퓨터 프로그램이 특정 시점에 작업 중이던 메모리 상태를 기록한 것. 실제로는, 그 외에 중요한 프로그램 상태도 같이 기록되곤 하는데, 프 로그램 카운터, 스택 포인터 등 CPU 레지스터나, 메모리 관리 정보, 그 외 프로세서 및 운영 체제 플래그 및 정보 등이 포함된다. 코어 덤 프는 프로그램 오류 진단과 디버깅에 쓰인다

 

 

C프로그램에서 exit을 안했는데 왜 별일이 없었을까요?

exit()을 사용해보신 분들도 있으실 겁니다. 학부에서는 별 차이가 없다고 가르쳐주시는 교수님들도 계시죠. RTS(Runtime System)이 그 역할을 해줍니다. C runtime같은 경우는 프로그램이 프로세스가 되면서 control을 가져오고 main 호출이 끝나고 나갈 때 runtime system이 다시 받습니다. Runtime system이 eixt을 해주는거죠.(Compiler가 exit()을 넣어준다는 말도 있습니다.) 즉, 내가 프로그램을 돌린다는 것은 runtime system안에서 돌리는 것이고 그것이 lanbuage specific하고 여러가지 처리를 해주신다고 생각하시면 됩니다. Virtual machine은 훨씬 broad한 execution environment 개념입니다.

 

 

14.04. Container

왜 갑자기 뜬금없이 컨테이너 이야기냐고 하실수도 있겠습니다. 사실 프로세스는 pid와 함께 uid를 갖습니다. uid란 user id로 file access에서 굉장히 중요합니다. 지금까지 언급하지 않은 이유는 대부분 PC에서 singly user이기 때문이였죠. 시스템이 커지고 cluster가 되는 현 시점과 같이 node, machind을 나누어 사용할 때는 user id가 중요합니다. File system을 기본적으로 공유한다는 디자인이 있기 때문에 access 권한을 linux는 uid로 define하죠.(즉, uid가 다르면 다른 사람이 만든 file로 보고 접근을 막게 설계한다는 것입니다.) 

 

Single name space

이 개념은 조금 어려워서 최대한 안하려고 했는데, 쉽게 설명드리자면 어떤 컴퓨터 시스템에서 프로세스 100번이 있다면 그것은 unique하다는 것입니다. 새로운 프로세스를 만들더라도 pid는 절대로 100번이 될 수 없다는 거죠. 

 

can see or use the resources associated with that namespace

 

 

Container는 자신만의 name space를 가진 프로세스입니다. 왠 뚱딴지 같은 소리냐. 즉, Multiple user name space를 컨테이너가 가능하게 해준다는 거죠.(Network) Name space가 연구가 꾸준히 되고 있었는데 linux 커널에 본격적으로 도입된 것은 불과 10년정도 전입니다. 하나의 프로세스는 하나의 namespace라고 적용하였죠. 그 이전에는 single name space였습니다. 프로세스 관점에서 컨테이너가 컨테이너 간 프로세스를 isolation시킨다고 생각하시면 됩니다. 컨테이너에 대해서는 나중에 본격적으로 살펴볼 기회가 있을 겁니다. 지금은 같은 OS를 쓰면서 isolation하기 때문에 VM이나 XEN보다 가벼운 가상화정도라는 것 정도만 알고 넘어가도 충분합니다.(사실 저도 독학중이라 프로세스와 컨테이너의 관계에 대해서 아직 큰 그림(?)을 못 그리는 것 같습니다.)

 

 

이번 포스팅이 프로세스에 대해 알아보는 마지막 포스팅이 될 것 같습니다. Container 부분에서는 아직 이해가 부족해 명확하지 않거나 정확하지 않은 내용이 있을 수 있습니다. 이런 부분에 대한 지적은 언제든 환영입니다. 또 다른 질문이나 아이디어도 환영하고요. 많은 댓글 부탁드립니다. 감사합니다.

 

 

 

반응형

'학부공부 > OS_운영체제' 카테고리의 다른 글

16. Port [Interesting topic to study]  (0) 2021.11.24
15. IPC(1)  (2) 2021.11.21
13. Garbage collector [Interesting topic to study]  (0) 2021.11.15
12. Process(2)  (0) 2021.11.15
11. bss [Interesting topic to study]  (0) 2021.11.12

댓글