I/O Multiplexing
입출력 다중화
- TCP 소켓으로 연결된 두 Process들의 I/O Port가 두 개 이상인 경우에 행해지는 Multiplexing이다.
- I/O Multiplexing은 네트워크 프로그램에 국한된 개념이 아니며, 다른 Application에서도 응용될 수 있다.
* I/O Multiplexing이 필요한 경우
- TCP Client가 다수의 Descriptor를 처리해야 하는 경우
- TCP Client가 다수의 Socket을 동시에 처리해야 하는 경우
- TCP Server가 Listening Socket과 Connected Socket을 모두 처리해야 하는 경우
- Server가 TCP와 UDP를 동시에 지원해야 하는 경우
- Server가 여러 서비스 혹은 프로토콜을 처리해야 하는 경우
I/O Models
- UNIX에서 사용 가능한 I/O Model은 크게 5가지로 존재한다.
- Input: 네트워크로부터 Data를 수신 \(\to\) 받은 Data를 Kernel에서 User Process로 복사
1) Blocking I/O Model
- 가장 기본적인 I/O Model이다.
- Socket의 Default Type이다.
- \(\texttt{read()}\)시에 Data가 입력될 때 까지 해당 \(\texttt{pthread}\)를 Block시키고 Scheduling을 수행한다.
2) Nonblocking I/O Model
- Blocking I/O 방식보다 더 나은 Performance를 보여준다.
- 여러 Server Farm에서 각각의 Server가 한 가지 일만 수행(최소한의 I/O만 수행)하고,
거의 모든 Resource를 특정 Job이 독점하는 경우에 적합하다.
3) I/O Multiplexing Model
- \(\texttt{select()}\)와 \(\texttt{poll()}\)을 이용한 방법이다.
(I/O Multiplexing을 위한 System Call들이다.)
- 개념적으로, 여러 Thread가 Blocking I/O를 실행하는 것으로 구현되어 있다.
- 근래에는 \(\texttt{pthread}\)를 이용한 POSIX Multithreading 방식으로 I/O Multiplexing이 수행된다.
4) Signal Driven I/O Model
- \(\texttt{SIGIO}\) Signal을 이용한 방법이다.
- I/O가 가능해진 시점에 Signal을 발생시키고, I/O를 요청한 Process(Thread)가 Cathcing하는 메커니즘이다.
5) Asynchronous I/O Model
- POSIX의 \(\texttt{aio()}\)를 이용한 방법이다.
- POSIX에서 Real Time Application을 지원하고자 만들어진 방법이다.
- 시스템 간 호환성이 가장 떨어지는 방법이다. (System Dependent)
- 또한, 프로그래밍적 장점, 퍼포먼스적 장점이 적다.
* Synchronous I/O Model : I/O Operation이 끝날 때 까지, read() 계열 System Call에 의해 해당 User-Process가 Block된다.
- Blocking I/O Model
- Nonblocking I/OModel
- I/O MultiplexingModel
- Signal Driven I/O Model
* Asynchronous I/O Model : I/O를 요청한 User-Process를 Block시키지 않는다.
- Asynchronous I/O Model
Blocking I/O Model
* 표현의 편의를 위해, 복잡한 TCP 대신, UDP를 통해 Blocking I/O Model을 표현한다.
- Application(User Process)에서는 recvfrom()을 통해, Kernel에 도착한 Datagram을 Application의 Buffer에 복사한다.
(이 때, Application Buffer에 복사될 때 까지, 해당 User Process는 Blocked된다.)
- wait for data 단계에서는 네트워크로부터 Datagram이 수신되기를 기다린다.
- copy data from kernel to user 단계에서는 Data를 Kernel에서 User 프로세스의 Memory로 복사한다.
- 단, wait for data가 수행된 직후, 곧바로 copy data from kernel to user가 수행된다는 보장은 없다.
(시스템의 Scheduling Algorithm에 따라 상이해지는 부분이다.)
- data가 입력되면, H/W Interrupt가 발생하여 H/W Interrupt Service Routine이 실행되고,
data를 Network Buffer(Network Interface Card의 Memory)에 옮겨놓는다.
※ 네트워크에 관련된 프로세스가 일반적으로 더 높은 우선순위를 갖는다.
- TIME_WAIT 상태에 있다가, TIME_OUT이 발생된 프로세스가 있는 경우, 해당 프로세스를 우선시하여 스케줄링 한다.
recvfrom() System Call (URL)
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buffer, size_t length, int flags,
struct sockaddr *restrict address, socklen_t *restrict address_len);
- recvfrom: Receive a Message from a Socket
- Connection Socket 혹은 Connectinoless Socket으로부터 메시지를 수신한다.
- Application이 수신한 데이터의 Source Address를 회수하는 것을 허용하기 때문에,
일반적으로 recvfrom()은 Connectionless Socket에서 많이 사용된다.
Return:
- 수신에 성공하면, 수신한 메세지의 크기를 Byte단위로 리턴한다.
- 수신할 메세지가 없거나, shutdown이 수행되면 0을 리턴한다.
- 그 이외의 경우 -1을 리턴하고 errno를 적절히 설정한다.
Arguments:
1) int sockfd
- Socket File Descriptor
2) void *restrict buffer
- 수신한 메시지를 저장할 Buffer의 Pointer
3) size_t length
- buffer의 크기
4) int flags
a) MSG_PEEK
b) MSG_OOB
c) MSG_WAITALL
5) struct sockaddr *restrict address
- Sending Address가 저장된 sockaddr Structure
- Null Pointer도 허용된다.
6) socklen_t *restrict address_len
- address의 크기
- Null Pointer도 허용된다.
Nonblocking I/O Model
- 요청한 I/O 작업을 Blocked 상태로 진입하지 않고 처리하게 하는 방식이며,
Block이 불가피한 경우, Blocked 상태로 진입하는 대신, 리턴문을 통해 종료한다.
- Blocking I/O Model과 같은 종류의 System Call을 이용한다.
(Nonblocking 방식이라 해서, 특별한 종류의 System Call을 사용하지 않는다.)
- Nonblocking 방식으로 진행하기 시작한 이후, Blocking 방식으로 전환할 수 없다.
- recvfrom()이 내부적으로 read()를 수행하는데,
Data가 준비되지 않으면 errno를 BWOULDBLOCK로 설정하고, read()는 -1을 리턴하고 끝나며, Block되진 않는다.
- 일반적으로, recvfrom()은 read()를 성공적으로 수행할 때 까지 CPU Quantum내에서 read()를 반복적으로 Polling한다.
(Spinlock 방식)
I/O Multiplexing Model
- select() 또는 poll()을 이용한 방법이다.
- select()를 호출하여 I/O를 할 수 있는 File Descriptor를 리턴받아 해당 File Descriptor를 통해 I/O를 수행한다.
- Application은 select()에서 Blocked되어 Datagram Socket으로부터 메시지를 수신할 때 까지 기다린다.
- select()로부터 Readable Socket을 리턴받으면,
Application은 recvfrom()을 호출하여 수신한 메시지를 Application Buffer에 복사한다.
- Blocking I/O Model에 대비적으로, I/O Multiplexing Model에서는 다수의 Descriptor를 처리할 수 있다.
- I/O Multiplexing Model은 Multi-Thread를 사용한 Blocking I/O Model과 유사하다.
* pthread Model
- 각각의 thread가 각각의 fd를 Blocking I/O하는 형태이다.
man page 요약하기
Signal-Driven I/O Model
- I/O Operation이 요청되면, Signal Handler로부터 SIGIO Signal이 발생되고,
이를 Catching하여 준비된 Desciptor에 대한 I/O Operation을 수행시키는 형태이다.
- 즉, 네트워크로부터 메시지를 수신할 때 까지 Block되어 있을 필요가 없다.
- Socket이 Signal에 의해 구동되는 형태이며, sigaction() System Call을 이용하여 Signal Handler를 지정한다.
(sigaction()은 곧바로 리턴되므로, Block될 염려가 없다.)
- 수신한 메시지를 읽을 준비가 되면, User-Process에 SIGIO Signal이 전달된다.
- 이 때, Signal Handler가 직접 recvfrom()을 수행할 수도 있고,
해당 User-Process에게 메시지를 Read할 것을 명령할 수도 있다.
Asynchronous I/O Model
- Signal-Driven I/O Model과 달리, I/O Operation이 완료된 후에야
Kernel은 User-Process에게 I/O가 완료되었음을 알린다.
- 즉, 네트워크로부터 수신된 메시지가 Application의 Buffer에 복사되고 나서야
I/O Operation이 완료되었다는 Signal이 User-Process에게 전달된다.
\(\texttt{select()}\) Function (URL)
* 참고 (URL)
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
/* Macro Functions */
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
- I/O를 수행할 Descriptor들이 Ready State가 될 때 까지 대기하는 System Call이다.
(Ready State : I/O Operation에 대한 준비를 마친 상태)
- 다수의 File Descriptor를 Monitoring할 수 있다.
* Constant: FD_SETSIZE
- fd_set Type의 Descriptor의 개수이다.
- 일반적으로, 1,024로 정의되어 있다.
* Type: fd_set
- File Descriptor들을 한 번에 관리하기 위한, Bit 단위 Type
- 이미지 출처 (URL)
Return:
- 성공적으로, select()가 수행된 경우, Ready 상태에 있는 Descriptor의 개수를 리턴한다.
- 어떤 File Descriptor도 Ready 상태로 들어가기 전에, Time-Out이 발생할 경우 0을 리턴한다.
- 에러가 발생되면, errno를 설정하고, -1을 리턴한다.
Arguments:
1) int nfds
- Monitoring할 File Descriptor 개수의 최댓값이다.
- nfds는 readfds, writefds, exceptfds 중 어떠한 File Descriptor의 개수보다도 커야 한다.
2) fd_set *readfds
- Reading에 대해, Ready 상태에 있는 File Descriptor들을 담고 있는 Pointer이다.
- Reading에 대해 Ready 상태에 있다 함은, Read Operation을 Block되지 않고 수행할 수 있는 상태임을 의미한다.
- select()가 리턴되면, readfds에서 Ready 상태에 있는 File Descriptor들을 제외한 모든 Descriptor들은 Clear된다.
3) fd_set *writefds
- Writing에 대해, Ready 상태에 있는 File Descriptor들을 담고 있는 Pointer이다.
- Writing에 대해 Ready 상태에 있다 함은, Write Operation을 Block되지 않고 수행할 수 있는 상태임을 의미한다.
- 단, 대량의 Write 연산의 경우 Block될 가능성이 있다.
- select()가 리턴되면, writefds에서 Ready 상태에 있는 File Descriptor들을 제외한 모든 Descriptor들은 Clear된다.
4) fd_set *exceptfds
- Exceptional Condition에 있는 File Descriptor들을 담고 있는 Pointer이다.
- select()가 리턴되면, exceptfds에서 Exceptional Condition이 발생한 File Descriptor들을 제외한
모든 Descriptor들은 Clear된다.
5) struct timeval *timeout
- File Descriptor가 Ready 상태에 놓일 때 까지의 대기시간을 지정한 값이다.
- timeout값이 NULL이면, 하나의 Descriptor중 하나가 I/O준비를 마칠 때 까지 대기하며,
timeout값이 0이면, 대기하지 않고, Descriptor를 점검한 후 바로 리턴하며,(Polling시에 유용하게 사용된다.)
timeout에 특정 값이 지정되면, Descriptor 중 하나가 I/O 준비를 마치거나 해당 값 만큼은 대기한 후에 리턴된다.
* \(\texttt{select()}\)가 Blocked 상태에서 해제되는 조건
- File Descriptor가 Ready 상태에 놓이는 경우
- Signal Handler에 의한 Interrupt가 발생한 경우
- Timeout이 발생한 경우
* \(\texttt{timeval}\) Structure
#include <sys/time.h>
struct timeval {
long tv_sec; // sec
long tv_usec; // micro-sec
};
* Descriptor Ready Condition (디스크립터 준비 조건)
Socket이 Read Operation에 대한 Ready State에 있기 위한 조건 (Readable Condition) | |
1) Socket Receive Buffer에 있는 데이터의 크기가 현재 Receive Buffer의 Low-Water Mark보다 크거나 같은 경우 | - 이 경우, Socket에 대한 Read Operation은 Block되지 않고 진행된다. - SO_RCVLOWAT 옵션을 이용하여 Receiving Buffer 크기의 하한선을 지정할 수 있다. (TCP, UDP의 Buffer Low-Water Mark의 Default는 1이다.) |
2) TCP Connection이 FIN을 수신한 경우 | - 이 경우, Read Operation은 Block되지 않고, 0(EOF)을 리턴한다. |
3) Listening Socket이 하나 이상의 Connection을 유지하고 있는 경우 | - 보통, Listening Socket에 대한 accept()는 Block되지 않는다. |
4) Socket Error가 지속되고 있는 경우 | - 이 경우, Socket에 대한 Read Operation은 Block되지 않으며, Read Operation은 errno에 특정 에러 상수를 설정하고, -1을 리턴한다. - SO_ERROR 옵션을 지정하여 getsockopt() 호출해 지속되고 있는 에러에 대처할 수 있다. |
Socket이 Write Operation에 대한 Ready State에 있기 위한 조건 (Writable Condition) | |
1) Send Buffer의 사용 가능한 공간이 Send Buffer의 Low-Water Mark보다 크거나 같은 경우 | - Socket을 Nonblocking으로 지정하면 Write Operation은 Block되지 않는다. - SO_SNDLOWAT 옵션을 이용하여 Send Buffer의 Low-Water Mark 값을 지정할 수 있다. (TCP, UDP의 Send Buffer의 Low-Water Mark의Default는 2,048이다.) |
2) Connection의 Half가 Closed된 경우 | - 이 경우, Write Operation은 SIGPIPE Signal을 발생시킨다. |
3) Nonblocking connnect()를 사용하는 Socket이 Connection에 성공했거나, connect()에 실패한 경우 | |
4) Socket Error가 지속되고 있는 경우 | - 이 경우, Socket에 대한 Write Operation은 Block되지 않으며, Write Operation은 errno에 특정 에러 상수를 설정하고, -1을 리턴한다. - SO_ERROR 옵션을 지정하여 getsockopt() 호출해 지속되고 있는 에러에 대처할 수 있다. |
Socket이 Exception State에 있기 위한 조건 (Exception Condition) | |
1) Socket에 Out-of-Band Data가 있는 경우 | |
2) Socket이 Out-of-Band Mark에 있는 경우 |
* Low-Water Mark
- Buffer에서 내보낼 데이터의 최소 단위
- 즉, Low-Water Mark에 지정된 값만큼의 데이터는 모여야 Buffer에서 빠져나갈 수 있다.
※ Summary of Conditions that cause a Socket to be Ready for \(\texttt{select()}\)
Condition | is Readable? | is Writable? | is Exception? |
Data to read | V | ||
Read half of the connection closed | V | ||
New connection ready for listening socket | V | ||
Space available for writing | V | ||
Write half of the connection closed | V | ||
Pending error | V | V | |
TCP out-of-band data | V |
\(\texttt{str_cli()}\) Function (User-Defined)
- Echo Program의 Clienct-Site 프로그램에서 str_cli()에 select()를 사용함으로써 아래와 같은 기능을 구현할 수 있다.
1) 상대 TCP로부터 데이터를 수신하면, Socket은 Readable해지고, read()는 읽어들인 데이터의 수를 Byte 단위로 리턴한다.
2) 상대 TCP로부터 FIN을 수신하면, Socket은 Readable해지고, read()는 0을 리턴한다. (EOF Event에 대한 대처)
3) 상대 TCP로부터 RST를 수신하면, Socket은 Readable해지고, read()는 -1을 리턴하며, errno에 에러상수를 설정한다.
* \(\texttt{str_cli()}\) Function using \(\texttt{select()}\) (Figure 6.9)
#include "unp.h"
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) {
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) {
if (Fgets(sendline, MAXLINE, fp) == NULL)
return;
Writen(sockfd, sendline, strlen(sendline));
}
}
}
\(\texttt{fileno()}\) Function (URL)
#include <stdio.h>
int fileno(FILE *stream);
- \(\texttt{stream}\)에 할당된 Descriptor를 리턴한다.
Batch Input and Buffering
shutdown() Function
TCP Echo Server
pselect() Function
\(\texttt{poll()}\) Function (URL)
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
Reference: UNIX Network Programming Volume 1, 3E: The Sockets Networking API (W. Richard Stevens, Bill Fenner, Andrew M. Rudoff 저, Addison-Wesley, 2004)