입출력 전반
-
입력과 출력은 정보 전달을 위한 핵심 과정이다. 프로세스 간의 정보 전달을 궁극적인 목적으로 하는 소켓 프로그래밍에서도 입출력은 핵심 절차이다.
-
입출력 시스템의 동작 원리에 대해서 이해한다면 제작하는 프로그램의 완성도를 높일 수 있을 것이다.
파일 디스크립터, 파일 테이블, 파일
-
파일 디스크립터는 프로세스가 파일에 접근할 때, 파일을 식별하기 위한 이름표이다. 소켓 또한 생성되면 파일 디스크립터 (소켓 디스크립터)가 부여된다.
-
운영체제는 프로세스가 접근할 수 있는 자원인 파일을 다루기 위해서 프로세스별로 파일 디스크립터 테이블을 관리한다.
-
예를 들어서 프로세스 A가
open()
함수를 이용하여 파일을 열었다면 프로세스 A의 파일 디스크립터 테이블에 파일에 접근하기 위한 정보가 기록된다. -
또한 운영체제는 시스템 전체에 열려 있는 모든 파일 정보(장치 및 소켓 포함)가 저장된다.
-
디스크립터 테이블에는 다음과 같은 두 가지 정보가 저장된다.
- 파일 디스크립터의 속성 정보를 저장하고 있는
flags
- 운영체제가 관리하는 파일 레코드를 가리키는 포인터
- 파일 디스크립터의 속성 정보를 저장하고 있는
-
파일 테이블에는 다음과 같은 세 가지 정보가 저장된다.
file offset
: 열려 있는 파일 중 현재 읽기 또는 쓰기가 실행될 위치를 저장status flag
:open
시스템 콜에서 지정하는 파일의 정보들을 저장- 실제 파일 위치를 가리키는 포인터
파일 디스크립터 테이블과 열린 파일 테이블의 엔트리(Entry)
-
open()
함수 또는socket()
함수를 이용하면 프로세스 단위로 파일 디스크립터를 관리하기 위해서 사용되는 파일 디스크립터 테이블에 하나의 엔트리가 생성된다. -
이 엔트리에는 열린 파일 테이블의 엔트리를 참조하기 위한 포인터가 포함되어 있다. 또한, 앞의 함수들은 시스템 전체의 열린 파일을 관리하기 위해 사용되는 열린 파일 테이블의 엔트리도 생성하는 역할을 한다.
- 해당 엔트리는 실제 파일이나 소켓이 가리키는 포인터를 가지고 있다. 같은 파일을 서로 다른 두 프로세스가 열 경우에 열린 파일 테이블의 엔트리는 두개가 되지만, 두 엔트리에서 가리키는 실제 저장소는 하나가 된다.
- 프로세스가
fork()
함수에 의해서 복제되면 같은 파일 테이블 엔트리를 가리키는 파일 디스크립터 테이블 엔트리가 생성된다.
dup()
계열 함수를 호출하면 파일 디스크립터를 복사할 수 있다.dup
함수에 의해서 생성된 파일 디스크립터는 원본 파일 디스크립터와 동일한 열린 파일 테이블 엔트리를 참조한다.
int dup(int oldfd);
결과값 : 복사된 파일 디스크립터(새로 부여된 디스크립터)
- 같은 프로세스 내에서 파일 디스크립터를 복제하는 함수이다.
int dup2(int oldfd, int newfd);
결과값 : 복사된 파일 디스크립터 (새로 부여된 디스크립터)
파일 오프셋이란?
-
파일 오프셋은 읽기/쓰기의 기준이 되는 포인터 값이다.
-
오프셋은 읽기/쓰기의 기준이 파일의 시작지점으로부터 몇 바이트 떨어져있는지를 나타낸다. 예를 들어서,
read()
함수를 호출하여 5바이트를 읽는다면 오프셋 값을 기준으로 파일에서 5바이트의 데이터가 읽혀진다. 또한 이 경우에 오프셋 값은 5만큼 증가한다. -
물론 소켓에서는 오프셋 값은 의미가 없지만, 파일 입출력을 다룰 때는 오프셋 값에 주의해야한다.
-
여기서 주목할만한 포인트는 오프셋 값은 파일 디스크립터 테이블이 아니라, 열린 파일 테이블에 있다는 점이다.
-
따라서
dup()
로 복제한 파일 디스크립터는 오프셋 값을 원본 디스크립터와 공유한다. 반면에 개별적인open()
함수를 통해서 열린 파일 테이블에 독립적인 엔트리를 생성한 두 파일디스크립터들은 오프셋을 공유하지 않는다.
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
인자
- fd: 오프셋 값을 조회하거나 변경하고자하는 파일을 가리키는 디스크립터
- offset: 변경할 오프셋의 크기
- whence: 오프셋 변경의 기준이 되는 지점 (세 가지 매크로를 제공한다.)
결과값
- 변경된 매크로 값 반환, 실패시 -1
- whence 값으로 사용가능한 매크로
- SEEK_SET: 파일의 시작 지점
- SEEK_CUR: 현재 오프셋 값
- SEEK_END: 파일의 마지막 지점 + 1
예제
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char *argv[]) {
int fd1, fd2, fd3;
char buff[BUFSIZ];
int read_len, n_write;
if (argc != 2) {
printf("usage: %s filename \n", argv[0]);
return -1;
}
fd1 = open(argv[1], O_RDWR | O_CREAT | O_TRUNC);
fd2 = open(argv[1], O_RDONLY);
fd3 = dup(fd1);
fgets(buff, BUFSIZ - 1, stdin);
read_len = strlen(buff);
n_write = write(fd1, "abc", 3);
n_write += write(fd1, buff, read_len + 1);
if (n_write == -1) printf("error");
printf("offset (fd1): %d\n", (int) lseek(fd1, 0, SEEK_CUR));
printf("offset (fd2): %d\n", (int) lseek(fd2, 0, SEEK_CUR));
printf("offset (fd3): %d\n", (int) lseek(fd3, 0, SEEK_CUR));
read(fd2, buff, 4);
write(1, buff, 4);
printf("-----\n");
printf("offset (fd1): %d\n", (int) lseek(fd1, 0, SEEK_CUR));
printf("offset (fd2): %d\n", (int) lseek(fd2, 0, SEEK_CUR));
printf("offset (fd3): %d\n", (int) lseek(fd3, 0, SEEK_CUR));
close(fd1);
fgets(buff, BUFSIZ - 1, stdin);
read_len = strlen(buff);
n_write += write(fd3, buff, read_len);
lseek(fd2, 0, SEEK_SET);
memset(buff, 0, BUFSIZ);
read_len = read(fd2, buff, n_write);
write(1, buff, n_write);
close(fd2);
close(fd3);
return 0;
}
-
파일 디스크립터 fd1, fd2, fd3를 생성하는데, fd1, fd2는 각각
open()
함수로 열고 fd3는 fd1을dup()
함수를 통해서 복제한 것이다. -
프로그램을 실행하면서 인자로
test.txt
를 넘겼다. 따라서 실행 파일과 같은 폴더에test.txt
파일이 생성된다. -
그 다음으로는 사용자에게 문자열을 입력 받는 부분인데, 여기서는
this i a test file
이라는 문자열이 입력된다. -
문자열 입력 후 파일 디스크립터
fd1
을 이용하여 파일에 쓰기 작업을 수행한다. 이때 사용자에게 입력 받은 문자열에 앞서 “abc” 라는 문자열을 저장한다. 따라서 문자열 24개와 엔터를 포함한 25개가 오프셋 값으로 저장된다. -
fd2는 fd1과 같은 파일을 가리키고 있지만 서로 다른 열린 파일 테이블을 가리키고 있으므로 오프셋의 값이 0이라는 것을 확인할 수 있다.
-
그 후에 fd2를 이용하여 파일의 4글자 값을 읽었을 때 오프셋 값이 증가한 것을 확인할 수 있다.
소켓 전용 입출력 함수
-
지금까지 TCP 소켓에서는 읽기 쓰기는 저수준 입출력 함수인
read
함수와write
함수를 이용했다. -
read()
,write()
함수는 리눅스의 추상화된 파일 (파일, 소켓, 디바이스 장치)에서 공통적으로 사용할 수 있는 저수준 입출력 함수이다. -
이러한 함수와는 별개로 소켓 전용 입출력 함수에 대해서 알아보도록 하자.
ssize_t send(int socket, const void *buffer, size_t length, int flags);
인자
-
socket: 데이터 전달을 위해서 사용하고자 하는 소켓 디스크립터
-
buffer: 전달하려고 하는 데이터를 저장하고 있는 공간의 주소
-
length: 전달하려고 하는 데이터의 길이
-
flag: 데이터 전송 시 사용할 수 있는 옵션들
-
send()
함수는write()
함수보다 하나의 인자를 더 가지고 있다. -
소켓 입출력에서 필요한 옵션들을 지정하는데 사용할 수 있는
flags
라는 인자이다. -
flags
는 파일을open
할 때 사용하는flags
처럼 여러 설정 값의 비트 연산으로 구성된다. -
flags
인자에 대해서는 읽기 기능을 수행하는 소켓 전용 함수인recv
를 다룬 후에 자세히 알아보자.
ssize_t recv(int socket, void *buffer, size_t length, int flags);
인자
- socket: 데이터 읽기 작업을 위해 사용하고자하는 소켓 디스크립터
- buffer: 읽어온 데이터를 저장하려고 하는 공간의 주소
- length: 읽으려고 하는 데이터의 최대 길이
- flag: 데이터 읽기 작업을 위해 사용할 수 있는 옵션들
flags
-
두 함수에서 공통으로 사용하는
flags
인자에 대해서 알아보자. -
MSG_OOB: Out of Band 데이터 전송을 위한 옵션이다. TCP의 Urgent 기능에 관한 것으로 MSG_OOB가 설정되면 Urgent 데이터로 취급하게 된다.
-
MSG_DONTWAIT: 입출력 함수의 동작이 블록 되지 않게 된다. 즉 논 블록으로 동작하게 설정한다.
-
다음은
send()
함수에서 사용할 수 있는flags
이다. -
MSG_DONTROUTE: 메시지가 라이터를 통해 다른 네트워크로 전달되는 것을 막는다.
-
MSG_PEEK: 수신 버퍼에 데이터가 들어있는지를 알아보기 위해서 사용한다.
MSG_PEEK
옵션이 활성화 되어 있는 상태에서recv
함수로 읽어진 데이터는 수신 버퍼에서 지워지지 않는다.
Urgent 데이터
- TCP 기능중에서는 전송하는 데이터 중에 긴급한 메시지가 있음을 알리는 기능이 있다.
“전송하는 데이터 중에서 긴급한 메시지가 있으니, 이 부분을 확인하시오.”
-
이러한 긴급한 데이터의 전송은 주목적이 아니라 특수한 목적이기 때문에
In Band
의 반대말인 Out of Band(OOB) 전송으로 불린다. -
예제를 통해서 알아보자.
예제
- Urgent 데이터 전송을 위한 서버 / 클라이언트 프로그램
- 클라이언트 프로그램은 서버 프로그램으로 다음과 같은 데이터 순서대로 전송한다.
- Normal_MSG1
- Normal_MSG2
- Urgent_MSG
- Normal MSG4
- 3번째 데이터는 Urgent Flag를 셋팅하여 전송한다.
- 서버 프로그램은 클라이언트 프로그램이 전송하는 데이터를 화면에 출력한다.
클라이언트 프로그램
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int connect_sd;
struct sockaddr_in client_addr;
int client_addr_len, read_len;
char write_buffer[BUFSIZ];
if (argc != 3) {
printf("usage: %s <IP Address> <Port Number>\n", argv[0]);
return -1;
}
connect_sd = socket(PF_INET, SOCK_STREAM, 0);
printf("=== client program ===\n");
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_addr.s_addr = inet_addr(argv[1]);
client_addr.sin_port = htons(atoi(argv[2]));
connect(connect_sd, (struct sockaddr *) &client_addr, sizeof(client_addr));
send(connect_sd, "normal_msg1", strlen("normal_msg1"), 0);
send(connect_sd, "normal_msg2", strlen("normal_msg2"), 0);
send(connect_sd, "urgent_msg", strlen("urgent msg"), MSG_OOB);
send(connect_sd, "normal_msg3", strlen("normal_msg3"), 0);
close(connect_sd);
return 0;
}
서버 프로그램
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
int listen_sd, connect_sd;
void urgent_handler(int sig);
int main(int argc, char *argv[]) {
struct sockaddr_in server_addr, client_addr;
int client_addr_len, read_len, str_len, state;
char read_buffer[BUFSIZ];
pid_t pid;
struct sigaction act;
if (argc != 2) {
printf("usage: %s [port number]\n", argv[0]);
return -1;
}
act.sa_handler = urgent_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
printf("server start...\n");
listen_sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[1]));
bind(listen_sd, (struct sockaddr *) &server_addr, sizeof(server_addr));
listen(listen_sd, 5);
client_addr_len = sizeof(client_addr);
connect_sd = accept(listen_sd, (struct sockaddr *) &client_addr, &client_addr_len);
fprintf(stderr, "a client is connected...\n");
fcntl(connect_sd, F_SETOWN, getpid());
state = sigaction(SIGURG, &act, 0);
while (1) {
read_len = recv(connect_sd, read_buffer, sizeof(read_buffer), 0);
read_buffer[read_len] = '\0';
printf("client: %s\n", read_buffer);
if (read_len == 0) {
close(connect_sd);
break;
}
}
fprintf(stderr, "the client is disconnected.\n");
close(listen_sd);
return 0;
}
void urgent_handler(int sig) {
int read_len;
char read_buffer[BUFSIZ];
read_len = recv(connect_sd, read_buffer, sizeof(read_buffer), MSG_OOB);
read_buffer[read_len] = '\0';
printf("client(urgent): %s\n", read_buffer);
}
-
OOB
전송에 대해서 정리하자면 긴급 데이터 처리를 위해서OOB
전송을 하더라도 데이터가 빨리 전송되는 것은 아니며 데이터 중에 한 바이트만 긴급 데이터로 취급된다는 것을 알 수 있다. -
“이 세그먼트에 긴급한 데이터가 있다.” 정도의 정보만 수신자 측에 전달할 수 있는 것이다.
표준 입출력 함수
- 시스템 콜은 사용자 모드와 커널 모드를 전환하기 때문에 자원을 많이 소모한다. 사용자 A와 사용자 B가 각각 100 바이트의 데이터를
WRITE
함수를 사용하여 파일을 작성한다고 생각을 해보자.
- 사용자 A는 100 바이트를 작성하는
write()
함수를 한 번 호출하여 100 바이트를 작성한다.- 사용자 B는 10 바이트를 작성하는
write()
함수를 10번 호출하여 100 바이트를 작성한다.
-
파일을 입력할 때마다
write()
함수를 호출하는 대신에 누군가가 입력할 데이터를 모아서 데이터가 충분히 많아졌을 때write()
함수를 호출하면 시스템 콜을 호출하는 횟수를 줄일 수 있을 것이다. -
표준 입출력 함수는 자체적으로 버퍼를 가지고 있기 때문에 시스템 콜 실행 횟수를 줄이는 기능을 할 수 있다.
스트림
-
표준 입출력 함수들은 시스템 콜의 실행 횟수를 줄이는 기능 이외에 문자열 처리를 쉽게 해준다는 장점이 있다.
-
소켓 프로그래밍을 할 때도 표준 입출력 함수들을 활용하면 복잡한 문자열 작업을 편하게 할 수 있는 경우가 많다.
-
스트림은 표준 입출력에서의 파일 시스템을 추상화한 개념이다.
fopen()
,fclose()
같은 표준 입출력 함수는 파일에 접근하기 위해서 FILE 구조체의 포인터를 사용한다. -
초기 제작 의도와는 다르게 스트림을 FILE 구조체의 포인터 자체로 간주하는 사람들도 많다.
-
표준 입출력 함수를 이용하는 프로그램들은 아래 소스와 같은 흐름을 따른다.
FILE *fp;
fp = fopen("filename.txt", "r");
...
fprintffp, "....");
....
fclose(fp);
- 위에서 처럼
fscanf()
,fprintf()
,fgets()
,fputs()
등등의 표준 입출력 함수를 사용하려면, 파일을 가리키는 FILE형 포인터 즉, 스트림을 생성해야한다.
FILE *fdopen(int fd, const char *mode);
- 파일 디스크립터로부터 스트림을 생성하는 방법은
fdopen()
함수를 이용하는 것이다.
int fileno(FILE *stream)
- 반대로 스트림에서 파일 디스크립터를 추출하는 것도 가능하다. 이때는
fileno
함수를 이용한다.
표준 입출력 버퍼 관리
-
파일 스트림에
fprintf()
나fputs()
같은 함수를 이용하여 쓰기 작업을 하면write()
관련 시스템 콜이 실행되어 파일 쓰기 작업이 수행된다. -
그러나 표준 입출력 함수의 사용이 시스템 콜로 바로 이어지는 것은 아니다. 표준 결과물은 표준 입출력 버퍼에 저장되어 있다가 상황에 맞게 한 번의 시스템 콜로 처리된다.
-
이러한 버퍼링은 시스템 콜의 횟수를 줄일 수 있다는 장점을 제공하는 동시에 버퍼링되어 있는 시간만큼 해당 작업이 지연된다는 단점을 가져올 수 있다.
-
따라서 경우에 따라 버퍼에 있는 내용을 강제로 배출하는 기능이 필요한다. 이러한 기능은
fflush()
함수가 수행한다.
int fflush(FILE *stream);
- 또한 표준 입출력에서 사용되는 버퍼의 크기를 설정하는 것이 가능하다.
void setbuff(FILE *stream, char *buf);
예제 - 표준 입출력 함수를 이용한 소켓 프로그래밍
클라이언트
nclude <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
int connect_sd;
struct sockaddr_in client_addr;
int client_addr_len, read_len;
char write_buffer[BUFSIZ];
FILE *wfp;
if (argc != 3) {
printf("usage: %s <IP Address> <Port Number>\n", argv[0]);
return -1;
}
connect_sd = socket(PF_INET, SOCK_STREAM, 0);
printf("=== client program ===\n");
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_addr.s_addr = inet_addr(argv[1]);
client_addr.sin_port = htons(atoi(argv[2]));
connect(connect_sd, (struct sockaddr *)&client_addr, sizeof(client_addr));
wfp = fdopen(connect_sd, "w");
while (1) {
printf("send msg: ");
fgets(write_buffer, BUFSIZ, stdin);
fputs(write_buffer, wfp);
fflush(wfp);
if (!strcmp(write_buffer, "END\n")) break;
}
fclose(wfp);
return 0;
}
서버
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int listen_sd, connect_sd;
struct sockaddr_in server_addr, client_addr;
int client_addr_len, read_len, str_len, state;
char read_buffer[BUFSIZ];
FILE *rfp;
if (argc != 2) {
printf("usage %s [port number]\n", argv[0]);
return -1;
}
printf("server start...\n");
listen_sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[1]));
bind(listen_sd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(listen_sd, 5);
client_addr_len = sizeof(client_addr);
connect_sd = accept(listen_sd, (struct sockaddr*)&client_addr, &client_addr_len);
fprintf(stderr, "a client is connected...\n");
rfp = fdopen(connect_sd, "r");
while (!feof(rfp)) {
fgets(read_buffer, BUFSIZ, rfp);
printf("%s", read_buffer);
}
fprintf(stderr, "the client is disconnected. \n");
fclose(rfp);
return 0;
}
참고 문헌
>> Home