2005. 3. 15. 12:52
Unix System Programming 5 | Linux Unix | 2004/04/24 10:29 |
http://blog.naver.com/javabuilder/60001947114 | |
제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 가 성공적으로 수행되면 커널은 호출하는 프로세스의 복사본을 새로운 프로세스로서 생성 한다. 새로 생성된 프로세스를 자식프로세스(child process)라 하고, fork 를 호출한 프로세스를 부모 프로세스(parent process) 라 한다. fork 의 호출로 자식프로세스가 생성된 후에는 부모프로세스와 자식 프로세스가 동시에 수행 되며, 이때 두 프로세스는 fork 호출문의 바로 다음 문장부터 수행을 계속한다. 프로세스 식별번호 fork 는 인수없이 호출되고, 정수형의 pid 를 돌려준다. pid = fork(); 부모와 자식 프로세스를 구분하는 것은 pid의 값이다. 부모 프로세스는 pid 가 0 이 아닌 양의 정수 값을 갖는 반면 자식 프로세스는 0 을 갖는다. 부모 에게로 돌려주는 pid 값을 자식 프로세스의 프로세스 식별번호(process-id)라 한다. /* spawn -- demonstrate fork */ main() { int pid; /* hold process-id in parent */ printf("Just one process so far\n"); printf("Calling fork ...\n"); pid = fork(); /* create new process*/ 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 을 사용하는 다 음의 프로그램을 살펴보자. /* runls -- use "execl" to run ls */ 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[]; 로서 선언되어 있다. /* runls2 -- use execv to run ls */ 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; { /* body of program */ } argc 는 인수의 갯수를 나타내는 정수이고, argv 는 인수들의 배열을 가리킨 다. 자신의 첫번째 인수를 제외한 인수들을 표준 출력으로 출력하는 다음의 프 로그램을 살펴보자. /* myecho -- echo command line arguments */ 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 라는 시 스템 호출이 새로이 소개된다. /* runls3 -- run ls in a subprocess */ 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 child then exec ls */ 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) /* print error message and die */ 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); . . /* set close-on-exec flag on */ 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(); /* create new process */ if(pid == 0){ /* child */ /* do something..*/ }else{ /* parent, so wait for child */ 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; { /* do something */ } 5.10.4. 현재 작업 디렉토리 앞의 4 장에서 살펴보았듯이, 각 프로세스는 현재 작업 디렉토리와 연관된다. 현재 디렉토리 의 초기상태는 그 프로세스가 fork 나 exec 로 시작될 때 물려 받는다. 다시 말하면 한 프로세스는 그의 부모와 같은 디렉토리에 놓여진다. 자식 프로세스가 chdir 을 호출함으로써, 그 위치를 변화시켜도 부모 프로세스 의 현재 디렉토리는 변하지 않는다. 이런 이유로 표준 cd 명령어는 쉘 자체에 내장된 명령어이고, 프로그램에 대응하는 것이 아니다. 5.10.5. 사용자 식별번호와 그룹식별번호 각 프로세스는 실제 사용자식별번호와 그룹식별번호와도 연관된다. 이것들은 그 프로세스를 호출한 실제 사용자와, 그 사용자가 속한 그룹의 식별번호들이 다. 더 중요한 것은 유효 사용자식별번호와 유효그룹식별번호인데, 이것들은 어떤 사용자가 한 화일을 접근할 수 있는 지의 여부를 결정하는데 사용된다. 대부분의 경우에 유효사용자 식별 번호와 실제사용자 식별번호는 같다. 프로스 랩 화일의 set-user-id 비트가 1 이면, 그 프로그램이 exec 로 호출될 때, 그 프로세스 의 유효 사용자식별번호는 그 프로세스를 시작시킨 실제사용자가 아 니라. 프로그램 화일의 소유자가 된다. 프로세스와 연관된 사용자와 그룹의 식 별번호를 얻는데 쓰이는 시스템호출이 몇가지 있다. int uid, euid, gid, egid; /* get real user-id */ uid = getuid(); /* get effective user-id */ euid = geteuid(); /* get real group-id */ gid = getgid(); /* get effective group-id */ egid = getegid(); 유효사용자와 그룹의 식별번호를 지정할 때에는 다음의 두가지 호출이 유용하 다. int status, newuid, newgid; . . /* set effective user-id */ status = setuid(newuid); /* set effective group-id */ 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); 수퍼 사용자만이 인수를 음수로하여 우선순위를 높일수가 있다. |
'UNIX' 카테고리의 다른 글
main() 함수의 매개변수-> (argc,argv) (0) | 2005.03.15 |
---|---|
int argc, char **argv 의 정체가 정말 (0) | 2005.03.15 |
Unix 함수 설명 및 목록 (1) | 2005.03.15 |
프로세스의 상태를 얻는 매크로 (0) | 2005.03.14 |
crontab 등록하기 (2) | 2005.02.23 |