태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.
블로그 이미지

haRu™'s Thinks

느리게 걷기, 천천히 생각하기, 그리고 한 발 뒤에서 바라 보기… by haruroh


'Process'에 해당되는 글 3건

  1. 2007.12.22 24. Job Control(작업 제어)
  2. 2007.12.22 23. Processes
  3. 2007.12.22 22. 프로세스의 시동과 종료(Process StartUp)

24. Job Control(작업 제어)

목차 이전 : 23. 프로세스 다음 : 25. 사용자와 그룹


24 작업 제어

작업제어는 단일한 로그인 세션에서 사용자가 여러개의 프로세스 그룹들(또는 작업들) 사이에서 제어를 옮기는 것을 허용하는 프로토콜을 적용한다. 작업제어 기능은 대부분의 프로그램에서 자동적으로 적당하게 수행되고, 작업제어에 대한 특별한 어떤 것을 할 필요가 없다. 그래서 당신이 쉘이나 로그인 프로그램을 만들지 않는다면 이 장에 있는 것들을 무시할 수 있다.

당신이 이 장에 나와있는 것들을 이해하기 위해서는 시그널 핸들링(21장 [Signal Handling] 참조)과 프로세스 생성(23.2절 [Process Creation Concepts] 참조) 과 연관된 개념에 친숙해질 필요가 있다.


24.1 작업제어의 개념

대화형(interactive) 쉘의 기본적인 목적은 사용자 터미널로부터 명령문을 읽고 그 명령문에의해 지정된 프로그램을 실행하도록 프로세스들을 만드는 것이다. 그것은 fork( 23.4절 [Creadting a Process] 참조.)와 exec ( 23.5절 [Executing a File] 참조.)함수를 사용해서 할 수 있다.

단일한 명령문은 단지 한 개의 프로세스를 실행시키지만_종종 한 개의 명령문도 여러개의 프로세스들을 사용한다. 만일 당신이 쉘 커맨드에서 `|'을 사용한다면 당신은 명시적으로 그들 프로세스안에 여러개의 프로그램들을 요청한것이다. 그러나 만일 당신이 한 개의 프로그램을 실행시킬지라도, 그것이 내부적으로는 여러개의 프로세스들을 사용할 수 있다. 예를들어, `cc -c foo.c'와 같은 단일한 컴파일 명령문은, 전형적으로 네 개의 프로세스들을 사용한다. 만일 당신이 make를 실행한다면, 그 작업은 분리된 프로세스 안에서 다른 프로그램을 실행하는 것이다.

단일한 명령문에 소속되어 있는 프로세스들은 프로세스 그룹이나 작업(job)이라고 불린다. 당신은 동시에 그들 모든 프로세스들을 작동시킬 수 있다. 예를들어, C-c를 타이핑하면 전면 프로세스 그룹에 있는 모든 프로세스가 종료되도록 시그널 SIGINT를 보낸다. 세션은 프로세스들의 좀 큰 그룹이라고 할 수 있다. 일반적으로 단일한 로그인에 있는 모든 프로세스들은 같은 세션에 속한다.

모든 프로세스는 프로세스 그룹에 속한다. 어떤 프로세스가 만들어질 때, 그것은 부모 프로세스와 같은 프로세스 그룹과 세션의 멤버가 된다. 당신은 setpid 함수를 사용해서 다른 프로세스 그룹으로 소속을 바꿀 수 있는데, 그때 그 프로세스 그룹은 같은 세션에 소속된 프로세스 그룹이다.

다른 세션에 프로세스를 소속시키는 유일한 방법은 setsid 함수를 사용해서, 새로운 세션이나, 또는 세션 리더의 처음 프로세스를 만드는 것이다. 새로운 프로세스 그룹에 세션리더를 소속시키면, 당신은 다시 프로세스 그룹의 외부로 그것을 다시 옮길 수 없다.

보통, 새로운 세션들은 시스템 로그인 프로그램에 의해 만들어지고 세션 리더는 사용자의 로그인 쉘을 실행시키는 프로세스이다. 작업제어를 지원하는 쉘은 어떤 시간에 어떤 작업이 터미널을 사용할 수 있도록 제어를 조정해야만 한다. 그렇지않다면 여러개의 작업들이 동시에 터미널로부터 입력을 받아들이려 시도할것이고, 어떤 프로세스가 사용자가 타입한 입력을 받을 것인지가 혼란하게 될 수도 있다. 이것을 방지하기 위해서, 쉘은 이 장에 설명된 프로토콜을 사용해서 터미널 드라이버와 협력해야만 한다.

쉘은 한 번에 오직 한 프로세스 그룹에게만 터미널을 제어하는 제한없는 권한을 줄 수 있다. 이처럼 터미널을 제어하고 있는 프로세스 그룹을 전면 작업(foreground job)이라고 부른다. 터미널을 억세스 하지 않고 실행중인, 쉘에 의해 처리되고 있는 다른 프로세스 그룹들은 배경작업(background job)이라고 불린다.

만일 배경 작업이 터미널을 제어하여 터미널로부터 읽거나 쓰거나 할 필요가 있다면, 그것은 터미널 드라이버에 의해 멈추어진다. 사용자는 SUSP문자( 12.4.9절 [Special Characters] 참조)를 입력함으로써 전면 작업을 멈출 수 있고 프로그램은 SIGSTOP시그널을 보냄으로써 어떤 작업을 멈출 수 있다. 작업들이 멈추었을 때 쉘은 그들에 대하여 사용자에게 알릴 책임이 있고, 멈추어진 작업에 대해서 계속 진행할 것인지에 대한 여부를 사용자에게 선택하도록 허용하기 위한 메카니즘의 제공과 전면 작업과 배경 작업사이의 작업전환도 쉘에게 책임이 있다. 제어중인 터미널에 입/출력 하는것에 대한 자세한 정보는 24.4절 [Access to the Terminal] 참조.


24.2 작업제어는 선택적이다

모든 운영체제가 작업제어를 지원하는 것은 아니다. GNU 시스템은 작업제어를 지원하지만, 만일 다른 시스템에서 GNU 라이브러리를 사용한다고해도 그 시스템에서 작업제어를 지원하지 않을 수 도 있다. 당신은 시스템이 작업제어를 지원하는지에 대한 여부를 시험하기 위해서 컴파일할 때 _POSIX_JOB_CONTROL 매크로를 사용할 수 있다. 27.2절[System Options] 참조.

만일 작업제어가 지원되지 않으면, 한 세션만다 오직 한 개의 프로세스 그룹만 존재하고 그 프로세스 그룹은 항상 전면 작업으로써 행동한다. 부가적인 프로세스 그룹을 만드는 함수에는 에러코드 ENOSYS를 사용해서 간단히 실패했음을 알린다.

다양한 작업제어 시그널(21.2.5절 [Job Control Signals] 참조.)들을 대표하고 있는 매크로들은 작업제어가 지원되지 않는다고 할지라도 정의되어 있다. 그렇지만, 작업제어를 지원하지 않는 시스템에서 그 시그널들을 결코 발생되지 않고, 에러를 보고하거나 아무것도 하지않는 등의 시그널에 대한 행동을 정하거나 시험하거나 보내려고 시도하지 않는다.


24.3 프로세스의 터미널 제어하기

프로세스의 속성들중 하나는 그 프로세스가 제어중인 터미널이다. fork를 사용해서 만들어진 자식 프로세스는 그들의 부모 프로세스로부터 제어중인 터미널을 상속받는다. 이와같은 방법으로, 한 세션안에 있는 모든 프로세스들은 세션 리더(session leader)로부터 제어중인 터미널을 상속받는다. 터미널의 제어권을 가진 세션리더를 터미널의제어중인 프로세스(controlling process)라고 불린다.

당신이 로그인할 때 시스템이 당신을 위해서 그 일을 하기 때문에, 당신은 세션에 터미널 제어권을 할당하는 정확한 메카니즘에 대해서는 걱정할 필요가 없다. 개별적인 프로세스가 새로운 세션의 리더가 되기위해서 setsid를 호출할 때, 그 프로세스는 제어중인 터미널로부터 단절된다.


24.4 제어중인 터미널 억세스

터미널을 제어하고 있는 전면작업안의 프로세스들은 터미널에 대한 제한 없는 억세스 권한을 가진다; 배경프로세스들은 권한을 가지지 않는다. 이 절에서는 배경작업에 있는 프로세스가 터미널을 제어하려 시도할 때 어떤 일이 발생하는지에 대해서 자세히 설명하고 있다.

배경작업에 있는 프로세스가 전면작업이 제어하고 있는 터미널로부터 읽으려 시도할 때, 프로세스 그룹은 SIGTTIN 시그널을 받는다. 이것은 그룹안에 있는 모든 프로세스들의 동작을 멈추게 하는 원인이 된다(그 시그널이 처리되지 않거나 그들 스스로 멈추게 하지 않는다면). 그렇지만 읽기를 시도한 프로세스가 이 시그널을 무시하거나 블록한다면, 대신에 EIO 에러를 얻게 될 것이다.

유사하게, 배경작업에 있는 프로세스가 전면작업에서 제어하고 있는 터미널에 출력을 시도한다면, 그것에 대응된 디폴트 동작은 프로세스 그룹에게 SIGTTOU 시그널을 보내는 것이다. 그렇지만, 그 동작은 지역 모드 플래그(local modes flags)(12.4.7절 [Local Modes] 참조.)의 TOSTOP 비트에 의해서 갱신된다. 만일 이 비트가 설정되지 않는다면(이것이 디폴트이다.), 전면 작업에 의해 제어중인 터미널에 배경작업이 출력을 시도하는 것에는 시그널을 보내지 않고 항상 허가된다. 또한 출력을 시도하는 프로세스에 의해 SIGTTOU 시그널이 무시되거나 블록된다면 이때도 출력은 허가된다.

프로그램에서 할 수 있는 터미널 명령들의 대부분은 입력이나 출력을 위한 것이다. 기본적인 입력과 출력 함수에 대한 정보는 8.2절 [I/O Primitives] 참조.


24.5 고아가된 프로세스 그룹들

터미널 제어중이던 프로세스가 종료되면, 그 터미널은 해제되고 새로운 세션에 의해서 다시 제어될 수 있다.(실제로, 다른 사용자가 터미널 상에서 로그인 할 수 있다.) 이것은 만일 예전의 세션에 있는 어떤 프로세스가 그 터미널을 계속 사용하려고 시도한다면 문제가 발생할 수 있다.

그러한 문제들을 막기위해서, 세션리더가 종료된 후에도 계속 실행되고 있는 프로세스 그룹들은 고아가된 프로세스 그룹으로써 표시된다. 고아 프로세스 그룹(orphand process group) 안에 소속된 프로세스들은 터미널에 읽거나 쓰기를 할 수 없다. 만일 그렇게 하려 시도한다면 EIO 에러를 얻게 될 것이다.

어떤 프로세스 그룹이 고아가 되었을 때, 그 프로세스들은 SIGHUP 시그널을 받는다. 보통, 이것은 프로세스를 종료시키는 원인이 된다. 그렇지만, 만일 프로그램이 이 시그널을 무시하거나 시그널을 위한 핸들러를 만들었다면(21장 [Signal Handling] 참조.), 터미널을 제어중이던 프로세스가 종료되었다고 하더라도 실행중이던 고아 프로세스들은 계속 그 작업을 수행할 수 있다; 그렇지만 더 이상은 터미널의 억세스를 시도할 수 없다.


24.6 작업제어 쉘 실행시키기

이 절은 쉘이 작업 제어를 수행하기 위해서는 무엇을 해야만 하는지, 그 개념이 포함된 간단한 예제 프로그램을 통해서 설명하고 있다.

24.6.1절 [Data Structures]

이곳에서는 그 예제를 소개하고, 주요한 데이터 구조체를 소개하고 있다.

24.6.2절 [Initializing the Shell]

쉘이 작업제어를 준비하기 위해서 수행해야만 하는 동작들에 대해서 논의하고 있다.

24.6.3절 [Launching Jobs]

명령문을 실행하기 위해서 어떻게 작업들을 만들것이지에 대한 정보가 포함되어 있다.

24.6.4절 [Foreground and Background]

배경작업에 대립하는것으로써 전면으로 작업을 진출시킬 때 쉘이 무엇을 다르게 해야 만하는지를 설명하고 있다.

24.6.5절 [Stoppend and Terminated Jobs]

쉘에게 작업상황을 보고하기에 대해서 설명한다.

24.6.6 [Continuing Stopped Jobs]

작업이 멈추어졌을 때 다시 계속하기 위해서 어떻게 해야하는지를 설명한다.

24.6.7 [Missing Pieces] : 쉘의 다른 부분들에 대해서 설명하고 있다.

 

24.6.1 쉘을 위한 데이터 구조체들

이 장에 포함된 모든 예제 프로그램들은 간단한 쉘 프로그램의 일부분이다. 이 절은 이 예제에서 사용되고 있는 데이터 구조체와 유틸리티 함수들을 설명하고 있다.

간단한 쉘은 주로 두 개의 데이터 구조체를 다룬다. job 타입(type)은 파이프(pipes)로 서로 연결된 서브프로세스들의 집합인 작업에 대한 정보를 포함한다. process 타입(type)은 단일한 서브프로세스에 대한 정보를 저장하고 있다. 다음은 적절한 데이터 구조체 선언들이다.

/* process는 한 개의 단일한 프로세스이다. */
typedef struct process
{
struct process *next; /* 파이프라인에 있는 다음 프로세스 */
char **argv; /* exec을 위한 */
pid_tpid; /* 프로세스 ID */
char completed; /* 프로세스가 수행되었으면 참이다. */
char stopped; /* 만일 프로세스가 멈추었으면 참이다. */
int status; /* 보고된 상황들의 값 */
} process;
 
/* job 은 프로세스들의 파이프라인이다. */
typedef struct job
{
struct job *next; /* 다음 활동할 작업 */
char *command; /* 메시지를 위해서 사용되는, 커맨드 라인 */
process *first_process; /* 작업안에 있는 프로세스의 리스트 */
pid_t pgid; /* 프로세스 그룹 ID */
char notified; /* 사용자가 멈춘 작업에 대해서 말했으면 참이다. */
struct termios tmodes; /* 저장된 터미널 모드들 */
int stdin, stdout, stderr; /* 표준 입/출력 채널들 */
} job;
 
/* 활성화된 작업들은 리스트안에 연결되어 있고 이것이 그 헤드(head)이다.*/
job *first_job = NULL;
 
다음은 작업 오브젝트들은 운영하기 위해서 사용되는 유틸리티 함수들에 대한 것이다.
 
/* pgid를 통해서 활성 작업을 찾아라. */
job *
find_job (pid_t pgid)
{
job *j;
 
for (j = first_job; j; j = j->next) {
if (j->pgid == pgid)
return j;
}
return NULL;
}
 
/* 만일 job 안에 있는 모든 프로세스들이 완료되었거나 멈추어있다면 참을 리턴한다 */
int
job_is_stopped (job *j)
{
process *p;
 
for (p = j->first_process; p; p = p->next) {
if (!p->completed && !p->stopped)
return 0;
}
return 1;
}
 
/* 만일 job 안에 있는 모든 프로세스들이 완료되었다면 참을 리턴 */
int
job_is_completed (job *j)
{
process *p;
 
for (p = j->first_process; p; p = p->next) {
if (!p->completed)
return 0;
}
return 1;
}

 

24.6.2 쉘 초기화하기

일반적으로 작업제어를 수행하는 쉘 프로그램이 시작될 때, 그 쉘이 이미 자신의 작업제어를 수행하고 있는 다른 쉘로부터 호출되었을 경우에는 주의를 해야만 한다. 서브쉘(subshell)에서 작업제어가 가능하게 되기전에 우선 부모 쉘에 의해서 전면에 놓여져야만 한다. 그것은 getpgrp로 초기 프로세스 그룹 ID를 얻고, 현재 터미널을 제어하고 있는 전면작업의 프로세스 그룹 ID와 그것을 비교해서 한다( tcgetpgrp함수를 사용해서 추출될 수 있다. )

만일 그 서브쉘이 전면 작업으로써 실행되고 있지 않다면, 그것은 그 자신의 프로세스 그룹에 SIGTTIN 시그널을 보냄으로써 스스로 멈춰야만 한다. 그것은 전면에 그 자신을 제멋대로 놓을수 없다; 사용자가 부모 쉘에게 이것을 시키도록 기다려야만 한다. 만일 그 서브쉘이 계속된다면, 검사를 반복하고 만일 그것이 여전히 전면에 있지 않다면 다시 스스로 멈춘다.

일단 서브쉘이 부모 쉘에 의해서 전면에 놓이게되면, 서브쉘은 자신의 작업 제어가 가능하게 된다. 자신소유의 프로세스 그룹안에 그 자신이 놓이도록 setpgid를 호출하고, 그다음 전면에 이 프로세스 그룹이 놓이도록 tcsetpgrp를 호출함으로써 이러한 일을 한다.

쉘이 작업제어가 가능하게 되었을 때, 작업제어를 멈추도록 하는 모든 시그널에 대해서 무시하도록 그 자체에서 설정하기 때문에 그 자체는 우연히 멈추게 되지 않는다. 당신은 모든 멈춤 시그널들을 SIG_IGN 으로 설정하여 이러한 일을 할 수 있다.

비-대화식으로 실행되는 서브쉘은 작업제어가 지원되지도 않고 지원될 수도 없다. 그것은 그자체가 쉘인 프로세스 그룹안에 만든 모든 프로세스가 존재해야만 한다; 이것은 비-대화실 쉘과 부모 쉘에 의해 단일한 작업으로 취급되는 자식 프로세스를 허용한다. 이것은 작업제어를 사용하지 않지만 그것을 하는 쉘을 만들기 위해서는 기억해야만 한다.

다음은 이러한 모든 것을 어떻게 하는지 보여주는 예제 쉘을 위한 초기 코드이다.

/* 쉘의 속성을 기억하라. */
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
 
pid_t shell_pgid;
struct termios shell_tmodes;
int shell_terminal;
int shell_is_interactive;
 
/* 진행하기 전에 쉘이 전면작업으로써 대화식으로 실행되고 있는지를 확인하라. */
void
init_shell ()
{
 
/* 만일 대화식으로 실행된다면 보아라. */
shell_terminal = STDIN_FILENO;
shell_is_interactive = isatty (shell_terminal);
 
if (shell_is_interactive)
{
/* 전면에 놓일때까지 루프를 돈다. */
while (tcgetpgrp (shell_terminal) != (shell_pgid = getpgrp()))
kill (- shell_pgid, SIGTTIN);
 
/* 대화와 작업-제어 시그널들을 무시하라. */
signal (SIGINT, SIG_IGN);
signal (SIGQUIT, SIG_IGN);
signal (SIGTSTP, SIG_IGN);
signal (SIGTTIN, SIG_IGN);
signal (SIGTTOU, SIG_IGN);
signal (SIGCHLD, SIG_IGN);
 
/* 우리소유의 프로세스 그룹에 우리자신을 넣어라. */
shell_pgid = getpid ();
if (setpgid (shell_pgid, shell_pgid) < 0) {
perror("Couldn't put the shell in its own process group");
exit (1);
}
 
/* 터미널의 제어를 잡아라 */
tcsetpgrp (shell_terminal, shell_pgid);
 
/* 쉘을 위한 디폴트 터미널 속성을 저장하라. */
tcgetattr (shell_terminal, &shell_tmodes);
}
}

 

24.6.3 작업들을 개시하기

일단 쉘이 터미널을 제어하여서 작업제어를 수행하는 책임을 갖게 되면, 쉘은 사용자에의해 입력된 명령문에 응답하여 작업을 개시할 수 있다. 프로세스 그룹에서 프로세스들을 만들기 위해서는, 23.2절 [Process Creation Concepts] 에 설명된 fork 와 exec를 사용한다. 그곳에는 여러개의 자식 프로세스들이 이미 존재하고 있기 때문에, 그 일을 하기는 좀 많이 복잡하고 당신은 올바른 순서로 프로세스를 만들도록 주의를 해야만 한다. 그렇지 않다면, 고약한 경쟁상황(race condition)이 발생될 것이다.

당신은 프로세스들 사이의 부모-자식 관계를 어떻게 구성할것인지에 대해서 두가지를 선택할 수 있다. 프로세스 그룹안에 있는 모든 프로세스들을 쉘 프로세스의 자식 프로세스로 만들거나, 또는 그룹에 있는 한 개의 프로세스를 그 그룹에 있는 다른 모든 프로세스들의 조상이 되도록 만들수 있다. 이 장에 설명된 예제 쉘 프로그램은 첫 번째가 좀 간단하게 때문에 첫 번째를 사용한다.

만들어진 각각의 프로세스는, setpid를 호출하여 새로운 프로세스 그룹에 그 자신을 넣어야만 한다; 24.7.2절 [Process Group Functions] 참조. 새로운 그룹에 있는 첫 번째 프로세스는 그 프로세스 그룹의 리더가 되고 그 프로세스 ID가 그 그룹을 위한 프로세스 그룹 ID가 된다.

쉘은 새로운 프로세스 그룹으로 각각의 자식 프로세스들을 넣기 위해서 setpgid를 호출한다. 이것은 타이밍 문제가 내포되어 있다; 각각의 자식 프로세스는 새로운 프로그램이 실행되기 전에 프로세스 그룹에 들어가야만 하고, 쉘은 실행을 계속하기 전에 그룹이 가지고 있는 모든 자식 프로세스에 의존한다. 만일 자식 프로세스와 쉘이 모두 setpgid를 호출한다면, 이것은 프로세스가 먼저 그 일을 처리하도록 해서 아무런 문제가 발생되지 않을 것이다.

만일 전면 작업으로 어떤 작업이 진행되고 있다면, 새로운 프로세스 그룹을 tcsetpgrp를 사용해서 터미널을 제어하고 있는 전면으로 넣을 필요가 있다. 다시, 이것은 경쟁 상황을 피하기 위해서 각각의 자식 프로세스 뿐만아니라, 쉘에 의해서도 수행된다. 각 자식 프로세스의 다음 일은 시그널동작을 재설정하는 일이다.

초기화 하는동안, 쉘 프로세스는 작업제어 시그널들을 무시하도록 그 자신을 설정한다; 24.6.2절 [Initializing the Shell] 참조. 그 결과로, 그것이 만든 어떤 자식 프로세스도 또한 상속으로 인해서 그러한 시그널들을 무시하게 된다. 이것에 만족하지 못하면, 각각의 자식 프로세스는 그것이 만들어진 후에 SIG-DFL을 사용해서 그 시그널의 원래 동작으로 되돌려 설정할 수 있다.

쉘이 이러한 관습을 따르기 때문에, 응용 프로그램은 그들이 부모 프로세스로부터 시그널의 처리에 대한 것을 정확히 상속받는다고 가정 할 수 있다. 그러나 모든 응용프로그램은 멈춤 시그널들의 처리에 간섭할 수 없다. SUSP 문자의 일반적인 해석이 불가능하게 만든 응용프로그램은 사용자가 작업을 멈추게 하기 위해서 다른 메카니즘을 제공한다. 사용자가 이 메카니즘을 호출할 때, 그 프로그램은 단지 프로세스 그 자체가 아니라, 그 프로세스의 프로세스 그룹에게 SIGTSTP 시그널을 보낸다. 21.6.2절 [Signaling Another Process] 참조.

마침내, 각각의 자식 프로세스는 보통의 방법으로 exec를 호출하여야 한다. 이것은 표준 입/출력 채널들의 리다이렉션이 처리되는 지점이다. 8.8절 [Duplicating Descriptors] 를 참조하여 이것을 어떻게 하는지 보아라. 다음은 프로그램을 개시하기 위한, 대화식 쉘 프로그램에 있는 함수이다. 그 함수는 쉘에 의해서 자식 프로세스가 만들어진 후에 즉시 자식 프로세스에 의해서 실행되고 결코 리턴하지 않는다.

void
launch_process (process *p, pid_t pgid, int infile, int outfile, int errfile, int foreground)
{
pid_t pid;
 
if (shell_is_interactive)
{
/* 프로세스 그룹에 그 프로세스를 넣고 터미널을 프로세스 그룹에게 주어라, 적당하면, 이것은 쉘과 개별적인 자식 프로세스에 의해서 모두 실행되기 때문에 경쟁상황을 포함하고 있다. */
pid = getpid ();
if (pgid == 0) pgid = pid;
setpgid (pid, pgid);
if (foreground)
tcsetpgrp (shell_terminal, pgid);
 
/* 작업제어 시그널위한 처리를 디폴트로되돌려 설정하라. */
signal (SIGINT, SIG_DFL);
signal (SIGQUIT, SIG_DFL);
signal (SIGTSTP, SIG_DFL);
signal (SIGTTIN, SIG_DFL);
signal (SIGTTOU, SIG_DFL);
signal (SIGCHLD, SIG_DFL);
}
 
/* 새로운 프로세스를 위한 표준 입/출력 채널들을 설정하라. */
if (infile != STDIN_FILENO)
{
dup2 (infile, STDIN_FILENO);
close (infile);
}
if (outfile != STDOUT_FILENO)
{
dup2 (outfile, STDOUT_FILENO);
close (outfile);
}
if (errfile != STDERR_FILENO)
{
dup2 (errfile, STDERR_FILENO);
close (errfile);
}
 
/* 새로운 프로세스를 실행하라. exit를 확인하라. */
execvp (p->argv[0], p->argv);
perror ("execvp");
exit (1);
}

만일 쉘이 대화식으로 실행되지 않으면, 이 함수는 프로세스 그룹이나 시그널로 아무일도 하지 않는다. 작업제어를 수행하지 않는 쉘은 그 자체가 쉘인 프로세스 그룹안에 모든 서브프로세스들이 존재해야만 한다.

다음은 실제로 완전한 작업을 시작하는 함수이다. 자식 프로세스가 만들어진 후에, 이 함수는 전면이나 배경으로 새로이 만들어진 작업을 넣기위해서 어떤 다른 함수를 호출한다; 그것은 24.6.4절 [Foreground and Background] 에 설명되어 있다.

void
launch_job (job *j, int foreground)
{
process *p;
pid_t pid;
int mypipe[2], infile, outfile;
 
infile = j->stdin;
for (p = j->first_process; p; p = p->next)
{
/* 만일 필요하면 pipe들을 준비하라. */
if (p->next) {
if (pipe (mypipe) < 0) {
perror ("pipe");
exit (1);
}
outfile = mypipe[1];
} else {
outfile = j->stdout;
}
 
/* 자식 프로세스들을 만들어라 */
pid = fork ();
if (pid == 0) { /* 이것은 자식 프로세스이다. */
launch_process (p, j->pgid, infile, outfile, j->stderr, foreground);
} else if (pid < 0) {
/* fork 가 실패했다. */
perror ("fork");
exit (1);
} else { /* 이것은 부모 프로세스이다. */
p->pid = pid;
if (shell_is_interactive) {
if (!j->pgid)
j->pgid = pid;
setpgid (pid, j->pgid);
}
}
 
/* pipe들을 정리하라. */
if (infile != j->stdin)
close (infile);
if (outfile != j->stdout)
close (outfile);
infile = mypipe[0];
}
 
format_job_info (j, "launched");
 
if (!shell_is_interactive)
wait_for_job (j);
else if (foreground)
put_job_in_foreground (j, 0);
else
put_job_in_background (j, 0);
}

 

24.6.4 전면 과 배경

이제 전면(foreground)에 있는 작업을 시작할 때 쉘에 의해서 행해져야만 하는 동작은 무엇이며, 배경작업이 시작될 때 해야만 되는 것과 무엇이 어떻게 다른지 알아보자. 전면작업이 시작될 때, 쉘은 첫 번째로 tcsetpgrp를 호출해서 그 전면작업에 터미널 제어권을 줘야만 한다. 그리고나서, 쉘은 프로세스 그룹안에 있는 프로세스들이 종료되거나 멈출때까지 기다려야한다. 이것에 대한 상세한 내용은 24.6.5절 [Stopped and Terminated Jobs] 를 참조하라.

그룹안에 있는 모든 프로세스들이 수행됐거나 멈추었을 때, 쉘은 다시 tcsetpgrp를 호출해서 자신의 프로세스 그룹을 위해서 터미널 제어권을 되찾는다. 배경작업으로부터의 입/출력이나 사용자에의해 입력된 SUSP 문자가 원인이 된 멈춤 시그널이 프로세스 그룹에게 보내어지면, 보통, 작업안에 있는 모든 프로세스들이 함께 멈춘다.

전면작업이 터미널을 이상한 상황으로 만들었을지도 모르므로, 쉘은 계속하기전에 자신이 저장해놓았던 터미널 모드들을 반환하여야한다. 작업이 완전히 멈춘경우에, 쉘은 일단 현재 터미널 모드들을 저장하고, 그래서 만일 그 작업이 나중에 다시 계속된다면 그들을 반환할 수 있다. 터미널 모드들을 다루는 함수들은 tcgetattr 과 tcsetattr이 있다; 그들은 12.4절 [Terminal Modes] 를 참조하라. 다음은 위에 설명된 일들을 하는 예제 쉘을 함수이다.

/* 작업 j를 전면에 놓는다. 만일 cont가 0이 아니면, 저장된 터미널 모드들을 반환하고 우리가 블록하기 전에 그것이 계속되도록 SIGCONT 시그널을 프로세스 그룹에게 보낸다. */
void
put_job_in_foreground (job *j, int cont)
{
/* 전면으로 작업을 놓는다. */
tcsetpgrp (shell_terminal, j->pgid);
 
/* 만일 필요하다면 작업을 계속하도록 시그널을 보낸다. */
if (cont) {
tcsetattr (shell_terminal, TCSADRAIN, &j->tmodes);
if (kill (- j->pgid, SIGCONT) < 0)
perror ("kill (SIGCONT)");
}
 
/* 보고하도록 기다린다. */
wait_for_job (j);
 
/* 전면에 쉘을 다시 놓아라. */
tcsetpgrp (shell_terminal, shell_pgid);
 
/* 쉘의 터미널 모드들을 반환한다. */
tcgetattr (shell_terminal, &j->tmodes);
tcsetattr (shell_terminal, TCSADRAIN, &shell_tmodes);
}
만일 프로세스 그룹이 배경작업으로써 시작된다면, 쉘은 전면에 자신을 놓고 터미널로부터 명령문 읽기를 계속한다. 예제 쉘에서는, 배경에 작업을 놓아야 될 필요가 그다지 없다. 다음은 그것을 사용하는 함수이다.
/* 배경에 작업을 놓는다. 만일 cont 인수가 참이면, 그것이 계속되도록 SIGCONT 시그널을 프로세스 그룹에게 보낸다. */
void
put_job_in_background (job *j, int cont)
{
/* 만일 필요하면, 작업을 계속하도록 시그널을 보낸다. */
if (cont)
if (kill (-j->pgid, SIGCONT) < 0)
perror ("kill (SIGCONT)");
}

 

24.6.5 멈추고 종료된 작업들

전면 프로세스가 시작될 때, 쉘은 작업에 있는 모든 프로세스들이 멈추거나 종료될때까지 블록해야만 한다. 그것은 waitpid 함수를 호출함으로써 이루어진다; 23.6절 [Process Completion] 참조. 프로세스가 종료된것뿐만 아니라 멈춤 또한 보고하도록 WUNTRACED 옵션을 사용하라. 쉘은 배경작업이 멈추었거나 종료된 작업들을 사용자에게 보고하도록 배경작업들의 상황을 체크해야만 한다; 이것은 WNOHANG 옵션을 사용해서 waitpid 함수를 호출함으로써할 수 있다. 작업이 멈추었거나 종료되었음을 체크하는것과 같은 코드문을 넣는 좋은 위치는 새로운 명령문을 읽으려(prompting) 하기 전이다.

쉘은 SIGCHLD 시그널을 처리하는 핸들러를 만듦으로써 자식 프로세스를 위한 유용한 상황정보를 비동기적으로 통지받을 수 있다. 21장 [Signal Handling] 참조.

예제 쉘 프로그램의 경우에는, SIGCHLD 시그널은 보통 무시된다. 쉘이 조작하고 있는 전역 데이터 구조체를 재진입하는 문제를 피하기 위함이다. 그러나 쉘이 그들 데이터 구조체를 사용하지 않는 어떤 시간동안에는 _예를들어, 터미널에서 입력을 기다리는 때와 같은_SIGCHLD 시그널을 처리하는 핸들러는 가능하다. 동기적으로 상황들을 체크하는데 사용되는 같은 함수는 (이 경우, do_job_notification) 이 핸들러로부터 호출될 수 있다.

다음은 작업의 상황들을 체크하고 사용자에게 정보를 보고하는 것을 취급하는 예제 쉘 프로그램의 일부분이다.

/* waitpid에 의해 리턴된 프로세스 pid의 상황을 저장하라. 모두 잘 됐으면 0을 리턴하고, 아니면 0이 아닌값을 리턴하라. */
 
int
mark_process_status (pid_t pid, int status)
{
job *j;
process *p;
 
if (pid > 0) {
/* 프로세스를 위해서 기록을 갱신하라. */
for (j = first_job; j; j = j->next) {
for (p = j->first_process; p; p = p->next) {
if (p->pid == pid) {
p->status = status;
if (WIFSTOPPED (status)) {
p->stopped = 1;
} else {
p->completed = 1;
if (WIFSIGNALED (status))
fprintf(stderr, "%d: Terminated by signal %d.\n", (int)pid, WTERMSIG(p->status));
}
return 0;
}
fprintf (stderr, "No child process %d.\n", pid);
return -1;
} /* for 의 끝 */
} /* for 의 끝 */
} else if (pid == 0 || errno == ECHILD) {
/* 보고하기위해 준비된 프로세스가 없다. */
return -1;
} else { /* Other weird errors. */
perror ("waitpid");
return -1;
}
}
 
/* 블록하지지 않고, 유용한 상황정보를 가진 프로세스들을 체크하라. */
 
void
update_status (void)
{
int status;
pid_t pid;
 
 
do {
pid = waitpid(WAIT_ANY, &status, WUNTRACED|WNOHANG);
} while (!mark_process_status (pid, status));
}
 
/* 주어진 작업안에 있는 모든 프로세스들이 보고되었을때까지 블록하여, 유용한 상황정보를 가진 프로세스들을 체크하라. */
 
void
wait_for_job (job *j)
{
int status;
pid_t pid;
 
do {
pid = waitpid (WAIT_ANY, &status, WUNTRACED);
} while (((!mark_process_status (pid, status))
&& (!job_is_stopped (j))
&& (!job_is_completed (j)));
}
 
/* 사용자에게 보이기위한 작업상황에 대한 정보를 형식화하라. */
 
void
format_job_info (job *j, const char *status)
{
fprintf(stderr, "%ld (%s): %s\n", (long)j->pgid, status, j->command);
}
 
/* 멈추었거나 종료된 작업들에 대해서 사용자에게 보고하라. 활성화된 작업 리스트로부터 종료된 작업들을 지워라. */
 
void
do_job_notification (void)
{
job *j, *jlast, *jnext;
process *p;
 
/* 자식 프로세스를 위한 상황정보들을 갱신하라. */
update_status ();
 
jlast = NULL;
for (j = first_job; j; j = jnext)
{
jnext = j->next;
 
/* 만일 모든 프로세스들이 완수되었다면, 작업들이 완수되었음을 사용자에게 알리고 활성 작업 리스트에서 그것을 지워라. */
if (job_is_completed (j)) {
format_job_info (j, "completed");
if (jlast)
jlast->next = jnext;
else
first_job = jnext;
free_job (j);
} else if (job_is_stopped (j) && !j->notified) {
/* 더 이상 진행할 수 없음을 표시하여, 멈추어진 작업에 대하여 사용자에게 통지하라. */
format_job_info (j, "stopped");
j->notified = 1;
jlast = j;
} else /* 여전히 실행되고 있는 작업들에 대해서는 아무것도 말하지 말아라. */
jlast = j;
}
}

 

24.6.6 멈추어있는 작업들을계속 실행시키기

쉘은 프로세스 그룹에게 SIGCONT 시그널을 보냄으로써 멈추어진 작업을 계속하게 할 수 있다. 만일 작업이 전면에서 계속 실행되어야하면, 쉘은 일단 tcsetpgrp 를 호출해서 터미널 제어권을 그 작업에게 부여하고, 저장된 터미널 설정을 반환한다. 전면에서 작업이 재개된 후에, 쉘은 그 작업이 마치 전면에서 시작된것처럼, 그 작업이 멈추거나 종료되기를 기다린다.

예제 쉘 프로그램에서 새로이 만들어진 작업과 재개된 작업들에 대한 처리는 put_job_in_foreground 와 put_job_in_background 한쌍의 함수로 이루어진다. 그들 함수에 대한 정의는 24.6.4절 [Foreground and Background] 에 나와있다. 멈추었던 작업을 재개할 때, cont 인수에 주어진 0이 아닌값은 SIGCONT 시그널을 보내고 적당한 터미널 모드를 반환함을 확실하게 한다.

다음은 재개되어 실행되고 있는 작업에 대해서 쉘이 내부적으로 보유하고 있던 정보를 갱신하는 함수이다.

/* 멈추어있던 작업 J를 다시 실행되고 있는 작업으로써 표시하라. */
 
void
mark_job_as_running (job *j)
{
Process *p;
 
for (p = j->first_process; p; p = p->next)
p->stopped = 0;
j->notified = 0;
}
 
/* 작업 J를 계속하라. */
 
void
continue_job (job *j, int foreground)
{
mark_job_as_running (j);
if (foreground)
put_job_in_foreground (j, 1);
else
put_job_in_background (j, 1);
}

 

24.6.7 이 장에서 설명되지 않은 부분들

이장에 포함된 예제 쉘을 위한 여분의 코드는 전체 쉘 프로그램을 위한 일부분이다. 하지만, 어떻게 작업과 프로그램 데이터 구조체들이 할당되고 초기화되는지에 대해서는 전혀 얘기하지 않았다. 대부분의 실제 쉘은 명령문; 변수들; 약어, 대입, 그리고 파일이름의 패턴 매칭등을 위한 것을 제공하는, 복잡한 사용자 인터페이스로 되어있다. 이곳에 설명된 이 모든 것은 복잡함과는 거리가 멀다! 대신에, 우리는 어떻게 코아 프로세스를 생성을 하는지를 보여주고 그와 같은 쉘에서 어떤 작업제어 함수들이 호출될 수 있는지에 대해서 관심을 두었다. 다음은 우리가 표현했던 주요 엔트리 포인트를 요약한 테이블이다.

void init_shell (void)

쉘의 내부 상황을 초기화한다. 24.6.2절 [Initializing the Shell] 참조.

void init_sh

void launch_job (job *j, int foreground)

전면 또는 배경에서 작업 j를 시작한다. 24.6.3절 [Launching Jobs] 참조.

void do_job_notification (void)

종료되거나 멈추어진 어떤 작업들이 있는지 체크하고 보고한다. SIGCHLD 시그널을 위한 핸들러 안에서 또는 동기적으로 호출될 수 있다. 24.6.5절 [Stopped and Terminated Jobs] 참조.

void continue_job (job *j, int foreground)

작업 j를 재개한다. 24.6.6절 [Continuing Stopped Jobs] 참조.
물론, 실제 쉘은 작업을 처리하기 위해서 다른 함수들을 제공받기 원할지 모른다. 예를들어, 활성화된 모든 작업들의 리스트를 보여주거나, 작업에게 시그널(SIGKILL 과 같은)을 보내는 명령문은 유용할 것이다.


24.7 작업제어를 위한 함수들

이 절은 작업제어와 연관된 함수들을 자세하게 설명하고 있다.

 

24.7.1 제어중인 터미널 확인하기

당신은 제어중인 터미널을 개방하기 위해서 사용할 수 있는 파일이름을 얻기 위해서 ctermid 함수를 사용할 수 있다. GNU 라이브러리에서는, 항상 같은 문자열이 리턴된다: "/dev/tty". 그것은 현재 프로세스의 제어 중인 터미널(만일 그것이 한 개라면)을 참조하기위한 "이상한" 특별파일 이름이다. ctermid 함수는 헤더파일 `stdio.h'에 선언되어 있다.

함수 : char * ctermid (char *string)

ctermid 함수는 현재 프로세스를 위해서 제어중인 터미널의 파일이름이 포함된 문자열을 리턴한다. 만일 문자열이 널 포인터가 아니라면, 그것은 적어도 L_ctermid 문자들을 저장하고 있는 배열이 될 것이다. 문자열을 이 배열안에 리턴된다. 그렇지 않다면, 정적 지역안에 리턴된 문자열에 대한 포인터는 이 함수가 연속적으로 호출되어 덧씌워질 것이다.
만일 어떠한 이유로 파일이름이 결정될 수 없다면 빈 문자열(empty string)을 리턴한다. 심지어 파일이름이 리턴됐을지라도, 그 파일에 대한 억세스는 보증되지 않는다.

매크로 : int L_ctermid

이 매크로는 ctermid에 의해 리턴된 파일이름을 저장하기에 충분히 큰 문자열의 크기를 나타내는 정수 상수 표현식이다.

또한 12.1절 [Is It a Terminal] 에 있는 isatty 와 ttyname 함수를 참조하라.

 

24.7.2 프로세스 그룹 함수들

다음은 프로세스 그룹을 다루기 위한 함수를 설명하고 있다. 당신의 프로그램에서 그들 함수들을 사용하기 위해서는 헤더파일 `sys/types.h' 와 `unistd.h'를 포함해야만 한다.

함수 : pid_t setsid (void)

setsid 함수는 새로운 세션을 만든다. 호출한 프로세스는 세션리더가 되고, 그 프로세스의 프로세스 ID와 같은 ID를 가진 새로운 프로세스 그룹이 만들어지고 프로세스가 그 안에 소속된다. 새로운 프로세스 그룹에 다른 프로세스들이 없고 새로운 세션안에 다른 프로세스 그룹이 없이 초기화된다. 이 함수는 또한 호출한 프로세스가 제어중인 터미널을 갖지 않도록 만든다.
 
setsid 함수는 만일 성공하면 호출한 프로세스의 새로운 프로세스 그룹 ID를 리턴하고 실패하면 -1의 값을 리턴한다.  
다음은 이 함수를 위해 정의된 에러상황이다.

    EPERM

    호출한 프로세스가 이미 프로세스 그룹의 리더이거나, 같은 프로세스 그룹 ID를 가진 다른 프로세스 그룹이 있다.
 
getpgrp 함수는 두 개의 정의를 가진다; 한 개는 BSD 유닉스에서 왔고, 한 개는 POSIX.1 표준으로부터 온 것이다. 당신이 선택한 테스트 매크로(1.3.4절 [Feature Test Macros] 참조.)에 따라서 당신이 어떤 정의를 선택했는지가 결정된다. 즉, 당신이 _BSD_SOURCE를 정의하면 BSD 것이 선택되고; 그렇지않고, _POSIX_SOURCE 또는 _GNU_SOURCE를 정의한다면 POSIX의 것이 선택된 것이다. 오래된 BSD 시스템에서 만들어진 프로그램들은 _BSD_SOURCE 하에서 특별히 정의된 getgrp를 사용하도록 `unistd.h'를 인클루드 할 수없다. 당신은 BSD정의를 획득 하기 위해서 -lbsd-compact 로 그와 같은 프로그램들을 링크해야만 한다.

    POSIX.1 함수 : pid_t getpgrp (void)

    getpgrp의 POSIX.1 정의는 호출한 프로세스의 프로세스 그룹 ID를 리턴한다.

    BSD 함수 : pid_t getpgrp (pid_t pid)

    getpgrp 의 BSD 정의는 프로세스 pid의 프로세스 그룹 ID를 리턴한다. 당신이 호출한 프로세스에 대한 정보를 얻기위해서는 pid인수를 위해서 0의 값을 사용할 수 있다.

함수 : int setpgid (pid_t pid, pid_t pgid)

setpgid 함수는 프로세스 그룹 pgid에 프로세스 pid를 소속시킨다. 특별한 경우로, pid 또는 pgid는 호출한 프로세스의 프로세스 그룹을 지적하기 위해서 0으로 될 수 있다. 이 함수는 작업제어를 지원하지 않는 시스템에서는 실패한다. 24.2절 [Job Control is Optional] 참조하여 상세한 정보를 얻어라.
 
만일 명령이 성공하면, setpgid는 0을 리턴한다. 그렇지 않다면 -1을 리턴한다.  
다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EACCES : pid라는 이름을 가진 자식 프로세스가 만들어진 후에 exec 함수를 통해서 실행되었다.

EINVAL : pgid의 값이 유용하지 않다.

ENOSYS : 시스템이 작업제어를 지원하지 않는다.

EPERM

pid 인수에 의해 지적된 프로세스가 세션리더이거나, 호출한 프로세스와 같은 세션에 없거나, pgid 인수의 값이 호출한 프로세스와 같은 세션안에 있는 프로세스 그룹 ID와 매치되는 것이 없다.

ESRCH

pid 인수에 의해 지적된 프로세스가 호출한 프로세스가 아니거나 호출한 프로세스의 자식 프로세스가 아니다.

함수 : int setpgrp(pid_t pid, pid_t pgid)

이것은 setpgid를 위한 BSD용이다. 두 함수는 같은 일을 한다.

 

24.7.3 제어권을 가진 터미널을 억세스하기 위한 함수들

다음은 터미널의 전면프로세스 그룹을 알아내거나 설정하기 위한 함수들이다. 당신은 그들 함수들을 당신의 어플리케이션에서 사용하기 위해서는 헤더파일 `sys/types.h'와 `unistd.h'를 인클루드해야만 한다. 그들 함수들이 터미널 디바이스를 정하도록 파일 기술자 인수를 정한다고 하더라도, 전면작업은 터미널 파일 그자체와 연관되어 있고 특별히 개방된 파일 기술자가 아니다.

함수 : pid_t tcgetpgrp(int filedes)

이 함수는 기술자 filedes로 개방한 터미널과 연관된 전면 프로세스 그룹의 프로세스 그룹 ID를 리턴한다. 만일 아무런 전면 프로세스 그룹이 없다면, 현존하는 프로세스 그룹의 프로세스 그룹 ID와 매치 되지 않는, 1보다 큰 수를 리턴한다. 그러한 경우는 전면작업으로써 수행되고 있던 작업안에 있는 모든 프로세스들이 종료되고 전면으로 옮겨질 작업들이 없을 때 발생할 수 있다
 
에러의 경우에 -1의 값이 리턴된다. 다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EBADF : filedes 인수가 유용한 파일 기술자가 아니다.

ENOSYS : 시스템이 작업제어를 지원하지 않는다.

ENOTTY : filedes 인수와 연관된 터미널 파일이 호출한 프로세스가 제어중인 터미널이 아니다.

함수 : tcsetpgrp(int filedes, pid_t pgid)

이 함수는 터미널의 전면 프로세스 그룹 ID를 설정하는데 사용된다. 인수 filedes는 터미널을 지정하는 기술자이다; pgid 인수는 프로세스 그룹을 지정한다. 호출한 프로세스는 pgid와 같은 세션의 멤버가 되어야만 하고 같은 터미널을 제어하고 있어야만 한다.
 
터미널 억세스를 목적으로 하여, 이 함수는 출력으로 취급된다. 만일 배경 프로세스로부터 이 함수가 호출된다면, 보통 그 프로세스 그룹안에 있는 모든 프로세스들에게 SIGTTOU 시그널이 보내어진다. 만일 호출한 프로세스 자체가 SIGTTOU 시그널을 무시하는 예외의 경우라면, 그 명령은 수행되고 아무런 시그널도 보내어지지 않는다.
 
만일 성공하면, tcsetpgrp 는 0을 리턴한다. 에러가 발생한 경우에는 -1을 리턴한다.
다음의 errno는 이 함수를 위해 정의된 에러상황이다.

EBADF : filedes 인수가 유용한 파일 기술자가 아니다.

EINVAL : pgid 인수가 유용하지 않다.

ENOSYS : 시스템이 작업제어를 지원하지 않는다.

ENOTTY : filedes는 호출한 프로세스가 제어하고 있는 터미널이 아니다.

EPERM : pgid 는 호출한 프로세스와 같은 세션에 있는 프로세스 그룹이 아니다.


목차 이전 : 23. 프로세스 다음 : 25. 사용자와 그룹

'Programming > Unix Programing' 카테고리의 다른 글

26. System Information  (0) 2007.12.22
25. 사용자와 그룹  (0) 2007.12.22
24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
21. 신호처리(Signal)  (0) 2007.12.22
Comment 0 Trackback 0
Top

23. Processes

목차 이전 : 22. 프로세스의 시동과 종료 다음 : 24. 작업 제어


23 프로세스

프로세스들은 시스템의 자원들의 할당을 위한 기본적인 단위이다. 각 프로세스는 자신만의 주소공간과 (보통) 한 개의 제어 쓰레드를 갖는다. 프로세스는 프로그램을 실행한다; 당신은 같은 프로그램을 실행하는데 여러개의 프로세스를 가질 수 있지만, 각각의 프로세스는 자신의 주소공간에서 자신의 프로그램 복제본을 갖고 다른 프로그램 복사본과 독립적으로 실행된다.

프로세스들은 계층적으로 구성된다. 각 프로세스는 그것을 만들었던 부모 프로세스를 갖는다. 주어진 부모 프로세스에 의해 만들어진 프로세스는 자식 프로세스라고 불린다. 자식 프로세스는 부모 프로세스로 부터 그 속성의 대부분을 상속받는다.

이 장은 프로그램에서 어떤 자식 프로세스를 만들고, 종료하고, 제어하는지에 대해서 설명하고 있다. 실제로, 새로운 자식 프로세스를 만들로, 새로운 프로세스가 프로그램을 실행시키고, 원래의 프로그램과 자식 프로세스가 조화롭게 수행되도록 하는데에는 세 가지의 독림적인 명령이 있다.

system 함수는 다른 프로그램을 실행시키기 위한 간단하고, 이식성 있는 메커니즘을 제공한다. 그들은 자동적으로 세 개의 단계를 거친다. 만일 당신이 이러한 일들을 하는데 세심하게 제어할 필요가 있다면, 당신은 각 단계를 개별적으로 수행하는 기본 함수들을 사용할 수 있다.


23. 1 명령 실행시키기

다른 프로그램을 실행하기 위한 가장 쉬운 방법은 system 함수를 사용하는 것이다. 이 함수는 실행되는 서브프로그램의 모든 작업을 수행하지만, 당신이 세밀하게 제어를 할 수는 없다: 당신이 어떤 것을 할 수 있기 전에 그 서브 프로그램이 종료될 때까지 기다려야만 한다.

함수 : int system (const char *command)

이 함수는 쉘 커맨드로써 command를 실행한다. GNU C 라이브러리에서, 그것은 항상 커맨드를 실행하도록 디폴트 쉘 sh를 사용한다. 특별하게, 실행하기 위한 프로그램을 찾기 위해서 PATH에 있는 디렉토리들을 탐색한다. 반환값은 만일 쉘 프로세스를 만들기가 불가능하면 -1을 반환하고 그렇지 않다면 쉘 프로세스의 상황이 된다. 이 상황 코드가 어떻게 해석되어지는지에 대해서는 23. 6절 [Process Completion] 참조. system 함수는 헤더파일 `stdlib. h'에 선언되어 있다.
이식성 노트 : 어떤 C에서는 다른 프로그램을 실행할 수 있는 커맨드 프로세스의 개념을 가지지 않을 수도 있다. 당신은 커맨드 프로세스가 존재하는지에 대한 여부를 system(NULL)을 실행해봄으로써 알아낼 수 있다; 만일 그 반환값이 0이 아니면, 커맨드 프로세서는 유용하다.

popen 과 pclose 함수들은(10. 2절 [Pipe to a Subprocess] 참조) system 함수와 밀접하게 연관되어 있다. 그들은 실행되고 있는 커맨드의 표준 입/출력 채널과 통신하도록 부모 프로세스에게 허용한다.


23. 2 프로세스 만들기에 대한 원칙

이 절은 프로세스와 프로세스를 만드는데 포함되는 단계와 그것이 다른 프로그램을 실행하도록 만드는데 대한 개요가 있다. 각 프로세스는 프로세스 ID 번호에 의해 이름지어진다. 어떤 단일한 프로세스 ID는 그것이 만들어질 때 각 프로세스에게 할당되어진다. 프로세스의 수명이 다하면 부모 프로세스에게 보고된다; 그때, 그 프로세스 ID에 포함된 프로세스의 모든 자원들은 해제된다.

프로세스들은 fork 시스템 호출로 만들어진다( 그래서 새로운 프로세스를 만드는 동작을 forking a process라고 부르기도 한다. ). fork에 의해 만들어진 자식 프로세스는 오직 자신의 프로세스 ID를 제외하고는, 원래 부모 프로세스의 완전한 복제이다.

자식 프로세스를 만든 후에, 부모와 자식 프로세스 모두는 정상적으로 실행을 계속한다. 만일 당신이 당신의 프로그램에서 실행을 계속하기 전에 자식 프로세스가 실행을 끝내도록 기다리기 원한다면, fork 연산을 수행한 후에 wait 나 waitpid(23. 6절 [Process Completion] 참조)를 호출함으로써 명백하게 이일을 하도록 해야만 한다. 그들 함수들은 자식프로세스가 왜 종료되었는지에 대한 제한된 정보_예를 들면, exit 상황코드_를 준다.

새롭게 만들어진 자식 프로세스는 fork가 return을 호출한 지점에서 부모 프로세스와 같은 프로그램을 실행한다. 당신은 그 프로그램이 부모 프로세스 또는 자식 프로세스에서 실행되고 있는지에 대해서 알기 위해서 fork로부터의 반환값을 사용할 수 있다.

여러 개의 프로세스가 같은 프로그램을 실행하기는 때때로 유용하다. 그러나 자식 프로세스는 exec 함수들중의 하나를 사용해서 다른 프로그램을 실행할 수 있다; 23. 5절 [Executing a File] 참조. 프로세스가 실행시키고 있는 프로그램을 프로세스 이미지(process image)라고 부른다. 새로운 프로그램의 실행을 시작하는 것은 프로세스가 그 전의 프로세스 이미지에 대한 모든 것을 잊게 한다; 새로운 프로그램이 종료될 때, 그 전의 프로세스 이미지를 반환하지 않고, 프로그램처럼 종료한다.


23. 3 프로세스 식별

pid_t 데이터 타입은 프로세스 ID들을 나타낸다. 당신은 getpid를 호출함으로써 프로세스의 ID를 얻을 수 있다. getppid 함수는 현재 프로세스의 (이것은 또한 부모 프로세스의 ID로써 알려져 있다. ) 부모 프로세스 ID를 반환한다. 당신의 프로그램에서 그들 함수를 사용하기 위해서는 `unistd. h'와 `sys/types. h'의 헤더파일을 포함해야한다.

데이터 타입 : pid__t

pid_t 데이터 타입은 프로세스 ID를 표현하는 부호화 정수 타입이다. GNU 라이브러리에서, 이것은 int 이다.

함수 : pid_t getpid (void)

getpid 함수는 현재 프로세스의 프로세스 ID를 반환한다.

함수 : pid_t getppid (void)

getppid 함수는 현재 프로세스의 부모 프로세스 ID를 반환한다.


23. 4 프로세스 만들기

fork 함수는 프로세스를 만드는 기본동작(primitive)이다. 그것은 헤더파일 `unistd. h'에 선언되어 있다.

함수 : pid_t fork (void)

fork 함수는 새로운 프로세스를 만든다. 만일 그 명령이 성공하면, 그곳에는 부모와 자식 프로세스가 존재하고 그 둘은 fork의 반환값을 서로 다른 값으로 보게된다. 자식 프로세스 안에서는 0의 값을 반환하고 부모 프로세스 안에서는 자식 프로세스의 ID를 반환한다. 만일 프로세스 만들기가 실패하면, fork는 부모 프로세스에게 -1의 값을 반환한다.
 
다음의 errno는 fork를 위해 정의된 에러 상황이다.

EAGAIN

다른 프로세스를 만들만한 충분한 시스템 자원이 없거나, 사용자가 이미 너무 많은 프로세스들을 실행시키고 있다.

ENOMEM : 프로세스는 시스템이 공급할 수 있는 것보다 더 많은 공간을 필요로 한다.

다음은 부모 프로세스와는 다른, 자식 프로세스의 정해진 속성이다.

자식 프로세스는 자신만의 단일한 프로세스 ID를 갖는다.
 
자식 프로세스의 부모 프로세스 ID는 그 자신의 부모 프로세스의 ID이다.
 
자식 프로세스는 부모 프로세스가 개방한 파일 기술자의 자신 소유의 복사본을 가진다. 부모 프로세스 안에서 연속적으로 속성이 변화하는 파일 기술자들은 자식 프로세스의 파일 기술자에게 영향을 미치지 않고, 그리고 자식 프로세스에 속한 파일기술자의 속성이 변한다고 해도 그것또한 부모 프로세스의 기술자에게 영향을 미치지 못한다. 8. 7절 [Control Operations] 참조.
자식 프로세스를 위하여 경과된 프로세서 시간은 0으로 설정된다; 17. 1절 [Processor Time] 참조.
 
자식 프로세스는 부모 프로세스에 의해 설정된 파일 락(lock)들을 상속받지 않는다. 8. 7절 [Control Operations] 참조.
 
자식 프로세스는 부모 프로세스에 의해 설정된 알람(alarm)을 상속받지 않는다. 17. 3절 [Setting an Alarm] 참조.
 
자식 프로세스를 위해서 미처리중인 시그널의 설정(21. 1. 3절 [Delivery of Signal] 참조)은 소거된다. (자식 프로세스는 부모 프로세스로 부터 블록된 시그널의 마스크와 시그널 동작을 상속받는다. )

함수 : pid_t vfork (void)

vfork 함수는 fork와 유사하지만 더 효율적이다; 하지만, 그것을 안전하게 사용하기 위해서 따랴야만 하는 제한이 있다.
 
fork는 호출된 프로세스의 주소공산의 완전한 복제본을 만들고 부모와 자식 프로세스가 독립적으로 실행되도록 허용하지만, vfork는 이 복제본을 만들지 않는다. 대신에, vfork로 만들어진 자식 프로세스는 exec 함수들중의 하나가 호출될 때까지 부모의 주소공간을 분배받는다. 그것은 부모 프로세스가 일시적으로 실행을 멈추게 됨을 의미한다. 당신은 vfork로 만들어진 자식 프로세스가 부모로부터 분배받은 전역 데이터나 심지어 지역 변수들의 갱신을 허용하지 않도록 매우 주의해야만 한다. 게다가, 자식 프로세스는 호출된 vfork 함수로부터 반환할 수 없다(또는 vfork 함수의 밖으로 long jump하는 것). 이것은 부모 프로세스의 제어 정보를 매우 혼란스럽게 만든다. 만일 의심이 된다면, 대신에 fork를 사용하라. 어떤 운영체제는 실제로 vfork를 지원하지 않는다.
 
GNU C 라이브러리는 모든 시스템에서 vfork를 사용하도록 허용하지만, vfork가 유용하지 않다면 실제로는 fork를 실행한다. 만일 당신이 vfork를 사용하기 위해서 적당하게 예방조치를 취한다면, 당신의 프로그램은 시스템에서 대신에 fork를 사용해서라도 여전히 작업할 것이다.


23. 5 파일 실행시키기

이 절은 프로세스 이미지로써 파일을 실행시키는 exec부류의 함수들을 설명한다. 당신은 자식 프로세스가 만들어진 후에 자식 프로세스가 새로운 프로그램을 실행하도록 그들 함수들을 사용할 수 있다. 이 부류의 함수들은 같은 일을 하지만, 인수를 정하는 방법은 차이가 있다. 그들은 헤더파일 `unistd. h'에 선언되어 있다.

함수 : int execv (const char *filename, char *const argv[])

execv 함수는 새로운 프로세스 이미지로써 파일이름 filename을 실행한다. 널-종료 문자열의 배열인 argv 인수는 실행시키려는 프로그램의 메인 함수에 argv 인수로써 제공하기 위한 값으로 사용된다. 이 배열의 마지막 요소는 반드시 널 포인터가 되어야만 한다. 관례대로, 이 배열의 첫 번째 요소는 디렉토리 이름이 없는 프로그램의 파일 이름이다. 어떻게 프로그램이 그들 인수들을 억세스 하는지에 대한 자세한 것은 22. 1절 [Program Arguments] 참조. 새로운 프로세스 이미지를 위한 환경은 현재 프로세스 이미지의 environ변수로부터 획득된다; 환경에 대한 자세한 정보는 22. 2절 [Environment Variables] 참조.

함수 : int execl (const char *filename, const char *arg(), . . . )

이것은 execv와 유사하지만, argv 문자열은 배열이 아니라 개별적으로 정해져있다. 널 포인터가 마지막 인수로 주어져야만 한다.

함수 : int execve (const chat *filename, chat *const argv[], chat *const env[])

이것은 execv와 유사하지만, env 인수를 통해 새로운 프로그램을 위한 환경을 명시적으로 지정하도록 허용한다. 이것은 environ변수와 같은 형식으로써, 문자열의 배열이다; 22. 2. 1 [Environment Access] 참조.

함수 : int execle (const char *filename, const char *arg(), char *const env[], . . . )

이것은 execl과 유사하지만, 명시적으로 새로운 프로그램을 위한 환경을 정하도록 허용한다. 환경 인수는 마지막 argv 인수로써 표시된 널 포인터의 뒤에 따르고, environ 변수와 같은 형식으로 문자열의 배열이 된다.

함수 : int execvp (const char *filename, char *const argv[])

execvp 함수는 filename에 슬래쉬가 포함되어 있지 않으면 filename으로부터 완전한 파일 이름을 찾기 위해서, PATH 환경변수에 있는 디렉토리를 탐색한다는 점을 제외하고는, execv 와 유사하다 (22. 2. 2 [Standard Environment] 참조. ). 이 함수는 사용자가 선택한 곳에서 그들을 찾기 때문에, 시스템유틸리티 프로그램들을 실행하기에는 유용하다. 쉘들은 사용자가 타이핑한 커맨드를 실행하기 위해서 그것을 사용한다.

함수 : int execlp (const char *filename, const char *arg(), . . . )

이 함수는 execvp 함수처럼 filename과 같은 이름을 탐색한다는 점을 제외하고는, execl 함수와 같다. 서로 받아들인 인수 리스트와 환경 리스트의 크기는 ARG_MAX 바이트 보다 크지 않아야만 한다. 27. 1절 [General Limits] 참조. GNU시스템에서 크기(ARG_MAX와 비교하여)는 각 문자열에서, 문자열 안에 있는 문자들의 개수에 char *의 크기를 더하고, char *의 크기의 배수로 반올림된 하나를 더한다. 다른 시스템들은 크기를 셈하기 위한 다른 규칙을 갖는다.

그들 함수들은 일반적으로 반환하지 않는다. 실패가 발생하면 -1의 값을 반환한다. 보통의 파일 이름 구문 에러(6. 2. 3절 [File Name Errors] 참조. )들에 더해서, 다음의 errno는 이 그들 함수들을 위해서 정의된 에러상황이다.

    E2BIG

    새로운 프로그램의 인수 리스트와 환경 리스트의 결합된 크기가 ARG_MAX 바이트보다 크다. GNU 시스템은 인수 리스트의 크기에 아무런 제한을 두지 않기 때문에 이 에러가 발생하지는 않지만, 그 인수가 유용한 메모리의 크기보다 크다면 ENOMEN의 에러는 발생할 것이다.

    ENOEXEC : 정해진 파일이 올바른 형식이 아니기 때문에 실행될 수 없다.

    ENOMEM : 정해진 파일을 실행시키는데는 현재 유용한 것보다 더 많은 공간이 필요하다.

만일 새로운 파일의 실행이 성공하면, 그것은 마치 그 파일을 읽은 것 그 파일의 억세스 타임 필드(access time field)를 갱신한다. 9. 8. 9절[File Times] 를 참조하여, 파일의 억세스 타임에 대한 자세한 정보를 얻어라. 그 파일이 폐쇄된 지점에서 다시 어떤 것도 정해지지 않았다면, 그것은 프로세스가 종료되기전에나 다른 프로세스 이미지가 실행되기 전인 어떤 지점이다.

새로운 프로세스 이미지를 실행하는 것은 새로운 위치로 인수와 환경 문자열을 복사하고, 메모리의 내용을 완전히 바꾸는 것이다. 그러나 프로세스의 많은 다른 속성들은 변경되지 않는다.

만일 프로세스 이미지 파일의 set-user-ID 와 set-group-ID 모드 비트가 설정되면, 이것은 프로세스의 유효 사용자 ID와 유효 그룹 ID에게 영향을 미친다. 그러한 개념들은 25. 2절 [Process Persona] 에 상세하게 설명되어 있다.

현존하는 프로세스 이미지에서 무시되도록 설정된 시그널들은 새로운 프로세스 이미지에서도 또한 무시되도록 설정된다. 모든 다른 시그널들은 새로운 프로세스 이미지에서 디폴트 동작으로 설정된다. 시그널에 대한 더 많은 정보는, 21장 [Signal Handling] 참조하라.

실행중인 프로세스 이미지에서 개방한 파일 기술자들은, 그들이 FD_CLOEXEC (close-on-exec) 플래그를 설정하지 않는다면, 새로운 프로세스 이미지에서도 개방된 채로 남겨진다. 개방된 상태로 남겨진 파일들은 실행중인 프로세스 이미지로부터 파일 락들을 포함한, 개방 파일 기술의 모든 속성을 상속받는다. 파일 기술자는 8장 [Low-Level I/O] 에 상세하게 설명되어 있다.

파일과 비교하여 스트림은, exec 함수들을 통해서도 살아남지 못한다, 왜냐하면 그들은 프로세스 자체의 메모리 안에 위치하고 있기 때문이다. 새로운 프로세스 이미지는 그들이 새롭게 만든 스트림을 제외하고는 아무런 스트림도 갖지 않는다. pre-exec프로세스 이미지 안에 있는 스트림들의 각각은 그 내부에 기술자를 갖고있고, 그들 기술자는 exec을 통해서 살아남는다. (그들은 FD_CLOEXEC의 설정을 가지지 않고 제공된 것. 새로운 프로세스 이미지는 fdopen을 사용해서 새로운 스트림에 그들을 재연결 할 수 있다. (8. 4절 [Descriptors and Streams] 참조. )


23. 6 프로세스 종료

이 절에서 설명하고 있는 함수들은 자식 프로세스가 종료하거나 멈추도록 기다리는데 사용되고, 그러한 상황인지의 여부를 알아보는데 사용된다. 그들 함수들은 헤더파일 `sys/wait. h'에 선언되어 있다.

함수 : pid_t waitpid (pid_t pid, int *status_ptr, int options)

waitpid 함수는 프로세스 ID를 pid로 가진 자식 프로세스로 부터 상황 정보를 요청하는데 사용된다. 일반적으로, 호출된 프로세스는 자식 프로세스가 종료됨으로써 유용한 상황정보를 만들 때까지 잠시 중지되어 있다.

pid 인수를 위한 다른 값들은 특별한 뜻을 갖는다. -1의 값이나 WAIT_ANY는 어떤 자식 프로세스를 위한 상황정보를 요청한다; 0의 값이나 WAIT_MYPGRP는 호출된 프로세스와 같은 프로세스 그룹에 있는 어떤 자식 프로세스를 위한 정보를 요청한다; 그리고 다른 음수값 - pgid는 프로세스 그룹 ID로써 pgid를 가진 자식 프로세스를 위한 정보를 요청한다.

만일 자식 프로세스를 위한 상황정보가 즉시 유용한 상태라면, 이 함수는 기다림이 없이 즉시 반환한다. 만일 한 개의 적합한 자식 프로세스보다 많은 프로세스가 유용한 상황 정보를 갖고있다면, 그둘중의 하나가 임의로 선택되고, 그 상황은 즉시 반환된다. 다른 적합한 자식 프로세스로 부터 상황을 얻기 위하여, 당신은 waitpid를 다시 호출할 필요가 있다.

options 인수는 비트마스크이다. 그 값은 0이나 WNOHANG와 WUNTRACED 플래그들이 비트별 OR로 조합되어질 것이다. 당신은 부모 프로세스가 기다리지 않을 것임을 지적하기 위해서 WNOHANG 플래그를 사용할 수 있다; 그리고 WUNTRACED플래그는 종료된 프로세스 뿐만 아니라 멈추어진 프로세스들로 부터 상황 정보를 요청하기 위해서 사용되어진다.

자식 프로세스로 부터의 상황정보는 status_ptr이 널 포인터가 아니라면, status_ptr이 가리키고 있는 오브젝트 안에 저장된다. 반환값은 보통 보고된 상황을 가진 자식 프로세스의 프로세스 ID 가 된다.

만일 WNOHANG 옵션이 지정됐고 어떤 자식 프로세스도 기다리고 있지 않다면, 그 값은 0이된다. 에러가 발생한 경우에 -1을 반환한다. 다음의 errno는 이 함수를 위해서 정의된 에러상황이다.

    EINTR

    그 함수는 호출된 프로세스에게 배달된 시그널에 의해 인터럽트 되어졌다. 21. 5절 [Interrupted Primitives] 참조.

    ECHILD

    그곳에 기다리고 있는 자식 프로세스가 아무 것도 없거나, 정해진 pid가 호출된 프로세스의 자식프로세스가 아니다.

    EINVAL : 올바르지 못한 값이 options 인수로써 공급되었다.

다음 기호 상수들은 waitpid 함수에게 pid 인수를 위한 값으로써 정의되었다.

    WAIT_ANY

    이 상수 매크로는 waitpid가 어느 자식 프로세스에 대한 상황정보를 반환하도록 지정되었다. (값은 -1)

    WAIT_MYPGRP

    이 상수는(값은 0) 호출된 프로세스와 같은 프로세스 그룹 안에 있는 어떤 자식 프로세스에 대한 상황정보를 반환하도록 지정되었다.

다음의 기호 상수들은 waitpid 함수의 options 인수를 위한 플래그로써 정의되었다. 당신은 그들은 비트별-OR 연산을 통해서 조합할 수 있다.

    WNOHANG

    이 플래그는 만일그곳에 준비된 자식 프로세스가 없다면, waitpid가 기다리지 않고 즉시 반환하도록 지정되었다.

    WUNTRACED

    이 플래그는 종료된 것뿐만 아니라 멈추어진 자식 프로세스에 대한 상황을 보고하도록 지정되었다.

함수 : pid_t wait (int *status_ptr)

이 함수는 waitpid의 간소화된 변형이고, 어떤 한 개의 자식 프로세스가 종료될 때까지 기다리는데 사용된다. 호출은:
wait (&status)
는 정확히 다음과 동등하다:
waitpid (-1, &status, 0)

다음은 기다림이 없이, 종료된 모든 자식 프로세스에서 보내온 상황을 얻기 위해서는 waitpid를 어떻게 사용하는지에 대한 예제가 있다. 이 함수는 SIGCHLD 시그널을 위한 핸들러로써 만들어졌는데, 그 시그널은 적어도 한 개의 자식 프로세스가 종료되었음을 알리기위해서 발생 된다.

void
sigchld_handler (int signum)
{
int pid;
int status;
while (1)
{
pid = waitpid (WAIT_ANY, &status, WNOHANG);
if (pid < 0)
{
perror ("waitpid");
break;
}
if (pid == 0)
break;
notice_termination (pid, status);
}
}


23. 7 프로세스 종료 상황들

만일 자식 프로세스의 종료 상황 값(22. 3절 [Program Termination] 참조. )이 0이면, waitpid 또는 wait에 의해 보고된 상황 값 또한 0이다. 당신은 다음의 매크로를 사용해서 반환된 상황값안에 있는 암호화된 정보를 테스트할 수 있다.

매크로 : int WIFEXITED (int status)

이 매크로는 만일 자식 프로세스가 exit 또는 _exit로써 정상적으로 종료되면 0이 아닌 값을 지닌다.

매크로 : int WEXITSTATUS (int status)

만일 status가 참인 상태의 WIFEXITED이면, 이 매크로는 자식프로세스로부터의 종료 상황 값의 하위 8비트를 반환한다. 22. 3. 2절 [Exit Status] 참조.

매크로 : int WIFSIGNALED (int status)

이 매크로는 만일 자식 프로세스가 처리되지 않은 시그널을 받았기 때문에 종료되었다면 0이 아닌 값을 반환한다. 21장 [Signal Handling] 참조.

매크로 : int WTERMSIG (int status)

만일 status가 참인 WIFSEGNALED이면, 이 매크로는 자식 프로세스를 종료시켰던 시그널의 시그널 번호를 반환한다.

매크로 : int WCOREDUMP (int status)

이 매크로는 자식 프로세스가 종료되고 코어를 생성했다면, 0이 아닌 값을 반환한다.

매크로 : int WIFSTOPPED (int status)

이 매크로는 만일 자식 프로세스가 멈추어져있다면 0이 아닌 값을 반환한다.

매크로 : int WSTOPSIG (int status)

만일 status가 참인 WIFSTOPPED 이면, 이 매크로는 자식 프로세스를 멈추게 한 원인이 된 시그널의 시그널 번호를 반환한다.


23. 8 BSD 프로세스 Wait 함수들

GNU 라이브러리는 BSD 유닉스와의 호환성을 위해서 그들과 연관된 기능들을 제공한다. BSD는 int 와 다르게 상황 값을 표현하는 union wait 데이터 타입을 사용한다. 두 개의 표현은 실제로 상호간에 변경 가능하다; 그들은 동일한 비트 패턴을 표현하기 때문이다. WEXITSTATUS 와 같은 매크로를 정의하고 있는 GNU C 라이브러리는 오브젝트의 둘중 한 종류를 선택해서 작업할 것이고, wait 함수는 status_ptr 인수로써 포인터의 한 종류를 받아들이도록 정의된다. 그들 함수들은 `sys/wait. h'에 선언되어 있다.

데이터 타입 : union wait

이 데이터 타입은 프로그램 종료 상황 값들을 표현하고, 다음과 같은 멤버들을 갖는다.

int w_termsig

이 멤버의 값은 매크로 WTERMSIG의 결과와 같다.

int w_coredump

이 맴버의 값은 매크로 WCOREDUMP의 결과와 같다.

int w_retcode

이 맴버의 값은 매크로 WEXITSTATUS의 결과와 같다.

int w_stopsig

이 맴버의 값은 매크로 WSTOPSIG의 결과와 같다.

직접적으로 이 맴버들을 억세스 하는 대신에, 당신은 동등한 매크로 를 사용하도록 하라.

함수: pid_t wait3 (union wait *status_ptr, int options, struct rusage *usage)

usage가 널 포인터라면, wait3는 waitpid(-1, status`ptr, options)와 같다. 만일 usage가 널 포인터가 아니라면, wait3는 *rusage에 있는 자식 프로세스를 위한 사용 형태를 저장한다(그러나 오직 멈추어 있는 것이 아니라, 종료된 자식 프로세스라면. )17. 5절 [Resource Usage] 참조.

함수 : pid_t wait4 (pid_t pid, union wait *status_ptr, int options, struct rusage *usage)

만일 usage가 널 포인터라면, wait4는 waitpid (pid, status`ptr, options)와 같다. 만일 usage가 널 포인터가 아니라면, wait4는 *rusage에 있는 자식 프로세스를 위하여 사용 형태들을 저장한다. (그러나 오직 자식 프로세스는 멈추어 있는 것이 아니라, 종료된 것이다. ) 17. 5절 [Resource Usage] 참조.


23. 9 프로세스 만들기 예제

다음은 내장 시스템과 유사한 함수를 어떻게 만들 것인지를 보여주는 예제 프로그램이다. 그것은 `sh -c command'와 동등한 것을 사용하여 command 인수를 실행한다.

#include <stddef. h>
#include <stdlib. h>
#include <unistd. h>
#include <sys/types. h>
#include <sys/wait. h>
 
/* 쉘 프로그램을 사용해서 command 를 실행한다. */
#define SHELL "/bin/sh"
 
int
my_system (const char *command)
{
int status;
pid_t pid;
 
if ((pid = fork()) == 0) { /* 이것은 자식 프로세스로써 쉘 커맨드를 실행한다. */
execl (SHELL, SHELL, "-c", command, NULL);
_exit (EXIT_FAILURE);
} else if (pid < 0) { /* fork가 실패하였으므로 실패를 보고하라. */
status = -1;
} else { /* 이것은 부모 프로세스로써, 자식이 수행을 완료할 때까지 기다린다. */
if (waitpid (pid, &status, 0) != pid)
status = -1;
}
return status;
}

당신이 이 예제에서 주목해야 할 것이 두 가지 있다. 프로그램에 공급된 첫 번째 argv 인수는 실행시키려는 프로그램의 이름을 표현한다는 것을 기억하라. 그것은 execl의 호출에서, SHELL에게 일단 실행하려는 프로그램의 이름이 주어지고 두 번째로 argv[0]을 위한 값을 공급하는 이유이다.

자식 프로세스에서 execl 호출은 만일 그것이 성공하면 반환하지 않는다. 만일 그것이 실패하면, 당신은 자식 프로세스가 종료되도록 무엇인가를 해야만 한다. 단지 return을 통하여 나쁜 상황 코드만을 반환하는것은 원래의 프로그램을 실행시키던 두개의 프로세스를 그냥 남기게 된다. 대신에, 올바른 처리는 부모 프로세스에게 자식 프로세스가 실패를 보고하는 것이다.

그것을 수행하도록 -exit를 호출하라. exit 대신에 _exit를 사용하는 이유는 stdout와 같은 완전히 버퍼화된 스트림들을 플러쉬 하는 것을 피하기 위함이다. 아마도 데이터를 담고 있는 그들 스트림의 버퍼들은 fork에 의해 부모 프로세스로 부터 복사된 것이고, 데이터는 부모 프로세스에 의해 결국 출력될 것이다. 자식 프로세스에서 호출된 exit는 데이터를 두 번 출력할 것이다. 22. 3. 5절 [Termination Internals] 참조.


목차 이전 : 22. 프로세스의 시동과 종료 다음 : 24. 작업 제어

'Programming > Unix Programing' 카테고리의 다른 글

25. 사용자와 그룹  (0) 2007.12.22
24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
21. 신호처리(Signal)  (0) 2007.12.22
19. 지역과 세계화  (0) 2007.12.22
Comment 0 Trackback 0
Top

22. 프로세스의 시동과 종료(Process StartUp)

목차 이전 : 21. 시그널 처리 다음 : 23. 프로세스


22 프로세스의 시동과 종료

프로세스는 시스템 자원들의 할당을 위한 기본적인 동작 단위이다. 각 프로세스는 자신만의 주소 공간(address space)과 제어의 한 쓰레드를 갖는다. 한 프로세스는 한 프로그램을 실행한다; 당신이 같은 프로그램을 실행하는데 여러개의 프로세스를 가질 수 있지만, 각각의 프로세스는 자신의 주소공간에서 자신의 프로그램 복제본을 갖고 다른 프로그램 복사본과 독립적으로 실행된다.

 
** 역자주 : thread(쓰레드) : 프로세스 보다 더 작은 하나의 작업단위.

일반적으로 우리는 하나의 프로세스를 단위로 하여 독립적인 작업을 수행하는 것으로 알고 있지만, 컴퓨터 기술의 발전으로 프로세스라는 단위는 너무나 큰 단위가 되어버리고 이를 대신하여 쓰레드라는 단위가 사용되게 되었다. 쓰레드를 사용하는 운영체제에서는 작업의 스케줄링을 수행할 때 쓰레드를 단위로 하여 스케줄링을 한다.

이 장은 당신의 프로그램에서 프로세스를 시동하고, 그것을 종료시키고, 부모 프로세스로 부터 정보(인수들과 환경)를 받기 위하여 무엇을 해야만 하는지를 설명하고 있다.


22. 1 프로그램 인수들

시스템은 main 함수를 호출함으로써 C 프로그램을 시작한다. 그것은 당신이 프로그램을 만들 때 main 이라 이름지어진 함수를 꼭 만들어야 함을 의미하고_그것을 하지 않았을 때 에러없이 프로그램을 링크할 수 없을 것이다.

main 함수는 아무런 인수를 취하지 않거나, 커멘드 라인에서 받아들이는 인수를 나타내는 두 개의 인수를 취한다. 다음처럼.

    int main (int argc, char *argv[])

커멘드 라인 인수들은 공백으로 분리된 토큰으로써 프로그램을 호출하도록 쉘 커맨드에 주어진다; 그래서, `cat foo bar'라고 했을 때 인수는 `foo' 와 `bar'가 된다. 어떤 프로그램에서 커맨드 라인에 인수를 넣는 방법은 오직 main함수의 인수를 통해서 가능하다. 만일 main 함수가 인수를 취하지 않는다면, 당신은 커맨드 라인에서 인수를 취할 수 없다.

argc 인수는 커맨드 라인 인수들의 개수를 말하고, argv 인수는 C 문자열의 벡터이다; argv의 각 요소들은 공백으로 분리된 개별적인 커맨드 라인 인수 문자열이다. 실행시키려는 프로그램의 파일이름 또한 첫 번째 요소로써 벡터 안에 포함된다. argc의 값은 이 요소들의 개수이다. 널 포인터가 항상 마지막 요소가 된다: 즉, argv[argc]는 널 포인터이다. 커맨드 `cat foo bar' 에서, argc는 3이고, argv는 "cat", "foo", 그리고 "bar" 라는 세 개의 요소들을 갖는다.

만일 당신의 프로그램에서 커맨드 라인 인수들의 구분이 간단하다면, 당신은 직접 argv로부터 간단하게 하나씩 인수를 가져올 수 있다. 당신의 프로그램이 한정된 인수들의 개수를 갖지않거나, 모든 인수들을 한꺼번에 취하지 않는다면 같은 방법으로 인수들을 취할 수 있지만, 그렇지 않다면 그것을 구문분석 하기 위해서 getopt를 사용해서 인수들을 취해야 할 것이다.

 

22. 1. 1 프로그램 인수 구문 관례들

POSIX에서 커맨드 라인 인수들을 위해서 다음과 같은 관례들을 권장하고 있다. getopt (22. 1. 2 [Parsing Options] 참조)는 다음의 권장사항을 쉽게 충족시키도록 만든다.

  • 하이픈(hyphen (`_') ) 구획문자(delimiter)로 시작하는 인수들은 옵션이 있다.
  • 한 개의 토큰에서 한 개의 하이픈 구획문자 뒤에 따르는 것은, 만일 그 옵션들이 인수들을 취하지 않는다면 다중 옵션이 된다. 그래서, `-abc'는 `-a -b -c'와 동등하다.
  • 옵션 이름들은 단일한 알파벳 문자들이다( isalnum에 관하여; 4. 1절[Classification of Characters] 참조)
  • 어떤 옵션들을 인수를 필요로 한다. 예를 들어, ld 명령의 `-o' 옵션은 출력 파일 이름으로 하나의 인수를 필요로 한다.
  • 옵션과 옵션이 필요로 하는 인수들은 토큰으로 분리되어 나타날 수도 있고, 그렇지 않을 수도 있다. (즉, 그들을 분리하는 공백문자는 선택적이다. ) 그래서, `-o foo' 와 `-ofoo'는 동등한다.
  • 옵션들은 전형적으로 다른 비-옵션 인수들에 선행된다.

GNU C 라이브러리에 있는 getopt의 구현은 당신의 프로그램을 사용하는 사용자가 심지어 옵션과 비-옵션 인수들을 혼합했을지라도, 구문분석을 위해서 모든 비-옵션 인수들 전에 옵션 인수들이 나타나도록 만든다.

인수 `--'는 모든 옵션들을 종결시킨다; 다음에 따르는 인수들은 그들이 하이픈으로 시작할지라도, 비-옵션 인수들로 취급된다.

한 개의 하이픈 문자가 들어가 있는 토큰은 보통 비-옵션 인수로써 취급된다. 관례상, 그것은 기본 입력과 출력 스트림으로 입력이나 출력을 정하기 위해서 사용된다.

옵션들은 어떤 순서대로 공급되어지고, 또는 여러번 중복해서 나타나기도 할 것이다. 해석은 특정한 응용 프로그램이 맨 왼쪽에 있다고 해석한다.

GNU 는 그들의 관례에 따라서 긴 옵션을 더한다. 긴 옵션들은 `--'뒤에 따르는 이름으로 구성되는데 그 이름은 영숫자와 대쉬들로 구성된다. 옵션 이름들은 전형적으로 하나에서 세 개 단어의 길이를 갖고, 그들은 하이픈으로 단어들을 분리한다. 사용자들은 옵션을 대표하는 약자가 단일하다면 얼마든지 옵션이름들을 축약할 수 있다. 한 개의 긴 옵션을 위한 인수를 정하기 위해서는, `--name=value'라고 써라. 이 구문은 선택적인 인수를 받아들이는 긴 옵션을 가능하게 한다. GNU 시스템은 쉘에서 긴 옵션 이름을 지원할 것이다.

 

22. 1. 2 프로그램 옵션들을 구문 분석하기

이번에는 getopt 함수를 어떻게 호출하는지에 대해서 자세히 설명한다. 이 기능을 사용하기 위해서, 당신의 프로그램에 헤더파일 `unistd. h'를 포함시켜야만 한다.

변수 : int opterr

만일 이 변수의 값이 0이 아니면, getopt는 만일 알지 못하는 옵션 문자가 들어오거나 인수를 필요로 하는 옵션이 옵션을 갖고있지 않다면 표준 에러 스트림에 에러메세지를 프린트한다. 이것은 디폴트 동작이다. 만일 당신이 이 변수를 0으로 설정하면, getopt는 아무런 메시지도 프린트하지는 않지만, 에러를 지적하기 위해서 여전히 ? 문자를 여전히 반환한다.

변수 : int optopt

getopt가 알려지지 않은 옵션 문자와 만나거나 필요한 인수가 빠져있는 옵션일 때, getopt는 그 옵션 문자를 이 변수에 저장한다. 당신은 자신만의 진단 메시지를 제공하기 이해서 이 변수를 사용할 수 있다.

변수 : int optind

이 변수는 앞으로 분석할 배열 argv의 다음 요소에 대한 인덱스로 getopt에 의해 설정된다. 일단 getopt가 옵션 인수들 모두를 발견했다면, 당신은 나머지 비-옵션 인수들이 어디에서 시작하는지 결정하기 위해서 이 변수를 사용할 수 있다. 이 변수의 초기값은 1이다.

변수 : chat *optarg

이 변수는 인수들을 받아들이는 옵션들을 위해서, 옵션 인수들의 값을 가리키는 포인터로써 getopt에 의해 설정된다.

함수 : int getopt(int argc, char **argv, const char *options)

getopt 함수는 argv와 argc 인수들에 의해 지정된 인수 목록으로부터 다음 옵션 인수를 얻는다. 보통, 그들 값들은 main에 의해 받아들여진 인수들로부터 직접적으로 온다. 옵션 인수는 이 프로그램에서 유용하게 쓰일 옵션 문자들을 지정한 문자열이다. 문자열 안에 있는 옵션 문자는 인수가 필요함을 알리기위해서 콜론(`: ')의 다음에 나타난다.

만일 옵션 인수 문자열이 하이픈(`-')으로 시작한다면, 이것은 특별하게 취급된다. 그들은 옵션 문자가 `\0'인 것처럼 반환되어, 옵션이 없는 인수들을 허용한다. getopt 함수는 다음 커맨드 라인옵션의 옵션 문자를 반환한다. 더 이상 유용한 옵션 인수들이 없을 때, -1을 반환한다. 그곳은 이제 비-옵션 인수들이 있을 것이다; 당신은 이것을 체크하기 위해서 argc 파라미터에 대응하는 외부변수 optind를 비교해야만 한다.

만일 어떤 옵션이 인수를 가진다면, getopt 는 변수들 optarg안에 그것을 저장함으로써 인수를 반환한다. 당신이 덧씌워질지도 모르는 정적 변수가 아닌, 보통의 argv 배열을 가리키는 포인터를 라면, optarg 문자열을 복사할 필요가 없다.

만일 알려지지 않은 옵션문자를 사용하거나, 옵션 인수가 빠져있다면 getopt는 `?'를 반환하고 현재 옵션 문자로 외부변수 optopt를 설정한다. 만일 옵션의 첫 번째 문자가 콜론(`: ') 이라면, getopt는 옵션 인수가 빠져있음을 알리기위해서 `?' 대신에 `: '를 반환한다. 또한, 만일 외부변수 opterr이 0이 아니면(디폴트이다. ), getopt는 에러 메시지를 출력한다.

 

22. 1. 3 getopt를 사용해서 인수를 구문 분석하는 예제

다음은 getopt가 전형적으로 어떻게 사용되는지를 보여주는 예제이다. 핵심은 다음과 같다:

보통, getopt는 루프 안에서 호출된다. getopt가 더 이상 나타난 옵션이 없음을 알리기위해서 -1을 반환 하면, 루프는 종료한다.

switch 구문은 getopt로부터의 반환값에 따른 각각의 처리에 사용된다. 각각의 경우는 프로그램에서 나중에 사용될 변수를 설정하게 된다.

두 번째 루프는 남아있는 비-옵션 인수들을 처리하는데 사용된다.

#include <unistd. h>
#include <stdio. h>
 
int
main (int argc, char **argv)
{
int aflag = 0;
int bflag = 0;
char *cvalue = NULL;
int index;
int c;
 
opterr = 0;
 
while ((c = getopt (argc, argv, "abc: ")) != -1) {
switch (c)
{
case 'a':
aflag = 1;
break;
case 'b':
bflag = 1;
break;
case 'c':
cvalue = optarg;
break;
case '?':
if (isprint (optopt))
fprintf(stderr, "Unknown option `-%c'. \n", optopt);
else
fprintf (stderr,
"Unknown option character `\\x%x'. \n", optopt);
return 1;
default:
abort ();
}
}
printf("aflag = %d, bflag = %d, cvalue = %s\n", aflag, bflag, cvalue);
 
for (index = optind; index < argc; index++)
printf ("Non-option argument %s\n", argv[index]);
return 0;
}
 
다음은 위의 프로그램을 여러 가지 인수들의 조합을 사용했을 때 어떤 결과를 나타내는지에 대한 예이다.
 
% testopt
aflag = 0, bflag = 0, cvalue = (null)
 
% testopt -a -b
aflag = 1, bflag = 1, cvalue = (null)
 
% testopt -ab
aflag = 1, bflag = 1, cvalue = (null)
 
% testopt -c foo
aflag = 0, bflag = 0, cvalue = foo
 
% testopt -cfoo
aflag = 0, bflag = 0, cvalue = foo
 
% testopt arg1
aflag = 0, bflag = 0, cvalue = (null)
Non-option argument arg1
 
% testopt -a arg1
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument arg1
 
% testopt -c foo arg1
aflag = 0, bflag = 0, cvalue = foo
Non-option argument arg1
 
% testopt -a -- -b
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument -b
 
% testopt -a -
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument -

 

22. 1. 4 긴 옵션들을 구문 분석하기

단일한-문자 옵션들로만 되어있는 GNU-스타일의 긴 옵션들을 받아들이기 위해서는 getopt 대신에 getopt_long를 사용한다. 당신은 이런 가외의 작은 작업을 통해서 모든 프로그램이 긴 옵션들을 받아들이게 만들 수 있고, 초보자들에게 어떻게 그 프로그램을 사용하는지 도울 수 있다.

데이터 타입 : struct option

이 구조체는 getopt_long을 위한 단일한 긴 옵션 이름을 설명한다. 긴 옵션을 구성하는 옵션의 하나인 인수 longopts는 그들 구조체의 배열이 되어야만 한다. 그 배열은 0으로 채워진 요소로써 끝난다. struct option 구조체는 다음과 같은 필드를 갖는다.

const char *name

이 필드는 옵션의 이름으로 문자열이다.

int has_arg

이 필드는 옵션이 인수를 취하는지 또는 취하지 않는지의 여부를 알린다. 그것은 정수이고 그곳에 세 개의 합리적인 값들이 있다: no_argument, required argument, 그리고 optional_argument.

int *flag

int val

이 필드들은 그 옵션이 발생할 때 옵션에 대하여 어떻게 보고를 하거나 동작할 것인지를 제어한다. 만일 flag가 널 포인터라면, val은 이 옵션을 대표하는 값이다. 종종 그 값들은 특정한 긴 옵션들의 단일함을 확인하기 위해서 선택되어진다. 만일 플래그가 널 포인터가 아니라면, 그것은 이 옵션을 위한 플래그인 int형 변수의 주소가 되어질 것이다. val안의 값은 나타난 옵션을 지적하도록 플래그 안에 저장하기위한 값이다.

함수 : int getopt__long (int argc, char **argv, const char *shortopts, struct option *longopts, int *indexptr)

벡터 argv ( 길이는 argc이다. )로부터 옵션들을 해독한다. 인수 shortopts는 getopt가 해독할 수 있는 만큼의 짧은 옵션을 나타내고, 인수 longopts는 받아들이기 위한 긴 옵션들을 나타낸다. getopt_long이 짧은 옵션을 만났을 때, 그것은 getopt가 하는것과 같은 일을 한다: 그것은 옵션을 위한 문자 코드를 반환하고, optarg에 옵션 인수들을(만일 그것이한 개를 가진다면) 저장한다.
getopt_long가 긴 옵션을 만났을 때, 그것은 그 옵션을 정의하는 flag 와 val 필드에 기초한 동작들을 취한다.
 
만일 flag가 널 포인터라면, getopt_long 은 발견한 옵션을 지적하도록 val의 내용을 반환한다. 당신이 다른 의미를 가진 옵션을 위해서 val 필드 안에 별개의 값들을 배열하면, 당신은 getopt_long이 반환한 후에 그들 값을 해독할 수 있다. 만일 긴 옵션이 짧은 옵션과 동일하다면, 당신은 짭은 옵션의 문자 코드를 사용할 수 있다.
 
만일 flag가 널 포인터가 아니라면, 단지 프로그램 안에서 flag가 설정된 이 옵션을 의미한다. flag는 당신이 정의한 int형의 변수이다. flag 필드 안에 그 플래그의 주소를 저장하라. flag에 저장하려는 옵션값을 val 필드 안에 저장하라. 이 경우, getopt_long는0을 반환한다.
 
어떤 긴 옵션에서, getopt_long는 배열 longopts안의 옵션 정의에 대한 인덱스를, *indexptr에 저장하여 알린다. 당신은 longopts[*indexptr]. name 어로 옵션의 이름을 얻을 수 있다. 그래서 당신은 그들의 val 필드 안에 있는 값이나, 그들의 인덱스에 의해서 긴 옵션들을 구분할 수 있다. 당신은 플래그를 설정한 긴 옵션들을 이 방법으로 구분할 수 있다.
 
어떤 긴 옵션이 인수를 가질 때, getopt_long는 반환하기 전에 변수 optarg안에 인수값을 저장한다. 어떤 옵션이 아무런 인수를 가지지 않을 때, optarg안의 값은 널 포인터이다. 이것은 공급된 인수가 옵션을 가졌는지 아닌지에 대한 여부를 당신에게 알리는 방법인 것이다. getopt_long는 더 이상 처리할 옵션들이 없을 때, -1을 반환하고, 다음 남아있는 인수의 argv 인덱스를 변수 optind 안에 남긴다.

 

22. 1. 5 긴 옵션들의 구분분석에 대한 예제

#include <stdio. h>
 
/* 플래그는 `--verbose' 에 의해 설정한다. */
static int verbose_flag;
 
int
main (argc, argv)
int argc, char **argv;
{
int c;
 
while (1) {
static struct option long_options[] = {
/* 그들 옵션들은 플래그를 설정한다. */
{"verbose", 0, &verbose_flag, 1},
{"brief", 0, &verbose_flag, 0},
/* 그들 옵션들은 플래그를 설정하지 않는다. 우리는 그들의 인덱스에 의해 그들을 구분한다. */
{"add", 1, 0, 0},
{"append", 0, 0, 0},
{"delete", 1, 0, 0},
{"create", 0, 0, 0},
{"file", 1, 0, 0},
{0, 0, 0, 0}
};
 
/* getopt_long는 이곳에 옵션 인덱스를 저장한다. */
int option_index = 0;
 
c = getopt_long(argc, argv, "abc: d: ", long_options, &option_index);
 
/* 옵션들의 끝을 검출한다. */
if (c == -1)
break;
 
switch (c) {
case 0:
/* 만일 이 옵션이 플래그를 설정하면, 지금 아무런 일도 하지 말아라. */
if (long_options[option_index]. flag != 0)
break;
printf("option %s", long_options[option_index]. name);
if (optarg)
printf(" with arg %s", optarg);
printf("\n");
break;
case 'a':
puts("option -a\n");
break;
case 'b':
puts("option -b\n");
break;
case 'c':
printf("option -c with value `%s'\n", optarg);
break;
case 'd':
printf("option -d with value `%s'\n", optarg);
break;
case '?':
/* getopt_long는 이미 에러 메시지를 프린트했다. */
break;
default:
abort ();
} /* switch 의 끝*/
} /* while 의 끝 */
 
/* 그들이 맞추었던 것으로 `--verbose'와 `--brief'를 보고하는 대신에, 우리는 그들로부터 나온 결과로 마지막 상황을 보고한다. */
if (verbose_flag)
puts ("verbose flag is set");
 
/* 남아있는 코맨드 라인 인수들을( 옵션이 없는) 출력하라. */
if (optind < argc) {
printf ("non-option ARGV-elements: ");
while (optind < argc)
printf ("%s ", argv[optind++]);
putchar ('\n');
}
 
exit (0);
}


22. 2 환경 변수들

프로그램이 실행될 때, 두 가지 방법으로, 실행될 문맥(context)에 대한 정보를 받는다. 첫 번째 방법은 argv와 argc 인수를 메인 함수에서 사용하는 것으로써 이것에 대한 것은 22. 1절 [Program Arguments] 에서 설명하였다. 두 번째 방법은 환경변수를 사용하는 것인데 지금부터 설명할 것이다. argv를 사용하는 방법은 실행될 특정한 프로그램에 지정된 커맨드 라인 인수를 주기 위해서 전형적으로 사용된다. 다른 방법으로, 환경은, 드물게 변경되고 덜 빈번하게 억세스 되는, 많은 프로그램에 의해 분배될 정보를 기억한다.

이 절에서 논의되고 있는 환경변수들은 당신이 쉘에게 커맨드를 보내고 사용될 것을 배정하는 환경변수와 같은 것이다. 쉘로부터 실행된 프로그램들은 쉘로부터 환경변수 모두를 상속받는다. 표준 환경 변수들은 사용자의 홈 디렉토리, 터미널 타입, 현재 지역 등등에 대한 정보를 위해서 사용된다; 당신은 다른 목적으로 부가적인 환경변수를 정의할 수 있다. 모든 환경변수들의 가진 값들의 집합은 집합적으로 환경으로써 알려진다.

환경변수들의 이름은 대소문자를 구분하고 문자 `='를 포함해서는 안된다. 시스템-정의 환경변수들은 항상 대문자이다. 환경변수들의 값은 문자열로써 표현될 수 있는 어떤 것도 가능하다. 값에는 널 문자가 포함 되서는 안된다, 왜냐하면 이것은 문자열의 끝으로 간주되기 때문이다.

 

22. 2. 1 환경 검색

환경변수의 값은 getenv 함수를 통해서 억세스될 수 있다. 이것은 헤더 파일 `stdlib. h'에 선언되어 있다.

함수 : char *getenv (const char name)

이 함수는 환경변수 name의 값인 문자열을 반환한다. 당신은 이 문자열을 갱신할 수 없다. 어떤 GNU 라이브러리를 사용하지 않는 비-유닉스 시스템들은 getenv 함수의 연속적인 호출에 의해서 덮어 쓰여질지 모른다(다른 라이브러리 함수에 의하지 않는다. ) 만일 환경변수 name이 정의되지 않는다면, 그 값은 널 포인터이다.

함수 : int putenv (const char *string)

putenv 함수는 환경으로부터 정의를 더하거나 제한다. 만일 string이 `name=value'의 형식이라면, 그 정의는 환경에 더해진다. 그렇지 않다면, 그 string은 환경변수의 이름으로써 해석되고, 환경 안에서 이 변수에 대한 정의가 제거된다. GNU 라이브러리는 SVID와의 호환성을 위해서 이 함수를 제공하고 있고; 다른 시스템에서 이 함수 는 유용하지 않다.
당신은 환경에 더 많은 변수들을 더하기 위해서 환경 오브젝트의 내재된 표현을 직접적으로 다룰 수 있다( 예로, 당신이 실행하려는 것에 대해서 다른 프로그램으로 통신하기 위해서; 23. 5절 [Executing a File] 참조).

변수 : chat **environ

환경은 문자열의 배열로써 표현된다. 각 문자열은 `name=value'의 형식을 갖는다. 환경 안에 나타난 문자열의 순서는 중요하지 않지만, 같은 이름이 한번 이상 나타나서는 결코 안된다. 배열의 마지막 요소는 널 포인터이다. 이 변수는 헤더파일 `unistd. h'에 선언되어 있다. 만일 당신이 환경변수의 값을 얻기 원한다면 getenv를 사용하라.

 

22. 2. 2 표준 환경 변수들

그들 환경변수들은 표준 의미를 갖는다. 이것은 그들이 항상 환경 안에서만 나타남을 의미하지는 않는다; 그러나 그 변수들이 어디에서든 나타난다고 해도 그들은 그들만의 의미를 갖기 때문에 당신이 다른 목적으로 환경변수 이름을 사용하려고 시도해서는 안된다.

HOME

이것은 사용자의 홈 디렉토리나, 초기 디폴트 작업 디렉토리를 나타내는 문자열이다. 사용자는 어떤 값으로 HOME를 설정할 수 있다. 만일 당신이 어떤 특정한 사용자를 위해서 적당한 홈 디렉토리를 부여하기를 원한다면, HOME을 사용할 수 없고; 대신에, 사용자 데이터 베이스에서 사용자의 이름을 찾아보라 ( 25. 12절 [User Database] 참조). 대부분의 목적에서는, HOME을 사용하는 것이 좋다, 왜냐하면 정확하게 사용자가 그 값을 정하도록 허용하기 때문이다.

LOGNAME

이것은 로그 인 하기 위해서 사용되는 사용자 이름이다. 환경에서 이 값은 제멋대로 조정될 수 있기 때문에, 이것은 프로세스를 실행시키고 있는 사용자를 구분하기 위한 신뢰 가능한 방법이 아니다; getlogin과 같은 함수(25. 11 [Who Logged In] 참조)는 그러한 목적을 위해서는 오히려 더 낫다. 대부분의 목적을 위해서는 LOGNAME이 사용자가 그 값을 지정하도록 하기 때문에 사용하기 더 좋다.

PATH

경로는 파일을 찾기 위해서 사용되는 디렉토리 이름들의 열(sequence) 이다. 변수 PATH는 실행시키려는 프로그램을 찾기 위해서 사용되는 경로는 저장하고 있다. execlp와 execvp 함수들은 (23. 5절 [Executing a File] 참조) 그들 함수들에 의하여 실행된 쉘과 다른 유틸리티에서, 이 환경변수를 사용한다. 경로의 구분은 콜론으로 분리된 디렉토리 이름들의 순차열(sequence) 이다. 디렉토리 이름 대신에 빈 문자열은 현재의 디렉토리를 나타낸다( 9. 1절 [Working Directory] 참조).
 
이 환경변수의 전형적인 값은 다음과 같은 문자열이 될 것이다.
: /bin: /etc: /usr/bin: /usr/new/X11: /usr/new: /usr/local/bin

TERM

이것은 프로그램 출력을 받아들이는 터미널의 종류를 지정한다. 어떤 프로그램들은 터미널의 특정한 종류에 의해 지원되는 특별한 이스케이프 시퀀스 또는 터미널모드들을 이용하기 위해 이 정보를 사용하게 만들 수 있다. termcap 라이브러리( Termcap 라이브러리 매뉴얼에서 "Finding a Terminal Description" 절을 참고)를 사용하는 많은 프로그램들은 TERM 환경변수를 사용한다.

TZ

이것은 시간대(time zone)를 지정한다. 이 문자열의 형식과 어떻게 그것을 사용하는지에 대한 자세한 정보는 17. 2. 5절 [TZ Variable] 참조.

LANG

속성 범주를 사용하도록 디폴트 지역을 지정한다.

LC_ALL

범주를 위한 환경변수가 아니면 설정한다. 지역에 대한 자세한 정보를 19장 [Locales] 참조.

LC_COLLATE

이것은 문자열 정렬을 위해서 어떤 지역을 지정한다.

LC_CTYPE

이것은 문자 집합(sets)과 문자 분류를 위해 사용하도록 어떤 지역을 지정한다.

LC_MONETARY

이것은 형식화된 통화량 값을 사용하기 위한 어떤 지역을 지정한다.

LC_NUMERIC

이것은 형식화된 숫자들을 사용하기 위한 어떤 지역을 지정한다.

LC_TIME

이것은 형식화된 날짜/시간값들을 위해서 어떤 지역을 지정한다.

_POSIX_OPTION_ORDER

만일 이 환경변수가 정의되었다면, 그것은 getopt에 의한 커맨드 라인 인수의 재정리를 금지한다. 22. 1. 1절 [Argument Syntax] 참조)


22. 3 프로그램 종료

프로그램을 종료시키는 평상적인 방법은 메인 함수가 반환하도록 하는 것이다. 메인 함수로부터 반환되는 분기(exit) 상황 값은 프로세스의 부모 프로세스나 쉘에게 정보를 보고하기 위해서 사용된다. 프로그램은 exit 함수를 호출함으로써 또한 종료할 수 있다. 또한, 프로그램은 시그널들에 의해서 종료될 수 있다; 이것은 21장 [Signal Handling] 에 자세하게 설명되어 있다. abort함수는 프로그램을 죽이는(kill) 시그널을 발생시킨다.

 

22. 3. 1 보통의 종료

프로세스는 프로그램이 exit를 호출할 때 보통 종료된다. main 함수로부터의 반환은 exit 를 호출하는것과 같고, main이 반환한 값은 exit에 인수로써 사용된다.

함수 : void exit (int status)

exit 함수는 상황 status로 프로세스를 종료한다. 이 함수는 반환하지 않는다.
보통의 종료는 다음과 같은 동작들의 원인이 된다:
 
1. atexit 나 on_exit 함수들에 등록된 함수들은 등록의 역순으로 호출되어진다. 이 메커니즘은 프로그램이 종료할 때 수행되어지는 자신만의 "cleanup" 동작들을 지정하도록 당신의 응용프로그램에게 허용한다. 전형적으로, 이것은 파일에 프로그램 상황 정보를 저장하거나, 공유 데이터베이스에서 락들을 해제하는것과 같은 일들을 하는데 사용된다.
 
2. 버퍼된 출력 데이터가 모두 기록되어, 개방된 모든 스트림이 폐쇄되었다. 7. 4절 [Closing Streams] 참조. 또한, tmpfile 함수에의해 개방되었던 임시 파일들은 제거된다; 9. 10절 [Temporary Files] 참조.
 
3. 프로그램을 종료시키도록 _exit가 호출되었다. 22. 3. 5절 [Termination Internals] 참조.

 

22. 3. 2 Exit 상황들

프로그램이 종료할 때, 그것은 분기 상황(exit status)을 사용해서, 종료의 원인에 대한 작은 양의 정보를 부모 프로세스에게 반환할 수 있다. 이것은 현존하는 프로세스가 exit에게 인수로써 주는 0에서 255사이의 값이다. 보통 당신은 성공이나 실패에 대한 매우 광범위한 정보를 보고하기 위해서 exit 상황을 사용할 것이다. 당신은 실패의 이유에 대해서 매우 자세하게는 정보를 줄 수는 없고, 대부분의 부모 프로세스도 더 이상 세밀한 정보를 원하지 않을 것이다.

어떤 프로그램이 반환하는 값은 어떤 부류의 상황 값에 속한다. 대부분의 관습은 성공하면 0이고 실패하면 1이다. 그러나 비교를 수행하는 프로그램은 다른 관습을 사용한다: 매치되지 않았음을 지적하기 위해서 1을 사용하고 비교 불가능임을 지적하기 위해서 2를 사용한다. 당신의 프로그램이 그것을 위한 종료관습을 이해하려면 종료관습을 따라야한다.

일반적인 관습은 상황 값으로 128개를 예약하고 특별한 목적을 위해서 확장한다. 특별하게, 128개의 값은 서브프로세스에서 다른 프로그램에게 실패를 지적하기 위해서 사용된다. 이 관습은 보편적으로 제공되지는 않지만, 당신의 프로그램이 그것을 따르도록 하는 것이 좋다.

 
주의 : 분기상황으로써 에러들의 개수를 사용하려고 시도하지 말아라.

이것은 실제로 매우 유용하지 않다; 부모 프로세스는 얼마나 많은 에러들이 발생했는지에 주의를 기울이지 않는다. 그것보다 더 심각한 경우로는, 상황 값이 8비트에서 짤리기 때문에 만일 프로그램에서 256개의 에러를 보고한다면, 부모 프로세스는 0개의 에러가 난 것으로 보고를 받을 것이고_결국 성공한 것으로 되어버린다. 같은 이유로, 255개를 초과할 수 있기 때문에 종료의 상황으로써 에러의 개수를 사용하여 작업하지 말아라.

 
이식성 노트: 어떤 비-POSIX 시스템들은 종료 상황 값들을 위해서 다른 관습을 사용한다. 좀더 좋은 이식성을 위해서는, 성공과 실패를 위한 상황 값으로 매크로 EXIT_SUCCESS 와 EXIT_FAILURE 를 사용할 수 있다. 그들은 헤더파일 `stdlib. h'에 선언되어 있다.

매크로 : int EXIT__SUCCESS

이 매크로는 성공적인 프로그램 수행을 알리기위해서 exit 함수와 함께 사용될 수 있다. POSIX 시스템에서, 이 매크로의 값은 0이고, 다른 시스템에서 이 값은 어떤 다른 정수 표현( 아마도 비-상수 )이 될 것이다.

매크로 : int EXIT__FAILURE

이 매크로는 비성공적인 프로그램 수행을 알리기위해서 exit 함수와 함께 사용될 수 있다. POSIX 시스템에서, 이 매크로의 값은 1이다. 다른 시스템들에서 그 값은 어떤 다른 정수 표현( 아마도 비-상수 )이 될 것이다. 다른 0이 아닌 상황 값들은 앞으로의 일을 알린다. 어떤 프로그램들은 특정한 종류의 "비-성공"을 알리기위해서 다른 0이 아닌 상황 값들을 사용한다. 예를 들어, diff는 파일들이 서로 다름을 의미할 때 상황 값 1을 사용하고, 파일을 개방하기가 어렵다면 2 또는 그 이상의 값을 사용한다.

 

22. 3. 3 종료시의 상황정리

당신의 프로그램에서 보통의 종료가 발생하면 그 자신만의 정리(cleanup) 함수를 실행하여 정돈할 수 있다. 만일 당신이 여러 개의 응용 프로그램에서 한 개의 라이브러리를 사용하고 있을 때, 모든 응용프로그램이 종료되기전에 라이브러리의 정리(cleanup) 함수들을 명시적으로 호출한다면 신뢰성이 없다. atexit 또는 on_exit를 사용해서 라이브러리 그 자체에서 정리 함수를 설정함으로써, 응용 프로그램에게는 보이지 않는 정리(cleanup)함수를 만드는 것이 더 신뢰성 있는 프로그램이 된다.

함수 : int atexit (void (*function) (void))

atexit 함수는 프로그램이 정상적으로 종료하면 호출되도록 함수 function을 등록한다. 그 function은 아무런 인수가 없이 호출된다. atexit로부터의 반환값은 성공하면 0이고 만일 그 함수가 등록될 수 없으면 0이 아닌 값을 반환한다.

함수 : int on__exit (void (*function)(int status, void *arg), void *arg)

이 함수는 atexit 보다는 좀더 다양함을 구사할 수 있는 것이다. 이 함수는 두 개의 인수로써 함수 function과 포인터 arg를 받아들인다. 정상적이 프로그램 종료시하다, 그 function은 두 개의 인수들, 즉 exit에 주었던 상황 값과 arg와 함께 호출된다. 이 함수는 SunOS와의 호환성을 위해 GNU C 라이브러리에 포함되었으며, 다른 시스템에서는 지원되지 않을 것이다.
 
다음은 exit 와 atexit 의사용을 설명하는 프로그램 예제이다.
 
#include <stdio. h>
#include <stdlib. h>
 
void bye (void)
{
puts ("Goodbye, cruel world. . . . ");
}
 
int main (void)
{
atexit (bye);
exit (EXIT_SUCCESS);
}
 
이 프로그램이 실행됐을 때, 단지 메시지를 프린트하고 종료한다.

 

22. 3. 4 프로그램 중지시키기

당신은 abort 함수를 사용하여 당신의 프로그램을 중지시킬 수 있다. 이 함수를 위한 프로토타입은 `stdlib. h'에 있다.

함수 : void abort (void)

abort 함수는 비정상적으로 프로그램을 종료시킨다. 이것은 atexit 또는 on_exit로 등록된 정리(cleanup)함수들을 실행하지 않는다. 이 함수는 SIGABRT 시그널을 발생시킴으로써 프로세스를 종료시키고, 당신의 프로그램은 이 시그널을 가로채서 처리하는 핸들러를 포함할 수 있다.

 

22. 3. 5 내부적 종료

_exit 함수는 exit에 의해서 프로세스를 종료하기 위해서 사용되는 기본 동작이다. 이것은 `unistd. h'에 선언되어 있다.

함수 : void __exit (int status)

_exit 함수는 상황 status로 프로세스가 종료되도록 하는 기본 동작(primitives)이다. 이 함수를 호출하는 것은 atexit 또는 on_exit에 등록된 정리(cleanup) 함수들이 실행되지 않는다.

명백히 종료를 호출하거나, 또는 시그널의 결과에 의한 종료이거나_어떤 이유에 의해서 프로세스가 종료될 때, 다음과 같은 일이 발생한다.

프로세스에서 모든 개방된 파일 기술자들은 폐쇄된다. 8장 [Low-Level I/O] 참조.

종료 상황 코드의 하위 8비트는 wait 나 waitpid를거쳐서 부모 프로세스에게 보고되도록 저장된다; 23. 6절 [Process Completion] 참조.

종료된 프로세스의 어느 자식 프로세스들은 새로운 부모 프로세스가 할당된다. (이것은 프로세스 ID 1로서, 처음의 프로세스이다. )

SIGCHLD 시그널은 부로 프로세스에게 보내어진다.

만일 그 프로세스가 제어중인 터미널을 가진 세션 리더(session leader)라면, SIGHUP 시그널은 전면 작업에 있는 각각의 프로세스에게 보내어 지고, 제어중인 터미널은 세션으로부터 분열된다. 24장 [Job Control] 참조.

어떤 프로세스의 종료가 프로세스 그룹을 고아가 되도록 하고, 프로세스 그룹의 어떤 멤버를 멈추도록 하는 원인이 된다면, SIGHUP 시그널과 SIGCONT 시그널이 그룹에 있는 각각의 프로세스에게 보내어진다. 24장 [Job Control] 참조.


목차 이전 : 21. 시그널 처리 다음 : 23. 프로세스

'Programming > Unix Programing' 카테고리의 다른 글

24. Job Control(작업 제어)  (0) 2007.12.22
23. Processes  (0) 2007.12.22
22. 프로세스의 시동과 종료(Process StartUp)  (0) 2007.12.22
21. 신호처리(Signal)  (0) 2007.12.22
19. 지역과 세계화  (0) 2007.12.22
18. 확장된 문자  (0) 2007.12.22
Comment 0 Trackback 0
Top

prev 1 next