C++/linux

(5) 프로세스 관리

로파이 2022. 5. 11. 14:20

프로세스

실행 중인 프로그램을 지칭. 메모리에 적재된 바이너리 이미지, 가상화된 메모리 인스턴스, 열린 파일, 커널 리소스, 사용자 정보, 하나 이상의 스레드를 포함한다.

 

프로세스 ID

  • 프로세스 ID(pid)는 유일한 식별자로 구분된다.
  • 커널이 실행하는 idle 프로세스의 pid는 0이다.
  • 시스템 부팅이 끝나면 커널이 실행하는 최초의 프로세스 init 프로세스의 pid는 1이다.

init 프로세스

init 프로세스는 다음 순서대로 경로에서 찾아 먼저 찾은 프로세스를 실행한다.

  • /sbin/init : 가장 먼저 찾는 init 프로세스
  • /etc/init : 두번째로 찾는 init 프로세스
  • /bin/init : 우선 탐색에 실패했을 때 찾는 init 프로세스
  • /bin/sh : 커널이 앞의 순서댈 init 프로세스를 찾는 데 실패한 뒤에 실행하는 shell의 위치.

init 프로세스는 시스템 초기화, 다양한 서비스 구동, login 프로그램 실행 등을 포함한다.

 

프로세스 ID 할당

최대 pid 값은 부호있는 16비트 정수의 최댓값 32768이다. pid는 증가하는 순서대로 할당된다.

 

프로세스 ID 타입

<sys/types.h>헤더에 pid_t 타입으로 정의된 자료형은 int를 가리킨다.

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);
  • getpid : 호출한 프로세스의 pid를 반환한다.
  • getppid: 호출한 부모 프로세스의 pid를 반환한다.

 

프로세스 실행

int execl (const char* path, const char* arg, ...);

현재 프로세스를 path가 가리키는 프로그램으로 대체한다. arg는 프로그램의 실행을 위한 첫번째 인자이고 추가 인자는 ...으로 넘겨준다. 해당 함수는 현재 프로세스를 새로운 프로세스로 대체하므로 실행 중 인 프로세스의 일부 정보들이 초기화된다.

ex) 대기 중인 시그널, 메모리 락 해제, 프로세스 통계 초기화. 

 

프로세스 생성

pid_t fork(void);

fork() 호출이 성공하면 실행한 프로세스의 거의 모든 내용이 동일한 새로운 프로세스를 생성한다.

호출한 프로세스는 부모 프로세스가 되고 자식 프로세스의 pid를 반환받는다. 생성된 프로세스는 자식 프로세스가 되며 자식 프로세스의 반환값은 0이다.

 

copy-on-write

fork() 자식 프로세스를 만들때 모든 프로세스 메모리를 복사하게되는데 linux는 copy-on-write 방법으로 이를 가능하게 한다.

모든 리소스를 복사하게 되면 부하가 생기는 데 이를 줄이기 위해 지연 생성 방법을 사용한다. copy-on-write는 페이지 단위로 작동하며 한 페이지에 포함된 리소스를 가리키는 주소가 자식 프로세스에서 참조될 때 복사가 수행된다. 따라서 기존 리소스들은 참조되기 전까지 포인터 형태로 복사가 되고 실제 내용 복사는 나중에 이루어지게 된다.

커널은 페이지 관련 자료구조에서 읽기 전용과 copy-on-write로 표시하여 어떤 페이지를 참조하려고 할 때 페이지 폴트를 발생시켜 페이지를 복사하고 copy-on-write 속성을 비운다.

 

프로세스 종료

void exit(int status);

반환값 status를 반환하고 프로세스를 종료시킨다.

이 때 atexit()로 등록한 함수가 있다면 등록 역순으로 호출된다.

 

atexit()

int atexit (void (*function)(void));

프로세스 종료시에 호출되는 함수를 등록하는 시스템 함수이다. 등록한 함수들은 등록 순의 역순으로 (스택 LIFO) 호출된다.

 

SIGCHLD

프로세스가 종료될 때 커널은 SIGCHLD 시그널을 부모 프로세스로 보낸다.

 

자식 프로세스 기다리기

자식 프로세스는 종료 이후 반환값이나 프로세스 정보를 부모 프로세스에 전달하여 정보를 얻고자 한다. 자식 프로세스가 이미 종료하였을 때 최소한의 정보만을 가지고 있는 상태를 좀비 프로세스라고 한다.

pid_t wait (int* status);

wait()를 호출하면 종료된 프로세스이 pid를 반환하며 에러가 발생한 경우 -1을 반환한다.

자식 프로세스가 종료되지 않았다면 종료될 때까지 블록된다.

status에는 종료된 자식 프로세스에 대한 정보를 담고 있고 다음 함수를 통해 비트 정보를 보고 알 수 있다.

int WIFEXITED (status); // 정상 종료
int WIFSIGNALED (stauts); // 시그널에 의해 강제 종료
int WIFSTOPPED (status); // 프로세스 멈춤
int WIFCONTINUED (status); // 프로세스 실행이 다시 진행되었음
int WEXITSTATUS (status); // 프로세스 반환값 (exit(n))

 

특정 프로세스 기다리기

pid_t waitpid (pid_t pid, int* status, int options);

특정 프로세스 id를 알 경우에 호출 가능하다. options로는 or 연산자로 WNOHANG, WUNTRACED, WCONTINUED를 전달 가능하다.

 

셸을 통해 명령 실행하기 system()

int system (const char* command);
  • 유틸리티나 셸 스크립트를 실행하기 위해 사용한다. 
  • command 인자로 주어진 명령을 실행한다. command 인자는 /bin/bash -c <command>로 셸이 실행하기 위한 인자이다.

예시)

#include <stdlib.h>

int main()
{
    int ret = system("ls -al");

    if (WIFEXITED(ret))
    {
        printf("Exit code %d.\n", WEXITSTATUS(ret));
    }

    return 0;
}

 

세션과 프로세스 그룹

각 프로세스는 제어 목적으로 모아둔 하나 이상의 프로세스가 모인 집합, 프로세스 그룹에 속한다.

 

각 프로세스 그룹은 프로세스 그룹 ID로 구분하며 프로세스 그룹마다 그룹 리더가 있다. 그룹원이 하나라도 남아있는 경우 그룹은 사라지지 않는다.

 

새로운 사용자가 시스템에 로그이하면 로그인 프로세스는 사용자 로그인 셸 프로세스 하나로 이루어진 새로운 셰션을 생성하고 로그인 셸은 세션 리더로 동작한다. 세션 리더의 pid는 세션 ID로 사용된다.

 

세션은 하나 이상의 프로세스 그룹으로 이루어진 집합이다.

 

프로세스 그룹이 작업 제어와 다른 셸 기능을 쉽게 하도록 모든 구성원에게 시그널을 보내는 메커니즘을 제공한다면 세션은 제어 터미널을 축으로 로그인을 통합하는 기능을 제공한다.

 

세션에 속한 프로세스 그룹은 하나의 포그라운드 프로세스 그룹과 0개 이상의 백그라운드 프로세스 그룹으로 나뉜다. 사용자가 터미널을 종료하게 되면 포그라운드 프로세스 그룹 내 모든 프로세스에 SIGQUIT 시그널이 전달딘다. 터미널에서 네트워크 단절이 포착되면 포그라운드 프로세스 그룹 내 모든 프로세스에 SIGHUP 시그널이 전달된다. CTRL+C와 같은 사용자 취소 명령어가 입력된 경우 SIGINT 시그널이 포그라운드 그룹 내 모든 프로세스에 전달된다.

 

세션은 하나 이상의 프로세스 그룹으로 이루어지고 프로세스 그룹은 하나 이상의 프로세스로 이루어진다.

cat sample.txt | grep mark | sort

위 셸  명령어는 세 개의 프로세스를 가지는 하나의 프로세스 그룹을 생성한다. 셸에서 한 번에 세 프로세스 모두에 시그널을 보낼 수 있다.

 

새로운 세션 생성하기 setsid

pid_t setsid (void);

setsid()를 호출하면 그 프로세스가 프로세스 그룹의 리더가 아니라고 가정하고 새로운 세션을 생성한다. 호출한 프로세스는 새 세션에 속한 유일한 프로세스이다.

 

데몬 프로세스

데몬은 백그라운드에서 실행됨 제어 터미널이 없는 프로세스이다. 일반적으로 부팅 시에 시작되어 root 혹은 특수한 사용자 계정 권한으로 실행되어 시스템 수준의 작업을 처리한다. 데몬 프로세스는 init 프로세스의 자식이 되어야한다.

 

데몬 프로세스를 만드는 방법

1. fork()를 통해 자식 프로세스를 생성한다.

2. 부모 프로세스에서 exit을 호출하여 종료시킨다.

3. setsid()를 호출하여 자식 프로세스를 프로세스 그룹 및 세션의 리더가 되도록 한다. 프로세스는 제어 터미널과 이제 연관되지 않는다.

4. chdir()로 작업 디렉토리를 최상위 디렉토리(루트)로 변경한다.

5. 모든 파일 디스크립터를 닫는다.

6. 0,1,2 표준 입출력,에러에 관련된 파일 디스크립터를 /dev/null로 리다이렉트한다.

 

데몬 프로세스 예시

int main()
{
    pid_t pid;
    int i;

    pid = fork();
    if (pid == -1)
        return -1;
    else if (pid != 0)
        exit(EXIT_SUCCESS);

    if (setsid() == -1)
        return -1;

    char cwd[PATH_MAX];
    if (getcwd(cwd, sizeof(cwd)) == NULL)
    {
        print_last_error("getcwd()");
        return -1;
    }

    if (chdir("/")== -1)
        return -1;

    for (int i = 3; i < 1024; ++i)
        close(i);

    int null_fd = open("dev/null", O_RDWR);
    dup2(null_fd, STDOUT_FILENO);
    dup2(null_fd, STDIN_FILENO);
    dup2(null_fd, STDERR_FILENO);

    // daemon process
    strcat(cwd, "/daemon.txt");
    const char* msg = "hello world!\n";
    int fd = creat(cwd, 0644);
    write(fd, msg, strlen(msg));

    close(fd);
    close(null_fd);

    return 0;
}

 

dup2 함수

int dup2(int fd, int fd2);

열려있는 파일 디스크립터 fd2를 fd로 리다이렉션 시킨다. 이제 fd2를 통한 입출력은 실제로는 fd를 통해 이루어진다.

 

'C++ > linux' 카테고리의 다른 글

(6) Pthread 쓰레드 / PMutex 뮤텍스  (0) 2022.05.12
(4) 고급 입출력 readv/writev/epoll/mmap  (0) 2022.05.10
(3) 버퍼 입출력  (0) 2022.05.10
(2) 다중 입출력 select와 poll  (0) 2022.05.10
(1) errno와 파일 입출력  (0) 2022.05.10