반응형
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 - 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 Multiplexing - 1. Multiprocessing과 Multiplexing 비유

최근, 아니 비교적 최근에 어떤 게임 클라이언트를 활용한 게임 서버를 만드는 프로젝트를 진행했던 적이 있고, 지금도 조금씩 조금씩 하고 있으나, 우선 순위에 밀려 소홀하긴 하지만 진행하고

typingdog.tistory.com

이전 글에서 비유를 통해서 Multiplexing이 뭔지 느껴 보았을 터이다.

그 멀티플렉싱 기반의 다중 처리 소켓 모델이 있는데, 이 소켓 모델을 이용해서 다중 접속에 대한 처리를 서버 하나로 처리할 것이고, 그 과정을 정리하는 블로깅이다. 

이런 상황에서 서버를 어떻게 코딩 해야? 멀티 플렉싱 모델을 적용할 수 있을지를 기록할 것이다. 여기서 우리는 select 라는 함수를 사용하는데 일반 함수를 띡

strtok( ... );

호출하는 것과는 차원이 다르게 알아둬야 할 배경이 많다. 내가 느끼기엔 ㅋㅋ (strtok 함수도 사용하기 까다로웠음)

일단 기본 컨셉을 다루기 전에 앞서, 리눅스 시스템에서의 파일 디스크립터라는 개념을 알아야 한다. 윈도우의 경우에는 핸들이라는 개념으로 사용되는데 거의 비슷한 개념이라고 생각하고 모델을 구현해도 무리가 없다.

File Descriptor(파일 디스크립터) : 직역해보면 '파일 설명자'이다. 조금만 다듬으면 파일 식별자 라고 생각하면 되는데, 파일을 생성할 때, 해당 파일에 주어지는 번호라고 생각하면 된다. 즉, 그 파일을 간단하게 지목할 수 있는 번호표와 같다. 그런데 리눅스에서는 생성되는 파일 뿐만 아니라, 생성되는 소켓에 대해서도 파일 취급을 하고, 파일 디스크립터를 할당한다. 다음과 같이 말이다.(0과 1과 2는 표준 입출력들에 대해 이미 예약되어 있다. - 순서대로 stdin, stdout, stderr)

응 너도.

근데 이걸 왜 알아야하냐? select 함수는 바로 이 파일 디스크립터를 잘 요리조리 볶아서 다중 접속 처리를 하기 때문이다. 윈도우도 마찬가지다. 파일 디스크립터에서 핸들을 다룬다라는 것 외에는 다를 바가 없다.

그러면 기본 컨셉을 조금 표현해 볼 것이다.

첫 번째, 파일 디스크립터의 등록이다.

그림 내의 글을 읽어보면 되지만, 부가 설명을 넣자면 fd_set 은 구조체 자료형이다. 

요런 식으루 생겼고 ㅋㅋ (답없다ㅋㅋ), 비트처럼 동작한다고 생각하면 된다. 생성된 소켓들의 파일 디스크립터들이 배열로 나열되고, 0과 1로 변화의 유무를 표현한다고 생각하면 마음이 편할 것 같다.ㅋㅋ

이러한 배열을 조작하는 매크로 함수가 위위 그림에 나열한 것들이다.

FD_ZERO : 해당 배열을 모두 0으로 초기화.
FD_SET : 해당 파일 디스크립터를 배열에 등록.
FE_CLR : 해당 파일 디스크립터를 배열에서 제거.
FD_ISSET : 해당 파일 디스크립터가 배열에 등록되어 있니? 

코드로 확인하겠다 나중에ㅋㅋ

두 번째, 준비는 완료 되었다! Select 함수의 호출이다!!!!

위에는 select 함수의 인자 설명이고, 아래는 호출의 예이다.

__nfds 인자는 select 함수가 검사할 파일 디스크립터의 범위를 묻는 것이다. 범위면 범위지 왜 max + 1 같은 숫자를 입력하는건가? 

리눅스에서 파일 디스크립터는 0부터 순차적으로 생성이 된다. 그러니까 제일 마지막에 생성되는 파일 디스크립터가 곧  총 생성된 파일 디스크립터 중 마지막 번호를 가지고 있을 것이고, 거기서 더하기 1을 한 값이 총 갯수가 된다. 

 

fd_max는 이렇게 계속해서 갱신된다 ㅋㅋㅋ

파일 디스크립터가 "순차적"으로 값이 증가하는 것이 보장되기 때문에 범위를 지정하는 항목에 끝 번호 + 1 이 올 수 있는거다. 끝 번호 + 1 을 알면 0 ~ 끝 번호가 범위겠거니~ 하면 되니까!

추가 -- 

아, 맞다! 윈도우 같은 경우는 핸들이 순차적으로 생성되지 않는다! 그래서 fd_set을 다루는 방법이 조금 다르긴 하다 ㅋㅋ 그래서 윈도우에서는 첫 번째 인자를 사용하지 않고 뭐 알아서 범위를 잡아서 계산해주나보다. 다만, 리눅스 소켓 계열과 호환성 때문에 남겨두었다고 한다. 이거 빼먹을 뻔 했음 - 근데 나는 리눅스 계열 소켓만 준비했기 때문에...

--

세 번째, select 함수 호출 이후 변화를 살펴볼 것이다. select 함수 호출 직 후부터 지정한 시간동안 변화가 발생했을 때와 발생하지 않았을 때를 본다.

시간을 지정한다고 했는데 간단하다. 아래 그림 두 장과 설명을 보면 된다.

struct timeval 구조체 정의(좌)와 시간을 지정하는 방법(우)

먼저, 변화가 발생했을 때이다.

두 개 변화했으니 저 변한 녀석들에 대해서만 반복문이든 뭐든 써서 데이터 수신 처리와 그에 맞는 송신 처리를 진행하면 된다!! 참 쉽지 않는가!!!! 

저 두 녀석을 찾는 방법은 FD_ISSET 이라는 매크로 함수를 이용하여 찾아낸다. 정확히는

fd_set 배열 내 일어난 변화가 클라이언트1 fd인가?
fd_set 배열 내 일어난 변화가 클라이언트2 fd인가?

라는 질문을 FD_ISSET 과 조건문으로 주구장창 물어보면 된다 ㅋㅋ 그 방법은 코드로 익히는게 빠르다.

그럼 변화가 발생하지 않을 때에는? 

그냥 뭐 처리할게 없다. 왜냐? 뭔가 송신한 클라이언트가 없으니 변화가 안 일어났기 때문이다 ㅋㅋ

 

자, 이렇게 하면 과정들을 대충 훑어보았다. 개념만 대충 잡히면 된다. 나머지는 코드로 확실히 보면 되니까 말이다. 함수들이나 매개변수의 정확한 용어나 쓰임을 분석 단위로 보려면 찾아보면 된다. 그거 올려놓으면 나중에 까먹어서 다시 볼 때 하나도 머리에 안 들어오고 안 읽힌다 

다음 시간에는 코드다. 정리하기 너무 힘들다 진짜 ㅋ

 

728x90
반응형
728x90
반응형

최근, 아니 비교적 최근에 어떤 게임 클라이언트를 활용한 게임 서버를 만드는 프로젝트를 진행했던 적이 있고, 지금도 조금씩 조금씩 하고 있으나, 우선 순위에 밀려 소홀하긴 하지만 진행하고 있다. 

아무래도 게임 서버인 만큼 소켓 프로그래밍이 빠질 수가 없는 영역인데, 이를 개발하면 바로바로 기록해뒀어야 하는데 그러지 못해 지금 땅을 치고 후회하고 있다 휴...

그 프로젝트에서 사용된 소켓 모델은 I/O Multiplexing이다. 원래 처음에는 Multi Processing 기반으로 구현해보고, 한 100명 접속할 미래를 김칫국 마시 듯 생각해서 Multi Threading 기법으로 간단히 구현했다가, 신경써야 할 부분이 많아서 Multi plexing 기반으로 구현을 해보았다.

먼저 각각의 차이를 알아야 하는데, Thread 개념은 Processing 포스팅 때 비교하도록하고, 지금은 완전 다른 양상을 보이는 Plexing과 Processing을 비교하도록 하겠다.

나만의 비유가 들어간다.

A라는 과목을 가리치는 교사들이 있다. 이 클래스에는 A 과목만 들으러 학생들이 온다. 이는 전제이다.

멀티 프로세싱 모델을 비교한 것이다. 학생에게 마치 과외 선생님처럼 교사가 할당되어, 학생에게서 질문이나 요구를 받아서 그에 따른 대답을 해준다.

하지만 이는 매~우 비효율적이다. 전제처럼 같은 수업 내용을 듣는 학생들인데 교사가 굳이 일대 일로 붙는다는 것은 교사라는 재능, 인적 자원이 너무나도 낭비되는 것이다! 왜냐? 같은 과목을 듣는 학생을 한데 모아서, 질문이 있는 사람들에게만 대답해주고, 가르치는 것은 일방적으로, 기본적으로 수행하면 되기 때문이다! 

그래서 또 그려왔다.

얼마나 효율적인가, 학생들 입장에서는 특화된 교육을 못 받아서 질이 떨어졌다고 생각할 수 있지만 정말 실력이 좋은 교사로 채용할수록 교육의 질을 확실하게 커버할 수 있다.

바로 이 비유에서 볼 수 있듯이 여러 학생들에게 교사 한 명이 배치되어 '가르침'이라는 기본 동작을 수행하고, 질문을 하는 학생들을 대상으로 따로 처리를 한다.

이번에 기록하는 모델 또한 마찬가지이다.

여러 학생들에게 -> 접속해 온 여러 클라이언트들에게
교사 한 명 -> 서버 하나
기본 동작을 수행 -> 접속 요청 수락 및 접속 종료 등
질문을 하는 학생들 -> 패킷을 송신하는 클라이언트

접속해 온 여러 클라이언트들에게 서버 하나가 배치되어 '접속 요청 수락 및 접속 종료 등의 송 수신'이라는 기본 동작을 수행하고, 패킷을 송신하는 클라이언트들을 대상으로 따로 처리를 한다.

이렇게 바꿔서 서버-클라이언트 관점에서 이야기해볼 수 있다. 100% 기술적으로 맞지 않을 수 있지만 크게 보았을 때 그렇다는 것이다.

나는 그래서 앞으로 저 두 번째 그림에서의 교사에 대한 설명을 기록하고 만들 것이다! ㅋㅋ

728x90
반응형

+ Recent posts