UNIX I/O - 시스템프로그래밍

UNIX I/O - 시스템프로그래밍

Tag
Computer Science Engineering
System Programming

Device Terminology

  • Peripheral device ( 주변 장치 / 입출력 장치 )
    • 컴퓨터 시스템에 접근 가능한 하드웨어 기기
  • Device driver ( 소프트웨어 모듈 )
    • OS의 모듈 중 하나.
    • device가 I/O 요청을 하게 되면 system call
    • device operation의 구체적인 내용을 숨기고, 정상적이지 않은 사용으로부터 보호한다.
 

Device Terminology2

  • UNIX는 사용자가 device 인터페이스를 쉽게 사용할 수 있도록 함수를 제공한다.
    • open, close, read, write, and ioctl
  • 모든 device를 파일 ( special files (/dev 디렉터리) )로 매핑해서 다루게 된다.
 

UNIX files

  • unix file은 바이트들의 sequence 이다.
 

UNIX I/O I

  • open
    • UNIX에서 파일 i/o 를 수행하려면 무조건 open 해야한다.
  • close
    • I/O 작업이 끝난 파일
  • ( lseek ) file position / file offset
    • 오픈된 파일 내부에서 그 다음에 일어날 file io operation을 가리키는 포인터의 위치 조절
    • 파일의 처음부터 얼마나 떨어진 byte offset ( 처음엔 당연히 0 )
    • read, write는 어디 부분부터 읽거나 쓰거나 하는 것이 안된다. sequential 하게 작동하기 때문.
      • 이때 file position을 바꿔야 해당 실행이 가능한데, 이를 위해 seek 함수를 사용한다.
  • read
  • write
 

Reading

#include <unistd.h> ssize_t read(int fildes, void* buf, size_t nbyte);
  • ssize_t
    • integer
  • size_t
    • unsigned integer
 
  • fildes ( file descriptor )
    • 파일 read를 하려면 먼저 open을 해야한다. open 함수의 return 값은 integer type이며 이는 file descriptor 이다.
  • buf
    • read를 하기 이전에 char buf[n]; 을 먼저 하게된다.
    • 파일 입력해온 데이터를 저장할 버퍼.
    • uninitialized pointer를 넣을 경우 에러가 터지게 된다.
  • nbyte
    • 요청하는 byte 수
    •  

Read function

  • return value
    • 실제로 읽어들인 byte 수
    • 에러가 나면 -1을 리턴하고 errno를 세팅한다
  • read operation을 수행할 때, 요청한 bytes 수보다 더 적은 bytes만 읽어들일 수 있다.
    • ex) 요청이 성공적으로 만족되기 이전에 EOF에 닿았을 경우
  • EOF 상태일 경우 0을 리턴한다. ( 읽을 byte가 없기 때문 )
    •  

File descriptor

  • 오픈 되어있는 파일 / device를 나타내는 지시자이다.
 
우리가 꼭 open하지 않아도 이미 열려있는 파일 입출력 장치를 나타내는 CONSTANTS ( 키보드 등 )
  • STDIN_FILENO
    • 표준 입력 장치 ( 키보드 )
    • 0번으로 지정되어있다.
  • STDOUT_FILENO
    • 포준 출력 장치 ( 스크린 )
    • 1번으로 지정되어있다.
  • STDERR_FILENO
    • 표준 출력 장치와 똑같지만, 우선순위가 높은 에러메시지 등을 출력할 때에는 이것을 쓰는게 좋다.
    • 프로그램이 절대 close를 하면 안된다.
    • 2번으로 지정되어있다.
    •  
 
notion image
위의 상황의 경우 buf 포인터 변수가 가리키고 있는 주소가 없기 때문에, 에러가 터진다.
컴파일시에는 에러가 나지 않지만, 아마 memory access violation 을 발생 시킬 것이다.
 
#include <errno.h> #include <unistd.h> // 3번째 파라미터의 경우 한 줄을 읽을 때 최대 몇 byte까지 읽을 것이냐. // ex) 80이라면 80바이트 이전에 읽기가 끝났다면 성공, 80바이트가 넘어간다면 실패 int readline(int fd, char *buf, int nbytes) { // 지금까지 읽어들인 byte 수 int numread = 0; int returnval; // nbytes - 1 이면 안된다. while (numread < nbytes -1) { returnval = read (fd, buf + numread, 1); // read 함수가 에러 났고, errno가 interrupt 라면 실제 에러상황이 아니기 때문에 continue; if ((returnval == -1) && (errno == EINTR)) continue; // 이미 파일의 끝에 가있고 한 byte도 읽지 않았다면 (input file이 아예 빈파일 인 경우) 0 리턴 if ((returnval == 0) && (numread == 0) return 0; // 한 줄을 다 읽어들이기 이전에 eof를 만났다면 if (returnval == 0) break; // read 함수가 에러났을 경우 if (returnval == -1) return -1; numread++; // 방금 읽은 값이 개행문자라면, 그 뒤에 null을 넣어주고 몇 글자를 읽었는지 int 값을 return. if (buf[numread-1] == ‘\n’) { buf[numread] = ‘\0; return numread; } } errno = EINVAL; return -1; }
 
notion image
 

Writing

#incldue <unistd.h> ssize_t write(int fildes, const void *buf, size_t nbyte);
  • fildes
    • 어디에 쓸 것인가?
  • buffer
    • 어떤것을 쓸 것 인가?
요청한 byte 수보다 더 적은 수를 파일에 적을 수 있다.
 
notion image
 

File offset

파일을 오픈하게 되면, 오픈한 파일의 정보 중 offset이 초기화된다.
다음에 I/O 를 수행할 위치.
notion image
 

Example1

notion image
키보드로 부터 최대 1024 바이트 만큼의 char array를 입력 받아 buf에 저장한다.
buf를 읽어서 스크린에 보여준다.
 
📌
3 바이트만을 입력한 뒤, write 함수가 실행될 때 3바이트와 쓰레기값이 같이 출력될 것이다.
 

Example2

notion image
버퍼에 입력을 받은 다음, 실제로 입력받은 수만큼만 write를 하게 된다.
 
📌
- bytesread만큼 write 한다는 보장이 없다. ( write 함수 자체에서 에러가 났을 경우 ) - interrupt에 의해서 errno 와 함께 -1을 리턴할 수도 있다.
 

Example 3

#include <errno.h> #include <unistd.h> #define BLKSIZE 1024 // BLKSIZE 만큼 읽어서 target file에 적는다. 그리고 다시 다시 다시.. (소스파일의 끝까지) // source file의 fd, target file의 fd ( file descriptor ) int copyfile(int fromfd, int tofd) { char *bp; char buf[BLKSIZE]; // source file로 부터 몇 바이트를 읽었는지 int bytesread; // target file에 몇 바이트를 썼는지 int byteswritten = 0; // 소스파일로부터 타겟파일로 총 몇 바이트를 복사했는지 int totalbytes = 0; for ( ; ; ) { while (((bytesread = read(fromfd, buf, BLKSIZE)) == -1) && (errno == EINTR)); /* handle interruption by signal */ if (bytesread <= 0) break; /* real error or end-of-file on fromfd / 종료조건 */ bp = buf; while (bytesread > 0) { while(((byteswritten = write(tofd, bp, bytesread)) == -1 ) && (errno == EINTR));/* handle interruption by signal */ if (byteswritten <= 0) break; /* real error on tofd */ totalbytes += byteswritten; bytesread -= byteswritten; // bp를 쓰는 이유? write 함수는 꼭 bytesread만큼 쓴다는 보장이 없다. // 어느정도만 썼을 경우 그 만큼만 주소값을 옮겨서 그 이후부터 써라. 라는 용도로 사용하기 위해. bp += byteswritten; } if (byteswritten == -1) break; } return totalbytes; }

Restart read/write after a signal

r_write
요청된 바이트 수만큼 쓰여질 때까지 반복하는 함수이다.
 
readwrite
블럭 (PIPE_BUF) 하나를 copy 해주는 함수이다.
  • PIPE_BUF
    • read, write를 호출할 때 중간에 방해받지 않고 read, write를 할 수 있다는 것을 보장해주는 사이즈.
    • 방해받지 않는다! ( 이를 atomic task 라 한다. )
 
copyfile
example3과 같은 내용이지만 간략하게
 

readblock() 교재 내의 함수

r_read의 문제
: 종종 요청한 바이트 수보다 더 적은 바이트 수를 읽기도 한다.
 

Opening

#include <fcntl.h> #include <sys/stat.h> int open(const char *path, int flag); int open(const char *path, int flag, mode_t mode);
  • path
    • path임
  • flag
    • 읽기모드 / 쓰기모드 / 읽기 쓰기 모드
  • mode
    • 만약 파일을 생성하는 경우, 어느 사용자가 이 파일에 access할 수 있는지 permission을 정함
    •  

Flag

  • access mode
    • O_RDONLY: read only
    • O_WRONLY: write only
    • O_RDWR: read and write
  • 추가적인 flag ( | 연산자와 함께 사용 가능 )
    • O_APPEND: file offset을 파일의 마지막으로 옮긴 다음 write를 실행하려고 할때.
    • O_TRUNC: 파일의 내용을 모두 지우고 새롭게 데이터를 채울 때
    • O_CREAT: 파일이 없다면 새로 파일을 생성할 때. mode를 무조건 적어야함
      • 만약 생성을 목적으로 적었는데, 이미 있는 파일이 있었던 경우 덮어쓰는 현상이 발생한다.
    • O_EXCL (exclusive) : O_CREAT와 무조건 같이 적어야함. 아니면 에러 터짐.
      • 생성하려고 할때 이미 있는 파일이라면 에러를 내보냄
    • O_NOCTTY 수업시간에 언급 X
 
  • 만약 새로 파일을 생성하여 open하고 싶은 경우 이며, 이미 존재한다면 에러를 터트릴때?
    • O_CREATE | O_EXCL
 

Permission mask

  • a user ( or owner ), a group, everybody else
 

POSIX symbolic names

sys/stat.h
notion image
맨 아래의 두개는 언급 X
 
– create a file called “info.dat” in the current directory
– if the “info.dat” file already exists, it is overwritten.
– The new file can be read or written by the user and only read by everyone else.
 
⇒ open(”info.dat”, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_SIROTH);
 

copyfilemain.c

#include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <sys/stat.h> #define READ_FLAGS O_RDONLY #define WRITE_FLAGS (O_WRONLY | O_CREAT | O_EXCL) #define WRITE_PERMS (S_IRUSR | S_IWUSR) int main(int argc, char *argv[]) { int bytes; int fromfd, tofd; if (argc != 3) { fprintf(stderr, "Usage: %s from_file to_file\n", argv[0]); return 1; } // 파일을 연다 if ((fromfd = open(argv[1], READ_FLAGS)) == -1) { perror("Failed to open input file"); return 1; } // 겹치지 않은 이름의 파일을 생성하기 위해 open if ((tofd = open(argv[2], WRITE_FLAGS, WRITE_PERMS)) == -1) { perror("Failed to create output file"); return 1; } // 카피 bytes = copyfile(fromfd, tofd); printf("%d bytes copied from %s to %s\n", bytes, argv[1], argv[2]); return 0; /* the return closes the files */ }
 

Closing

cp command

상단의 copyfilemain.c 예시를 보면 Open 함수를 호출한 뒤, close하는 부분이 존재하지 않는다.
main에서 바로 종료가 되기 때문에 프로그램이 알아서 cleanup을 진행시켜 문제가 되지 않는다.
하지만 리소스를 사용한 채로 프로그램이 계속 실행한다면, memory leak이 발생한다.
 
#include <unistd.h> int close(int filedes);
 

lseek

file offset (file position)을 임의의 위치로 옮기기 위해서 사용한다.
 
#include <sys/types.h> #include <unistd.h> off_t lseek(int filedesc, off_t offset, int start_flag);
  • off_t offset
    • 현재 파일 file offset을 몇 바이트 옮길 것이냐 ( 정수 )
  • int start_flag
    • 기준점
      • SEEK_SET: 파일의 시작점을 기준
      • SEEK_CUR: file offset 기준
      • SEEK_END: 파일의 끝을 기준
  • 리턴 값
    • 시작 지점부터 file offset이 몇 바이트만큼 떨어져있냐
 
lseek를 이용하여 파일의 끝에서 expand할 수 있다.
 

File representation

  • File pointer
    • structure이며, 결국 내부에는 File descriptor를 가지고 있다. ⇒ FILE* fp;
    • ISO C 함수를 사용한다 (fopen, fscanf, fprintf, fread, fwrite, fclose…)
    • stdin, stdout, stderr ( stdio.h에 명시되어있음 )
  • File descriptor
    • UNIX I/O 함수 (open, read, write, close, ioctl…)
    • STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO
    •  

File descriptor

notion image
프로세스마다 가지고 있음 ⇒ File descriptor table
커널에 있는 부분 ⇒ System file table, In-memory inode table
 
system file table, in-memory inode table에 count값이 존재하는 이유는 언제 엔트리를 제거해야할 지를 판단하기 위해서 이다.
 
  1. 해당 프로세스는 File descriptor table은 이미 존재하고 있고 File descriptor table은 오픈된 파일에 해당되는 정보로 갈 수 있는 포인터를 가지고 있다. 기본적으로 3개는 가지고 있게 된다. ⇒ 커널이 자동으로 채워둠 ( STDIN, STDOUT, STDERR )
  1. open 함수를 호출하면, 해당 경로의 파일을 열고 file descriptor를 리턴할 것이다. 이는 위 예시의 3 엔트리에 채워짐을 의미
  1. System file table은 open 함수를 호출할 때 마다 채워진다. 엔트리 안에는 오픈한 파일의 일부 정보가 채워져 있다. ( file offset, open mode, 몇개의 file descriptor table 엔트리가 나를 가르키고 있는지( 여러 엔트리가 refer할 수 있음)에 대한 정보 count )
  1. in-memory inode table은 파일의 실제 정보를 가지고 있다. ( 여러개의 system file table이 가리킬 수 있기 때문에 count를 가지고 있음 ) 실제로 열린 파일에 대한 메타 정보를 가지고 있는 structure ( inode ) 로 갈 수 있는 정보 ( inode ⇒ 실제 컨텐츠로 갈 수 있음 )

File descriptors

  • 프로세스마다 가지고 있는 file descriptor table의 index이다.
  • system file table
    • 프로세스들이 공유하는 커널 영역에서 관리가 되는 테이블이다.
    • open할 때마다 엔트리가 추가된다.
    • file offset, access mode, count 정보 등등이 유지가 된다.
    • 여러개의 system file table entry가 하나의 in-memory inode table entry를 가리킬 수 있다
  • in-memory inode table
    • system file table entry에 의해 in-memory inode table entry가 가리켜진다.
    • 실제 열린 파일의 inode로 넘어갈 수 있는 포인터를 가진다.
 

example

프로세스가 close함수를 호출하면 어떻게 될까?
  1. fd index에 해당하는 entry를 삭제한다.
  1. system file table에 해당 entry를 바로 삭제하는 것이 아니라, count값을 하나 감소시킨다. 0이 되면 본인을 가리키는 file descriptor table entry가 존재하지 않는 것이기 때문에, 해당 엔트리가 삭제된다.
  1. in-memory inode table의 count값 또한 system file table entry가 삭제 되었다면 하나 감소 시킨다.
 
만약 두 프로세스가 같은 파일을 open하여 write 함수를 호출할 경우?
p 프로세스가 “abc”를 적는다고 가정하면, p 프로세스가 가지고 있는 file offset 정보는 system file table entry에 존재한다. 그 다음 p’ 프로세스가 “123”을 적는다고 가정하면, 각각 p’ 프로세스는 p 프로세스와 다른 system file table entry를 가리키고 있기 때문에 file offset은 0으로 초기화 되어있어 파일에 덮어 쓰게 된다.
 
두 개의 프로세스가 같은 파일을 오픈했을 때 같은 system file table entry를 가리킬 경우는 언제일까? ⇒ p 프로세스가 open을 하고 나서 p 프로세스가 fork를 해서 자식 프로세스를 생성했을 때 file descriptor table 정보 또한 복제된다.
 
만약 file offset값이 in-memory inode table에 있다면?
두 개의 프로세스는 같은 file offset을 공유하기 때문에 덮어쓰는 일이 존재하지 않는다.
 

File pointer

  • FILE structure
    • buffer와 함께 file descriptor 값을 가지고 있다.
    • 쓴 내용을 buffer안에 쓴 이후 조건에 만족했을 때 나중에 실제 파일에 적용된다.
  • fprintf를 실행하면 어떻게 되는가?
    • fprintf(fp, “%s %d”, … );
    • 디스크에 적는 것 대신 fully buffered 된다.
    • 버퍼가 가득 차게 되면, I/O subsystem call을 호출하여 적는다
    • buffering delay를 피하기 위해 fflush 를 호출하여 현재 버퍼 내용을 파일에 쓰게 할 수 있다.
    • Terminal file의 경우 fully buffered가 아닌 line buffered가 된다.
      • STDERR 장치의 경우 버퍼링이 되지 않는다.
 
notion image
 

Inheritance of file descriptors

ex) my.dat 파일은 “ab”의 내용을 갖고 있다.
 
my.dat라는 파일을 오픈한 뒤에 fork. 같은 파일의 내용을 읽을 때 어떻게 되는가
int main(void) { char c = '!'; int myfd; if ((myfd = open("my.dat", O_RDONLY)) == -1) { perror("Failed to open file"); return 1; } if (fork() == -1) { perror("Failed to fork"); return 1; } read(myfd, &c, 1); printf("Process %ld got %c\n", (long)getpid(), c); return 0; }
⇒ nnn 프로세스: a / mmm 프로세스: b
 
fork한 뒤에 my.dat 라는 파일을 오픈. 같은 파일의 내용을 읽을 때 어떻게 되는가
int main(void) { char c = '!'; int myfd; if (fork() == -1) { perror("Failed to fork"); return 1; } if ((myfd = open("my.dat", O_RDONLY)) == -1) { perror("Failed to open file"); return 1; } read(myfd, &c, 1); printf("Process %ld got %c\n", (long)getpid(), c); return 0; }
⇒ nnn 프로세스 : a / mmm 프로세스 : a
 

Line buffering example

#include <stdio.h> #include <unistd.h> int main(void) { printf("This is my output."); fork(); return 0; }
  • fork 이전에 “This is my output”을 버퍼링 해놓는다.
  • fork를 하게 되면 부모 프로세스의 file pointer 또한 상속받는다.
  • main이 끝남으로써 프로세스가 종료되면 버퍼가 flush 된다.
  • output은 두번 보일 것이다.
 
#include <stdio.h> #include <unistd.h> int main(void) { printf("This is my output.\n"); fork(); return 0; }
  • fork 이전에 버퍼는 flush된다
  • output은 한 개만 보일 것이다.
 
Example
#include <stdio.h> int main(void){ fprintf(stdout, "a"); fprintf(stderr, "a has been written\n"); fprintf(stdout, "b"); fprintf(stderr, "b has been written\n"); fprintf(stdout, "\n"); return 0; }
notion image
 
#include <stdio.h> int main(void){ int i; fprintf(stdout, "a"); scanf("%d", &i); fprintf(stderr, "a has been written\n"); fprintf(stdout, "b"); fprintf(stderr, "b has been written\n"); fprintf(stdout, "\n"); return 0; }
⇒ scanf 동작은 stdout의 버퍼를 비우는 작업을 선행하고 나서 STDIN을 통해 입력을 받는다.
notion image
 

Filters and redirection

  • filters
    • input을 받아서 transformation을 가한다 ( 필터링을 한다 ) 그리고 output으로 보낸다.
      • head, tail, more, sort, grep, awk…
  • Redirection
    • > : redirection of standard output
      • ex) ls > test.txt = ls의 output이 test.txt 파일에 쓰인다
    • < : redirection of standard input
      • ex) ls < test.txt = test.txt 파일이 ls의 input으로 쓰인다
 
redirection의 구동 방식
⇒ ls > temp.txt의 경우를 예시로 들어보자.
  1. ls의 경우 원래에는 fd 1. 즉 STDOUT에 write 하는 것 이지만, temp.txt로 redirection을 하게되면 temp.txt 파일을 open 하게 된다. temp.txt file descriptor를 fd 3 이라 하자.
  1. ls의 file descriptor entry가 STDOUT이 아닌 fd 3의 엔트리값으로 변경되어 temp.txt에 적게 되는 것이다.
 

Redirection in C program

#include <unistd.h> int dup2(int fildes, int fildes2);
fildes ⇒ fildes2 로 file descriptor 엔트리를 변경할 수 있다.
  • dup2는 fildes2가 오픈되어있다면 close를 한다. 그 이후 fildes entry를 fildes2 entry에 복사한다
 

예시

⇒ STDOUT을 my.file 파일로 redirect하는 함수.
 
#include <fcntl.h> #include <stdio.h> #include <sys/stat.h> #include <unistd.h> #include "restart.h" #define CREATE_FLAGS (O_WRONLY | O_CREAT | O_APPEND) #define CREATE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) int main(void){ int fd; fd = open("my.file", CREATE_FLAGS, CREATE_MODE); if(fd == -1){ perror("Failed to open my.file"); return 1; } // fd 엔트리의 내용(my.file)은 STDOUT_FILENO 엔트리 내용에 복사된다. if(dup2(fd, STDOUT_FILENO) == -1){ perror("Failed to redirect standard output"); return 1; } // r_close 함수는 interrupt가 일어나지 않도록 close하는 함수이다. => fd 엔트리 삭제됨 // my.file system file table의 entry는 사라지지 않는다 (STDOUT_FILENO 엔트리가 가르키고 있기 때문) if(r_close(fd) == -1) { perror("Failed to close the file"); return 1; } if(write(STDOUT_FILENO, "OK", 2) == -1){ perror("Failed in writing to file"); return 1; } return 0; }