[Linux_System_programming] 05 프로세스 관리

Linux System Programming (Robert Love) chapter 05 Process management

유닉스는 바이너리 이미지를 메모리에 적재하는 과정에서 새로운 프로세스를 생성하는 부분을 분리함 이는 fork(), exec()라는 두 개의 시스템 콜을 필요

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

  • 바이너리 : 디스크에 저장되어 있는 컴파일된 실행 할 수 있는 코드
  • 프로세스 : 실행 중인 프로그램 (가상화된 메모리 인스턴스, 커널리소스, 보안정보 및 스레드를 지님)
  • 스레드 : 프로세스 내 실행단위(각 스레드는 가상화된 프로세서, 스택, 레지스터, 명령어 포인터를 가짐)

프로세스 ID (PID)

  • PID는 프레세스의 식별자로 해당 프로세스가 살아있는 동안 유일한 값
  • idle프로세스의 PID는 0이고 init 프로세스의 PID는 1
  • 사용자가 커널에 명시적으로 요청하지 않으면 커널은 다음 순서로 init프로세스를 확인하여 실행, 실패 시 커널 패닉
    • /sbin/init -> /etc/init -> /bin/init -> /bin/sh

PID 할당

  • 보통 PID의 최대값은 32768이며 /proc/sys/kernel/pid_max로 설정 가능
  • pid값이 pid_max 값에 도달해서 처음부터 다시 할당하기전까지는 앞선 pid 값이 비어 있더라도 재사용 하지 않음

프로세스 계층

  • 새로운 프로세스를 생성(spawn)하는 프로세스를 부모 프로세스, 새롭게 생성된 프로세스를 자식 프로세스
  • 모든 프로세스는 사용자와 그룹이 소유, 소유란 리소스에 대한 접근권한을 제어하기 위해 사용
  • 모든 프로세스는 다른 프로세스와의 관계를 표한하고 있는 프로세스 그룹의 일부, 자식 프로세스는 부모 프로세스의 프로세스 그룹에 속함

pid_t

  • pid는 pid_t 자료형으로 표현되며 <sys/type.h>에 정의, 보통 int자료형에 대한 typedef 임

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

  • getpid() 시스템콜은 호출한 프로세스의 pid를 반환
  • getppid() 시스템 콜은 호출한 프로세스의 부모 프로세스 pid를 반환

새로운 프로세스 실행하기

  • exec 시스템 콜 : 프로그램 바이너리를 메모리에 적재, 프로세스의 주소공간에 있는 이전 내용 대체 후 새로운 프로그램의 실행 시작
  • fork 시스템 콜 : 부모 프로세스를 거의 그대로 복제하여 새로운 프로세스를 생성

exec 함수들

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

int execl(const char* path, const char* arg, ...);
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 envp[]);
int execvp(const char* file, char* const envp[]);
int execve(const char* filename, char* const argv[], char* const envp[]);
  • execl 시스템콜을 호출 시 현재 프로세스를 path가 가리키는 프로그램으로 대체
  • …은 가변인자로 가변인자의 목록은 반드시 NULL로 끝나야 함
  • 일반적으로 반환 값이 없으나 에러가 발생할 경우 -1을 반환하고 errno를 적절한 값으로 설정
  • 호출이 성공시 다음의 프로세스 속성도 변화
    • 대기중인 시그널은 사라짐
    • 프로세스가 받은 시그널은 시그널 핸들러가 더이상 프로세스의 주소 공간에 존재하지 않으므로 디폴트 방식으로 처리
    • 메모리 락이 해제
    • 스레드의 속성 대부분이 기본값으로 변경
    • 프로세스의 통계 대부분이 재설정
    • 메모리에 매핑된 파일을 포함하여 그 프로세스 메모리 주소 공간과 관련된 내용 사라짐
    • atexit()의 내용 처럼 사용자 영역에만 존재하는 모든 내용이 사라짐
  • pid, ppid, 우선순위, 소유자/그룹 속성은 유지
  • 열린 파일은 그대로 상속되나 실제로는 exec 호출 전 파일을 모두 close(), 하거나 fcntl()을 이용하여 커널이 모두 닫도록 하는 것을 많이 사용
  • 시스템 콜 함수는 execve이며 나머지는 래퍼 함수
    • l & V : 인자를 리스트로 제공해야 하는지 배열(벡터)로 제공해야 하는지
    • p : file 인자값을 사용자의 실행 경로 환경 변수에서 찾음

fork() 시스템콜

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

pid_t fork(void);
  • fork()는 fork()를 실행한 프로세스(부모)와 거의 모든 내용이 동일한 새로운 프로세스(자식)을 생성
    • 자식 프로세스의 pid는 부모와 다르며 ppid는 부모의 pid
    • 자식 프로세스의 리소스 통계는 0으로 초기화
    • 처리되지 않은 시그널은 모두 사라지고 자식 프로세스로 상속되지 않음
    • 부모 프로세스의 파일 락은 자식에게 상속되지 않음
  • 자식 프로세스에서 fork()시스템콜 반환 값은 0, 부모에서는 자식의 pid
  • 실패시 -1을 반환하고 errno를 적절한 값으로 설정, 실패시 자식프로세스는 생성 안됨

copy-on-write

  • 리눅스는 fork()시 부모 프로세스의 주소 공간을 모두 복사하는 것이 아니라 페이지에 대한 COW를 수행
    • 페이지가 변경될 때 까지 복사를 미루고 페이지가 변경되면 복사 후 쓰기

vfork()

  • fork()와 같은 동작을 하지만 자식프로세스는 즉시 exec계열의 함수를 호출하던가 _exit()함수를 호출해야함
  • 주소공간 복사를 하지 않기 위한 시스템 콜이나 COW 도입 후 잘 안쓰임, 쓰지말자

프로세스 종료하기

1
2
3
#include <stdlib.h>

void exit(int status);
  • exit() 호출 시 몇 가지 기본적인 종료 단계를 거쳐 커널이 프로세스 종료
  • 반환값 없음(프로세스가 종료되기 때문에)
  • status 인자는 프로세스의 종료 상태를 나타내기 위한 값으로 쉘 같은 다른 프로그램에서 확인
    • status & 0377 이 부모프로세스로 반환(ex. exit(EXIT_SUCCESS))
  • C 라이브러리 프로세스 종료 절차
    • atexit()나 on_exit()에 등록된 함수를 등록 역순 호출
    • 열려있는 모든 표준 입출력 스트림버퍼 비움
    • tmpfile()함수를 통해 생성한 임시파일 삭제
  • 위의 절차 종료후 _exit() 시스템콜을 호출(남은 종료 절차를 커널이 처리하도록)
    • 리소스 정로(할당된 메모리, 열린파일, 시스템 V 세마포어)

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

  • 프로그램 끝가지 진행(main 함수 반환)
    • 프로그램 정상 종료 시 exit(0), return 0을 반환하여 명시적으로 종료상태를 반환하는 것이 좋음
  • 시그널 SIGTERM, SIGKILL 송신
  • 커널에게 밉보이기(잘못된 연산 및 세그멘테이션 폴트)

atexit

1
2
3
#include <stdlib.h>

int atexit(void (*func)(void));
  • 프로세스의 정상 종료 시 호출할 함수를 등록
    • 시그널에 의한 종료 시 등록된 함수는 미호출
    • exec 함수 호출 시 등록된 함수 목록 삭제
  • 등록된 함수는 등록된 함수의 역순으로 호출
  • 등록 가능한 함수의 개수는 ATEXIT_MAX 임(32개)
  • 성공 시 0을 반환, 에러 발생 시 -1 반환

SIGCHLD

  • 프로세스 종료 시 커널은 SIGCHLD 시그널을 부모 프로세스로 전송
  • signal(), sigaction() 시스템 콜을 사용해서 이 시그널을 처리하도록 할 수 있음

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

  • 자식프로세스가 부모 프로세스보다 먼저 죽으면 자식 프로세스는 좀비 프로세스로 변경 됨 (종료값 회수를 위해)
    • 좀비 프로세스는 커널 자료 구조만 가지고 있는 프로세스 뼈대
    • 좀비가 된 프로세스는 부모가 자산의 상태를 조사하도록 기다리고 부모가 자신의 정보를 회수한 다음에서야 공식 종료 됨
1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait (int *status);
  • wait() 호출 시 종료된 프로세스의 pid를 반환, 자식 프로세스 미종료 시 종료 때까지 블록
  • 에러가 발생한 경우 -1을 반환 후 errno 설정(ECHILD, EINTR)
  • status가 NULL이 아닐 경우 추가 정보가 그 포인터에 저장, 이를 해석하기 위한 매크로를 함께 제공
    • int WIFEXOTED(status) 등 (책 참조)

특정 프로세스 기다리기

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

pid_t waitpid (pid_t pid, int *status, int options);
  • pid인자는 기다리기를 원하는 프로세스를 지정
    • < -1 : 절대값 gid 일치, -1 : 모든 프로세스, 0 : 호출한 프로세서와 동일한 프로세스 그룹, 0 > : pid 일치
  • status인자는 wait()와 같음
  • option인자는 WNOHANG 등을 OR한 값 (책참조)
  • 반환값은 상태가 변경된 프로세스의 pid, 에러 발생시 -1 반환하고 errno를 변경(ECHILD, EINTR, EINVAL)

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

1
2
3
#include <sys/wait.h>

pid_t waitid (idtype_t idtype, id_t id, siginfo_t *infop, int options);
  • waitid()는 POSIX의 XSI확장에서 정의하고 리눅스에서 제공
  • wait()보다 다양한 옵션 및 정보(siginfo_t 등)를 제공하나 리눅스가 아닌 시스템의 이식성을 고려하면 단순한 함수를 쓰는게 낫다

BSD 방식으로 기다리기

1
2
3
4
5
6
7
#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);
  • BSD에서 제공하는 독자적인 함수
  • POSIX에서 정의한 함수가 아니므로 리소스 사용 정보가 매우 중요한 경우에만 사용할 것

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

1
2
3
4
#define _XOPEN_SOURCE /* WEXITSTATUS 등을 사용할 경우 */
#include <stdlib.h>

int system (const char* command);
  • 새로운 프로세스를 생성하고 종료를 기다르는 동작을 하나로 묶은, 즉 동기식 프로세스 생성 인터페이스
  • command 인자는 /bin/sh -c 뒤에 붙을 명령으로, command가 NULL이면 /bin/sh가 유효한 경우 0이 아닌 값을 반환(반대 시 0반환)
  • 호출 성공 시 wait()와 같이 그 명령의 상태를 반환, 실행한 명령의 종료코드는 WEXITSTATUS로 알 수 있음
  • 명령을 실행하는 동안 SIGCHLD는 블록되고 SIGINT와 SIGQUIT는 무시됨

좀비프로세스

  • 프로세스가 종료될 때 리눅스 커널은 그 프로세스의 자식 프로세스들을 모두 init프로세스(pid 0)으로 입양
  • init프로세스는 차례대로 주기적으로 자식 프로세스를 기다리며 오랫동안 좀비 상태로 남아있지 않도록 함

사용자와 그룹

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

  • 프로세스에 연관된 사용자ID는 4종료
    • 실제 사용자ID : 그 프로세스를 최초로 실행한 사용자의 uid
    • 유효 사용자ID : 그 프로세스가 현재 영향을 미치고 있는 사용자id(접근권한은 이 값을 기준으로 점검)
    • 저장된 사용자ID : 프로세스의 최초 유효 사용자ID, 프로세스가 포크되면 자식프로세스는 부모의 저장된 사용자ID를 상속
  • 프로세스 초기에 실제 사용자ID와 유효 사용자ID는 동일, exec호출 도중 유효 사용자ID는 프로그램 파일을 소유한 사용자ID로 변경
  • 실제 사용자ID는 프로그램을 실제로 실행하는 사용자에서 속한 유효 사용자ID이며 저장된 사용자ID는 exec과정에서 suid 바이너리로 변경되기 전까지 유효한 사용자ID
  • 유효 사용자ID가 가장 중요한 값(자격 검증)

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

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

int setuid (uid_t uid);
int setgid (uid_t gid);
  • setuid()를 호출 시 현재 프로세스의 유효 사용자 ID를 설정한다
    • 프로세스의 현재 유효 사용자ID가 0(root)일 때 실제 사용자와 저장된 사용자ID 역시 설정됨, root는 uid로 어떤 값이든 사용가능
    • 프로세스의 현재 유효 사용자ID가 0이 아닐 때, 실제 사용자와 저장된 사용자ID만 유효 사용자ID로 설정 가능
  • 성공할 경우 0반환, 실패 시 -1 반환 errno를 변경 (EAGAIN, EPERM)

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

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

int seteuid (uid_t euid);
int setegid (uid_t egid);
  • seteuid()를 호출 시 유효 사용자ID를 euid로 설정
    • root 사용자는 euid 값으로 어떤 값이든 사용 가능
    • 비 root 사용자는 euid 값으로 실제 사용자와 저장된 사용자ID만 유효 사용자ID로 설정 가능
  • 성공할 경우 0반환, 실패 시 -1 반환 errno를 변경 (EAGAIN, EPERM)
  • 비 root사용자의 경우 seteuid()와 setuid()는 동일하게 동작
    • 즉 항상 seteuid()를 사용하는 것이 표준에 맞음
    • 프로세스가 root로 실행된다면 setuid()를 사용하는 것이 합리적

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

  • BSD는 사용자ID와 그룹ID를 설정할 수 있는 독자적인 인터페이스를 제공(setreuid(), setregid())

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

  • HP-UX도 사용자ID와 그룹ID를 설정할 수 있는 독자적인 인터페이스를 제공(setreuid(), setregid())

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

  • 비 root 프로세스는 seteuid()를 사용해서 유효 사용자 ID를 변경
  • root 프로세스는 세가지 사용자ID를 모두 바꾸려면 setuid()를 유효 사용자ID만 임시로 바꾸려고 한다면 seteuid()를 사용

세션과 프로세스 그룹

  • 프로세스 그룹이란 작업 제어 목적으로 하나 이상의 프로세스를 모아 놓은 집합
  • 프로세스 그룹의 주된 속성은 그룹 내 모든 프로세스에게 시그널을 보낼 수 있음
  • 프로세스 그룹은 프로세스 그룹ID(pgid)로 구분되며 이는 프로세스 그룹 리더의 pid와 동일
  • 구성원이 하나라도 남아있다면 프로세스 그룹은 사라지지 안흠(리더가 종료되더라도 프로세스 그룹은 남음)
  • 새로운 사용자가 처음으로 시스템 로그인 시 로그인 프로세스는 사용자 로그인 셸 프로세스 하나로 이루어진 새로운 세션을 생성(로그인 셀은 세션리더로 동작)
  • 세션은 하나 이상의 프로세스 그룹이 들어 있는 집합. 세션 리더의 pid는 세션 ID
  • 세션은 로그인한 사용자 활동을 처리 및 제어터미널(tty)과 사용자 사이를 연결(세션은 대부분 쉘과 관련을 맺음)
  • 세션은 제어 터미널을 둘러싼 로그인을 통합하는 기능

세션 시스템 콜

1
2
3
#include <unistd.h>

pid_t setsid(void);
  • setsid()는 새로운 세션 내부에 새로운 프로세스 그룹을 생성하며 호출한 프로세스를 그 세션과 프로세스 그룹의 리더로 함
  • setsid() 호출이 성공 시 새롭게 생성한 세션의 ID를 반환, 실패시 -1 반환하며 errno를 EPERM으로 설정(호출한 프로세스가 이미 프로세스 그룹 리더)
  • 어떤 프로세스가 프로세스 그룹 리더가 되지 않게 하는 가장 손쉬운 방법은 프로세스를 포크하고 부모 프로세스를 종료한다움 자식프로세스에서 setsid()를 호출
  • getsid()로 현재 세션ID를 얻을 수 있음

프로세스 그룹 시스템 콜

1
2
3
4
#define _XOPEN_SOURCE 500
#include <unistd.h>

pid_t setpgid(pid_t pid, pid_t pgid);
  • setpgid()는 pid인자로 지정한 프로세스의 프로세스 그룹ID를 pgid로 설정
  • pid가 0이면 현재 프로세스의 프로세스 그룹ID를 변경, pgid인자가 0인경우 pid인자로 지정한 프로세스ID를 프로세스 그룹ID로 설정
  • setpgid() 호출이 성공 시 0을 반환, 에러 발생시 -1을 반환하고 errno를 설정(EACCES, EINVAL, EPERM, ESRCH)
    • pid로 지정한 프로세스가 해당 시스템콜을 호출하는 프로세스이거나 호출하는 프로세의 자식프로세스이며 아직 exec를 호출하지 않았고 부모프로세스와 동일한 세션일 것
    • pid로 지정한 프로세스가 세션의 리도가 아닐 것
    • pgid가 이미 있으면 호출하는 프로세스와 동일한 세션에 속해 있을 것
  • getpgid()로 프로세스의 프로세스 그룹ID를 얻는 것도 가능

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

  • setpgrp()/getpgrp() 같은 오래된 BSD 인터페이스도 있음

데몬

  • 데몬은 백그라운드에서 수행되며 제어 터미널이 없는 프로세스
  • 데몬은 일반적으로 부팅 시에 시작되며 root 혹은 다른 특수한 사용자 계정 권한으로 실행 되어 시스템 수준의 작업을 처리
  • crond, sshd처럼 편의를 위해 데몬의 이름은 d로 끝나는 경우가 많은데 필수적이거나 보편적인 것은 아님
  • 데몬의 필수조건은 반드시 init의 자식 프로세스여야 하며 터미널과 연결되어 있으면 안됨
  • 데몬이 되는 과정은 다음과 같다, 대부분의 유닉스 시스템은 daemon()함수를 제공하여 하기 과정을 자동화
    1. fork()를 호출해서 데몬이 될 새로운 프로세스 생성
    2. 부모 프로세스에서 exit()를 호출해 데몬 프로세스의 부모 프로세스 종료(데몬 프로세스가 프로세스 그룹의 리더가 되지 않도록)
    3. setsid()를 호출하여 데몬이 새로운 프로세스 그룹과 세션의 리더가 되도록 함(데몬프로세스가 제어터미널에 연관되지 않도록)
    4. chdir()을 사용하여 작업디렉토리를 루트 디렉토리로 변경(임의의 디렉토리가 계속 열린 상태로 실행되어 그 디렉토리를 해제하지 못하는 경우 방지)
    5. 모든 파일 디스크립터를 닫음
    6. 0,1,2 파일 디스트립터(표준 입력, 출력, 에러)를 열고 /dev/null로 다이렉트한다.
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy