본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

Ch 18. 내용 확인문제

by minjunkim.dev 2020. 8. 10.

   모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


01. 하나의 CPU를 기반으로 어떻게 둘 이상의 프로세스가 동시에 실행되는지 설명해보자. 그리고 그 과정에서 발생하는 컨텍스트 스위칭이 무엇인지도 함께 설명해보자.

- CPU에 의한 실행대상 프로세스를 매우 짧은 시간마다 교체하기 때문에 둘 이상의 프로세스가 (거의) 동시에 실행 가능하다.

 

- 컨텍스트 스위칭이란, CPU에 의한 실행의 대상을 변경하는 과정에서 발생하는 준비과정으로써, 이전에 실행되던 프로세스의 데이터를 메모리 공간에서 내리고 이어서 실행될 프로세스의 데이터를 메모리 공간 위에 올리는 작업이다. (더 정확히 따지자면, CPU 레지스터에 있는 프로세스 데이터를 내리고, 메모리에 대기하던 프로세스 데이터를 CPU 레지스터로 올리는 작업)



02. 쓰레드의 컨텍스트 스위칭이 빠른 이유는 어디에 있는가?
그리고 쓰레드간의 데이터 교환에는 IPC와 같은 별도의 기법이 불필요한 이유는 무엇인가?

# 쓰레드의 등장
- 멀티프로세스의 특징을 유지하면서 단점을 극복하기 위해 등장
- 멀티프로세스의 단점을 최소화하기 위해 설계된 일종의 "경량화된" 프로세스
- 장점
1) 쓰레드의 생성 및 컨텍스트 스위칭은 프로세스보다 빠르다
2) 쓰레드 사이에서의 데이터 교환에는 특별한 기법이 필요치 않다.

 

- 쓰레드는 데이터/힙영역을 공유하고 스택영역만 분리시킴으로써 다음과 같은 장점을 얻음
1) 컨텍스트 스위칭시 데이터 영역과 힙은 올리고 내릴 필요가 없다.(컨텍스트 스위칭이 빠른 이유)
2) 데이터 영역과 힙을 이용해서 데이터를 교환할 수 있다.(IPC와 같은 별도의 기법이 불필요한 이유)



03. 실행흐름의 관점에서 프로세스와 쓰레드를 구분하여라.

- 프로세스 : "운영체제 관점"에서 별도의 실행흐름을 구성하는 단위
- 쓰레드 : "프로세스 관점"에서 별도의 실행흐름을 구성하는 단위



04. 다음 중에서 임계영역과 관련해서 맞게 설명하고 있는 것을 모두 고르면?

a. 임계영역은 둘 이상의 쓰레드가 동시에 접근(실행)하는 경우에 문제가 발생하는 영역을 의미한다.

# 틀린 내용

 

b. 쓰레드에 안전한 함수는 임계영역이 존재하지 않아서 둘 이상의 쓰레드가 동시에 호출해도 문제가 발생하지 않는 함수를 의미한다.
=> 쓰레드에 안전한 함수도 임계영역은 존재할 수 있다. 문제가 발생하지 않을 뿐이다.

c. 하나의 임계영역은 하나의 코드블록으로만 구성된다. 하나의 임계영역이 둘 이상의 코드블록으로 구성되는 경우는 없다. 즉, A 쓰레드가 실행되는 코드블록 A와, B 쓰레드가 실행되는 코드블록 B 사이에서는 절대 임계영역이 구성되지 않는다.

=> 쓰레드 동기화를 진행하지 않는다면 둘 이상의 코드블록으로 구성될 가능성이 있다. (동일한 메모리에 접근만 가능하다면)

d. 임계영역은 전역변수의 접근코드로 구성된다. 이외의 변수에서는 문제가 발생하지 않는다.
=> 전역변수가 아니더라도 동시에 접근 가능한 메모리라면 문제가 발생할 수 있다.



05. 다음 중에서 쓰레드의 동기화와 관련해서 맞게 설명하고 있는 것을 모두 고르면?

a. 쓰레드의 동기화는 임계영역으로의 접근을 제한하는 것이다.

b. 쓰레드의 동기화에는 쓰레드의 실행순서를 컨트롤한다는 의미도 있다.

c. 뮤텍스와 세마포어는 대표적인 동기화 기법이다.


# 틀린 내용

d. 쓰레드의 동기화는 프로세스의 IPC를 대체하는 기법이다.
=> 같은 프로세스 내의 모든 쓰레드는 스택을 제외한 나머지 메모리 공간을 공유한다. 따라서, IPC와 같은 별도의 통신 기법이 필요하지 않다.



06. 리눅스의 쓰레드를 완전히 소멸하는 방법 두 가지를 설명하여라

# 쓰레드를 소멸하는 두가지 방법
- 리눅스의 쓰레드는 쓰레드의 main 함수를 반환했다고 해서 자동으로 소멸하지 않음
- 쓰레드의 소멸을 직접적으로 명시해야만 함, 안그러면 쓰레드에 의해 할당된 메모리 공간이 계속해서 남아있게됨
1) pthread_join 함수의 호출 => 쓰레드의 종료대기 및 소멸까지 유도가 되나,

쓰레드가 종료될때까지 해당 프로세스가 블로킹 상태에 놓이게 됨.
2) pthread_detach 함수의 호출을 통해서 쓰레드의 소멸을 주로 유도함.

- 이 함수을 호출했다고 해서, 종료되지 않은 쓰레드가 종료되거나 블로킹 상태에 놓이지 않음.
- 이 함수를 통해, 종료된 쓰레드에게 할당된 메모리의 소멸을 유도할 수 있음.
- 이 함수 호출 이후에는 해당 쓰레드를 대상으로 pthread_join() 함수를 호출할 수 없으니 주의하자.


07. 에코 서버를 멀티쓰레드 기반으로 구현해보자. 단, 클라이언트가 전송하는 메시지의 저장을 목적으로 선언되는 메모리 공간(char형 배열)을 모든 쓰레드가 공유하도록 하자.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define BUF_SIZE 100

char buf[BUF_SIZE];

void* handle_clnt(void* arg);
void error_handling(char* message);

pthread_mutex_t mutx; // 뮤텍스를 통한 쓰레드 동기화를 위한 변수

int main(int argc, char * argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;
    pthread_t t_id;

    if(argc!=2) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(EXIT_FAILURE);
    }

    pthread_mutex_init(&mutx,NULL); // 뮤텍스 생성

    serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성

    /* 서버 주소정보 초기화 */
    memset(&serv_adr,0,sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    /* 서버 주소정보를 기반으로 주소할당 */
    bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));

    /* 서버소켓(리스닝소켓)이 됨 
    연결요청 대기큐를 생성하고, 클라이언트의 연결요청을 기다림
    */
    listen(serv_sock,5);

    while(true)
    {
        clnt_adr_sz=sizeof(clnt_adr);

        /* 클라이언트의 연결요청을 수락하고,
        클라이언트와의 송수신을 위한 새로운 소켓 생성 */
        clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
        printf("Connected client IP: %s \n",inet_ntoa(clnt_adr.sin_addr));
        // 클라이언트의 IP정보를 문자열로 변환하여 출력

        pthread_create(&t_id,NULL,handle_clnt,(void*)&clnt_sock); // 쓰레드 생성 및 실행
        pthread_detach(t_id); // 쓰레드가 종료되면 소멸시킴
    }

    return 0;
}

void* handle_clnt(void* arg)
{
    int clnt_sock=*((int*)arg); // 클라이언트와의 연결을 위해 생성된 소켓의 파일 디스크립터
    int str_len=0,i;

    //pthread_mutex_lock(&mutx); // 뮤텍스 lock
    while((str_len=read(clnt_sock,buf,sizeof(buf)))!=0) // 클라이언트로부터 EOF를 수신 할때까지 읽어서
        write(clnt_sock,buf,str_len); // 클라이언트로 에코
    //pthread_mutex_unlock(&mutx); // 뮤텍스 unlock
    
    close(clnt_sock); // 클라이언트와의 송수신을 위한 생성했던 소켓종료
    return NULL;

}

void error_handling(char* message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(EXIT_FAILURE); // -1
}

 


08. 위의 문제 7에서는 에코 메시지의 송수신에 사용할 메모리 공간을 모든 쓰레드가 공유할 것을 요구하고 있다. 이렇게 구현을 하면 동기화를 해도, 안해도 문제가 발생하는데, 각각의 경우에 따라서 어떠한 문제가 발생하는지 설명해 보자.

 

- 동기화를 한 경우 : 클라이언트로부터 문자열을 읽어들이는 read 함수 호출이 임계영역이 포함된다. 따라서 한 쓰레드로 한 클라이언트에게 문자열을 읽는 동안에는, 다른 쓰레드로 다른 클라이언트에게 문자열을 읽을 수가 없게 된다.

 

- 동기화를 안한 경우 : 둘 이상의 쓰레드가 동일한 메모리 공간에 동시에 접근을 해서 문제를 일으킬 수 있다.


[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어