• I/O multiplexing
    : synchronous, Blocking I/O 형태의 멀티 프로세스 서버, 멀티 스레드 서버 모두 고성능 서버로는 부적합하다.
    : 클라이언트마다 프로세스나 스레드를 할당하면 Context Switching 문제로 메모리적인 문제가 발생한다.
    -> 멀티플렉싱 개념 필요

    - poll
      ■ 1_poll
       : mkfifo fifo 파이프 만들기
       : IPC 전용 메커니즘
       : cat > myfifo로 리다이렉션

    #include <unistd.h>
    
    #include <stdio.h>
    #include <fcntl.h>
    
    int main()
    {
    	char buff[100];
    	int ret;
    
    	int fd = open("myfifo", O_RDWR);
    
    	while (1)
    	{
    		ret = read(0, buff, sizeof buff);
    		buff[ret] = '\0';
    		printf("Keyboard : %s\n", buff);
    
    		ret = read(fd, buff, sizeof buff);
    		buff[ret] = '\0';
    		printf("myfifo : %s\n", buff);
    	}
    }
    

      ■ 2_poll
    #include <unistd.h>
    
    #include <stdio.h>
    #include <fcntl.h>
    
    #include <poll.h>
    
    int main()
    {
    	char buff[100];
    	int ret;
    
    	int fd = open("myfifo", O_RDWR);
    
    
    	// 비동기적으로 이벤트를 처리할 디스크립터 배열
    	struct pollfd fds[2];
    
    	while (1)
    	{
    		fds[0].fd = 0;
    		fds[0].events = POLLIN;  // 관심 있는 이벤트의 종류
    														 // read  : POLLIN
    														 // write : POLLOUT
    
    		fds[1].fd = fd;
    		fds[1].events = POLLIN;
    
    
    		poll(fds, 2, -1);
    
    
    		// 주의할 점 : 이벤트가 동시에 발생할 수 있으므로
    		//             절대 else 로 묶으면 안된다.
    		if (fds[0].revents & POLLIN) 
    		{
    			ret = read(0, buff, sizeof buff);
    			buff[ret] = '\0';
    			printf("Keyboard : %s\n", buff);
    		}
    
    		if (fds[1].revents & POLLIN)
    		{
    			ret = read(fd, buff, sizeof buff);
    			buff[ret] = '\0';
    			printf("myfifo : %s\n", buff);
    		}
    	}
    }
    


    - select (참고: http://ozt88.tistory.com/21)
     : select는 싱글스레드로 다중 I/O를 처리하는 멀티플렉싱 통지모델의 가장 대표적인 방법이다.
     : 해당 파일 디스크립터가 I/O를 할 준비가 되었는지 알 수 있다면, 그 파일 디스크립터가 할당받은 커널 Buffer에 데이터를 복사해주기만 하면 된다. 이런 목적하에 통지모델은 파일디스크립터의 상황을 파악할 수 있게 하는 기능을 할 수 있어야한다. select는 많은 파일 디스크립터들을 한꺼번에 관찰하는 FD_SET 구조체를 사용하여 빠르고 간편하게 유저에게 파일 디스크립터의 상황을 알려준다.
     : select를 사용해서 I/O의 상황을 알기 위해서는 프로세스가 커널에게 직접 상황 체크를 요청해야한다. 프로세스가 커널의 상황을 지속적으로 확인하고 그에 맞는 대응을 하는 형태로 구성되기 때문에 프로세스와 커널이 서로 동기화된 상태에서 정보를 주고 받는 형태로 볼 수 있다. 따라서 select의 통지형태를 동기형 통지방식이라 부를 수 있다. 그리고 select 그 자체는 I/O를 담당하지 않지만, 통지하는 함수의 호출방식이 timeout에 따라 non-blocking 또는 blocking 형태가 된다. timeout을 설정하지 않으면, 관찰 대상이 변경되지 않는 이상 반환되지 않으므로 blocking 함수가 되고, timeout이 설정되면 주어진 시간이 지나면 시간이 다되었다는 정보를 반환하므로 non-blocking 함수가 된다. 

    #include  <stdio.h>
    
    #include  <unistd.h>
    #include  <sys/types.h>
    #include  <sys/select.h>
    #include  <sys/socket.h>
    
    #include  <arpa/inet.h>
    #include  <netinet/in.h>
    
    inline int max(int a, int b)
    {
    	return a > b ? a : b;
    }
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr = {0, };
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	bind(sock, (struct sockaddr*)&saddr, sizeof saddr);
    	listen(sock, 5);
    
    	// fd_set : bit array
    	fd_set socks;					// source data
    	fd_set readsocks;
    
    	FD_ZERO(&socks);
    	FD_SET(sock, &socks);
    
    	int maxfds = sock;
    
    	while (1) 
    	{
    		readsocks = socks;
    
    		int ret = select(maxfds + 1, &readsocks, 0, 0, 0);
    		printf("ret : %d\n", ret);
    
    		for (int i = 0 ; i < maxfds + 1 ; ++i)
    		{
    			if (FD_ISSET(i, &readsocks))
    			{
    				if (i == sock)
    				{
    					struct sockaddr_in caddr = {0, };
    					socklen_t clen = sizeof(caddr);
    					int csock = accept(sock, (struct sockaddr*)&caddr, &clen);
    
    					char* cip = inet_ntoa(caddr.sin_addr);
    					printf("Connected from %s\n", cip);
    
    					// 새로운 연결을 등록해주어야 한다.
    					FD_SET(csock, &socks); 
    					maxfds = max(maxfds, csock);
    				}
    				else
    				{
    					int csock = i;
    					char buf[1024];
    					int n = read(csock, buf, sizeof buf);
    					
    					if (n <= 0)
    					{
    						printf("연결 종료!!\n");
    						close(csock);
    
    						// 종료된 디스크립터를 등록 해지해야 한다.
    						FD_CLR(csock, &socks);
    					} 
    					else
    						write(csock, buf, n);
    
    				}
    			} 
    		}
    
    #if 0
    		while (1)
    		{
    			char buf[1024];
    			int n = read(csock, buf, sizeof buf);
    			if (n == 0)
    				close(csock);
    			else if (n == -1)
    				close(csock);
    
    			write(csock, buf, n);
    		}
    #endif
    	}
    
    	close(sock);
    }
    


  • Synchronous Blocking I/O
    : 지금까지의 서버는 하나의 클라이언트에 대해서만 요청을 처리할 수 있다.
    : 하나의 클라이언트 요청이 끝나기 전까지 다음 요청을 처리할 수 없다.
    => Iteration Server를 도입해야한다.

  • Multi Process Model


    - 7_server
     : fork()를 통한 프로세스 생성
     : 독립된 주소를 갖으므로 안정적이지만, 프로세스 복제에 대한 오버헤드가 크고, Context Switching 비용이 높다.
     : 데이터를 공유하기 위해 별도의 작업이 필요하다. (IPC)

    #include <stdio.h>
    #include <unistd.h>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/tcp.h>
    
    #include <arpa/inet.h>
    #include <string.h>
    
    #include <signal.h>    // signal
    #include <sys/wait.h>  // wait()
    
    // 4. 시그널은 중첩되서 발생하지 않는다. - 루프 도입
    // 5. 시그널 핸들러는 비동기적으로 동작합니다.
    //    핸들러 내부에서 블록킹 연산을 절대 수행하면 안됩니다.
    //  - waitpid
    
    void handleClient(int csock);
    void handleChildProcess(int signo)
    {
    	printf("Cleanup child Prcess!\n");
    	// while (wait(0) > 0)
    	while (waitpid(-1, 0, WNOHANG) > 0) 
    		;
    }
    
    int main() {
    	// 3. 자식 프로세스의 상태를 수거해야 한다.
    	//  -> 좀비 프로세스로 인한 메모리 누수를 방지할 수 있다.
    	// signal handler 등록
    
    	// sigaction
    	signal(SIGCHLD, handleChildProcess);
    
    
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr;
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY;
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	int ret = bind(sock, (struct sockaddr *)&saddr, sizeof saddr);
    	if (ret != 0) {
    		perror("bind()");
    		return -1;
    	}
    
    	listen(sock, 5);
    
    	// 하나의 클라이언트만 요청을 처리할 수 있다.
    	//  => Iteration Server를 도입해야 한다.
    
    	while (1)
    	{
    		struct sockaddr_in caddr;
    		socklen_t size = sizeof(caddr);
    
    		int csock = accept(sock, (struct sockaddr *)&caddr, &size);
    
    		char *client_ip = inet_ntoa(caddr.sin_addr);
    		printf("클라이언트 %s 가 접속되었습니다.\n", client_ip);
    
    
    		// 하나의 클라이언트 요청이 끝나기 전까지 다음 요청을 처리할 수 없다.
    		//  1) pid = fork() - 리턴값을 통한 부모 / 자식 분기
    		//   - 자식 프로세스 : 0
    		//   - 부모 프로세스 : 자식 프로세스 pid 
    
    		//  2) fork()가 수행되었을 경우, 사용하지 않는 핸들을 닫아야 한다.
    
    		pid_t pid = fork();
    		if (pid == 0)
    			handleClient(csock);
    
    		close(csock);
    	}
    	
    	close(sock);
    }
    
    #include <stdlib.h>
    
    void handleClient(int csock)
    {
    	int n;
    	char buf[1024];
    
    	while (1) {
    		n = read(csock, buf, sizeof(buf));
    
    		if (n <= 0) {
    			printf("연결 종료!\n");
    			break;
    		}
    
    		n = write(csock, buf, n);
    	}
    
    	// 연결 종료
    	close(csock);
    	exit(0);
    } 

  • Multi Thread Model
    - 8_server
     : pthread 도입
     : kernel 영역을 제외한 복제로 스레드 생성에 대한 오버헤드가 낮고, Context Switching 비용도 낮다. 또 주소를 공유하기 때문에 자원공유가 편리하다.
     :하나의 Thread가 문제가 발생할 경우 전체 Process가 죽을 수 있다.

    // # g++ 8_server.cc -o server -lpthread
    #include <stdio.h>
    #include <unistd.h>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/tcp.h>
    
    #include <arpa/inet.h>
    #include <string.h>
    
    #include <signal.h>    // signal
    #include <sys/wait.h>  // wait()
    
    #include <pthread.h>
    // 멀티 프로세스 모델은 안전성은 뛰어나지만, 반면에 
    // 주소 공간의 분리에 의한 데이터 공유가 힘듭니다.
    // - IPC를 사용해야 한다.
    
    // => 스레드 모델을 도입하자. (pthread)
    void* handleClient(void* arg);
    
    int main() {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in saddr;
    	saddr.sin_family = AF_INET;
    	saddr.sin_port = htons(4000);
    	saddr.sin_addr.s_addr = INADDR_ANY;
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	int ret = bind(sock, (struct sockaddr *)&saddr, sizeof saddr);
    	if (ret != 0) {
    		perror("bind()");
    		return -1;
    	}
    
    	listen(sock, 5);
    
    	while (1)
    	{
    		struct sockaddr_in caddr;
    		socklen_t size = sizeof(caddr);
    
    		int csock = accept(sock, (struct sockaddr *)&caddr, &size);
    
    		char *client_ip = inet_ntoa(caddr.sin_addr);
    		printf("클라이언트 %s 가 접속되었습니다.\n", client_ip);
    
    		pthread_t thread;
    		pthread_create(&thread, 0, handleClient, &csock);
    
    		// 종료 처리를 해야 한다. - join, detach
    		// exit(0); 는 프로세스의 종료 -> return 으로 변경
    		// ★ 스레드가 온전히 종료할수 있도록 만드는 것이 중요
    		// 스레드도 결과적으로 fork 를 사용하므로 스레드에 관한 종료가 필요 join 또는 detach
    
    		// join vs detach?
    		// pthread_join을 통하여 정상적으로 종료된 보조스레드의 자원을 정상적으로 반환시켜주며,
    		// ptrhead_detach함수를 통하여 스레드가 종료시 자원을 정상적으로 반환시켜주도록 하는 함수로
    		// 메모리릭을 방지할 수 있다.
    
    		// - pthread_detach
    		// 생성된 thread에서 pthread_detach를 하면 join할 필요없이 생성된 thread가 할일을 다 마치고
    		// pthread_exit를 하는 순간 자원이 OS에 반납(보장됨)
    		// --> detach(떼어내다), 생성된 스레드를 메인 스레드에서 분리시킨다.
    		// - 호출방법 : 선호출, 후호출 2가지 방식
    		// 선호출 :  int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    		// 후호출 : pthread_detach(p_thread);
    		// --> detach를 하려면 선호출로 하는것을 추천한다.
    		// why? 후호출을 하기전에 thread가 종료된다면 메모리릭 발생(희박한 가능성이지만...)
    
    		// - pthread_join
    		// 보조 스레드의 종료까지 대기해주는 함수(즉, 메인스레드의 종료 대기)
    		// int pthread_join(pthread_t th, void **thread_return);
    		// 첫번째 아규먼트 th는 기다릴(join)할 쓰레드 식별자이며, 두번째 아규먼트 thread_return은 쓰레드의 리턴(return) 값이다. thread_return 이 NULL 이 아닐경우 해다 포인터로 쓰레드 리턴 값을 받아올수 있다.
    		pthread_detach(thread);
    
    	}
    	
    	close(sock);
    }
    
    #include <stdlib.h>
    
    void* handleClient(void* arg)
    {
    	int csock = *(int*)arg;
    
    	int n;
    	char buf[1024];
    	while (1) {
    		n = read(csock, buf, sizeof(buf));
    
    		if (n <= 0) {
    			printf("연결 종료!\n");
    			break;
    		}
    
    		n = write(csock, buf, n);
    	}
    
    	// 연결 종료
    	close(csock);
    
    	return 0;
    }
    

    - 7_client ~ 8_client
    #include <unistd.h>
    
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    #include <stdio.h>
    #include <string.h>
    
    #include <linux/tcp.h>
    
    int main()
    {
    	int sock = socket(PF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in addr = {0, };
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(4000);
    	addr.sin_addr.s_addr   = inet_addr("0.0.0.0");
    
    	int option = true;
    	socklen_t optlen = sizeof(option);
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    
    	int ret = connect(sock, (struct sockaddr*)&addr, sizeof addr);
    
    	if (ret != 0) {
    		printf("접속 실패!!\n");
    		perror("connect()");
    		return -1;
    	}
    
    	while (1) {
    		char buf[1024];
    		scanf("%s", buf);
    
    		ret = write(sock, buf, strlen(buf));
    		if (ret <= 0)
    			break;
    
    		ret = read(sock, buf, sizeof buf);
    		if (ret <= 0)
    			break;
    
    		write(1, buf, ret);
    		putchar('\n');
    	}
    
    	close(sock);
    }
    


  • I/O Models
    - blocking I/O vs. nonblocking I/O
     ■ blocking I/O
      : I/O 작업은 유저 영역에서 직접 수행할 수 없고, 커널 영역에서만 가능하다. 따라서 I/O 작업을 하기 위해서는 커널에 I/O 작업을 요청해야한다.
      : I/O 작업이 진행되는 동안 유저 영역 (process)의 작업은 중단한채 대기해야한다. 이처럼 I/O 동작 (데이터를 받을 준비, 데이터를 커널 영역에서 유저 영역으로 복사 등)으로 인해 프로세스가 block 상태가 되는 것을 blocking 된다고 한다.




     ■ nonblocking I/O
      : I/O 작업을 진행하는 동안 유저 프로세스의 작업을 중단시키지 않는다. 
      : 유저 프로세스가 커널의 read를 기다리는 것이 아니라 system call을 이용하여 반복적으로 요청하고, 이에 대한 버퍼값이 존재하면 유저 영역으로 복사해준다.
      : 반복적인 system call로 리소스가 남용




    - I/O multiplexing (select, poll) 

     : non-blocking 모델의 문제를 해결하기 위해 고안

     : I/O 처리가 필요한 파일 디스크립터 등을 가려내서 알려준다.




    - signal driven I/O (epoll, SIGIO), RTS(Real-Time Signal)
       Reactor Pattern
        : synchronous, non-blocking I/O handling and relies on an event notification interface

      
     Proactor Pattern
        : 
    asynchronous, non-blocking I/O operations, as provided by interfaces such as POSIX AIO




    synchronous I/O vs. asynchronous I/O (POSIX aio_functions)
     : 동기(synchronous)와 비동기(asynchronous)는 서로 메시지를 주고받는 상대방이 어떤 방식으로 통신을 하는가에 대한 개념이다.
     : I/O 통지모델에서 대화하는 주체들은 커널과 프로세스이다. 프로세스는 커널에게 I/O처리를 요청하고, 커널은 프로세스에게 I/O 상황을 통지한다. 우선 I/O 요청은 반드시 동일하게 처리될 수 밖에 없는 부분이고, 결국에 커널이 프로세스에게 어떤 방식으로 통지하느냐에 따라 동기형이냐 비동기형이 결정될 것이다.
     

      ■ synchronous I/O
       : 동기형 통지모델의 프로세스는 커널에게 지속적으로 현재 I/O 준비 상황을 체크한다.
       : 즉 커널이 준비되었는지를 계속 확인하여 동기화 하는 것이다. 따라서 동기형 통지모델에서 Notify를 적극적으로 진행하는 주체는 유저의 프로세스가 되며 커널은 수동적으로 유저 프로세스의 요청에 따라 현재의 상황을 보고한다.
     

      ■ asynchronous I/O

       : 비동기형 통지모델은 일단 커널에게 I/O작업을 맡기면 커널의 작업 진행사항에 대해서 프로세스가 인지할 필요가 없는 상황을 말한다.
       : 유저의 프로세스가 I/O 동기화를 신경쓸 필요가 없기에 비동기형이라고 부를 수 있다. 따라서 비동기형 통지모델에서 Notify의 적극적인 주체는 커널이 되며, 유저 프로세스는 수동적인 입장에서 자신이 할일을 하다가 통지가 오면 그때 I/O 처리를 하게 된다. 




  • I/O Model 비교


참고: http://ozt88.tistory.com/20


+ Recent posts