전공/시스템 프로그래밍(운영체제)

2. System Call: open

문정훈 2021. 10. 12. 21:44

1.  System call open

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open (const char *name, int flags);
int open (const char *name, int flags, mode_t mode)

 리눅스 시스테 콜의 open은 첫 번째 매개변수로 주어진 문자열의 경로에 해당하는 파일 열고 파일 디스크립터를 반환하는 것이다. 

두 번째 매개변수는 파일을 열 때 플레그를 지정한다. 그리고 세 번째 매개변수는 파일을 열 때 사용자가 그 연 파일의 권한을 부여하고 여는 것이다.  

 

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
	 int fd;
	fd = open("./file.txt", O_WRONLY | O_CREAT, 662);
	
	return 0;
}

위 코드는 open 시스템 콜을 사용해 파일을 open한 예시이다.  파일을 쓰기 전용으로 열고 파일이 없다면 662-(umask) 권한을 가지도록 새로운 파일을 생성한다.

 

※ umask

파일을 생성할 때 리눅스에서 일반 파일은 umask가 적용 안된 상태에서 기본적으로 666으로 파일을 만든다. umask의 기본 값은 002이기 때문에 vi 편집기로 파일을 만들면 664(rw-rw-r)로 만들어진다.

만약 umask가 001이라면 666에서 rwx중 x를 빼는 것이다. 근데 666은 이미 x가 빠진 (rw-rw-rw) 이기 때문에 666의 권한을 가지고 파일이 만들어진다. 

만약 umask 값이 005였다면 vi 편집기로 파일을 만들면 666에서 005( --- --- r-x)를 빼는 것이기 때문에 

666(rw-rw-rw-)에서 기타 사용자 권한의 r과 x를 빼서 664 로 만들게 된다. 

 

정리=> 666에서 umask 값 005를 빼서 661로 만들어진다? -> 아님 

 

 

1) open의 두 번째 매개변수 : 파일 플레그 

open() 함수의 두 번째 인자로 전달하는 플레그는 파일을 읽기 위한 것인지, 쓰기 위한 것인지, 읽기 쓰기 혼용을 위한 것인지 3가지 중 하나를 필수적으로 전달해야 한다. 

O_READONLY  읽기 전용
O_WRONLY    쓰기 전용
O_RDWR      읽기 / 쓰기 혼용

 

 

<플레그 정리>

아래의 추가 플레그를 선택적으로 함께 지정할 수 있다. 

O_APPEND
이는 파일을 열 떄 append모드로 파일을 연다. 쓰기 작업이 진행되면 file offset이 항상 파일의 끝을 가리켜 쓰게된다. 
O_ASYNC

 특정 파일에서 읽기/쓰기가 가능해질 때(파일 디스크립터가 음수가 아닐 때) 시그널이 발생한다. O_ASYNC 플래그는 일반 파일에는 사용할 수 없고, 터미널과 소켓 파일에만 사용할 수 있다.
async 모드는 다른 프로세스에서 이미 읽고 있을 때 다른 프로세스의 사용이 끝나면 알려주는 것이다.
O_CLOEXC

열린 파일에 대해 close-on-exec 플래그를 설명하며 새 프로세스를 실행하면 자동으로 닫힌다.
O_CREAT 

파일을 open할 때 파일이 없다면 파일을 새로 만든다. 이때 세 번째 인자로 권한을 부여하지 않으면 파일이 생성되지 않는다. 서 벤째 인자로 주어진 권한에 umask를 뺀 권한으로 새로운 파일을 생성함.
O_DIRECT 

직접 입출력 작업을 수행하기 위해 파일을 연다.
O_DIRECTORY 

name의 경로에 해당하는 파일이 디렉토리가 아니면 open()호출을 실패한다. 
O_EXCL 

O_CREAT와 함께 사용하면 name으로 지정한 파일이 이미 있을 때 open() 호출이 실패한다. 파일 생성 과정에서 중복, 즉 경쟁 상대를 피하기 위해 자주 사용된다.

O_LARGEFILE 

용량이 2GB를 초과하는 파일을 열며, 64bit offset을 사용한다

 

<다른 추가 플레그 정리>

O-NOATIME+

파일을 열면 항상 시간이 기록되는데 이 모드로 열면 해당 파일에 접근했다는 시간 정보를 업데이트하지 않는다. 백업을 하려할 때 사용한다.
O_NOFLLOW

심볼릭 링크라면 열리지 않는다.

O_NONBLOCK

이 파일 열고 난 다음 다른 프로세스가 끼어들 수 없다. 독자적으로 사용하는 것임
파일을 가능한 nonblocking 모드로 연다. open() 시스템 콜이나 다른 연산은 입출력 과정에서 프로세스를 block하지 않는다. FIFO에서만 이러한 동작 방식이 정의된다.
쉽게말해
O_SYNC

쓰기 작업이 끝날때까지 다른 프로세스가 건들 수 없다. 이 파일을 잠그고 기록하는 것이다.
open 시 파일을 동기식 입출력으로 연다. 일반적으로 읽기 연산인 동기식 연산인데 리눅스에서는 비동기 방식으로 여는 플레그가 있기 때문에 이 방식이 존재한다고 생각하면 됨.
데이터를 물리적으로 디스크에 쓰기 전까지는 쓰기 연산이 완료되지 않는다.
O_TRUCN

파일이 존재하고, 파일이 일반 파일이고, 또 flags 인자에 쓰기가 가능하도록 되어 있으면(O_WRONLY 쓰기 전, 또는 O_RDWR 읽기 쓰기 전용) 파일 길이를 0으로 만든다.

2) open의 세 번째 매개변수 : 파일 권한

상수 권한
S_IRWXU S_IRWXU
S_IRUSR u+r
S_IWUSR u+w
S_IXUSR u+x
S_IRWXG g+rwx
S_IRGRP g+r
S_IWGRP g+w
S_IXGRP g+x
S_IRWXO o+rwx
S_IROTH o+r
S_IWOTH o+w
S_IXOTH o+x

open 시스템 콜의 세 번째 인자는 플레그 중 O_CREAT 플레그와 함께 사용되야한다. 

O_CREAT 플레그를 지정하면 파일을 열 때 만약 파일이 없다면 새로 파일을 만드는데 그때 세 번째로 인자로 주어진 권한을 가지고 파일을 만든다.

 

권한은 위 표와 같이 상수를 | 로 연결하여 지정할 수도 있지만 8진수 3자리를 사용해 나타낼 수 있다. 이때 3번째 인자로 지정한 권한에서 umask 값은 뺴고 새로 만들어지는 파일의 권한이 부여된다. 

예를 들어 664로 3번째 인자를 줬고 umask 값이 002라면 662로 파일의 권한이 부여된다. 

 

 

2.  File descriptor value

리눅스 시스템에서 모든 것은 파일로 관리되기 떄문에 파일을 열고 읽고 쓰는 작업은 매우 중요하다.

모든 파일은 자신을 식별하고, 자신임을 알려주는 파일 디스크립터를 가진다. 파일에 접근하기 위해서는 파일 디스크립터를 가지고 있어야한다. 

파일을 읽거나 쓰기 전에는 먼저 open을 해줘야한다.

 

  1. 이때 open 된 파일들은 file table이라는 목록에 파일 디스크립터가 기록되고 커널에 의해 관리된다.
  2. 파일 디스크립터가 인덱싱된 file table에는 해당 파일의 inode 정보를 가리키는 inode table의 포인터를 가지고 있다.
  3. Inode table에는 해당 파일의 inode 정보가 저장되어 있다. 

 

 

파일을 open 하게 되면 파일을 가리키기 위한 값을 하나 리턴 받는데 이것이 파일 디스크립터이고 이 값은 0부터 시작한다. 원래는 1024까지 있는데 1,048,576이까지 가질수도 있으며 파일을 열 떄 오류가 발생하면 음수를 리턴한다.

 

각각의 프로세스별로 공통의 파일 디스크립터를 가지고 있다.

예를 들어 p1, p2, p3 있다하면 모두가 가리키고 있는 공통적인 파일 디스크립터가 있는데 그것은 위 그림에서 0이면 standard in(키보드, 마우스) , 1stadard out(모니터) 2error이다.


 

3. O_SYNC, O_DSYNC, O_RSYNC, O_DIRECT 플레그에 자세히 정리함

1) O_SYNC

int fd;

fd = open(File, O_WRONLY | O_SYNC);

if(fd == -1) {
	perror("open");
    return -1;
}

파일을 open할 때 일단 쓰기 전용으로 열고, O_SYNC 플레그를 지정하였다. 

그럼 어떤 작업을 하던 HDD와 버퍼가 강제로 sync가 되야함을 의미함. 부연설명 하면 sync란 버퍼랑 HDD를 일치 시킨다는 것인데 write로 어떤 작업을 하면 RAM에 버퍼에 작성하는데 곧 바로 버퍼에서 HDD를 flush 하요 sync를 한다는 플레그가 된다. 

 

이것의 단점은 무엇일까?

심지어 O_SYNC 플레그로 연 파일을 read할 때도 바뀐 내용은 없지면 sync가 일어난다. (flush 작업이 진행된다.)

=> 시스템에 무리가 많이 간다. 

read를 하는데도 왜 sync 작업이 일어날까?

RAM에 있는 load된 파일을 읽을 때 파일의 메타 데이터가 바뀌는데 메타 데이터 중 마지막으로 파일에 접근한 시간 같은 메타 데이터가 read할 때 바뀔 수 있기 때문에 이는 버퍼에 있는 이 메타 정보랑 HDD에 있는 파일의 메타 정보가 서로 다를수 있게 되기 때문에 sync 작업이 일어난다. 

 

정리=> O_SYNC 플레그가 사용되면 RAM에 버퍼에 있는 파일 내용, 메타 데이터가 바뀌면 곧 바로 sync 작업이 일어나며 write는 RAM 버퍼에 load된 파일의 내용을 변경하고 read는 메타 데이터를 변경한다. 

 

이 플레그를 O_RSYNC 플레그와 구별하기 위해 좀 더 구체화 하면 

read로 바뀐 dirty buffer의 내용은 메타 데이터 변경 뿐이다. 따라서 파일의 내용은 sync하지 않고 메타 데이터 만 sync 한다.

write로 바뀐 dirty buffer의 내용은 메타 데이터뿐만 아니라 파일의 내용까지 바꾸므로 메타 데이터, 파일 내용 모두 sync한다.  

 

 

2) O_DSYNC

메타 데이터 말고 안에 파일의 내용만 수정된 경우 sync를 한다. write 연산이 이루어져 RAM 버퍼에 load된 파일의 "내용"이 수정되면, 이때 메타 데이터는 빼고 dirty buffer의 변경된 내용만 flush 하는 sync 작업을 OS의 스케줄링 없이 곧 바로 실행한다. 

이 플레그는 메타 데이터는 바로 기록하지 않이(실제론 나중에 기록 되긴함) 좀 위험한 플레그임

만약에 P1 프로세스에서 이 플레그를 사용해 write를 했다면, 파일의 내용만 sync되지 파일의 크기가 수정되었다는 메타 데이터는 바로 저장되지 않는다. 

근데 이때 P2 프로세스에서 HDD의 내용만 바뀌고 파일의 크기인 메타 정보는 아직 sync되지 않은 상태에서 접근한다.

그러면 P2는 이전의 파일 크기를 가지는 메타 데이터로 접근하는 것이기 때문에 파일의 내용은 수정되었는데 메타 데이터인 파일의 사이즈는 이전의 정보라 이 이전의 정보인 파일 사이즈만큼만 파일을 읽어버리는 문제가 발생함.4

 

정리=>

이 모드로 파일을 열고 write하면 HDD의 파일의 내용과 메타데이터가 일치하지 않는 시점이 생기는데 이 시점에 다른 프로세스에서 파일에 접근하면 문제가 발생한는 단점이 있다.

 

3) O_RSYNC

read write 상관 없이 항상 read, write가 호출되면 메타 데이터 그리고 파일 내용 모두 걍 같이 sync를 맞추라는 플레그가 된다. 

 

 

4) O_DIRECT

<O_DIRECT를 설명하기 위해 DB의 예시를 든 설명>

데이터 베이스는 파일 management를 OS가 안해줬으면 하는 경우가 있다. 자세히 설명하면 

DB 프로그램에 저장된 것이 HDD에 저장을 하고 읽어오는 작업을 하는데, 주로 DB의 동작은 HDD에서 아주 큰 데이터를 읽어오거나 저장하는데,

DB는 효율적으로 HDD에 접근해 데이터를 읽어오고 저장하는 것이 목적이기 때문에

DB가 데이터를 읽는 시점이나 저장하는 시점은 DB 프로그램이 알아서 결정을 한다.

 

DB는 OS에서 제공하는 다음과 같은 기능

  1. HDD의 캐쉬같은 것을 사용하고 싶지 않다. !!
  2. 커널이 원하는 시점에 sync 작업을 하는 기능

위와 같은 기능을 DB는 사용하고 싶지 않아한다. 

 

<O_DIRECT>

위 비유와 같이 O_DIRECT 플레그를 사용하면 OS가 관여하는 것을 무시하고 sync 작업 등을

알아서 한다는 개념이다. 


 

4. System call: close

1) int close(int fd);

메인 메모리의 file table 에 들어있는 파일 디스크립터를 삭제 하는 것이다.


 

c언어에서 개발자 입장에서는 시스템 콜 함수던 라이브러리 함수던 단지 함수의 형태이므로 특별히 구분되지는 않는다. 

시스템 콜이 자주 호출되면 시스템 resource를 효츌적으로 사용할 수 없고, 성능에 영향을 준다.

그리고 시스템 콜은 User 모드가 아닌 Kernel모드로 실행된다.

c언에서 시스템 콜을 최소화하기 위해 fopen, fclose, fread, fwrite와 같은 API를 제공한다.