본문 바로가기
프로그래밍/UNIX 고급 프로그래밍

제8장 프로세스 제어

by Ohdumak 2019. 11. 18.

2017/11/02 - [프로그래밍/UNIX 고급 프로그래밍] - 제7장 프로세스 환경



8.1 소개


프로세스 제어에는 새 프로세스의 생성, 프로그램의 실행, 프로세스의 종료가 포함된다.



8.2 프로세스 식별자


각 프로세스는 고유한 프로세스 ID가 있다. 

시스템에는 특별한 프로세스들이 존재하나, 그 세부사항은 구현마다 다르다. 일반적으로 프로세스 ID 0은 흔히 스와퍼(swapper)라고 부르는 스케줄러 프로세스에 배정된다. 이 프로세스는 커널의 일부인 시스템 프로세스이다. 프로세스 ID 1은 일반적으로 init 프로세스인데, 시스템 시동 과정의 끝에서 커널이 실행한다. 

Mac OX X 10.4에서 init프로세스는 launchd 프로세스로 대체되었다. launchd 프로세스는 init과 같은 종류의 과제를 수행하나, 기능성이 확장 되었다.


각각의 UNIX 시스템 구현에는 운영체제 서비스들을 제공하는 고유한 커널 프로세스들이 있다. 예를 들어 UNIX 시스템의 일부 가상 메모리 구현들에서 프로세스 ID 2는 pagedaemon이다. 이 프로세스는 가상 메모리 시스템의 페이징을 지원하는 역할을 담당한다.

#include <unistd.h>
pid_t getpid(void);
		// 반환값: 호출 프로세스의 프로세스 ID

pid_t getppid(void);
		// 반환값: 호출 프로세스의 부모 프로세스 ID

uid_t getuid(void);
		// 반환값: 호출 프로세스의 실제 사용자 ID

uid_t geteuid(void);
		// 반환값: 호출 프로세스의 유효 사용자 ID

gid_t getgid(void);
		// 반환값: 호출 프로세스의 실제 그룹 ID

gid_t getegid(void);
		// 반환값: 후출 프로세스의 유효 그룹 ID

이 함수들 중 오류를 반환하는 것은 하나도 없음을 주목하기 바란다.



8.3 fork 함수


기존 프로세스가 새 프로세스를 생성하는 한 가지 방법은 fork 함수를 호출하는 것이다.

#include 

pid_t fork(void);
		// 반환값: 자식에게는 0, 부모에게는 자식 프로세스 ID, 오류 시 -1

fork가 생성한 새 프로세스를 자식 프로세스(child process)라고 부른다. 이 함수는 한 번 호출되나 두 번 반환된다. 부모 프로세스에게 자식의 프로세스ID가 반환되는 이유는, 하나의 부모가 여러 개의 자식 프로세스를 생성할 수 있으며, 한 프로세스가 자신의 자식들의 프로세스ID를 알아내는 다른 수단은 없기 때문이다. fork가 자식에게 0을 돌려주는 이유는, 프로세스의 부모는 오직 하나뿐이며, 자식은 언제라도 getppid를 호출해서 자신의 부모의 프로세스ID를 알아낼 수 있기 때문이다.

 자식 프로세스와 부모 프로세스 fork 호출 이후의 명령들을 계속 실행한다. 자식은 부모의 복사본이다. 예를 들어 자식은 부모의 자료 구역과 힙, 스택의 복사본을 가진다. 이것은 자식을 위한 복사본임을 주의하기 바란다. 부모와 자식이 그 메모리 영역들을 공유하는 것은 아니다. 단 텍스트 구역은 부모와 자식이 공유한다.

 그런데 요즘 구현들은 부모의 자료 구역과 스택 및 힙 전체를 복사하지 않는다. 자식이 fork 다음에 exec를 호출하는 경우가 많기 때문이다. 대신 구현들을 쓸 때 복사(copy-on-write, COW)라는 기법을 사용한다. 이 경우에는 해당 영역들을 부모와 자식이 공유하되, 커널이 그 영역을 읽기 전용으로 설정해서 보호한다. 만일 두 프로세스 중 하나라도 그 영역들을 수정하려고 하면 커널은 해당 메모리 조각(보통은 가상 메모리 시스템의 한 '페이지')에 대해서만 복사본을 생성한다.


예제

도해8.1의 프로그램은 fork함수의 사용법을 보여준다. 특히, 자식 프로세스에서 변수들을 변경해도 부모 프로세스의 변수에는 영향이 미치지 않음을 시연한다.

도해8.1 fork함수의 사용법을 보여주는 프로그램

#include "apue.h"

int		globvar = 6;		/* 초기화된 자료 구역의 외부 변수 */
char	buf[] = "a write to stdout\n";

int main(void) {
	int var;		/* 스텍의 자동 변수 */
	pid_t pid;
	pid_t tmp_pid = 0;

	var = 88;
	if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1) {
		err_sys("write error");
	}
	printf("before fork\n");	/* stdout을 방출하지 않음 */

	if ((pid = fork()) < 0) {
		err_sys("fork error");
	} else if (pid == 0) { 		/* 자식 */
		globvar++;				/* 부모의 변수들을 수정 */
		var++;
		tmp_pid = getppid();
	} else {					/* 부모 */
		sleep(2);
		tmp_pid = pid;
	}

	printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var);
	exit(0);
}

다음은 이 프로그램을 실행한 예이다.

$ ./fork 
a write to stdout
before fork
pid = 20030, tmp_pid = 20029, glob = 7, var = 89
pid = 20029, tmp_pid = 20030, glob = 6, var = 88
$ ./fork > temp.txt
$ cat temp.txt 
a write to stdout
before fork
pid = 20032, tmp_pid = 20031, glob = 7, var = 89
before fork
pid = 20031, tmp_pid = 20032, glob = 6, var = 88 

strlen은 종료 널 바이트를 제외한 문자열 크기를 돌려준다.

sizeof는 종료 널 문자가 포함되어 있다. 또한 strlne은 실행 시점에서 함수 호출로 이어지는 반면 sizeof은 컴파일 시점에서 버퍼 길이를 계산한다.

 프로그램을 대화식으로 실행할 때에는 첫 번째 printf 줄의 복사본 하나만 화면에 나타난다. 이는 표준 출력 버퍼가 새 줄에 의해 방출되었기 때문이다. 그러나 표준 입출력을 파일로 재지정하면 printf 줄의 복사본이 두 개 기록된다. fork 이전의 printf가 한번만 호출되긴 하지만, 해당 줄은 fork가 호출될 때 여전히 버퍼에 남아 있는 상태이다. fork에 의해 부모의 자료 구역이 자식으로 복사될 때 그 버퍼도 함께 자식에게 복사된다. 따라서 부모와 자식 모두 해당 줄을 담은 표준 입출력 버퍼를 가지고 있는 상태가 된다. exit 바로 전의 두 번째 printf 호출은 그냥 자신의 자료를 기존 버퍼에 추가한다. 두 프로세스가 종료 될 때 각자의 버퍼 복사본이 최종적으로 방출된다.


파일 공유

프로그램의 부모의 표준 출력을 재지정하면 자식의 표준 출력도 재지정된다. fork의 한 가지 특성은 부모에 열려 있는 모든 파일 서술자가 자식에게 복제된다는 것이다.

 fork 이후에 서술자들을 처리하는 정상적인 방법

  1. 부모가 자식의 완료를 기다린다. 이 경우 부모는 자신의 서술자에 대해 특별한 처리를 할 필요가 없다. 자식의 종료된 시점에서, 그 동안 자식이 읽거나 쓴 공유 서술자들의 파일 오프셋들은 이미 적절히 갱신된 상태이다.
  2. 부모와 자식이 각자의 서술자들로 작업을 진행한다. 한 프로세스가 다른 프로셋의 열린 서술자에 간섭하지 않도록 하는 것이다. 이런 시나리오는 네트워크 서버들에서 자주 볼 수 있다.

부모가 자식에게 물려주는 속성

  • 실제 사용자 ID, 실제 그룹 ID, 유효 사용자 ID, 유효 그룹 ID
  • 추가 그룹 ID들
  • 프로세스 그룹 ID
  • 세션 ID
  • 제어 터미널
  • 사용자 -ID- 설정 플래그와 그룹 -ID- 설정 플래그
  • 현재 작업 디렉터리
  • 루트 디렉터리
  • 파일 모드 생성 마스크
  • 신호 마스크와 관련 설정들
  • 임의의 열린 파일 서술자에 대한 exec-시-닫기 플래그
  • 환경
  • 부착된 공유 메모리 구역들
  • 메모리 매핑들
  • 자원 한계들

부모와 자식의 차이점

  • fork의 반환값이 다르다.
  • 프로세스 ID 들이 다르다
  • 각자 부모 프로세스 ID가 다르다. 자식의 부모 프로세스 ID는 부모의 프로세스 ID이고, 부모의 부모 프로세스 ID는 변하지 않는다.
  • 자식의 tms_utime, tms_stime, tms_cutime, tms_cstime값은 0으로 설정된다.
  • 부모가 잠근 파일 자물쇠들은 자식에게 상송되지 않는다.
  • 아직 발동되지 않는 경보(alarm)들은 자식에서 모두 해제된다.
  • 자식의 유보 중 신호 집합은 빈 집합으로 설정된다.

 fork가 실패하는 주된 원인은 두 가지로, (a) 시스템에 이미 프로세스가 너무 많이 있거나(대체로 이는 뭔가 잘못 되었음을 뜻한다) (b) 이 실제 사용자 ID의 프로세스 전체 개수가 시스템의 한계를 넘은 것이다.

 fork의 활용법

  1. 프로세스가 자신을 복제해서, 부모와 자식이 코드의 서로 다른 부분을 각자 실행하게 한다. 이는 네트워크 서버들에서 흔히 볼 수 있는 방식이다.
  2. 프로세스가 자신과는 다른 프로그램을 실행한다. 이는 셸에서 흔히 볼 수 있다. 이 경우 자식은 fork 반환 직후 exec류 함수를 호출한다.



8.4 vfork 함수


vfork 함수는 호출 방법이 fork와 동일하나, 의미론(실행 시점에서의 작동 방식)은 조금 다르다.

여기서 이 함수를 소개하는 것은 전적으로 역사적인 이유에서이다. 이식성을 갖추고자 하는 응용 프로그램은 이 함수를 사용하지 말아야 한다.

vfork 함수는 fork처럼 새 프로세스를 생성하나, 부모의 주소 공간을 자식에게 복사하지 않는다는 차이가 있다.

 두 함수의 또 다른 차이점은, vfork는 자식이 먼저 실행됨을 보장한다는 것이다. 자식이 exec나 exit를 호출하기 전까지는 부모의 실행이 유보되며, exec나 exit를 호출하면 실행이 재개된다. (따라서 자식이 그 두 함수 중 하나를 호출하기 전에 부모의 어떤 행동에 의존한다면 교착 상태에 빠질 수 있다.



8.5 exit 함수


하나의 프로세스가 정상적으로 종료되는 방식은 다음 다섯 가지이다.

  1. main 함수로부터의 반환. exit를 호출하는 것과 동등하다.
  2. exit 함수 호출. ISO C에 정의된 이 함수를 호출하면 이전에 atexit함수로 등록한 모든 종료 처리부가 호출되며, 모든 표준 입출력 스트림이 닫힌다. ISO C는 파일 서술자나 다중 프로세스(부모와 자식), 작업 제어를 다루지 않으므로, 유닉스 시스템의 입장에서 이 함수의 정의는 불완전하다.
  3. _exit 또는 _Exit 호출. ISO C는 _Exit를 종료 처리부들이나 신호 처리부들을 실행하지 않고 프로세스를 종료하기 위한 수단이라고 정의한다. 
  4. 프로세스의 마지막 스레드를 시작한 루틴으로부터의 반환. 그러나 스페드의 반환값이 프로세스의 반환값으로 쓰이지는 않는다. 마지막 스레드가 그 시동 루틴으로부터 반환되면 프로세스는 종지 상태를 0으로 해서 종료된다.
  5. 프로세스의 마지막 스레드에서 pthread_exit 호출.
비정상 종료는 다음 세 가지 방식이다.
  1. abort 호출. SIGABRT 신호가 발생된다는 점에서, 이는 아래 항목(2번)의 한 특수한 사례이다.
  2. 프로세스가 특정 신호를 받아서.(신호는 제10장에서 좀 더 자세하게 설명한다.) 그 신호는 프로세스 자신이 발생한 것일 수도 있고(이를테면 abort 함수를 호출해서) 다른 어떤 프로세스나 커널이 발생한 것일 수도 있다. 커널은 예를 들어 프로세스가 자신의 주소 공간 외부에 있는 메모리를 참조하려 하거나 0으로 나누기가 실행되면 신호를 발생한다.
  3. 마지막 스레드가 취소 요청을 받아 들여서. 기본적으로 취소(cancellation)는 지연된 방식으로 발생한다. 즉, 한 스레드가 다른 어떤 스레드의 취소를 요청하면, 얼마 후에 대상 스레드가 종료된다.
프로세스가 어떻게 종료되었든, 결국에는 커널의 동일한 코드가 실행된다. 그 커널 코드는 프로세스의 모든 열린 서술자를 닫고 프로세스가 사용하던 메모리를 해제하는 등의 정리 작업을 수행한다.
 종료 상태는 세 종료 함수(exit, _exit, _Exit)의 인수로 주어진 값 또는 main이 돌려준 값이고 종지 상태는 최종적으로 _exit가 호출되었을 때 커널이 종료 상태를 적절히 변환해서 설정한 값이다. 도해 8.4에 부모 프로세스가 자식의 종지 상태를 알아내는 다양한 방법이 정리되어 있다. 자식이 정상적으로 종료되었다면 부모는 자식의 종료 상태를 얻을 수 있다.
 종료되었지만 부모 프로세스가 아직 종지 상태를 회수하지 않은 프로세스를 가리켜 좀비라고 부른다.


8.6 wait 함수와 waitpid 함수


정상적으로든 비정상적으로든 프로세스가 종료될 때 커널은 그 부모에게 SIGCHLD 신호를 보내서 자식의 종료 사실을 알려준다. 자식의 종료는 비동기적인(즉, 부모의 실행 도중 언제라도 일어날 수 있는) 사건이므로, 이 신호는 커널에서 부모로의 비동기 통지에 해당한다. 부모 프로세스가 wait나 waitpid를 호출했을 때 다음과 같은 일들이 생길 수 있다는 점을 아는 것이 중요하다.

  • 모든 자식이 아직 실행 중이면 호출이 차단된다.
  • 종료되어 종지 상태가 회수되길 기다리고 있는 자식이 존재하면 호출이 즉시 반환된다. 이 때 반환값은 그 자식 프로세스의 프로세스 ID이다.
  • 자식 프로세스가 하나도 없으면 호출이 즉시 반환되며, 반환값은 오류 코드이다.
프로스세가 SIGCHLD 신호를 받아서 wait를 호출했다면 그 호출은 즉시 반환되리라고 짐작할 수 있다. 그러나 임의의 시점에서 호출한 것이라면 호출이 차단될 가능성이 있다.
#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
		// 반환값(둘 다): 성공 시 프로세스 ID, 0(아래 설명 참고), 오류 시 -1
두 함수의 차이점
  • wait 함수를 호출한 프로세스의 실행은 자식 프로세스가 종료될 때까지 차단될 수 있다. 그러나, waitpid에는 그러한 차단을 방지하는 옵션을 제공
  • 가장 먼저 종료되는 자식을 기다리는 wait 함수와는 달리, waitpid 함수는 기다릴 프로세스를 좀 더 구체적으로 지정할 수 있는 수단을 제공
WIF로 시작하는 네 개의 상호 배타적인 매크로들로 프로세스의 종료 방식을 알아낼 수 있다. 그리고 이 네 매크로 중 어떤 것이 참인지에 따라, 다른 여러 매크로들을 이용해서 종료 상태, 신호 번호 등을 알아낼 수 있다. 도해 8.4는 이 네 개의 상호 배타적 매크로를 정리한 것이다.

도해8.4 wait와 waitpid가 돌려준 종지 상태를 조사하는 매크로들

매크로

설명 

WIFEXITED(status) 

만일 자식 프로세스(종지 상태가 status인)가 정상적으로 종료되었으면 참. 이 경우 자식이 exit나 _exit, _Exit에 넘겨준 인수(종료 상태)의 하위 8비트를 

 WEXITSTATUS(status)

로 얻을 수 있다. 

WIFSIGNALED(status)

만일 자식 프로세스가 비정상적으로 종료되었으면 참. 이 경우 종료를 유발한 신호의 번호를

 WTERMSIG(status)

로 얻을 수 있다. 추가로, 일부 구현은

 WCOREDUMP(status)

라는 매크로도 제공한다(단일 UNIX 규격의 일부는 아님). 이 매크로는 종료된 프로세스의 코어 파일이 생성되었다면 참으로 평가된다.

WIFSTOPPED(status)

만일 자식 프로세스가 현재 정지된(stop) 상태이면 참. 이 경우 정지를 유발한 시스템의 번호를

 WSTOPSIG(status)

로 얻을 수 있다.

WIFCONTINUED(status)

만일 자식이 작업 제어 정지 이후 재개되었으면 참(XSI 옵션: waitpid에만 해당) 


waitpid 함수의 pid 인수는 그 값에 따라 다양한 의미로 쓰인다.

pid == -1
- 임의의 자식 프로세스를 기다린다. 이 경우 waitpid는 wait와 같다
pid > 0
- 프로세스 ID가 pid와 같은 자식 프로세스를 기다린다.
pid == 0
- 프로세스 그룹 ID가 호출 프로세스의 것과 동일한 임의의 자식 프로세스를 기다린다.
pid < -1
- 프로세스 그룹 ID가 pid의 절대값과 같은 임의의 자식 프로세스를 기다린다.

도해8.7 waitpid의 options인수에 사용할 수 있는 상수들

상수

설명

WCONTINUED 

만일 구현이 작업 제어를 지원한다면, pid에 해당하는 자식 프로세스들 중 정지 이후 재개되었으면  

WNOHANG

pid에 해당하는 자식의 종료 상태를 즉시 회수할 수 있는 상황이 아니어도 waitpid함수의 호출이 차단되지 않는다. 이 경우 반환값은 0이다. 

WUNTRACED

만일 구현이 작업 제어를 지원한다면, pid에 해당하는 자식 프로세스들 중 정지된, 그리고 그 후로 아직 상태가 보고되지 않은 임의의 자식의 상태가 반환된다. 반환값이 정지된 자식 프로세스에 해당하는지는 WIFSTOPPED 매크로로 알아낼 수 있다. 


wait 함수가 제공하지 못하는 waitpid 함수만의 기능은 다음 세 가지이다.
  1. waitpid 함수로는 특정한 하나의 프로세스만 기다릴 수 있는 반면 wait 함수는 종료된 임의의 자식의 상태를 알려준다.
  2. waitpid 함수는 wait의 비차단 버전에 해당하는 기능을 제공한다. 자식의 상태를 알아야 하지만 호출이 차단되지는 않아야 할 때가 종종 있다.
  3. waitpid 함수는 작업 제어를 지원한다(WUNTRACED 옵션과 WCONTINUED 옵션을 통해서).


8.7 waitid 함수


waitid함수는 waitpid와 비슷하되 좀 더 유연하다.

#include <sys/wait.h>

int waitpid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
		// 반환값: 성공 시 0, 오류 시 -1


waitpid 처럼 waitid에서도 기다릴 특정 자식 프로세스를 지정할 수 있다. 그런데 waitpid에서는 기달리 자식(들)을 지정하는 프로세스ID나 프로세스 그룹ID를 하나의 인수로 부호화했지만, 이 함수에서는 그 둘을 개별적인 인수로 지정한다. id 인수는 ID 종류를 뜻하는 idtype인수의 값에 따라 다르게 해석된다.


8.8 wait3 함수와 wait4 함수


wait나 waitid, waitpid 함수에는 없고 이들에게만 있는 유일한 특징은, 종료된 프로세스와 그 프로세스의 모든 자식 프로세스가 사용하던 자원들을 요약한 정보를 커널로부터 얻도록 하는 추가적인 인수이다.



8.9 경쟁 조건


경쟁 조건(race condition)은 여러 개의 프로세스들이 공유 자료를 가지고 뭔가를 수행하려고 하는데 최종 결과가 그 프로세스들이 실행되는 순서에 따라 달라지는 상황을 말한다. 

프로그램에 TELL, WAIT 루틴을 적용한다면 경쟁 조건을 없앨 수 있다.


8.10 exec류 함수들


fork 함수의 한 가지 용법은 새 프로세스(자식)을 생성하고 그 프로세스에서 exec류 함수들 중 하나를 호출해서 다른 프로그램을 실행하는 것이다. 프로세스가 exec류 함수들 중 하나를 호출하면 그 포르세스는 새 프로그램으로 완전히 대체되며, 새 프로그램은 자신의 main 함수로부터 실행을 시작한다. exec류 함수가 호출되어도 프로세스 ID가 변하지는 않는다. exec류 함수가 새 프로그램을 생성하는 것은 아니기 때문이다. exec류 함수는 단지 현재 프로세스(의 텍스트, 자료, 힙, 스택구역)를 디스크에서 불러온 새 프로그램으로 대체할 뿐이다.

 exec류 함수는 총 일곱 가지인데, 그 일곱 함수 모두에 적용되는 논의에서는 그 함수들을 그냥 'exec 함수'라고 통칭해서 부르는 경우가 많다.

#include <unistd.h>

int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );

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

int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *cosnt envp[] */);

int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);

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

int fexecve(int fd, char *const argv[], char *const envp[]);
		// 반환값(모두): 오류 시 -1, 성공 시 반환되지 않음

 이 함수들의 차이점은 명령줄 인수 목록의 전달 방식에 관련된 것이다(함수 이름에서 l은 list[목록]을, v는 vector[벡터]를 뜻한다. execl 함수와 execlp, execle 함수에서는 명령줄 인수들을 새 프로그램에 각각 개별적인 인수들로 지정해야 하며, 인수들이 더 이상 없음을 알려주기 위해 마지막에 널 포인터도 하나 지정해야 한다. 나머지 네 함수(execv, execvp, execve, fexecve)에서는 인수들을 가리키는 포인터들의 배열('벡터')을 만들고 그 배열의 주소를 하나의 인수로서 함수에 넘겨주어야 한다.

 이 일곱 가지 exec류 함수의 인수들을 기억하기란 쉽지 않은데, 함수 이름의 글자들이 조금은 도움이 될 것이다. p 자는 함수가 filename 인수를 받고 PATH 환경 변수를 이용해서 실행 파일을 찾는다는 뜻이다. l 자는 함수가 인수들의 목록을 받는다는 뜻이고, v 자는 argv[] 벡터를 받는다는 뜻이다. 그 둘을 동시에 받는 함수는 없다. 마지막으로 e 자는 현재 환경을 사용하는 대신 envp[] 배열을 받아서 사용한다는 뜻이다.



8.11 사용자 ID와 그룹 ID의 변경


UNIX 시스템에서 시스템이 파익하고 있는 현재 날짜를 변경할 수 있는 능력 등의 특권(privilege)이나 특정 파일을 읽고 쓸 수 있는 권한 등의 접근 제어는 사용자 ID와 그룹 ID에 근거한다.

 실제 사용자 ID와 유효 사용자 ID는 setuid 함수로 설정할 수 있다. 마찬가지로, 실제 그룹 ID와 유효 그룹 ID는 setgid 함수로 설정할 수 있다.



8.12 해석기 파일


현재의 모든 유닉스 시스템은 해석기(interpreter)파일을 지원한다. 해석기 파일은 첫 줄이 다음과 같은 형태인 텍스트 파일이다.


#! 경로이름 [추가적인 인수]

가장 흔히 쓰이는 해석기 파일은 다음과 같은 줄로 시작한다.

#!/bin/sh

해석기 파일(#!로 시작하는 텍스트 파일)과 해석기(해석기 파일의 첫 줄에 있는 경로이름으로 지정된 프로그램)는 서로 다른 것임을 주의



8.13 system 함수


프로그램 안에서 하나의 명령 문자열을 실행할 수 있다. system함수가 ISO C에 정의되어 있지만 그 작동 방식은 시스템에 크게 의존적이다.

 system은 fork와 exec, waitpid의 호출로 구현되므로, 반환값은 크게 세 종류이다.

  1. fork 호출이 실패했거나 waitpid가 EINTR 이외의 오류를 돌려주었다면, system은 errno를 해당 오류로 설정하고 -1을 돌려준다.
  2. exec가 실패한 경우(이는 셸을 실행할 수 없었다는 뜻이다) 반환값은 셸이 exit(127)을 실행했을 때와 동일하다.
  3. 그 외의 경우, 즉 fork와 exec, waitpid 모두 성공한 경우 system의 반환값은 waitpid에 지정된 형식의 셸의 종지 상태이다.
 exit 대신 _exit를 호출한다는 점을 주목하기 바란다. 이는 fork호출에 의해 부모에게 자식으로 복사되었을 수 있는 임의의 표준 입출력 버퍼들이 자식에서 방출되는 일이 없도록 하기 위한 것이다.
 fork와 exec를 직접 호출하는 것에 비한 system의 장점은, 필요한 모든 오류 처리와 신호 처리를 system이 수행해 준다는 것이다.
 wait류 함수들 중 특정한 자식을 지정해서 기다릴 수 있는 함수가 없다는 점이 POSIX.1에 waitpid 함수가 추가된 이유 중 하나이다.
 사용자-ID-설정 프로그램이나 그룹-ID-설정 프로그램에서 system 함수를 사용해서는 절대로 안된다.


8.14 프로세스 회계


대부분의 유닉스 시스템은 프로세스 회계(accounting)를 수행하는 옵션을 제공한다. 해당 옵션이 켜지면 커널은 프로세스가 종료될 때마다 회계 레코드를 기록한다. 이 회계 레코드에는 명령의 이름, 소비된 CPU 시간, 사용자 ID와 그룹 ID, 시작 시간 등으로 구성된 소량의 이진 자료가 담긴다.

 - 프로세스 회계는 그 어떤 표준에도 명시되어 있지 않다.

 - Solaris 10은 입출력랴을 바이트 단위로 관리

 - FreeBSD 8.0, Mac OS X 10.6.8은 블록 단위로 관리 (서로 다른 블록 크기를 구분하지 않아 블록 개수가 무의미)

 - Linux 3.2.0은 입출력 통계치를 아예 수집하지 않는다.


 프로세스 회계의 활성화, 비활성화는 acct 함수로 설정한다. 이 함수는 오직 accton 명령에서만 쓰인다.

 CPU 시간이나 전송된 문자 개수 등 회계 레코드에 필요한 자료는 커널이 새 프로세스(fork 이후)가 생성될 때마다 초기화해서 프로세스 테이블에 담아 둔다. 각 회계레코드는 프로세스가 종료될 때 해당 파일에 기록된다.

 첫째, 종료되지 않은 프로세스의 회계 레코드는 얻을 수 없다.

 둘째, 회계 파일에 있는 레코드들의 순서는 프로세스들이 종료되는 순서에 해당한다.



8.15 사용자 식별


자신의 실제, 유효 사용자, 그룹 ID들은 그 어떤 프로세스도 알아낼 수 있다.

일반적으로 시스템은 사용자가 로그인에 사용한 이름을 유지하며, getlogin 함수를 이용하면 그 로그인 이름을 얻을 수 있다. 데몬에서 이 함수를 호출하면 호출이 실패한다.

 로그인 이름을 알아낸 후 getpwnam 함수를 이용해서 패스워드 파일의 해당 사용자 항목을 조회할 수 있다.



8.16 프로세스 스케줄링


예전에는 UNIX 시스템이 제공하는 스케줄링 우선순위 제어 수단은 세밀하지 못했다.

 프로세스가 여러 스케줄링 등급들 중 하나를 선택하고 그 행동 방식을 좀 더 세밀하게 조율할 수 있는 인터페이스가 POSIX 실시간 확장들에 추가되었다. 

 프로세스가 자신의 예의도를 조회하거나 설정할 때 사용하는 함수는 nice이다.


 nice 함수 대신 getpriority 함수로도 프로세스의 예의도록 얻을 수 있다. getpriority 함수는 관련된 프로세스들의 그룹의 예의도록 조회하는 용도로 쓰인다.


 who 인수의 의미는 which 인수에 의해 결정되고, PRIO_PROCESS(프로세스), PRIO_PGRP(프로세스 그룹), PRIO_USER(사용자 ID)를 의미한다.



8.17 프로세스 시간


어떤 프로세스이든, times 함수를 호출해서 자신과 임의의 종료된 자식들의 시간 값을 얻을 수 있다.



8.18 요약


고급 프로그래밍을 위해서는 UNIX 시스템의 작업 제어를 상세하게 이해하는 것이 꼭 필요하다. fork, exec류 함수들, _exit, wait, waitpid만 숙달하면 된다.

728x90

댓글