Computer Science/시스템 프로그래밍

[시스템 프로그래밍] Chapter 4

seungwon9201 2024. 4. 12. 22:44

Device Terminology 1

  • peripheral device : 컴퓨터의 주변장치(입출력 장치) ex. disk, tapes, CD-ROMs, 키보드 등 
  • Device driver : OS의 모듈 중에 하나, 하드웨어를 access 하기 위한 여러 가지 방법들을 모아서 숨겨놓는 역할을 하는 부분, 디바이스 드라이버는 다바이스 오퍼레이션에 세부적인 부분들을 숨기고 인터페이스만 제공해 준다.

Device Terminology 2

  • open, close, read, write 이 4개의 함수만 있으면 IO device의 종류에 상관없이 데이터를 쓰고 읽을 수 있음
  • 디스크 파일이나 다른 입출력장치들이 일반 file을 access 하듯이 동일한 함수를 이용해서 access 할 수 있음

UNIX files

  • 유닉스에서의  파일은 세부적으로 나눠서 관리하지 않고 일렬의 byte들의 집합임
  • 모든 IO디바이스들은 파일로서 표현되어 있다. 
  • 심지어 OS kernel도 파일로 표현이 가능

UNIX I/O 

  • UNIX I/O : 다양한 종류의 디바이스들이 파일로 표현이 되기 때문에 파일을 통해서 I/O를 수행하는 방법만 알면 됨
  • 모든 input output 오퍼레이션이 일관된 방법으로 다뤄지도록 시스템 콜 함수들을 제공해 줌

Reading

첫번째 파라미터는 읽어들일 파일의 디스크립터(오픈을 먼저 해야됨), 두번째 파라미터는첫번째 파라미터의 파일을 읽어들인 데이터를 저장하는 공간, 세번째 파라미터는 몇 바이트를 읽어 들일건지 요청하는 바이트 수

 

  • Return value : read함수에서는 10바이트를 요청했어도 1바이트를 읽으면 성공으로 간주함. 에러가 발생 시 -1 값이 리턴되고 에러코드는  errno변수에 설정이 됨.
  • ★read operation을 수행할 때 요청한 바이트수보다 적은 바이트가 리턴될 수가 있음. 
  • read함수는 end of file 캐릭터를 만날 때까지 반복해서 읽을 수 있음 

File descriptor : 오픈되어 있는 파일을 가리 킬 수 있는 지시자 

프로그램을 시작을 하게 되면 3개의 오픈 스트림을 가지고 시작한다. 즉, 커널이 세 개의 표준 입출력장치에 대해서 open을 자동으로 해준다는 말 

표쥰입력, 표준출력, 표준에러 장치는 따로 오픈을 하지 않아도 프로세스가 시작이 되면 커널이 알아서 오픈을 해주는 장치니까 read/write 써서 사용하면 됨


Writing

첫번째 파라미터는 데이터를 쓸 디스크립터, 두번째 파라미터는 read함수와 마찬가지로 user provide버퍼(그러나 버퍼의 의미가 다름) 두번째 파라미터로 지정한 user provide 버퍼안에 있는 내용을 읽어서 파일에 쓰겠다는것(read함수와 반대의미를 가지고 있음), 몇 바이트를 쓸것인지를 요청하는 것

정리하면, 두 번째 파라미터 버퍼 안에 있는 데이터를 읽어서 첫번째 파라미터의 파일로 쓰겠다는 것이고 두번째 파라미터 버퍼안에 있는 데이터중에서 n바이트만큼 읽어서 파일에 쓰겠다는 것(실제 파일에 쓰이는 바이트 수는 n보다 작은 바이트가 쓰일 수 있음)

 

file offset

다음의 I/O를 수행할 위치를 file offset이 가리키고 있고 read, write함수를  호출하면 I/O작업은 현재 file offset값에서부터 I/O작업을 수행을 하고 그다음에 성공적으로 read, write를 수행한 byte 수만큼 offset값이 자동으로 이동한다.

 

file offset은 오픈된 파일에 대해서만 사용할 수 있는 정보
버퍼의 크기를 1024로 설정하고 키보드로 인풋을 받고 그것을 출력하고자 하는 의도로 만들어진 코드이다. 그러나 write함수는 버퍼안에 있는 모든 내용을 다 출력하도록 설정이 되어있기 때문에 만약에 input값을 abc만 줬다면 abc뿐만아니라 나머지 1021개의 버퍼안에 담긴 쓰레기 값도 모두 출력이 되기때문에 잘못된 코드이다.
byteread를 통해서 인풋을 받은 바이트 값을 저장을한다. 그리고 read함수의 리턴값만큼 까지만 출력을 하도록 write함수를 지정, 그러나 또 잘못된 부분이 있음. 그러나 write함수도 read함수와 마찬가지로 요청한 바이트수 만큼 써진다는 보장을 할수가 없기 때문에 abc를 입력했다 하더라도 a만 출력이 될 수 있음 . 그래서 write의 리턴값을 또 확인을 하고 만약에 써야될 바이트 수보다 적은 바이트가 출력이 됐다면 write함수를 반복해서 출력하도록 설정해야한다. 게다가, 이 코드는 인터럽트 때문에 -1이 리턴이 됐다면 이것은 실제로 에러가 아니라 외부요인 때문에 발생한것이니 다시 요청하라는 식이 있어야함

 

fromfd 소스파일 디스크립터로부터 타겟파일 디스크립터파일을 copy하는 예제. read, write함수는 시그널에의해서 인터럽트되어 중단됐을 경우에는 다시 호출하도록 짜여져있고 write함수를 호출하는 부분은 write함수의 두번째 파라미터로 버퍼의 포인터인 buf를 사용한것이 아닌 bp포인터를 대신 사용했다. 왜냐하면 bp를 통해서 버퍼안에 타겟파일로 써야할 부분을 계속 이동하는 포인터로 쓰기위해

r_read() : 이 함수를 호출하면 인터럽트 때문에 중단되는 경우를 수동으로 작성하지 않아도 된다.

r_write() : 요청하는 바이트만큼 바이트만큼 쓰일 때까지 반복을 한다.

readwrite() : 처음 블록 하나를 소스파일에서 read 해서 타깃파일로 write해주는 함수이다, 버프사이즈는 PIPE_BUF(중간에 방해받지 않도록 read, write함수를 완료하도록 보장을 해주는 크기)이다.

copyfile() : 예제 3번에 있는 내용을 r_read, r_write 함수로 구현 

 

readblock() : r_read() 함수를 개선한 함수. r_read()의 기능을 더해서 요청한 바이트 수만큼 다 읽도록 반복하는 함수. 그래서 요청한 바이트 수만큼 읽지 못하면 에러(-1을 리턴)


Opening

첫번째 파라미터에는 내가 오픈하려고하는 경로명을 스트링으로 주는것, 두번째 파라미터는 flag(파일 오픈할때 어떤 모드로 오픈할지 정하는 것즉, access mode), 마지막 파라미터는 permission정보(읽기, 쓰기, 실행 권한)

O_RDONLY : 읽기 전용모드

O_WRONLY : 쓰기 전용모드

O_RDWR : 읽기, 쓰기 모드

 

O_APPEND : 기본적으로 file offset은 파일이 오픈 후 시작점을 가리키는데, 파일을 APPEND모드로 시작을 하게 되면 앞에 기존의 내용이 있다면 파일의 시작점으로 가는 게 아니라 내용의 끝지점부터 가리켜서 추가적인 내용을 쓸 수 있음

O_TRUNC :  내용이 있는 파일을 오픈할 때 이 모드를 사용하면 파일 안에 있는 내용을 모두 지우고 오픈함

O_CREAT : 새로 파일을 생성하고 오픈하고 싶을 때 사용

O_EXCL : CREAT모드와 함께 사용, temp.txt파일을 만들어서 오픈하려고 하는데 이미 그 파일이 존재한다면 EXCL모드에 의해서 에러를 발생 킴. 그래서 기존에 있는 파일을 실수로 오픈해서 거기에 다른 내용을 덮어쓰는 위험을 방지할 수 있음

O_CREAT | O_EXCL을 사용하자.

 

권한

 


Closing


lseek

파일을 오픈한 뒤에 생성되는 file offset 혹은 file position값을 임의의 위치로 이동시키기 위해서 사용하는 함수

첫번째 파라미터는 파일 디스크립터, 두번째 파라미터는 현재 파일 포지션 옵셋을 몇바이트만큼 이동할지 나타내고 세번째 파라미터는 이동시킬 파일 옵셋의 시작지점을 나타냄
예시


File representation

file descriptors vs pointers

파일을 오픈했을 때 리턴값이 file descriptors.

  • File pointers : fopen, fscanf, fprintf, fread, fwrite, fclose와 같은 함수를 호출하면 리턴값이 file descriptors가 아니라 file pointer가 리턴된다.
  • File descriptors : open, read, write, close 등과 같은 함수를 호출하면 리턴값이 file descriptors가 된다

파일 디스크립터란 오픈된파일의 지시자의 역할이고, 프로세스의 파일디스크립터 테이블의 인덱스가 파일 디스크립터이다. 시스템 파일 테이블의 엔트리 안에는 오픈된 파일에 대한 정보를 담고있다.  in-memory inode테이블안에는 실제 오픈된 active한 파일로  즉, 파일의 아이노드로 찾아갈 수 있는 정보가 포함되어 있음, 파일디스크립터 테이블에서 표준입력, 표준출력, 표준에러  세 개의 엔트리는 시스템에 의해서 자동으로 오픈이된다.

파일디스크립터에서 close함수를 호출하면 파일디크스립터에 있는 엔트리는 바로 삭제가 되지만 시스템파일 테이블이나 인메모리 아이노드테이블에서는 삭제가 되는 것이 아니라 카운트 값이 하나 감소함. 왜냐하면 다른 프로세스가 이 파일을 오픈해서 사용하고 있을 수도 있기 때문에

 

위의 그림에서 close(myfd) 함수를 호출하게 되면 어떻게 될까?

close함수를 호출한 엔트리의 네 번째 엔트리를 삭제를 하고 시스템 파일 테이블에 해당 엔트리도 같이 삭제를 함. 왜 삭제를 하냐면 시스템 파일 테이블의 엔트리의 카운트 값이 1이었기 때문에 0으로 감소시킴. 또한 OS는 인메모리 아이노드 테이블 엔트리의 카운트 값을 2에서 1로 하나 감수시킨다. 만약 아이노드 엔트리의 카운트값이 0이 되면 아이노드 테이블의 엔트리도 삭제를 한다. 이 내용은 시스템 파일 테이블 엔트리에도 그대로 적용된다.

 

만약 두 개의 프로세스가 같은 파일을 오픈해서 write 하게 되면 어떻게 될 것인가?

각 프로세스가 다른 프로세스에 썼던 내용을 덮어쓰게된다. ex) 123을 입력하고 abc를 입력한다면 1b3 등과 같이 출력될 수 있음

 

만약에 systemfile table안에 file-offset값이 in-memory inode table안에 있게 되면 두 개의 프로세스가 같은 파일에 서로 다른 내용을 쓰게 될 경우 어떻게 될 것인가?
앞선 프로세스가 쓴 내용뒤에 뒤에 실행하는 프로세스의 내용이 이어서 써짐 ex) abc123, a1 b2 c3(프로세스가 스케줄링되는 거에 따라 다를 수 있음)

 

 

file pointer : 버퍼공간을 가지고 있는 것

파일포인터를 통해서 파일포인터가 가리키는 파일에다가 어떤 내용을 쓰겠다고 한다면 어떤 일이 생길까?

-디스크에 직접 쓰이는 대신에 FILE structure에 fully buffered(write를 계속해서 버퍼가 가득 차게 되면 그제야 버퍼에 있는 내용이 실제파일 안에 쓰임)된다. 버퍼가 가득 찰 때까지 기다리는 문제가 발생하는 것을 fflush(버퍼가 조금만 채워져도 강제로 강제로 버퍼를 비우고 파일디스크립터를 통해서 실제파일에 쓰이게 하도록 하는 함수)로 예방하자.

-터미널 파일에 내용을 쓰릴경우 fully buffered가 아니라 line buffered(버퍼가 한 줄이 차면 한 줄을 쓰도록 하는 함수)된다. 스탠더드 에러 장치에 출력을 하게 되면 버퍼링 과정 없이 바로 장치로 출려된다.

 

파일 포인터와 파일 디스크립터의 차이점은 파일 포인터가 버퍼를 추가로 가지고 있다는 점임

 

파일이라는 structure가 버퍼를 가지고 있고 파일포인터 내부에 파일디스크립터를 가지고 있다.

 

 

왼쪽코드는 프로세스가 my.dat라는 파일을 오픈을 하고 난 뒤에 fork를 해서 자식프로세스를 만들고 부모와 자식프로세스가 같은 파일의 내용을 한바이트 읽었을 때 어떤 내용을 읽을 것이냐를 나타낸 식이다. 오픈을 하고 읽게 되면 현재 파일 offset에서 1바이트를 읽음, 처음 파일이 오픈되었을 때 file-offset값은 0으로 초기화가 된 상태로 부모는 a를 읽고 file-offset값은 1로 업데이트가 되고 출력이 된다. 그다음 자식프로세스는 부모와 같은 시스템파일 테이블 엔트리를 공유하고 있기 때문에 현재 offset값인 1부터 읽게 되어서 b를 출력하게 된다. 

 

오른쪽 코드는 fork를 먼저 해서 자식프로세스를 만들고 부모와 자식프로세스가 각각 파일을 오픈하고 읽은 내용을 출력하는 식이다. 처음부터 바로 fork로 자식프로세스를 만들고 자식 프로세스는 초기 상태의 파일 디스크립터 테이블을 그대로 물려받음, 부모프로세스부터 실행한다고 가정하면 부모프로세스는 파일 디스크립터 테이블에 엔트리가 하나 추가가 되고 그 엔트리가 맵핑이 되는 시스템파일 테이블에도 엔트리가 추가가 되고 인메모리 아이노드 테이블에도 엔트리가 추가가 되어서 my.dat파일을 가리키게 될 것이다. 부모는 파일 옵센값인 0을 읽어서 a를 출력한다. fileoffset값은 1로 업데이트가 된다. 자식프로세스도 별도로 호출을 하면 파일 디스크립터에 엔트리가 추가가 되고 별도로 오픈을 한 것이기 때문에 시스템 파일 테이블에 별도의 엔트리가 추가가 된다. 그래서 파일옵셋값은 부모와 공유하지 않는다. 그래서 자식도 a값을 출력을 하게 된다. 부모프로세스와 자식프로세스가 파일 옵셋값을 공유하지 않기 때문에 첫 번째 바이트 값을 읽게 된다. 

 

파일포인터는 파일스트럭쳐를 가리키고 이 안에는 버퍼가 들어있음, 그래서 파일포인터안에 내용을 쓰게되면 실제  파일에 쓰이는게 아니라 버퍼안에 먼저 내용이 쓰이게됨 그래tj 버퍼가 가득차면 실제 파일에 내용이 쓰이기된다(fully buffered, 일반파일의 경우), 터미널 파일인 경우 line buffered된다.

왼쪽은 this is my output. this is my output이라는 출력값이 나온다. 오른쪽 코드는 this is my output이라는 값이 쓰이다가 줄 바꿈 문자를 만나서 프로세스의 버퍼 안에 내용이 출력이 되고 버퍼가 비워진다. 그다음 fork를 해서 자식프로세스가 만들어졌다. 그래서 자식프로세스는 버퍼가 비어있기에 바로 종료하게 된다. 그래서 this is my output 만 출력이 된다. 왜냐면 fork 되기 전에 버퍼가 flush 되었기 때문.

 

터미널로 파일포인터를 통해서 출력을 하는 경우에는 라인단위로 버퍼링이 된다. 스탠더드 아웃풋으로 출력을 하게 될경우 파일포인터의 버퍼에 쌓이다가 라인 단위로 버퍼링이 되서 한 라인이 쌓이면 화면에 출력이 된다. 하지만 스탠다드 에러 장치로 출력을 하게 되면 버퍼링 없이 바로 화면에 출력이된다. 


filters and redirection 

  • Filters : 인풋을 받아서 읽어 들이고 인풋의 내용을 조건에 맞도록 변형시키고 변형시켜서 나온 결과를 아웃풋으로 보내는 역할 ex) head, tail, more, sort, grep, awk 등등
  • Redirection : 방향을 바꾼다 
    • > : 아웃풋 redirection으로 왼쪽 명령어의 출력결과가 오른쪽 명령어로 출력된다. 
    • < : 인풋 redirection으로 오른쪽파일의 출력결과를 왼쪽 명령어로 인풋을 넣어서 출력시킴

3개의 엔트리는 자동으로 오픈되고 my.file을 엔트리에 추가를한다. dup2를해서 3의 엔트리값을 표준출력(1)값으로 변경, 그리고 close를 호출해서 4번쨰엔트리를 삭제, 그다음 표준출력(1)에다가 OK라는 값을 출력하겠다라는 뜻