Home [Linux System Programming] Ch05 프로세스 관리
Post
Cancel

[Linux System Programming] Ch05 프로세스 관리

[Ch05 프로세스 관리]

  • 유닉스는 새로운 바이너리 이미지를 메모리에 적재하는 과정에서 새로운 프로세스를 생성하는 부분을 분리했다.

5.1 프로그램, 프로세스, 스레드

  • 바이너리는 디스크 같은 저장 장치에 기록되어 있는 컴파일된 실행할 수 있는 코드를 말한다.
    • 흔히 프로그램을 지칭하기도 함.
    • 때로는 애플리케이션을 뜻하기도 함.
    • /bin/ls, /usr/bin/X11 모두 바이너리
  • 프로세스는 실행 중인 프로그램을 뜻함.
    • 프로세스는 메모리에 적재된 바이너리 이미지와 가상화된 메모리의 인스턴스, 열린 파일 같은 커널 리소스, 관련된 사용자 정보와 같은 보안 정보와 하나 이상의 스레드를 포함하고 있다.
  • 스레드는 프로세스 내 실행 단위.
    • 각각의 스레드는 가상화된 프로세서를 가지고 있음
      • 프로세서에는 스택, 레지스터, 명령어 포인터 같은 프로세서의 상태가 포함되어 있다.
      • Q) 리눅스에서 스레드를 어떻게 구현했을까? (프로세스와 거의 유사하다고함…)
        • 스레드든 프로세스든 다 Task로 구분하고 스케줄링함.
        • 또? 구조는?
    • 싱글 스레드 프로세스는 프로세스가 곧 스레드가 된다.

5.2 프로세스 ID

  • 모든 프로세스는 프로세스 ID(pid)라고 하는 유일한 식별자로 구분됨.
  • pid는 특정 시점에서 유일한 값임을 보장한다.
    • 커널이 같은 프로세스 식별자를 다른 프로세스에 다시 할당하지 않으리라 가정한다.
  • 동작 중인 다른 프로세스가 없을 때 커널이 실행하는 idle 프로세스는 Pid가 0이다.
  • 부팅이 끝나면 커널이 실행하는 최초 프로세스인 init 의 pid는 1이다.
  • 리눅스 커널이 적절한 init 프로세스를 찾으면서 실행할 프로세스를 결정하는 순서
    1. /sbin/init
    2. /etc/init
    3. /bin/init
    4. /bin/sh
      • 이 순서에서 가장 먼저 찾은 프로세스를 실행함.
      • 네 가지 모두 실패하면 panic 발생
        • 복구할 수 없는 치명적인 내부 에러를 감지

5.2.1 프로세스 ID 할당

  • 보통 커널의 최대 pid 값은 32768이다.
    • pid값으로 signed 16bit integer를 사용했던 오래된 유닉스 시스템과의 호환성을 위함.
  • 커널은 pid를 순서대로 엄격하게 할당한다.
    • /proc/sys/kernel/pid_max 값에 도달해서 처음부터 다시 할당하기 전까진 앞선 pid가 비어있더라도 재사용되지 않는다.

5.2.2 프로세스 계층

  • Spawn(새로운 프로세스를 생성하는) 프로세서를 부모 프로세스라고 한다.
  • 새롭게 생성된 프로세스를 자식 프로세스라고 한다.
  • init 프로세스를 제외한 모든 프로세스는 다른 프로세스로부터 생성된다.
    • → 그래서 모든 자식 프로세스에는 부모 프로세스가 있다.
    • ppid 로 확인가능함.
  • 모든 프로세스는 사용자그룹이 소유하고 있다.
    • 모든 자식 프로세스는 부모 프로세스의 사용자와 그룹 권한을 상속받는다.
    • 접근 권한을 제어하기 위해 사용됨.
  • 프로세스 그룹
    • 프로세스와 다른 프로세시의 관계를 표현하고 있음.
1
2
3
4
5
6
7
8
9
10
11
12
13
$ echo $$
19610

$ cat | grep | wc -l

////////// other shell /////////
$ ps -A -o pid,ppid,pgid,sid,command

  PID  PPID  PGID   SID COMMAND
19610 18942 19610 19610 /usr/bin/zsh
29844 19610 29844 19610 cat
29845 19610 29844 19610 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-d
29846 19610 29844 19610 wc -l

5.2.3 pid_t

  • pid 는 pid_t 자료형으로 표현됨

    • C의 int 자료형에 대한 typedef이다.
    1
    2
    3
    4
    5
    
    typedef __kernel_pid_t __pid_t;
    
    #ifndef __kernel_pid_t
    	typedef int __kernel_pid_t;
    #endif
    
    • pid_t 자료형은 사실 int 형이다.

5.2.4 프로세스 ID와 부모 프로세스 ID 얻기

  • getpid() 시스템 콜은 호출한 프로세스의 pid를 반환함.
1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t getpid (void);
  • getppid() 는 호출한 프로세스의 부모 프로세스 pid를 반환함
1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t getppid (void);
  • 예제
    1
    2
    
    printf ("My pid=%jd\n", (intmax_t) getpid ());
    printf ("Parent's pid=%jd\n", (intmax_t) getppid ());
    

5.3 새로운 프로세스 실행하기

  • 유닉스에서는
    1. 프로그램 이미지를 메모리에 적재하고 실행하는 과정
    2. 새로운 프로세스를 생성하는 과정
      • 두 가지가 분리되어 있음.

5.3.1 exec 함수들

  • exec 류 시스템 콜은 한 가지로 제공되지 않고 여러 형태로 제공된다.

  • 먼저 excel을 알아보자

1
2
3
4
5
#include <unistd.h>

int execl (const char *path,
           const char *arg,
           ...);
  • 호출하면 현재 프로세스를 path가 가리키는 프로그램으로 대체한다.
  • arg
    • path에 명시된 프로그램을 위한 첫 번째 인자다.
  • ...
    • 가변인자
    • 뒤에 다른 인자가 여럿 올 수 있음
    • 마지막은 반드시 NULL로 끝나야함.
  • 예제

    1
    2
    3
    4
    5
    
    int ret;
    
    ret = execl ("/bin/vi", "vi", NULL);
    if (ret == 1)
            perror ("execl");
    
    • 실행 파일의 경로인 path의 마지막 요소 vi 를 첫 번째 인자로 두어, 프로세스의 fork()/exec 과정에서 argv[0]을 검사하여 바이너리 이미지의 이름을 찾을 수 있도록 한다.
  • 일반적으로 반환값이 없다.
    • 에러 발생시 -1 반환, errno 설정
    • 성공 시
      • 새로운 프로그램의 시작점으로 건너 뛰므로 이전에 실행했던 코드는 그 프로세스의 주소 공간에 더 이상 존재하지 않음.
      • 대기 중인 시그널 사라짐
      • 프로세스가 받은 시그널은 시그널 핸들러가 더 이상 프로세스의 주소 공간에 존재하지 않으므로 디폴트 방식으로 처리됨
      • 메모리 락이 해제됨
      • 스레드의 속성 대부분이 기본값으로 돌아감
      • 프로세스의 통계 대부분이 재설정됨
      • 맵핑된 파일을 포함하여 프로세스의 메모리 주소 공간과 관련된 모든 내용이 사라짐
      • 사용자 영역에만 존재하는 모든 내용이 사라짐.
  • Q) 마지막에 NULL을 넣는 이유는 뭘까?
  • 즉, fork 로 child 프로세스를 만든 후 그 프로세스를 새로운 독립적인 프로세스로 만들어 주는 역할을 한다.

다른 exec 함수들

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>

int execlp (const char *file,
            const char *arg,
            ...);

int execle (const char *path,
            const char *arg,
            ...,
            char * const envp[]);

int execv (const char *path, char *const argv[]);

int execvp (const char *file, char *const argv[]);

int execve (const char *filename,
            char *const argv[],
            char *const envp[]);
  • exec 라는 기본 이름 뒤에 함수의 특징을 나타내는 알파벳이 뒤따름.
    • l 포함하는 함수 : 인자를 리스트로 전달함.
    • v 포함하는 함수 : 인자를 배열로 전달함.
    • e 포함하는 함수 : 새로 생성되는 함수의 환경변수를 지정함
    • p 포함하는 함수 : 실행파일을 사용자의 환경변수에서 찾도록함
  • exec 함수군 중에서 execve() 만 시스템 콜이고 다른 것들은 래퍼 함수임.
  • 인자로 배열을 사용하면 인자를 실행 시간에 결정할 수 있다는 장점이 있음
  • Q) 바로 위의 문장 무슨 뜻?
  • Q)tip 으로 나온 execlp()와 execvp() 함수의 보안위험 무슨말?? 205페이지

5.3.2 fork() 시스템 콜

  • fork 시스템 콜을 사용해서 현재 실행 중인 프로세스와 동일한 프로세스를 새롭게 실행할 수 있다.
1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t fork (void);
  • fork() 호출이 성공하면 fork() 를 실행한 프로세스와 거의 모든 내용이 동일한 새로운 프로세스를 생성함.
  • 두 프로세스는 계속 실행 상태
  • 성공 시 부모 프로세스에서는 fork() 시스템 콜의 반환값은 자식 프로세스의 pid가 됨.
  • 필수적인 항목을 제외하고는 거의 모든 측면에서 자식,부모가 동일
    • 자식의 pid는 새롭게 할당됨
    • 자식의 ppid는 부모의 pid
    • 자식 프로세스에서 리소스 통계는 0으로 초기화됨
    • 처리되지 않은 시그널은 모두 사라지고 자식 프로세스로 상속되지 않음.
    • 부모가 가지고 있던 파일 락은 상속되지 않음
  • 호출 실패 시 -1반환, errno 설정
  • 예제

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    pid_t pid;
    
    pid = fork ();
    if (pid > 0)
            printf ("I am the parent of pid=%d!\n", pid);
    else if (!pid)
            printf ("I am the child!\n");
    else if (pid == 1)
            perror ("fork")
    
  • 가장 흔한 예제는 새로운 프로세스를 생성하고 그 후에 새 프로세스에 새로운 바이너리 이미지를 올리는 것임.

    • 즉 ,fork() 하고 자식 프로세스는 exec 를 진행함
    • 예제

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      
      pid_t pid;
      
      pid = fork ();
      if (pid == 1)
              perror ("fork");
      
      /* the child ... */
      if (!pid) {
              const char *args[] = { "windlass", NULL };
              int ret;
      
              ret = execv ("/bin/windlass", args);
              if (ret == 1) {
                      perror ("execv");
                      exit (EXIT_FAILURE);
              }
      }
      
      • 부모 프로세스는 자식 프로세스가 생겼다는 사실 외에는 아무런 변화 없이 진행됨

copy-on-write

  • 기존에는 fork() 수행 시 페이지 단위로 복사했음
  • 최신 유닉스 시스템은 주소 공간 모두 복사하는 것이 아니라 페이지에 대한 COW를 수행함.
  • COW
    • 복사에 의한 부하를 완화하기 위한 최적화기법.
    • 자신이 가지고 있는 리소스의 읽기 요청이 들어오더라도 포인터만 넘겨받으면 된다는 전제에서 시작함.
    • 쓰기 작업을 할 경우에만 복사가 일어남.

vfork()

  • 쓸모없는 주소 공간 복사 문제를 해결하기 위한 옛날 노력
  • fork()와 같은 동작을 하지만 자식 프로세스는 즉시 exec 계열의 함수를 성공적으로 호출하든가, _exit() 함수를 호출해서 프로세스를 끝내야함
  • vfork() 구현은 버그를 수반함
    • exec 호출이 실패할 경우는?
    • 자식 프로세스가 어떻게 처리해야할지 파악하거나 종료하기 전까지는 계속 멈춰있음.
    • Q) 요부분 다시 이해하기

5.4 프로세스 종료하기

1
2
3
#include <stdlib.h>

void exit (int status);
  • exit()을 호출하면 몇 가지 기본적인 종료 단계를 거쳐 커널이 프로세스를 종료함.
  • 반환값이 없기 때문에 exit 호출 이후 명령은 의미가 없음
  • 종료 순서
    1. atexit()이나 on_exit()에 등록된 함수가 있다면 등록 수선의 역순으로 호출
    2. 열려있는 모든 표준 입출력 스트림의 버퍼를 비운다.
    3. tmpfile() 함수를 통해 생성한 임시 파일을 삭제
  • 프로세스가 사용자 영역에서 해야하는 모든 작업을 종료시킴.
  • 마지막으로 exit()은 _exit() 시스템 콜을 실행해서 나머지 단계를 커널이 처리하게 한다.
    • 프로세스가 종료되면 커널은 해당 프로세스가 생성한 더 사용되지 않는 모든 리소스를 정리한다.
  • _exit()을 직접 사용하면 표준 출력 스트림을 비우는 등의 사후 처리를 직접 해야한다.
    • vfork()를 사용하면 _exit() 을 사용해야함.

5.4.1 프로세스를 종료하는 다른 방법

  • 고전적인 방법은 시스템 콜을 사용하지 않고, 단순히 프로그램을 끝까지 진행시키는 것임
    • main() 함수가 반환되는 경우
    • 하지만 이런 경우도 컴파일러가 프로그램 종료 코드 이후에 exit() 시스템 콜을 묵시적으로 추가한다.
  • SIGTERMSIGKILL 을 보내서 종료도 가능
  • 커널에 밉보이는 방법
    • 잘못된 연산 수행
    • 세그멘테이션 폴트 일으킴
    • 메모리 고갈
    • 리소스 과다 소모
    • → 프로세스를 강제로 죽임

5.4.2 atexit()

  • atexit() 함수는 프로세스가 종료될 때 실행할 함수를 등록하기 위한 용도로 사용됨.
1
2
3
#include <stdlib.h>

int atexit (void (*function)(void));
  • 정상적으로 실행되면 프로세스가 정상적으로 종료될 때 호출할 함수를 등록함.
  • 등록할 함수는 아무런 인자도 갖지 않고 어떠한 값도 반환하지 않는 함수여야함.
    • 이 함수들을 스택에 저장되며 LIFO 방식으로 실행됨.
    • 등록된 함수에서 exit()을 호출한다면 무한 루프에 빠진다. _exit() 을 사용하자
  • 예제

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    #include <stdio.h>
    #include <stdlib.h>
    
    void out (void)
    {
            printf ("atexit() succeeded!\n");
    }
    
    int main (void)
    {
            if (atexit (out))
                    fprintf(stderr, "atexit() failed!\n");
    
            return 0;
    }
    

5.4.3 on_exit()

  • SunOS4 는 atexit()와 동일한 자신만의 함수를 정의하고 있으며, 리눅스의 glibc에서도 지원한다.
1
2
3
#include <stdlib.h>

int on_exit (void (*function)(int, void *), void *arg);
  • atexit()와 동일하게 동작하지만 등록할 수 있는 함수의 프로토 타입이 다름.
1
void my_function (int status, void *arg);

5.4.4 SIGCHLD

  • 프로세스가 종료될 때 커널은 SIGCHLD 시그널을 부모 프로세스로 보낸다.
  • 기본적으로 부모 프로세스는 이 시그널을 무시하며 아무런 행동도 하지 않음.
  • 하지만 프로세스는 signal() 이나 sigaction() 시스템 콜을 사용해서 처리한다.
  • 부모 프로세스 관점에서는 자식 프로세스의 종료가 비동기로 일어남

5.5. 자식 프로세스 종료 기다리기

  • 시그널을 통해 알림을 받는 방법도 훌륭하지만, 많은 부모 프로세스는 자식 프로세스 중 하나가 종료될 때 좀 더 많은 정보를 얻고자 함.
  • 자식 프로세스가 완전히 사라져버리면 정보를 얻을 수 없어서, 유닉스 초기 설계자들은 자식 프로세스가 부모 프로세스보다 먼저 죽으면 자식 프로세스를 좀비 프로세스로 바꾸기로 함.
    • 아주 최소한의 기본적인 커널 자료구조만 가지고 있음.
    • 좀비프로세스는 부모 프로세스가 자신의 상태를 조사하도록 기다림.
    • 부모 프로세스가 종료된 자식 프로세스로부터 정보를 회수한 다음에야 공식적으로 종료됨.
  • 리눅스 커널은 종료된 자식 프로세스에 대한 정보를 얻기 위해 몇 가지 인터페이스를 제공함
  • wait()
1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait (int *status);
  • wait()을 호출하면 종료된 프로세스의 pid를 반환하며 에러가 발생한 경우 -1을 반환.
  • 만약 자식 프로세스가 종료되지 않았다면 자식 프로세스가 종료될 때까지 블록됨. (대기 상태)
  • 이미 종료된 상태라면 호출은 즉시 반환됨.
  • 예제

    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
    31
    32
    33
    34
    35
    36
    37
    
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main (void)
    {
            int status;
            pid_t pid;
    
            if (!fork ())
                    return 1;
    
            pid = wait (&status);
            if (pid == 1)
                    perror ("wait");
    
            printf ("pid=%d\n", pid);
    
            if (WIFEXITED (status))
                    printf ("Normal termination with exit status=%d\n",
                            WEXITSTATUS (status));
    
            if (WIFSIGNALED (status))
                    printf ("Killed by signal=%d%s\n",
                            WTERMSIG (status),
                            WCOREDUMP (status) ? " (dumped core)" : "");
    
            if (WIFSTOPPED (status))
                    printf ("Stopped by signal=%d\n",
                            WSTOPSIG (status));
    
            if (WIFCONTINUED (status))
                    printf ("Continued\n");
    
           return 0;
    }
    
    • 이 프로그램은 즉시 종료되는 자식 프로세스를 생성한다.
    • 부모 프로세스는 wait() 시스템 콜을 호출해서 자식 프로세스의 상태를 확인한다. 자식 프로세스의 pid와 어떻게 종료되었는지를 출력함.

5.5.1 특정 프로세스 기다리기

  • 자식프로세스의 행동을 관찰하는 것은 중요한데, 모든 자식프로세스의 상황을 확인하는 것은 번거롭다.
  • 기다리길 원하는 프로세스의 pid를 알고있다면 waitpid() 시스템 콜을 사용할 수 있다.
1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid (pid_t pid, int *status, int options);
  • watipid()wait() 보다 훨씬 강력한 버전이다.
  • pid
    • < -1
      • 프로세스 gid가 이 값의 절댓값과 동일한 모든 자식 프로세스를 기다림.
    • -1
      • 모든 자식 프로세스를 기다린다. 이렇게 하면 wait()와 동일하게 동작함
    • 0
      • 호출한 프로세스와 동일한 프로세스 그룹에 속한 모든 자식 프로세스를 기다림
    • 0

      • 인자로 받은 pid와 일치하는 자식 프로세스를 기다린다.
  • status
    • wait() 의 status와 동일하게 동작
  • options
    • WNOHANG
      • 이미 종료된 자식 프로세스가 없다면 블록되지 않고 바로 반환
    • WUNTRACED
      • 호출하는 프로세스가 자식 프로세스를 추적하지 않더라도 반환되는 status 인자에 WIFSTOPPED비트가 설정됨
    • WCONTINUED
  • waitpid() 는 상태가 바뀐 프로세스의 pid를 반환함.
    • WNOHANG 이 설정되고 지정한 자식 프로세스의 상태가 아직 바뀌지 않았다면 0을 반환
  • 에러 발생시 -1 반환, errno 설정
  • 예제

    • pid가 1742인 특정 자식 프로세스의 반환값을 알려고 하며 자식 프로세스가 아직 종료되지 않았다면 즉시 반환 하는 예제
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    int status;
    pid_t pid;
    
    pid = waitpid (1742, &status, WNOHANG);
    if (pid == 1)
            perror ("waitpid");
    else {
            printf ("pid=%d\n", pid);
    
            if (WIFEXITED (status))
                    printf ("Normal termination with exit status=%d\n",
                            WEXITSTATUS (status));
    
            if (WIFSIGNALED (status))
                    printf ("Killed by signal=%d%s\n",
                            WTERMSIG (status),
                            WCOREDUMP (status) ? " (dumped core)" : "");
    }
    
  • 사용법

    1
    2
    3
    4
    5
    
    wait (&status);
    
    둘은 동일
    
    waitpid (1, &status, 0);
    

5.5.2 좀 더 다양한 방법으로 기다리기

  • waitid()
1
2
3
4
5
6
#include <sys/wait.h>

int waitid (idtype_t idtype,
            id_t id,
            siginfo_t *infop,
            int options);
  • wait() 이나 waitpid()와 마찬가지로 waitid()는 자식 프로세스를 기다리고 상태변화 (종료, 멈춤, 다시 실행) 을 얻기 위해 사용함.
  • 더 많은 옵션을 제공하는 대신 훨씬 복잡함.
  • pid인자 하나로 기다리는 것이 아니라, idtype 과 id 인자로 기다릴 자식 프로세스를 지정함.
  • idtype
    • P_PID
      • pid가 id와 일치하는 자식 프로세스를 기다림
    • P_GID
      • gid가 id와 일치하는 자식 프로세스를 기다림
    • P_ALL
      • 모든 자식 프로세스를 기다림. id는 무시됨
  • id_t 타입은 거의 보기 힘든 타임. 일반적인 식별 번호를 나타내는 타입이다.
    • 나중에 새로운 idtype 값이 추가되었을 경우를 대비하여 미리 정의된 타입이 나중에 새롭게 생성된 식별자를 저장할 수 있도록 충분한 여유를 제공하기 위해서임.
  • options
    • WEXITED
    • WSTOPPED
    • WCONTINUED
    • WNOHANG
    • WNOWAIT
  • 성공적으로 반환하면 유효한 siginfo_t 타입을 가리키는 infop 인자에 값을 채운다.
  • siginfo_t 타입
    • si_pid
    • si_uid
    • si_code
      • 자식프로세스의 종료상태를 나타냄
    • si_signo
    • si_status
  • 성공하면 0반환, 에러 발생 시 -1 반환하고 errno 설정
  • waitid() 는 siginfo_t 구조체를 통해 다양한 정보를 얻을 수 있다. 근데 만약 이런 정보가 필요하지 않다면 단순 함수를 사용하는게 시스템 이식성을 위해서 더 바람직하다.

5.5.3 BSD 방식으로 기다리기 : wait3()과 wait4()

  • BSD는 자식 프로세스의 상태 변화를 기다리기 위한 두 가지 독자적인 함수를 제공함.
    • Berkeley Software Distribution
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>

pid_t wait3 (int *status,
             int options,
             struct rusage *rusage);

pid_t wait4 (pid_t pid,
             int *status,
             int options,
             struct rusage *rusage);
1
2
3
pid_t wait3(int* status, int options, struct rusage* rusage) {
  return wait4(-1, status, options, rusage);
}
  • 함수 뒤에 붙은 숫자는 인자의 개수를 뜻한다.
  • rusage 인자만 예외로 하면 두 함수는 waitpid와 흡사하다.
  • 예제

    • wait3()
    1
    2
    3
    4
    5
    
    pid = wait3 (status, options, NULL);
    
    동일
    
    pid = waitpid (1, status, options);
    
    • wait4()
    1
    2
    3
    4
    5
    
    pid = wait4 (pid, status, options, NULL);
    
    동일
    
    pid = waitpid (pid, status, options);
    
  • wait3()는 모든 자식 프로세스의 상태 변화를 기다리며, wait4()는 pid인자로 지정한 특정 자식 프로세스의 상태 변화만 기다린다.
  • options 인자는 waitpid와 동일
  • rusage (Resource Usage)

    • waitpid와 가장 큰 차이점인데, rusage 포인터가 NULL이 아니면 자식 프로세스에 관한 정보를 채워넣는다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    #include <sys/resource.h>
    
    struct rusage {
            struct timeval ru_utime; /* user time consumed */
            struct timeval ru_stime; /* system time consumed */
            long ru_maxrss;   /* maximum resident set size */
            long ru_ixrss;    /* shared memory size */
            long ru_idrss;    /* unshared data size */
            long ru_isrss;    /* unshared stack size */
            long ru_minflt;   /* page reclaims */
            long ru_majflt;   /* page faults */
            long ru_nswap;    /* swap operations */
            long ru_inblock;  /* block input operations */
            long ru_oublock;  /* block output operations */
            long ru_msgsnd;   /* messages sent */
            long ru_msgrcv;   /* messages received */
            long ru_nsignals; /* signals received */
            long ru_nvcsw;    /* voluntary context switches */
            long ru_nivcsw;   /* involuntary context switches */
    };
    
  • 성공할 경우 상태가 변경된 프로세스의 pid를 반환함. 실패 시 -1반환하고 errno 설정
  • wait3 와 wait4 는 POSIX가 정의한 함수는 아니므로 리소스 사용정보가 매우 중요한 경우에만 사용하자.

5.5.4 새로운 프로세스를 띄운 다음에 기다리기

  • ANSI C와 POSIX는 새로운 프로세스를 생성하고 종료를 기다리는 동작을 하나로 묶은, 말하자면 동기식 프로세스 생성 인터페이스를 정의하고 있음.
1
2
3
4
#define _XOPEN_SOURCE    /* if we want WEXITSTATUS, etc. */
#include <stdlib.h>

int system (const char *command);
  • 함수이름이 system인 이유는 동기식 프로세스 생성이 시스템 외부로 셸 띄우기라고 불리기 때문임.
    • 흔히 간단한 유틸리티나 셸 스크립트를 실행할 목적으로 system() 을 사용하는데 종종 실행 결과의 반환값을 얻기 위한 명시적인 목적으로 사용하기도 한다.
  • command
    • 주어진 명령을 실행한다.
    • /bin/sh -c 뒤에 command가 따라 붙음
    • 즉, 셸에 그대로 전달되는 것
    • NULL 이면 /bin/sh가 유효하면 0이 아닌 값, 그렇지 않다면 0 반환
  • 호출이 성공하면 wait()와 마찬가지로 그 명령의 상태를 반환함.
  • 실행한 명령의 종료 코드는 WEXITSTATUS 로 얻을 수 있다. 명령어 수행에 실패했다면 exit(127)과 동일
  • command 를 실행하는 동안 SIGCHLD 는 블록되고 SIGINT와 SIGQUIT는 무시된다.

    • 몇 가지 주의점

      • system()이 반복문안에서 실행될 때 문제가 발생함.
        • 프로그램이 자식 프로세스의 종료 상태를 적절하게 검사할 수 있도록 해야한다.
      • 예제

        1
        2
        3
        4
        5
        6
        7
        8
        9
        
        do {
                int ret;
        
                ret = system ("pidof rudderd");
                if (WIFSIGNALED (ret) &&
                    (WTERMSIG (ret) == SIGINT ||
                     WTERMSIG (ret) == SIGQUIT))
                        break; /* or otherwise handle */
        } while (1);
        
  • fork(), exec 함수군, waitpid()를 사용해서 system() 구현하기

    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
    31
    32
    33
    34
    
    /*
     * my_system - synchronously spawns and waits for the command
     * "/bin/sh -c <cmd>".
     *
     * Returns −1 on error of any sort, or the exit code from the
     * launched process. Does not block or ignore any signals.
     */
    int my_system (const char *cmd)
    {
            int status;
            pid_t pid;
    
            pid = fork ();
            if (pid == 1)
                    return 1;
            else if (pid == 0) {
                    const char *argv[4];
    
                    argv[0] = "sh";
                    argv[1] = "-c";
                    argv[2] = cmd;
                    argv[3] = NULL;
                    execv ("/bin/sh", argv);
    
                    exit (1);
            }
    
            if (waitpid (pid, &status, 0) == 1)
                    return 1;
            else if (WIFEXITED (status))
                    return WEXITSTATUS (status);
    
            return 1;
    }
    
    • System 의 문제
      • child의 stdout 등 의 값을 볼 수가 없음.
      • popen을 쓴다?

5.5.5 좀비 프로세스

  • 실행을 마쳤지만 부모 프로세스에서 종료 코드를 읽어가지 않은, 즉 부모 프로세스에서 wait() 시스템 콜을 호출하지 않은 프로세스를 의미
  • 최소한의 기본 뼈대만 유지할만큼 적은 리소스를 차지하지만 어쨌든 시스템 리소스를 계속 소비하고 있음.
  • 부모 프로세스가 종료될 때마다 리눅스 커널은 그 프로세스의 자식 프로세스 목록을 뒤져서 모두 init 프로세스 (pid = 1) 의 자식으로 입양시킨다.
    • 고아 프로세스가 생기지 않도록 보장함.

5.6 사용자와 그룹

5.6.1 실제, 유효, 저장된 사용자 ID와 그룹 ID

  • 그룹아이디도 아래와 동일하게 적용됨
  • 실제 사용자 ID (Rreal User ID)
    • 프로세스를 최초로 실행한 사용자의 ID
  • 유효 사용자 ID (Effective User ID)
    • 프로세스가 현재 영향을 미치고 있는 사용자 ID
  • 저장된 사용자 ID (Saved User ID)
    • 프로세스의 최초 유효 사용자 ID
  • → 유효한 사용자 ID가 가장 중요한 값. 이는 프로세스의 자격을 확인하는 과정에서 점검하는 사용자 ID이다

5.6.2 실제, 저장된 사용자, 그룹 ID 변경하기

1
2
3
4
5
#include <sys/types.h>
#include <unistd.h>

int setuid (uid_t uid);
int setgid (gid_t gid);
  • setuid()
    • 호출하면 현재 프로세스의 유효 사용자 ID를 설정한다.
      • 현재 유효 사용자 ID가 0(root) 이면 실제 사용자저장된 사용자 ID 역시 설정됨
    • 성공하면 0반환, 실패 시 -1 반환 errno 설정
  • 앞의 내용은 그룹 ID에도 동일하다.

5.6.3 유효 사용자 ID나 유효 그룹 ID 변경하기

1
2
3
4
5
#include <sys/types.h>
#include <unistd.h>

int seteuid (uid_t euid);
int setegid (gid_t egid);
  • seteuid() 를 호출하면 유효 사용자 ID 를 euid로 설정한다.
  • root 사용자는 euid 값으로 어떤 값이든 사용할 수 있다.
  • 비 root 사용자는 seteuid() 와 setuid() 가 동일하게 동작함.
  • 앞의 내용은 그룹 ID에도 동일하다.

5.6.4 BSD 방식으로 사용자, 그룹 ID 변경하기

1
2
3
4
5
#include <sys/types.h>
#include <unistd.h>

int setreuid (uid_t ruid, uid_t euid);
int setregid (gid_t rgid, gid_t egid);
  • setreuid() 를 호출하면 프로세스의 실제 사용자 ID와 유효 사용자 ID를 각각 ruid와 euid로 설정한다.

5.6.5 HP-UX 방식으로 사용자, 그룹 ID 변경하기

1
2
3
4
5
#define _GNU_SOURCE
#include <unistd.h>

int setresuid (uid_t ruid, uid_t euid, uid_t suid);
int setresgid (gid_t rgid, gid_t egid, gid_t sgid);
  • setresuid() 를 호출하면 실제, 유효, 그리고 저장된 사용자 ID를 각각 ruid, euid, suid로 설정한다.
    • -1 을 지정하면 ID는 변경하지 않은채로 놔둔다.

5.6.6 바람직한 사용자/그룹 ID 조작법

  • 비 root 프로세스는 seteuid() 를 사용해서 유효 사용자 ID를 바꿔야한다.

5.6.7 저장된 사용자 ID 지원

5.6.8 사용자 ID와 그룹 ID 얻어오기

1
2
3
4
5
#include <unistd.h>
#include <sys/types.h>

uid_t getuid (void);
gid_t getgid (void);
1
2
3
4
5
#include <unistd.h>
#include <sys/types.h>

uid_t geteuid (void);
gid_t getegid (void);
  • 이 함수들은 실패하지 않으며 유효 사용자 ID와 유효 그룹 ID를 반환함.

5.7 세션과 프로세스 그룹

  • 각 프로세스는 작업 제어 목적으로 관련된 하나 이상의 프로세스를 모아놓은 집합인 프로세스 그룹의 일원이다.
  • 프로세스 그룹의 주된 속성은 그룹 내 모든 프로세스에게 시그널을 보낼 수 있다는 점이다.
  • 각 프로세스 그룹은 pgid 로 구분하며 프로세스 그룹마다 그룹 리더가 있다.
  • 구성원이 하나라도 남아있는 동안에는 그룹이 사라지지 않음
  • 그룹 리더가 종료되더라도 프로세스 그룹은 남는다.

  • 새로운 사용자가 처음으로 시스템에 로그인하면 로그인 프로세스는 사용자 로그인 셸 프로세스 하나로 이루어진 새로운 세션을 생성한다.
  • 세션은 하나 이상의 프로세스 그룹이 들어 있는 집합이다.
  • 로그인한 사용자 활동을 처리하며 사용자의 터미널 입출력을 다루는 tty 장치로 명시되어 제어 터미널과 사용자 사이를 연결한다.
  • 대부분 셸과 관련을 맺고 있다.

  • 프로세스 그룹이 작업 제어와 다른 셸 기능을 쉽게 하도록 모든 구성원에게 시그널을 보내는 메커니즘을 제공한다면 세션은 제어 터미널을 둘러싼 로그인을 통합하는 기능을 제공한다.
  • 세션에 속한 프로세스 그룹은 하나의 Foreground 프로세스 그룹과 0개 이상의 Background 프로세스 그룹으로 나뉜다.
    • 사용자가 터미널을 종료하면 Foreground 프로세스 그룹 내 모든 프로세스에 SIGQUIT 시그널이 전달됨
    • 터미널에서 네트워크 단절이 포착되면 Foreground 프로세스 그룹 내 모든 프로세스에 SIGHUP 시그널이 전달됨.
    • 사용자가 Ctrl + C 같은 취소 명령을 입력했을 경우 Foreground 프로세스 그룹 내 모든 프로세스에 SIGINT 시그널이 전달됨.
  • 예제
    • 사용자가 시스템에 로그인했고, bash의 pid가 1700일 때 bash 인스턴스는 gid가 1700인 새로운 프로세스 그룹의 유일한 멤버이자 리더가 됨.
      • 그 셸에서 실행하는 명령어는 세션 1700에 속하는 새로운 프로세스 그룹에서 동작.
      • 사용자에게 직접 연결되어 있고 터미널 제어가 가능한 프로세스 그룹 중 하나가 포어그라운드 프로세스 그룹
      • 다른 프로세스 그룹은 백그라운드 프로세스 그룹
    • 다른 예제
      1
      
      $ cat ship-inventory.txt | grep booty | sort
      
      • 세 개의 프로세스를 가지는 하나의 프로세스 그룹은 생성함.
      • → 셸에서 한번에 세 프로세스 모두에 시그널을 보낼 수 있다는 뜻.
      • 사용자가 명령어 뒤에 &를 붙인다면 백그라운드로 돈다. Untitled

5.7.1 세션 시스템 콜

  • 시스템에 로그인을 하는 시점에 셸은 새로운 세션을 생성한다. 이 작업은 새로운 세션을 쉽게 만들 수 있는 특수한 시스템 콜을 통해서 이루어진다.
1
2
3
#include <unistd.h>

pid_t setsid (void);
  • setsid() 를 호출하면 그 프로세스가 프로세스 그룹의 리더가 아니라고 가정하고 새로운 세션을 생성한다.
  • 호출한 프로세스는 새롭게 만들어진 세션의 유일한 멤버이자 리더가 되며 제어 tty를 가지지 않는다.
  • → setsid() 는 새로운 세션 내부에 새로운 프로세스 그룹을 생성하며 호출한 프로세스를 그 세션과 프로세스 그룹 모두의 리더로 정한다.
  • 호출 성공 시 새롭게 생성한 세션의 ID 반환, 실패 시 -1 반환하고 errno 설정.(EPERM 뿐)
  • 예제

    • 어떤 프로세스가 프로세스 그룹 리더가 되지 않게 하는 가장 손쉬운 방법은 프로세스를 포크하고 부모 프로세스를 종료한 다음 자식 프로세스에서 setsid() 를 호출하는 것
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    pid_t pid;
    
    pid = fork ();
    if (pid == 1) {
            perror ("fork");
            return 1;
    } else if (pid != 0)
            exit (EXIT_SUCCESS);
    
    if (setsid () == 1) {
            perror ("setsid");
            return 1;
    }
    
  • 세션 ID를 얻는 법

    1
    2
    3
    4
    
    #define _XOPEN_SOURCE 500
    #include <unistd.h>
    
    pid_t getsid (pid_t pid);
    
    • getsid() 호출이 성공하면 pid가 가리키는 프로세스 세션 ID를 반환한다.
    • Q) 왜 유용하지 않음?
    • getsid() 는 드물지만 주로 진단 목적으로 사용함

      • pid값이 0 이면 getsid() 를 호출한 프로세스의 세션 ID를 반환한다.
      1
      2
      3
      4
      5
      6
      7
      
      pid_t sid;
      
      sid = getsid (0);
      if (sid == 1)
              perror ("getsid"); /* should not be possible */
      else
              printf ("My session id=%d\n", sid);
      

5.7.2 프로세스 그룹 시스템 콜

  • setpgid() 는 pid 인자로 지정한 프로세스의 프로세스 그룹 ID를 pgid로 설정한다.
1
2
3
4
#define _XOPEN_SOURCE 500
#include <unistd.h>

int setpgid (pid_t pid, pid_t pgid);
  • pid인자가 0인 경우 현재 프로세스의 프로세스 그룹 ID를 변경하며 pgid 인자가 0인 경우 pid 인자로 지정한 프로세스의 ID를 프로세스 그룹 ID로 설정한다.
  • 호출 성공 여부
    • pid로 지정한 프로세스가 해당 시스템 콜을 호출하는 프로세스이거나 호출하는 프로세스의 자식 프로세스이며 아직 exec 호출을 하지 않았고 부모 프로세스와 동일한 세션에 속해 있어야 한다.
    • pid로 지정한 프로세스가 세션의 리더가 아니어야 한다.
    • pgid가 이미 있으면 호출하는 프로세스와 동일한 세션에 속해 있어야 한다.
    • pgid 값이 양수여야 한다.
  • 세션과 마찬가지로 프로세스의 프로세스 그룹 ID를 얻는 것도 가능하지만 유용하지 않음.

    1
    2
    3
    4
    
    #define _XOPEN_SOURCE 500
    #include <unistd.h>
    
    pid_t getpgid (pid_t pid);
    
  • Q) 왜 유용하지 않음?

5.7.3 사용되지 않는 프로세스 그룹 관련 함수들

  • 리눅스는 프로세스 그룹 ID를 가져오거나 설정하는 두 가지 오래된 BSD 인터페이스를 지원한다.
  • 프로세스 그룹 ID를 설정할 때 사용

    1
    2
    3
    
    #include <unistd.h>
    
    int setpgrp (void);
    
    1
    2
    
    if (setpgrp () == 1)
            perror ("setpgrp");
    
    • 다음 setpgid() 코드와 동일함
    1
    2
    
    if (setpgid (0,0) == 1)
            perror ("setpgid");
    
    • 둘 다 현재 프로세스를 현재 프로세스의 pid와 같은 프로세스 그룹으로 설정함.
  • getpgrp() 함수는 프로세스의 그룹 ID를 알아내기 위해 사용함

    1
    2
    3
    
    #include <unistd.h>
    
    pid_t getpgrp (void);
    
    1
    
    pid_t pgid = getpgrp ();
    
    • getpgid() 코드와 동일
    1
    
    pid_t pgid = getpgid (0);
    

5.8 데몬

  • Daemon 은 백그라운드에서 수행되며 제어 터미널이 없는 프로세스이다.
  • 일반적으로 부팅 시에 시작되며 root 혹은 다른 특수한 사용자 계정 (apache, postfix … ) 권한으로 실행되어 시스템 수준의 작업을 처리한다.
  • 데몬의 두 가지 일반적인 요구사항
    1. 반드시 init 프로세스의 자식 프로세스여야 함.
    2. 터미널과 연결되어 있으면 안됨.
  • 다음 과정을 통해 데몬이 될 수 있다.
    1. fork() 를 호출해서 데몬이 될 새로운 프로세스를 생성
    2. 부모 프로세스에서 exit() 을 호출해서 데몬 프로세스의 부모 프로세스를 종료한다.
    3. setsid()를 호출해서 데몬이 새로운 프로세스 그룹과 세션의 리더가 되도록 한다.
    4. chdir() 를 사용하여 작업 디렉터리를 루트 디렉터리로 변경한다.
    5. 모든 fd 를 닫는다.
    6. 0, 1, 2번 fd(각각 표준 입력,출력,에러) 를 열고 /dev/null 로 리다이렉트 한다.
  • 예제

    • 규칙에 따라 스스로 데몬이 되는 예제 코드
    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <linux/fs.h>
    
    int main (void)
    {
            pid_t pid;
            int i;
    
            /* create new process */
            pid = fork ();
            if (pid == 1)
                    return 1;
            else if (pid != 0)
                    exit (EXIT_SUCCESS);
    
            /* create new session and process group */
            if (setsid () == 1)
                    return 1;
    
            /* set the working directory to the root directory */
            if (chdir ("/") == 1)
                    return 1;
    
            /* close all open files--NR_OPEN is overkill, but works */
            for (i = 0; i < NR_OPEN; i++)
                    close (i);
    
            /* redirect fd's 0,1,2 to /dev/null */
            open ("/dev/null", O_RDWR);     /* stdin */
            dup (0);                        /* stdout */
            dup (0);                        /* stderror */
    
            /* do its daemon thing... */
    
            return 0;
    }
    
  • 대부분의 유닉스 시스템은 C 라이브러리에서 daemon() 함수를 제공하여 이 과정을 자동화하여 간단하게 쓸 수 있다.

    1
    2
    3
    
    #include <unistd.h>
    
    int daemon (int nochdir, int noclose);
    
    • nochdir 인자가 0이 아니면 현재 작업 디렉터리를 루트 디렉터리로 변경하지 않는다.
    • noclose 인자가 0이 아니면 열려있는 모든 fd를 닫지 않는다.
      • 일반적으로 두 인자를 0으로 넘긴다.
This post is licensed under CC BY 4.0 by the author.