임베디드/[ Linux Kernel ]

[ Linux Kernel ] 09. 중간정리 및 Daemon(데몬)/Server(서버)

kim.svadoz 2020. 8. 15. 15:01
반응형

12. 총정리


지금까지 다뤘던 내용들을 총 엮어서 설명을 진행한다. 꽤나 복잡한 그림이 엮여 나오니 설명과 함께 따라오도록 노력해보자. 일단 아래 그림에 분홍색 구간은 커널이다. 커널 안에는 여러가지 시스템 콜이 존재하고 있다. 그리고 이 시스템 콜들은 context_switch()와 같은 내부함수와 연관이 있으며 각 하드웨어 자원마다 자료구조가 존재(struct CPU)한다. 아래 그림 또한 그림에 오류가 있는 부분이 있는데, 오류가 난 부분은 설명하면서 함께 나오니 너무 걱정할 필요는 없다.

image-20200811162036018

  1. fork()를 진행하면 커널로 진입한다. 커널에서 fork( )는 부모 프로세스와 똑같은 image를 생성한다.
  2. 점선으로 표시된 이유는 아직 CPU 제어가 부모 프로세스에 있기 때문에 자식 프로세스로 향하는 선은 점선으로 표시가 되어 있다.
  3. 그 다음 fork() 작업이 끝나고 리턴한다. 앞서 언급했듯 부모 프로세스에서 fork()를 실행했을 때의 결과값과 자식 프로세스가 실행했을 때의 결과값은 다르다고 했다. 일단 첫번째로 리턴되는 건 부모 프로세스의 PID가 리턴되므 else문으로 가서 wait( ) 시스템 콜을 호출한다.
  4. wait() 시스템 콜의 요청을 처리하기 위해 또 다시 커널모드로 진입한다. wait( )은 CPU를 잠시 포기하겠다는 의미이기 때문에 context_switch( ) 함수를 실행한다.
  5. 그림을 정정해야 한다. wait()에서 context_switch()로 가는 것이기 때문에 5번 화살표의 방향은 반대가 되어야한다. context_switch() 함수가 실행되면서, 먼저 CPU에 있던 state vector 영역에 해당하는 정보를 부모 프로세스의 PCB에 덮어 쓴다(저장한다). 이렇게 저장을 해야 후에 자식 프로세스의 작업이 끝나고 돌아왔을 때, 부모 프로세스의 PCB에 저장되어 있는 상태값들을 보고 후에 다시 부모 프로세스로 돌아가서 남은 작업들을 원활하게 처리할 수 있다.
  6. 그런데 자식 프로세스가 생겨날 때 애초에 부모프로세스에서 fork()가 일어나던 시점에 형성된 것이므로, 자식 프로세스의 PC(Program Counter)fork() 중간을 가리키고 있었을 것이다. 따라서 제어흐름은 6번 화살표를 따라 fork()로 가게 되고,자식 프로세스의 시작은 fork( )에서 시작되는 것이다.
  7. 자식 프로세스에서 실행되고 있는 fork()의 리턴 값은 당연히 자식 프로세스의 PID일 것이다. 따라서 자식 프로세스가 실행하기로 되어 있는 exec()이 호출된다.
  8. exec( )이 해주는 작업은 하드 디스크에 저장되어 있는 프로그램 코드(유저가 exec 시스템 콜의 매개변수로 준 프로그램)를 불러들여 현재 진행되고 있었던 프로세스 이미지 위에 덮어 씌우는 작업이다.
  9. 따라서 디스크에 유저가 exec()시스템 콜에 매개변수로 넘긴 ls에 해당하는 프로그램이 현재 진행중이었던 쉘(자식 프로세스) 위에 덮어 씌어지게 된다.

image-20200811162058192

  1. 덮어씌어진 후에 ls 프로그램의 main( )으로 흐름이 넘어간다.

  2. ls의 코드가 전부 실행된 후 exit( )이 호출되면서 흐름은 12번으로 넘어간다.

  3. 소스코드 상에 exit( )이 존재하지 않아도 컴파일러가 알아서 삽입을 해주기에, exit()을 무사히 실행할 수 있다. exit()은 지금까지 진행중었던 프로세스로부터 CPU를 뺐고 다른 프로세스에게 재할당해 주는 과정이 있기에 마찬가지로 context_switch( )를 호출하게 된다.

  4. 자신을 호출한 프로세스로부터 CPU를 뺐고, ready queue에 가서 CPU를 기다리고 있던 프로세스 중 우선순위가 높은 프로세스를 골라서 해당 프로세스의 PCB 안의 상태값들을 현재 CPU의 레지스터에 복사 붙여넣기(복붙) 한다.

  5. ready queue에 부모 프로세스만 남아있다고 가정한다면, 부모 프로세스가 선택되어 실행될 것이고 부모 프로세스는 wait()을 진행하고 있었기 때문에 wait() 중간부터 다시 실행된다.

  6. 14번까지의 작업이 끝났다면 쉘은 다시 사용자로부터 또다른 명령을 기다리고 있게된다.

12.1 용어 정리

총정리인 만큼, 기존의 프로그램과 프로세스 차이에 대해서도 한 번 짚어보고 가도록 한다.

image-20200811162228948

프로그램이 실행중일 때 우리는 프로그램을 프로세스라 부른다. a.out 형식을 가지고 main()함수부터 시작하게 되어 있다. 스케쥴링과 보호의 단위이고, 자원을 할당받는 과정을 수반하고 유저모드와 커널모드를 왔다갔다 하면서 진행된다.

image-20200811162244670

유저 영역(user space)의 text는 instruction(명령문)을 의미한다. databss에 대한 설명은 위 그림의 Note파트에 서술되어 있다. 먼저, 두 개의 배열(array)이 존재한다. A라는 배열은 초기값을 할당해줬고 B라는 배열에는 초기값을 주지 않았다. 만약 배열의 크기가 100만 정도에 전역변수로 선언되어 있다면? A 배열처럼 초기값을 할당 해줬다면 디스크에서 백만 개의 셀을 갖고 있어야 한다(사전에 자원이 지급됨). 만약 초기값을 주지 않았다면 디스크에 실제로 존재하진 않고 해당 배열이 실행 중 로드 될 때만 할당되게 된다.

초기에 값이 할당된 부분을 data라고 하며 초기에 할당되지 않은 데이터 부분을 bss라고 한다. heap은 동적 메모리 할당에 쓰여지는 데이터 영역이며 stack은 함수 호출 등에 사용되는 자료구조다.

커널 영역(kernel space)에는 PCB와 stack이 존재한다. HW(CPU) 쪽에서는 state vector가 존재한다. 이런 것들을 합쳐서 우리는 context라고 부른다.

Daemon (데몬) 또는 Server

서버(혹은 데몬)는 무엇일까? 근본적으로 서버는 a.out(실행 파일)이다. 다만 조금 특이한 알고리즘을 가지고 있을 뿐이다. 아래 그림을 살펴보자.

image-20200811162320894

맨 처음 서버 혹은 데몬이 시작되는 건 부팅 될 때(boot time)다. 부팅하고 나서 대부분의 시간은 잠들어 있다. 요청이 올 때만 해당 요청을 서비스 해주고 서비스가 끝나면 또 잠들게 된다. 이런 프로그램을 우리는 데몬 또는 서버라고 부른다. 만약 프린트 서버가 존재한다고 하면, 프린트 서버는 말 그대로 프린트 요청이 올 때만 프린트를 해주고 그 이외에는 잠든다. 네트워크 서버 또한 네트워크 요청(연결, 해제 등)이 올 때만 처리하고 그 이외에는 잠든다.

서버라는 것은 하드웨어의 개념이 아니라 소프트웨어의 개념인 것이다. 항상 incoming request가 오는지 안 오는지 지켜보고 있으며 서비스가 올 때만 서비스를 해주게 되어 있다.

image-20200811162335435

리눅스 시스템에서 사용되는 명령어 ps(Process State)를 살펴보자. 현재 기기에서 어떤 프로세스가 작동하고 있는지를 나타낸다. -e 옵션의 경우 시스템 프로세스까지 전부 보여주는 명령어다. 웹서버나 네트워크서버 등의 모든 시스템 프로세스의 상태를 보여주는 명령어다.

image-20200811162351823

보통 데몬이나 서버 프로그램의 경우 이름 뒤에 d자가 붙는다. httpd는 웹에서의 통신에 사용되는 데몬이고, ftpd는 파일전송 서버를 나타내는 등 다양한 서버와 데몬이 존재하고 있다.


생각만큼 크게 어렵지 않았던 3강이다. 결국 모든 프로그램은 알고리즘을 이해하는 것이 전부가 아닐까 하는 생각이 든다. 애초에 프로그램이란 건 논리의 집합이고, 해당 논리대로 작업을 하는 것이니 그 논리만 파악하고 있으면 그 프로그램을 아는 것이니까. 그나저나 강의 노트를 작성하는 건 생각만큼 쉬운 일이 아니라는 걸 다시한 번 체감한다. 내가 듣고 이해하는 것과 다시 누군가에게 풀어서 설명하는 건 천지차이니까.


13. 복습


이번 4번째 강의에서는 fork()를 통해 프로세스를 생성해 내는 과정에 대해 더 자세히 알아보는 시간을 갖는다. 또 PCB 내용을 분류해 볼 것이며 fork()와는 조금 다른 clone()에 대해서도 다룰 예정이다. 이번 강의는 부모 프로세스가 어떻게 자식 프로세스를 어떤 과정을 통해서 만들어 내는지를 확실히 알아야 이해할 수 있기에 먼저 지금까지 배운 내용 중 일부분을 복습을 하고 4강을 진행 할 것이다.


지금까지 한 내용들은 아래 등장하는 두개의 그림에 잘 정리 되어있다. 그림에 나와있는 순서들을 머리속에 담아만 둘 수 있다면 앞으로 좀 더 심화적인 내용을 이해할 때 큰 도움이 될 것이다. 그럼 지금부터 그림과 함께 설명을 보도록 하자.

image-20200811162640825

가운데에 보라색 박스로 그려져 있는 커널이 있다. 그리고 좌측에 유저가 작성한 프로그램인 쉘이 있다. 또 여기서 살펴볼 프로세스는 쉘 프로세스로 부모(Parent)와 자식(Child) 두개가 존재한다. 실제로 동작할 때는 훨씬 더 많은 프로세스들이 동작하고 있기 때문에 CPU 자원을 바로바로 받지는 못한다는 점을 알아두고 아래 흐름을 살펴보자.

  1. 먼저 우리 프로그램에다 ls 명령어를 쳤다고 가정하자. 그러면 프로그램은 ls라는 자식 프로세스를 만들려고 할 것이다.
  2. 그럼 자식을 만들기 위해 먼저 fork()를 실행한다. 이때 이 fork()는 쉘에 있는게 아니라 커널안에 있는 것이다. 시스템을 직접적으로 다루는 중요한 동작은 모두 커널이 관리한다. fork()는 작성된 프로그램과 똑같은 데이터를 복사해 만들어 줄 것이다. 그림에 표시된 점선은 제어흐름이 넘어간다는 뜻이 아니라 단지 데이터만 복사 된다는 뜻이다.
  3. 그렇게 fork()를 하고나서 다시 돌아와서 PID 값을 비교해 보니 자식 프로세스의 pid값이 리턴되었으므로 현재 부모 프로세스 제어흐름에 있다는 뜻이므로 else로 간다. fork()는 두번 리턴되는데 한번은 부모 프로세스에게 fork()로 만들어진 자식 프로세스의 pid값을 넘겨주고 한번은 자식 프로세스에게 0값을 넘겨준다. 자식 프로세스의 제어 흐름에는 0값이 전달된다.
  4. 이렇게 else로 들어온 부모 프로세스는 시스템 콜인 wait()을 호출한다. 이때 wait()를 한 이유는 부모 프로세스가 CPU를 포기하고 자식 프로세스에게 CPU를 넘겨주기 위한 것이다. 즉 실행흐름을 자식 프로세스에게 넘겨주기 위함이다.
  5. 그러면 wait()에서 CPU를 넘겨주기 위해 context_switch()를 실행하면서 지금까지 동작했던 부모 프로세스의 state vector들을 부모 프로세스의 PCB(Process Control Block)에 저장한다. 그 후 CPU를 기다리고 있는 프로세스들의 정보가 있는 ready queue에 가서 우선순위가 제일 높은 프로세스의 PCBCPU에 연결 시켜 준다. 이때 알아야 할 내용은 커널은 유저마다 커널 스택을 하나씩 가지고 있다는 점이다. 현재 커널 스택에는 wait()와 관련된 지역 변수들이 먼저 들어가 있다. 그리고 그 위에 context_switch()에 관련된 지역 변수들이 저장되어 있다. 부모 프로세스의 PCB에는 이러한 정보들이 저장되어 있다.
  6. CPU를 처음으로 넘겨받은 자식 프로세스는 return부터 해야하는 상황에 처해있다. 자식프로세스는 만들어 질때 부모 프로세스의 상태정보를 똑같이 복사해 만들어지기 때문에 fork()작업을 마무리 하고 있던 부모프로세스의 상황 또한 그대로 복사 되기 때문이다. 그래서 자식 프로세스는 fork()return을 하게 되면서 fork()는 두번 리턴한다는 개념이 생겨난 것이다. 단지 이번에는 자식 프로세스의 실행흐름이라는 점이 다르고 리턴된 pid값이 0이고 0값을 토대로 ifelse중 프로그램 내에서 어떤 제어흐름으로 갈지를 결정하게 된다.
  7. 리턴된 pid값이 0인것을 보면 자식 프로세스라는 뜻이므로 if문 안으로 들어가게 된다. 거기서 exec()을 하게 된다.
  8. 위 그림에서는 exec()에 매개변수가 ls인 상황이다. 이 명령어는 매개변수로 넘어온 프로그램을 찾고 해당 프로그램 이미지를 로드한다. 따라서 exec()이 실행되면서 디스크에 가서 ls를 찾는다.
  9. 그 후 자식 프로세스쪽에 디스크에서 찾은 ls내용을 덮어씌운다. 이로서 자식 프로세스는 더 이상 부모 프로세스의 복제품이 아닌 자신만의 역할을 하는 프로세스로 된다.

image-20200811162702431

  1. exec()을 통해 디스크에서 ls를 가져와 현재 이미지(코드)에 덮어씌우고

  2. 자식 프로세스는 자신이 할 일을 진행한다. 할 일이란 ls가 하는 작업과 동일하다.

  3. 일을 다 하고나면 이제 CPU가 필요 없으니 프로세스를 종료하기 위해 시스템 콜 exit()을 호출한다.

  4. 그럼 이제 또 CPU를 다른 프로세스를 주기 위해 context_switch()를 하게 되고 이때 부모 프로세스의 PCB를 불러온다. 그럼 이때 커널의 스택에는 wait()와 그 위에 context_switch()가 쌓여있는 상태로 있다. 보라색 커널 영역의 그림에는 스택이 반대로 표현되어 있다. 또한 wait()context_switch()사이에 있는 exec()exit()은 중간에 분명 스택에 쌓이긴 했으나 13번 실행흐름 전에 각각 실행이 끝나면서 스택에서 빠져나가 있는 상태다.

  5. 마지막으로 스택의 가장 상위에 위치하고 있는 context_switch()에서 wait()으로, 그리고 wait()에서 다시 부모쪽에서 시스템 콜 wait()을 호출한 곳으로 돌아가게 된다.

이렇게 하면 fork()의 과정이 끝이난다. 복습을 통하여 부모의 프로세스가 어떻게 자식 프로세스를 생성하고 자식 프로세스는 어떻게 종료되는지에 대해 알아보았다. 그렇다면 이제 본격적으로 fork()를 통해 프로세스를 생성하는 과정을 자세히 알아보자.

반응형