TCP / IP 소켓 프로그래밍 6
18. 멀티쓰레드 기반의 서버구현
18-1. 쓰레드의 이론적 이해
- 프로세스 : 운영체제 관점에서 별도의 실행흐름을 구성하는 단위
- 쓰레드 : 프로세스 관점에서 별도의 실행흐름을 구성하는 단위
즉, 프로세스가 하나의 운영체제 안에서 둘 이상의 실행흐름을 형성하기 위한 도구라면, 쓰레드는 하나의 프로세스 내에서 둘 이상의 실행흐름을 형성하기 위한 도구로 이해할 수 있다.
18-2. 쓰레드의 생성 및 실행
** 쓰레드는 별도의 실행흐름을 갖기 때문에 쓰레드만의 main함수를 별도로 정의해야한다.
pthread_create() : 별도의 실행흐름을 형성해 줄 것을 운영체제에게 요청하는 함수
#include <pthread.h>
int pthread_create(
pthread_t *restrict thread, const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);
//성공 시 0, 실패 시 0 이외의 값 반환
thread : 생성할 쓰레드의 ID 저장을 위한 변수의 주소값 전달, 참고로 쓰레드는 프로세스와 마찬가지로 쓰레드의 구분을 위한 ID가 부여된다.
attr : 쓰레드에 부여할 특성 정보의 전달을 위한 매개변수, NULL 전달 시 기본적인 특성의 쓰레드가 생성된다.
start_routine : 쓰레드의 main 함수 역할을 하는 , 별도 실행흐름의 시작이 되는 함수의 주소값 (함수 포인터) 전달.
arg : 세번째 매개변수를 통한 등록된 함수가 호출될 때 전달할 인자의 정보를 담고있는 변수의 주소 값 전달.
pthread_join() : 첫번째 매개변수가 전달되는 ID의 쓰레드가 종료될 때까지, 이 함수를 호출한 프로세스(또는 쓰레드)를 대기상태에 둔다.
#include <pthread.h>
int pthread_join(pthread_t thread, void **stattus);
//성공 시 0, 실패 시 0 이외의 값 반환
thread : 이 매개변수에 전달되느 ID의 쓰레드가 종료될 때까지 함수는 반환하지 않는다.
status : 쓰레드의 main함수가 반환하는 값이 저장될 포인터 변수의 주소 값을 전달한다.
18-3. 쓰레드의 문제점과 임계영역(Critical Section)
*** 쓰레드의 문제점은 하나의 변수에 둘 이상의 쓰레드가 동시에 접근하는것이 문제
- 두 쓰레드가 동시에 Thread_inc 함수를 실행하는 경우
- 두 쓰레드가 동시에 Tread_des 함수를 실행하는 경우
- 두 쓰레드가 각각 Tread_inc 함수와 Tread_des 함수를 동시에 실행하는 경우
임계영역은 서로 다른 두 문장이 각각 다른 쓰레드에 의해서 동시에 실행되는 상황에서 만들어 질 수 있다.
임계영역 이란?
임계 영역이란 한 순간 반드시 프로세스 하나만 진입해야 하는데, 프로그램에서 임계 자원을 이용하는 부분으로 공유 자원의 독점을 보장하는 코드 영역을 의미한다. 임계 구역은 지정된 시간이 지난 후 종료된다.
[임계 영역을 해결하기 위한 방법]
- 뮤텍스, 세마포어, 모니터 등이 있다.
- 상호 배제, 한정 대기, 융통성이라는 조건을 만족한다.
- 상호 배제 : 한 프로세스가 임계 영역에 들어갔을 때 다른 프로세스는 들어갈 수 없음.
- 한정 대기 : 특정 프로세스가 영원히 임계 영역에 들어가지 못하면 안 됨.
- 진행 : 임계 구역에 들어간 프로세스가 없는 상태에서, 들어가려고 하는 프로세스가 여러 개 있다면 어느 것이 들어갈지를 적절히 결정해주어야 한다.
- 융퉁성 : 한 프로세스가 다른 프로세스의 일을 방해해서는 안됨.
18-4. 쓰레드 동기화
[동기화가 필요한 상황]
- 동일한 메모리 영역으로의 동시접근이 발생하는 상황
- 동일한 메모리 영역에 접근하는 쓰레드의 실행순서를 지정해야 하는 상황 (실행순서 컨트롤)
[뮤텍스(Mutex)]
Mutual Exclusion 의 줄임말로써 쓰레드의 동시접근을 허용시키지 않는다는 의미가 있다.
뮤텍스 쓰레드 함수
pthread_mutex_init() : 뮤텍스 자물쇠 시스템 생성 함수
pthread_mutex_destroy() : 뮤텍스 자물쇠 시스템 소멸 함수
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//성공 시 0, 실패 시 0 이외의 값 반환
mutext : 뮤텍스 생성시에는 뮤텍스의 참조 값 저장을 위한 변수의 주소 값 전달, 그리고 뮤텍스 소멸 시에는 소멸하고자 하는 뮤텍스의 참조 값을 저장하고 있는 변수의 주소값 전달.
attr : 생성하는 뮤텍스의 특성정보를 담고 있는 변수의 주소 값 전달, 별도의 특성을 지정하지 않을 경우에는 NULL전달.
***attr 에 NULL 전달하는 경우 매크로 PTHREAD_MUTEX_INITIALIZER을 이용해서 다음과 같이 초기화 가능.
= pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
가급적이면 pthread_mutex_init함수를 이용한 초기화를 추천한다. 왜냐하면 매크로를 이용한 초기화는 오류발생에 대한 확인이 어렵다.
[임계영역 뮤텍스 설정]
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//성공 시 0, 실패 시 0 이외의 값 반환
임계영역에 들어가기에 앞서 호출하는 함수 pthread_mutex_lock()
[형식]
pthread_mutex_lock(&mutex);
//임계영역 시작
// ....
//임계영역 끝
pthread_mutex_unlock(&mutex);
** 둘 이상의 쓰레드 접근을 허용하지 않게된다.
세마포어
뮤텍스를 바탕으로 세마포어는 0과1만을 사용하는 쓰레드의 '실행순서 컨트롤'중심의 동기화
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
//성공 시 0, 실패 시 0 이외의 값 반환
sem : 세마포어 생성시에는 세마포어의 참조 값 저장을 위한 변수의 주소 값 전달, 그리고 세마포어 소멸 시에는 소멸하고자 하는 세마포어 소멸 시에는 소멸하고자 하는 세마포어의 참조 값을 저장하고 있는 변수의 주소값 전달.
pshared : 0이외의 값 전달 시, 둘 이상의 프로세스에 의해 접근 가능한 세마포어 생성, 0전달시 하나의 프로세스 내에서만 접근 가능한 세마포어 생성, 우리는 하나의 프로세스 내에 존재하는 쓰레드의 동기화가 목적이므로 0을 전달한다.
value : 생성되는 세마포어의 초기 값 지정.
[mutex의 lock, unlock함수에 해당하는 세마포어 관련함수]
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
//성공 시 0, 실패 시 0 이외의 값 반환
sem : 세마포어의 참조 값을 저장하고 있는 변수의 주소 값 전달, sem_post에 전달되면 세마포어의 값은 하나 증가, sem_wait에 전달되면 세마포어의 값은 하나 감소.
sem_init함수가 호출되면 운영체제에 의해서 세마포어 오브젝트라는 것이 만들어 지는데, 이곳에는 "세마포어 값(Semaphore Value)"이라 불리는 정수가 하나 기록된다.
sem_post함수를 호출하면 세마포어의 값이 1이 되므로 , 이 1을 0으로 감소시키면서 블로킹 상태에서 빠져나가게 된다.
이런한 특징을 이용해서 임계영역을 동기화 시킨다.
[형식]
sem_wait(&sem);//세마포어 값을 0으로...
// 임계영역의 시작
// ....
// 임계영역의 끝
sem_post(&sem);//세마포어 값을 1로...
18-5. 쓰레드의 소멸과 멀티쓰레드 기반의 다중접속 서버의 구현
[쓰레드를 소멸하는 두가지 방법]
- pthread_join 함수호출
- pthread_detach 함수호출
#include <pthread.h>
int pthread_detach(pthread_t thread);
//성공 시 0, 실패 시 0 이외의 값 반환
thread : 종료와 동시에 소멸 시킬 쓰레드의 ID 정보 전달.
[멀티쓰레드 기반의 다중접속 서버의 구현 예제]
chat_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#define BUF_SIZE 100
#define MAX_CLNT 256
void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
void error_handling(char * msg);
int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx; //쓰레드 동기화 뮤텍스
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
int clnt_adr_sz;
pthread_t t_id; // 쓰레드 ID 변수명 선언
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
pthread_mutex_init(&mutx, NULL); // 쓰레드 동기화 하기 전 초기화
serv_sock=socket(PF_INET, SOCK_STREAM, 0); // 서버 소켓 생성
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]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1) // 소켓에 IP, PORT 할당
error_handling("bind() error");
if(listen(serv_sock, 5)==-1) // 클라이언트와 연결 대기 상태
error_handling("listen() error");
while(1)
{
clnt_adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
// 클라이언트 연결요청 수락한 클라이언트 소켓 생성
pthread_mutex_lock(&mutx); // 쓰레드 임계영역 시작
clnt_socks[clnt_cnt++]=clnt_sock;
pthread_mutex_unlock(&mutx); // 쓰레드 임계영역 끝
pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock); 쓰레드 생성
pthread_detach(t_id);// 쓰레드 메모리에서 완전히 소멸되도록 실행
printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
}
close(serv_sock);
return 0;
}
void * handle_clnt(void * arg)
{
int clnt_sock=*((int*)arg); // 클라이언트 소켓 담을 변수 선언
int str_len=0, i;
char msg[BUF_SIZE]; // 메시지 문자열 버퍼 크기 선언
while((str_len=read(clnt_sock, msg, sizeof(msg)))!=0)
send_msg(msg, str_len);
pthread_mutex_lock(&mutx);//임계영역 시작
for(i=0; i<clnt_cnt; i++) // remove disconnected client
{
if(clnt_sock==clnt_socks[i]) // 클라이언트 소켓 = 클라이언트 접근관련 배열 소켓 같으면
{
while(i++<clnt_cnt-1)
clnt_socks[i]=clnt_socks[i+1];
break;
}
}
clnt_cnt--;
pthread_mutex_unlock(&mutx); // 임계영역 끝
close(clnt_sock);
return NULL;
}
void send_msg(char * msg, int len) // send to all
{
int i;
pthread_mutex_lock(&mutx);
for(i=0; i<clnt_cnt; i++)
write(clnt_socks[i], msg, len);
pthread_mutex_unlock(&mutx);
}
void error_handling(char * msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
chat_clnt.c
입력과 출력의 처리를 분리시키기 위해서 쓰레드를 생성하였다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define BUF_SIZE 100
#define NAME_SIZE 20
void * send_msg(void * arg);
void * recv_msg(void * arg);
void error_handling(char * msg);
char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr;
pthread_t snd_thread, rcv_thread;
void * thread_return;
if(argc!=4) {
printf("Usage : %s <IP> <port> <name>\n", argv[0]);
exit(1);
}
sprintf(name, "[%s]", argv[3]);
sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error");
pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);
pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);
pthread_join(snd_thread, &thread_return);
pthread_join(rcv_thread, &thread_return);
close(sock);
return 0;
}
void * send_msg(void * arg) // send thread main
{
int sock=*((int*)arg);
char name_msg[NAME_SIZE+BUF_SIZE];
while(1)
{
fgets(msg, BUF_SIZE, stdin);
if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n"))
{
close(sock);
exit(0);
}
sprintf(name_msg,"%s %s", name, msg);
write(sock, name_msg, strlen(name_msg));
}
return NULL;
}
void * recv_msg(void * arg) // read thread main
{
int sock=*((int*)arg);
char name_msg[NAME_SIZE+BUF_SIZE];
int str_len;
while(1)
{
str_len=read(sock, name_msg, NAME_SIZE+BUF_SIZE-1);
if(str_len==-1)
return (void*)-1;
name_msg[str_len]=0;
fputs(name_msg, stdout);
}
return NULL;
}
void error_handling(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
int argc와 char *argv[]란?
main 함수가 int main(int argc, char **argv){} 다음과 같이 구성되어있었다.
정확히 argc와 argv가 어떤 인자인지 알기 위해 쓰는 글이다. main함수를 포함한 모든 함수에는 인수(파라미터)를 지정할 수 있다. main 함수의 매개변수는 보통 아무것도 사용하지 않지만 다음 모양과 같은 모양으로 쓰일 수 있다.
int main(int argc, char *argv[]) 함수에서는 윈도우나 리눅스 같은 OS 명령 프롬포트를 이용해 인자를 전달해서 작동할 수 있도록 한다. c언어에서 main 함수는 프로그램이 최초로 실행되는 곳이다. 또한 매개변수는 함수를 호출할 때 전달되는 데이터를 의미하는데 우리는 main()함수의 매개변수를 넘겨줌으로써 원하는 실행결과를 도출하고자 한다 .
1. int argc
- argc = argument count
- argc는 운영체제가 이 프로그램을 실행했을 때 전달되는 인수의 갯수이다.
- 즉, main()함수에 전달되는 데이터의 갯수를 의미한다.
2. char *argv[]
- argv = argument variable
- char *argv[]: 문자열의 주소를 저장하는 포인터 배열
- argv[0]은 프로그램의 실행경로이다.
- argv[1], argv[2] ... 에는 순서대로 사용자가 입력한 argument가 저장된다.
예를 들어, int main(int argc, char *argv[])에 ./tiny 8000 aaa이라는 입력을 준다면,
argc는 2개일 것이고, argv[0]에는 실행경로인 ./tiny가 들어가고, argv[1]에는 8000이 들어가고, argv[2]에는 aaa가 들어갈 것이다.
=> argv의 각 인자는 띄어쓰기로 구분된다.

3. int main(int argc, char *argv[]) VS int main(int argc, char **argv)
위 함수의 차이점은 무엇인가? ? !
결론부터 말하면 두 함수는 똑같다.
기본적으로 char *argv[]는 char 포인터 배열을 의미하고, char **argv는 char 포인터에 대한 포인터를 의미한다.
※ 더블 포인터(포인터의 포인터)는 포인터 변수를 가리키는 또 다른 포인터 변수를 의미한다.
다시 예를 들어 int main(int argc, char **argv)에 ./tiny 8000 aaa를 인자로 넣어줬다고 가정하면,
여기서 argv는 문자열의 시작 주소를 나타내는 3개(argc)의 char*의 배열을 가리키는 포인터이다.
첫번째 문자열의 시작 주소를 나타내는 argv[0]은 첫 번째 문자열에 대한 포인터이고, *argv[0]은 첫 번재 문자열의 첫 번째 문자를 나타낸다. 그렇기에 **argv도 첫 번째 문자열의 첫 번째 문자를 나타낸다.
1. argv는 현재 처리해야할 문자열에 대한 포인터이다.
2. *argv는 현재 처리해야 할 문자열이다.
3. **argv는 현재 처리해야 할 문자열의 첫 글자이다.
4. *argv + 1은 현재 처리해야할 문자열의 두 번째 문자에 대한 포인터이다. &argv[0][1]과 같다.
결론)
main()함수의 argument는 여러가지 파라미터에 따른 시뮬레이션을 해야할 경우에 유용하게 사용된다.
입력이 여러 개 있을 경우(예: 8000 aaa bbbb...)에는 문자열들이 여러개 있기 때문에 더블(2) 포인터가 필요하다.