'process 프로세스'에 해당되는 글 1건

  1. 2012.04.06 프로세스 (process)

프로세스 (process)

OS/리눅스 & 유닉스 2012. 4. 6. 17:31


Contents

1 프로세스에 대해서
2 프로세스의 상태
3 프로세스의 모드
4 프로세스의 실행
5 멀티.다중 - 프로세스
5.1 fork를 이용한 자식프로세스 생성
5.2 fork와 exec를 이용한 새로운 프로세스의 생성
6 프로세스 관계
6.1 부모프로세스와 자식프로세스 init 프로세스
6.2 프로세스의 identify와 관계에서의 위치
6.3 고아 프로세스
6.4 Daemon 프로세스

1 프로세스에 대해서

리눅스 운영체제가 하는 가장 중요한 일중의 하나는 프로그램을 실행시키는 것이다. 프로그램은 컴퓨터가 이해할 수 있는 명령어들과 명령을 수행하기 위한 데이터를 포함한 실행가능한 객체다. 이들 프로그램은 하드디스크와 같은 보조기억장치에 위치하는데 실행하면, 운영체제는 이들을 읽어서 주기억장치에 복사 한다. 프로그램이 복사된 이미지가 올라가는 것이라고 볼 수 있는데, 이러한 프로그램의 실행 된 객체를 프로세스혹은 프로그램의 실행 이미지라고 말한다. 

 
이렇게 프로그램을 직접실행시키지 않고, 메모리로 이미지를 카피해서 실행시키는 데에는 다음과 같은 이유가 있다.
  1. 서로 완전히 독립적인 프로그램의 실행 가능
  2. 여러개의 이미지를 만들 수 있으므로, 멀티프로세스/멀티쓰레딩 지원

 

2 프로세스의 상태

앞서 살펴봤듯이, 프로세스는 프로그램의 실행이미지로 동시에 수많은 동일한 혹은 다른 프로그램들이 실행될 수 있다. 동시란 말에 주목할 필요가 있는데, 여기에서 말하는동시는 같은 시간에 실행되는 것을 의미하지 않는다. 리눅스 운영체제에서 동시라는 것은 여러개의 프로세스를 짧은 시간동안 switching하면서 실행하는 것을 의미한다. A, B, C 4개의 프로그램이 있다면, A프로그램이 끝날때까지 기다렸다가 B를 실행하는 것이 아닌, A실행을 잠깐 중단시키고, B로 스위칭해서 실행을 하고 다시 중단시키고, C를 실행 하는 방식이다. C를 실행한 후에는 다시 짧은 시간에 A로 넘어가서 이전의 중단된 시점에서 다시 프로세스를 수행한다. 

이러한 switching 시간은 매우 짧기 때문에, 실제로는 동시에 실행되지 않지만 동시에 실행되는 것처럼느껴진다. 이것을 time sharing 혹은 시분할 방식이라고 한다. 이 기술은 Unix에 기본적인 기능인데, 애초에 Unix가 시분할 시스템 - time sharing system- 개발 프로젝트에서 잉태된 운영체제이기 때문이다. 

이렇게 (완벽한 동시는 아니지만)동시에 프로세스를 실행하는 것을 멀티태스킹 운영체제라고 하며, 시간을 쪼개는 방식으로 멀티태스킹을 구현하는 것을 시분할 방식 멀티태스킹이라고 한다. 리눅스 운영체제는 시분할 방식 멀티태스킹환경을 지원한다. 아래 그림은 시분할 방식에서의 프로세스가 실행되는 방식을 보여주고 있다. 

timeline.png

프로세스가 한번에 실행되지 않고, 시간을 기준으로 switching 됨으로써, 프로세스의 현재 상태가 중요해진다. 프로세스가 중단된 상황인지, 실행되고 있는지등의 정보를 알고 있어야만 올바른 시간에 switching이 가능하기 때문이다.

프로세스는 다음과 같은 4가지의 상태중 하나를 가지게 된다.
  1. running 상태 : 실행가능한 상태를 말한다.
  2. waiting 상태 : 어떤 조건을 기다리는 상태. 
  3. stopped 상태 : 실행이 중단된 상태.
  4. zombie 상태 : 실행이 끝나고, 메모리 상에서 프로세스의 이미지가 제거 되었으나 운영체제의 커널은 여전히 프로세스의 정보를 가지고 있는 상태. zombie에 대해서는 뒤에서 자세히 다루도록 하겠다. 

3 프로세스의 모드

프로세스는 유저모드와 커널모드의 두가지 모드를 오가면서, 실행이 된다. 
  1. 유저모드 : 사용자 연산을 위한 모드로 사칙연산과 같은 연산작업으로 사용자 권한으로 각정 명령이 실행된다.
  2. 커널모드 : 주로 컴퓨터의 자원인 메모리, 하드디스크등의 장치에 접근하기 위한 모드로, 커널권한으로 실행된다. 

kermode.png

굳이 귀찮게 커널모드라는 걸 두는 이유는 자원에 대한 보안의 목적이 가장 크다. 리눅스는 다중사용자 운영체제이다. 만약 메모리, 사운드카드, 하드디스크와 같은 자원에 아무런 제한없이 접근이 가능해진다면, 심각한 보안문제가 발생할 수 있을 것이다. 리눅스는 커널모드라는 것을 두어서 이문제를 해결하는데, 만약 프로세스가 시스템자원을 사용하길 원한다면, 커널에서 제공하는 API를 이용해서, 커널에 자원을 사용하겠음을 요청해야만 한다. 이렇게 되면, 운영체제 차원에서 자원에 대한 접근을 제어할 수 있게 될 것이다. 

이렇게 커널에서 커널로 요청을 하기 위해서 제공하는 함수를 시스템콜이라고 부른다. 우리는 이미 몇개의 시스템콜을 사용해 봤는데, read(2), write(2), open(2)등이 대표적인 시스템콜이다. 예컨데, 프로세스는 malloc(2)함수를 호출해서 커널에 메모리의 사용을 요청할 수 있다. 요청을 받은 커널은 사용 가능한 연속적인 메모리 공간를 만든 다음, 이 메모리 공간의 주소 값을 가지는 포인터를 반환한다.

4 프로세스의 실행

리눅스에서 새로운 프로세스를 실행시키는 유일한 방법은 execl(2)을 이용하는 것이다. 이 함수는 다음과 같이 사용할 수 있다.
#include <unistd.h> 
int execl(const char *path, const char *arg, ...); 
 
  • path : 실행되는 프로그램의 완전한 경로다.
  • arg : 이것은 프로그램이 실행될때, 넘겨질 실행인자들로, 여러 개가 정의될 수 있다. 더이상 넘겨질 실행인자가 없다는 것을 분명하 하기 위해서, 마지막에 NULL을 입력해줘야 한다. 간단히 ls(1)를 실행시키는 프로그램을 만들어 보자.
#include <unistd.h> 
 
int main(int argc, char **argv) 
{ 
    execl("/bin/ls", "ls", "-al", NULL); 
} 
 

execl 함수는 프로그램을 실행시켜서 새로운 프로세스를 실행하면, 현재의 프로세스 이미지를 덮어써 버린다. 예를들어 위의 프로그램이름이 execTest이라고 가정해보자. execTest 프로그램을 실행시키면, execTest의 실행이미지인 execTest 프로세스가 생성될 것이다. 여기에 execl을 이용해서 /bin/ls 를 실행시키면, /bin/ls의 실행이미지로 완전히 대체되어 버린다. 아래의 프로그램을 실행시켜 보자. 
001  #include <unistd.h>
002  #include <stdio.h>
003  
004  int main(int argc, char **argv)
005  {
006    printf("Start\n");
007    execl("/bin/ls", "ls", "-al", NULL);
008    printf("End\n");    // 실행되지 않는다.
009  }
010  
다음은 실행결과다. 
$ ./execTest 
Start 
drwxr-xr-x  2 yundream yundream 4096 2008-02-29 00:08 . 
drwxr-xr-x 60 yundream yundream 4096 2008-02-29 00:08 .. 
-rwxr-xr-x  1 yundream yundream 6585 2008-02-29 00:08 execTest 
-rw-r-----  2 yundream yundream   81 2007-12-17 23:59 hello.txt 
-rw-r--r--  1 yundream yundream   12 2007-11-26 23:58 test.txt 
-rw-r--r--  1 yundream yundream  489 2007-11-26 23:53 write.c 
$ 
 
우선 6번째 코드인 printf가 실행된건 분명히 확인할 수 있을 것이다. 그다음 7번째 줄인 execl이 호출되어서 /bin/ls -al 이 실행되었다. 그런데, 8번째 줄은 실행되지 않았다 ? 앞에서 말했다 시피, execl이 호출되면서 프로세스의 이미지 자체가 /bin/ls 로 덮어써져 버렸기 때문에, 8번째 코드가 아예 실행이 되지 않기 때문이다.

execl을 호출하면 프로세스의 이미지를 완전히 덮어쓰게 된다는 점을 이해하는건 그리 어렵지 않을 것이다. 그렇다면, 새로운 프로세스를 호출하고 나서, 원래의 프로그램으로 되돌아 오려면 어떡해야 하나 라는 고민이 생겨날 것이다. 새로운 프로세스를 실행시키고 나서, 프로세스가 종료되면 원래 상태로 되돌아오는 가장 대표적인 프로그램은shell일 것이다. 쉘의 프롬프트에서 ls 를 입력하면, ls 실행된 후 다시 쉘상태로 되돌아 와서 프롬프트가 떨어지는 것을 확인할 수 있다. execl 함수는 프로세스를 덮어써 버리기 때문에, 쉘과 같은 프로그램의 제작이 불가능하다. 어떻게 쉘 같은 프로그램을 만들 수 있을까 ?

이 문제는 fork(2)를 이용한 다중 프로세스 생성기법으로 해결할 수 있는데, 이에 대한 내용은 조금 뒤에 알아보도록 할 것이다.

5 멀티.다중 - 프로세스

유닉스 운영체제는 다중 프로세스를 지원한다고 알고 있다. 그런데 앞에서 프로세스를 생성하는 유일한 방법은 execl 함수를 이용하는 것이라고 배웠다. 문제는 execl 함수는 원본 프로세스의 이미지를 덮어써 버린다는 것으로, 이렇게 되면 운영체제는 동시에 단지 하나의 프로세스만을 가질 수 있게 될 것이다. 

유닉스 운영체제는 fork라는 프로세스 복사 함수를 이용해서 이 문제를 해결할 수 있다. fork는 원본프로그램의 복사판을 만드는 함수다. fork와 execl 함수는 분명히 다르다는 점을 인지하도록 하자. execl은 다른 프로세스를 생성하지만 fork는 자기자신을 복제한다. 즉 유닉스 운영체제에서 새로운 프로세스를 생성시키는 유일한 방법은 여전히 execl 함수를 사용하는 것이다.
  +---------+        +--------------+ 
  | Process |----+---| Copy Process | 
  +---------+    |   +--------------+ 
                 |   +--------------+ 
                 +---| Copy Process | 
                 |   +--------------+ 
                 |  
                 +--- ... 
 
프로세스를 복사하는게 포크와 비슷하다고 해서 fork라는 이름을 붙이게 되었다.

이때 원본 프로세스를 부모 프로세스라고 하고, 부모 프로세스로 부터 복사 되어서 새로 생성된 프로세스를 자식 프로세스라고 한다. 이들 프로세스의 관계에 대해서는 뒤에 따로 살펴보도록 하겠다.

이제 fork함수를 이용함으로써, execl이 가지는 원본 프로세스 이미지를 덮어쓰는문제를 해결할 수 있다. fork를 해서 원본프로세스의 복사본을 만들고, 여기에서 execl을 이용해서 새로운 프로세스를 실행시키는 것이다. 이러한 식으로 프로그램을 생성시키는 가장 대표적인 프로그램이 바로 shell 프로그램이다. shell 에서 ls 프로그램을 실행시키면, 다시 shell로 되돌아오는 것을 확인할 수 있을 것이다. 이는 shell 이 ls 명령을 받으면 fork함수를 이용해서 자식 프로세스를 만들고, 이 자식 프로세스에서 execl을 이용해서 ls를 실행시키기 때문에 가능해진다.

이렇게 fork & execl 을 이용하면, 진정한 멀티 프로세스 환경이 가능해 지게 된다. 유닉스 운영체제는 fork & execl 을 통해서 생성된 수많은 프로세스를 시분할방식으로 동시에 수행함으로써, 멀티 프로세스 환경을 제공한다.

5.1 fork를 이용한 자식프로세스 생성

fork 는 자기자신을 복사해서 프로세스를 생성하는 운영체제에서 제공하는 함수로, 그 분기되는 모습이 포크와 비슷하다고 해서 fork라고 이름지워졌다. 

http://lh3.ggpht.com/_Os5qf5urx_A/S3BAokS02AI/AAAAAAAABHM/dwfPuU1guYA/s400/fork.png

fork 함수는 다음과 같이 선언되어 있다.
#include <unistd.h> 
 
pid_t fork(void); 
 
코드내에서 fork(2)함수를 호출하면, 자식프로세스가 생성이 된다. 이 과정은 자식이 부모의 유전학적 정보를 상속받는 것과 비슷한데, 실제 자식프로세스는 부모로 부터 많은 정보들을 그대로 상속받는다. 예를들자면, 부모프로세스의 정보들, 열려있는 파일, signal정보, 메모리에 있는 많은 정보들이다. fork함수가 성공적으로 수행되어서 자식 프로세스가 생성되면, 부모프로세스에게는 새로 생성된 자식프로세스의 PID가 리턴이 되고, 자식프로세스에게는 0이 리턴된다. 

다음은 fork를 이용해서 자식프로세스를 생성시킨 프로그램이다. 프로그램의 이름은 forktest.c 로 하자.
#include <unistd.h> 
#include <stdlib.h> 
#include <string.h> 
#include <stdio.h> 
 
int main() 
{ 
    int pid; 
    int i; 
 
    i = 1000; 
    pid = fork(); 
    if (pid == -1) 
    { 
        perror("fork error "); 
        exit(0); 
    } 
    // 자식프로세스가 실행시키는 코드 
    else if (pid == 0) 
    { 
        printf("자식 : 내 PID는 %d\n", getpid()); 
        while(1) 
        { 
            printf("-->%d\n", i); 
            i++; 
            sleep(1); 
        } 
    } 
    // 부모프로세스가 실행시키는 코드 
    else 
    { 
        printf("부모 : 내가 낳은 자식의 PID는 %d\n", pid); 
        while(1) 
        { 
           printf("==>%d\n", i); 
           i += 4; 
           sleep(1); 
        } 
    } 
} 
 
컴파일 한 후 실행시켜 보기 바란다. 부모프로세스와 자식프로세스가 동시에 주어진 코드를 실행시키는 것을 확인할 수 있을 것이다. ps 를 이용하면 이들 프로세스와의 관계를 명확하게 확인할 수 있다.
$ ps -ef | grep forktest 
UID        PID  PPID  C STIME TTY          TIME CMD 
yundream 12119  8557  0 17:33 pts/0    00:00:00 ./forktest 
yundream 12120 12119  0 17:33 pts/0    00:00:00 ./forktest 
 
우리는 PID 12120을 가지는 자식프로세스가 생성되었음을 확인할 수 있다. PID 12120인 프로세스가 자식프로세스인 것은 PPID값을 이용해서 확인가능 하다. PPID 는parent Process ID의 줄임말이다. PID, PPID 등에 대한 것은 이 문서의 후반부에 자세히다루도록 할 것이다. 

5.2 fork와 exec를 이용한 새로운 프로세스의 생성

그럼 예제 코드를 이용해서 fork & execl의 작동방식에 대해서 알아보도록 하겠다. 여기에서 만들고자 하는 프로그램은 간단한 shell 프로그램이다.

001  #include <stdlib.h>
002  #include <string.h>
003  #include <unistd.h>
004  #include <stdio.h>
005  #include <sys/wait.h>
006  #include <sys/types.h>
007  
008  #define chop(str) str[strlen(str)-1] = 0x00;
009  
010  int main(int argc, char **argv)
011  {
012    char buf[256];
013    printf("My Shell\n");
014    int pid;
015    while(1)
016    {
017      // 사용자 입력을 기다린다.
018      printf("# ");
019      fgets(buf, 255, stdin);
020      chop(buf);
021  
022      // 입력이 quit 라면, 프로그램을 종료한다.
023      if (strncmp(buf, "quit", 4) == 0)
024      {
025        exit(0);
026      }
027  
028      // 입력한 명령이 실행가능한 프로그램이라면
029      // fork 한후 execl을 이용해서 실행한다.
030      if (access(buf, X_OK) == 0)
031      {
032        pid = fork();
033        if (pid < 0)
034        {
035          fprintf(stderr, "Fork Error");
036        }
037        if (pid == 0)
038        {
039          if(execl(buf, buf, NULL) == -1)
040            fprintf(stderr, "Command Exec Error\n\n");
041          exit(0);
042        }
043        if (pid > 0)
044        {
045          // 부모 프로세스는 자식프로세스가 종료되길 기다린다.
046          int status;
047          waitpid(pid, &status, WUNTRACED);
048        }
049      }
050      else // 만약 실행가능한 프로그램이 아니라면, 에러메시지를 출력
051      {
052          fprintf(stderr, "Command Not Found\n\n");
053      }
054    }
055  }
056  
이 프로그램은 아주 간단한 shell으로, 프로그램의 인자를 처리하지도 못하지만, fork 와 execl을 설명하는데에는 부족함이 없을 것이다. 다음은 실행시킨 예이다. 프로그램이름은 myshell 로 했다.
MY Shell 
$ myshell 
# /usr/bin/w 
 01:15:32 up  2:58,  4 users,  load average: 0.47, 0.50, 0.62 
USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU WHAT 
yundream :0       -                00:05   ?xdm?  14:20m  0.05s /bin/bash /usr/ 
yundream pts/1    :0               00:06    9.00s  1.47s  1.24s w3m -F http://w 
yundream pts/3    :0               00:54    0.00s  0.22s  0.00s ./myshell 
yundream pts/4    :0.0             00:53   22:13m  0.40s  0.27s BitchX irc.nuri 
 
# ll 
Command Not Found 
 
# quit 
$ 
 

6 프로세스 관계

6.1 부모프로세스와 자식프로세스 init 프로세스

위에서 fork와 exec 함수를 이용해서 프로세스를 실행시키는 방법에 대해서 알아보았다. 여기에서 우리는 프로세스가 전혀 독립적으로 생성되는게 아닌, 부모 프로세스에서 생긴다는 것도 덤으로 배우게 되었다. 부모프로세스가 있다면, 자식뻘이 되는 프로세스가 있을 것이다. 부모프로세스로 부터 fork되어새 생성된 프로세스를 자식 프로세스라고 부른다.

부모프로세스와 자식프로세스의 관계는 쉽게 이해되었을 것이다. 그렇다면, 부모의 부모의 부모의 부모의 프로세스가 있을 것이고, 최초의 아담격인 프로세스가 있으리라는걸 추리할 수 있을 것이다.

바로 init가 모든 프로세스의 조상이 되는 프로세스다. 모든 프로세스는 init로 부터 fork & exec 되어서 생성이 된다. pstree명령을 이용하면, 프로세스의 관계를 확인할 수 있다.
$ pstree 
init─┬─NetworkManager───{NetworkManager} 
     ├─NetworkManagerD 
     ├─acpid 
     ├─amarok───11*[{amarok}] 
     ├─atd 
     ├─avahi-daemon───avahi-daemon 
     ├─bonobo-activati───{bonobo-activati} 
     ├─console-kit-dae───61*[{console-kit-dae}] 
     ├─cron 
     ├─cupsd 
     ├─2*[dbus-daemon] 
     ├─dbus-launch 
       .... 
 

6.2 프로세스의 identify와 관계에서의 위치

프로세스는 운영체제위에서 실행되는 실행객체이다. 객체가 객체로써 정체성을 가지기 위해서는 다른객체와 자신을 분리할 수 있는 identify 를 가지고 있어야 한다.

각 프로세스는 다음의 2가지 요소를 이용해서 자신의 identify 를 확보할 수 있다.
  1. name : 프로세스의 이름이다. 
  2. PID : Process ID로 운영체제가 각각의 프로세스에 부여하는 유일한일련 번호다. 프로세스의 이름만으로도 identify를 확보할 수 있을거라고 생각할 수 있지만 이름이 같은 프로세스가 생성될 수 있으므로, name만 가지고는 identify를 확보할 수 없다. 때문에 운영체제에서 일련번호를 부여하게 된다. 이 번호는 중복되지 않는 유일한 번호다. 일종의 주민등록번호 정도로 보면 될 거 같다. 

프로세스 이름과 PID 를 이용해서 프로세스를 identify(식별)할 수 있게 되었다. 하지만 이것만으로는 부족하다. 프로세스는 운영체제 위에서 독립적으로 존재하지만 또한 다른 프로세스들과 관계를 맺고 있기 때문이다. 어떤 프로세스는 반드시 어떤 프로세스의 자식 프로세스가 되어야 한다. 혹은 다른 프로세스의 부모가 되기도 한다. 

즉 프로세스의 identify와 함께, 프로세스의 관계에서의 위치도 정의할 수 있어야 한다.

그래서, 각 프로세스는 name과 PID외에도 프로세스군에서의 자신의 위치를 정의하기 위한 다음과 같은 정보들을 가진다.
  1. PPID : 부모프로세스의 ID로 어떤 프로세스로 부터 생성이 되었는지를 알려준다. 
  2. PGID : 프로세스는 여러개의 자식프로세스를 만들어낼 수 있다. 그렇다면, 이들 프로세스는 {부모-->{자식,자식,자식}}과 같이 하나의 가계를 만들 수 있을 것이다. 운영체제에서는 이것을 가계라고 하는 대신 group이라고 한다. PGID는 프로세스가 어느 그룹에 포함되어 있는지에 대한 정보를 알려준다. PGID는 일련번호로 되어 있으며, 보통 부모프로세스의 PID즉 PPID가 PGID가 된다. 즉 프로세스그룹은 부모프로세스의 PID를 공통분모로 해서 하나의 그룹을 만들게 된다.

위의 forktest.c 프로그램을 실행시키고 다음과 같이 ps 를 이용해서 프로세스 정보를 알아보도록 하자. 
$ ps -efjc | grep forktest  
UID        PID  PPID  PGID   SID CLS PRI STIME TTY          TIME CMD 
yundream 12198  8557 12198  8557 TS   24 17:40 pts/0    00:00:00 ./forktest 
yundream 12199 12198 12198  8557 TS   21 17:40 pts/0    00:00:00 ./forktest 
 
프로세스의 상세정보들이 출력됨을 알 수 있다. PID가 12199인 프로세스가 12198로 부터 생성된 자식프로세스임을 확인할 수 있다. 또한 12198 프로세스는 PID 8557 로 부터 생성된 자식 프로세스임을 미루어 짐작할 수 있다. 그렇다면 PID 8557인 프로세스가 어떤 프로세스인지 확인해 보도록 하자. 
$ ps -efjc | grep 8557 
UID        PID  PPID  PGID   SID CLS PRI STIME TTY          TIME CMD 
yundream  8557  8550  8557  8557 TS   24 13:37 pts/0    00:00:00 bash 
 
그렇다. bash 프로그램임을 알 수 있다. bash는 우리가 forktest 프로그램을 실행시킨 쉘프로그램으로, bash도 fork()를 이용해서 forktest 를 실행시켰을 것임으로 forktest의 부모프로세스가 된다. 이들의 관계는 다음과 같은 Tree 형태로 표현할 수 있을 것이다.
        fork&exec                    fork 
  bash ---+----------- forktest ---+-------- forktest 
 
bash의 부모프로세스는 PID 8550을 가지는 프로세스일 것이고, 거슬로 올라가면 결국 init 프로세스를 만나게 될 것이다.

그렇다면 왜 그룹이 중요한 걸까. 단지 분류하기 좋게 하기 위해서 ? 물론 그런이유도 있기는 하지만, 좀 더 근본적인 이유가 있다.

그룹은 실생활에서의 가족들이 그렇듯이, 공통의 자원을 공유하는 관계로 서로에게 영향을 끼친다. 즉 부모프로세스가 종료되면 자식 프로세스도 따라서 종료되어버리거나 부모로부터 버려진 고아가 되는 등의 영향을 받는다. 또한 부모프로세스는 자식프로세스를 종료시킬 수 있으며, 아예 분가시켜버릴 수도 있다. 

부모와 자식프로세스간의 어떤 매체를 이용해서 소통 이루어진다는 건데, 리눅스 운영체제는 signal이라는 매체를 이용해서, 부모와 자식프로세스간에 소통을 한다. 예를들자면 부모프로세스가 너 그냥 죽어라라고 신호를 보내건나 보내지 않는 식이다. 만약 부모프로세스가 죽으면서, 자식프로세스들에게 너희도 따라서 죽어라 - 좀 잔인한가? - 라고 하면, 자식프로세스들도 함께 죽는 거고, 자기만 죽겠다고 하고 신호를 보내지 않는다면, 자식프로세스는 고아프로세스가 되는 식이다. 

signal의 사용과 고아프로세스에 대한 것은 따로 언급될 것이다. 

6.3 고아 프로세스

위에서 프로세스는 부모프로세스와 그룹을 맺는다는 것을 배웠다. 그리고 고아 프로세스에 대해서도 간단하게 알아보았다. 고아 프로세스란 즉, 부모프로세스가 죽으면서 자신만 죽어서 자식프로세스는 그대로 남아있는 상태다. 부모프로세스가 죽었으니 고아가 될수 밖에...!!! 

고아 프로세스는 어떻게 될까. 그냥 버려질까 ? 그렇다면 너무 비정한것 같다는 생각이 든다. 유닉스 운영체제를 만들던 개발자들이 매우 인간적이여서 그랬는지는 모르겠지만 이들은 고아가된 프로세스를 init 프로세스가 관리해서 버려지지 않도록 설계를 했다. 현실에서 고아를 버리지 않고, 사회에서 보호하는 것처럼 말이다.

이론적으로 고아 프로세스는 아주 간단하게 만들수 있다. 자식프로세스를 생성시킨 후 부모프로세스를 종료시키기만 하면 된다. 

위의 forktest.c에서 자식 프로세스가 고아 프로세스가 되도록 수정해 보았다. 프로그램의 이름은 forktest2.c 로 하겠다. 
#include <unistd.h> 
#include <stdlib.h> 
#include <string.h> 
#include <stdio.h> 
 
int main() 
{ 
    int pid; 
    int i; 
 
    i = 1000; 
    pid = fork(); 
    if (pid == -1) 
    { 
        perror("fork error "); 
        exit(0); 
    } 
    // 자식프로세스가 실행시키는 코드 
    else if (pid == 0) 
    { 
        printf("자식 : 내 PID는 %d\n", getpid()); 
        while(1) 
        { 
            printf("-->%d\n", i); 
            i++; 
            sleep(1); 
        } 
    } 
    // 부모프로세스가 실행시키는 코드 
    else 
    { 
        printf("부모 : 내가 낳은 자식의 PID는 %d\n", pid); 
        sleep(1); 
        printf("T.T 나죽네\n"); 
        exit(0); 
    } 
} 
 
이제 실행시켜 보도록 하자. 
$ ./forktest  
부모 : 내가 낳은 자식의 PID는 8207 
자식 : 내 PID는 8207 
-->1000 
T.T 나죽네 
-->1001 
yundream@yundream-desktop:~$ -->1002 
-->1003 
 
yundream@yundream-desktop:~$ -->1004 
-->1005 
-->1006 
-->1007 
-->1008 
 
쉘과는 따로 자식프로세스가 계속 실행되는걸 알 수 있을 것이다. 이제 Ctrl+C 를 눌러보자. Ctrl+C를 누르면 일반적으로 프로세스는 종료가 된다는 것을 경험적으로 알고 있을 것이다. - 정확히 말하자면 SIGINT라는 시그널이 전달되고, 이에 대한 반응으로 프로세스가 죽는다. 시그널은 나중에 다룰 것이다 -. 그러나 Ctrl+C를 아무리 눌러도 자식프로세스가 죽지 않는걸 알 수 있을 것이다. 왜냐하면, bash 의 자식의 자식 프로세스, 즉 같은 그룹에 속하지 않은 전혀 다른 그룹의 프로세스가 되었기 때문이다. ps 결과로 확인해 보도록 하자. 

#ps -efjc | grep forktest 
UID        PID  PPID  PGID   SID CLS PRI STIME TTY          TIME CMD 
yundream  8207     1  8206  8093 TS   24 00:16 pts/5    00:00:00 ./forktest 
 
PPID가 1 즉 init의 자식프로세스가 되었음을 확인할 수 있다. 집도 절도 없는 고아프로세스라는 얘기가 되겠다. 

6.4 Daemon 프로세스

고아프로세스는 어감이 좋지 않아 보이기는 하지만, 프로세스의 또다른 가능성을 보여준다. 즉 현재 유저와 프로세스의 영향을 받지 않고 백그라운드에서 실행되는 프로세스의 제작에 관한 것이다. 

이렇게 현재 화면과 프로세스에서 떨어져 나가서 독립적으로 실행되는 프로세스를 데몬 프로세스라고 한다. 가장 대표적인 프로그램이 웹서비스를 위한 웹서버 프로그램일 것이다. 이런 프로그램들은 거의 운영체제가 시작됨과 동시에 시작되어서 운영체제가 끝날때까지 뒤에서 우리가 눈치채지 못하는 상태에서실행이 된다. 

데몬프로세스가 되려면 다음과 같은 조건을 갖추어야 한다.
  1. 일단 고아 프로세스가 되어야 한다.
    데몬 프로세스는 완전히 독립된 프로세스다. 그러므로 고아 프로세스가 되어야 한다. 예컨데, 가족으로 부터 독립해서 사회로 나가야 된다는 얘기가 되겠다.
  2. 표준입력, 표준출력, 표준에러을 닫는다. 
    표준입력과 표준출력표준에러는 사용자와 프로세스가 상호작용 하기 위한 장치로, 표준입력은 키보드, 표준출력은 모니터로 대응된다. 데몬 프로세스는 뒤에서 독립적으로 돌아가는 프로세스 이므로 사용자와의 상호작용을 해서는 안된다. 그러므로 표준입력과 표준출력을 표준에러를 닫아줄 필요가 있다. 뒤에서 혼자 돌아야 하는 프로그램인데, 모니터에 (forktest2.c 와 같이) 잡다한 메시지를 출력해서는 안될 것이기 때문이다. 이런 데몬프로세스와의 상호작용은 IPC혹은 로그 파일 등을 통해서 이루어진다. IPC는 Inter Process Communication 의 줄임말로 프로세스간 내부통신을 위해서 사용되는 설비이다. 회사내부에서 부서원간 통화를 위해 사용되는 전화라고 볼 수 있을 것이다. 이에 대한 내용은 별도의 장을 할애해서 다루도록 할것이다. 
  3. 터미널을 가지지 않는다. 
    terminal(터미널)이란 사용자가 컴퓨터에 접속된 상태를 말한다. 이 터미널에 키보드와 모니터와 같은 장치가 연결되어 있고, 이것을 이용해서 사용자의 프로세스가 컴퓨터와 연결이 된다. 데몬 프로세스는 사용자 환경과 독립되어야 하므로 터미널을 끊어줘야 한다. 

그렇다면, 고아 프로세스를 만든다음 고아 프로세스로 부터 표준입력,출력,에러를 닫고 터미널을 제거시키면 데몬 프로세스가 될 것이라는 것을 예상 할 수 있을 것이다. 데몬 프로세스를 만드는건 이 3가지의 과정의 코드화다.

다음은 완전한 데몬 프로세스다. 프로그램이름은 daemon.c로 하자. 
001  #include <unistd.h>
002  #include <stdlib.h>
003  #include <string.h>
004  #include <stdio.h>
005  
006  int main()
007  {
008      int pid;
009      int i;
010  
011      i = 1000;
012      pid = fork();
013      if (pid == -1)
014      {
015          perror("fork error ");
016          exit(0);
017      }
018      // 자식프로세스가 실행시키는 코드
019      else if (pid == 0)
020      {
021          printf("자식 : 내 PID는 %d\n", getpid());
022          close(0);
023          close(1);
024          close(2);
025          setsid();
026          while(1)
027          {
028              printf("-->%d\n", i);
029              i++;
030              sleep(1);
031          }
032      }
033      // 부모프로세스가 실행시키는 코드
034      else
035      {
036          printf("부모 : 내가 낳은 자식의 PID는 %d\n", pid);
037          sleep(1);
038          printf("T.T 나죽네\n");
039          exit(0);
040      }
041  }
042  
  1. 39 에서 부모프로세스를 종료한다. 
  2. 22,23,24 에서 표준입력,표준출력,표준에러를 닫았다.
  3. setsid()를 이용해서, 사용자환경에서 독립된 자신의 환경을 만든다. 기존의 환경이 리셋되면서 터미널이 사라진다. 또한 새로운 터미널을 지정하지 않았기 때문에, 이 프로세스는 결과적으로 터미널을 가지지 않게 된다.

ps를 통해서 프로세스의 상태를 확인해 보도록 하자.
$ ps -efjc | grep daemon  
UID        PID  PPID  PGID   SID CLS PRI STIME TTY          TIME CMD 
yundream  8252     1  8252  8252 TS   24 00:43 ?        00:00:00 ./daemon 
 
PPID가 1 이고, 새로운 Session ID인 8252를 가졌으며 (이 프로세스가 세션의 주인이므로, PID가 SID가 된다) 터미널(TTY)가 없음을 확인할 수 있다. 28번 줄의 printf 결과도 화면에 출력되지 않는 것을 확인할 수 있다. 완전한 데몬 프로세스가 만들어진 것이다. 

데몬 프로세스는 특히 Internet서버 프로그램을 만드는데, 중요하게 사용되는 기법으로 네트워크 프로그래밍에서 중요하게 다루어지는 기술이다.

'OS > 리눅스 & 유닉스' 카테고리의 다른 글

Warning : incompatible implicit declaration of built-in function xxxxxxxxx  (0) 2012.04.26
kill -0  (0) 2012.04.26
getprocs64  (0) 2012.04.02
pthread_detach  (0) 2012.03.24
nohup  (0) 2012.03.22
: