본문 바로가기

xv6-book

xv6-book (1.2 I/O and File descriptors)

I/O & 파일 디스크립터

 

파일 디스크립터는 프로세스가 읽거나 쓸 수 있는 커널이 관리하는 객체를 나타내는 작은 정수입니다. 프로세스는 파일, 디렉터리 또는 장치를 열거나, 파이프를 생성하거나, 기존의 디스크립터를 복제함으로써 파일 디스크립터를 획득할 수 있습니다. 간단하게 말해서, 파일 디스크립터가 참조하는 객체를 종종 "파일"이라고 합니다. 파일 디스크립터 인터페이스는 파일, 파이프 및 장치 간의 차이를 추상화하여 모두 바이트 스트림으로 보이도록 합니다.

 

내부적으로, xv6 커널은 파일 디스크립터를 프로세스별 테이블의 인덱스로 사용하여, 모든 프로세스가 0부터 시작하는 개별적인 파일 디스크립터 공간을 갖도록 합니다. 관례적으로, 프로세스는 파일 디스크립터 0(표준 입력)에서 읽고, 파일 디스크립터 1(표준 출력)에 출력을 씁니다. 또한 에러 메시지는 파일 디스크립터 2(표준 에러)에 씁니다. 우리가 나중에 볼 것처럼, 셸은 I/O 리디렉션과 파이프라인을 구현하기 위해 이 관례를 활용합니다. 셸은 항상 세 개의 파일 디스크립터가 열려 있음을 보장합니다. 이들은 기본적으로 콘솔용 파일 디스크립터입니다.

 

read와 write 시스템 호출은 파일 디스크립터로 지정된 열린 파일에서 바이트를 읽거나 쓰는 데 사용됩니다. read(fd, buf, n) 호출은 파일 디스크립터 fd로부터 최대 n 바이트를 읽어서 이를 buf에 복사하고, 읽은 바이트 수를 반환합니다. 파일을 가리키는 각 파일 디스크립터에는 연관된 오프셋이 있습니다. read는 현재 파일 오프셋에서 데이터를 읽은 다음 그 오프셋을 읽은 바이트 수만큼 전진시킵니다. 더 이상 읽을 바이트가 없을 때, read는 파일의 끝을 나타내기 위해 0을 반환합니다.

 

write(fd, buf, n) 호출은 buf에서 n 바이트를 파일 디스크립터 fd에 씁니다. 그리고 쓰여진 바이트 수를 반환합니다. 오류가 발생할 때에만 n보다 적은 바이트가 쓰입니다. read와 마찬가지로, write는 현재 파일 오프셋에서 데이터를 쓰고, 그런 다음 쓰여진 바이트 수만큼 오프셋을 전진시킵니다. 각 write 호출은 이전 호출이 끝난 곳에서 시작합니다.

다음 프로그램 조각(프로그램 cat의 핵심을 형성함)은 표준 입력에서 데이터를 표준 출력으로 복사합니다. 오류가 발생하면 표준 에러에 메시지를 작성합니다.

 

char buf[512];
int n;

for(;;) {
    n = read(0, buf, sizeof buf);
    if(n == 0)
        break;
    if(n < 0) {
        fprintf(2, "read error\n");
        exit(1);
    }
    if(write(1, buf, n) != n) {
        fprintf(2, "write error\n");
        exit(1);
    }
}

 

코드 조각에서 중요한 점은 cat이 파일, 콘솔 또는 파이프에서 읽고 있는지, 출력하고 있는지 알지 못합니다. 파일 디스크립터 0이 입력이고 파일 디스크립터 1이 출력임을 약속하는 것은 cat의 간단한 구현을 가능하게 합니다.

 

close 시스템 호출은 파일 디스크립터를 해제하여 이를 재사용할 수 있도록 하며, 이후의 open, pipe 또는 dup 시스템 호출에서 사용됩니다. 새로 할당된 파일 디스크립터는 항상 현재 프로세스에서 사용되지 않는 가장 낮은 번호의 디스크립터입니다.

 

파일 디스크립터와 fork는 I/O 리디렉션을 쉽게 구현할 수 있도록 상호작용합니다. fork는 부모의 파일 디스크립터 테이블을 메모리와 함께 복사하여 자식이 부모와 정확히 같은 열린 파일을 가지고 시작하도록 합니다. 시스템 호출 exec는 호출한 프로세스의 메모리를 교체하지만 파일 테이블은 보존합니다. 이 동작은 셸이 fork하여 자식에서 선택한 파일 디스크립터를 다시 열고, 그런 다음 exec를 호출하여 새 프로그램을 실행하여 I/O 리디렉션을 구현할 수 있도록 합니다. 여기에는 셸이 "cat < input.txt" 명령을 실행하는 데 사용되는 코드의 간소화된 버전이 있습니다:

 

char *argv[2];

argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
    close(0);
    open("input.txt", O_RDONLY);
    exec("cat", argv);
}

 

자식 프로세스가 파일 디스크립터 0을 닫은 후에, open은 그 파일 디스크립터를 새로 열린 input.txt에 사용할 것입니다. 0은 가장 작은 사용 가능한 파일 디스크립터가 될 것입니다. 그럼으로써 cat은 파일 디스크립터 0(표준 입력)이 input.txt를 가리키게 됩니다. 이 시퀀스는 자식의 디스크립터만 수정하므로 부모 프로세스의 파일 디스크립터는 변경되지 않습니다.

xv6 셸의 I/O 리디렉션 코드는 정확히 이 방식으로 작동합니다(user/sh.c:82). 코드의 이 지점에서 셸은 이미 자식 셸을 fork했고, runcmd는 exec를 호출하여 새 프로그램을 로드할 것입니다.

 

open의 두 번째 인자는 open이 무엇을 수행할지 제어하는 비트로 표현된 플래그 집합입니다. O_RDONLY, O_WRONLY, O_RDWR, O_CREATE 및 O_TRUNC입니다. 이들은 open에게 파일을 읽기 위해 열지, 쓰기 위해 열지, 또는 읽기 및 쓰기 모두 위해 열지, 파일이 존재하지 않으면 파일을 생성하고 파일을 길이를 0으로 잘라내도록 지시합니다.

 

이제 fork와 exec가 별도의 호출로 나뉘어 있는 것이 왜 유용한지 명확해졌을 것입니다. 두 호출 사이에는 셸이 자식의 I/O를 재지정할 수 있는 기회가 있지만, 주 셸의 I/O 설정을 방해하지 않습니다. 대신 가상의 결합된 forkexec 시스템 호출을 상상할 수도 있습니다. 그러나 그러한 호출로 I/O 리디렉션을 수행하는 옵션은 번거로워 보입니다.

 

fork는 파일 디스크립터 테이블을 복사하지만, 각 기본 파일 오프셋은 부모와 자식 사이에 공유됩니다.

 

if(fork() == 0) {
    write(1, "hello ", 6);
    exit(0);
} else {
    wait(0);
    write(1, "world\n", 6);
}

 

파일 디스크립터 1에 연결된 파일에는 "hello world"라는 데이터가 포함됩니다. 부모의 write(자식이 끝난 후에만 실행되는, wait 덕분에)는 자식의 write가 끝난 곳에서 이어집니다. 이 동작은 (echo hello; echo world) >output.txt와 같은 셸 명령의 순차적 출력을 생성하는 데 도움이 됩니다.

 

dup 시스템 호출은 기존 파일 디스크립터를 복제하여 해당 기존 파일 디스크립터와 동일한 기존 I/O 객체를 가리키는 새로운 파일 디스크립터를 반환합니다. fork로 복제된 파일 디스크립터와 마찬가지로 이 두 파일 디스크립터는 오프셋을 공유합니다. 이것은 파일에 "hello world"를 쓰는 또 다른 방법입니다.

 

fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);

 

만약 두 파일 디스크립터가 동일한 원래 파일 디스크립터에서 fork 및 dup 호출의 시퀀스를 통해 파생되었다면, 두 파일 디스크립터는 오프셋을 공유합니다. 그렇지 않으면, 파일 디스크립터는 오프셋을 공유하지 않습니다. 즉, 동일한 파일에 대한 open 호출에서 생성되었더라도 오프셋은 공유되지 않습니다. Dup은 셸이 다음과 같은 명령을 구현할 수 있도록 합니다: ls existing-file non-existing-file > tmp1 2>&1. 여기서 2>&1은 셸에게 명령에 파일 디스크립터 2가 디스크립터 1의 복제본임을 알려줍니다. 기존 파일의 이름과 존재하지 않는 파일의 오류 메시지가 모두 파일 tmp1에 표시됩니다. xv6 셸은 오류 파일 디스크립터에 대한 I/O 리디렉션을 지원하지 않지만, 이제 이를 어떻게 구현할 수 있는지 알게 되었습니다.

파일 디스크립터는 강력한 추상화입니다. 왜냐하면 그들이 연결된 내용의 세부 사항을 숨겨주기 때문입니다. 파일 디스크립터 1에 쓰는 프로세스는 파일에 쓰거나, 콘솔과 같은 장치에 쓰거나, 파이프에 쓸 수 있습니다.
 

 

더보기

representing : 대표하는

by convention : 관례에 따라

associated : 관련된

Fewer : 보다 작은

occurs : 발생하다

guaranteed : 보장

attached : 포함된

'xv6-book' 카테고리의 다른 글

xv6-book (1.5 Real world)  (2) 2024.02.06
xv6-book (1.4 File system)  (1) 2024.02.05
xv6-book (1.3 Pipes)  (0) 2024.02.04
xv6-book (Ch1.1-Processes and memory)  (0) 2024.01.30
xv6-book (Ch1-Operating system interfaces)  (0) 2024.01.29