🔔 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다. 혹여나 틀린 부분이 있다면 지적해주시면 감사드리겠습니다.
execlp
, execvp
에서 문자 'p'는 함수가 PATH 환경 변수를 사용하여 실행 파일을 찾는다는 것을 의미한다. 따라서 아래와 같이 PATH
에 해당 경로가 위치해있어야 한다.
execl
, execle
, execlp
의 문자 'l'은 함수가 인수를 list로 받는다는 것을 의미하며, execv
, execve
, execvp
의 문자 'v'는 함수가 argv[]
vector로 인자를 받는다는 것을 의미한다. execle
, execve
에서 문자 'e'는 함수가 현재 environment
을 사용하는 대신 envp[]
를 사용함을 의미한다.
# include <unistd.h>
int main() {
printf(“executing ls\n”);
execl (“/bin/ls”, “ls”, “-l”, (char*) 0);
perror (“execl failed to run ls”);
return 1;
}
execl
이므로 list 형태로 인자를 전달하며, 마지막에는 (char*) 0
으로 null
혹은 null pointer
로 끝을 알린다.
# include <unistd.h>
int main() {
char *const av[] = {“ls”, “-l”, (char *)0};
printf(“executing ls\n”);
execv (“/bin/ls”, av);
perror (“execv failed to run ls”);
return 1;
}
execv
이므로 인자와 함게 null
까지 vector
에 담아 전달해준다.
/* myecho */
int main(int argc, char** argv) {
while (--argc > 0)
printf(“%s “, *++argv);
printf(“\n”);
return 0;
}
/* run_myecho */
#include <unistd.h>
int main() {
char * const argin[] = {“./myecho”, “hello”, “world”, (char *)0};
execvp (argin[0], argin);
return 1;
}
// $./run_myecho
// hello world
execvp
이므로 argin[0]
과 같이 file 이름만 전달하였으며, 해당 file이 위치하는 경로가 PATH
에 존재해야한다. myecho
에서 전달받을 때, 가장 끝에 있는 (char *)0
는 전달되지 않는다.
단순 fork
만을 사용하면, 한 파일에서 parent
와 child
의 코드를 전부 포함해야 하므로, exec
와 같이 사용하는 경우가 일반적이다.
이에 대한 간단한 예시 코드를 살펴보겠다.
# include <unistd.h>
int fatal(char *s) {
perror(s);
exit(1);
}
int main() {
pid_t pid;
switch (pid = fork()) {
case -1:
fatal (“fork failed”);
return 1;
case 0:
execl (“/bin/ls”, “ls”, “-l”, (char *)0);
fatal (“exec failed”);
return 1;
default:
wait((int*)0);
printf(“ls completed\n”);
return 0;
}
}
fork
를 수행하여 return값을 pid
에 저장한다.
child의 경우
- pid = 0
ls -l
명령어를 실행
parent의 경우
- pid > 0
wait((int*)0)
: child가 종료될때까지 대기- 이후 종료
이를 그림으로 나타내면 아래와 같다.
shell에서 option이 없는 command를 실행하는 과정에도 활용될 수 있다.
int docommand(char *command) {
pid_t pid;
if ((pid = fork()) < 0)
return -1;
if (pid == 0) { /* child */
execl (“/bin/sh”, “sh”, “-c”, command, (char *)0);
perror (“execl”);
exit(1);
}
wait((int *)0);
return(0);
}
인자로 전달받은 command
를 fork
를 통해 child
에서 실행한 이후, 종료하는 간단한 함수이다.
fork
시에 parent의 file descriptor
는 child의 file descriptor
와 동일하다. 가장 상위 process의 file descriptor
에 0, 1, 2번이 기본적으로 포함되어있고, 하위 process들은 이 file descriptor
를 그대로 가져오기 때문에 항상 0, 1, 2번은 기본적으로 가지고 있다.
이에 대한 예시 코드는 아래와 같다.
int printpos(const char *string, int filedes) {
off_t pos;
if (( pos = lseek (filedes, 0, SEEK_CUR)) == -1)
fatal (“lseek failed”);
printf (“%s:%ld\n”, string, pos);
}
int main() {
int fd;
pid_t pid;
char buf[10];
if ((fd = open(“data”, O_RDONLY)) == -1)
fatal(“open failed”);
read(fd, buf, 10);
printpos(“Before fork”, fd);
switch (pid = fork()) {
case -1:
fatal(“fork failed”);
break;
case 0:
printpos(“Child before read”, fd);
read(fd, buf, 10);
printpos(“Child after read”, fd);
default:
wait((int *)0);
printpos(“Parent after wait”, fd);
}
return 0;
}
printpos
는 lseek
를 이용하여 현재 해당 파일의 file position
을 출력한다.
처음에 파일을 open
한 이후에 10만큼 읽어 buf
에 저장하여 file position
을 10으로 이동시킨다. 이후 fork
하여 child
process에서 10만큼 더 읽어 file position
을 20으로 이동시킨다.
이 경우에 child
의 file position
은 20이며, 동시에 parent
도 동일한 file descriptor table
을 공유하므로 해당 file position
또한 20으로 변경된 것을 알 수 있다.
즉, 서로 동일한 파일을 바라보기 때문에 동일한 position을 공유한다는 의미이다.
process 자체에 대한 정보보다는 부수적인 data들이 유전된다.
- real uid, real gid, effective uid, effective gid
- session id
- 환경변수들
- supplementary gids, process gid
... 이하 생략
process와 직접적으로 관련된 data들은 유전되지 않는다.
- fork부터의 return value
- pid
- 부모 pid
- process 실행 시간
- File locks, pending alarms, set of pending signal 등 부모가 약속한 것들
exec
역시 fork
와 마찬가지로 대부분이 그대로 유지되지만, close-on-exec flag
가 설정된 경우, 해당 file descriptor
는 닫히게 된다.
해당 flag는 default는 0으로 off이지만, 아래 명령어를 통해 flag를 설정해줄 수 있다.
fcntl(fd, F_SETFD, 1);
process를 정상적으로 종료하는 방식은 5가지, 비정상적으로 종료하는 방법은 3가지가 있다.
Normal termination
- main으로부터 return
exit()
호출_exit()
,_Exit()
호출- 마지막 thread에서 return (관련된 detail한 방식 2가지)
Abnormal termination
abort()
호출- thread에서 취소 request
- signal 수신
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
argument
- int status : 프로그램이 어떻게 종료되었는지를 나타내는 정수, 일종의 프로그램의 유서
- 하위 8bits만 반영됨, parent에서는
wait()
을 이용해 전달받음.
return
- 정상 종료일 경우 0
- 0이 아닐 경우는 문제가 발생한 경우
exit(0)
은 return(0)
와 동일한 역할이며, exit
의 경우 buffer를 모두 비우는 (buffer에 있는 작업을 모두 마무리 이후) clean up processing
이후 종료하며, _exit(), _Exit()
은 바로 종료하게 된다는 차이점이 존재한다.
기본적으로 exec
실행 시, start-up routine
을 거쳐 main
이 실행되는데, 명시적으로 _exit
, _Exit
을 호출하지 않는 이상 항상 exit
을 호출하게 된다.
exit
과정에서 exit handler
는 프로그램이 종료될 때 수행해야 할 함수들을 가리키는 역할을 하며, 이는 아래의 function으로 지정이 가능하다.
#include <stdlib.h>
int atexit(void (*func)(void));
argument
- void (*func)(void) : 실행할 함수
return
- 성공할 경우 0 return
- error 발생 시, 0이 아닌 값 return
일반적으로 32개의 function을 등록할 수 있다.
이에 대한 예시는 아래와 같다.
void func1() { printf(“print func1\n”); }
void func2() { printf(“print func2\n”); }
void func3() { printf(“print func3\n”); }
void func4() { printf(“print func4\n”); }
int main() {
pid_t pid;
atexit(func1);
atexit(func2);
atexit(func3);
atexit(func4);
if ((pid = fork()) < 0) {
perror(“fork failed”);
exit(1);
}
if (pid == 0) {
printf(“child process is called\n”);
printf(“child process calls exit\n”);
exit(0);
}
wait(NULL);
printf(“parent process calls exit\n”);
exit(0);
}
atexit
으로 등록해준 역순으로 실행되며, handler
역시 자식에게 유전되는 정보 중 하나이다. 따라서 parent에서 등록하여도 child에서 동일하게 실행된다.
process가 종료되면, kernel
이 열린 모든 file descriptor
를 닫아주고, 사용된 memory
를 release 해준다. 하지만 위에서 유서라 칭했던 exit status
, PID
, CPU time
은 남아있다. 이를 parent에서 wait
을 통해 처리해줘야 한다.
#include <sys/wait.h>
pid_t wait(int *statloc);
argument
- int *statloc : 상위 16bits는 0, 그 다음 8bits는 exit number, 하위 8bits는 다른 정보가 담김.
child
의exit status
를 가리키며,NULL
일 경우, 무시됨.
return
- 성공할 경우 child의
PID
를 return- error 발생 시 -1 return :
errno = ECHILD
로 자식이 없음을 의미- 종료 신호와 함께 0 return
기본적으로 여러명의 자식이 존재하면, 가장 먼저 종료되는 자식을 기준으로 return 된다.
또한 2가지 macro를 사용해 아래의 경우를 확인할 수 있다.
- WIFEXITED : exit status가 0이 아니면 1 return, 아니면 0 return (즉, 중간 8bit에 해당하는 exit number를 확인하는 것. 0이면 정상 종료를 의미)
- WEXITSTATUS : exit status return (8bit 오른쪽으로 shift 이후, 하위 8bit return)
이에 대한 예시 코드는 아래와 같다.
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
main() {
pid_t pid;
int status, exit_status;
if ((pid=fork()) < 0)
fatal (“fork failed”);
if (pid == 0) {
sleep(4);
exit(5);
}
if ((pid = wait(&status)) == -1) {
perror(“wait failed”);
exit(2);
}
if (WIFEXITED(status)) {
exit_status = WEXITSTATUS(status);
printf(“Exit status from %d was %d\n”, pid, exit_status);
}
exit(0);
}
fork
하여 return 받은 pid가 0보다 작다면 error 에 해당한다. pid가 0일 경우, 4초동안 sleep 후, exit(5)
를 실행한다. parent는 wait(&status)
를 통해 status에 exit status
를 저장한다. 이를 WIFEXITED
macro를 이용해 0이 아님을 판단하고, 0이 아닐 경우에는 WEXITSTATUS
macro를 이용하여 하위 8bit, 즉 실질적인 exit number
인 5를 출력하게 된다.
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *sataloc, int options);
argument
- pid_t pid
- == -1 : 어떤 child인지 상관 없음 (wait
과 동일한 역할)
- > 0 : PID가 pid와 동일한 child를 wait
- == 0 : PGID가 calling process와 동일한 child를 wait
- < 0 : PGID가 pid의 절대값과 같은 child를 wait?- int *statloc :
wait
과 동일- int options
- WCONTINUED : 정지 및 재개된 child process의 status를 전달받음
- WNOHANG : 종료된 child가 없을 경우, 즉시 0을 반환하며 block하지 않음
- WUNTRACED : 종료된 child process의 status를 전달받음
return (
wait
의 return과 동일)
- 성공할 경우 child의
PID
를 return- error 발생 시 -1 return :
errno = ECHLID
로 자식이 없음을 의미- 종료 신호와 함께 0 return
이에 대한 예시는 아래와 같다.
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
main() {
pid_t pid;
int status, exit_status;
if ((pid=fork()) < 0)
fatal (“fork failed”);
if (pid == 0) {
sleep(4);
exit(5);
}
while ((pid = waitpid(pid, &status, WNOHANG)) == 0) {
printf(“Still waiting...\n”);
sleep(1);
}
if (WIFEXITED(status)) {
exit_status = WEXITSTATUS(status);
printf(“Exit status from %d was %d\n”, pid, exit_status);
}
exit(0);
}
wait
의 예제와 비슷한 결과를 보여주는 코드이나, waitpid
의 WNOHANG
option의 경우, **이전에 종료된 child process가 존재하면 해당 pid를 return, 없다면 바로 0을 return한다. 0일 경우, 1초 sleep 이후 다시 waitpid
를 실행하는 코드이다. 출력 결과는 동일하게 5를 출력한다.
wait
은 child process가 종료될때까지 block되는 것에 비해, waitpid
는 WNOHANG
option을 사용하면 non blocking으로 수행이 가능하다.
parent process에서 wait
을 호출하지 않아 완전히 종료되지 않은 child process를 Zombie process
라고 한다. 실행이 완료되었지만, 여전히 process table에 남아있는 상태가 된다.
위 그림이 Zombie process
의 예시이다.
parent process가 child process보다 먼저 종료된 경우, child process는 Orphan process
가 된다. 해당 process의 parent는 init process (pid = 1)
가 되며, 이는 주기적으로 wait
을 호출하므로, Zombie process
로 남지는 않는다.
위의 그림이 그 예시이며, Orphan process
역시 init
에서 wait
을 하기 전까지는 Zombie process
가 된다.
모든 process는 Zombie process
인 기간이 존재한다.
echo $?
직전에 종료한 process의 return값을echo
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[]) {
pid_t pid;
if (argc < 2) {
printf("Usage: %s PROG [PROG_ARG]...\n", argv[0]);
return 1;
}
pid = fork();
argv[argc] = NULL;
if (pid == 0) {
execvp(argv[1], &argv[1]);
perror("execv failed");
return 1;
}
else if (pid < 0) {
perror("fork failed");
return 2;
}
wait(NULL);
return 0;
}
위 코드에서는 execvp
를 사용하였다. 이 대신 execlp
를 사용할 경우, 이후 인자가 몇개가 나열될지 예상이 불가능하므로, vector
로 전달이 가능한 execvp
를 사용하는 것이 적절하다.
또한 execv
역시 명령어를 실행하는 것이므로, 이름으로 전달받을 수 있는 execvp
를 사용하는 것이 적합하다.
wait(NULL)
은 parent process에서 child process의 작업이 종료될때까지 기다리는 역할을 한다. 해당 코드가 없을 경우, child가 종료되고 parent가 종료되는 것이 아니라, 그 반대가 될 수도 있으므로 상황에 맞게 사용하는 것이 중요하다.
시프를 듣기전에 이 글을 봤었다면... 라는 생각이 드네요. 잘 읽었습니다.