파일 디스크립터의 모든 것: 리눅스 I/O의 핵심
중급 개발자를 위한 파일 디스크립터 완벽 가이드
목차
- 파일 디스크립터란 무엇인가
- 파일 디스크립터 테이블의 내부 구조
- 표준 입출력과 파일 디스크립터
- 파일 디스크립터 조작
- 파일 디스크립터와 프로세스
- 파일 디스크립터 고급 기법
- 파일 디스크립터 제한과 관리
- 실전 문제 해결
1. 파일 디스크립터란 무엇인가
1.1 기본 개념
**파일 디스크립터 (File Descriptor, fd)**는 유닉스/리눅스 시스템에서 열린 파일을 참조하는 정수 값입니다.
파일 디스크립터 = 커널이 관리하는 파일 테이블의 인덱스핵심 특징:
- 음이 아닌 정수 (0, 1, 2, 3, ...)
- 프로세스마다 독립적인 파일 디스크립터 공간
- "모든 것은 파일이다" (Everything is a file) 철학의 구현체
1.2 파일 디스크립터가 가리키는 것들
파일 디스크립터는 단순한 파일 이상을 가리킵니다:
| 타입 | 설명 | 예시 |
|---|---|---|
| 일반 파일 | 디스크의 파일 | /etc/passwd, data.txt |
| 디렉터리 | 디렉터리도 파일 | /home/user/ |
| 소켓 | 네트워크 연결 | TCP/UDP 소켓 |
| 파이프 | 프로세스 간 통신 | ` |
| 장치 파일 | 하드웨어 장치 | /dev/null, /dev/random |
| 심볼릭 링크 | 다른 파일을 가리킴 | 링크 파일 |
| eventfd | 이벤트 통지 | 커널 이벤트 |
| timerfd | 타이머 | 타이머 이벤트 |
| signalfd | 시그널 | 시그널 수신 |
1.3 Go에서 파일 디스크립터 확인하기
go
package main
import (
"fmt"
"net"
"os"
)
func main() {
// 1. 일반 파일 열기
file, err := os.Create("test.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
defer file.Close()
// 파일 디스크립터 가져오기
fileFd := file.Fd()
fmt.Printf("파일 'test.txt'의 파일 디스크립터: %d\n", fileFd)
// 2. 네트워크 소켓
listener, err := net.Listen("tcp", ":0")
if err != nil {
fmt.Println("Error:", err)
return
}
defer listener.Close()
// 소켓의 파일 디스크립터 가져오기
tcpListener := listener.(*net.TCPListener)
listenerFile, _ := tcpListener.File()
socketFd := listenerFile.Fd()
fmt.Printf("TCP 리스너의 파일 디스크립터: %d\n", socketFd)
// 3. 표준 입출력
fmt.Printf("표준 입력(stdin)의 파일 디스크립터: %d\n", os.Stdin.Fd())
fmt.Printf("표준 출력(stdout)의 파일 디스크립터: %d\n", os.Stdout.Fd())
fmt.Printf("표준 에러(stderr)의 파일 디스크립터: %d\n", os.Stderr.Fd())
// 4. 파이프 생성
reader, writer, err := os.Pipe()
if err != nil {
fmt.Println("Error:", err)
return
}
defer reader.Close()
defer writer.Close()
fmt.Printf("파이프 읽기 끝의 파일 디스크립터: %d\n", reader.Fd())
fmt.Printf("파이프 쓰기 끝의 파일 디스크립터: %d\n", writer.Fd())
}출력 예시:
파일 'test.txt'의 파일 디스크립터: 3
TCP 리스너의 파일 디스크립터: 4
표준 입력(stdin)의 파일 디스크립터: 0
표준 출력(stdout)의 파일 디스크립터: 1
표준 에러(stderr)의 파일 디스크립터: 2
파이프 읽기 끝의 파일 디스크립터: 5
파이프 쓰기 끝의 파일 디스크립터: 61.4 왜 파일 디스크립터를 사용하는가?
추상화의 힘:
go
package main
import (
"fmt"
"io"
"os"
)
// 파일 디스크립터 덕분에 모든 I/O가 동일한 인터페이스 사용
func readFromAnything(fd uintptr) {
file := os.NewFile(fd, "generic")
defer file.Close()
buffer := make([]byte, 1024)
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
fmt.Println("Error:", err)
return
}
fmt.Printf("읽은 데이터: %s\n", buffer[:n])
}
func main() {
// 일반 파일에서 읽기
file, _ := os.Open("test.txt")
readFromAnything(file.Fd())
file.Close()
// 네트워크 소켓에서 읽기
// (같은 함수로 처리 가능!)
// 파이프에서 읽기
// (같은 함수로 처리 가능!)
}보안과 권한 관리:
- 커널이 파일 디스크립터를 통해 접근 권한 검증
- 프로세스는 직접 파일 경로가 아닌 fd로만 접근
- 권한 체크는 open() 시점에만 수행
2. 파일 디스크립터 테이블의 내부 구조
2.1 세 가지 테이블
리눅스 커널은 파일 디스크립터를 관리하기 위해 세 계층의 테이블을 사용합니다:
┌─────────────────────────────────────────────────────────┐
│ 프로세스 A │
│ │
│ ┌────────────────────────────────┐ │
│ │ 파일 디스크립터 테이블 │ │
│ ├────┬────────────────────────────┤ │
│ │ 0 │ → 파일 테이블 항목 1 │ │
│ │ 1 │ → 파일 테이블 항목 2 │ │
│ │ 2 │ → 파일 테이블 항목 2 │ (같은 항목 공유) │
│ │ 3 │ → 파일 테이블 항목 3 │ │
│ │ 4 │ → 파일 테이블 항목 4 │ │
│ └────┴────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 커널 전역: 파일 테이블 │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 파일 테이블 항목 1 │ │
│ │ - 파일 상태 플래그 (읽기/쓰기) │ │
│ │ - 파일 오프셋 (현재 위치) │ │
│ │ - inode 포인터 ───────────┐ │ │
│ └─────────────────────────────│────────────┘ │
│ │ │
│ ┌─────────────────────────────│────────────┐ │
│ │ 파일 테이블 항목 2 │ │ │
│ │ - 파일 상태 플래그 │ │ │
│ │ - 파일 오프셋 │ │ │
│ │ - inode 포인터 ───────────┐ │ │ │
│ └───────────────────────────│─│────────────┘ │
└────────────────────────────────│─│──────────────────────┘
↓ ↓
┌─────────────────────────────────────────────────────────┐
│ 커널 전역: inode 테이블 │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ inode 1 │ │
│ │ - 파일 타입 (일반/디렉터리/소켓) │ │
│ │ - 파일 크기 │ │
│ │ - 권한 정보 │ │
│ │ - 타임스탬프 │ │
│ │ - 디스크 블록 위치 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘2.2 각 테이블의 역할
1. 파일 디스크립터 테이블 (Per-Process)
- 각 프로세스가 소유
- fd 번호 → 파일 테이블 항목 매핑
fork()시 자식에게 복사됨
2. 파일 테이블 (System-wide)
- 커널 전역
- 열린 파일의 상태 정보:
- 파일 오프셋: 현재 읽기/쓰기 위치
- 접근 모드: O_RDONLY, O_WRONLY, O_RDWR
- 상태 플래그: O_APPEND, O_NONBLOCK 등
- 여러 fd가 같은 항목을 공유 가능 (
dup(),fork())
3. inode 테이블 (System-wide)
- 파일 시스템의 메타데이터
- 실제 파일을 나타냄
- 여러 파일 테이블 항목이 같은 inode 공유 가능
2.3 Go로 테이블 구조 이해하기
go
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
// 같은 파일을 두 번 열기
file1, _ := os.OpenFile("test.txt", os.O_RDWR, 0644)
file2, _ := os.OpenFile("test.txt", os.O_RDWR, 0644)
defer file1.Close()
defer file2.Close()
fmt.Printf("file1 fd: %d\n", file1.Fd())
fmt.Printf("file2 fd: %d\n", file2.Fd())
// file1에서 5바이트 읽기
buf1 := make([]byte, 5)
file1.Read(buf1)
fmt.Printf("file1에서 읽음: %s\n", buf1)
// file1의 현재 위치 확인
pos1, _ := file1.Seek(0, os.SEEK_CUR) // 현재 위치 반환
fmt.Printf("file1 오프셋: %d\n", pos1) // 5
// file2의 현재 위치 확인 (독립적인 파일 테이블 항목)
pos2, _ := file2.Seek(0, os.SEEK_CUR)
fmt.Printf("file2 오프셋: %d\n", pos2) // 0 (독립적!)
// 하지만 같은 inode를 가리킴
stat1, _ := file1.Stat()
stat2, _ := file2.Stat()
sys1 := stat1.Sys().(*syscall.Stat_t)
sys2 := stat2.Sys().(*syscall.Stat_t)
fmt.Printf("file1 inode: %d\n", sys1.Ino)
fmt.Printf("file2 inode: %d\n", sys2.Ino)
fmt.Printf("같은 inode? %v\n", sys1.Ino == sys2.Ino) // true!
}출력:
file1 fd: 3
file2 fd: 4
file1에서 읽음: Hello
file1 오프셋: 5
file2 오프셋: 0
file1 inode: 12345678
file2 inode: 12345678
같은 inode? true2.4 dup()와 파일 테이블 공유
go
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
// 파일 열기
file1, _ := os.OpenFile("test.txt", os.O_RDWR, 0644)
defer file1.Close()
// dup(): 같은 파일 테이블 항목을 가리키는 새 fd 생성
newFd, err := syscall.Dup(int(file1.Fd()))
if err != nil {
fmt.Println("dup 실패:", err)
return
}
file2 := os.NewFile(uintptr(newFd), "duped")
defer file2.Close()
fmt.Printf("원본 fd: %d\n", file1.Fd())
fmt.Printf("복제된 fd: %d\n", file2.Fd())
// file1에서 5바이트 읽기
buf := make([]byte, 5)
file1.Read(buf)
fmt.Printf("file1에서 읽음: %s\n", buf)
// file1의 오프셋 확인
pos1, _ := file1.Seek(0, os.SEEK_CUR)
fmt.Printf("file1 오프셋: %d\n", pos1) // 5
// file2의 오프셋도 같음! (같은 파일 테이블 항목 공유)
pos2, _ := file2.Seek(0, os.SEEK_CUR)
fmt.Printf("file2 오프셋: %d\n", pos2) // 5 (공유!)
// file2에서 계속 읽기
buf2 := make([]byte, 5)
file2.Read(buf2)
fmt.Printf("file2에서 읽음: %s\n", buf2) // 다음 5바이트
// 이제 둘 다 오프셋 10
pos1, _ = file1.Seek(0, os.SEEK_CUR)
pos2, _ = file2.Seek(0, os.SEEK_CUR)
fmt.Printf("file1 오프셋: %d, file2 오프셋: %d\n", pos1, pos2) // 10, 10
}핵심 차이점:
open()두 번: 서로 다른 파일 테이블 항목 → 독립적인 오프셋dup(): 같은 파일 테이블 항목 공유 → 오프셋 공유
3. 표준 입출력과 파일 디스크립터
3.1 특별한 파일 디스크립터들
모든 프로세스는 시작 시 세 개의 파일 디스크립터를 가집니다:
| fd | 이름 | 심볼릭 상수 | 용도 |
|---|---|---|---|
| 0 | stdin | STDIN_FILENO | 표준 입력 |
| 1 | stdout | STDOUT_FILENO | 표준 출력 |
| 2 | stderr | STDERR_FILENO | 표준 에러 |
3.2 리다이렉션의 원리
쉘에서 command > output.txt를 실행하면 무슨 일이 일어날까요?
go
package main
import (
"fmt"
"os"
"syscall"
)
// 표준 출력을 파일로 리다이렉트하는 예제
func redirectStdout() {
// 1. 새 파일 열기
file, err := os.Create("output.txt")
if err != nil {
fmt.Fprintln(os.Stderr, "파일 생성 실패:", err)
return
}
defer file.Close()
// 2. stdout(fd 1)을 백업
oldStdout, _ := syscall.Dup(syscall.Stdout)
// 3. stdout을 file로 복제 (dup2)
// 이제 fd 1이 file을 가리킴
syscall.Dup2(int(file.Fd()), syscall.Stdout)
// 4. 이제 fmt.Println은 파일로 출력됨!
fmt.Println("이 메시지는 output.txt에 기록됩니다")
fmt.Println("stdout이 리다이렉트되었습니다")
// 5. stdout 복원
syscall.Dup2(oldStdout, syscall.Stdout)
syscall.Close(oldStdout)
// 6. 이제 다시 터미널로 출력
fmt.Println("stdout이 복원되었습니다 (터미널에 표시)")
}
// stderr와 stdout의 차이
func stderrVsStdout() {
fmt.Println("이것은 stdout입니다")
fmt.Fprintln(os.Stderr, "이것은 stderr입니다")
// 쉘에서 실행:
// go run main.go > output.txt
// → stdout만 파일로, stderr는 여전히 터미널에 표시됨
// go run main.go 2> error.txt
// → stderr만 파일로, stdout은 여전히 터미널에 표시됨
// go run main.go > output.txt 2>&1
// → stdout과 stderr 모두 파일로
}
func main() {
fmt.Println("=== 리다이렉션 데모 ===")
redirectStdout()
fmt.Println("\n=== stderr vs stdout ===")
stderrVsStdout()
}3.3 파이프의 구현
go
package main
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
)
// 파이프를 사용한 프로세스 간 통신
func pipeExample() {
// os.Pipe()는 두 개의 fd를 생성:
// - reader: 읽기 전용 fd
// - writer: 쓰기 전용 fd
reader, writer, err := os.Pipe()
if err != nil {
fmt.Println("Pipe 생성 실패:", err)
return
}
fmt.Printf("파이프 생성: reader fd=%d, writer fd=%d\n",
reader.Fd(), writer.Fd())
// 고루틴에서 데이터 쓰기
go func() {
defer writer.Close()
for i := 0; i < 5; i++ {
fmt.Fprintf(writer, "메시지 %d\n", i)
}
}()
// 메인에서 데이터 읽기
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
fmt.Printf("받은 데이터: %s\n", scanner.Text())
}
reader.Close()
}
// 자식 프로세스와 파이프로 통신
func childProcessPipe() {
// `echo hello | grep hello` 구현
// 1. 첫 번째 명령: echo hello
cmd1 := exec.Command("echo", "hello world")
// 2. 두 번째 명령: grep hello
cmd2 := exec.Command("grep", "hello")
// 3. cmd1의 stdout을 cmd2의 stdin으로 연결
pipe, err := cmd1.StdoutPipe()
if err != nil {
fmt.Println("파이프 생성 실패:", err)
return
}
cmd2.Stdin = pipe
cmd2.Stdout = os.Stdout
// 4. 명령 실행
cmd1.Start()
cmd2.Start()
cmd1.Wait()
cmd2.Wait()
}
// 양방향 파이프
func bidirectionalPipe() {
// 자식 프로세스와 양방향 통신
cmd := exec.Command("cat") // cat은 입력을 그대로 출력
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
cmd.Start()
// 데이터 보내기
go func() {
defer stdin.Close()
fmt.Fprintln(stdin, "첫 번째 줄")
fmt.Fprintln(stdin, "두 번째 줄")
fmt.Fprintln(stdin, "세 번째 줄")
}()
// 데이터 받기
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Printf("받음: %s\n", scanner.Text())
}
cmd.Wait()
}
func main() {
fmt.Println("=== 기본 파이프 ===")
pipeExample()
fmt.Println("\n=== 자식 프로세스 파이프 ===")
childProcessPipe()
fmt.Println("\n=== 양방향 파이프 ===")
bidirectionalPipe()
}3.4 /dev/null의 활용
go
package main
import (
"fmt"
"os"
"os/exec"
)
func discardOutput() {
// 출력을 /dev/null로 버리기
devNull, _ := os.OpenFile("/dev/null", os.O_WRONLY, 0)
defer devNull.Close()
cmd := exec.Command("ls", "-la")
cmd.Stdout = devNull // 출력 버림
cmd.Stderr = devNull // 에러도 버림
cmd.Run()
fmt.Println("명령이 실행되었지만 출력은 버려졌습니다")
}
func nullAsInput() {
// /dev/null을 입력으로 사용 (즉시 EOF)
devNull, _ := os.Open("/dev/null")
defer devNull.Close()
buffer := make([]byte, 100)
n, err := devNull.Read(buffer)
fmt.Printf("읽은 바이트: %d, 에러: %v\n", n, err) // 0, EOF
}
func main() {
discardOutput()
nullAsInput()
}4. 파일 디스크립터 조작
4.1 기본 시스템 콜
open/close/read/write:
go
package main
import (
"fmt"
"syscall"
)
func lowLevelFileOps() {
// open() 시스템 콜
fd, err := syscall.Open("test.txt",
syscall.O_RDWR|syscall.O_CREAT, 0644)
if err != nil {
fmt.Println("Open 실패:", err)
return
}
defer syscall.Close(fd)
fmt.Printf("파일 열림: fd=%d\n", fd)
// write() 시스템 콜
data := []byte("Hello, File Descriptor!\n")
n, err := syscall.Write(fd, data)
if err != nil {
fmt.Println("Write 실패:", err)
return
}
fmt.Printf("작성된 바이트: %d\n", n)
// lseek() 시스템 콜 - 파일 오프셋 이동
offset, err := syscall.Seek(fd, 0, 0) // 파일 시작으로
if err != nil {
fmt.Println("Seek 실패:", err)
return
}
fmt.Printf("현재 오프셋: %d\n", offset)
// read() 시스템 콜
buffer := make([]byte, 100)
n, err = syscall.Read(fd, buffer)
if err != nil {
fmt.Println("Read 실패:", err)
return
}
fmt.Printf("읽은 데이터 (%d 바이트): %s", n, buffer[:n])
}
func main() {
lowLevelFileOps()
}4.2 dup/dup2 - 파일 디스크립터 복제
go
package main
import (
"fmt"
"os"
"syscall"
)
func dupExample() {
file, _ := os.Create("dup_test.txt")
defer file.Close()
originalFd := int(file.Fd())
fmt.Printf("원본 fd: %d\n", originalFd)
// dup() - 사용 가능한 가장 작은 fd로 복제
newFd, err := syscall.Dup(originalFd)
if err != nil {
fmt.Println("Dup 실패:", err)
return
}
defer syscall.Close(newFd)
fmt.Printf("복제된 fd: %d\n", newFd)
// 원본으로 쓰기
syscall.Write(originalFd, []byte("Hello from original\n"))
// 복제본으로 쓰기 (같은 파일 오프셋 공유)
syscall.Write(newFd, []byte("Hello from duplicate\n"))
}
func dup2Example() {
// dup2() - 특정 fd 번호로 복제
file, _ := os.Create("dup2_test.txt")
defer file.Close()
// stdout(1)을 파일로 교체
oldStdout, _ := syscall.Dup(syscall.Stdout)
syscall.Dup2(int(file.Fd()), syscall.Stdout)
// 이제 fmt.Println은 파일로 출력됨
fmt.Println("이것은 파일에 기록됩니다")
// 복원
syscall.Dup2(oldStdout, syscall.Stdout)
syscall.Close(oldStdout)
fmt.Println("stdout이 복원되었습니다")
}
func main() {
fmt.Println("=== dup() 예제 ===")
dupExample()
fmt.Println("\n=== dup2() 예제 ===")
dup2Example()
}4.3 fcntl - 파일 디스크립터 제어
go
package main
import (
"fmt"
"os"
"syscall"
)
func fcntlExample() {
file, _ := os.Create("fcntl_test.txt")
defer file.Close()
fd := int(file.Fd())
// 1. 파일 상태 플래그 가져오기
flags, err := syscall.FcntlInt(uintptr(fd), syscall.F_GETFL, 0)
if err != nil {
fmt.Println("F_GETFL 실패:", err)
return
}
fmt.Printf("현재 플래그: 0x%x\n", flags)
// 읽기/쓰기 모드 확인
accessMode := flags & syscall.O_ACCMODE
switch accessMode {
case syscall.O_RDONLY:
fmt.Println("모드: 읽기 전용")
case syscall.O_WRONLY:
fmt.Println("모드: 쓰기 전용")
case syscall.O_RDWR:
fmt.Println("모드: 읽기/쓰기")
}
// 2. 논블로킹 모드로 설정
_, err = syscall.FcntlInt(uintptr(fd), syscall.F_SETFL,
flags|syscall.O_NONBLOCK)
if err != nil {
fmt.Println("F_SETFL 실패:", err)
return
}
fmt.Println("논블로킹 모드로 설정됨")
// 3. Close-on-exec 플래그 설정
_, err = syscall.FcntlInt(uintptr(fd), syscall.F_SETFD,
syscall.FD_CLOEXEC)
if err != nil {
fmt.Println("F_SETFD 실패:", err)
return
}
fmt.Println("Close-on-exec 플래그 설정됨")
}
// 논블로킹 소켓 설정
func nonblockingSocket() {
// 소켓 생성
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
if err != nil {
fmt.Println("Socket 실패:", err)
return
}
defer syscall.Close(fd)
// 논블로킹 모드로 설정
flags, _ := syscall.FcntlInt(uintptr(fd), syscall.F_GETFL, 0)
syscall.FcntlInt(uintptr(fd), syscall.F_SETFL,
flags|syscall.O_NONBLOCK)
fmt.Println("논블로킹 소켓 생성됨")
}
func main() {
fmt.Println("=== fcntl 예제 ===")
fcntlExample()
fmt.Println("\n=== 논블로킹 소켓 ===")
nonblockingSocket()
}4.4 ioctl - 장치 제어
go
package main
import (
"fmt"
"os"
"syscall"
"unsafe"
)
// 터미널 크기 가져오기
type winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
func getTerminalSize() {
ws := &winsize{}
// TIOCGWINSZ ioctl로 터미널 크기 조회
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL,
uintptr(syscall.Stdout),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(ws)))
if errno != 0 {
fmt.Println("ioctl 실패:", errno)
return
}
fmt.Printf("터미널 크기: %d행 x %d열\n", ws.Row, ws.Col)
}
// 파일이 터미널인지 확인 (isatty)
func isTerminal(fd int) bool {
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL,
uintptr(fd),
syscall.TIOCGWINSZ,
0)
return errno == 0
}
func main() {
getTerminalSize()
fmt.Printf("stdout이 터미널인가? %v\n", isTerminal(syscall.Stdout))
fmt.Printf("stderr이 터미널인가? %v\n", isTerminal(syscall.Stderr))
// 파일은 터미널이 아님
file, _ := os.Create("test.txt")
defer file.Close()
fmt.Printf("파일이 터미널인가? %v\n", isTerminal(int(file.Fd())))
}5. 파일 디스크립터와 프로세스
5.1 fork()와 파일 디스크립터 상속
go
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func forkAndInherit() {
// 부모 프로세스에서 파일 열기
file, _ := os.Create("inherited.txt")
defer file.Close()
file.WriteString("부모가 작성\n")
// 자식 프로세스 생성 (exec)
cmd := exec.Command("sh", "-c",
fmt.Sprintf("echo '자식이 작성' >&%d", file.Fd()))
// 기본적으로 Go는 자식에게 fd를 전달하지 않음
// ExtraFiles를 사용하여 명시적으로 전달
cmd.ExtraFiles = []*os.File{file}
cmd.Run()
// 파일 내용 확인
file.Seek(0, 0)
content := make([]byte, 100)
n, _ := file.Read(content)
fmt.Printf("파일 내용:\n%s", content[:n])
}
// 자식 프로세스가 부모의 fd를 상속하는 예제
func parentChildFd() {
reader, writer, _ := os.Pipe()
// 자식 프로세스 생성
cmd := exec.Command("sh", "-c", "echo 자식 프로세스 메시지")
cmd.Stdout = writer
// 고루틴에서 파이프 읽기
go func() {
defer reader.Close()
buf := make([]byte, 100)
n, _ := reader.Read(buf)
fmt.Printf("부모가 받음: %s", buf[:n])
}()
writer.Close() // 부모는 쓰기 끝을 닫음
cmd.Run()
}
func main() {
fmt.Println("=== fork와 파일 디스크립터 상속 ===")
forkAndInherit()
fmt.Println("\n=== 부모-자식 파이프 통신 ===")
parentChildFd()
}5.2 Close-on-exec 플래그
go
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func closeOnExecExample() {
// 파일 열기
file, _ := os.Create("cloexec_test.txt")
defer file.Close()
fd := int(file.Fd())
// Close-on-exec 플래그 설정
syscall.FcntlInt(uintptr(fd), syscall.F_SETFD, syscall.FD_CLOEXEC)
fmt.Printf("파일 fd: %d (CLOEXEC 설정됨)\n", fd)
// 자식 프로세스 생성
// exec 호출 시 이 fd는 자동으로 닫힘
cmd := exec.Command("sh", "-c",
fmt.Sprintf("ls -l /proc/self/fd/%d 2>&1", fd))
output, _ := cmd.Output()
fmt.Printf("자식 프로세스 출력:\n%s", output)
// "No such file or directory" - fd가 닫혔음을 확인
}
// Go의 os.OpenFile은 기본적으로 O_CLOEXEC 사용
func goCloexecBehavior() {
file, _ := os.Create("test.txt")
defer file.Close()
// Go는 자동으로 CLOEXEC 설정
fd := int(file.Fd())
flags, _ := syscall.FcntlInt(uintptr(fd), syscall.F_GETFD, 0)
if flags&syscall.FD_CLOEXEC != 0 {
fmt.Println("Go는 기본적으로 CLOEXEC를 설정합니다")
}
}
func main() {
fmt.Println("=== Close-on-exec 데모 ===")
closeOnExecExample()
fmt.Println("\n=== Go의 CLOEXEC 동작 ===")
goCloexecBehavior()
}5.3 프로세스의 /proc/self/fd
go
package main
import (
"fmt"
"io/ioutil"
"net"
"os"
)
func inspectFileDescriptors() {
// 여러 종류의 fd 생성
file, _ := os.Create("test.txt")
defer file.Close()
listener, _ := net.Listen("tcp", ":0")
defer listener.Close()
reader, writer, _ := os.Pipe()
defer reader.Close()
defer writer.Close()
// /proc/self/fd 디렉터리 읽기
files, err := ioutil.ReadDir("/proc/self/fd")
if err != nil {
fmt.Println("이 시스템은 /proc을 지원하지 않습니다")
return
}
fmt.Println("현재 프로세스의 열린 파일 디스크립터:")
for _, f := range files {
fdNum := f.Name()
link, _ := os.Readlink("/proc/self/fd/" + fdNum)
fmt.Printf("fd %s -> %s\n", fdNum, link)
}
}
func main() {
inspectFileDescriptors()
}출력 예시:
현재 프로세스의 열린 파일 디스크립터:
fd 0 -> /dev/pts/0
fd 1 -> /dev/pts/0
fd 2 -> /dev/pts/0
fd 3 -> /path/to/test.txt
fd 4 -> socket:[12345]
fd 5 -> pipe:[67890]
fd 6 -> pipe:[67890]6. 파일 디스크립터 고급 기법
6.1 Scatter-Gather I/O (readv/writev)
여러 버퍼를 한 번의 시스템 콜로 읽기/쓰기:
go
package main
import (
"fmt"
"os"
"syscall"
)
func writevExample() {
file, _ := os.Create("writev_test.txt")
defer file.Close()
fd := int(file.Fd())
// 여러 버퍼 준비
buffers := [][]byte{
[]byte("첫 번째 버퍼\n"),
[]byte("두 번째 버퍼\n"),
[]byte("세 번째 버퍼\n"),
}
// writev: 여러 버퍼를 한 번에 쓰기
// (내부적으로 하나의 시스템 콜)
iovecs := make([]syscall.Iovec, len(buffers))
for i, buf := range buffers {
iovecs[i] = syscall.Iovec{
Base: &buf[0],
Len: uint64(len(buf)),
}
}
n, _, errno := syscall.Syscall(syscall.SYS_WRITEV,
uintptr(fd),
uintptr(uintptr(unsafe.Pointer(&iovecs[0]))),
uintptr(len(iovecs)))
if errno != 0 {
fmt.Println("writev 실패:", errno)
return
}
fmt.Printf("writev로 작성된 총 바이트: %d\n", n)
}
func readvExample() {
file, _ := os.Open("writev_test.txt")
defer file.Close()
fd := int(file.Fd())
// 여러 버퍼 준비
buf1 := make([]byte, 20)
buf2 := make([]byte, 20)
buf3 := make([]byte, 20)
iovecs := []syscall.Iovec{
{Base: &buf1[0], Len: uint64(len(buf1))},
{Base: &buf2[0], Len: uint64(len(buf2))},
{Base: &buf3[0], Len: uint64(len(buf3))},
}
n, _, errno := syscall.Syscall(syscall.SYS_READV,
uintptr(fd),
uintptr(uintptr(unsafe.Pointer(&iovecs[0]))),
uintptr(len(iovecs)))
if errno != 0 {
fmt.Println("readv 실패:", errno)
return
}
fmt.Printf("readv로 읽은 총 바이트: %d\n", n)
fmt.Printf("버퍼 1: %s", buf1)
fmt.Printf("버퍼 2: %s", buf2)
fmt.Printf("버퍼 3: %s", buf3)
}
func main() {
fmt.Println("=== writev 예제 ===")
writevExample()
fmt.Println("\n=== readv 예제 ===")
readvExample()
}6.2 sendfile - 제로 카피 전송
커널 내부에서 직접 파일 → 소켓 전송 (유저스페이스 복사 없음):
go
package main
import (
"fmt"
"io"
"net"
"os"
"syscall"
)
// sendfile을 사용한 제로 카피 파일 전송
func sendfileTransfer(socket net.Conn, filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
stat, _ := file.Stat()
fileSize := stat.Size()
// TCP 소켓의 파일 디스크립터
tcpConn := socket.(*net.TCPConn)
socketFile, _ := tcpConn.File()
socketFd := int(socketFile.Fd())
fileFd := int(file.Fd())
// sendfile 시스템 콜
// 커널 내부에서 file -> socket으로 직접 전송
offset := int64(0)
written, err := syscall.Sendfile(socketFd, fileFd, &offset, int(fileSize))
if err != nil {
return err
}
fmt.Printf("sendfile로 전송: %d 바이트\n", written)
return nil
}
// 일반 방식 (유저스페이스 버퍼 사용)
func normalTransfer(socket net.Conn, filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// 유저스페이스 버퍼를 통한 전송
// file -> 유저 버퍼 -> socket (두 번의 복사)
written, err := io.Copy(socket, file)
if err != nil {
return err
}
fmt.Printf("일반 방식 전송: %d 바이트\n", written)
return nil
}
func compareTransferMethods() {
// 큰 테스트 파일 생성
file, _ := os.Create("large_file.bin")
file.Write(make([]byte, 10*1024*1024)) // 10MB
file.Close()
// 서버 시작
listener, _ := net.Listen("tcp", ":0")
addr := listener.Addr().String()
// 서버 측 (수신)
go func() {
conn, _ := listener.Accept()
defer conn.Close()
io.Copy(io.Discard, conn) // 데이터 버림
}()
// 클라이언트 측 (송신) - sendfile 사용
conn1, _ := net.Dial("tcp", addr)
sendfileTransfer(conn1, "large_file.bin")
conn1.Close()
listener.Close()
}
func main() {
compareTransferMethods()
}성능 차이:
- 일반 방식: file → 커널 버퍼 → 유저 버퍼 → 커널 버퍼 → 소켓 (4번 복사)
- sendfile: file → 소켓 (커널 내부에서 1번 복사)
6.3 splice - 파이프를 통한 제로 카피
go
package main
import (
"fmt"
"os"
"syscall"
)
// splice를 사용한 파일 간 복사 (파이프 경유)
func spliceFileCopy(src, dst string) error {
// 소스 파일 열기
srcFile, _ := os.Open(src)
defer srcFile.Close()
// 대상 파일 생성
dstFile, _ := os.Create(dst)
defer dstFile.Close()
// 파이프 생성
pipeR, pipeW, _ := os.Pipe()
defer pipeR.Close()
defer pipeW.Close()
srcFd := int(srcFile.Fd())
dstFd := int(dstFile.Fd())
pipeRFd := int(pipeR.Fd())
pipeWFd := int(pipeW.Fd())
// 파일 크기 확인
stat, _ := srcFile.Stat()
remaining := stat.Size()
// splice 루프
for remaining > 0 {
// 소스 -> 파이프
n, err := syscall.Splice(srcFd, nil, pipeWFd, nil,
int(remaining), syscall.SPLICE_F_MOVE)
if err != nil {
return err
}
// 파이프 -> 대상
_, err = syscall.Splice(pipeRFd, nil, dstFd, nil,
n, syscall.SPLICE_F_MOVE)
if err != nil {
return err
}
remaining -= int64(n)
}
fmt.Printf("splice로 복사 완료: %d 바이트\n", stat.Size())
return nil
}
func main() {
// 테스트 파일 생성
src := "source.txt"
dst := "destination.txt"
os.WriteFile(src, []byte("Hello, splice!"), 0644)
spliceFileCopy(src, dst)
// 결과 확인
content, _ := os.ReadFile(dst)
fmt.Printf("복사된 내용: %s\n", content)
}6.4 memfd - 익명 파일 디스크립터
go
package main
import (
"fmt"
"syscall"
"unsafe"
)
// memfd_create: 메모리 기반 익명 파일 생성
func memfdExample() {
// memfd_create 시스템 콜
name := []byte("my_memfd\x00")
fd, _, errno := syscall.Syscall(319, // memfd_create 시스템 콜 번호
uintptr(unsafe.Pointer(&name[0])),
uintptr(0), // flags
0)
if errno != 0 {
fmt.Println("memfd_create 실패:", errno)
return
}
defer syscall.Close(int(fd))
fmt.Printf("memfd 생성: fd=%d\n", fd)
// 메모리에 데이터 쓰기
data := []byte("메모리 기반 파일 데이터")
syscall.Write(int(fd), data)
// 읽기
syscall.Seek(int(fd), 0, 0) // 처음으로
buffer := make([]byte, 100)
n, _ := syscall.Read(int(fd), buffer)
fmt.Printf("읽은 데이터: %s\n", buffer[:n])
// 특징: 파일 시스템에 실제 파일이 없음!
// /proc/self/fd/X로만 접근 가능
}
func main() {
memfdExample()
}7. 파일 디스크립터 제한과 관리
7.1 파일 디스크립터 제한 확인
go
package main
import (
"fmt"
"syscall"
)
func checkFdLimits() {
var rLimit syscall.Rlimit
// 소프트/하드 제한 조회
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
fmt.Println("Getrlimit 실패:", err)
return
}
fmt.Printf("파일 디스크립터 제한:\n")
fmt.Printf(" 소프트 제한: %d\n", rLimit.Cur)
fmt.Printf(" 하드 제한: %d\n", rLimit.Max)
}
func increaseFdLimit() {
var rLimit syscall.Rlimit
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
fmt.Printf("현재 소프트 제한: %d\n", rLimit.Cur)
// 소프트 제한을 하드 제한까지 증가
rLimit.Cur = rLimit.Max
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
fmt.Println("Setrlimit 실패:", err)
fmt.Println("(root 권한이 필요할 수 있습니다)")
return
}
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
fmt.Printf("증가된 소프트 제한: %d\n", rLimit.Cur)
}
func main() {
fmt.Println("=== 파일 디스크립터 제한 조회 ===")
checkFdLimits()
fmt.Println("\n=== 제한 증가 시도 ===")
increaseFdLimit()
}7.2 파일 디스크립터 누수 감지
go
package main
import (
"fmt"
"io/ioutil"
"os"
"runtime"
"time"
)
// 파일 디스크립터 누수 예제 (나쁜 코드!)
func leakyFunction() {
for i := 0; i < 100; i++ {
file, _ := os.Create(fmt.Sprintf("leak_%d.txt", i))
file.WriteString("data")
// file.Close()가 없음 - 누수 발생!
}
}
// 현재 프로세스의 열린 fd 개수 확인
func countOpenFds() int {
if runtime.GOOS != "linux" {
return -1
}
files, err := ioutil.ReadDir("/proc/self/fd")
if err != nil {
return -1
}
return len(files)
}
func detectLeak() {
fmt.Printf("시작 시 열린 fd: %d\n", countOpenFds())
leakyFunction()
// GC 실행해도 fd는 닫히지 않음!
runtime.GC()
time.Sleep(100 * time.Millisecond)
fmt.Printf("누수 후 열린 fd: %d\n", countOpenFds())
// 올바른 정리
for i := 0; i < 100; i++ {
os.Remove(fmt.Sprintf("leak_%d.txt", i))
}
}
// 올바른 방법: defer 사용
func properFileHandling() {
before := countOpenFds()
for i := 0; i < 100; i++ {
func() {
file, _ := os.Create(fmt.Sprintf("proper_%d.txt", i))
defer file.Close() // 함수 종료 시 자동으로 닫힘
file.WriteString("data")
}()
}
runtime.GC()
time.Sleep(100 * time.Millisecond)
after := countOpenFds()
fmt.Printf("처리 전 fd: %d, 처리 후 fd: %d\n", before, after)
// 정리
for i := 0; i < 100; i++ {
os.Remove(fmt.Sprintf("proper_%d.txt", i))
}
}
func main() {
fmt.Println("=== 파일 디스크립터 누수 감지 ===")
detectLeak()
fmt.Println("\n=== 올바른 파일 처리 ===")
properFileHandling()
}7.3 파일 디스크립터 풀링
go
package main
import (
"fmt"
"os"
"sync"
)
// 파일 디스크립터 풀
type FdPool struct {
pool chan *os.File
factory func() (*os.File, error)
mu sync.Mutex
created int
maxSize int
}
func NewFdPool(factory func() (*os.File, error), maxSize int) *FdPool {
return &FdPool{
pool: make(chan *os.File, maxSize),
factory: factory,
maxSize: maxSize,
}
}
func (p *FdPool) Get() (*os.File, error) {
select {
case file := <-p.pool:
return file, nil
default:
p.mu.Lock()
defer p.mu.Unlock()
if p.created < p.maxSize {
file, err := p.factory()
if err != nil {
return nil, err
}
p.created++
return file, nil
}
// 풀이 가득 참, 대기
return <-p.pool, nil
}
}
func (p *FdPool) Put(file *os.File) {
select {
case p.pool <- file:
default:
// 풀이 가득 참, fd 닫기
file.Close()
p.mu.Lock()
p.created--
p.mu.Unlock()
}
}
func (p *FdPool) Close() {
close(p.pool)
for file := range p.pool {
file.Close()
}
}
func main() {
// 최대 10개의 파일 핸들만 유지
pool := NewFdPool(func() (*os.File, error) {
return os.OpenFile("/dev/null", os.O_RDWR, 0)
}, 10)
defer pool.Close()
// 100개의 작업을 10개의 fd로 처리
for i := 0; i < 100; i++ {
file, _ := pool.Get()
// 작업 수행
file.Write([]byte("data"))
pool.Put(file)
}
fmt.Println("100개 작업을 제한된 fd로 완료")
}8. 실전 문제 해결
8.1 "Too many open files" 에러 해결
go
package main
import (
"fmt"
"net"
"os"
"syscall"
)
func reproduceTooManyFiles() {
// fd 제한을 낮게 설정 (테스트용)
var rLimit syscall.Rlimit
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
originalLimit := rLimit.Cur
rLimit.Cur = 50 // 매우 낮게 설정
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
fmt.Println("fd 제한을 50으로 설정")
// 많은 파일 열기 시도
files := []*os.File{}
for i := 0; ; i++ {
file, err := os.Create(fmt.Sprintf("test_%d.txt", i))
if err != nil {
fmt.Printf("에러 발생 (파일 %d개 생성 후): %v\n", i, err)
break
}
files = append(files, file)
}
// 정리
for _, file := range files {
file.Close()
os.Remove(file.Name())
}
// 제한 복원
rLimit.Cur = originalLimit
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
}
// 해결 방법 1: 제한 증가
func solution1() {
var rLimit syscall.Rlimit
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
fmt.Printf("현재 제한: %d\n", rLimit.Cur)
rLimit.Cur = rLimit.Max
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
fmt.Printf("증가된 제한: %d\n", rLimit.Cur)
}
// 해결 방법 2: 리소스 정리
func solution2() {
files := []*os.File{}
for i := 0; i < 100; i++ {
file, _ := os.Create(fmt.Sprintf("test_%d.txt", i))
files = append(files, file)
// 주기적으로 정리
if i%10 == 9 {
for _, f := range files {
f.Close()
}
files = []*os.File{}
}
}
}
// 해결 방법 3: 연결 풀 사용
type ConnectionPool struct {
conns chan net.Conn
}
func NewConnectionPool(size int, factory func() (net.Conn, error)) *ConnectionPool {
pool := &ConnectionPool{
conns: make(chan net.Conn, size),
}
for i := 0; i < size; i++ {
conn, _ := factory()
pool.conns <- conn
}
return pool
}
func (p *ConnectionPool) Get() net.Conn {
return <-p.conns
}
func (p *ConnectionPool) Put(conn net.Conn) {
p.conns <- conn
}
func main() {
fmt.Println("=== 'Too many open files' 재현 ===")
reproduceTooManyFiles()
fmt.Println("\n=== 해결 방법 1: 제한 증가 ===")
solution1()
}8.2 파일 디스크립터 공유 문제
go
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
// 문제: 자식 프로세스가 부모의 fd를 상속받아 문제 발생
func problematicFdSharing() {
// 서버 소켓 생성
listener, _ := net.Listen("tcp", ":8080")
defer listener.Close()
// 자식 프로세스 생성
cmd := exec.Command("sleep", "10")
cmd.Start()
// 문제: 서버를 재시작하려고 하면
// "address already in use" 에러 발생
// 왜? 자식이 소켓 fd를 상속받아 아직 열려있음!
fmt.Println("자식 프로세스가 소켓을 상속받았습니다")
}
// 해결: CLOEXEC 플래그 사용
func solutionCloexec() {
// Go는 기본적으로 CLOEXEC 설정
file, _ := os.Create("test.txt")
defer file.Close()
// 수동으로 설정하려면:
fd := int(file.Fd())
syscall.FcntlInt(uintptr(fd), syscall.F_SETFD, syscall.FD_CLOEXEC)
// exec 시 자동으로 닫힘
cmd := exec.Command("ls", "-l", "/proc/self/fd")
output, _ := cmd.Output()
fmt.Printf("%s", output)
}
func main() {
solutionCloexec()
}8.3 파일 디스크립터 번호 재사용 문제
go
package main
import (
"fmt"
"os"
"time"
)
// 문제: fd 번호가 재사용되어 혼동 발생
func fdReuseIssue() {
file1, _ := os.Create("file1.txt")
fd1 := file1.Fd()
fmt.Printf("file1 fd: %d\n", fd1)
file1.Close()
// fd가 재사용됨
file2, _ := os.Create("file2.txt")
fd2 := file2.Fd()
fmt.Printf("file2 fd: %d\n", fd2)
if fd1 == fd2 {
fmt.Println("같은 fd 번호가 재사용되었습니다!")
}
file2.Close()
}
// 해결: fd를 식별자로 사용하지 않기
type FileHandle struct {
file *os.File
id uint64 // 고유 ID
}
var nextID uint64 = 0
func NewFileHandle(filename string) (*FileHandle, error) {
file, err := os.Create(filename)
if err != nil {
return nil, err
}
nextID++
return &FileHandle{
file: file,
id: nextID,
}, nil
}
func (fh *FileHandle) ID() uint64 {
return fh.id
}
func (fh *FileHandle) Close() error {
return fh.file.Close()
}
func solutionUniqueID() {
fh1, _ := NewFileHandle("file1.txt")
id1 := fh1.ID()
fh1.Close()
fh2, _ := NewFileHandle("file2.txt")
id2 := fh2.ID()
fh2.Close()
fmt.Printf("file1 ID: %d, file2 ID: %d\n", id1, id2)
fmt.Println("고유 ID를 사용하여 혼동 방지")
}
func main() {
fmt.Println("=== fd 재사용 문제 ===")
fdReuseIssue()
fmt.Println("\n=== 해결: 고유 ID 사용 ===")
solutionUniqueID()
}8.4 파일 디스크립터와 고루틴 안전성
go
package main
import (
"fmt"
"os"
"sync"
)
// 문제: 여러 고루틴이 같은 fd에 접근
func unsafeFileAccess() {
file, _ := os.Create("unsafe.txt")
defer file.Close()
var wg sync.WaitGroup
// 여러 고루틴이 동시에 쓰기
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// 경쟁 조건: 파일 오프셋 공유
fmt.Fprintf(file, "고루틴 %d\n", n)
}(i)
}
wg.Wait()
// 결과: 데이터가 뒤섞임 가능성
}
// 해결: 뮤텍스 사용
type SafeFile struct {
file *os.File
mu sync.Mutex
}
func NewSafeFile(filename string) (*SafeFile, error) {
file, err := os.Create(filename)
if err != nil {
return nil, err
}
return &SafeFile{file: file}, nil
}
func (sf *SafeFile) Write(data []byte) (int, error) {
sf.mu.Lock()
defer sf.mu.Unlock()
return sf.file.Write(data)
}
func (sf *SafeFile) Close() error {
return sf.file.Close()
}
func safeFileAccess() {
file, _ := NewSafeFile("safe.txt")
defer file.Close()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
data := fmt.Sprintf("고루틴 %d\n", n)
file.Write([]byte(data))
}(i)
}
wg.Wait()
fmt.Println("안전한 파일 쓰기 완료")
}
// 더 나은 방법: 각 고루틴이 자신만의 fd 사용
func bestPractice() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// 각 고루틴이 독립적인 fd 사용
file, _ := os.OpenFile("best.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer file.Close()
fmt.Fprintf(file, "고루틴 %d\n", n)
}(i)
}
wg.Wait()
fmt.Println("각 고루틴이 독립적인 fd 사용")
}
func main() {
fmt.Println("=== 안전하지 않은 접근 ===")
unsafeFileAccess()
fmt.Println("\n=== 뮤텍스 사용 ===")
safeFileAccess()
fmt.Println("\n=== Best Practice ===")
bestPractice()
}결론
핵심 요약
파일 디스크립터는 I/O의 핵심
- 모든 I/O는 fd를 통해 수행
- "모든 것은 파일이다" 철학의 구현
세 계층 구조
- 파일 디스크립터 테이블 (프로세스별)
- 파일 테이블 (시스템 전역)
- inode 테이블 (파일 시스템)
fd 조작의 중요성
- dup/dup2: 리다이렉션과 파이프 구현
- fcntl: 논블로킹, CLOEXEC 등 제어
- ioctl: 장치 특화 제어
성능 최적화
- readv/writev: Scatter-Gather I/O
- sendfile/splice: 제로 카피
- memfd: 메모리 기반 파일
실전 주의사항
- fd 누수 방지 (defer 사용)
- 제한 관리 (ulimit, rlimit)
- 고루틴 안전성 고려
- CLOEXEC로 의도치 않은 상속 방지
Best Practices
DO:
- 항상
defer file.Close()사용 - fd 제한 모니터링
- 에러 처리 철저히
- 고루틴 간 fd 공유 시 동기화
- 프로덕션 환경에서 CLOEXEC 활용
DON'T:
- fd를 영구적인 식별자로 사용 (재사용됨)
- 무한정 fd 생성
- Close 누락
- 자식 프로세스에 불필요한 fd 노출
추가 학습 자료
- man 페이지:
man 2 open,man 2 dup,man 2 fcntl - 리눅스 커널 소스:
fs/file.c,fs/open.c - 책: "The Linux Programming Interface" by Michael Kerrisk
- 책: "Advanced Programming in the UNIX Environment" by Stevens
강의 종료
파일 디스크립터는 유닉스/리눅스 시스템 프로그래밍의 기초입니다. 이 개념을 완전히 이해하면 I/O, 네트워킹, 프로세스 관리 등 모든 시스템 프로그래밍 영역에서 도움이 됩니다!