728x90
반응형
 

C기반 I/O Multiprocessing - 8. 드디어 올리는 멀티 프로세싱

진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다.. 포스팅 순서가 사실 멀티 프로세싱이 먼저이지만, 게임 서버 개발에 사용한 것은 멀티 플렉싱

typingdog.tistory.com

지난 번 포스팅에 이어서 작성한다.

fork 함수를 통해서 프로세스의 복사본을 생성할 때는 꼭 중요한 규칙이 있다.

자식 프로세스가 종료되면 이 자식 프로세스의 반환 값을 부모 프로세스를 반환 받아야만
자식 프로세스가 메모리 상에서 아예 소멸될 수 있다.

그러니까 자식이 행한 모든 행위는 부모가 책임져야 한다 와 비슷한 이야기인 것이다...ㅋㅋㅋㅋ 이 때, 반환이라함은 return이나 exit(n) 함수를 자식 프로세스에서 실행하여 반환하는 것을 의미한다.

좀비 프로세스의 등장

그렇다면 이러한 규칙을 어기게 된다면 어떻게 될까? 그것은 다음의 코드를 보면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <unistd.h>
 
int main(void)
{
    pid_t pid;
 
    printf("fork() 함수를 실행!!\n");
    pid = fork(); // 프로세스 복사
 
    if(pid == 0// 자식 프로세스 분기
    {
        printf("자식 프로세스 실행 흐름입니다[pid:%d]\n", pid);
    }
    else // 부모 프로세스 분기
    {
        printf("부모 프로세스 실행 흐름입니다[pid:%d]\n", pid);
        sleep(30);
        // 부모 프로세스는 자식 프로세스와 달리 30초 동안 무언가 더 이것 저것 실행하다가 넘어감.
    }
 
    if(pid == 0// 자식은 그냥 끝나버리지만 운영체제에 의해서 잡힘.
        printf("자식 프로세스 실행이 종료 됩니다.[pid:%d]\n", pid);
    else
        printf("부모 프로세스 실행이 종료 됩니다.[pid:%d]\n", pid);
 
    return 0;
}
cs

좀비 프로세스란? 메모리 상에는 있지만 아무 기능을 못하는 상태로 자원만 할당하고 있는 채로 있는 상태를 이야기 한다. 즉, 말 그대로 좀비인 프로그램인 것이다.

지금 부모가 낳은 자식인 자식 프로세스가 종료를 했지만 종료하지 못하고, 좀비 형태로 남아있다는데!! 이게 무슨 말인가? 왜 이런 현상이 발생했는지 다음의 그림을 보면 된다.

번호 순서대로 읽으면 된다.

결국, OS 개입과 5번 처럼 부모의 OS에게로 요청이 없기 때문에 자식은 좀비 형태로 살다가 죽게되는 것이다ㅠㅠ. 

좀비 프로세스의 해결방안

그래서 이러한 좀비 프로세스 문제를 해결하기 위해서 약 3가지 정도의 해결 방안들이 제시된다. 이러한 방법들의 근본은 자식 프로세스의 반환 값을 부모 프로세스에게 전달되도록 해주는 과정들이라는 것이 공통 근본이다.

1. wait() : wait 함수를 실행 함과 동시에 현재 메모리 상에 존재하는 종료 '처리' 된 자식 프로세스를 탐지하여 순서대로 반환 값을 얻어내고 종료 '처리'된 자식 프로세스를 소멸시킨다.

단, wait() 함수는 블로킹이다 ㄷㄷ. 즉, 종료 처리된 자식 프로세스가 없다면 부모 프로세스의 실행 흐름이 멈추고 대기한다는 소리이다. 매우 치명적이다 .. ㅋㅋ 

주석을 조금 상세히 적어놨다. 읽으면 될 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
 
int main(int argc, char *argv[])
{
    int status;
    pid_t pid = fork(); // A 부모 - B 자식으로 분기 됨.
 
    if(pid == 0// B 자식 분기
    {
        return 3// B 자식은 3 값을 반환하여 종료 처리. 이 때부터 B는 종료 처리된 자식 프로세스이다.
    }
    else // A 부모 분기
    {
        printf("A 부모의 실행 흐름 - B 자식 프로세스의 PID : %d \n", pid); // B 자식의 pid가 호출된다.
 
        pid = fork(); // 한번더 프로세스를 복사하여 A 부모 - C 자식으로 분기 됨.
        if(pid == 0// C 자식 분기
        {
            exit(7); // C 자식은 7 값을 반환하여 종료 처리. 이 때부터 C는 종료 처리된 자식 프로세스이다.
        }
        else
        {
            printf("A 부모의 실행 흐름 - C 자식 프로세스의 PID : %d \n", pid); // B 자식의 pid가 호출된다.
 
            // 먼저 종료 처리된 B 자식 프로세스에 대해서
            wait(&status); // wait 함수 호출함으로써 좀비 프로세스인 B가 반환 값 회수와 동시에 소멸. status에 여러 값이 등록되며
            // 이 때 만약 종료 처리된 자식 프로세스가 하나도 없다면, 위의 라인에서 블로킹 된다.
            if(WIFEXITED(status)) // WIFEXITED 매크로 함수로 정상 종료 여부를 확인하고,
                printf("B 자식 프로세스가 반환한 값 : %d \n", WEXITSTATUS(status)); // WEXITSTATUS 매크로 함수로 전달된 반환 값을 받아옴.
 
            // 그 다음 종료 처리된 C 자식에 대해서
            wait(&status); // wait 함수 호출함으로써 좀비 프로세스인 C가 반환 값 회수와 동시에 소멸. status에 여러 값이 등록되며
            // 이 때 만약 종료 처리된 자식 프로세스가 하나도 없다면, 위의 라인에서 블로킹 된다.
            if(WIFEXITED(status)) // WIFEXITED 매크로 함수로 정상 종료 여부를 확인하고,
                printf("C 자식 프로세스가 반환한 값 : %d \n", WEXITSTATUS(status)); // WEXITSTATUS 매크로 함수로 전달된 반환 값을 받아옴.
 
            sleep(30); // ps -aux 로 확인하기 위해서 부모 프로세스가 종료되지 않도록 잡아둔다.
        }
    }
}
cs

2. waitpid() : waitpid 함수는 wait과 본질적인 역할은 동일하나, 블로킹이 없다!!!

역시 마찬가지로 주석을 읽으면 이해에는 문제가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
 
int main(int argc, char * argv[])
{
    int status;
    pid_t pid = fork(); // 부모 A 와 자식 B로 분기
 
    if(pid == 0// B 자식 분기
    {
        sleep(15); // 15초 뒤에 종료되도록 잡아둔다. -> 늦게 종료 처리를 하기 위해서
        return 24;
    }
    else // 부모라면
    {
        while(!waitpid(-1&status, WNOHANG))
            // 종료된 자식 프로세스가 없으면 0을 반환한다. -> 0을 반환하지 않을 때까지 반복.
            // WNOHANG 상수로 인자가 전달되는 경우, 종료 처리된 자식 프로세스가 존재하지 않다면 0을 리턴한다.
        {
            // 여기 들어왔다는 소리는 wait 처럼 블로킹 되지 않고 다른 작업을 할 수 있다는 뜻이다.
            sleep(3); 
            puts("sleep 3sec.");
        }
        if(WIFEXITED(status))
            printf("A 부모의 실행 흐름 - B 자식 프로세스의 PID : %d \n", WEXITSTATUS(status));
    }
    return 0;
}
cs

3. signal(), sigaction()

이전의 방법들은 부모 프로세스에서 자식 프로세스가 종료 처리가 될 때를 대비해서 꼭 wait 함수나, waitpid 함수를 통해서 종료 처리된 자식 프로세스가 존재하는지 탐색하는 과정을 거쳤다.

그런데, 자식 프로세스가 언제 종료될지를 모르기 때문에, wait이나 waitpid 류의 함수를 주기적으로 계속 실행시켜서 확인을 해야한다. 이는 매우 비효율적이다.

그래서 사람들은 해결책으로 이러한 생각을 한다.


1) 아니 그러면 운영체제가 자식 프로세스가 종료 처리되는 걸 뻔히 알고 있으면서
말도 안 해주는게, 이게 사람 새끼냐?( 팩트> os는 사람 아님 )

2) 그러지말고, 자식 프로세스가 종료 처리되는걸 운영체제는 알고 있으니,
운영체제는 자식 프로세스가 종료 처리되는 순간이 오면, 그걸 부모 프로세스에게 알려주자!

3) 이 때 부모 프로세스는 자식 프로세스의 반환 값을 회수 처리하는 로직을 담은 함수를 만들어서
운영체제에게 등록해놓아라.

4) 그러면 운영체제가 그 알려주는 방법으로 부모 프로세스가 등록해놓은 함수를 실행하는 것으로 하면 되지 않겠냐?


기가 막히고 참된 효율적인 대화이다..

그래서 등장한 것이 signal(), sigaction() 이 함수들인데, 같은 역할을 하지만 signal 함수는 유닉스 별로 다르기 때문에 사용되지 않으며 호환을 위해서만 남겨져 있다. 그래서 sigaction() 이라는 함수를 통해 시그널을 등록하는 과정을 아래에 예제로 보일 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
 
void GetChildReturnValue(int sig)
{
    int status;
    pid_t pid = waitpid(-1&status, WNOHANG);
    if(WIFEXITED(status))
    {
        printf("종료 처리된 자식 프로세스가 소멸되었습니다.[pid:%d][return:%d]\n", pid, WEXITSTATUS(status));
    }
    return;
}
 
int main(int argc, char * argv[])
{
    pid_t pid;
 
    struct sigaction act;
 
    act.sa_handler = GetChildReturnValue; // 운영체제가 호출할 함수를 등록한다.
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, 0);
    // 첫 번째 인자 :
    // SIGALRM(alarm 함수를 통해 등록된 시간이 다 지난 상황)
    // SIGINT(Ctrl+c 조합 키가 눌린 상황)
    // SIGCHLD(자식 프로세스가 종료된 상황)
    // 두 번째 인자 :
    // 호출될 함수와 설정이 등록되어 있는 변수의 주소 등록
    // 세 번쨰 인자 :
    // 이전에 등록되었던 시그널 핸들러의 함수 포인터를 얻는데 사용되는 인자이고, 보통 필요 없으므로 0을 전달한다.
 
    pid = fork(); // 부모 A 와 자식 B로 분기
 
    if(pid == 0// B 자식 분기
    {
        printf("B 자식 프로세스가 복사 생성되었습니다 [pid:%d]\n", pid);
        sleep(5); // 15초 뒤에 종료되도록 잡아둔다. -> 늦게 종료 처리를 하기 위해서
        printf("B 자식 프로세스가 종료 처리됩니다.\n");
        return 24;
    }
    else // 부모라면
    {
        int i = 0;
        for(i=0;i<10;i++)
        {
            printf("%d\n",i);
            sleep(1);
        }
        printf("부모 프로세스가 종료됩니다.\n");
    }
    return 0;
}
 
cs

자식 프로세스가 종료되고 바로 소멸까지 이어지는 모습을 확인할 수 있다.

비로소 좀비 프로세스 상태를 막을 수 있는 방법들을 살펴봤다.

728x90
반응형

+ Recent posts