SWjungle/#컴퓨터시스템

Unix I/O

장영 2023. 9. 17. 22:36

File


📌file

  • 리눅스에서 file은 , 여러 바이트의 시퀀스이다.
  • "모든 것이 파일이다"
  • 모든 I/O device는 파일로 표현될 수 있다.
    • /dev/sda2 -> /usr Disk Partition
    • /dec/tty -> 터미널
  • 디렉토리도 파일이다.
  • 커널도 파일이다
    • /boot/vmlinuz-3.13.0-55-generic ~> 커널 이미지
    • /proc ~> OS 커널의 자료구조
  • 당연히, 프로그램도 파일이다.
    • /bin/ls : ls 프로그램
  • 각 device와 데이터를 파일화 하는 이유는 커널이 이를 뽑아낼 수 있게 하기 위함이다.
  • 즉, 모든 입출력 데이터를 파일화한다.
  • 파일 관련 기본 unix i/o(system call)
    • open() : 파일 열기
    • close() : 파일 닫기
    • read() : 파일 읽기
    • write() : 파일 쓰기
    • lseek() : current file position 을 찾는다.
      • 즉, 입출력을 위한 파일 내의 Offset을 찾는다.
      • lseek을 통해 파일 Offset을 마음대로 변경할 수 있다.

📌파일 타입

  • 각 파일에는 해당 파일이 system에서 가지는 역할에 대한 type이 있다.
  • 일반 파일 : 임의의 정보를 가진 일반적인 파일을 의미 (document, audio, program 파일등)
  • 디렉토리 : 관련된 파일의 집합을 위한 인덱스를 가지는 파일을 의미 즉, 폴더를 의미
    • a.txt , b.txt , c.txt라는 파일들이 workspace라는 디렉토리에 들어있다고 해보자
    • 디렉토리도 파일이다. 이 파일에는 다른 파일을 가리키기 위한 인덱스인 innode가 있다.
    • 즉, 일반적인 파일 처럼 어떠한 컨텐츠를 가진 것은 아니지만, 내부의 각 파일들을 인덱싱 할 수 있는 table을 가지고 있는 파일을 디렉토리라 하는것
  • 소켓 : 다른 머신에 있는 프로세스와 소통하기 위한 파일이다.
    • 즉, 이더넷과 같은 네트워크에서 , 프로세스 1이 클라이언트에, 프로세스 2가 서버에 있다면, p1이랑 p2가 서로 소통하기 위해 필요한 파일을 소켓이라 부르는 것이다.

 

📌file 열기 & 닫기

 

💡파일을 연다는 것은, 커널에게 " 나 이제 이 파일 접근할 준비됐어 " 라고 알리는 것과 같다.

int fd; /* file descriptor */

if ((fd = open("/etc/hosts", O_RDONLY)) < 0) {
	perror("open");
    exit(1);
}
#include <sys/type.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(char *filename, int flags, mode_t mode);
  • open 함수를 사용하면, 커널이 리턴 값으로 fd를 반환한다.
    • return 값이 -1 이면 에러상황이다.
  • 프로세스는 file descriptor table을 가진다.
  • 리눅스에서 생성된 각 프로세스는 아래의 세 파일 지정자를 default로 가진다.
    • 0번 : Standard Input (stdin, STDIN_FILENO)
    • 1번 : Standard Output (stdout, STDOUT_FILENO)
    • 2번 : Standard Error (stderr, STDERR_FILENO)
    • 이 세 파일 지정자는 예약(Reserved)되어 있다. 따라서, 프로세스에서 open함수를 처음 호출하면, 3번 인덱스부터 반환한다. 

💡파일을 닫는다는 것은 커널에게 "나 이제 이 파일 접근 끝낼게 " 라고 알리는 것과 같다.

int fd;
int ret; 		/* Return Value */
				// 리턴값을 확인하는 습관은 매우 좋은 습관이다.
if ((ret = close(fd)) < 0) {
	perror("File Close Error");
	exit(1);
}
#include <unistd.h>
int close(int fd);
  • 마지막으로, 프로세스는 오픈한 파일을 close 함수를 호출해서 닫는다.

 

📌파일 읽기와 쓰기

 

💡파일을 읽는 다는것은 current file position에서부터 복수의 바이트를 복사해서 메인 메모리에 놓고, 그 다음 current file position을 업데이트 하는것이다. 즉, 선 copy 후 fd update이다.

  • fd가 어딘가를 가리키고 있고, 그곳에서부터 데이터를 얼마만큼 복사하고, 그러고나서 fd를 옮기고 이런 과정이다.
char buf[512];
int fd; 		
int m; 				/* 읽은 바이트 개수 */

if ((fd = open("/etc/hosts", O_RDONLY)) < 0) {
	perror("File Open Error");
	exit(1);
}

if ((m = read(fd, buf, sizeof(buf))) < 0) {
	perror("File Read Error");
	exit(1);
}
  • read(fd, buf , sizeof(buf)의 의미
    • fd의 current file position 부터 sizeof(buf)만큼 읽어서 buf에 넣고, 읽은 바이트 개수를 반환한다.
    • read 함수의 반환값이 0보다 작으면 에러 상황이다.
    • 내가 요청한 사이즈보다 실제 읽은 바이트 수가 적은 것은 에러가 아니다.
      • 이를 short count 라고 한다. 위 예시 코드에서 m<sizeof(buf)상황이다.

 

💡파일에 쓴다는 것은, 메인메모리에서 복수의 바이트를 복사해서 current file position에서 부터 덮어쓰고, 그 다음 current file position을 업데이트 하는 것이다. 즉, 선 복사 후 fd 업데이트 하는것

 

char buf[512];
int fd; 
int m; 				/* 쓰여진 바이트 개수 */

if ((fd = open("/etc/hosts", O_RDONLY)) < 0) {
	perror("File Open Error");
	exit(1);
}

if ((m = write(fd, buf, sizeof(buf)) < 0) {
	perror("File Write Error");
	exit(1);
}
  • wirte(fd, buf, sizeof(buf))의 의미
    • sizeof(buf)만큼 buf를 복사해서 fd의 current file position에서 부터 넣고, 쓴 바이트 개수를 반환한다.
    • write 함수의 반환값이 0보다 작으면 에러 상황이다.
    • 내가 요청한 사이즈보다 실제 읽은 바이트 수가 적은 것은 에러가 아니다.
      • 이를 short count라고 한다. 위 예시코드 m<sizeof(buf) 상황

 

✏️간단한 c코드

int main(void) {
	char c;

	while(Read(STDIN_FILENO, &c, 1) != 0)	// Wrapper로 씌운 Read/Write
		Write(STDOUT_FILENO, &c, 1);
	
    return 0;
}
  • 터미널에서 문자열이 입력되면, 한문자씩 읽어서 화면에 쓰는 프로그램이다.
  • 매우 비효율적인 프로그램이다.
  • read와 write는 system call 이다. 즉 overhead가 많다. 한 번의 호출에 대략 20ms정도에 overhead가 난다고 한다. 이는 사람한테 짧은 시간일지 몰라도, 컴퓨터에겐 너무나 긴 시간이다. 위 코드처럼 read/write를 잦게 호출하는 경우, 시간 비효율이 매우 높은 것이다.
read나 write 시에는, 시간 효율을 높이기 위해, 큰 바이트 단위로 읽는 것이 좋다. 일반적으로 Chunk(0.5KB, 512B)단위로 읽고 쓰곤 한다.

 

 

RIO Package


📌RIO Package

  • roboust for i/o
  • 네트워크 입출력에 맞게 알맞게 구현

  • RIO Package는 Unbuffered와 Buffered를 모두 제공한다.
    • Unbuffered : rio_readn, rio_writen
    • Buffered : rio_readlineb, rio_readnb

📌버퍼없는 rio 입력 및 출력 함수

  • unbuffered의 경우, unix의 read & write와 동일하다.
  • unix의 read & write는 unbuffered input, output 이다
  • 네트워크 소켓에서 데이터를 주고받을 때 좋은 성능을 보인다고 한다.
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
/* Return */
// num of bytes transferred if OK
// 0 on EOF (rio_readn only)
// -1 on error

 

  • rio_readn은 EOF를 만날때만 short count 상황이 만들어진다.
    • 버퍼가 제공되지 않기 때문에 얼마를 읽을지 알고 있을때만 써야한다.
  • rio_writen은 short count 상황이 만들어지지 않는다.
ssize_t rio_readn(int fd, void *usrbuf, size_t n) {
    size_t nleft = n;          // 남은 바이트 수를 저장할 변수 nleft를 n으로 초기화
    ssize_t nread;             // read 함수의 반환값을 저장할 변수 nread
    char *bufp = usrbuf;       // 버퍼 포인터를 사용자가 제공한 usrbuf로 설정

    while (nleft > 0) {
        if ((nread = read(fd, bufp, nleft)) < 0) {  // 파일로부터 nleft만큼 읽음
            if (errno == EINTR)      // 만약 인터럽트가 발생하면 다시 시도
                nread = 0;
            else
                return -1;           // 그 외의 오류 상황 시 -1 반환하여 종료
        } 
        else if (nread == 0)       // 만약 파일의 끝을 만났을 때
            break;                 // 루프를 종료하고 반환
        nleft -= nread;            // 읽은 바이트 수를 nleft에서 빼줌
        bufp += nread;             // 버퍼 포인터를 읽은 만큼 우측으로 이동
    }
    
    return (n - nleft);           // 읽은 총 바이트 수를 반환
}

 

📌RIO버퍼를 통한 입력함수

  • buffered의 경우 내부 메모리 버퍼를 두어 조금 더 효율적인 입출력을 할 수 있다.
  • Unix I/O는 Unbuffered이다. 따라서 Buffered RIO가 더 수행속도가 빠르다.
void rio_readinitb(rio_t *rp, int fd);
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
/* Return */ 
// num of bytes read if OK
// 0 on EOF
// -1 on error
  • rio_readlineb : fd에서 텍스트 라인을 읽을 수 있는대로 최대로 읽고, 버퍼에 넣어 놓는다.
    • 네트워크 소켓에서 텍스트를 읽을 때 상당히 효율적이다.
    • 종료조건
      • MaxLen만큼 바이트가 읽혔을때
      • EOF를 만났을 때
      • Newline Character를 만났을 때 (이것이 readnb와의 차이)
  • rio_readnb : fd에서 최대 바이트만큼 읽을 수 있는대로 읽고, 버퍼에 넣어 놓는다.
    • 종료조건
      • MaxLen만큼 바이트가 읽혔을때
      • EOF를 만났을 때

📌rio_t 구조체

typedef struct {
	int rio_fd; 				/* 내부 버퍼를 위한 파일 지시자 */
	int rio_cnt; 			/* 내부 버퍼엔 들어있지만 아직 사용자가 읽지 않은 양 */
	char *rio_bufptr; 		/* 내부 버퍼엔 들어있지만 아직 사용자가 읽지 않은 구간의 시작 */
	char rio_buf[RIO_BUFSIZE]; 	/* 내부 버퍼 */
} rio_t;

  • 이러한 구조체 자료형을 가지고, 그림과 같은 원리로 buffered I/O를 구현한다.