[Network] 10. 멀티 프로세스 기반의 서버 구현
프로세스의 이해와 활용
다중 접속 서버의 구현 방법들
다중접속 서버 : 둘 이상의 클라이언트에게 동시 접속을 허용하여 동시에 둘 이상의 클라이언트에 서비스를 제공하는 서버를 의미한다.
- 멀티프로세스 기반 서버 : 다수의 프로세스를 생성하는 방식
- 멀티플렉싱 기반 서버 : 입출력 대상을 묶어서 관리하는 방식
- 멀티쓰레딩 기반 서버 : 클라이언트의 수만큼 쓰레드를 생성하는 방식
프로세스와 프로세스 IP
프로세스 : 실행 중인 프로그램
멀티 프로세스 OS는 둘 이상의 프로세스를 동시에 생성 가능하다.
운영체제는 생성되는 모든 process에 id(PID)를 할당하며, 이를 ps 명령어를 통해 확인할 수 있다.
fork 함수의 호출을 통한 프로세스의 생성
한 프로그램에서 여러개의 프로세스를 생성할 때에는 fork 명령어를 이용하면 된다.
#include <unistd.h>
pid_t fork(void);
fork를 성공하면 PID를 return 한다. 실패시 -1을 return 한다.
fork 함수가 호출되면 부모 프로세스와 자식 프로세스가 독립적으로 실행되게 되는데, 이때, fork의 반환값을 보고 두 개를 구분할 수 있다.
부모 프로세스 : fork 의 반환값은 자식의 PID
자식 프로세스 : fork 반환값은 0
이 성질을 이용해 두 프로세스가 할 일을 구분해 코드를 작성할 수 있다.
pit_t pid = fork()
if (pid == 0) // 자식 프로세스
{
// 자식 프로세스가 할 일
}
else
{
// 부모 프로세스가 할 일
}
프로세스 & 좀비 프로세스
좀비 프로세스
실행이 완료되었음에도 소멸하지 않은 프로세스이다. 프로세스는 main 함수가 반환되었을 때 소멸되는데 반환이 되지 않았다는 것은 리소스가 메모리 공간에 존재하고 있다는 의미이다.
좀비 프로세스의 생성 원인
자식 프로세스가 종료될 때의 반환 값이 부모프로세스에 전달되지 않으면 좀비 프로제스가 된다.
- 인자를 전달하면서 exit 전달
- main 함수에서 return 값 전달
위 두 상황이 자식 프로세스의 종료 상태 값이 OS 에 전달되는 방법이다.
자식 프로세스는 부모 프로세스가 wait() 시스템 함수를 호출해야 종료가 된다. 보통 wait()는 부모 프로세스가 소멸될 때 발생하고 이렇게 되면 할 일이 모두 끝난 상태인데도 부모 프로세스가 종료될 때까지 프로세스 시스템 테이블에 남아있게 된다. 이 것이 많이 누적되다보면 문제가 발생된다.
그래서 좀비 프로세스가 발생하는지 판단하고 이를 회수해주어야하며, wait를 사용하는 방법과 waitpid를 사용하는 2가지 방법이 있다.
좀비 프로세스의 소멸 : wait 함수
int status
pid_t pid = fork();
if (pid == 0)
{
return 3;
}
else
{
printf("Child PID: %d\n" , pid);
pid = fork();
if(pid == 0)
{
exit(7);
}
else
{
printf("Child PID : %d\n", pid);
wait(&status); //child process 가 종료될때까지 기다림
if(WIFEXITED(status))
printf("Childe send one : %d\n", WEXITSTATUS(status));
wait(&status);
if(WIFEXITED(status))
printf("Child send two : %d\n", WEXITSTATUS(status));
sleep(30);
}
}
wait 함수의 경우 자식 프로세스가 종료가 되지 않은 경우 블로킹 상태에 빠진다.
WIFEXITED : 자식 프로세스가 정상 종료일 때 true
WEXITSTATUS : 자식 프로세스의 전달 값을 반환함.
좀비 프로세스의 소멸 : waitpid 함수
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int * statloc, int options);
waitpid 함수는 wait함수와 다르게 블로킹 상태에 빠지지 않는다는 장점이 있다.
if(pid == 0)
{
sleep(15);
return 24;
}
else
{
while(!waitpid(-1, &status, WHOHANG))
{
sleep(3);
puts("sleep 3 sec");
}
if(WIFEXITED(status))
printf("Child send %d \n", WEXITSTATUS(status));
}
return 0;
}
시그널 핸들링
시그널이란?
특정 상황이 되었을 때 운영체제가 프로세스에게 해당 상황이 발생했음을 알리는 일종의 메시지.
-> 시그널을 미리 등록해두면 프로세스가 OS로부터 시그널을 받을 수 있다.
signal 함수
#include <signal.h>
void (*signal(int signo, void(*func)(int)))(int);
signal은 매개변수가 int 이고 반환형이 void 인 함수 포인터이다.
sigaction 함수
#include <signal.h>
int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);
sigaction 구조체 변수에 함수의 정보를 채워서 인자로 전달한다.
sa_mask 와 sa_flags 는 모두 0으로 초기화한다. (좀비 소멸 목적으로 쓰는 변수가 아니라서)
void timeout(int sig)
{
if(sig == SIGALRM)
puts("time out")
alarm(2);
}
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGALRM, &act, 0);
alarm(2);
for(i = 0; i< 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
sa_handler에 handler 함수를 등록하고 sigaction의 인자로 전달한다.
우리는 이 시그널 핸들링 기법을 통해 좀비 프로세스를 자동으로 소멸 시킬 것이다.
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if(WIFEXITED(status))
{
printf("Removed proc id : %d\n", id);
printf("Child send: %d\n", SEXITSTATUS(status));
}
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
SIGCHID는 자식 프로세스가 소멸될 때 발생한다. 즉, 자식 프로세스가 소멸될 때 read_childproc을 실행해 자동으로 waitpid가 실행될 수 있도록 한다.
멀티태스킹 기반의 다중접속 서버
프로세스 기반 다중접속 서버 모델
연결이 하나 생성될 때마다 프로세스를 생성해서 해당 클라이언트에 대해 서비스를 제공하는 것이다.
작동 방식은 아래와 같다.
while(1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock == -1)
continue; //클라이언트가 연결될 때 까지 반복
else
puts("new client connected...");
//클라이언트가 들어오면 새 프로세스 생성
pid=fork();
if(pid==-1) //error ( fork 실패 )
{
close(clnt_sock);
continue;
}
if(pid == 0) // 자식 프로세스 => client
{
close(serv_sock); //서버 소켓은 닫아준다.
// 클라이언트에게 서비스 제공
while((str_len = read(clnt_sock, buf, BUF_SIZE))!=0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock); // 클라이언트의 소켓은 닫는다.
}
fork 함수를 호출하면 디스크립터가 복사된다. 그래서 fork를 실행하고 난 뒤 직접 닫아줘야한다.
TCP의 입출력 루틴 분할
입출력 루틴 분할의 이점과 의미
소켓은 양방향 통신이 가능하다. 따라서 아래 그림과 같이 입력을 담당하는 프로세스와 출력을 담당하는 프로세스를 각각 생성하면, 입력과 출력을 각각 별도로 진행시킬 수 있다.
두 개를 분할하면 보내고 받는 과정을 한번에 할 수 있다.
'Computer Science > Network' 카테고리의 다른 글
[Network] 12. IO 멀티플렉싱 (0) | 2023.05.28 |
---|---|
[Network] 11. 프로세스간 통신 (0) | 2023.05.28 |
[Network] 9. 소켓의 다양한 옵션 (0) | 2023.05.28 |
[Network] 8. 도메인 이름과 인터넷 주소 (0) | 2023.05.28 |
[Network] 7. 소켓의 우아한 종료 (0) | 2023.05.28 |