system함수는 유닉스 운영체제에는 모두 지원합니다. system함수는 입력받은 command의 문자열을 실제로 실행시켜주는 함수입니다.
system함수를 사용하기 위해서는 stdlib.h 헤더파일을 include 해야합니다.
#include <stdlib.h>
system함수의 원형은 아래와 같습니다.
int system(const char *command);
사용하는 방법은 매우 간단합니다. command에 실행할 명령어를 전달해주기만 하면 됩니다. 아래의 사용 예를 보시면 금방 사용하실수 있을겁니다.
사용예)
//system_test.c
#include <stdlib.h>
#include <stdio.h>
int main(){
char *command="ls -al";
int ret;
ret = system(command);
printf("system함수 종료 :%d\n",WEXITSTATUS(ret));
}
# gcc system_test.c
# ./a.out
합계 208
drwxr-xr-x 19 ubuntu ubuntu 4096 4월 11 17:22 .
drwxr-xr-x 6 root root 4096 4월 1 15:38 ..
-rw------- 1 ubuntu ubuntu 378 4월 11 17:17 .Xauthority
-rw------- 1 ubuntu ubuntu 5496 4월 11 12:42 .bash_history
-rw-r--r-- 1 ubuntu ubuntu 220 2월 22 2021 .bash_logout
...
system 함수 호출 완료 ret:0
system함수의 내부
system함수에 NULL을 전달하게 되면 적절한 명령처리기가 존재한다면 0을 돌려줍니다. 그 외에는 상황에 따라 다릅니다. system함수를 내부적으로 들여다보면 fork, exec, waitpid로 이루어진 함수입니다. 이 세개의 함수에 대해서 모르신다면 아래의 포스팅을 참고하시기 바랍니다.
2. exec함수가 실패했다면 이런 경우에는 shell을 실행할수 없다는 뜻이며, exit(127)과 동일합니다.
3. 그 외의 경우에는 waitpid에 지정된 셸의 종지 상태가 return됩니다.
아래의 코드는 system함수를 흉내낸 코드입니다.
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int system(const char *cmd){
pid_t pid;
int status;
if(cmd == NULL) return 1; //UNIX에는 명령 처리기가 존재
if((pid = fork()) < 0){
status = -1; //프로세스 생성 에러
}else if(pid == 0){ //자식 프로세스
execl("/bin/sh","sh","-c",cmd,(char*)0);
_exit(127); //위 2번읜 case
}else{ //부모 프로세스 : 자식이 끝날때까지 기다림
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTR){ //위 1번의 case
status = -1;
break;
}
}
}
return status;
}
int main(){
int ret;
ret = system("ls -al");
printf("system함수 종료 :%d\n",WEXITSTATUS(ret));
}
이러한 구현사항때문에 내부적으로 fork()로 자식 프로세스를 수행하고 자식 프로세스는 exec함수를 호출하는데요. 부모 프로세스는 waitpid로 자식 프로세스를 기다리기 때문에 system다음 줄의 printf가 실행될수 있는 것이죠.
종지 상태 확인
WEXITSTATUS로 실제 exit()이나 return값을 확인할수 있습니다. 아래는 main에서 바로 return 18로 빠져 나오는 한 프로그램입니다. 혹은 exit(18)을 해도 똑같습니다.
//program.c
int main(){
//exit(18);
return 18;
}
# gcc program.c -o program
# ls program
program
program이라는 실행파일이 생겨납니다. 이제 이 실행파일을 실행시키기 위해 system함수를 사용해보겠습니다.
//system_test.c
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
int ret;
ret = system("./program");
printf("system함수 종료 :%d\n",WEXITSTATUS(ret));
}
# gcc system_test.c
# ./a.out
system함수 종료 :18
우리가 return했던 값을 확인할 수 있죠? 단순 ret값을 출력하는게 아닌 매크로를 통해서 종지상태를 확인해야한다는 점을 기억하세요.
wait, waitpid 함수는 아래의 header파일을 include해야합니다. 이 두 함수는 부모프로세스가 자식프로세스를 기다리는 공통점이 있습니다만 waitpid는 어떤 자식을 기다릴지 조금 더 구체적으로 지정할 수 있습니다.
이 두 함수의 반환값은 성공시 process id를 반환하고 오류시 -1을 반환합니다.
#include <sys/types.h>
#include <sys/wait.h>
wait 함수
pid_t wait(int *wstatus);
성공시 프로세스의 pid를 반환합니다. wstatus는 종료상태를 알 수 있는데, 굳이 종료상태를 알 필요가 없다면 NULL을 전달해주세요.
wstatus를 알 수 있는 방법은 <sys/wait.h>에 정의되어있는 매크로들을 사용하면 됩니다. 아래는 그 매크로들과 설명입니다.
매크로
설명
WIFEXITED(wstatus)
returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main().
WEXITSTATUS(wstatus)
returns the exit status of the child. This consists of the least significant 8 bits of the status argument that the child specified in a call to exit(3) or _exit(2) or as the argument for a return statement in main(). This macro should be employed only if WIFEXITED returned true.
WIFSIGNALED(wstatus)
returns true if the child process was terminated by a signal.
WTERMSIG(wstatus)
returns the number of the signal that caused the child process to terminate. This macro should be employed only if WIFSIGNALED returned true.
WCOREDUMP(wstatus)
returns true if the child produced a core dump. This macro should be employed only if WIFSIGNALED returned true.
This macro is not specified in POSIX.1-2001 and is not available on some UNIX implementations (e.g., AIX, SunOS). Therefore, enclose its use inside #ifdef WCOREDUMP ... #endif.
WIFSTOPPED(wstatus)
returns true if the child process was stopped by delivery of a signal; this is possible only if the call was done using WUNTRACED or when the child is being traced (see ptrace(2)).
WSTOPSIG(wstatus)
returns the number of the signal which caused the child to stop. This macro should be employed only if WIFSTOPPED returned true.
WIFCONTINUED(wstatus)
(since Linux 2.6.10) returns true if the child process was resumed by delivery of SIGCONT
그렇다면 WIFEXITED는 false이고 WIFSIGNALED는 true네요. 이처럼 위의 매크로로 자식 프로세스의 종료 상태를 알 수 있습니다.
그런데 wait은 종료되는 자식 프로세스 중 먼저 종료되는 프로세스가 있다면 바로 호출되어지는데 만약 우리가 특정 자식프로세스만을 기다린다할때는 아래의 waitpid함수를 사용해야합니다.
waitpid
pid_t waitpid(pid_t pid, int *wstatus, int options);
역시 호출 성공시 그 프로세스의 pid, 아니라면 -1을 반환합니다.
pid: 인자로 pid는 기다릴 프로세스 식별자인데 양수일 경우만 해당합니다. 아래는 pid에 따라서 어떻게 동작하는지를 설명합니다.
pid
설명
< -1
meaning wait for any child process whose process group ID is equal to the absolutevalue of pid.
-1
meaning wait for any child process.
0
meaning wait for any child process whose process group ID is equal to that of thecalling process.
> 0
meaning wait for the child whose process ID is equal to the value of pid.
wstatus : wstatus는 위의 wait과 같이 종지상태를 얻어옵니다.
option : option은 조금 더 구체적인 동작을 지정합니다. option의 종류는 아래와 같습니다.
OPTION
설명
WNOHANG
return immediately if no child has exited.
WUNTRACED
also return if a child has stopped (but not traced via ptrace(2)). Statusfor traced children which have stopped is provided even if this option is notspecified.
WCONTINUED
(since Linux 2.6.10)also return if a stopped child has been resumed by delivery of SIGCONT.
wait(&status)은 waitpid(-1, &wstatus, 0)를 호출한 것과 같습니다.
위 코드에서는 첫번째 자식 프로세스는 기다리지 않습니다. waitpid에 WNOHANG 옵션을 주어서 자식이 종료되었는지 아닌지에 따라 작업을 구분지을 수 있습니다. 이때 waitpid는 지정된 프로세스가 끝나지 않았다면 0을 반환하고 끝났다면 그 pid를 반환하게 됩니다.
결과는 아래와 같습니다.
# gcc process_waitpid2.c
# ./a.out
child2 not finished
child2 process:5670
child1 process:5669
child2 not finished
child1 end
child2 not finished
child2 not finished
child2 end
parent end
waitpid가 wait에서 제공하지 못하는 3가지를 제공합니다.
1. waitpid함수로 특정 pid를 기다릴 수 있습니다. wait은 임의의 자식에 대해서 종료상태를 알려주지요.
2. waitpid는 wait과는 달리 자식이 종료될때까지 기다리지 않아도 됩니다. 바로 호출을 할 수 있습니다.
우선 이 코드는 문제가 좀 있는 코드입니다. 부모프로세스가 먼저 종료되었기 때문입니다. 자식이 먼저 종료된다는 사실을 확보하기 위해 1초를 기다리는 이유가 바로 이 사실을 확인하기 위해서 입니다. 자식 프로세스는 부모 프로세스를 잃어 다른 부모를 찾게 됩니다. 바로 init 프로세스라고 하는데요. init프로세스의 pid가 바로 1입니다.
그러니까 모든 프로세스의 선조가 init프로세스라는 이야기죠.
이런 프로세스를 부모를 잃었다고 하여 고아프로세스가 됩니다.
그렇기 때문에 모든 부모님이 그렇듯 자식을 기다려주어야합니다. 그렇기 때문에 wait이라는 시스템 콜이 있지요.
코드를 수정해서 다시 확인합시다.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(){
int pid;
int status;
int terminatedPid;
if((pid=fork()) == 0){
printf("\t [child process %d] created from %d\n",
getpid(),getppid());
sleep(1);
exit(0);
}
printf("[parent process %d] create %d process, ppid:%d\n",
getpid(),pid,getppid());
terminatedPid = wait(&status);
printf("[parent process ] process wait process %d, status %d\n",
terminatedPid,status);
}
wait : wait의 첫번째 인자는 상태를 나타냅니다. 종료 상태를 받아올 수 있는 거죠. 반환값은 종료된 자식의 pid를 반환합니다.
[parent process 21803] create 21804 process, ppid:20819
[child process 21804] created from 21803
[parent process ] process wait process 21804, status 0
이제 우리가 원하는 결과가 나왔군요. 자식프로세스를 기다려야합니다.
좀비프로세스
하지만 반대로 부모프로세스는 종료하지 않았지만 자식 프로세스가 먼저 종료하게 되면 어떨까요? 단, wait 호출을 하지 않고 말입니다.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
if(fork()==0){
printf("\t child process created and exit\n");
exit(0);
}
printf("parent process sleep\n");
sleep(10);
sleep(10);
printf("process exit\n");
}
이 후 컴파일하고 실행합니다.
# gcc process.c
# ./a.out &
[1] 22111
parent process sleep
child process created and exit
process exit
# ps -ef | grep 22111
...
root 22112 22111 0 10:26 pts/2 00:00:00 [a.out] <defunct>
...
부모 프로세스는 총 20초간 수행합니다. pid가 22111인 부모프로세스의 자식 프로세스 22112가 defunct인것이 보이세요? 바로 좀비 프로세스입니다.
부모프로세스는 자식 프로세스를 기다리고 있지 않아 종료상태를 얻지 못하고 있습니다. 그게 좀비 프로세스가 된 이유인데요. 이 종료 상태는 의외로 중요한데 예를 들어 쉘스크립트를 통해 명령을 실행할때 종료상태에 따라 분기를 하기 위해서 사용되기도 합니다.
부모 프로세스가 종료상태를 얻어와야 커널에서 자식 프로세스의 정보를 갖고 있는 구조체를 해제할 수 있습니다. 그 구조체가 task_struct라는 구조체입니다.
부모 프로세스는 커널이 관리하는 task_struct 구조체에서 멤버 pid와 exit_code를 통해서 종료상태를 얻어올 수 있습니다. 비록 좀비프로세스는 직접적으로 CPU, 메모리를 소모하지는 않지만 task_struct를 유지해야하기 때문에 메모리 낭비가 있게 되지요.
task_struct에서 pid와 exit_code, 무엇이 생각나세요?
wait의 반환값과 인자값입니다. 그 때문에 wait을 통해 해결할 수 있습니다.
코드를 다음과 같이 수정해보세요.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(){
if(fork() == 0){
printf("\tchild process created and exit\n");
exit(0);
}
printf("parent process sleep\n");
wait(NULL);
sleep(10);
sleep(10);
printf("process exit\n");
}
부모 프로세스에서 바로 wait 콜을 호출합니다. 그러니까 종료상태를 알게 되죠. 결과도 그럴까요?
# gcc process.c
# ./a.out &
[1] 23839
parent process sleep
child process created and exit
# ps -ef | grep 23839
root 23839 20819 0 11:31 pts/2 00:00:00 ./a.out
root 23842 20819 0 11:31 pts/2 00:00:00 grep --color=auto 23839
process exit
오.. 좀비프로세스가 없어졌어요.
또는 시그널을 이용한 방법도 있습니다.
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void childSignal(int sig){
printf("signal:%d\n",sig);
wait(NULL);
}
int main(){
signal(SIGCHLD,childSignal);
if(fork() == 0){
printf("\tchild process created and exit\n");
exit(0);
}
printf("parent process sleep\n");
sleep(10);
sleep(10);
printf("process exit\n");
}
(시그널을 알아야 코드를 이해하는 데 도움이 됩니다.)
그리고 컴파일하고 실행해보도록 합시다. 그리고 재빠르게 ps 명령어를 쳐서 확인해보세요.
# gcc process.c
# ./a.out &
[1] 23451
parent process sleep
child process created and exit
signal:17
# ps -ef | grep 23451
root 23451 20819 0 11:09 pts/2 00:00:00 ./a.out
root 23461 20819 0 11:10 pts/2 00:00:00 grep --color=auto 23451
process exit
이제 좀비프로세스가 없어졌군요!
왜 그럴까요??
우선 부모프로세스는 시그널을 등록합니다. 핸들러는 childSignal입니다.
자식 프로세스가 종료가 되면 시그널 번호 SIGCHLD로 부모프로세스에 시그널을 보냅니다.
그렇다면 그 순간 시그널 핸들러가 동작하죠. 그렇다면 부모프로세스는 childSignal함수안에 wait을 호출합니다.
그러므로 종료상태를 받을 수 있게 되고 자식 프로세스는 좀비프로세스가 되지 않습니다.
아차, sleep함수는 일정 시간 동안 시그널을 기다립니다. 그 시간안에 시그널을 받는 다면 sleep은 동작을 멈추게 됩니다.
왜 2번 sleep을 호출하는지 알겠죠? 바로 ps를 입력할 시간을 확보하기 위해서 입니다.
자식 프로세스의 종료 상태 알아오기
이번에는 wait에 인자를 이용하여 자식 프로세스의 종지 상태를 알아보도록 합시다. 종지상태를 조사하는 매크로는 표에 나와있습니다.
매크로
설명
WIFEXITED(status)
자식프로세스가 정상적으로 종료되었으면 true를 반환합니다. 이때 자식이 exit, _exit, _Exit으로 넘겨준 인수의 하위 8비트를 얻을 수도 있습니다. 그럴때 WEXITSTATUS(status)를 사용하세요.
WIFSIGNALED(status)
만일 자식프로세스가 신호를 받아 비정상적으로 종료되었다면 참을 반환합니다. 이때 신호번호를 알려면 WTERMSIG(status)를 이용하여 알 수 있습니다. WCOREDUMP(status) 매크로를 지원하는 시스템은 코어덤프를 생성했는지도 알 수 있습니다.
WIFSTOPPED(status)
자식 프로세스가 정지된 상태라면 참을 반환합니다. 이때 정지를 유발한 신호는 WSTOPSIG(status)를 통해 알아낼 수 있습니다.
WIFCONTINUED(status)
만일 자식이 작업 제어 정지 이후 재개되었으면 참을 반환합니다.
다음은 위의 매크로를 사용한 예제입니다.
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
void pr_exit(int status){
if(WIFEXITED(status))
printf("normal termination, exit status = %d\n",WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("abnormal termination, signal number =%d%s\n",
WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? " (core file generated)": "");
#else
"");
#endif
else if(WIFSTOPPED(status))
printf("child stopped, signal number = %d\n",
WSTOPSIG(status));
}
int main(void){
pid_t pid;
int status;
if((pid = fork()) < 0){
printf("[1] fork error\n");
exit(0);
} else if(pid == 0) {
// 자식프로세스 곧장 7 반환하며 종료
exit(7);
}
//부모 프로세스에서 자식 상태 점검
if(wait(&status) != pid){
printf("[1] wait error\n");
exit(0);
}
//자식의 종료 상태 출력
pr_exit(status);
if((pid = fork()) < 0){
printf("[2] fork error\n");
exit(0);
} else if(pid == 0) {
//자식 abort signal 발생하며 종료, 신호번호 6
abort();
}
//부모 프로세스에서 자식 상태 점검
if(wait(&status) != pid){
printf("[2] wait error\n");
exit(0);
}
//자식의 종료 상태 출력
pr_exit(status);
return 0;
}
컴파일 후 실행해보면 자식 프로세스가 어떻게 종료가 되었는지 알 수 있습니다.
# ./a.out
normal termination, exit status = 7
abnormal termination, signal number =6 (core file generated)
자식 프로세스가 exit(7)로 종료되었음을 부모 프로세스는 WEXITSTATUS매크로로 알 수 가 있네요.
이제 fork와 wait을 사용하는 방법을 아시겠나요?
하지만 wait에는 문제점이 존재하는데요. 그 문제점을 보완하는 함수 waitpid가 존재합니다.