UNIX OS는 system call을 통해 service를 제공한다.
system call이란 OS내에서 유효한 user program에의해 호출될 수 있는 함수이다.
앞서봤던 I/O library 함수같은건 OS 이식성이 있다. 하지만 여기서 살펴볼 system call은 UNIX specific이다. 이를 통해 어떻게 standard library 함수들을 만들었는지 알아본다.
(다른 OS이더라도 기본 원리는 같다. insight 기르려면 공부하도록.)
UNIX에서의 I/O는 모두 file에서 이루어진다. 왜냐하면 모든 잡다한 devices들(keyboard, monitor 포함)이 file system 내에선 file로 다뤄지기 때문이다.(directory도 file도 다뤄진다.)
이덕에 하나의 interface로 모든 주변 기기/file을 다룰 수 있다.
file을 read/write하기전에 그 의도를 system에 알려야하는데, 그 과정을 opening the file이라한다.
file open시에 권한같은게 다 잘 확인되면 작은 non-negative interger을 반환하는데, 이를 file descriptor라 한다.
open file의 정보는 system이 관리하고, user는 이 file descriptor로만 file을 참조(refer)한다.
keyboard/screen은 근데 너무 자주 쓰인다.
그래서 shell로 특정 프로그램을 실행시키면 세개의 파일이 자동으로 열린다.
(간단히 말하자면 open을 한다는 것이 (검사 후) file과 숫자를 연결하는 작업이다. 즉, shell로 특정 프로그램 실행시키면 미리 정해진 세개의 파일을 숫자와 연결시킨다고 할 수 있다.)
그리고 그 file들은 각각 0
,1
,2
의 descriptor을 가지고, 덕분에 각 프로그램은 file open 고려할 필요없이 I/O를 할 수 있다.(0,1,2 가 각각 표준 입력/출력/에러 임)
유닉스의 탄생 정리한 곳에서도 말했는데,
(정확힌 모르겠으나) redirection을 사용한다는 것은,
file descriptor 0,1,2를 정해진 keyboard,screen으로 연결시키는 것이 아닌,
`>` 등을 사용해 표기한 곳과 연결시키는 것 같다.
redirection이나 pipe를 사용하면 shell이 해당 프로그램의 file descriptor들의 default assignment를 수정하게 되는 것이다.
file이 아닌 shell이 수정한다.
(0
이 keyboard가 아닌 다른 file을 가리키게하고..)
정리하자면, UNIX에서 모든 주변기기 등은 file로 다루어진다.
file을 read하고 write하려면 system에 그것에 접근하겠다고 알려주는, open을 해야한다.
open을 하면 각 file은 file descriptor라는 작은 숫자를 부여해 구분한다.
(이게 단순한 숫자인 이유는 프로세스가 유지하는 FD 테이블의 index이기 때문이다.)
https://dev-ahn.tistory.com/96
프로세스마다 open file descriptor table를 가진다.
(당연함. 다른 프로세스에서 연 파일을 다른 프로세스가 접근하는건 넌센스)
shell로 program을 실행하면 자동으로 open해주는 file이 3개있다.
그 3개는 프로세스마다 자동으로 open되는 것이다.
open되면 당연히 file descriptor가 각각 생긴다.
그리고 뭐 어려울거없이 stdin, stdout, stderr은 각각 C code에서 활용할 수 있게
각각 descriptor 정보도 포함하고, 나머지 필요한 정보들도 포함하도록 정의되는 것이다.
실제로 `_IO_FILE` 구조체(FILE 구조체)에 `_fileno` 멤버를 포함해 해당 정보를 저장한다.
아래 두 system call은 syscalls.h
에 있다.
int read(int, char*, int);
int write(int, char*, int);
첫번째 인자로 file descriptor, 두번째 인자로 정보를 읽어올/작성할 character array, 세번째인자로 적용할 byte수를 입력받는다.
return 값은 실제로 옮겨진 byte 수이다.(요청한 byte 수보다 작으면 뭔가 있는 것)
syscalls.h
에는 BUFSIZ
라는 것도 정의돼있는데, 이는 해당 system에 좋은 size를 명시한다.
(그럼 이를 이용해 배열 만들어서 입출력 관여해주면 좋겠..지?)
아래는 직접 system call을 이용해 library 함수 몇개를 간단하게 만들어본다.
>getchar
: simple buffered version
#include "syscalls.h"
int getchar(void)
{
static char buf[BUFSIZ];
static char *bufp = buf;
static int n = 0;
if (n == 0) { //if buffer is empty
n = read(0, buf, sizeof buf);
bufp = buf;
}
return (--n >= 0) ? (unsigned char) *bufp++ : EOF;
}
//위 버전의 함수를 테스트할때 <stdio.h>가 include되어있다면 `#undef getchar` 해줘야함.
> open
<fcnt1.h>
header 혹은 BSD 버전 유닉스라면 <sys/file.h>
에 있다.
int open(char*, int, int);
첫번째 인자로 filename을 받고, 두번째 인자로 어떻게 file을 열지(read/write 등)을 정하는 flag 정보를 받는다.
마지막 인자로는 permission 정보인데, 그냥 거의 0이라고만 하네.
file descriptor을 반환한다.
존재하지 않는 file을 열면 error이다.
flag 정보를 표현하는 macro도 정의돼있다. p.172하단.
> creat
int creat(char*, int);
첫번째 인자로 filename을 받고, 두번째인자론 permission 정보를 받는다.
file descriptor을 반환한다.
얘는 open
과 다르게 새로운 file을 만들거나 기존 file을 re-write할때 사용한다. 그래서 이미 존재하는 file을 열어도 된다.
premission 정보는 9 bits로 표현된다. read/write부터 owner의 그룹 등등의 정보를 저장한다.
creat으로 파일을 처음 만드는 것이라면 두번째 인자가 그 file의 permission 정보로 저장된다.
creat 이름이 이렇게 된데에는 켄 톰프슨의 개인적 취향을 빼면 별다른 이유가 없다.
롭 파이크가 켄에게 유닉스를 처음부터 다시 개발할 수 있다면 무엇을 바꾸고 싶은지 물어본 적이 있다.
켄은 이렇게 대답했다. "creat 대신 e가 붙은 create로 만들겠네"
> close & unlink
close(int)
는 file descriptor와 open file사이 관계를 끊는다. 그리고 해당 file descriptor(숫자)를 free해서 다른 file이 사용할 수 있도록한다.
unlink(char*)
는 해당 file name을 file system에서 삭제한다.
standard library의 remove
함수와 대응된다.
p.174부터...
(간략) lseek
는 fseek 같은 역할을 하는 함수이다. 이를 이용해 file을 배열처럼 다룰 수 있다(대신 access는 느림). 그렇게 file내 아무 위치의 정보를 읽어오는 등의 작업을 할 수 있다.
fopen, getc, listing directories, memory allocation 등
다양한 함수/프로그램을 직접 system call 이용해서 만드는 part. 대충 맥만 짚고 쭉 봄. 보니까 stdio header code 좀 가져와서 설명도 좀 해주고, stdin 애들도 어떻게 초기화되는지 보여주고 재밌음.
p.175~
system call은 OS마다 다르고, 기계어는 cpu마다 다르다.
C code가 portability를 유지할 수 있는 이유는
각 OS마다 그 OS의 interface(system call)을 알맞게 사용해서 library가 작성되고,
cpu에 맞는 기계어로 잘 번역되도록 컴파일러도 각각 만들어져있기 때문이다.
즉, 옮겨갈때마다 컴파일러만 잘 맞게 사용해주면 문제없다.
<간단정리>
나중에 다시 볼때 맥만 짚게 대충 (나만 알아볼 수 있게) 정리..
(어 근데 적고 보니 너무 대충 정리해서 나도 나중에는 못알아볼수도 있겠는데)
fopen : open/creat 함수 이용해서 파일 열어주고 stdio 헤더에 있는 정보들 채워넣어주면 됨. FILE 구조체에도 정보 넣어줘야되고..
fsize : 기존에 있는 프로그램은 아닌데, UNIX file system 설명해주면서 만들어봄. UNIX에선 directory를 file로 취급하는데, directory file엔 해당 directory에 있는 file들의 정보들이 들어있다. 그리고 그 정보들은 filename과 inode number로 구성된다.
inode number는 indoe list란 곳에 해당 file 정보가 있는 index이다.
inode list의 각 element에는 filename을 뺀 해당 file의 정보들이 들어가있는 곳이다.
뭐어쨌든 여차저차 만들며 얻은 교훈은, (1)대부분 프로그램은 system program이 아닌 OS가 제공하는 서비스를 이용해 만들뿐란 것과 (2)잘 만지면 system-dependent인 정보를 이용해 system-independent인 interface를 만들어낼 수 있단 것이라고 하네. 그리고 그렇게 system-independent인 interface를 만든 좋은 예시가 standard library이고..
(맞다, standard library가 제공하는 interface는 일정하지만 OS마다 세부 구현 사항은 다름)
malloc/free : heap을 malloc/free만 사용하진 않는다. 그래서 heap에 할당받은 공간이 연속적이지 않을 수 있다. free된 공간은 순서대로 순환 link로 묶여서 관리된다. free된 공간엔 해당 공간의 크기, 다음 공간을 가리키는 포인터, 그리고 그 공간 그 자체로 구성된다.(이런 정보들을 header라 한다.) 연속된 freed block은 하나의 큰 block으로 합쳐지도록 해서 공간이 너무 조각나지 않게한다.
allocation 요청이 오면, freed memory를 돌며 요청된 공간보다 큰 공간이 나오면 그걸 적절하게 잘라서 할당해준다.(Find first fit, not best fit)
그리고 free linked list에서 그 할당될 공간을 빼버리고 그 공간을 malloc이 반환하는 것이다.(그래서 free를 안하면 애초에 공간 해제가 안되는거네. free를 해서 freed 공간 linked list에 포함되도록 해줘야 나중에 다시 쓸 수 있는건데, 안하면 영영 못쓰는거지)
만약 아무 공간도 맞지 않는다면, OS에 요청해서 다른 큰 덩어리를 받아서 free에 연결한다.
header도 잘 align될 필요가 있다. 그래서 header와 most restrictive type을 묶어서 union으로 만든다.(여기서 말하는 most restrictive type은 쉽게말해 가장 큰 공간 요구하는 type)
HEADER
는 말 그대로 header 부분만 나타낸다. header 뒤에 freed 공간이 딸려오는 형태.
(아 이거보면서 알았는데 malloc이라고 딱 요청한만큼 할당해주는게아니구나, 얘도 편한 chunk단위로 할당해주네. 5byte할당 요청한다고 딱 5만 해주는게 아니라.. 어차피 가리키는 포인터가 딱딱 들어맞아서 넉넉해도 상관은 없는거지)
os에서 메모리 할당 받아오는건 expensive operation이다. 그래서 malloc 호출될때마다 할당받는게아니라 남은 freed 공간이 없으면 morecore 함수를 통해 받아온다.(즉, 위 얘기는 그 할당받아온 메모리를 어떻게 관리(allocate,free)할건지에 대한 얘기인 것.)
morecore함수의 세부구현사항은 os마다 다르다, UNIX에선 sbrk system call을 사용해 morecore을 구현한다.
전체적인 코드 이해는 참고해서 보면 도움될듯.