Stream (1) - Buffer
Stream
1. Stream을 사용하는 이유
프로그램은 정보를 전달 하거거나, 정보를 전달받는 작업의 연속이다.
정보를 주고받기 위해 Computer는 수많은 입출력(I/O) 장치와 연결된다.
Keyboard, Mouse, Monitor, Printer 등 모든 입출력 장치 마다 입출력 방법을 별도로 만드는 것은 불가능하다.
이것은 마치 C++ 언어가 CPU 제조사마다 다른거나, 애플이 C타입을 쓰지 않고 8핀, 12핀 독자 규격을 사용했던 것과 같다.
Stream은 여러 입출력 장치들 간에 데이터를 읽고 쓰는 통일된 방법이다.
입출력 장치의 종류에 상관없이, 운영체제는 Stream 표준으로 입출력 기기간에 데이터를 주고받는다.
장치가 달라지더라도, 항상 똑같은 절차를 통해 데이터를 읽고 쓰는 방법을 표준 입출력 스트림이라고 한다.
리눅스에서는 Network Socket의 데이터 송/수신, 모니터, 프린터, 파일 입출력 모두 동일한 방식을 사용한다.
그래서 네트워크 데이터 송/수신 함수의 이름이 read, write 인 경우가 있다.
윈도우는 방식이 다르며, 소켓(WinSock)은 send, recv(receive) 함수를 사용한다.
2. Stream 용어 이해
- 영어로 '흐르는 시냇물' 이며, 가상 연결 통로에서 바이너리 데이터가 시냇물처럼 흐르는 모습을 의미한다.
- 끊기지 않고 연속적으로 "데이터가 흐르는 과정"을 추상화(구현) 한 용어다.
- Data 가, Source(원본)에서 Destination(목적지)로 Block 단위로 전달되는 과정을 구현(추상화)한 기술을 의미한다.
- printf, scanf 함수의 내부 구현 구조를 모르더라도 사용할 수 있다. stream도 마찬가지다.
stream이라는 것을 사용하는 것만으로, 키보드에서 입력받은 Data 를 파일이나, 모니터로 내보낼 수 있다.
3. Stream(스트림)을 사용 예시
3.1 Stream 입출력 예시 1
3.2 Stream 입출력 예시 2 - 표준 입출력
4. stream 적용 예시
4.1 C언어, 표준 입출력 stream 사용 방법
- C언어, <stdio.h> 라이브러리를 사용해 표준 입출력을 사용한다.
printf( ) stdout stream
scanf( ) stdin stream
4.2 C++, 표준 입출력 stream 사용 방법
- C++, <iostream> 라이브러리를 사용해표준 입출력을 사용한다.
cin 객체
cout 객체
4.3 C언어, FILE stream 사용 방법
- 파일 입출력은, 표준 스트림 처럼 자동으로 Stream이 생성되지 않는다.
File* 라는 Stream을 의도적으로 생성시켜 사용한다.
그래서 의도적으로 생성한 stream 은 필수로 닫아줘야 한다. - File* fp = fopen("파일.txt", "w");
fp 라는 file stream 을 생성한 뒤 사용한다.
fprintf( fp , "Hello world\n");
fscanf( fp , " ");
4.4 소켓 stream 사용 방법
- Network 통신에서 Socket 으로 통신하면 socket stream 이 자동 생성된다.
- Socket 꼭! 닫아야 하는 이유는 stream을 닫아야 하기 때문이다.
- Database 도 마찬가지의 경우다.
4.5 Qt Stream 사용 방법
- Qt i/o 공식문서 링크
- Qt 네트워크에서 파일 송수신 방법
- QDataStream 을 socket에 연결한다. (로컬 파일 연결 작업과 동일)
- QByteArray 타입의 데이터를 stream으 주고 받으면 된다.
5. Linux 표준 입출력 Stream의 물리적 위치
- 리눅스의 경우, 최상위 경로에서 dev 디렉토리에서 확인 가능하다.
운영체제가 만들어 놓은 stdin, stdout, stderr 스트림을 확인 할 수 있다. - 위 이미지에서 오른쪽 0, 1, 2 번호는 파일 디스크립터(File Descriptor)이다.
- 프로세스(프로그램 실행)가 시작되면, stdin, stdout, stderr 정보와 함께 실행된다.
- File* 를 사용하면 File Stream이 생성되고 File Descriptor를 부여받는다.
- 아래 stream 모두 File Descriptor 라는 고유 정수 식별값을 부여받는다.
- 입력
- 출력
- 파일
- 프린터
- 모니터
- 데이터베이스
- 소켓
- ...
- 아래 stream 모두 File Descriptor 라는 고유 정수 식별값을 부여받는다.
- Socket 이 연결되면 Socket Stream이 생성되고 File Descriptor 를 부여받는다.
즉, 서버에서 연결된 Socket 들을 fd(File Descripor) 정수값으로 로 식별하여 Client를 구분 할 수 있다. - Qt 에서 Socket의 File Descriptor 확인 방법
- socket->socketDescriptor()
- QString::number(socket->socketDescriptor())
- File Descriptor 값은 stream에 부여되는 중복되지 않는 고유한 정수값으로 Socket 의 식별이 가능하다.
6. 파일 디스크립터(File Descriptor)
리눅스에서 파일 디스크립터(File Descriptor)란, C 언어와 같은 프로그래밍 언어가 아닌 시스템(OS)에 의해 관리되는 일련의 정수값으로 파일 또는 소켓의 식별에 사용된다.
*윈도우는 파일과 소켓을 구분한다.
▶ UNIX에서 파일을 새로 열면(open) int 타입의 정수를 리턴하는데, 이는 파일 기술자 테이블(file descriptor table) 의 index 번호다.
▶ 예를들어, 2개의 파일을 open하면 파일 기술자는 3과 4가 배정되며, 소켓을 생성하면 파일 기술자와 똑같은 기능과 역할을 하는 소켓 기술자(socket descriptor)가 리턴된다. 즉, 파일과 소켓이 기술자 테이블을 공유한다.
▶ 단순하게 설명하자면, 단체 생활에서 이름보다 번호를 사용하는 것이 편한것과 같다.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void)
{
int fd1, fd2, fd3;
fd1=socket(PF_INET, SOCK_STREAM, 0);
fd2=open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
fd3=socket(PF_INET, SOCK_DGRAM, 0);
printf("file descriptor 1: %d\n", fd1); // file descriptor 1: 3
printf("file descriptor 2: %d\n", fd2); // file descriptor 1: 4
printf("file descriptor 3: %d\n", fd3); // file descriptor 1: 5
// 3 부터 시작하는 이유는 0,1,2 는 표준 입출력에 할당되었기 때문이다.
// 아래 getchar(); 는 이 프로그램이 종료되지 않을 때 사용.
//getchar();
close(fd1);
close(fd2);
close(fd3);
return 0;
}
7. 실행중인 프로그램(Process)의 Stream과 File Descriptor
7.1 리눅스 프로세스 확인 방법
- 리눅스에서 실행중인 프로세스를 확인하는 방법은 아래와 같다.
ps au
7.2 실행 파일명으로 프로세스 아이디(PID) 찾기
- 실행중인 프로세스의 스트림을 확인하기 위해, 프로세스 아이디를 찾는다.
ps au | grep 실행파일명
이전 예제에서, getchar 부분의 주석을 제거하고 실행하여, 입력 전까지 프로세스가 종료되지 않도록 한다.
실행중인 프로세스의 아이디(PID)를 찾는다.
7.3 PID 경로에서 fd(File Descriptor) 확인
- 실행중인 프로세스 아이디 디렉토리로 이동하여, fd 경로에서 리스트를 확인하면, 실행중인 프로그램 내에서 생성한 file descriptor 리스트를 확인 할 수 있다.
cd /proc/프로세스아이디/fd
ls -al
표준 입출력의 파일 디스크립터는 프로세스가 실행되면 공유된다.
프로그램이 실행되어 생성한 파일 디스크립터(소켓2개, 파일1개)는 3,4,5 번의 스트림을 생성하고 프로세스 고유 fd를 할당받았다
Buffer(버퍼)
1. Buffer(버퍼)를 사용하는 이유
- 버퍼는 여러 입출력 장치간에 데이터를 읽고 쓰는 동안 발생하는 속도 차이를 해결한다.
- 1,000 바이트를 읽는다고 가정할 때, 1바이트를 여러 번 읽을 경우 CPU 사용횟수와 메모리 접근시 평균 탐색시간이 누적되어 한 번에 1000 바이트를 읽는 것 보다 시간이 오래 걸린다.
- 1바이트씩 1,000번 읽기
- 1,000바이트씩 1번 읽기
- 보통 8KB(8192Byte) 정도의 버퍼 크기를 유지한다.
2. Buffer(버퍼)란?
- 영어로 '완충제' 이다.
- Buffer(버퍼)는 스트림과 같은 데이터의 가상 연결 통로에서
데이터를 읽고 쓰는 동안 발생하는 속도 차이를 해결하기 위해
데이터가 임시 저장되는 메모리의 물리적 공간을 의미하는 용어다.
3. Buffer(버퍼)를 사용한 화면 출력 예시
입력 버퍼: 사용자와 저장장치의 입력은 프로그램의 연산 속도보다 훨씬 느리다.
프로그램이 이러한 입력의 완료를 대기하는 것은 낭비가 심하다.
출력 버퍼: 모니터의 출력은 프로그램의 연산 속도보다 매우 느리다.
프로그램이 모니터 출력 완료를 대기하는 것 또한 엄청난 낭비다.
*예) 컴퓨터 게임에서 화면 주사율 딜레이 및 수직 동기화 백버퍼, 프론트 버퍼 이슈가 발생하는 경우
: 프로그램의 연산 속도는 입력과 출력 시간에 비해 빠르다.
그래서 입력 버퍼와 출력 버퍼를 사용해 딜레이 문제를 보완한다.
4. Buffer 적용 예시
개발자가 소스코드 레벨에서 버퍼를 직접 구현하는 것도 한가지 방법이지만,
프로그래밍 언어가 제공하는 '표준 입출력 라이브러리'를 사용하면, 내부에 버퍼링 구현체를 손쉽게 사용 할 수 있다.
C언어, 표준 입출력 stream
- C언어, <stdio.h> 라이브러리를 사용해 표준 입출력을 사용한다.
printf( ) - 스트림과 연결된 버퍼 구현체 - stdout stream
scanf( ) - 스트림과 연결된 버퍼 구현체 - stdin stream
C++, 표준 입출력 stream
- C++, <iostream> 라이브러리를 사용해표준 입출력을 사용한다.
cin 객체
cout 객체
C/C++ 언어의 표준 입출력 라이브러기 기반 함수들은 내부적으로 입출력 버퍼를 사용하여 데이터를 처리한다.
표준 스트림을 사용하는 입출력 버퍼 구현체는 겉으로 드러나지 않는다.
다음과 같은 buffer 구현체는 표준 입출력 라이브러리가 사용하는 stdin, stdout, stderr 스트림의 버퍼가 아니다.
char buffer[100];
gets_s(buffer, sizeof(buffer));
5. "buffer 를 비운다"는 의미
입력 버퍼 사용 관점에서,
사용자가 데이터를 입력하면, 프로그램으로 바로 전달되는 것이 아니라 우선 입력버퍼에 데이터가 저장된다.
그리고 엔터 키를 치거나, 개행문자(\n)가 입력버퍼로 들어오거나, 입력버퍼가 가득차면,
버퍼를 비우면서 데이터를 프로그램으로 전달한다.
출력 버퍼 관점에서
프로그램에서 출력장치(모니터 등)로 데이터가 바로 전달되지 않고 우선 출력버퍼에 데이터가 저장된다.
그리고 개행문자(\n)가 출력버퍼로 들어오거나, 출력버퍼가 가득차면 버퍼를 비우면서 데이터를 출력장치로 전달한다.
"buffer 를 비운다" 는 의미는, "버퍼에 저장된 데이터가 버퍼를 떠나 목적지로 전송된다"는 의미에 가깝다.
6. 버퍼 사이즈 조절 예시
setvbuf(파일포인터, 사용자지정입출력버퍼, 모드, 크기);
int setvbuf(FILE *_Stream, char *_Buffer, int _Mode, size_t _Size);
// 설정 변경에 성공하면 0을 반환, 실패하면 0이 아닌 값을 반환
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 1024
int main()
{
char buf[BUFFER_SIZE];
setbuf(stdout, buf); // 표준 출력 스트림을 buf[BUFFER_SIZE]; 로 대체함
printf("Hello!"); // buf 배열에 쌓임
sleep(1);
printf("\n"); // buf 배열에 쌓임
sleep(1);
printf("check how setbuf works!!"); // buf 배열에 쌓임
sleep(1);
setbuf(stdout, NULL); // 버퍼가 해제되었으므로 한번에 출력
printf("How");
sleep(1);
printf(" are");
sleep(1);
printf(" you?");
sleep(1);
printf("\n");
exit(0);
}