더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

프로세스(process)

프로세스는 간단히 말하자면 실행 중인 프로그램을 의미합니다. 아마 여러분들은 컴퓨터를 하면서 아주 빈번하게 듣는 용어이기도 합니다.

실행 중인 프로그램이라??

컴퓨터에서 말하는 실행 중인 프로그램이라는 건 저장공간에 있는 실행 파일이 메모리를 할당받아 명령어를 수행한다는 것을 의미합니다.

우리가 예를 들어 한글이라는 프로그램을 설치했다고 가정합시다. 아직은 더블클릭으로 프로그램을 실행하지 않았으므로 프로세스가 아닙니다. 설치 후 보고서를 작성하기 위해 프로그램을 실행한다면 그 순한 한글이라는 프로세스가 생성되는 것입니다.

프로세스에서 프로세스를 생성할 수도 있습니다. 이 경우 생성한 프로세스는 부모 프로세스, 생성당한 프로세스를 자식프로세스라고 합니다.

 

 

 

코드 영역(혹은 텍스트 영역이라고도 함) : 프로세스의 명령어가 존재합니다. CPU는 이 영역에서 명령을 실행합니다. 위 그림에서는 부모 자식 프로세스간 별개로 텍스트 영역이 존재하는 것처럼 보이지만 실제로 코드영역은 프로 자식 프로세스간 공유가 됩니다.

 

데이터 영역 : 전역 변수와 정적 변수가 존재합니다. 프로그램이 실행하는 동시에 할당되고 종료시 소멸됩니다.

 

힙 영역 : 동적할당에 필요한 메모리입니다. 프로그램 종료시 해제되지 않고 직접 해제해 주어야 합니다. C언어에서는 free를 사용해서 해제하게 됩니다. 

 

스택 영역 : 함수 호출 시 매개변수, 지역변수가 이 곳에 자리잡게 됩니다. 함수가 종료하게 되면 해제됩니다.

 

자식 프로세스를 생성하는 시스템 콜이 fork라고 합니다. fork는 자기 자신을 복사합니다만 결국 다른 별도의 메모리를 갖게 됩니다. 그래서 자식 프로세스에서 변수의 값을 바꾸어도 부모 프로세스에 영향을 주지 않게 되죠.

 

자식 프로세스 생성(fork)

프로세스를 하나 생성하면서 특징을 같이 이야기 해봅시다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int g_var = 1; 
char buf[] = "a write to stdout\n"; 

int main(){ 
        int l_var; 
        pid_t pid; 
        l_var = 10; 

        if(write(STDOUT_FILENO,buf,sizeof(buf)-1) != sizeof(buf)-1){ 
                printf("write error\n"); exit(0); 
        } 

        printf("fork start!\n"); 

        //fflush(stdout); 
        if((pid = fork())<0){ 
                printf("fork error\n"); 
                exit(0); 
        } else if(pid == 0){ 
                //child 
                g_var++; 
                l_var++; 
        } else { 
                //parent sleep(3); 

        } 

        printf("pid = %ld, g_var = %d, l_var = %d\n",
                        (long)getpid(),g_var,l_var); 

        exit(0); 
}
 
이제 컴파일 후 실행시켜보도록 하지요.
 # gcc fork.c 
 # ./a.out
 a write to stdout
 fork start!
 pid = 83929, g_var = 2, l_var = 11
 pid = 83928, g_var = 1, l_var = 10

 

위 코드를 설명하자면 fork를 통해서 자식 프로세스를 생성하게 됩니다. fork가 정상적으로 호출이되면 자식 프로세스는 0이 반환되고 부모 프로세스에게는 자식의 프로세스 id가 반환됩니다. 

자식 프로세스는 변수들의 값을 변경시키고 있습니다. 그 결과는 위와 같습니다.

 

위의 실행 결과를 파일로 저장해볼까요?

 # ./a.out > fork.out
 # cat fork.out
 a write to stdout
 fork start!
 pid = 83960, glob = 2, var = 11
 fork start!
 pid = 83959, glob = 1, var = 10

자세히보면 fork_start!라는 문자열이 두번 출력되고 있네요. 이같은 결과가 우리에게 시사하는 바는 자식 프로세스는 부모 프로세스의 버퍼까지 복사한다는 사실입니다.

printf는 내부적으로 write 시스템 콜을 사용합니다. printf는 write를 최소한으로 그리고 효율적으로 사용하기 위해 내부적으로 버퍼를 사용하고 있습니다.

터미널 콘솔에 출력할때는 l줄 단위 버퍼링 방식을 사용합니다. 이 말은 줄이 바뀌면(엔터) 화면에 출력한다는 것입니다. 이와 다르게 파일에 기록할때는 full 버퍼링 방식을 사용합니다. 이 방식은 버퍼가 전부 채워져야 파일에 기록하는 버퍼링 방식입니다.

위의 테스트 결과에서 내용을 파일(fork.out)에 저장하는 것이므로 full buffering 방식을 사용하며 이때 버퍼가 다 차지 않았고 이 후에 자식 프로세스를 생성했습니다.

자식 프로세스는 부포 프로세스의 버퍼까지 전부 복제하였고 프로그램이 종료가 될때 버퍼를 비우며 종료가 됩니다.

 

위의 fflush(stdout)을 주석해제하고 다시 실행해보시기 바랍니다. fork전에 버퍼를 다 방출하기 때문에 fork_start!라는 문자열이 한번만 출력됩니다.
 

고아 프로세스

프로세스를 생성하기만 하면 다가 아닙니다. 왜 그런지 실행과 결과를 보면서 이야기 하도록 하지요.

#include <stdio.h> 
#include <unistd.h> 
#include <stdlib.h> 

int main(){ 
        int pid; 

        if((pid=fork()) == 0){ 
                sleep(1); 
                printf("\t [child process %d] created from %d\n", getpid(),getppid());
                exit(0); 
        } 

        printf("[parent process %d] create %d process, ppid:%d\n", getpid(),pid,getppid()); 

}

getpid : 현재 프로세스의 pid를 반환합니다. 

getppid : 부모의 프로세스 id를 반환하지요.

 

이 코드의 결과를 예상해보세요.

자식 프로세스에서는 부모의 프로세스 ID를 출력하니까 부모의 getpid와 같을 겁니다. 그럴까요??

 

[parent process 21162] create 21163 process, ppid:20819

[child process 21163] created from 1

 

????

우리의 예상과는 다른데요??

분명 부모프로세스의 pid는 21162인데 자식 프로세스의 부모프로세스 pid가 쌩뚱맞은 1입니다.

 

왜 그럴까요??

 

우선 이 코드는 문제가 좀 있는 코드입니다. 부모프로세스가 먼저 종료되었기 때문입니다.  자식이 먼저 종료된다는 사실을 확보하기 위해 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가 존재합니다. 

아래의 포스팅을 참고하세요.

https://reakwon.tistory.com/99

 

반응형
블로그 이미지

REAKWON

와나진짜

,