본문 바로가기
지식/시스템프로그래밍

System-Level I/O

by 칙칙폭폭 땡땡 2025. 4. 21.
반응형

Unix I/O

리눅스에서 파일은 바이트로 이루어진 녀석

그래서 종류도 다양할 수 있음

  • Regular file
  • Directory
  • Socket

Opening Files

open() 은 커널에게 “이 파일을 사용하겠다”고 알리는 시스템 호출

이게 성공하면 파일 디스크립터를 반환해줌.

반환값인 fd 는 해당 파일을 가리키는 식별 번호

이걸로 나중에 read(), write(), close() 등을 호출함.

만약에 open()이 실패하면 fd == -1 이고 에러로는 perror() 출력

근데 내가 이렇게 직접 호출하기 전에 프로세스가 시작할 때 기본적으로 파일을 3개 열고 시작함.

  • 0 : stdin, 표준 입력
  • 1 : stdout, 표준 출력
  • 2 : stderr , 표준 에러 출력

아무 파일을 열기 전에 기본적으로 0, 1, 2는 이미 열려 있음

그래서 내가 open() 으로 파일을 열면 그 때는 fd가 3일거임.

Closing Files

리눅스에서 시스템 자원은 유한하니까 파일 열고 놓고 안쓰고 그러지 말자.

다 썼고, 안쓸거면 닫아라

close(fd) 는 커널에게 “이제 이 파일 안쓸거야” 라고 알려주는 시스템 호출임

인자로는 열러있는 파일 디스크립터(fd)를 줘야 함.

반환값으로는 0이면 성공, -1이면 실패임

그리고 이미 닫은 파일을 또 닫으려고 하면 에러임

그래서 별 문제 없을 거 같아도 항상 반환값을 확인하는게 중요함.

Reading Files

  • fd : 내가 읽은 파일의 파일 디스크립터
  • buf : 데이터를 저장할 버퍼
  • count : 최대 몇 바이트까지 읽을 것인지

현재 파일의 위치(offset)에서부터 최대 count 바이트 까지 읽어서 buf에 복사됨

최대 count이지 딱 count가 아님!!!! 덜 읽을 수도?? = short count

실제로 읽은 바이트 수를 반환하고 이 수만큼 파일 위치도 자동으로 앞으로 이동함.

Short Count

read() 는 항상 count 개수만큼 읽는 게 아님

Copy
int n = read(fd, buf, 512);
if (n < 512) {
    // EOF에 도달했거나, 인터럽트로 인해 더 적게 읽음
}
  • 파일의 끝에 가까워서 더 이상 읽을게 없거나
  • 인터럽트에 의해서 덜 읽었거나

그러면 읽은 바이트 수만 큼만 리턴함

에러 아님!!!!!!!!!!!!!!

그리고 읽을게 없으면 아예 없으면 0을 리턴하고

에러가 발생하면 -1을 리턴함

Writing Files

read() 랑 정반대 동작

  • fd : 데이터를 쓸 파일 디스크립터
  • buf : 데이터를 담고 있는 메모리 버퍼
  • count : 최대 몇 바이트를 쓸 지

buf 에 들어 있는 count 바이트 중 일부나 전부를 현재 파일 위치부터 파일에 씀

이후에는 파일 오프셋이 자동으로 앞으로 감.

내가 512바이트 쓰라고 했는데도 다 못쓸 수 있음.

에러가 아님

이유로는

  • 디스크 공간 부족
  • 쓰다가 인터럽트 발생

마찬가지로 short count 가능

Simple Unix I/O example

사용자의 표준 입력을 표준 출력으로 한 바이트씩 복사하는 코드

여기서 STDIN_FILENO 는 0일거임.

while의 조건은 사용자의 입력이 존재하는 한 0이 아닐거임.

STDOUT_FILENO 는 터미널 디스플레이이 표시함

걍 echo 하는 거임

lseek()

Copy
off_t lseek(int fd, off_t offset, int whence);
  • fd : 파일 디스크립터
  • offset : 이동할 바이트 수, 해당 파일 포인터의 위치에서 얼마나 이동할 지
  • whence : 기준점
Copy
// 현재 위치(오프셋) 확인하기
off_t pos = lseek(fd, 0, SEEK_CUR);
Copy
// 파일의 시작위치에서 5바이트 앞으로 이동
lseek(fd, 5, SEEK_SET);
write(fd, "HELLO", 5);

buffer를 문자열로 사용가능함

이 녀석은 마지막에 null 포함하고 있음

그래서 6바이트 캐릭터를 담을 수 있는 상수 버퍼임.

근데 5개만 wirte 하는 것

Copy
off_t end = lseek(fd, 0, SEEK_END);
printf("File size" %ld bytes\n",end);

파일 끝에서 0바이트 떨어진 곳(= 걍 파일의 끝)으로 이동

RIO Package

이건 걍 책에만 있는건데 short count가 문제라서 그걸 고려해서 함수들을 만든거임

Unbuffered I/O

  • rio_readn(int fd, void *usrbuf, size_t n)
  • rio_writen(int fd, void *usrbuf, size_t n)

Buffered I/O

  • rio_readline()
  • rio_readnb()

Buffered RIO는 thread-safe하고 , 같은 파일 디스크립터에 대해서 여러 RIO 함수가 뒤섞여서 사용 가능하다.

(thread-safe ≠ no data race, 쓰레드 세이프하다는 말이 데이터 레이스가 없다는 말이 아님!!!!!!!)

쓰레드 세이프 하다는 것은 서로 다른 쓰레드가 있을 때, fd가 같더라도 rio 객체가 달라서(서로 다른 내부 버퍼를 가져서) 접근하는게 안전하다는 것

근데 같은 fd에 대해서 다른 내부 버퍼를 가지고 있다는 것은 쓰레드1의 버퍼가 처음으러부터 8바이트 읽고, 쓰레드2가 처음부터 8바이트 읽고 싶었지만 쓰레드2는 그 다음부터 읽어진다는 것 ⇒ 데이터 레이스 존재

그래서 결론은 읽는데 문제는 없지만 결과가 보장되지 않는다는 것!!

Unbuffered RIO Input and Output

n은 얼마나 읽을 건지, 쓸 건지

  • rio_readn() : short count가 발생할 수 있지만 파일의 끝일 때만 발생한다.
  • rio_writen() : short count가 발생하지 않는다! (never)

얘네들은 OS 내부에서 커널에서의 공유 버퍼임.

그래서 mutex로 원자적으로 관리되기 때문에 interleaved 되어도 상관 없음

⇒ 쓰레드 세이프

rio_readn()

여기서 read(fd, bufp, nleft) 를 while문 안에서 계속 반복적으로 수행하도록 함.

에러나면 걍 리턴하는건 자명

  • nleft : 내가 앞으로 읽어야 할 바이트 수
  • bufp : 내가 읽은것을 저장할 버퍼
  • nread : 내가 현재 읽은 바이트 수

반복문을 돌 때마다 nleft랑 bufp가 변하고 있는 것을 알 수 있다.

반복문으로 못 읽은 걸 읽을 수록 nleft 는 읽은 만큼 줄어들거임

반복문으로 못 읽은 걸 읽을 수록 bufp 의 위치는 읽은 만큼 이동해야함.

Buffered RIO Input Functions

  • rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
  • rio_readnb(rio_t *rp, void *usrbuf, size_t n)

Buffered I/O: Implementation

이제 버퍼드가 어떻게 작동하는지 살짝 살펴보자.

기존에는 계에속 read() 를 반복적으로 호출했는데, 이젠 안그래도 됨.

일단 크게 잡고 1번 read() 을 호출해서 내부 버퍼에 저장해줌.

그리고 이 내부 버퍼에서 차근 차근 읽음

그러다가 가져온걸 다 읽었으면 그때 다시 크게 잡아서 read()

(반복, …)

How the Unix Kernel Represents Open Files

  • Descriptor Table : 각 프로세스마다 존재
  • Open File Table : 커널 전역에서 공유
  • v-node Table : 파일의 시스템 레벨에서의 메타데이터

File sharing

하나의 파일을 여러 디스크립터로 열었을 때 어떻게 되나

동일한 파일을 open() 으로 두 번 열었을 때 발생하는 상황

그러면 fd 도 다르고, 슬롯이 연결되는 Open File Table 도 다름.

하지만!!!!! v-node table 은 같은 걸 가리킴

각자 자기만의 파일 포지션을 가짐.

서로 읽는거에 영향을 주지 않음

하지만 결국 같은 파일이라서 내용이 수정되면 그건 공유됨.

How Processes Share Files: fork

부모가 포크를 해서 자식을 만들면 자식은 부모의 디스크립터 테이블을 그대로 복사 받음

값은 같지만 테이블은 별도임!!!

하지만 이 디스크립터들이 가리키는 open file table은 같은 곳을 가리킴

여기서 참조 카운트가 2로 증가함 (자기한테 들어오는 화살표가 2개니까)

I/O Redirection

  • dup2(oldfd, newfd) : newfd를 oldfd와 같은 파일로 연결시킴

(근데 이거 어따 써.. 저렇게 해서 뭐할건데…)

Buffering in Standard I/O

Copy
printf("h");
printf("e");
printf("l");
printf("l");
printf("o\n");

이런 코드가 있을 때 이걸 write() 로 한 글자씩 처리하면 시스템 콜을 계속 해야되서 엄청 느려짐

이걸 피하기 위해서 버퍼링 사용

printf() 는 버퍼에 글자만 집어 넣음

그러다가 \n, fflush(), exit() 을 만나면 발생함.

이렇게 하면 1번만 시스템 콜 하니까 엄청 빨라짐



반응형