Unix System Programming 5 - :namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />김성호(moohou) :namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
_________________________________________________________________________
제5장. 프로세스
5.1. 프로세스 개념의 복습
UNIX 에서의 프로세스란 간단히 말하면 수행중인 프로그햄 그 자체이며, 이것은 다른 환경에서 말하는 태스크(task) 개념에 해당한다.
쉘(shell)은 하나의 명령을 수행하기 위하여 어떤 포그램을 시작할 때마다 새로운 프로세스를 생성한다.
UNIX 프로세스 환경은 화일시스템의 디렉토리 트리와 같은 계층적인 구조를 가진다. 프로세스 트리의 꼭대기에는 하나의 제어 프로세스가 존재하는데, 이것은 init 라 하는 매우 중요한 프로그램의 수행이며, 궁극적으로 모든 프로세스는 이것으로 비롯된다. 프로세스간의 통신을 위해 제공되는 시스템 호출들에 대해서는 다음 장에서 설명하기로 하고, 여기서는 다음과 같은 핵심적인 것만을 소개한다.
fork : 호출 프로세스와 똑같은 프로세스를 하나 생성한다. fork 는 가장 기본적인 프로세스 생성 프리미티브이다.
exec : 시스템 호출들의 모임으로서, 각각은 동일한 기능 즉, 한 프로세스를 그 자신의 기억장소에 새로운 프로그램을 대치시킴으로써 변환시키는 기능을 수행한다. exec 호출들 각각의 차이는 그들의 매개변수 목록들이 어떤방법으로 작성되는가에 있다.
wait : 프로세스의 동기화(synchronization)를 위한 초보적인 호출이다. 프로세스를 연관된 다른 프로세스가 끝날때까지 기다리게 한다.
exit : 프로세스를 종료할때 사용된다.
5.2. 프로세스의 생성
5.2.1. fork 시스템 호출
fork 시스템 호출은 기본적인 프로세스 생성 프리미티브이다. 이것을 통하여 UNIX 는 다중처리 시스템(multitasking system)으로 전환된다.
int pid;
pid = fork();
fork 가 성공적으로 수행되면 커널은 호출하는 프로세스의 복사본을 새로운 프로세스로서 생성한다. 새로 생성된 프로세스를 자식 프로세스(childprocess)라 하고, fork 를 호출한 프로세스를 부모 프로세스(parent process)라 한다.
fork의 호출로 자식프로세스가 생성된 후에는 부모 프로세스와 자식 프로세스가 동시에 수행되며, 이때 두 프로세스는 fork 호출문의 바로 다음 문장부터 수행을 계속한다.
프로세스 식별번호
fork 는 인수없이 호출되고, 정수형의 pid 를 돌려준다.
pid = fork();
부모와 자식 프로세스를 구분하는 것은 pid의 값이다. 부모 프로세스는 pid 가 0 이 아닌 양의 정수 값을 갖는 반면 자식 프로세스는 0 을 갖는다.
부모에게로 돌려주는 pid 값을 자식 프로세스의 프로세스 식별번호(process-id)라 한다.
main()
{
int pid; /* hold process-id in parent
*/
printf("Just one process so far\n");
printf("Calling fork ...\n");
pid = fork();
if(pid == 0)
printf("I'm the child\n");
else if(pid > 0)
printf("I'm the parent, child has
pid %d\n", pid);
else
printf("Fork returned error code,
no child\n");
}
fork 뒤의 if 문에 3개의 분기(branch)가 존재한다.
첫번째 분기는 변수 pid 값이 0 일때, 자식 프로세스를 위한 동작을 명세하는 것이고, 두번째 분기는 pid 값이 양수일때에 해당하는 것으로, 부모 프로세스가 해야할 동작을 나타낸다.
그리고 세번째 분기는 pid 가 음수값(실제는 -1)을 가질 때에 해당하는데, fork 가 자식 프로세스의 생성이 실패하였을 때의 동작을 말해준다.
이런 상황은 부모프로세스가 다음과 같은 두 종류의 제한을 깨뜨리려고 했을 때 발생한다. 첫째는 시스템 차원에서 허용되는 프로세스의 갯수이고, 둘째는 개별 사용자가 동시에 수행시킬 수 있는 프로세스의 수에 대한 제한이다.
이런 두 종류의 상황에서 오류변수 errno 는 EAGAIN 이라는 코드를 값으로 가진다. 왜 fork 가 유용한 호출인가를 논의해 보자. 가장 핵심은 fork 가 UNIX 의 다른 기능들과 연관될때 더 유용해진다는 것이다.
예를들어 fork 로 만들어진 부모와 자식 프로세스는 UNIX 에서 제공하는 signal 이나 pipe 등과 같은 프로
세스간의 통신기능을 이용하여 서로 협조해 가면서, 서로 관련되어 있지만 서로 다른 일들을 해 나갈 수가 있다.
5.3. exec 를 이용한 새 프로그램의 수행
5.3.1. exec 군
exec 군(family) 에 속한 어떤 호출은 새로운 프로그램의 수행을 위해 사용될 수 있다.
char *path, *file;
char *arg0, *arg1, ..., *argn;
char *argv[];
int ret;
.
.
ret = execl(path, arg0, arg1, ..., argn,
(char *)0);
ret = execv(path, argv);
ret = execlp(file, arg0, arg1, ..., argn,
(char *)0);
ret = execvp(file, argv);
exec 의 모든 변종들은 동일한 기능을 수행한다. 즉, 호출 프로세스(exec 를 호출하는)의 기억 장소에 새로운 프로그램을 로드(load)함으로써, 호출 프로세스가 새 프로그램을 수행하도록 한다. exec 가 성공적으로 수행되면 호출 프로그램은 완전히 새로운 프로그램으로 대치되고, 그 프로그램의 처음부터 수행은 시작하게 된다.
결과는 새로 만들어진 프로세스 하나만이 존재하는데, 이프로세스는 자기를 호출한 프로세스 와 똑같은 프로세스 식별번호를 가진다. exec 는 호출 프로세스와 동시에 수행하는 새로운 부프로세스를 생성하는 것이 아니라는 점이 중요하다. exec 로 부터의 복귀값은 없다.
excl 의 모든 인수는 문자형의 포인터이다. 첫번째 인수인 path 는 새로이 수행될 프로그램이 들어있는 화일의 이름을 가리킨다. 이것은 절대 또는 상대적인 유효한 경로이름이어야 한다.
execl(혹은 execv)는 쉘명령이 들어있는 화일은 수행시키지 못한다. 두번째 인수 arg0 은 관레적으로 앞자리의 경로이름을 제거한 프로그램 또는 명령의 이름이 된다. 마지막 임을 알리는 표시로 null 포인터가 존재해야 한다. 디렉토리를 나열하는 프로그램 ls 를 수행하기 위해 execl 을 사용하는 다음의 프로그램을 살펴보자.
main()
{
printf("executing ls\n");
execl("/bin/ls", "ls", "0l", (char *)
0);
/* If execl returns, the call has
failed, so ... */
perror("execl failed to run ls");
exit(1);
}
execl 이 성공적으로 호출되면 호출 프로그램을 제거하여 수행되지 않게 하고, execl 이 호출되지 않아 호출 프로그램이 살아 남으면 오류가 발생되도록 하는 것이다. 이런 이유로 execl 과 그 변종들이 복귀될때는 항상 -1 을 돌려준다.
execv, execlp 와 execvp
execv 는 2개의 인수만을 가진다. 첫째는 수행될 프로그램의 경로이름을 가지고 있는 문자열을 가리키는 포인터 둘째는 문자 포인터의 배열로서
char *argv[];
로서 선언되어 있다.
main()
{
char *av[3];
av[0] = "ls";
av[1] = "-l";
av[2] = (char *)0;
execv("/bin/ls", av);
/* again - getting this far implies
error */
perror("execv failed");
exit(1);
}
execlp 와 execvp 도 execl 과 execv 와 거의 비슷하다. 가장 중요한 차이는 execlp 와 execvp 의 첫번째 인수가 경로이름이 아니라 단순히 화일이름을 가리킨다는 데에 있다.
5.3.2. exec 에 의해 전달된 인수에의 접근
모든 프로그램은 자신의 main 함수로 전달된 인수를 통해서 자신을 호출한 exec 호출의 인수에 접근할 수 있다. 이 인수들은 프로그램의 main 함수를 다음과 같이 정의함으로써 사용될 수 있다.
main(argc, argv)
int argc;
char **argv;
{
}
argc 는 인수의 갯수를 나타내는 정수이고, argv 는 인수들의 배열을 가리킨다. 자신의 첫번째 인수를 제외한 인수들을 표준 출력으로 출력하는 다음의 프로그램을 살펴보자.
main(argc, argv)
int argc;
char ** argv;
{
while(--argc > 0)
printf("%s ", *++argv);
printf("\n");
}
5.4. exec 와 fork 의 공동이용
fork 와 exec 를 함께 사용함으로써 프로그래머에게 더 많은 기능을 제공할 수 있다. fork 로 자식 프로세스를 만들고, 그 자식프로세스 안에서 exec 를 이용하면, 부모프로세스의 입장에서는 자신을 죽이지 않고도 전혀 다른 프로그램을 부프로세스로 가질 수 있게 된다.
간단히 오류 루틴 fatal 과 wait 라는 시스템 호출이 새로이 소개된다.
main()
{
int pid;
pid = fork();
/* if parent, use wait to suspend
* execution until child finishes
*/
if(pid > 0){
wait((int*)0);
printf("ls completed\n");
exit(0);
}
if(pid == 0){
execl("/bin/ls", "ls", "-l",
(char *)0);
fatal("execl failed");
}
/* getting here means pid is
* negative, so error has
* occurred
*/
fatal("fork failed");
}
fatal 은 한 메세지를 출력하기 위해서 단순히 기존의 perror 를 호출한다.
fatal(s)
char *s;
{
perror(s);
exit(1);
}
이 예에서 wait 는 fork 호출로 자식 프로세스를 생성한 직후에 호출된다. 시스템은 이 호출로 인해 자식이 끝날때까지 부모를 sleep 상태에 둔다.
5.5. 상속된 자료와 화일 기술어
5.5.1. fork 에 있어서의 화일과 자료
fork 로 생성된 자식프로세서는 부모프로세스와 거의 똑같다. 특히 부모 프로세스가 가지고 있던 변수의 값들은 자식프로세스에게 그대로 전달된다. (fork 자신으로부터의 복귀값은 예외임)
자식에게 주어지는 변수의 값들은 부모 프로세스가 가진 변수의 값들의 '복제'이기 때문에 기억장소에서 서로 다른 위치에 놓이게 된다. 그러나 fork 이전에 개방된 화일들은 부모와 자식 프로세스간에 매우 밀접하게 연관 된다.
이것은 각 화일의 읽기-쓰기 포인터가 부모와 자식사이에서 공유되기 때문이다. 이러한 공유는 읽기-쓰기 포인터가 프로그램 자체내에 명시적으로 선언되는 것이 아니라 시스템이 관리하는 것이기 때문에 가능하다.
결론적으로 한 자식프로세스가 어떤 화일에서 정방향으로 포인터를 전진시키면 부모 프로세스에서도 새로운 위치로 이동된다.
5.5.2. exec 와 개방된 화일
보통 개방된 화일 기술어들도 exec 를 호출했을 때 생성된 프로세스에 전달된다. 즉, 원래의 프로그램에서 개방된 화일들은 exec 를 통해서 전혀 새로운 프로그램이 시작될 때도 개방된 상태가 보존된다. 그런 화일들에 대한 읽기-쓰기 포인터들도 exec 호출에 의해 변화되지 않는다. fcntl 루틴을 이용하면 한 화일과 연관된 close-on-exec 플래그를 조절할 수 있다.
#include <fcntl.h>
.
.
int fd;
fd = open("file", O_RDONLY);
.
.
fcntl(fd, F_SETFD, 1);
close-on-exec 플래그는 명령문
fcntl(fd, F_SETFD, 0);
에 의해 off 로 된다. 플래그의 현재 값은 다음과 같이 얻어질 수 있다.
res = fcntl(fd, F_GETFD, 0);
정수형 변수 res 는 close-exec 플래그가 화일 기술어 fd 에 대해 on 일때 1값을 가지며, 그렇지 않으면 0 을 가진다.
5.6. exit 시스템 호출
int status;
exit(status);
exit 은 이미 익숙한 것으로, 프로세스를 종료시키고자 할 때 사용된다. 물 프로세스는 프로그램을 수행하며 main 함수의 끝이 도달하거나, main 에서 return 문을 수행할 때에도 종료된다. exit 호출에서 가장 중요한 것은 모든 개방된 화일 기술어를 닫는 것이다.
5.7. wait 를 이용한 프로세스의 동기화
int retval, status;
retval = wait(&status);
retval = wait((int *)0);
wait 는 자식프로세스가 수행되고 있는 동안 부모프로세스의 수행을 일시적으로 중단시킨다. 자식이 수행을 마치면, 기다리던 부모는 수행을 재개한다. 하나 이상의 자식이 수행되고 있으면, wait 는 자식프로세스들 중 하나가 최초로 종료되는 시간에 복귀된다.
pid = fork();
if(pid == 0){
}else{
wait((int *)0);
}
fork 와 wait 의 조합은 자식프로세스가 exec를 통해 완전히 서로 다른 프로그램을 수행 하도록 되어있을 때 이용된다. wait 가 -1을 돌려주면 살아있는 자식프로세스가 없다는 의미이고, 이 경우 errno 는 오류 코드 ECHILD 를 가지게 된다.
5.8. 좀비와 불완전한 종료
1. 부모 프로세스가 wait 를 수행하지 않고 있는 상태에서 자식이 종료할때
2. 하나 이상의 자식 프로세스가 수행되고 있는 상태에서 부모가 종료할때
1. 의 경우 종료하는 프로세스는 일종의 잊혀진 장소로 옮겨져서 좀비(zombie)가 된다. 좀비 프로세스는 프로세스를 제어하기 위해 커널이 관리하고 있는 테이블에 등록되어 있으면서 커널의 다른 자원들은 사용하지 않는다.
2. 의 경우, 부모는 정상적인 종료가 허용된다. 부모 프로세스의 자식들(좀비를 포함한)은 시스템의 초기화 프로세스에게 맡겨진다(초기화 프로세스가 부모의 역활을 함).
5.9. smallsh : 명령어 처리기
-- 생략
5.10. 프로세스 속성
각각의 UNIX 프로세스는 몇 가지의 속성(attribute)들을 가지는데, 이 속성은 프로세스의 수행과 수행계획(scheduling), 화일 시스템의 보안 유지등을 시스템이 조정하는데에 도움을 준다.
5.10.1. 프로세스 식별번호
시스템은 각 프로세스에게 프로세스 식별번호라는 음이 아닌 정수를 부여한다. 프로세스 식별번호는 해당 프로세스가 종료하면 다시 사용될 수 있지만, 한 시점에서는 유일하게 프로세스를 지정한다.
프로세스 0은 수행 계획프로세스(scheduler) 이고, 프로세스 1은 /etc/init 프로그램을 수행하는 초기화 프로세스이다. 시스템 호출을 이용하여 자신의 프로세스 식별번호를 참조할 수 있다.
pid = getpid();
getppid 를 사용하면 호출 프로세스의 부모 프로세스의 프로세스 식별번호를 얻을 수 있다.
ppid = getppid();
5.10.2. 프로세스 그룹과 프로세스 그룹 식별번호
UNIX 는 프로세스들이 어떤 그룹에 속하는 것을 허용한다. 각 프로세스 그룹은 프로세스 그룹 식별번호라 불리는 정수로 표시된다. 처음에 프로세스는 fork 나 exec 를 호출할 때 자신의 프로세스 그룹 식별번호를 새로운 프로세스에게로 상속한다. 그러나 한 프로세스는 setpgrp 를 호출함으로써 자신을 새로운 그룹에 넣을 수 있다.
newpg = setpgrp();
newpg 는 새로운 프로세스 그룹 식별번호인데, 실은 호출 프로세스의 프로세스 식별번호와 동일한 값이다. 한 프로세스는 자신의 현재 프로세스 그룹 식별번호를 getpgrp 라는 시스템 호출을 이용하여 얻을 수 있다.
pgid = getpgrp();
자신의 프로세스 그룹 식별번호를 수정하지 않은 프로그램이라면, pgid 의 값은 자신의 조상중의 쉘 프로세스의 프로세스 식별번호가 될 것이다.
5.10.3. 환경
프로세스의 환경(environment)은 간단히 말하면 null 로 끝나는 문자열의 모임인데, 프로그램 안에서는 문자형 포인터의 null 로 끝나는 배열로 표현된다.
관례적으로, 각 환경 문자열은 다음과 같은 형태를 가진다.
name = something
프로그래머는 프로그램의 main 함수의 인수리스트에 envp 라는 또 하나의 인수를 첨가함으로써, 프로세스의 환경을 직접사용할 수 있다.
다음의 프로그램은 envp 의 유형을 보여준다.
main(argc, argv, envp)
int argc;
char **argv, **envp;
{
}
5.10.4. 현재 작업 디렉토리
앞의 4 장에서 살펴보았듯이, 각 프로세스는 현재 작업 디렉토리와 연관된다. 현재 디렉토리의 초기상태는 그 프로세스가 fork 나 exec 로 시작될 때 물려받는다. 다시 말하면 한 프로세스는 그의 부모와 같은 디렉토리에 놓여진다.
자식 프로세스가 chdir 을 호출함으로써, 그 위치를 변화시켜도 부모 프로세스의 현재 디렉토리는 변하지 않는다. 이런 이유로 표준 cd 명령어는 쉘 자체에 내장된 명령어이고, 프로그램에 대응하는 것이 아니다.
5.10.5. 사용자 식별번호와 그룹식별번호
각 프로세스는 실제사용자 식별번호와 그룹 식별번호와도 연관된다. 이것들은 그 프로세스를 호출한 실제 사용자와, 그 사용자가 속한 그룹의 식별번호들이다. 더 중요한 것은 유효사용자 식별번호와 유효그룹 식별번호인데, 이것들은 어떤 사용자가 한 화일을 접근할 수 있는 지의 여부를 결정하는데 사용된다.
대부분의 경우에 유효사용자 식별번호와 실제사용자 식별번호는 같다. 그러나 프로세스 실제사용자 화일의 set-user-id 비트가 1 이면, 그 프로그램이 exec 로 호출될 때, 그 프로세스의 유효사용자 식별번호는 그 프로세스를 시작시킨 실제사용자가 아니라. 프로그램 화일의 소유자가 된다.
프로세스와 연관된 사용자와 그룹의 식별번호를 얻는데 쓰이는 시스템호출이 몇 가지 있다.
int uid, euid, gid, egid;
uid = getuid();
euid = geteuid();
gid = getgid();
egid = getegid();
유효사용자와 그룹의 식별번호를 지정할 때에는 다음의 두가지 호출이 유용하다.
int status, newuid, newgid;
.
.
status = setuid(newuid);
status = setgid(newgid);
두 루틴의 복귀값이 0 이면 수행의 성공을, 1 이면 실패를 나타낸다.
5.10.6. 화일크기의 제한 : ulimit
시스템 V 에는 프로세스마다 wait 시스템 호출을 이용하여 만들 수 있는 화일의 크기에 제한이 있다. 화일 크기의 제한은 ulimit 라는 시스템 호출로 조작된다.
long retval, newlimit, ulimit();
int cmd;
.
.
retval = ulimit(cmd, newlimit);
현재 화일의 크기 제한을 얻어내기 위하여 프로그래머는 cmd 인수를 1로 하고 ulimit 를 호출한다. 복귀값인 retval 은 512바이트를 한 블럭으로 하는 단위이다. 화일 크기의 제한을 바꾸려면 cmd 를 2 로 하고, 화일의 크기에 대한 새로운 제한을 512바이트 블럭을 단위로 newlimit 에 저장한다.
5.10.7. 프로세스 우선 순위 : nice
시스템이 cpu 시간의 비율을 결정할 때, 특정한 프로세스는 그의 nice 값(정수)에 의거하여 시간이 할당된다. Nice 값은 0 에서 시스템이 정하는 최대값(보통은 39)까지이다. 큰 값을 가질수록 프로세스는 낮은 우선순위를 가진다.
nice 호출은 하나의 인수를 필요로 하는데, 그것은 현재의 nice 값에다 증가시키려는 만큼의 양의 정수값, 즉 증가분을 말한다.
nice(5);
수퍼사용자만이 인수를 음수로하여 우선순위를 높일 수가 있다.
'Academy I > Tech Academy' 카테고리의 다른 글
Unix System Programming 9 (0) | 2014.12.16 |
---|---|
Unix System Programming 8 (0) | 2014.12.16 |
Unix System Programming 7 (0) | 2014.12.16 |
Unix System Programming 6 (0) | 2014.12.16 |
Unix System Programming 4 (0) | 2014.12.16 |
Unix System Programming 3 (0) | 2014.12.16 |
Unix System Programming 2 (0) | 2014.12.16 |
Unix System Programming 1 (0) | 2014.12.16 |