본문 바로가기

system&OS

시스템 레벨 I/O에 대하여(1)

시스템 프로그래밍, 그 중에서 시스템 레벨 I/O 을 공부하면서 스스로 헷갈렸던 것을 정리해본다.

 

1. 파일 디스크립터가 무엇인가?

 

리눅스 시스템에는 파일을 관리하는 테이블이 있다. File Descriptor Table 이라고 부른다.

테이블은 array이다. 그리고 테이블의 각 element는 파일이다.

파일을 열면 테이블에 element를 추가한다.

그래서 index만 가지고 테이블에 접근해서 파일을 가리킬 수 있다.

index 를 바로 파일 디스크립터(file descriptor) 라고 부른다.

파일 디스크립터는 프로그래밍 언어 상으로는 결국 단순한 정수인 것이다.

파일 디스크립터 테이블. 각 테이블의 element는 실제 파일 내용을 가리키고 있다.

 

2. default 파일 디스크립터

 

그러면 프로그램을 실행하면 처음엔 아무 파일도 열려있지 않나? 하면 그렇지 않다.

리눅스 시스템은 3개의 default 파일 디스크립터를 시스템이 시작할때 미리 정해놓는다.

다시 말하면 숫자 3개는 이미 사용처가 정해져있다는 것이다.

default 파일 디스크립터는 STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO 이렇게 3개이며,

#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define SDTERR_FILENO 2

다음과 같이 숫자 0, 1, 2로 할당되어있다.

 

3. 스트림이 무엇인가?

 

스트림은 장치 간 데이터 전송을 담당하는 연결 통로이다.

키보드로 입력을 하면 그 전기신호가 컴퓨터에 입력이 되어야할 것 아닌가?

또 컴퓨터에 입력된 전기신호는 다시 모니터로 출력이 되어야한다.

키보드와 컴퓨터, 컴퓨터와 모니터는 데이터가 전달되는 통로가 있고

물리적으로는 케이블 등으로 연결되어있을 것이다.

논리적으로는 이것을 '스트림' 이라고 부른다. 

그렇다면 시스템 레벨 I/O 와 이게 무슨 상관이 있냐? 

 

4. 표준 스트림

 

리눅스에서는 모든 것을 파일로 관리한다.

이 부분부터 약간 헷갈리기 시작한다.

리눅스에서는 상술했듯 파일뿐 만 아니라

스트림, 네트워크 소켓 등을 모두 '파일처럼' 관리한다.

그래서 스트림 또한 마치 파일처럼 파일 디스크립터 테이블에 위치하고,

파일 디스크립터로 그 스트림을 참조할 수 있다.

 

리눅스는 시스템이 시작되자마자 표준 스트림을 3가지 만들고 

파일 디스크립터 테이블에 자동으로 추가할 뿐 아니라 따로 이름까지 가지고 있다.

표준 입력(stdin), 표준 출력(stdout), 표준 에러(stderr) 가 바로 그것이며

C언어의 stdio.h 에 이것이 FILE* 타입으로 정의되어있다(constant).

또한 이 스트림들은 각각 파일 디스크립터 테이블의 0,1,2 번째 원소이다.

 

이름이 약간 익숙하지 않은가?

#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define SDTERR_FILENO 2

아까 본 default 파일 디스크립터이다.

 

즉 파일 디스크립터 테이블에는 표준 스트림이 시스템 시작부터 있으며,

default 파일 디스크립터를 통해 이 표준 스트림을 사용할 수 있는 것이다.

 

잠깐 개념을 정리하자면

파일 디스크립터(숫자) 스트림
STDIN_FILENO(0) stdin
STDOUT_FILENO(1) stdout
STDERR_FILENO(2) stderr

이렇게 되겠다.

 

스트림과 파일 디스크립터는 언어상에서 관계가 어떻게 될까?

파일 디스크립터 테이블을 table 이라고 한다면,

table[0] = stdin 이 될 것 이다.

C언어에서 파일 디스크립터로 파일 스트림을 여는 함수는 fdopen() 함수이다.

간단한 예제를 통해 이해해보자.

#include<stdio.h>
int main()
{
	FILE* outputStream = _fdopen(1, "r+");
	fprintf(outputStream, "hello stream!\n");
	return 0;
}

fdopen() 함수로 파일 디스크립터 1 의 스트림을 요청했다. table[1] = stdout 이다.

outputStream에는 stdout 이 들어가고, fprintf(outputStream, ...) 은 그러니 stdout 으로 출력하는 함수이다.

모니터(표준출력)에 hello stream! 이 잘 출력되는 것을 확인할 수 있다.

 

반대로 파일 스트림을 통해 파일 디스크립터를 얻을 수 있을까?

있다. C언어의 fileno() 함수는 파일 스트림(FILE* 타입) 을 받아 그 스트림의 파일 디스크립터(int 타입)를 반환한다.

그렇다면 fileno(stdout) = 1 이 되지 않을까. 예제를 통해 확인해보자.

#include<stdio.h>
int main()
{
	printf("%d\n", _fileno(stdout));
	return 0;
}

1이 출력되는 것을 확인할 수 있다.

 

5. 파일 스트림, 파일 디스크립터

 

C언어에서 파일을 여는 함수는 fopen() 이다. 파일을 열고 그 파일 스트림을 반환한다.

파일 스트림은 파일 디스크립터 테이블에 추가되고 고유한 파일 디스크립터를 갖는다.

스트림을 통해 파일에 입출력을 수행할 수도 있고(fprintf, fscanf),

파일 디스크립터를 통해 입출력을 수행할 수도 있다(read, write).

아래의 예제를 통해 확인해보자.

#include<stdio.h>
#include<io.h>
#pragma warning(disable:4996)
int main()
{
	char statement1[100] = "this is an input by stream.\n";
	char statement2[100] = "this is an input by file descriptor.\n";

	FILE* fileStreamA = fopen("A.txt", "w");
	FILE* fileStreamB = fopen("B.txt", "w");

	fprintf(fileStreamA, statement1);
	fprintf(fileStreamB, statement1);

	int fileDesA = _fileno(fileStreamA);
	int fileDesB = _fileno(fileStreamB);

	printf("file descriptor of A is: %d\n", fileDesA);
	printf("file descriptor of B is: %d\n", fileDesB);

	_write(fileDesA, statement2,sizeof(statement2));
	_write(fileDesB, statement2, sizeof(statement2));

	return 0;
}

파일 2개(A.txt, B.txt) 를 열고 한 번은 스트림을 사용해서, 한 번은 파일 디스크립터를 사용해서 출력하는 코드이다.

표준출력으로는 3, 4 가 출력되는 것을 확인할 수 있다.

A.txt를 열면서 그 스트림이 파일 디스크립터 테이블에 추가되고 파일 디스크립터 3을 받은 것이다.

B.txt 또한 마찬가지이다.

 

이후의 내용은 시스템 레벨 I/O에 대하여(2) 로 이어진다.

'system&OS' 카테고리의 다른 글

시스템 레벨 I/O에 대하여(2)  (0) 2022.04.10