[Ch05 프로세스 관리]
- 유닉스는 새로운 바이너리 이미지를 메모리에 적재하는 과정에서 새로운 프로세스를 생성하는 부분을 분리했다.
5.1 프로그램, 프로세스, 스레드
바이너리
는 디스크 같은 저장 장치에 기록되어 있는 컴파일된 실행할 수 있는 코드를 말한다.- 흔히 프로그램을 지칭하기도 함.
- 때로는 애플리케이션을 뜻하기도 함.
- /bin/ls, /usr/bin/X11 모두 바이너리
프로세스
는 실행 중인 프로그램을 뜻함.- 프로세스는 메모리에 적재된 바이너리 이미지와 가상화된 메모리의 인스턴스, 열린 파일 같은 커널 리소스, 관련된 사용자 정보와 같은 보안 정보와 하나 이상의 스레드를 포함하고 있다.
스레드
는 프로세스 내 실행 단위.- 각각의 스레드는 가상화된 프로세서를 가지고 있음
- 프로세서에는 스택, 레지스터, 명령어 포인터 같은 프로세서의 상태가 포함되어 있다.
- Q) 리눅스에서 스레드를 어떻게 구현했을까? (프로세스와 거의 유사하다고함…)
- 스레드든 프로세스든 다 Task로 구분하고 스케줄링함.
- 또? 구조는?
- 싱글 스레드 프로세스는 프로세스가 곧 스레드가 된다.
- 각각의 스레드는 가상화된 프로세서를 가지고 있음
5.2 프로세스 ID
- 모든 프로세스는 프로세스 ID(
pid
)라고 하는 유일한 식별자로 구분됨. pid
는 특정 시점에서 유일한 값임을 보장한다.- 커널이 같은 프로세스 식별자를 다른 프로세스에 다시 할당하지 않으리라 가정한다.
- 동작 중인 다른 프로세스가 없을 때 커널이 실행하는
idle
프로세스는 Pid가0
이다. - 부팅이 끝나면 커널이 실행하는 최초 프로세스인
init
의 pid는1
이다. - 리눅스 커널이 적절한
init
프로세스를 찾으면서 실행할 프로세스를 결정하는 순서- /sbin/init
- /etc/init
- /bin/init
- /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 새로운 프로세스 실행하기
- 유닉스에서는
- 프로그램 이미지를 메모리에 적재하고 실행하는 과정
- 새로운 프로세스를 생성하는 과정
- 두 가지가 분리되어 있음.
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 호출 이후 명령은 의미가 없음
- 종료 순서
- atexit()이나 on_exit()에 등록된 함수가 있다면 등록 수선의 역순으로 호출
- 열려있는 모든 표준 입출력 스트림의 버퍼를 비운다.
- tmpfile() 함수를 통해 생성한 임시 파일을 삭제
- 프로세스가 사용자 영역에서 해야하는 모든 작업을 종료시킴.
- 마지막으로 exit()은 _exit() 시스템 콜을 실행해서 나머지 단계를 커널이 처리하게 한다.
- 프로세스가 종료되면 커널은 해당 프로세스가 생성한 더 사용되지 않는 모든 리소스를 정리한다.
_exit()
을 직접 사용하면 표준 출력 스트림을 비우는 등의 사후 처리를 직접 해야한다.vfork()
를 사용하면_exit()
을 사용해야함.
5.4.1 프로세스를 종료하는 다른 방법
- 고전적인 방법은 시스템 콜을 사용하지 않고, 단순히 프로그램을 끝까지 진행시키는 것임
main()
함수가 반환되는 경우- 하지만 이런 경우도 컴파일러가 프로그램 종료 코드 이후에
exit()
시스템 콜을 묵시적으로 추가한다.
SIGTERM
과SIGKILL
을 보내서 종료도 가능- 커널에 밉보이는 방법
- 잘못된 연산 수행
- 세그멘테이션 폴트 일으킴
- 메모리 고갈
- 리소스 과다 소모
- → 프로세스를 강제로 죽임
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와 일치하는 자식 프로세스를 기다린다.
- < -1
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는 무시됨
- P_PID
- 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 */ };
- waitpid와 가장 큰 차이점인데,
- 성공할 경우 상태가 변경된 프로세스의 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);
- system()이 반복문안에서 실행될 때 문제가 발생함.
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을 쓴다?
- System 의 문제
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
시그널이 전달됨.
- 사용자가 터미널을 종료하면 Foreground 프로세스 그룹 내 모든 프로세스에
- 예제
- 사용자가 시스템에 로그인했고, bash의 pid가 1700일 때 bash 인스턴스는 gid가 1700인 새로운 프로세스 그룹의 유일한 멤버이자 리더가 됨.
- 그 셸에서 실행하는 명령어는 세션 1700에 속하는 새로운 프로세스 그룹에서 동작.
- 사용자에게 직접 연결되어 있고 터미널 제어가 가능한 프로세스 그룹 중 하나가 포어그라운드 프로세스 그룹
- 다른 프로세스 그룹은 백그라운드 프로세스 그룹
- 다른 예제
1
$ cat ship-inventory.txt | grep booty | sort
- 세 개의 프로세스를 가지는 하나의 프로세스 그룹은 생성함.
- → 셸에서 한번에 세 프로세스 모두에 시그널을 보낼 수 있다는 뜻.
- 사용자가 명령어 뒤에 &를 붙인다면 백그라운드로 돈다.
- 사용자가 시스템에 로그인했고, bash의 pid가 1700일 때 bash 인스턴스는 gid가 1700인 새로운 프로세스 그룹의 유일한 멤버이자 리더가 됨.
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 … ) 권한으로 실행되어 시스템 수준의 작업을 처리한다.
- 데몬의 두 가지 일반적인 요구사항
- 반드시 init 프로세스의 자식 프로세스여야 함.
- 터미널과 연결되어 있으면 안됨.
- 다음 과정을 통해 데몬이 될 수 있다.
- fork() 를 호출해서 데몬이 될 새로운 프로세스를 생성
- 부모 프로세스에서 exit() 을 호출해서 데몬 프로세스의 부모 프로세스를 종료한다.
- setsid()를 호출해서 데몬이 새로운 프로세스 그룹과 세션의 리더가 되도록 한다.
- chdir() 를 사용하여 작업 디렉터리를 루트 디렉터리로 변경한다.
- 모든 fd 를 닫는다.
- 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으로 넘긴다.