예외(Exception)는 운영체제 커널이 프로세스(process)라는 개념을 제공할 수 있게 하는 기본적인 구성 요소입니다. 프로세스는 컴퓨터 과학에서 가장 심오하고 성공적인 아이디어 중 하나입니다.
현대 시스템에서 프로그램을 실행할 때, 우리는 마치 우리 프로그램이 시스템에서 유일하게 실행되는 것처럼 느끼는 환상(illusion)을 경험합니다. 우리 프로그램은 프로세서와 메모리 모두를 독점적으로 사용하는 것처럼 보입니다. 프로세서는 프로그램의 명령어들을 중단 없이 순서대로 실행하는 것처럼 보이며, 프로그램의 코드와 데이터는 시스템 메모리에 있는 유일한 객체인 것처럼 보입니다. 이러한 환상들은 바로 프로세스라는 개념을 통해 제공됩니다.
사용자가 셸(shell)에 실행 파일 이름을 입력하여 프로그램을 실행할 때마다, 셸은 새로운 프로세스를 생성하고 이 새로운 프로세스의 컨텍스트 안에서 해당 실행 파일을 실행시킵니다. 응용 프로그램 역시 새로운 프로세스를 생성하여 자신의 코드를 실행하거나 다른 응용 프로그램을 실행할 수 있습니다.
운영체제가 프로세스를 어떻게 구현하는지에 대한 상세한 설명 대신, 프로세스가 응용 프로그램에 제공하는 핵심적인 추상화(abstractions)에 초점을 맞춥니다.
프로세스는 시스템에서 (실제로는) 다른 많은 프로그램이 동시에 실행(concurrently)되고 있음에도 불구하고, 각 프로그램에게 마치 프로세서를 독점적으로 사용하는 듯한 환상(illusion)을 제공합니다.
만약 디버거를 사용해 우리 프로그램의 실행을 한 단계씩(single-step) 추적해 본다면, 우리는 일련의 프로그램 카운터(PC, Program Counter) 값들을 관찰하게 될 것입니다. 이 PC 값들은 오로지 우리 프로그램의 실행 파일 또는 런타임에 동적으로 연결된 공유 객체(shared objects)에 포함된 명령어들에만 해당하는 값들입니다.
이러한 PC 값의 순서(sequence)를 논리적 제어 흐름(logical control flow) 또는 간단히 논리적 흐름(logical flow)이라고 합니다.

(CSAPP 그림 8.12 참조) 세 개의 프로세스를 실행하는 시스템을 생각해 봅시다. 프로세서의 단일한 물리적 제어 흐름(physical control flow)은 각 프로세스당 하나씩, 세 개의 논리적 흐름으로 분할됩니다. (그림에서) 각 수직선은 한 프로세스의 논리적 흐름의 일부를 나타냅니다.
이 예시에서 세 논리적 흐름의 실행은 인터리브(interleaved)되어 있습니다. (즉, 실행이 겹쳐져서 일어납니다.)
여기서 핵심은 프로세스들이 차례를 바꿔가며(take turns) 프로세서를 사용한다는 것입니다. 각 프로세스는 자신의 흐름의 일부를 실행한 뒤 선점(preempted)(일시적으로 중단)되며, 그동안 다른 프로세스들이 자신의 차례를 가집니다.
이 프로세스 중 하나의 컨텍스트에서 실행되는 프로그램 입장에서는, 자신이 프로세서를 독점적으로 사용하는 것처럼 보입니다.
그 반대라는 유일한 증거는, 만약 우리가 각 명령어의 경과 시간(elapsed time)을 정밀하게 측정한다면, 우리 프로그램의 일부 명령어들 실행 사이에 CPU가 주기적으로 멈추는(stall) 것처럼 보인다는 점입니다. 하지만 프로세서가 멈출 때마다, 이후 프로그램의 메모리 위치나 레지스터의 내용에는 어떠한 변경도 없이 우리 프로그램의 실행을 재개합니다.
논리적 흐름(Logical flow)은 컴퓨터 시스템에서 다양한 형태로 나타납니다. 예외 핸들러(Exception handlers), 프로세스(processes), 시그널 핸들러(signal handlers), 스레드(threads), 그리고 자바 프로세스(Java processes)가 모두 논리적 흐름의 예입니다.
실행이 다른 흐름과 시간상으로 겹치는(overlaps in time) 논리적 흐름을 동시성 흐름(concurrent flow)이라고 부르며, 이 두 흐름은 동시적으로 실행된다(run concurrently)고 말합니다.
더 정확하게 정의하자면, 두 흐름 X와 Y는 다음과 같은 경우에만 서로에 대해 동시적입니다:

예를 들어, (앞의) 그림 8.12에서 프로세스 A와 B는 동시적으로 실행되며, A와 C 또한 동시적으로 실행됩니다. 반면에 B와 C는 동시적으로 실행되지 않는데, 이는 B의 마지막 명령어가 C의 첫 번째 명령어가 실행되기 전에 실행되기 때문입니다. (즉, 실행 시간이 겹치지 않습니다.)
여러 흐름이 동시적으로 실행되는 일반적인 현상을 동시성(concurrency)이라고 합니다. 하나의 프로세스가 다른 프로세스들과 차례를 바꿔가며(taking turns) 실행되는 개념은 멀티태스킹(multitasking)이라고도 알려져 있습니다.
프로세스가 자신의 흐름의 일부를 실행하는 각각의 기간(time period)을 타임 슬라이스(time slice)라고 부릅니다. 따라서 멀티태스킹은 타임 슬라이싱(time slicing)이라고도 합니다. 예를 들어, 그림 8.12에서 프로세스 A의 흐름은 2개의 타임 슬라이스로 구성됩니다.
주목할 점은, 동시성 흐름이라는 개념은 그 흐름들이 실행되는 프로세서 코어(processor cores)의 수나 컴퓨터의 수와 무관하다는 것입니다. 두 흐름이 시간상 겹치기만 한다면, 설령 같은 프로세서(단일 코어)에서 실행되더라도 이들은 동시적입니다.
하지만, 동시성 흐름의 진부분집합(proper subset)인 병렬 흐름(parallel flows)을 식별하는 것이 유용할 때가 있습니다. 만약 두 흐름이 서로 다른 프로세서 코어나 컴퓨터에서 동시적으로 실행된다면, 우리는 이 흐름들을 병렬 흐름이라 부르며, 병렬적으로 실행된다(running in parallel) 또는 병렬 실행(parallel execution)을 갖는다고 말합니다.
프로세스는 각 프로그램에게 마치 시스템의 주소 공간(address space)을 독점적으로 사용하는 듯한 환상(illusion)을 제공합니다.
-bit 주소를 가진 기기에서, 주소 공간은 까지, 총 개의 가능한 주소 집합입니다. 프로세스는 각 프로그램에게 자신만의 사적인 주소 공간(private address space)을 제공합니다. 이 공간이 '사적(private)'이라는 의미는, 이 공간의 특정 주소와 연결된 메모리 바이트는 일반적으로 다른 어떤 프로세스에 의해서도 읽히거나 쓰일 수 없다는 것입니다. (즉, 프로세스 간 메모리 침범이 불가능합니다.)
비록 각 사적인 주소 공간과 연결된 메모리의 내용은 일반적으로 서로 다르지만, 모든 공간은 동일한 일반적인 구조(organization)를 가집니다. 예를 들어, (CSAPP) 그림 8.13은 x86-64 리눅스 프로세스의 주소 공간 구조를 보여줍니다.

0x400000 주소에서 시작합니다.운영체제 커널이 완벽한 프로세스 추상화를 제공하기 위해서는, 프로세서가 응용 프로그램이 실행할 수 있는 명령어와 접근할 수 있는 주소 공간 영역을 제한하는 메커니즘을 제공해야 합니다.
프로세서는 일반적으로 제어 레지스터(control register) 내의 모드 비트(mode bit)를 통해 이 기능을 제공합니다. 모드 비트는 프로세스가 현재 누리고 있는 권한(privileges)을 나타냅니다.
리눅스는 /proc 파일 시스템이라는 영리한 메커니즘을 제공하여, 사용자 모드 프로세스가 커널 자료구조의 내용에 접근할 수 있도록 합니다.
/proc: 많은 커널 자료구조의 내용을 텍스트 파일 계층 구조로 내보냅니다(exports). 사용자 프로그램은 이 파일들을 읽을 수 있습니다./proc/cpuinfo), 특정 프로세스의 메모리 세그먼트 (/proc/<pid>/maps)/sys: (리눅스 커널 2.6부터 도입) 시스템 버스 및 장치에 대한 추가적인 저수준 정보를 내보냅니다.운영체제 커널은 문맥 교환(context switch)이라고 알려진 더 높은 수준의 예외 제어 흐름(exceptional control flow) 형태를 사용하여 멀티태스킹(multitasking)을 구현합니다. 문맥 교환 메커니즘은 (8.1절에서 논의한) 더 낮은 수준의 예외 메커니즘 위에 구축됩니다.
커널은 각 프로세스에 대한 문맥(context)을 유지 관리합니다. 문맥이란 커널이 선점된(preempted) 프로세스를 다시 시작하기 위해 필요한 상태(state)입니다. 이는 다음과 같은 객체들의 값을 포함합니다.
프로세스 실행 중 특정 시점에, 커널은 현재 프로세스를 선점(preempt)하고 이전에 선점되었던 다른 프로세스를 다시 시작하기로 결정할 수 있습니다. 이 결정을 스케줄링(scheduling)이라고 하며, 커널 내의 스케줄러(scheduler)라고 불리는 코드에 의해 처리됩니다. 커널이 실행할 새 프로세스를 선택하면, 커널이 그 프로세스를 스케줄했다고 말합니다.
커널이 새 프로세스를 스케줄한 후에는, 문맥 교환(context switch)이라는 메커니즘을 사용하여 현재 프로세스를 선점하고 제어권을 새 프로세스로 이전합니다. 문맥 교환은 다음 3단계를 수행합니다.
read 시스템 콜이 디스크 접근을 필요로 할 때, 커널은 디스크 데이터 도착을 기다리는 대신 다른 프로세스를 실행할 수 있습니다.sleep 시스템 콜은 명시적으로 현재 프로세스를 잠재우도록 요청합니다.
read 시스템 콜을 실행하여 커널로 트랩(trap)합니다.read 시스템 콜 바로 다음 명령어로 복귀합니다.Unix 시스템 수준 함수들은 오류가 발생하면 일반적으로 -1을 반환하고, 전역 정수 변수 errno에 오류의 원인을 나타내는 값을 설정합니다.
프로그래머는 항상 오류를 확인해야 하지만, 안타깝게도 오류 확인 코드가 코드를 복잡하게 만들고 가독성을 떨어뜨리기 때문에 생략하는 경우가 많습니다.
예를 들어, fork 함수 호출 시 기본적인 오류 확인 방법은 다음과 같습니다.
1 if ((pid = fork()) < 0) { // fork 실패 시 -1 반환
2 // strerror(errno): errno 값에 해당하는 오류 메시지 문자열 반환
3 fprintf(stderr, "fork error: %s\\n", strerror(errno));
4 exit(0); // 프로그램 종료
5 }
이 반복적인 코드를 단순화하기 위해 unix_error와 같은 사용자 정의 오류 보고 함수를 만들 수 있습니다.
void unix_error(char *msg) /* Unix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
이 함수를 사용하면 fork 호출 코드가 더 간결해집니다.
if ((pid = fork()) < 0)
unix_error("fork error");
코드를 더욱 단순화하기 위해, 오류 처리 래퍼(error-handling wrappers)를 사용할 수 있습니다 (W. Richard Stevens가 개척).
foo에 대해, 이름의 첫 글자만 대문자로 바꾼 래퍼 함수 Foo를 정의합니다.예를 들어, fork 함수의 래퍼 Fork는 다음과 같습니다.
pid_t Fork(void)
{
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error"); // 오류 시 종료
return pid;
}
이 래퍼를 사용하면, fork 호출은 매우 간결한 한 줄이 됩니다.
pid = Fork();
Unix는 C 프로그램에서 프로세스를 조작하기 위한 여러 시스템 콜을 제공합니다. 이 섹션에서는 중요한 함수들과 그 사용 예를 설명합니다.
모든 프로세스는 고유한 양수(0이 아닌) 프로세스 ID (PID)를 가집니다.
getpid() 함수: 호출한 프로세스 자기 자신의 PID를 반환합니다.getppid() 함수: 호출한 프로세스의 부모 프로세스 (즉, 이 프로세스를 생성한 프로세스)의 PID를 반환합니다.#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
// 호출자 또는 부모의 PID를 반환
프로그래머 관점에서 프로세스는 다음 세 가지 상태 중 하나에 있다고 생각할 수 있습니다.
SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 시그널을 받으면 중단되며, SIGCONT 시그널을 받을 때까지 이 상태를 유지하다가 다시 실행 상태가 됩니다. (시그널은 8.5절에서 자세히 설명)main 루틴에서 반환함, (3) exit 함수를 호출함.exit 함수#include <stdlib.h>
void exit(int status);
// 이 함수는 반환하지 않음 (Does not return)
exit 함수는 status 종료 상태 값과 함께 프로세스를 종료합니다. (main 루틴에서 정수 값을 반환하는 것도 종료 상태를 설정하는 다른 방법입니다.)
fork 함수부모 프로세스는 fork 함수를 호출하여 새로운 실행 중인 자식 프로세스를 생성합니다.
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
// 자식에게는 0, 부모에게는 자식의 PID, 오류 시 -1 반환
새로 생성된 자식 프로세스는 부모와 거의 동일하지만, 완전히 같지는 않습니다.
fork 호출 시 열어둔 파일 디스크립터(File descriptors)의 동일한 복사본. (즉, 자식도 부모가 열었던 파일을 읽고 쓸 수 있음)fork의 특징: "한 번 호출, 두 번 반환"fork 함수는 흥미로우며 종종 혼란스럽습니다. 한 번 호출되지만 두 번 반환하기 때문입니다.
fork는 자식 프로세스의 PID를 반환합니다.fork는 0을 반환합니다.자식의 PID는 항상 0이 아니므로, 반환 값을 통해 프로그램이 현재 부모에서 실행 중인지 자식에서 실행 중인지 명확하게 구분할 수 있습니다.
fork 예제 (그림 8.15)
fork를 호출하여 자식 프로세스를 만듭니다.fork 반환 시, 부모와 자식 모두 지역 변수 x의 값은 1입니다.x를 증가시켜 2를 출력하고 (if (pid == 0)), 부모는 x를 감소시켜 0을 출력합니다.fork의 미묘한 측면들fork는 부모에서 한 번 호출되지만, 부모와 자식 각각에게 한 번씩, 총 두 번 반환됩니다.fork 직후 부모와 자식의 주소 공간(스택, 변수 값, 힙, 코드 등)은 동일합니다. 하지만 이들은 별개의 프로세스이므로 각자의 사적인(private) 주소 공간을 가집니다. fork 이후 한쪽에서 변수(x)를 변경해도 다른 쪽에는 영향을 주지 않습니다. (이는 Copy-on-Write 메커니즘으로 효율적으로 구현됩니다.)stdout이 화면에 연결되어 있었으므로, 자식의 printf 출력도 화면으로 갑니다.main을 호출하는 것에 해당하며, 진입 간선(in-edge)이 없고 진출 간선(out-edge)이 하나입니다.exit 호출에 해당하는 정점으로 끝나며, 진입 간선이 하나이고 진출 간선(out-edge)이 없습니다.

-> 부모가 변수 x를 1로 설정합니다.
-> 부모가 fork를 호출하면, 자식 프로세스가 생성됩니다.
-> 자식 프로세스는 부모와 동시에(concurrently) 그리고 자신만의 사적인(private) 주소 공간에서 실행됩니다.
printf 문은 어느 순서로든 발생할 수 있습니다. 이는 두 실행 순서(부모-자식, 자식-부모)가 각각 그래프의 유효한 위상 정렬에 해당하기 때문입니다.
fork 호출을 이해하는 데 매우 유용합니다.fork 호출이 두 번 있는 프로그램은 (프로세스 그래프를 통해 알 수 있듯이) 총 4개의 프로세스를 실행하며, 각 프로세스의 printf 호출은 어떤 순서로도 실행될 수 있습니다.init 프로세스가 이들을 거둬들입니다.waitpid 함수를 호출하여 자신의 자식 프로세스가 종료되거나 멈추기를 기다립니다.#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
WNOHANG 옵션 사용 시 0, 오류 시 -1waitpid가 기다릴 자식 프로세스(대기 집합)는 pid 인자에 의해 결정됩니다.
pid > 0 : 대기 집합은 프로세스 ID가 pid와 일치하는 단일 자식 프로세스입니다.pid = -1 : 대기 집합은 부모의 모든 자식 프로세스입니다.(참고: waitpid는 유닉스 프로세스 그룹과 관련된 다른 종류의 대기 집합도 지원하지만, 여기서는 다루지 않습니다.)
waitpid의 기본 행동(옵션 0)은 대기 집합의 자식 중 하나가 종료(terminated)될 때까지 호출 프로세스를 중단(suspend)시키는 것입니다.
options 인자에 WNOHANG, WUNTRACED, WCONTINUED 상수를 조합하여 이 기본 행동을 수정할 수 있습니다.
WNOHANG (기다리지 않음)WUNTRACED (멈춤 상태도 감지)WCONTINUED (재개 상태 감지)SIGCONT 시그널을 받아 재개(resumed)될 때까지 기다립니다. (시그널은 8.5절에서 설명)옵션들은 비트 OR (|) 연산자를 사용하여 함께 조합할 수 있습니다.
WNOHANG | WUNTRACEDstatusp 인자가 NULL이 아닐 경우, waitpid는 반환을 유발한 자식에 대한 상태 정보를 statusp가 가리키는 status 변수에 인코딩합니다. wait.h 헤더 파일은 이 status 인자를 해석하기 위한 여러 매크로를 정의합니다:
WIFEXITED(status): 자식이 exit 호출이나 return을 통해 정상적으로 종료했다면 참(true)을 반환합니다.WEXITSTATUS(status): 정상적으로 종료된 자식의 종료 상태(exit status)를 반환합니다. 이 상태는 WIFEXITED()가 참을 반환했을 때만 정의됩니다.WIFSIGNALED(status): 자식 프로세스가 잡히지 않은(not caught) 시그널에 의해 종료되었다면 참을 반환합니다.WTERMSIG(status): 자식 프로세스를 종료시킨 시그널의 번호를 반환합니다. 이 상태는 WIFSIGNALED()가 참을 반환했을 때만 정의됩니다.WIFSTOPPED(status): 반환을 유발한 자식이 현재 멈춘(stopped) 상태라면 참을 반환합니다.WSTOPSIG(status): 자식을 멈추게 한 시그널의 번호를 반환합니다. 이 상태는 WIFSTOPPED()가 참을 반환했을 때만 정의됩니다.WIFCONTINUED(status): 자식 프로세스가 SIGCONT 시그널을 받아 재시작(restarted)되었다면 참을 반환합니다.waitpid는 -1을 반환하고 errno를 ECHILD로 설정합니다.waitpid 함수가 시그널에 의해 중단(interrupted)되었다면, -1을 반환하고 errno를 EINTR로 설정합니다.wait 함수는 waitpid의 더 간단한 버전입니다.
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statusp);
waitpid 함수는 다소 복잡하므로, 몇 가지 예제를 살펴보는 것이 유용합니다.

waitpid를 사용하여 N개의 모든 자식이 종료될 때까지 특정한 순서 없이(in no particular order) 기다립니다.while 루프의 테스트 조건으로 waitpid를 사용하여 모든 자식이 종료되기를 기다립니다.waitpid 호출은 임의의(arbitrary) 자식 하나가 종료될 때까지 중단(block)됩니다.waitpid는 해당 자식의 0이 아닌 PID를 반환합니다.exit 함수 호출을 통해) 정상적으로 종료되었다면, 부모는 종료 상태를 추출하여 stdout에 출력합니다.waitpid 호출은 1을 반환하고 errno를 ECHILD로 설정합니다.waitpid가 오류가 아닌 ECHILD로 정상 종료되었는지 확인합니다.linux> ./waitpid1
child 22966 terminated normally with exit status=100
child 22967 terminated normally with exit status=101

waitpid를 호출할 때 첫 번째 인자로 1 대신 저장해둔 특정 PID를 순서대로 전달합니다.sleep 함수는 프로세스를 지정된 시간(초) 동안 일시 중단(suspend)시킵니다.
<unistd.h>unsigned int sleep(unsigned int secs);sleep 함수가 시그널(Signal)에 의해 중단되어 일찍 반환된 경우, 남은 시간(초)을 반환합니다. (시그널은 8.5절에서 자세히 다룹니다.)pause 함수는 해당 프로세스가 시그널을 수신할 때까지 호출 함수를 재웁니다.
<unistd.h>int pause(void);execve 함수는 현재 프로세스의 문맥(context)에서 새 프로그램을 적재(load)하고 실행(run)합니다.
#include <unistd.h>
int execve(const char *filename, const char *argv[],
const char *envp[]);
execve 함수는 실행 가능한 객체 파일 filename을 argv 인수 목록 및 envp 환경 변수 목록과 함께 적재하고 실행합니다.
execve는 filename을 찾을 수 없는 경우와 같이 오류가 있을 때만 호출 프로그램으로 반환합니다. 따라서 fork가 한 번 호출되어 두 번 반환되는 것과 달리, execve는 한 번 호출되면 (성공 시) 절대 반환하지 않습니다.

argv 변수는 NULL로 끝나는(null-terminated) 포인터 배열을 가리키며, 이 배열의 각 포인터는 인수 문자열(argument string)을 가리킵니다 (그림 8.20). 관례적으로 argv[0]는 실행 파일의 이름입니다.
envp 변수는 NULL로 끝나는 포인터 배열을 가리키며, 각 포인터는 name=value 형식의 환경 변수 문자열을 가리킵니다.main 함수와 스택 구조execve가 filename을 적재한 후, (7.9절에서 설명한) 시작 코드(start-up code)를 호출합니다. 이 시작 코드는 스택을 설정하고 새 프로그램의 main 루틴으로 제어를 넘깁니다. main 루틴의 프로토타입은 다음과 같습니다:
int main(int argc, char **argv, char **envp);
// 또는
int main(int argc, char *argv[], char *envp[]);

main이 실행을 시작할 때, 사용자 스택은 그림 8.22와 같은 구조를 가집니다 (스택의 맨 아래(높은 주소)에서 맨 위(낮은 주소) 순서로):
envp[] 배열: NULL로 끝나는 포인터 배열. 각 포인터는 스택에 있는 환경 변수 문자열을 가리킴.environ은 이 포인터들의 첫 번째(envp[0])를 가리킵니다.argv[] 배열: NULL로 끝나는 포인터 배열. 각 포인터는 스택에 있는 인수 문자열을 가리킴.__libc_start_main)의 스택 프레임 (스택의 최상단)main 함수는 x86-64 스택 규율에 따라 레지스터에 저장된 세 개의 인수를 받습니다:
argc: argv[] 배열 내의 NULL이 아닌 포인터의 수.argv: argv[] 배열의 첫 번째 항목(argv[0])을 가리킴.envp: envp[] 배열의 첫 번째 항목(envp[0])을 가리킴.리눅스는 환경 배열을 조작하기 위한 여러 함수를 제공합니다.
getenv 함수#include <stdlib.h>
char *getenv(const char *name);
name이 존재하면 해당 포인터 반환, 없으면 NULL 반환getenv 함수는 환경 배열에서 name=value 형태의 문자열을 검색합니다. 찾으면 value에 대한 포인터를 반환하고, 그렇지 않으면 NULL을 반환합니다.setenv 및 unsetenv 함수#include <stdlib.h>
int setenv(const char *name, const char *newvalue, int overwrite);
void unsetenv(const char *name);setenv 반환: 성공 시 0, 오류 시 -1unsetenv 반환: 없음unsetenv: 만약 환경 배열에 name=oldvalue 형태의 문자열이 있다면, 이를 삭제합니다.setenv:name이 이미 존재(name=oldvalue)한다면: overwrite가 0이 아닌(nonzero) 경우에만 oldvalue를 newvalue로 교체합니다.name이 존재하지 않는다면: name=newvalue 문자열을 배열에 추가합니다.유닉스 셸(Shell)이나 웹 서버와 같은 프로그램들은 fork와 execve 함수를 매우 빈번하게 사용합니다.
sh, csh, bash 등)
(그림 8.23) 간단한 셸의 main 루틴은 다음과 같이 동작합니다.
stdin으로 커맨드 라인을 입력하기를 기다립니다.
(그림 8.24) 커맨드 라인을 평가하는 eval 함수의 동작입니다.
parseline 함수 호출 (파싱)
- (그림 8.25) 공백으로 구분된 커맨드 라인 인수들을 파싱하여 `execve`에 전달될 `argv` 벡터를 구축합니다.
- 첫 번째 인수는 **내장 셸 명령어(built-in command)**이거나, 새 자식 프로세스의 문맥에서 적재되어 실행될 **실행 파일**로 간주됩니다.
parseline은 1을 반환합니다. 이는 프로그램이 백그라운드(background)에서 실행되어야 함을 의미합니다 (셸은 자식의 종료를 기다리지 않습니다).builtin_command 함수 호출 (내장 명령어 확인)eval 함수는 builtin_command를 호출하여 첫 번째 인수가 내장 셸 명령어인지 확인합니다.quit): 즉시 해당 명령을 해석하고 1을 반환합니다. (실제 셸은 pwd, jobs, fg 등 수많은 내장 명령을 가집니다.)fork를 호출하여 자식 프로세스를 생성합니다.execve를 호출하여 요청된 프로그램을 실행합니다.eval 함수를 즉시 반환하고 루프의 맨 위로 돌아가 다음 커맨드 라인을 기다립니다.waitpid 함수를 사용하여 해당 작업(자식 프로세스)이 종료될 때까지 기다립니다. 작업이 종료되면 셸은 다음 반복으로 넘어갑니다.이 간단한 셸에는 결함(flaw)이 있습니다. 백그라운드로 실행된 자식들을 전혀 거둬들이지(reap) 않습니다. (이로 인해 좀비 프로세스가 발생합니다.)
이 결함을 올바르게 수정하기 위해서는 시그널(Signal)의 사용이 필요하며, 이는 다음 섹션에서 설명합니다.