관리 메뉴

Value Creator의 IT(프로그래밍 / 전자제품)

Chapter 10 멀티 프로세스 기반의 서버 구현 본문

1. 프로그래밍/4) Network

Chapter 10 멀티 프로세스 기반의 서버 구현

valuecreatort 2019. 10. 30. 23:54
반응형

1. 프로세스란?

실행중인 프로그램을 의미한다.

멀티프로세스 운영체제는 다수의 프로세스를 동시에 실행 가능하다.

프로세스마다 프로세스 ID가 할당된다.

 

2. Fork 함수 : 프로세스를 복사하는 함수.

복사되어서 생긴 프로세스는 자식 프로세스, 기존의 프로세스는 부모 프로세스라고 부른다.

#include <stdio.h>
#include <unistd.h>
int gval=10; //글로벌 변수

int main(int argc, char *argv[])
{
	pid_t pid; //프로세스 ID 타입의 변수 선언 
	int lval=20; //지역변수 
	gval++, lval+=5; //gval = 11, lval=25
	
	pid=fork(); //자식 프로세스 생성 	
	if(pid==0)	// if Child Process
		gval+=2, lval+=2; //자식프로세스의 변수값 변화, gval = 13, lval = 27 
	else			// if Parent Process
		gval-=2, lval-=2; //부모 프로세스의 변수값 변화, gval = 9, lval = 23
	 
	if(pid==0)
		printf("Child Proc: [%d, %d] \n", gval, lval); 
	else
		printf("Parent Proc: [%d, %d] \n", gval, lval);
	return 0;
    //결론 : 똑같은 프로그램이지만 다른 메모리에 할당된 자식프로세스와 부모프로세스의 동작을 알아보았다.
}

 

3, 좀비 프로세스 확인

자식 프로세스가 종료되었다는 것을 부모에게 알리지 않는다면, 자식 프로세스는 계속 살아있다.

그것을 좀비 프로세스라고 부른다.

(자식 프로세스가 종료되는 상황 : 인자를 전달하면서 exit를 호출하거나, main 함수에서 return 문을 실행하면서 반환)

아래 코드로 좀비 프로세스를 확인할 수 있다.

 

zombie.c

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	pid_t pid=fork();
	
	if(pid==0)     // if Child Process
	{
		puts("Hi I'am a child process");
	}
	else
	{
		printf("Child Process ID: %d \n", pid);
		sleep(30);     // Sleep 30 sec.
	} // 부모 프로세스와 자식 프로세스가 각각 실행된다.
    //자식 프로세스에서는 "Hi I'am a child process"가부모 프로세스에서는 

	if(pid==0)
		puts("End child process");
	else
		puts("End parent process");
	return 0;
}

 

 

 

 

4, 좀비 프로세스 해결 : wait 함수

 

wait.c

 

#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(); //첫 번째 자식프로세스 생성 
	
	if(pid==0)
	{
		return 3;  //첫 번째 자식 프로세스가 3을 반환하면서 종료 
	}
	else
	{
		printf("Child PID: %d \n", pid); // 첫 번째 자식 프로세스의 PID 출력 
		pid=fork(); //두 번째 자식 프로세스 생성 
		if(pid==0) 
		{
			exit(7); //두 번째 자식 프로세스가 7을 반환하면서 종료 
		}
		else
		{
			printf("Child PID: %d \n", pid); //두 번째 자식 프로세스의 PID 출력 
			wait(&status); //성공시 종료된 자식 프로세스의 PID, 실패시 -1 반환,,
            // wait 함수를 실행함으로써 status 라는 변수에 PID 또는 -1 값이 담긴다.
            // 단, wait 함수는 호출된 시점에 종료된 자식 프로세스가 없다면, 임의의 자식 프로세스가 종료될 때까지 블로킹 상태에 놓인다.
            // 함수 호출시 블로킹 상태 주의
            
			if(WIFEXITED(status)) //자식 프로세스가 정상 종료된 경우 true 
				printf("Child send one: %d \n", WEXITSTATUS(status)); //자식 프로세스의 반환 값 출력 

			wait(&status);
			if(WIFEXITED(status))
				printf("Child send two: %d \n", WEXITSTATUS(status)); //자식 프로세스의 반환 값 출력 	
			sleep(30);     // Sleep 30 sec.
		}
	}
	return 0;
}

 

 

결과

Child PID: 3859 
Child PID: 3860 
Child send one: 3 
Child send two: 7 

 

 

5. 좀비 프로세스 해결 2 : waitpid 함수

wait 함수의 블로킹 문제를 해결하기 위해 waitpid 함수를 사용한다.

 

waitpid.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int status;
	pid_t pid=fork();
	
	if(pid==0)
	{
		sleep(15);
		return 24;   	
	}
	else
	{
		while(!waitpid(-1, &status, WNOHANG))
		{
			sleep(3);
			puts("sleep 3sec.");
		}

		if(WIFEXITED(status))
			printf("Child send %d \n", WEXITSTATUS(status));
	}
	return 0;
}

/*
root@my_linux:/home/swyoon/tcpip# gcc waitpid.c -o waitpid
root@my_linux:/home/swyoon/tcpip# ./waitpid
sleep 3sec.
sleep 3sec.
sleep 3sec.
sleep 3sec.
sleep 3sec.
Child send 24 
*/

 

6. 시그널과 프로세스

프로세스에게 특정 메세지를 전달하는 것이 시그널이다.

SIGALARM : 알람 시간이 다 되면 시그널이 호출된다.

SIGINT : Ctrl+C가 눌리면 시그널이 호출된다.

SIGCHILD : 자식 프로세스가 종료되면 시그널이 호출된다.

 

signal.c

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig) //시그널 발생시 호출되는 함수는 '시그널 핸들러'라고 부른다.
{
	if(sig==SIGALRM) 
		puts("Time out!");
	
	alarm(2);	
}
void keycontrol(int sig)
{
	if(sig==SIGINT)
		puts("CTRL+C pressed");
}

int main(int argc, char *argv[])
{
	int i; 
	signal(SIGALRM, timeout);
	signal(SIGINT, keycontrol);
	alarm(2); 

	for(i=0; i<3; i++)
	{
		puts("wait...");
		sleep(100); //실제로는 100초를 대기하지 않는다. 
                    //시그널이 발생하면 sleep 상태가 해제되기 때문이다.
	}
	return 0;
}

 

sigaction.c

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

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;
}

 

시그널 핸들링을 통해 좀비 프로세스를 소멸시킬 수 있다.

 

 

remove_zombie.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

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", WEXITSTATUS(status));
	}
}

int main(int argc, char *argv[])
{
	pid_t pid;
	struct sigaction act;
	act.sa_handler=read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	sigaction(SIGCHLD, &act, 0); //프로세스가 종료될 때마다 read_childproc(int sig) 함수를 실행

	pid=fork();
	if(pid==0)
	{
		puts("Hi! I'm child process"); //첫 번째 자식프로세스가 10초 뒤 12를 반환하면서 종료함
		sleep(10);
		return 12;
	}
	else
	{
		printf("Child proc id: %d \n", pid); //첫 번째 자식프로세스의 PID
		pid=fork();
		if(pid==0)
		{
			puts("Hi! I'm child process"); //두 번째 자식프로세스가 10초 뒤 24를 반환하면서 종료함
			sleep(10);
			exit(24);
		}
		else
		{
			int i;
			printf("Child proc id: %d \n", pid); //두 번째 자식프로세스의 PID 
			for(i=0; i<5; i++)
			{
				puts("wait..."); //wait메세지 5초에 한 번씩 5번 출력 
				sleep(5);
			}
		}
	}
	return 0;
}

/*
root@my_linux:/home/swyoon/tcpip# gcc remove_zombie.c -o zombie
root@my_linux:/home/swyoon/tcpip# ./zombie
Hi! I'm child process
Child proc id: 9529 
Hi! I'm child process
Child proc id: 9530 
wait...
wait...
Removed proc id: 9530 
Child send: 24 
wait...
Removed proc id: 9529 
Child send: 12 
wait...
wait...

*/

 

SIGCHILD 라는 시그널을 이용하여서 자식 프로세스의 소멸을 확인할 수 있다.

 

 

 

 

7. 멀티 프로세스(멀티태스킹) 기반 다중 접속 서버

 

echo_mpserver.c

 

#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 BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	
	pid_t pid;
	struct sigaction act; //sigaction을 이용해서 자식 프로세스의 소멸을 확인한다.
	socklen_t adr_sz;
	int str_len, state;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	act.sa_handler=read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	state=sigaction(SIGCHLD, &act, 0);
	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)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	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)
		{
			close(clnt_sock); //자식 프로세스가 종료되면, 클라이언트 접속을 끊는다. 
			continue;
		}
		if(pid==0) //자식 프로세스 실행 영역
		{
			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); //클라이언트 소켓은 자식 프로세스에서 사용한다.
            					// 부모 프로세스의 클라이언트 소켓은 닫아준다. 
	}
	close(serv_sock);
	return 0;
}

void read_childproc(int sig)
{
	pid_t pid;
	int status;
	pid=waitpid(-1, &status, WNOHANG);
	printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

 

echo_client.c

--> 이전 글의 소스코드와 똑같다.

2019/10/30 - [1. 프로그래밍/4) Network] - Chapter 5 TCP 기반 서버 클라이언트 2

 

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

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;

	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(sock==-1)
		error_handling("socket() error");
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while(1) 
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
			break;

		write(sock, message, strlen(message));
		str_len=read(sock, message, BUF_SIZE-1);
		message[str_len]=0;
		printf("Message from server: %s", message);
	}
	
	close(sock);
	return 0;
}

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

 

 

 

 

8. TCP의 입출력 루틴 분할

Read와 Write를 각각 부모 프로세스와 자식 프로세스가 담당하도록 분할하는 것이다.

이렇게 되면 Read와 Write를 동시에 진행할 수 있다.

상황에 따라 입출력 분할하는 방법을 사용할 수도 있고, 기존의 방법대로 구현할 수도 있다.

 

echo_mpclient.c

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

#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);

int main(int argc, char *argv[])
{
	int sock;
	pid_t pid;
	char buf[BUF_SIZE];
	struct sockaddr_in serv_adr;
	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	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=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("connect() error!");

	pid=fork();
	if(pid==0) //자식 프로세스는 write routine 호출
		write_routine(sock, buf);
	else //부모 프로세스는 read routine 호출
		read_routine(sock, buf);

	close(sock);
	return 0;
}

void read_routine(int sock, char *buf)
{
	while(1) //부모 프로세스는 읽기명령어를 호출하고, 읽을 내용이 없으면 return 한다.
	{
		int str_len=read(sock, buf, BUF_SIZE);
		if(str_len==0)
			return;

		buf[str_len]=0;
		printf("Message from server: %s", buf);
	}
}
void write_routine(int sock, char *buf)
{
	while(1)
	{
		fgets(buf, BUF_SIZE, stdin); //자식 프로세스는 최대 30개 크기의 문자를 입력받는다. 
		if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n"))
		{	
			shutdown(sock, SHUT_WR); //쓰기 소켓(출력 소켓)을 닫고, return
			return;
		}
		write(sock, buf, strlen(buf)); //버퍼 크기 만큼 쓰기를 진행한다.
	}
}
void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

 

 

 

 

 

 

 

 

 

 

반응형
Comments