📒 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다.
✔ 파일(File)
데이터를 포함하는 컨테이너.
연속된 바이트의 시퀀스.
모든 바이트는 디스크 파일에서 주소를 지정할 수 있음.
OS에 의해 어떠한 형식도 정해져있지 않음.
기존의 IBM OS의 경우 파일의 종류가 많고, 종류에 따라 read/write 하는 방법이 달랐음.
모든 파일의 인터페이스가 통일됨.
read/write만 있으면 됨.
✔ 파일 시스템(File System)
컴퓨터 파일과 데이터를 조작하고 저장하는 방법.
파일의 인터페이스를 통일함으로써 파일을 찾고 접근하기 쉬워짐.
데이터 저장 장치를 사용할 수 있음.
→ 하드 디스크, CD-ROM, USB.

위의 사진은 파일 시스템에 접근하기 위한 9가지 기본적인 시스템 콜.
유저 프로그램은 이 시스템 콜들을 이용해 파일 시스템에 접근함.
시스템 콜을 호출할 때마다 트랩(소프트웨어 인터럽트)이 발생.

character 디바이스와 block 디바이스를 구분해서 적절한 장치를 찾아 하드웨어를 컨트롤함.
character 디바이스는 I/O 단위가 1 byte이기 때문에 중간에 버퍼가 필요 없음.
block 디바이스는 I/O 단위가 1 block(보통 4k byte)이기 때문에 중간에 버퍼가 필요함.
✔ 파일에 관련된 9가지 기본적인 시스템 콜
💡 open
💡 create
💡 close
💡 read
💡 write
💡 lseek
디스크에 저장되어 있는 파일의 주소를 바이트 단위로 지정해서 그 위치에 있는 데이터를 찝어낼 때, 위치를 이동시킬 때.
디스크에서만 사용.
파일은 바이트 단위로 주소를 지정할 수 있음.
💡 unlink & remove
💡 fcntl
파일 컨트롤.
파일에 관한 attribute를 변경시키거나 읽을 때.
✔ File Descriptor란?
파일을 오픈할 때 넘어오는 오픈된 파일을 지정하는 번호.
파일을 새로 만들어서 열거나, 기존에 있는 파일을 열 때 커널이 file descriptor를 리턴해 줌.
open이나 create로 file descriptor를 받으면, 그것을 이용해 해당 파일에 read/write 할 수 있음.
음이 아닌 정수. 0부터 시작.
유닉스에서 프로세스를 만들면 기본적으로 3개의 file descriptor가 생성됨.
이것들은 open 하지 않아도 할당되어 있음.

0번 : Standard Input(keyboard)
1번 : Standard Output(Screen)
2번 : Standard Error(정상적인 출력인가?)
0 ~ 2번이 기본적으로 존재하기 때문에 어떤 파일을 처음으로 열면 3번이 할당됨.
→ 사용되지 않은 정수 중에서 가장 낮은 번호가 리턴됨.

/usr/include/fcntl.h
위와 같은 경로를 나타낼 때 '<>'를 사용.
/usr/include/가 디폴트 디렉토리.
✔ 어디에 사용되는가?
open 시스템 콜
시스템마다 리턴 값의 타입이 다 제각각임.
표준화 하기 위해 _t로 끝나는 데이터 타입으로 통일해버림.
이렇게 시스템마다 다른 타입에 대한 정보는 <sys/types.h>에 저장되어 있음.
<unistd.h>를 include 하면 그 안에 있는 <sys/types.h>가 자동적으로 include 됨.
ssize_t : 정수형
mode_t : unsigned integer
off_t : integer / long 정도 됨.

유닉스는 멀티 유저 시스템.
유저가 많기 때문에 효율적으로 유저를 나눔.
유저는 반드시 어떤 그룹에 속함.
대부분 한 개의 그룹에 속하지만 여러 그룹에 동시에 속해 있을 수도 있음.

permission에는 세 가지가 있음
r(readable) : 읽을 수 있는 권리.
w(writable) : 쓸 수 있는 권리.
x(executable) : 실행해서 프로세스를 만들 수 있는 권리.
파일의 소유자가 가지는 권한을 'user permission'
파일의 소유자가 속한 그룹의 다른 유저들이 가지는 권한 'group permission'
파일의 소유자가 속한 그룹의 바깥에 있는 유저들이 가지는 권한 'others permission'
유저 permission : 3 bit
그룹 permission : 3 bit
others permission : 3 bit
permission은 총 9 bit
permission은 숫자로도 줄 수 있음.
각각의 permission은 3 bit로 되어 있기 때문에 000 ~ 111의 총 8가지.
9 bit로 되어있는 총 permission은 8진수.
permission을 표현하는 숫자에서 맨 앞의 0은 8진수임을 의미.
뒤의 세 자리만 이용.
ex) 0644 → 110 100 100 → rw- r-- r--
permission을 변경하는 것은 파일의 소유자만 가능함.
✔ file permission의 기호

이건 참고로만 알아두자.
funtl.h에 들어있음.
앞에 S_는 항상 붙음.
USR, U : 유저
GRP, G : 그룹
OTH, O : 타인

인자에 [ ]로 싸여있는 것은 있어도 되고 없어도 되는 것.
✔ int flags
💡 O_RDONLY(#0)
💡 O_WRONLY(#1)
💡 O_RDWR(#2)
(optional flags)
💡 O_APPEND
💡 O_CREAT
💡 O_EXCL
💡 O_TRUNC
✔ mode_t mode
permission을 나타냄.
✔ 사용 예시
fd = open("/tmp/newfile", O_WRONLY);
// 파일이 존재하면 open, 존재하지 않으면 file open error.
fd = open("/tmp/newfile", O_WRONLY|O_CREAT, 0644);
// 파일이 존재하면 open, 존재하지 않으면 생성해서 open.
fd = open("/tmp/newfile", O_WRONLY|O_CREAT|O_EXCL, 0644);
// 파일이 존재하면 file open error, 존재하지 않으면 생성해서 open.
fd = open("/tmp/newfile", O_WRONLY|O_CREAT|O_TRUNC, 0644);
// 파일이 존재하면 빈 파일로 만들어서 open, 존재하지 않으면 생성해서 open.
fd = open("/tmp/newfile", O_WRONLY|O_APPEND);
// 파일이 존재하면 open해서 맨 뒤에 append, 존재하지 않으면 file open error.
file open error가 발생하면 리턴되는 file descriptor 값이 -1이 됨.
원래의 file descriptor는 음이 아닌 정수.
음수가 나온 경우는 정상적인 file descriptor가 아님.
→ 시스템 콜의 호출이 실패했다는 것을 나타냄.
대부분의 시스템 콜이 실패할 수 있음.

주어진 pathname의 파일을 mode 권한을 주어서 만들라.
리턴 값은 file descriptor. 에러일 때는 -1.
creat 시스템 콜은 항상
1) truncate 함.
2) write only로 open 함.
✔ 파일을 생성하는 방법
fd = creat("/tmp/newfile", 0644);
fd = open("/tmp/newfile", O_WRONLY|O_CREAT|O_TRUNC, 0644);
// 파일이 있으면 빈 파일로 만들어서 열고, 없으면 0644 권한을 줘서 만들라.
위의 두 코드는 같은 기능을 함.
디렉토리는 그 안에 있는 파일들의 목록을 갖고 있는 파일임.
새로운 파일이 만들어지면 그 이름이 디렉토리 파일에 추가되어야 함.
새 파일을 open하거나 creat한 경우, 그 파일이 생성되는 부모 디렉토리에 새로운 파일명이 write됨.
따라서 새로운 파일을 생성하려면 파일을 만든 사람이 부모 디렉토리에도 write 할 수 있어야 함.
해당 워킹 디렉토리에만 write 권한이 있으면 되는 것이 아니라 루트까지 write 권한이 다 있어야 함.
파일을 만든 사람이 그 파일의 소유자가 됨.
파일은 프로세스가 만듦.
따라서 프로세스가 파일의 소유자가 됨.
프로세스의 유저 아이디에는 두 가지가 있음.
1) effective user ID
2) real user ID
이 중 프로세스의 effective user ID가 파일의 소유자가 됨.
프로세스의 그룹 아이디에도 두 가지가 있음.
1) effective group ID
2) real group ID
이 중 프로세스의 effective group ID가 파일의 group ID가 됨.

open한 파일을 닫음.
프로그램이 종료될 때 close를 안 했더라도 자동으로 닫힘.
filedes = file descriptor

file descriptor가 가리키는 파일에서 n 바이트 만큼 읽어서 buffer가 가리키는 메모리부터 그 뒤로 n 바이트 만큼 넣어라.
void* buffer : 타입이 정해져 있지 않은 포인터를 넣음
→ 가변성을 두기 위함.
→ char든 int든 포인터가 들어오면 됨.
nread = read(filedes, buffer, 1024);
read 시스템 콜의 리턴 값은 실제로 읽은 바이트의 수.
실패하면 -1이 리턴됨.
filedes 파일에 충분한 데이터가 있으면 1024 byte를 읽음.
100 byte 밖에 없으면 100 byte만 읽음.
그럼 nread의 값은 100.
filedes가 가리키는 파일의 맨 끝까지 읽어서 end of file에 도달한 다음에 다시 읽으면 하나도 못 읽음.
→ 읽은 데이터의 바이트가 0임.
→ read의 리턴 값이 0.
✔ current file position
파일을 open할 때, file position(=file offset)이 정해짐.
→ 대게 파일의 맨 앞을 가리킴.
file position은 현재 읽거나 쓸 위치를 가리킴.
파일을 n byte만큼 읽고 나면 file position이 이동함.
그래서 다음에 다시 읽으면 전에 읽었던 위치 이후부터 읽음.

유저 프로그램에서 파일 open에 성공하면 file descriptor table에 파일을 가리키는 포인터가 형성됨.
그 포인터가 디스크에 있는 파일을 바로 가리키는 것이 아니라, 파일을 가리키고 있는 File Table이라는 자료 구조를 가리킴.
File Table의 맨 마지막에 있는 위치가 디스크에 있는 파일을 가리킴.
file offset : 현재 그 파일에서 읽고 있는 file position.
read 될 때마다 file offset 값이 업데이트 됨.
위 예제에서의 변화량 : 0 → 512 → 700 → 700
count : 해당 File Table을 가리키고 있는 file descriptor의 개수.

buffer에서 n byte만큼을 읽어서 filedes가 가리키는 파일에 씀.
리턴 값은 실제로 write 된 데이터의 바이트 수.
실패하면 -1을 리턴.
현재 file position에서부터 시작해서 n byte만큼 write.
n byte만큼 write 하면 file position이 n byte만큼 뒤로 이동.
이미 있는 파일을 open해서 write하면 덮어쓰기 됨.
→ 이전의 내용이 지워지고 새로 write 됨.
→ 기존에 쓰여져 있던 것이 새로 쓰는 바이트 보다 많은 경우, 덮어쓴 이후에 뒷쪽에 남아있는 기존의 내용이 없어지지 않음.
파일을 open 할 때, O_APPEND 옵션을 주면 file position이 맨 뒤인 end of file로 세팅됨.
이때 write를 하면 맨 뒤에 write 됨.

위의 예제는 name1 파일에 있는 내용을 name2 파일로 복사하는 코드.
outfile을 open 할 때, name2가 있으면 빈 파일로 만들어서 열고, 없으면 PERM의 권한을 주고 만들어서 open.
read의 리턴 값인 nread를 write의 인자로 사용
→ 읽은 만큼 write.
이때, write의 리턴 값이 nread 보다 작으면 while에서 에러가 발생함.
끝까지 돌다가 언젠가 end of file에 도달하면 nread의 값이 0이 됨.
→ while문 탈출.

OS에서는 어떤 프로세스가 혼자만 도는 것이 아님.
💡User time : 실제로 프로세스가 실행한 시간.
💡Real time : 시작해서 끝날 때까지의 OS와 다른 여타 프로세스들을 실행한 시간.
💡System time : 시작해서 끝날 때까지 OS만 실행한 시간.
버퍼 사이즈를 증가시키면 성능이 좋아짐.
But 무조건 좋아지는 것은 아님.
버퍼 사이즈가 4k byte일 때 가장 효율적임.
디스크에서는 4k byte 단위로 I/O가 일어남.
따라서 버퍼 사이즈를 I/O 단위만큼 하는 것이 가장 성능이 좋음.
시스템 콜은 굉장히 헤비한 함수.
실행하는데 시간이 굉장히 많이 걸림 + 유저 모드에서 커널 모드로 context switch도 발생함.
따라서 가능하면 시스템 콜의 호출 횟수를 줄이는 것이 성능에 유리함.
버퍼 사이즈가 작으면 시스템 콜의 호출 횟수가 늘어남.
💡delayed writing
버퍼 캐시에 4k byte가 꽉 차야 실제 I/O가 일어남.
4k byte가 되기 전까지는 버퍼 캐시에 넣어 놓기만 함.
write 시스템 콜을 호출했다고 하더라도 버퍼 캐시가 꽉 찰때까지 write를 안 하기 때문에 delayed write라고 함.

file offset을 바꿔주는 시스템 콜.
file offset은 다음에 읽거나 쓰는 위치를 표시한 것.
random access가 가능한 디스크 regular 파일에서만 쓸 수 있음.
유닉스의 디스크 파일은 각 바이트마다 addressable함.
→ lseek 시스템 콜 때문.
→ 중간에 있는 바이트부터 읽거나 쓰고 싶다면 file offset을 그 위치로 이동시키면 됨.
✔ int start_flag
세 가지 값 중 하나를 가질 수 있음.
💡SEEK_SET #0
파일의 시작 위치 기준.
현재 file offset 값에 관계 없음.
파일 시작 위치에서부터 offset 만큼 이동하라.
SEEK_SET에서 그거보다 앞으로는 갈 수 없음.
💡SEEK_CUR #1
read/write를 하다보면 현재의 file offset 값이 바뀌어져 있음.
현재의 file offset 기준.
현재의 file offset에서 offset 만큼 이동하라.
💡SEEK_END #2
파일의 끝 위치(End of File) 기준.
현재 file offset 값에 관계 없음.
파일의 끝 위치에서 offset 만큼 이동하라.
SEEK_END 에서는 앞뒤로 다 갈 수 있음.
SEEK_END를 통해 파일 사이즈를 계산할 수 있음.
✔ 예제
off_t newpos;
newpos = lseek(fd, (off_t)-16, SEEK_END);
// file offset을 EOF로 이동시키고, -16만큼 이동하라.
fd = open(fname, O_RDWR);
lseek(fd, (off_t)0, SEEK_END);
write(fd, outbuf, OBSIZE);
fd = open(fname, O_WRONLY|O_APPEND);
write(fd, outbuf, OBSIZE);
// 이 두 코드는 기능이 같음. 파일의 맨 끝에 wirte.
off_t filesize;
int filedes;
filesize = lseek(fd, (off_t)0, SEEK_END)
// SEEK_END를 통해 파일 사이즈를 계산하기.
파일에 접근할 때, 공유하면서 접근할 수 있음.
✔ 커널에서 어떻게 프로세스와 file access가 다루어지는가?
커널 안에 process table이라는 자료구조가 있음.
새로운 프로세스가 만들어지면 process table에 entry가 하나 생성됨.
entry 안에 해당 프로세스에 관한 각종 정보가 들어있음.
→ file descriptor table.
file descriptor table은 프로세스 하나 당 한 개씩만 존재.
서로 다른 프로세스의 file descriptor table에는 번호가 같은 칸에 다른 파일이 존재할 수 있음.
파일을 open 하면 커널은 file table을 생성함.
생성된 file table은 프로세스에 종속되어있지 않음.
각각의 file table은 entry를 갖고 있음.
→ 현재 파일을 어떤 상태로 open 했나(RDONLY, WRONLY..)
→ file offset 값
→ 실제 파일을 가리키는 v-node table
file table이 가리키는 파일을 read only로 open 했는데 write를 하려하면 막힘.
v-node와 i-node는 거의 같은 것.
i-node 위에 한 단계 높은 것이 v-node.
v-node table 혹은 i-node table은 실제 파일과 1 대 1로 대응됨.
특정 파일에 대한 v-node table 혹은 i-node table은 딱 하나만 존재.
But 특정 파일에 대한 file table은 여러개 있을 수 있음.
ex) 표준 출력과 표준 에러는 둘 다 스크린 디바이스에 해당함.
file descriptor 1번에 대응되는 file table과 file descriptor 2번에 대응되는 file table이 똑같은 v-node를 가리킴.
→ 1번과 2번이 파일을 공유함.
이 사례는 한 프로세스 내에서 파일을 공유한 사례.
서로 다른 프로세스도 같은 파일을 공유할 수 있음.
아래의 사진이 그 예시.

위의 두 file descriptor 값은 a.out에서의 3번, b.out에서의 3번을 각각 할당 받음.
이때, 둘이 같은 파일을 공유할 수 있음.
✔ 예제

한 프로세스 안에서 같은 파일에 대한 두 개의 file descriptor fd3, fd4를 선언.
file descriptor 3번과 4번이 가리키는 파일이 다름.
But 파일을 open할 때의 flag가 다름.

두 file descriptor는 flag가 다르기 때문에 file table이 같을 수 없음.
But, 같은 파일을 가리키기 때문에 v-node table은 같음.

fd3로 read를 수행하면 file descriptor 3번에 해당하는 file table의 offset은 바뀌지만, 4번에 해당하는 file table의 offset은 그대로 0임.

fd4로 read를 수행하면 3번에 해당하는 file table의 offset은 그대로 있고, 4번에 해당하는 file table의 offset은 바뀜.
결론 : 같은 파일에 대해 서로 다른 file descriptor가 유지하는 file offset이 서로 다르게 존재할 수 있음.

3번을 close하면 3번에 해당하는 file table이 없어짐.

4번을 close하면 4번에 해당하는 file table이 없어짐.
file table은 close 할 때마다 없어지지만, v-node는 없어지지 않음.
파일이 디스크 안에 존재하는 한 v-node는 절대적으로 존재함.

duplicate.
file descriptor를 복제하는 시스템 콜.
dup2는 filedes1을 filedes2로 복제함(original, copy)
dup은 인자로 주어지는 file descriptor가 복제됨.
이때의 리턴 값은 lowest unused integer.
newfd = dup(1);
newfd에 file descriptor로 3번이 할당되고,
1번 file descriptor와 3번 file descriptor가 같은 파일을 가리키게 됨.
2개의 file descriptor가 같은 file table을 가리키기 때문에, file table의 count 값이 2가 됨.
newfd = fcntl(1, F_DUPFD, 0);
0보다 큰 안 쓰이는 번호 중에서 가장 작은 번호가 newfd에 할당됨.
lowest unused file descriptor.
따라서 newfd = 3이 됨.

위의 그림과 같이 3번 file descriptor가 사전에 open 되어 있다고 가정하자.
fd4를 open하면 아래와 같이 됨.

여기서 dup2(3, fd4)를 실행하면, 3번의 내용을 fd4로 copy함.
→ 4번이 overwrite 되면서 4번도 3번이 가리키는 곳을 가리키게 됨.
→ 원래 test라는 파일을 가리키고 있었던 file table을 가리키고 있는 file descriptor 포인터가 끊어짐.


file control.
#include <fcntl.h>가 반드시 있어야 함.
✔ int cmd
이 파일에 적용할 명령들.
이 명령들의 종류에 따라서 세 번째 인자와 리턴 값이 달라짐.
호출에 실패하면 -1이 리턴됨.
💡F_DUPFD
dup2 시스템 콜과 같은 기능을 하는 명령.
존재하는 file descriptor를 복제함.
fcntl(1, F_DUPFD, 0);
위의 코드를 실행하면 1번 file descriptor가 가리키고 있는 포인터 값을 0보다 큰 unused lowest file descriptor 번호에 copy함.
1번이 가리키는 파일 포인터와 unused lowest file descriptor가 가리키는 파일 포인터가 같아짐.
💡F_GETFL, F_SETFL
파일 status flag에 접근.
✔ 예제

인자로 넘어온 filedes가 어떤 모드로 open된 것인지를 알아내는 프로그램.
arg1 = fcntl(filedes, F_GETFL)으로 읽혀온 정보들 중에서 어떤 모드인지에 대한 정보(flag 정보)만 가져와야 함.
ACCMODE(access mode)
#include <fcntl.h>에는 #define O_ACCMODE 값이 3으로 선언되어 있음.
→ 000 000 011
이 O_ACCMODE 값을 arg1과 bitwise and 연산을 하면 arg1에서 모드에 대한 정보가 아닌 값들은 다 걸러짐. flag 정보만 꺼내와짐.
→ read/write에 대한 정보만 읽어들임.
프로세스를 생성하면 0 ~ 2의 file descriptor가 무조건 세팅되어 있음.
0 : standard input.
1 : standard output.
2 : standard error.
✔ input redirection

쉘 프롬프트에 명령어를 입력하면 디폴트로 file descriptor 값 0(키보드)과 1(스크린)이 잡힘.
키보드를 치면 0번 file descriptor로 입력한 값을 읽을 수 있음.
file descriptor 0번을 따로 open 해주지 않아도 read(0,,)을 호출하면 키보드를 통해 입력받을 수 있음.
마찬가지로 1번을 따로 open 해주지 않아도 write(1,,)을 호출하면 스크린에 출력할 수 있음.

키보드 디바이스 파일에 대응되는 v-node
$ prog_name < infile
// 여기서의 infile은 파일 이름.

키보드로 입력된 것을 파일로 redirection 한 것.
키보드에서 읽지 않고 infile이라는 파일에서 입력을 받겠다고 redirection 한 것.
"input redirection" : 입력을 키보드에서 파일로 바꿔준 것.
커맨드 상에서 prog_name < infile을 입력하면 0번 file descriptor를 자동으로 바꿔줌.
prog_name 프로그램을 실행하기 전에 쉘에서 아래의 dup2 명령을 실행해서 0번 file descriptor를 바꿔줌.


위의 dup2 명령을 실행하면 기존에 키보드를 가리키고 있던 0번 file descriptor 대신 3번 file descriptor의 값으로 overwrite 해줌.
프로그램에서는 무조건 read(0,,) 또는 write(1,,)를 수행함.
키보드가 아닌 파일에서 입력을 받으려면 프로그램을 실행하기 전에 쉘에서 0번을 파일로 바꾸는 것.
따라서 위의 dup2는 프로그램 내에서 실행하는 것이 아님.
쉘에 prog_name < infile 명령이 입력되면 키보드를 파일로 리다이렉션하라는 의미로 이해하고 쉘이 그 명령을 수행함.
쉘이 하는 것. 프로그램이 하는 것이 아님.
✔ ouput redirection

input redirection과 같은 구조.
$ prog_name > outfile
output을 스크린으로 하는 것이 아니라 outfile이라는 파일로 하라는 redirection 명령.
쉘이 dup2를 통해 변경해줌.
✔ input/output redirection 동시에 하기
$ prog_name < infile > outfile
쉘이 0번과 1번을 미리 바꿔 놓음.
그런 다음에 prog_name이라는 프로그램을 실행함.
프로그램 안에서는 read(0,) write(1,)을 수행함.
💡정리
프로그램은 아무것도 모르고 read(0, ), write(1, )을 수행함.
쉘이 먼저 0번과 1번을 각각 파일로 리다이렉션 해놓는 것.
라이브러리는 시스템 콜은 아님.
getchar(), getc(), getline(), scanf() 등 입력을 받는 라이브러리 함수들은 커널에서 read를 사용함.
시스템 콜은 read 하나.
input을 받으려면 read를 쓸 수 밖에 없음.
✔ UNIX I/O(system call)
read, write, open 등이 존재.
기본적이고 simple sequence of bytes만 처리.
프로그래머에게 많은 것을 standard I/O로 남겨둠.
효율성 고려는 개발자가 알아서 함.
✔ Standard I/O
시스템 콜을 과도하게 쓰지 않으려면 자체적인 메모리 버퍼가 있어야 함.
→ automatic buffering
좀 더 프로그래머들이 쓰기 편한 인터페이스.
라이브러리를 사용하기 쉬움.
프로그래머는 효율성에 대해 걱정할 필요 없음.
libc.a라는 라이브러리 파일에서 써야 함.
💡함수 예시
파일을 열고 닫음 : fopen/fclose
바이트를 읽고 씀 : fread/fwrite
텍스트 라인을 읽고 씀 : fgets/fputs
포맷화해서 읽고 씀 : fscanf/fprintf

리턴 값이 파일의 포인터. 실패하면 NULL을 리턴.
✔ type
'b'가 붙으면 '바이트 단위로'라는 의미.
r or rb : 읽기 위해서 open
w or wb : 쓰기 위해서 생성하거나 빈 파일로 만듦.
a or ab : append.
r+ or r+b or rb+ : 읽고 쓰기 위해서 open.
w+ or w+b or wb+ : 읽고 쓰기 위해서 생성하거나 빈 파일로 만듦.
a+ or a+b or ab+ : EOF를 시작점으로 해서 읽고 씀.

커널 밑에 buffer cache가 있음.
라이브러리는 시스템 콜을 가능한 적게 호출하기 위해 유저 메모리 레벨에서 buffering mechanism을 씀.

표준 I/O는 buffering mechanism을 이용해 시스템 콜을 과도하게 호출해서 발생하는 비효율성을 피함.
buffering mechanism은 메모리 내에 존재함.
메모리 용량을 확보하기 위해 malloc이라는 시스템 콜을 사용함.

*restrict이 붙은 인자들은 서로 메모리를 공유하면 안된다는 의미.
유닉스는 시스템 차원에서 error handling 방법을 제공함.
커널에 있는 시스템 콜을 호출할 때, 실패하면 -1이 리턴됨.
성공하면 필요한 값을 리턴함.
만약 -1을 리턴하면서 에러가 발생했으면 실패했다는 사실은 알지만 이유는 알지 못함.
시스템 콜이 어떤 이유로 실패했는지를 자세히 설명해주는 정보를 갖고 있는 전역 변수가 세팅되어 있음.
→ errno.h 파일의 errno 변수.
#include <errno.h>를 선언해주면 errno가 extern으로 지정됨.
시스템 콜의 실패 원인을 나타내는 상수 값이 미리 정의되어 있음.
→ 대문자 E로 시작하는 상수들이 #define으로 정의됨.
실패했을 때, 그 원인에 대한 미리 정해진 상수 값을 errno에 세팅해줄 수 있음.
시스템 콜을 실행할 때마다 즉각 errno가 세팅됨.
한참 지나서 다른 시스템 콜을 호출하면 errno가 다시 세팅됨.
overwrite되기 때문에 이전의 error에 대한 errno는 확인할 수 없음.
또 다른 시스템 콜을 호출했는데 실패하지 않았으면 errno 값은 리셋되지 않고 이전의 값으로 유지됨.
에러가 발생하지 않는 시스템 콜을 실행했을 때, errno가 리셋되지 않음.
errno는 시스템 콜을 실행하는 동안 가장 최근의 error type을 저장하고 있음.
그러므로, 시스템 콜의 실행 실패를 확인하는 즉시 errno 값을 확인하라.

💡strerror
에러를 설명해주는 긴 문장을 리턴해줌.

💡perror(argv[0])
argv[0]의 형태로 문자열을 출력해주고, 그 뒤에 strerror(errno)가 출력됨.
→ 어떤 에러인지에 대한 문장이 출력됨.
따라서 이 perror를 이용해 디버깅할 수 있음.
✔ 예제



파일을 open하려는데 없으면 실패함.
파일이 존재하지만 permission이 없을 때도 실패함.