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()함수를 제공하여 하기 과정을 자동화
- fork()를 호출해서 데몬이 될 새로운 프로세스 생성
- 부모 프로세스에서 exit()를 호출해 데몬 프로세스의 부모 프로세스 종료(데몬 프로세스가 프로세스 그룹의 리더가 되지 않도록)
- setsid()를 호출하여 데몬이 새로운 프로세스 그룹과 세션의 리더가 되도록 함(데몬프로세스가 제어터미널에 연관되지 않도록)
- chdir()을 사용하여 작업디렉토리를 루트 디렉토리로 변경(임의의 디렉토리가 계속 열린 상태로 실행되어 그 디렉토리를 해제하지 못하는 경우 방지)
- 모든 파일 디스크립터를 닫음
- 0,1,2 파일 디스트립터(표준 입력, 출력, 에러)를 열고 /dev/null로 다이렉트한다.