공대생 정리노트
파일 I/O - write과 read시 파일 lock이 있을까? 본문
코드를 짜다가 여러 파일 디스크립터가 한 파일에 write을 할 때 디스크립터가 close를 하지 않으면 다른 파일 디스크립터로 쓰지 못하는지 햇갈렸다.
찾아보니 학교 전공 시간에 배웠던 것인데 까먹었었다.. 이번 기회에 글로 남겨 확실히 이해하고 넘어가려고 한다.
실험
package main
import (
"fmt"
"os"
"sync"
"time"
)
func createFileDescriptor(filename string) (*os.File, func()){
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
fmt.Println("err to open file")
}
close := func() {
err := fp.Close()
if err != nil {
fmt.Println("close file error: ", err)
}
}
return fp, close
}
func main() {
var wgFd sync.WaitGroup
wgFd.Add(1)
go func(){
defer wgFd.Done()
fd, close := createFileDescriptor("testFd")
for i := 1; i<100; i++ {
fd.WriteString("test statement1\n")
time.Sleep(time.Second*2)
}
close()
}()
time.Sleep(time.Second*1)
wgFd.Add(1)
go func(){
defer wgFd.Done()
fd, close := createFileDescriptor("testFd")
for i := 1; i<100; i++ {
fd.WriteString("test statement2\n")
time.Sleep(time.Second*2)
}
close()
}()
wgFd.Wait()
}
결과
test statement1
test statement2
test statement1
test statement2
test statement1
test statement2
test statement1
test statement2
test statement1
test statement2
...
파일 디스크립터를 닫지 않고 열어놔도 다른 파일 디스크립터로 해당 파일에 write을 할 수 있다.
혹시 파일 관련해서 lock이 있는지 궁금해서 소스 코드를 뒤적여봤다.
OS 패키지 살펴보기 - CloseOnExec이 무엇인가?
요약 : CloseOnExec 이란 flag를 발견해서 lock과 관련있는지 살펴봤는데 관련이 없었다
os.Openfile
// OpenFile is the generalized open call; most users will use Open
// or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.). If the file does not exist, and the O_CREATE flag
// is passed, it is created with mode perm (before umask). If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError.
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
testlog.Open(name)
f, err := openFileNolog(name, flag, perm)
if err != nil {
return nil, err
}
f.appendMode = flag&O_APPEND != 0
return f, nil
}
os.openFileNolog
// openFileNolog is the Unix implementation of OpenFile.
// Changes here should be reflected in openFdAt, if relevant.
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
setSticky := false
if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
if _, err := Stat(name); IsNotExist(err) {
setSticky = true
}
}
var r int
for {
var e error
r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e == nil {
break
}
// On OS X, sigaction(2) doesn't guarantee that SA_RESTART will cause
// open(2) to be restarted for regular files. This is easy to reproduce on
// fuse file systems (see https://golang.org/issue/11180).
if runtime.GOOS == "darwin" && e == syscall.EINTR {
continue
}
return nil, &PathError{"open", name, e}
}
// open(2) itself won't handle the sticky bit on *BSD and Solaris
if setSticky {
setStickyBit(name)
}
// There's a race here with fork/exec, which we are
// content to live with. See ../syscall/exec_unix.go.
if !supportsCloseOnExec {
syscall.CloseOnExec(r)
}
return newFile(uintptr(r), name, kindOpenFile), nil
}
openFileNolog 함수를 보면 시스템 콜로 파일을 열 때 flag에 syscall.O_CLOEXEC을 추가하는 것을 볼 수 있다.
또한 밑 코드는 supportsCloseOnExec이 아닐 때 CloseOnExec을 강제하는 것을 볼 수 있다.
그렇다면 CloseOnExec은 무엇일까?
exec()을 호출하는 프로그램이 열어놓은 모든 파일 디스크립터는 exec() 동안에도 계속 열려있는게 기본 설정이라 새로 시작되는 프로그램에도 사용될 수 있다.
이 방법을 이용하면 호출하는 프로그램이 특정 디스크립터를 열어놓아, 새 프로그램이 파일 이름을 알 필요도 없고, 파일을 열 필요도 없이 자동적으로 사용할 수 있다.
CLOSEXEC 플래그는 exec을 하기 전 해당 파일 디스크립터를 닫게 한다. 성공적인 exec() 동안에는 파일 디스크립터가 자동으로 닫히지만 exec()가 실패하면 열린 상태로 남는다.
결론 : CloseOnExec과 lock은 상관이 없다 (그래도 새로운 사실을 알았다)
I-노드
전공 수업에서 보았을 그림이다.
리눅스 서적(리눅스 API의 모든 것)을 찾아보다가 다시 발견한 그림인데, i-노드 테이블에 파일 잠금이 있는 것을 발견하였다.
여기서 주의해야할 것은 디스크상의 i-노드와 메모리상의 i-노드는 기록하는 것이 다르다.
디스크상의 i-노드는 종류, 권한, 타임스탬프 등과 같은 파일의 지속적인 속성을 기록.
파일에 접근하면 메모리에 i-노드의 복사본이 만들어지고, 이 복사본은 i-노드를 가리키는 열린 파일 디스크립션의 개수와 원본 i-노드가 있는 디바이스의 주 ID와 부 ID를 기록한다. 메모리상의 i-노드는 파일 잠금과 같은 파일이 열려 있는 동안 존재하는 여러 가지 단명하는 속성도 기록한다.
즉, 파일 잠금은 메모리상의 i-노드가 기록한다.
그렇다면 파일 잠금은 무엇일까?
파일 잠금
https://man7.org/linux/man-pages/man2/fcntl.2.html
위 링크는 파일 디스크립터를 관리하는 함수인 fnctl의 man 페이지이다.
설명을 읽다보면 다음 구절이 나온다.
Advisory record locking
Linux implements traditional ("process-associated") UNIX record locks, as standardized by POSIX. For a Linux-specific alternative with better semantics, see the discussion of open file description locks below.
F_SETLK, F_SETLKW, and F_GETLK are used to acquire, release, and test for the existence of record locks (also known as byte-range, file-segment, or file-region locks).
The third argument, lock, is a pointer to a structure that has at least the following fields (in unspecified order).
struct flock {
...
short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock (set by F_GETLK and F_OFD_GETLK) */
...
};
...
Mandatory locking
Warning: the Linux implementation of mandatory locking is unreliable. See BUGS below. Because of these bugs, and the fact that the feature is believed to be little used, since Linux 4.5, mandatory locking has been made an optional feature, governed by a configuration option (CONFIG_MANDATORY_FILE_LOCKING). ...
By default, both traditional (process-associated) and open file description record locks are advisory. Advisory locks are not enforced and are useful only between cooperating processes.
Both lock types can also be mandatory. Mandatory locks are enforced for all processes. ...
file lock은 Advisory Record Locking과 Mandatory Locking 두 가지로 나눌 수 있다.
Adivsory Record Lock은 기록하는 것과 관련된 lock을 얻는 것이고, Mandatory Locking은 모든 프로세스에게 강제되는 Lock이며 unreliable하다고 나와있다.
https://www.thegeekstuff.com/2012/04/linux-file-locking-types/
위의 article을 읽어보면 조금 더 풀어서 써놓았다.
요약하면,
Advisory Record Lock의 경우 프로세스 A가 WRITE lock을 얻고 파일에 쓰고 있어도, 프로세스 B가 lock을 얻지 않고 파일을 열고 쓸 수 있다. 여기서 프로세스 B를 non-cooperating process라고 하며 Advisory Record Lock이 동작하려면 참여하는 프로세스들이 cooperative해야 한다.
Mandatory Locking은 커널이 주어진 파일에게 접근하는 모든 프로세스가 열고, 읽고, 쓸때마다 lock을 얻었는지 검증한다.
리눅스에서 이를 적용하려면 따로 마운트해서 사용해야 한다.
결론
Mandatory Locking을 적용하지 않은 리눅스에서는 파일 I/O에 대해 자동으로 lock을 걸어주지 않아 race condition이 발생할 수 있다.
Mandatory Locking을 적용하면 부작용이 나올 수 있으니(위 fnctl man page) 사용자가 Advisory Record Lock을 사용하는 등의 처리를 해주어야 할 것 같다.
'언어 > Go' 카테고리의 다른 글
Map : concurrent 상황일 때 Read도 RLock을 해야 하는 이유 (0) | 2022.02.18 |
---|---|
동시성 성능 테스트 mutex VS chan VS atomic (0) | 2021.12.19 |
Effective Go (2) 요약 (0) | 2021.11.01 |
Effective Go (1) 요약 (0) | 2021.10.18 |
동시성 패턴 (0) | 2021.05.05 |