반응형
728x90
반응형
 

C기반 I/O Multithreading - 12. 멀티 프로세싱? 멀티 쓰레딩?

멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거리도 만만치 않기 때문에 좀 상세히 볼 필요가 있다ㅋ

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 13. 쓰레드의 생성과 소멸까지

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 15. 뮤텍스와 세마포어(1)

C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점 C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사

typingdog.tistory.com

 

 

C기반 I/O Multithreading - 16. 뮤텍스와 세마포어(2)

C기반 I/O Multithreading - 15. 뮤텍스와 세마포어(1) C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점 C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프..

typingdog.tistory.com

멀티 스레드와 이전 포스팅 시리즈(?)들이다.

이번에는 멀티 스레드를 기반으로 하며 Mutex 쓰레드 동기화 방법이 사용된 서버를 작성해볼 것이다.

위와 같은 쓰레드들 간에 공유가 가능한 데이터 영역의 변수들에 동기화 되지 않은 접근은 허용하지 않는다

마찬가지로 데이터 메모리 영역에 접근하는 부분은 모두 동기화 시킨다.

코드 및 실행 결과

 

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
 
#include <pthread.h>
 
#define BUFSIZE 30
#define MAXCLIENTS 30
 
pthread_mutex_t mutex;
int client[MAXCLIENTS] = { 0, };
int now = 0;
 
void * HandleClient(void * arg);
void ErrorMsg(const char *msg);
 
int main(void)
{
    pthread_t tid;
 
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
 
    socklen_t addr_size;
 
 
    pthread_mutex_init(&mutex, NULL); // 뮤텍스 id 생성
 
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    if(serv_sock == -1)
        ErrorMsg("socket() error");
 
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(7010);
 
    if(bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        ErrorMsg("bind() error");
 
    if(listen(serv_sock, 5== -1)
        ErrorMsg("listen() error");
 
    while(1)
    {
        addr_size = sizeof(clnt_addr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
        if(clnt_sock == -1)
            continue;
 
 
        pthread_mutex_lock(&mutex);
        client[now++= clnt_sock;
        pthread_mutex_unlock(&mutex);
 
        pthread_create(&tid, NULL, HandleClient, (void*)&clnt_sock);
        pthread_detach(tid);
 
        printf("클라이언트 연결 [%d]\n", clnt_sock);
    }
    return 0;
}
 
void * HandleClient(void * arg)
{
    int clnt_sock = *((int*)arg);
    int strLen, state;
    char buf[BUFSIZE];
    int i = 0;
 
    while((strLen=read(clnt_sock, buf, sizeof(buf)))!=0)
    {
        pthread_mutex_lock(&mutex);
        for (i = 0; i < now; i++)
            write(client[i], buf, strLen);
        pthread_mutex_unlock(&mutex);
    }
 
    pthread_mutex_lock(&mutex);
    for (i = 0; i < now; i++)
    {
        if(clnt_sock == client[i])
        {
            printf("클라이언트 연결 종료 [%d]\n", client[i]);
            for(;i<now-1;i++)
                client[i] = client[i+1];
            break;
        }
    }
    now--;
    pthread_mutex_unlock(&mutex);
    close(clnt_sock);
    return NULL;
}
 
void ErrorMsg(const char *msg)
{
    fputs(msg, stderr);
    fputc('\n',stderr);
    exit(1);
}
 
cs

문제 없이 통신이 잘 되는 모습이다. 접속한 클라이언트의 수가 너무 적어서 사실 확실히 확인한 케이스는 절대 아니지만 코드적으로만 보아도 의미가 있기 때문에 이것으로 마무리 짓겠다.

728x90
반응형
728x90
반응형
 

C기반 I/O Multithreading - 15. 뮤텍스와 세마포어(1)

C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점 C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사

typingdog.tistory.com

쓰레드를 동기화 하는 방법으로 뮤텍스에 이어서 세마포어다! 

좀 더 구체적으로 정확히 이야기하자면 바이너리 세마포어이다.

바이너리 세마포어란

이 바이너리 세마포어는 0과 1을 사용하여 실행 순서를 컨트롤 한다. 다음은 바이너리 세마포어가 임계영역에 대한 접근 순서를 동기화하는 과정을 적어놨다.

쓰레드 A가 먼저 sem_wait() 함수를 호출하여 임계 영역에 접근한 상태에서 쓰레드 B가 sem_wait() 함수를 호출하여 블로킹에 걸렸다가, 쓰레드 A가 sem_post() 함수를 호출하여 쓰레드 B가 sem_wait() 함수 처리 가능 상태가 되어서 이를 호출하고, 쓰레드 B가 임계 영역에 접근한 뒤, sem_post() 함수를 호출하여 쓰레드 B가 임계 영역에서 빠져나오는 과정을 설명한 것이다.

좀 설명이 복잡했지만 다음과 같다.

위에서 나온 함수들의 원형을 살펴보면 다음과 같다.

sem_init, sem_destroy, sem_wait, sem_post

이어서 간단한 예제를 확인하겠다. 지난 예제의 보완이다.

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
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
 
sem_t semaphore;
int common_value = 0;
 
void* t_main_plus(void *arg);
void* t_main_minus(void *arg);
 
int main(void)
{
    pthread_t tid1, tid2;
 
    sem_init(&semaphore, 01);
    // 세 번째 인자를 0으로 넣으면 sem_wait을 먼저 호출한 쓰레드에서 블로킹이 발생하는 것을 확인할 수 있다.
 
    pthread_create(&tid1, NULL, t_main_plus, NULL); // 쓰레드 생성
    pthread_create(&tid2, NULL, t_main_minus, NULL); // 쓰레드 생성
 
    pthread_detach(tid1); // tid1 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
    pthread_detach(tid2); // tid2 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
 
    sleep(7); // 종료되지 않도록 대기.
    sem_destroy(&semaphore);
 
    printf("메인함수가 종료됩니다. [common_value의 최종 값 : %d]\n",common_value);
    return 0;
}
 
void* t_main_plus(void *arg)
{
    int i = 0;
 
    printf("t_main_plus 쓰레드가 연산을 시작합니다. \n");
 
    sem_wait(&semaphore);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value+=1;
    sem_post(&semaphore);
 
    printf("t_main_plus 쓰레드가 종료됩니다.\n");
    return NULL;
}
 
 
void* t_main_minus(void *arg)
{
    int i = 0;
 
    printf("t_main_minus 쓰레드가 연산을 시작합니다. \n");
 
    sem_wait(&semaphore);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value-=1;
    sem_post(&semaphore);
 
    printf("t_main_minus 쓰레드가 종료됩니다.\n");
    return NULL;
}
cs

여러 차례 실행해도 common_value 값은 딱 0으로 맞아 떨어진다. 세마포어가 교착 상태도 없이 잘 수행해낸 것으로 확인되는 실행 결과이다. 

이로서 쓰레드 동기화에 관련된 내용은 마무리이고 다음 포스팅에는 뮤텍스를 이용한 서버 프로그램 작성이다.

728x90
반응형
728x90
반응형
 

C기반 I/O Multithreading - 14. 쓰레드의 치명적인 문제점

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거

typingdog.tistory.com

이 포스팅에서는 멀티 쓰레드 기반병렬 처리에서 발생하는 임계 영역과 관련된 문제를 해결하기 위한 방법이다. 위는 그 문제점에 대해서 다루었던 포스팅이다.

멀티 쓰레드 기반의 병렬 처리에서 임계 영역과 관련된 문제를 해결하기 위한 방법으로는

뮤텍스
세마포어

등이 존재한다. 이 방법들은 쓰레드 동기화라고 하는데, 쓰레드의 접근 순서로 인해 발생하는 문제를 해결하기 위한 순서의 동기화를 의미한다. 

이름부터 무슨 세포 이름 같기도 하고, 이상한 인상을 받았다. 그러나 잠시 후에는 진짜 개쩌는 놈들이라는 것을 알게 될 것이다. 작성자는 깔끔하게 문제가 해결되는 것을 보고 감탄을 했었다ㅋㅋㅋㅋ ㄹㅇ

Mutex, 뮤텍스

먼저, 뮤텍스이다. Mutual Exclusion의 줄임말로.

구글 번역기

역시나 쓰레드끼리 공유하는 변수에 동시 접근을 막는 의미로 사용된다. 이걸 정말 어떻게 기록해야할까 고민을 많이 했는데 코드와 함께 비유를 섞어서 하는게 났겠다 싶다.

뮤텍스는 임계 영역을 대상으로 문을 하나 포함한 벽과, 열쇠를 만드는 것으로 비유하면 될 것이다.

이 벽으로 둘러 쌓인 임계 영역에는 딱 한 사람만(하나의 쓰레드 실행 흐름만) 들어갈 수 있으며, 들어갈 때는 꼭 열쇠로 문을 잠그고 나올 때는 열쇠로 문을 열어 놓고 나간다.

근데 이상한 점이 있다. 그림으로만 보면 임계영역이라는 것은 하나인데, t_main_plus 함수와 t_main_minus 함수내에서 서로 다른 코드 영역이다. 

좀 더 정확히 말하자면 벽과 열쇠를 가져다가 사용할 수 있는 권한을 lock과 unlock을 통해 얻고 문을 열고 닫는 것이라고 생각하면 될 것 같다. "쓰레드가 각자 임계 영역에서 계산 및 작업을 할 것인데 벽과 열쇠로 둘러쌓지 않으면 작업을 못한다"라고 규정해버리면 자연스럽게 대기를 할 것이다. 

그런데 서로 다른 라인의 코드 영역이긴 하지만, 결국에는 접근하여 조작하는 변수는 같기 때문에 그림으로는 하나의 영역에 들어가는 것처럼 표현한 것이다. 조금 유연하게 이해를 해야할 필요가 있는 것 같다.. 아니면 조금 더 코드의 레벨로..? 

아무튼 그래서 뮤텍스 관련 함수를 한 번에 기록하겠다. 별로 필요로 하는 인자가 없기 때문에 간단하게 정리한다.

pthread_mutex_init, pthread_mutex_destroy, pthread_mutex_lock, pthread_mutex_unlock

이어서 간단한 예제를 확인하겠다. 지난 예제의 보완이다.

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
58
59
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
pthread_mutex_t mutex;
int common_value = 0;
 
void* t_main_plus(void *arg);
void* t_main_minus(void *arg);
 
int main(void)
{
    pthread_t tid1, tid2;
 
    pthread_mutex_init(&mutex, NULL);
 
    pthread_create(&tid1, NULL, t_main_plus, NULL); // 쓰레드 생성
    pthread_create(&tid2, NULL, t_main_minus, NULL); // 쓰레드 생성
 
    pthread_detach(tid1); // tid1 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
    pthread_detach(tid2); // tid2 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
 
    sleep(7); // 종료되지 않도록 대기.
    pthread_mutex_destroy(&mutex);
    printf("메인함수가 종료됩니다. [common_value의 최종 값 : %d]\n",common_value);
    return 0;
}
 
void* t_main_plus(void *arg)
{
    int i = 0;
 
    printf("t_main_plus 쓰레드가 연산을 시작합니다. \n");
 
    pthread_mutex_lock(&mutex);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value+=1;
    pthread_mutex_unlock(&mutex);
 
    printf("t_main_plus 쓰레드가 종료됩니다.\n");
    return NULL;
}
 
 
void* t_main_minus(void *arg)
{
    int i = 0;
 
    printf("t_main_minus 쓰레드가 연산을 시작합니다. \n");
 
    pthread_mutex_lock(&mutex);
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value-=1;
    pthread_mutex_unlock(&mutex);
 
    printf("t_main_minus 쓰레드가 종료됩니다.\n");
    return NULL;
}
cs

몇 번을 실행해도 백만씩 더하고, 빼기가 정확하게 이루어져 0을 이루는 모습이다. ㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷ

교착 상태, 데드락

이렇게 lock / unlock 기법을 사용하는 곳에 꼭 동반하는 문제가 무어냐면 교착 상태라는 데드락 상태이다.

그림의 숫자 순서대로 읽으면 되고, 이를 꼭 주의해야 한다. 1번의 경우는 생각보다 잘 나올 수 있다. lock을 건 쓰레드가 뻑이나거나 사망해버릴 경우 문이 잠긴 채로 unlock이 호출되지 않고 교착 상태에 들어가는데 이런 경우는 답이 없다 ㅋㅋㅋㅋ 디비에서도 그렇고 운영체제에서도 그렇고 비슷한 일들이 일어난다.

다음은 세마포어이다 (이진값을 이용한 세마포어)

728x90
반응형
728x90
반응형
 

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩?

멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거리도 만만치 않기 때문에 좀 상세히 볼 필요가 있다ㅋ

typingdog.tistory.com

 

 

C기반 I/O MultiThreading - 13. 쓰레드의 생성과 소멸까지

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩? 멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거

typingdog.tistory.com

쓰레드 이전 설명들이다. ㅋㅋ

이렇게 프로세스의 자식 프로세스의 반환 회수, 프로세스끼리 통신의 까다로움, 많은 메모리 자원 소모, 부담스러운 컨텍스트 스위칭의 오버 헤드 문제들이 거의 해결이 되는 전지전능한 쓰레드일 것이라고 생각했다.

그러나 언제나 그렇듯, 하나가 되면 하나가 문제가 생기는게 이치이듯이 쓰레드 또한 치명적인 문제가 있었다.

통신을 손쉽게 하기 위해서 힙이나 데이터 등의 공유 메모리 영역을 두었다. 아주 잘한 것이다. 그런데 이 공유 메모리 영역에서 여러 다른 기능을 하는 쓰레드들이 동일한 변수 등에 접근하여 CRUD가 일어나는 경우 문제가 생기는 것이다! 왜냐하면 다음과 같은 상황인 것이다.

후 그리느라 너무 힘들었다. 위와 같은 상황을 이야기하는 것이다. 순서대로 읽으면 된다. 그림에서 나타내는 바를 설명하자면 같은 메모리 영역을 동시에 혹은 순서의 정함이 없이 읽고 쓰는 경우, 해당 메모리 영역의 값에 대한 기대값을 보장할 수 없게 된다. 왜 그럴까? 이 또한 그림으로 준비해보았다. 

위의 순서대로 읽으면 된다.

결국, 쓰레드를 통한 병렬 처리가 진행될 때에는 프로세스와는 달리 공통적인 메모리 영역이 존재한다. 그래서 여기서 선언된 공통적인 변수에 동일한 임계 영역을 갖는 쓰레드들이 순서의 정함이 없거나, 동시에 접근을 하는 경우, 해당 변수로부터의 기대값을 보장할 수 없는 것이다. 다음 예를 통해서 확인해보겠다. 

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
int common_value = 0;
 
void* t_main_plus(void *arg);
void* t_main_minus(void *arg);
 
int main(void)
{
    pthread_t tid1, tid2;
 
    pthread_create(&tid1, NULL, t_main_plus, NULL); // 쓰레드 생성
    pthread_create(&tid2, NULL, t_main_minus, NULL); // 쓰레드 생성
 
    pthread_detach(tid1); // tid1 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
    pthread_detach(tid2); // tid2 에 해당하는 쓰레드가 종료됨과 동시에 소멸.
 
    sleep(7); // 종료되지 않도록 대기.
    printf("메인함수가 종료됩니다. [common_value의 최종 값 : %d]\n",common_value);
    return 0;
}
 
void* t_main_plus(void *arg)
{
    int i = 0;
 
    printf("t_main_plus 쓰레드가 연산을 시작합니다. \n");
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value+=1;
 
    printf("t_main_plus 쓰레드가 종료됩니다.\n");
    return NULL;
}
 
 
void* t_main_minus(void *arg)
{
    int i = 0;
 
    printf("t_main_minus 쓰레드가 연산을 시작합니다. \n");
    for(i=0; i<1000000; i++// for을 중복해서 쓴 것은 100번 type을 검사하는 것보단 났다고 생각.
        common_value-=1;
 
    printf("t_main_minus 쓰레드가 종료됩니다.\n");
    return NULL;
}
cs

서로 다른 연산을 하는 두 t_main_plus, t_main_minus 함수가 존재한다. 그리고 이 함수들은 전역 변수인 common_value 변수의 값을 +1, -1 조작하는 임계 영역을 가지고 있다.

그래서 각각 백만 번씩 1을 더하거나 빼도록 쓰레드를 동시에 실행하고 있는 상황이다.

t_main_plus에서는 백만번을 더하고, t_main_minus에서는 백만번을 빼니까 결국 둘이 상쇄되서 0이 되는 것이 정상 아닌가? 싶지만 결과를 확인해보자.

첫 번째 실행
두 번째 실행
세 번째 실행

아니 값이 이렇게도 다를까 싶다. 임계 영역으로 인해 기대 값이 전혀 보장되고 있지 않는 상태이다.

이 정도 예제이면 확실히 임계영역, 즉 쓰레드의 문제가 어떤 것인지 확실하게 이해가 될 것이라고 생각한다. 다음 포스팅에서는 이를 해결하는 방법에 대해서 올릴 것이다.

근데 사실 이를 해결하려면 방법은 하나다. 동시에 접근을 막거나 연산의 순서를 직접 정하는 것 뿐이다. 이 기본적인 해결 방법을 토대로 여러 방법들이 등장할 것이다.

728x90
반응형
728x90
반응형
 

C기반 I/O MultiThreading - 12. 멀티 프로세싱? 멀티 쓰레딩?

멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거리도 만만치 않기 때문에 좀 상세히 볼 필요가 있다ㅋ

typingdog.tistory.com

위의 포스팅에 이어서 작성하는 글이다.

코드를 통해 쓰레드를 생성부터 소멸까지 한번 확인해 볼 것이다.

쓰레드의 생성

쓰레드의 생성하는 함수에 대한 설명이며, 위 함수를 통해 쓰레드를 생성하는 예제를 실행해볼 것이다.

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
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
 
void* t_main(void *arg);
 
int main(void)
{
    pthread_t tid;
    int t_param = 8;
 
    if(pthread_create(&tid, NULL, t_main, (void *)&t_param)!=0// 3,4번째의 인자들의 자료형을 잘 맞춰주어야 한다.
    {
        printf("반환 값이 0이 아닌 값으로 pthread_create에 실패했습니다.\n");
        return -1;
    }
    sleep(10);
    printf("메인함수가 종료됩니다.\n");
 
    return 0;
}
 
void* t_main(void *arg)
{
    int i;
    int cnt = *((int*)arg);
 
    for(i=0; i<cnt; i++)
    {
        printf("thread_main이 실행중입니다\n");
        sleep(1);
    }
    return NULL;
}
cs

위의 예제를 실행한 결과이다. 메인 함수가 바로 종료되면 프로세스가 종료되기 때문에 그 내부의 실행 흐름인 쓰레드 또한 함께 바로 종료되기 때문에 메인 함수를 종료시키지 않고 붙잡아 둘 필요가 있었던 것이다. 그래서 17번 라인에서 sleep 함수로 10초를 버틴다!

위의 예제에 대한 실행 흐름을 그림으로 나타낸 것이다.

그런데 문제가 있다! 이전에 프로세스 이야기에서도 마찬가지였지만 sleep 함수를 통해서 프로그램의 실행 흐름을 대충 맞춘다는게 이게 말이나 되는 상황인가? 이러한 문제를 해결하기 위한 함수가 존재한다.

쓰레드가 종료할 때까지 대기! : pthread_join 함수의 등장 

첫 번째 인자로 넘어간 tid 값에 해당하는 쓰레드가 종료될 때 까지 main 함수는 대기를 한다!

코드를 보고 확인하겠다.

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
 
void* t_main(void *arg);
 
int main(void)
{
    pthread_t tid;
    int t_param = 8;
    int * return_val = NULL;
 
    if(pthread_create(&tid, NULL, t_main, (void *)&t_param) != 0// 3,4번째의 인자들의 자료형을 잘 맞춰주어야 한다.
    {
        printf("반환 값이 0이 아닌 값으로 pthread_create에 실패했습니다.\n");
        return -1;
    }
 
    if(pthread_join(tid, (void **)&return_val) != 0)
    {
        printf("반환 값이 0이 아닌 값으로 pthread_join에 실패했습니다.\n");
        return -1;
    }
    printf("pthread_join 함수 호출 직 후\n");
    printf("쓰레드 메인 함수에서 반환된 값 : [%d] \n"*return_val);
    printf("메인함수가 종료됩니다.\n");
    free(return_val);
    return 0;
}
 
void* t_main(void *arg)
{
    int i;
    int cnt = *((int*)arg);
    int * thread_return_value = (int *)malloc(sizeof(int));
 
    printf("thread_main이 실행 되었습니다!\n");
    printf("반환 값 계산 후, 잠시 10초 대기하겠습니다ㅋㅋ - main에서 pthread_join의 효과를 보기 위함.\n");
 
    *thread_return_value = 100;
    *thread_return_value = (*thread_return_value) * cnt;
 
    sleep(10);
    return thread_return_value;
}
cs

중간 중간에 설명해야할 부분이 있는 것 같다 ㅋㅋ 포인터들이 좀 많이 깔려있는데 다른 예제들에 비해서. 근데 전혀. 어렵지 않다. 

위 코드에서는 그림에 해당하는 부분으로 설명이 될 것 같다.

다만, 이렇게 더블 포인터까지 써가면서 가리키는 이유는 쓰레드 간의 통신이기 때문에 공유 메모리 영역인 힙 영역의 메모리 공간에 값을 저장하고 또 그것을 외부에서 참조해서 쓰는 것이다. 아래는 실행 결과이다.

아래는 실행 흐름을 그림으로 나타낸 것이다.

 

쓰레드의 소멸

쓰레드는 꼭 직접 소멸을 해줘야한다! 쓰레드의 main 함수(위에서는 t_main)를 반환했다고 자동 소멸이 되는 것이 절대 아니다.

그러면 뭐 closesocket 같은 전용 close 형식의 함수가 존재하는가? 그렇다! 존재한다! 다만 close~ 형태는 아니고 이미 본 것도 존재한다. 바로 아래의 함수들이다.

첫 번째는 이미 봤던 pthread_join 함수이다. 이 함수를 호출하면 해당 tid의 쓰레드가 종료될 때까지 대기할뿐만 아니라 소멸까지도 책임지고 담당한다. 그러나 치명적인 단점은 말 그대로 main 함수를 대기시키는 블로킹에 빠진다는 점.

그래서 나온 것이 다음이다.

이 함수를 사용하면 쓰레드의 소멸까지 담당하여 처리되는데, 중요한 것은 블로킹에 빠지지 않는다는 것이다!

다음 예제를 통해서 보겠다.

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 <pthread.h>
 
void* t_main(void *arg);
 
int main(void)
{
    pthread_t tid;
    int t_param = 8;
 
    int i = 0;
 
    if(pthread_create(&tid, NULL, t_main, (void *)&t_param) != 0// 3,4번째의 인자들의 자료형을 잘 맞춰주어야 한다.
    {
        printf("반환 값이 0이 아닌 값으로 pthread_create에 실패했습니다.\n");
        return -1;
    }
 
    printf("pthread_detach 함수 호출!\n");
    pthread_detach(tid); // tid 에 해당하는 쓰레드가 종료됨과 동시에 소멸시킨다.
 
    for(i=0; i<7; i++)
    {
        sleep(1);
        printf("쓰레드가 이미 종료되었지만, 블로킹 되지 않고 main만의 작업을 진행하다가 \n");
    }
    printf("main만의 작업 루틴 종료.\n");
    printf("메인함수가 종료됩니다.\n");
    return 0;
}
 
void* t_main(void *arg)
{
    int cnt = *((int*)arg);
 
    printf("thread_main이 실행 되었습니다!\n");
    printf("받은 값은 [%d]\n", cnt);
 
    printf("t_main 리턴 직전(종료라고 보면 됨)\n");
    return NULL;
}
cs

아래는 실행 결과로 블로킹이 없음을 확인할 수 있다.

위 예제에 대한 실행 흐름을 그림으로 나타내보았다.

 

쓰레드의 개념부터 생성 및 소멸까지 확인해보았다.

앞으로 남은 포스팅은 임계 영역에 대한 설명과 서버 프로그래밍이다. ㅋㅋ 제일 고비이다 고비..

728x90
반응형
728x90
반응형

멀티 프로세싱에 이어서, 멀티 프로세싱의 단점이 보완되는 멀티 쓰레딩 개념이다. 사실 단점이 보완되기는 하는데 함께 딸려오는 문제 거리도 만만치 않기 때문에 좀 상세히 볼 필요가 있다ㅋㅋ

아래는 멀티 프로세싱에 대한 포스팅의 시작이고, 그 아래에 있는 멀티 프로세싱 포스팅 중 제일 마지막 시리즈(?)에 프로세스 생성과 복사에 대한 개념이 조금 들어간다.

 

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

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

typingdog.tistory.com

 

 

C기반 I/O Multiprocessing - 11. 프로세스 정리 및 IPC

C기반 I/O Multiprocessing - 8. 드디어 올리는 멀티 프로세싱 진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다.. 포스팅 순서가 사실 멀티 프로세싱이 먼

typingdog.tistory.com

링크 올리면서 발견한건데.. 예제 쳐보고가 아니라 예제를 '타이핑'해보고...ㅋㅋㅋㅋ

멀티 프로세스의 특징과 단점

자, 일단은 멀티 프로세싱의 단점을 확인할 필요가 있다. 왜냐하면 멀티 쓰레딩멀티 프로세싱과 유사한 기능을 제공하지만 단점을 보완할 수 있는 개념이기 때문이다. 멀티 프로세스의 장점은 멀티 쓰레딩의 단점을 통해 확인해야하기 때문에 지금 이 포스팅에서는 언급하지 않겠다.

멀티 쓰레드는 겉에서 봤을 때에는 멀티 프로세스와 비스므리한 개념이다.
멀티 프로세스의 특징을 간단하게 다시 살펴보자.

-멀티 프로세스의 특징

1. 프로세스의 복사는 사실 프로세스 생성을 거친다.
2. 프로세스의 복사는 스택, 데이터, 힙 영역이 모두 복사되고, 부모와 자식은 서로 독립적인 메모리 공간을 갖는다.
3. 프로세스의 복사는 파일 디스크립터 복사로 인해 파일 디스크립터와 소켓의 연결 관계를 n:1로 만든다
4. 프로세스의 복사는 부모 프로세스와 자식 프로세스로 나뉘며, 부모의 프로세스가 자식의 프로세스 반환을 꼭 회수해야한다.

말 그대로 복사이기 때문에 위와 같은 현상들이 일어나는데, 이러한 특징들로 인해서 멀티 프로세스의 단점은 다음과 같다. 위의 멀티 프로세스의 특징에 대응되도록 색을 칠했다.

-멀티 프로세스의 단점

1. 프로세스 복사 과정은 시스템에 굉장히 부담스럽다. (프로세스의 생성 자체가 부담스럽다)
2. 프로세스는 서로 별개의 요소이기 때문에 별개의 독립된 메모리 공간을 갖고, 이로 인해서 프로세스 간에 통신이 어렵다 ( 사실 이건 장점일수도?ㅋㅋ )
3. 파일 디스크립터와 소켓의 연결 관계가 n:1일 경우, 소켓에 해당하는 모든 파일 디스크립터를 종료해야 비로소 소켓이 종료된다.
4. 프로세스의 복사는 부모 프로세스와 자식 프로세스로 나뉘는데, 이로 인해서 자식 프로세스의 반환 값 회수가 이루어지지 않으면 좀비 프로세스가 발생한다.
5. CPU가 프로세스 별로 시간을 할당해서 여러 프로세스를 처리하는 시분할 처리를 하기 위해 현재 처리할 프로세스를 메모리에 올리고, 현재 처리하지 않는 프로세스를 메모리에 내리는 작업을 PCB를 이용하여 행하는데 이를 컨텍스트 스위칭이라는 과정이다. 이러한 과정은 하드 디스크 레벨의 I/O 까지 연계되기 때문에 부담이 굉장히 크다.

-멀티 쓰레드는 멀티 프로세스의 어떤 부분들이 어떻게 커버되었을까?

단점이 생각보다 많다ㅋㅋㅋㅋㅋ 멀티 쓰레드는 멀티 프로세스의 단점을 커버하기 위해서 등장했다. ( 물론 전적으로 그렇지는 않겠다만ㅋ... ) 마찬가지로 위의 단점들에 대응되도록 색을 칠했다.

1. 쓰레드는 복사하는 과정이 아니며, 쓰레드의 생성과 쓰레드의 컨텍스트 스위칭은 프로세스의 그것들보다 빠르다.
2. 쓰레드는 메모리 영역을 통째로 복사하지 않고, 일부를 공유하며(힙, 데이터), 병렬 흐름에 필요한 부분(스택)만 독립적으로 갖는다. 그렇기 때문에 공유되는 영역을 통해 쓰레드 간의 통신이 가능하다.
3. 쓰레드의 생성은 프로세스처럼 통째로 복사하는 과정이 아니기 때문에 해당하지 않는다. 프로세스에는 File Descriptor table을 갖는데, 프로세스가 복사되면 이 테이블 자체가 복사된다. 그렇기 때문에 문제가 되는 것이고, 파일 디스크립터의 복사는 단순히 파일 디스크립터 값을 다른 변수로 넘겨주네 마네 하는 차원이 아니라는 소리다.
4. 프로세스처럼 통째로 복사되는 과정이 아니기 때문에 해당하지 않는다.
5. 쓰레드의 컨텍스트 스위칭은 쓰레드 라이브러리에 의해 TCB(Thread Controll Block) 을 참조하여 발생하며, OS 스케줄러에 의해 PCB를 참조하여 컨텍스트 스위칭되는 프로세스의 과정보다는 훨씬 빠르다.

-이런 멀티 프로세스의 단점을 커버하는 멀티 쓰레드(쓰레드)는 무엇인가?

위는 프로세스에 대한 포스팅에서 간단하게 정리한 정보인데, 프로세스와 비교하면서 보면 더 와닿을 것 같아서 다시 올렸다. 

프로세스 관점에서 별도의 실행 흐름을 구성하는 단위.
생성될 때, 프로세스와 마찬가지로 TCB(Thread Controll Block) 라고 하는 쓰레드에 대한 정보 구조가 생성된다.

쓰레드는 프로세스의 일부이며 실행의 단위이기 때문에 TCB에는 PCB만큼의 많은 정보가 저장되지는 않는다.
또한, 프로세스의 일부이기 때문에 프로세스가 할당 받은 메모리의 공간을 공유할 수 있다. (통신에 용이)

-그렇다면 멀티 쓰레드란 무엇일까?

히..힘들었..다..

프로세스 내에 여러 쓰레드를 운용하여 병렬 처리하는 방법을 멀티 쓰레드라고 한다.

개념적인 부분은 이 정도로 마무리 하고 다음 포스팅에서는 쓰레드 생성 및 소멸 등의 과정을 코드로 볼 것이다.

728x90
반응형
728x90
반응형
 

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

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

typingdog.tistory.com

 

C기반 I/O Multiprocessing - 9. 멀티 프로세싱에서 좀비 문제

C기반 I/O Multiprocessing - 8. 드디어 올리는 멀티 프로세싱 진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다.. 포스팅 순서가 사실 멀티 프로세싱이 먼

typingdog.tistory.com

 

C기반 I/O Multiprocessing - 10. 멀티 프로세싱 기반 서버

C기반 I/O Multiprocessing - 8. 드디어 올리는 멀티 프로세싱 진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다.. 포스팅 순서가 사실 멀티 프로세싱이 먼

typingdog.tistory.com

위 링크들에서 볼 수 있듯이 멀티 프로세싱의 주요 맥락을 확인하고, 포스팅하여 기록해보았다. 그런데 말입니다, 부모 프로세스와 자식 프로세스들 간에 통신이 필요한 경우는 어떻게 해야할까?

아니, 사실 프로세스들 간에 통신을 안 하는게 최고라고 나는 생각하지만 또, 그게 아닐 수 있으니 프로세스들 간의 통신 방법에 대해서 공부하고 넘어가는게 났겠다 싶어서 전에 공부를 해 보았다. 

일단, 프로세스들 간의 통신에 대해서 정리하기 전에 멀티 프로세스에 대한 정리를 한번 할 필요가 있다. 왜냐하면 이전 포스팅에서는 코드 구현 관점에서만 설명이 되어서 프로세스에 대한 충분한 설명이 이루어지지 않았다.

멀티 프로세스 개념 간단 정리

살짝 마음에 안들지만..

1. fork 함수를 통해서 프로세스가 복사되는데, 이 때, 복사라는 과정은 사실 새로운 프로세스의 생성 과정을 거친다.( PCB, Process Controll Block 이 함께 생성되어 생성된 프로세스에 대한 정보들을 저장하는 자료구조 블록이며, 컨텍스트 스위칭 때 이 정보를 활용한다)

2. fork 함수를 통해서 프로세스가 복사 시에는 부모 프로세스의 스택, 데이터, 힙 영역이 모두 복사되어 부모와 자식은 완전히 별개의 메모리 공간을 갖으며 내용 또한 그대로 복사된다.

자, 1번과 2번의 특징으로 인해서 프로세스 간에는 아무리 부모와 자식 사이의 관계라고 하더라도 서로 독립적인 메모리 공간을 가지고 있기 때문에 통신하기가 힘들다. 그래서 프로세스 간의 대화 방법으로 나온 것이 IPC이다. 

IPC란? 

IPCInter Process Communication의 줄임말로 프로세스 간의 통신 방법을 의미한다. 

내가 공부한 프로세스 간 통신 방법은 다음 그림으로 설명할 것이다.

이게 바로 파이프를 이용한 프로세스 간의 통신을 간략하게 그림으로 그려본 것이다.

파이프의 생성은 위와 같은 함수로 진행된다. 운영체제에서 생성되는 공간이기 때문에 프로세스들과는 무관한 독립된 공간이고, fork 시 코드에 포함이 되어 있더라도 복제되는 메모리 공간이 아니다.

예제 코드 및 실행 결과

어떻게 데이터를 주고 받는지 부모가 자식에게, 자식이 부모에게 데이터를 주고 받는 경우를 예제로 확인해 볼 것이다.

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 <unistd.h>
 
#define BUFSIZE 40
 
int main(void)
{
    int fds[2];
    pid_t pid;
 
    char from_c_to_p[] = "우리 부모님아!";
    char from_p_to_c[] = "내 자식아!";
 
    char buf[BUFSIZE];
 
    pipe(fds);
    // 파이프 생성 -> 파이프는 fork 에 의해 복사되지 않고, 운영체제에 의해 생성되는 메모리 공간이다.
    // 이 파이프를 이용하여 부모 프로세스와 자식 프로세스가 데이터를 교환할 수 있으며, 이런 통신 방법을 IPC 라고 한다.
    // 인자로 전달되는 fds에 데이터 입력과 출력에 해당하는 파일 디스크립터가 저장이 된다.
 
    // fds[0]에는 출력을 위한 파일 디스크립터가 저장되고, 프로세스는 이 디스크립터를 통해서 값을 가져온다.
    // fds[1]에는 입력을 위한 파일 디스크립터가 저장되고, 프로세스는 이 디스크립터를 통해서 값을 입력한다.
 
    pid = fork();
 
    if(pid == 0)
    {
        write(fds[1], from_c_to_p, sizeof(from_c_to_p));
       sleep(2);
 
        read(fds[0], buf, BUFSIZE);
        printf("부모가 자식에게 보내온 메시지 : [%s]\n", buf);
    }
    else
    {
        read(fds[0], buf, BUFSIZE);
        printf("자식이 부모에게 보내온 메시지 : [%s]\n", buf);
 
        write(fds[1], from_p_to_c, sizeof(from_p_to_c));
        sleep(5);
    }
    return 0;
}
cs

29번 라인과 40번 라인의 sleep 함수에 대한 설명이 필요한 것 같다ㅋㅋㅋ 먼저, 29번 라인은 자식 프로세스에서 실행되는 영역에 sleep 함수를 넣었다. 왜 넣었을까?

먼저, 29번 라인에 sleep 함수가 없다고 생각하고, 28번 라인에서 입력 파일 디스크립터를 이용하여 값을 파이프에 집어 넣으 후, 31번 라인에서 바로 출력 파일 디스크립터를 이용하여 값을 파이프에서 빼내면 자식 프로세스 본인이 방금 넣은 값이 나올 확률이 높다. 36번 라인에서 부모 프로세스도 출력 파일 디스크립터를 이용하여 파이프에서 값을 빼내고 있으므로 자식 프로세스에서의 31번 라인과 부모 프로세스에서의 36번 라인 중 어떤 라인을 먼저 읽느냐에 따라 달린 것이기 때문이다. 아래와 같이.

자식 프로세스가 파이프에 넣은 값을 자식 프로세스가 빼낸 모습

그러나 29번 라인에서 sleep 함수를 실행함으로써 부모 프로세스가 먼저 값을 가져갈 수 있도록 여유를 두면 정상적으로 실행된다 다음처럼.

정상적으로 실행된 모습

그리고 40번 라인의 부모 프로세스 실행 영역에서 5초 정도의 여유를 준 이유는 부모 프로세스가 종료하면 자식 프로세스도 함께 죽기 때문에 자식 프로세스가 충분히 부모가 전송한 데이터를 꺼낼 시간을 기다리기 위해서 5초의 여유를 준 것이다. 

 

그런데 이렇게 데이터를 넣고, 꺼내는 타임을 sleep 함수를 통해 실행 흐름까지 막아서 조정해가면서 행하는게 이게 프로그램으로서 말이나 된단 말인가?

아니, 그러면 애초에 부모용 pipe, 자식용 pipe 이렇게 구분해서

자식은 자식 파이프의 입력 디스크립터와, 부모 파이프의 출력 디스크립트만 사용하고,
부모는 부모 파이프의 입력 디스크립터와, 자식 파이프의 출력 디스크립트만 사용하면 된다.

다음 소스 코드와 실행 결과를 통해 볼 수 있다.

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
#include <stdio.h>
#include <unistd.h>
 
#define BUFSIZE 40
 
int main(void)
{
    int fds1[2], fds2[2]; // 순서대로 자식용 파이프, 부모용 파이프
    pid_t pid;
 
    char from_c_to_p[] = "우리 부모님아!";
    char from_p_to_c[] = "내 자식아!";
 
    char buf[BUFSIZE];
 
    pipe(fds1);
    pipe(fds2);
 
    pid = fork();
 
    if(pid == 0)
    {
        write(fds1[1], from_c_to_p, sizeof(from_c_to_p));
 
        read(fds2[0], buf, BUFSIZE);
        printf("부모가 자식에게 보내온 메시지 : [%s]\n", buf);
    }
    else
    {
        read(fds1[0], buf, BUFSIZE);
        printf("자식이 부모에게 보내온 메시지 : [%s]\n", buf);
 
        write(fds2[1], from_p_to_c, sizeof(from_p_to_c));
        sleep(5);
    }
    return 0;
}
cs

 

위와 같은 이런 모양새인 것이다.

이로써 IPC 까지 프로세스에 대한 내용은 모두 마친다.

728x90
반응형
728x90
반응형
 

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

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

typingdog.tistory.com

 

 

 

C기반 I/O Multiprocessing - 9. 멀티 프로세싱에서 좀비 문제

C기반 I/O Multiprocessing - 8. 드디어 올리는 멀티 프로세싱 진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다.. 포스팅 순서가 사실 멀티 프로세싱이 먼

typingdog.tistory.com

이전 시리즈(?) 들이다.

이전 내용들을 토대로 멀티 프로세싱 서버를 구현해볼 것이다. 간단하게 특징있는 부분만 짚어본다.

시그널을 등록하는 코드.

파일 디스크립터와 소켓의 관계를 보여준다.

프로세스가 복제가 되면 서버 소켓에 해당하는 파일 디스크립터도 복제가 되고, 클라이언트 소켓에 해당하는 파일 디스크립터도 복제가 된다. 그렇지만 파일 디스크립터는 프로세스의 소유이고, 소켓을 가리키는 숫자에 불과하기 때문에 복제가 되도 상관없지만 소켓 같은 경우는 운영체제 소유이기 때문에 복제가 되지 않고, 복제 되어서도 안된다. 

그렇지만 소켓을 제거하려면 해당 소켓을 동일하게 가리키는 파일 디스크립터를 모두 제거해야한다. 부모든 자식이든. 그렇기 때문에 각 부모, 자식에서는 필요한 디스크립터만 살려두고 나머지를 제거해야 나중에 잊지 않고 완벽하게 소켓을 제거할 수 있다.

전체 코드 및 실행 결과

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUFSIZE 30
 
void ErrorMsg(const char *msg);
void GetChildReturnValue(int sig);
 
int main(void)
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
 
    int first_clnt_sock = -1, last_clnt_sock = -1, i = 0;
 
    pid_t pid;
    struct sigaction act;
 
    socklen_t addr_size;
    int strLen, state;
 
    char buf[BUFSIZE];
 
    act.sa_handler = GetChildReturnValue;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, 0);
 
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    if(serv_sock == -1)
        ErrorMsg("socket() error");
 
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(7010);
 
    if(bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        ErrorMsg("bind() error");
 
    if(listen(serv_sock, 5== -1)
        ErrorMsg("listen() error");
 
    while(1)
    {
        addr_size = sizeof(clnt_addr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
        if(clnt_sock == -1)
            continue;
        else
            printf("새로운 클라이언트가 접속 [%d]\n", clnt_sock);
 
        // 서버 측에서 클라이언트 소켓을 일괄 제거 하기 위함.
        // 리눅스에서는 파일 디스크립터가 1씩 증가함을 전제로 작성됨.
        // 아래 로직 영역이 없으면 클라이언트 디스크립터는 항상 같은 값을 나타냄..
        if(first_clnt_sock == -1)
            first_clnt_sock = clnt_sock; // 처음 디스크립터 값만 기록함.
        last_clnt_sock = clnt_sock; // 계속 갱신.
 
        pid = fork();
        /*
            이 때, 프로세스가 복사되면서 파일 디스크립터 값 또한 복사가 된다. 다만 소켓은 복사 되지 않는다.
            그 이유는 소켓은 운영체제 소유, 파일 디스크립터는 프로세스의 소유.
 
            하나의 소켓에 두 개의 파일 디스크립터가 존재하는 경우에는 두 개의 파일 디스크립터가 모두
            소멸되어야 소켓이 비로소 소멸된다.
 
            -> 그렇기 때문에 꼭 서로에게 상관 없는 소켓의 파일 디스크립터는 꼭 닫아줘야 한다.
            -> 부모 프로세스의 영역에서 위 라인에서 얻은 클라이언트 소켓을 꼭 닫아준다.
        */
        if(pid == -1// 프로세스 생성이 제대로 안 됐을 경우에는
        {
            close(clnt_sock); // 연결을 끊어버림. 왜냐하면 프로세스 생성이 제대로 안됐기 때문에.
            continue// 또 다른 접속을 받기 위함.
        }
 
        if(pid == 0// 자식 프로세스 실행 영역
        {
            close(serv_sock);
            // 자식 프로세스에서는 서버 소켓은 필요 없다. 연결된 클라이언트에 대해서만 처리하면 되기 때문.
            // 하나의 소켓, 둘의 파일 디스크립터이므로 꼭 닫아줘야한다.
            while((strLen = read(clnt_sock, buf, BUFSIZE)) != 0)
            {
                printf("[%d]로부터 온 메시지 : [%s]\n", clnt_sock, buf);
                write(clnt_sock, buf, strLen); // 내용을 다시 보냄. 에코.
            }
 
            close(clnt_sock);
            printf("클라이언트 종료 : [%d]\n", clnt_sock);
 
            return 0// 자식 클라이언트에서의 인자 전달이 이루어짐.
        }
        else // 부모 프로세스 실행 영역
        {
            // 부모 실행 영역에서는 실행할 로직이 없다.
        }
    }
    // 안타깝게도 이를 실행할 수 있는 방법은 현재까지 공부한 내용으로는 없다.
    // 부모 프로세스에서 쓰레드 등을 이용해서 위의 while(1)을 탈출할 수 있도록 해야한다.
 
    for( i = first_clnt_sock; i <= last_clnt_sock; i++)
        close(i);
 
    return 0;
}
 
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;
}
 
void ErrorMsg(const char *msg)
{
    fputs(msg, stderr);
    fputc('\n',stderr);
    exit(1);
}
 
cs

728x90
반응형
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
반응형
728x90
반응형

진짜 다 까먹어서 다시 예제 쳐보고 다시 올린다... 정말 미리미리 올리고 자주자주 봐야합니다..

포스팅 순서가 사실 멀티 프로세싱이 먼저이지만, 게임 서버 개발에 사용한 것은 멀티 플렉싱 모델이었기 때문에 먼저 올렸었는데, 사실 멀티 프로세싱이 무엇인지, 그리고 그 단점이나 필요한 영역은 어느 부분인지 알아야 한다.

그래서 포스팅을 또 하게 되었다.

내가 구현했던 서버의 모델은 멀티 플렉싱 모델이었다(select, epoll) 이 모델은 위와 같은 구조로 이루어진다.

여러 학생들이 클라이언트들이고, 교사하나프로세스이다.
질의와 답변을 필요로 하는 학생들, 즉 송수신을 요청하는 애들만 빠른 반복문 처리를 통해 처리한다.

이러한 모델에서는 여러 명의 클라이언트들이 송수신이나 연결 등의 여러 요청을 할 때마다 그 요청만큼만 처리하는 구조이기 때문에 대용량의 파일을 송수신하는 연결과는 적합하지 않다. 

반면, 멀티 프로세싱 모델은 다음과 같다.

교사들 프로세스들이고, 여러 학생들이 클라이언트들인것이다.
학생들이 질의와 답변을 필요로 하든 안 하든 무조건 프로세스와 연결되어 1대1 통신이 진행되는 것이다.

이런 구조에서는 시간이 오래 걸리는 상담이나 컨설팅 같은 서비스가 교사와 학생들 사이에서 제공되어야 할 법하다. 마찬가지로 프로세스-클라이언트 입장에서 봤을 때, 용량이 크고 시간이 오래 걸리는 파일 전송 등이 자원 효율이 클 것이다.

이러한 멀티 프로세싱 기반의 소켓을 작성하기 위해서는 fork() 에 대한 개념이 필요하다.

그전에! 프로세스란 무엇인가?

운영체제 관점에서 별도의 실행 흐름을 구성하는 단위. 
생성될 때, PCB(Process Controll Block) 라고 하는 프로세스에 대한 정보 구조가 생성되며,
이 정보에는 해당 프로세스에 대한 구체적인 정보들이 저장된다
(메모리 할당, 파일 디스크립터 테이블 등등 수 많은..)

프로세스의 복사본을 생성하는 fork()

위와 같은 기본 코드가 있다. 10번 줄을 보면 fork 라는 함수를 호출한다. 이 fork() 함수프로세스를 그대로 복사하는 함수이다. 이 함수가 호출되는 순간!! 프로세스가 그대~로 복사되어 10번 라인부터는 흐름이 각각 따로 진행된다. 다음과 같이 말이다.

그렇다면 이 함수가 반환하는 값은 무엇일까? 원래 실행되던 흐름부모 프로세스라고 하고, 복사된 실행 흐름자식 프로세스라고 한다.

부모 프로세스의 실행 흐름일 때, 10번 라인에서 반환되는 값은 자식 프로세스의 프로세스 ID 번호이다.
자식 프로세스의 실행 흐름일 때, 10번 라인에서 반환되는 값은 무조건 0이다.

그래서 12번 라인과 16번 라인에서는 부모 프로세스와 자식 프로세스의 실행 흐름을 구별하기 위해 pid를 0인지 아닌지를 비교하고 각 실행 흐름에 맞는 처리를 진행하면 된다.

 

이런 식으로 프로세스를 생성하므로, 클라이언트가 접속하면 프로세스를 복사하여 클라이언트에게 할당해줘서 송수신처리를 담당하면 된다. 

그런데.. 이러한 방식에는 큰 문제가 있었으니..

 

이번 포스팅에서 제일 중요한 것은 프로세스의 복사는 아예 메모리까지 통채로 복사되어
실행 흐름이 완전 다른 별개로 나뉘어진 부모 프로세스, 자식 프로세스라는 것이다!

728x90
반응형

+ Recent posts