[CS] Everything is a File 철학

· 9 분 소요


리눅스에 대해 공부하다 보면 여기저기서 “Everything is a file”이라는 말을 듣게 된다. 장치 드라이버, 소켓, 프로세스 등 시스템 프로그래밍이나 인프라 관련 내용을 다룰 때마다 등장하는 이 철학은 Unix/Linux를 이해하는 핵심 개념이다. 이 글에서는 “Everything is a file” 철학의 의미와 장점, 그리고 현실적인 한계를 살펴본다.


TL;DR

  • Everything is a file: Unix/Linux에서 모든 리소스를 파일처럼 다룬다
  • 파일 종류 7가지: 일반 파일, 디렉토리, 심볼릭 링크, 문자/블록 장치, 소켓, 파이프
  • 주요 경로: /dev (장치), /proc (프로세스/시스템), /sys (커널/드라이버)
  • 장점: 단순성, 일관성, 유연성, 투명성
  • 한계: 기본 파일 연산(read/write)만으로는 복잡한 작업 표현 어려움
  • 실용적 해결: ioctl(), socket(), mmap() 등 전용 시스템 콜로 보완


Everything is a file

기본 개념

Unix/Linux에서는 “Everything is a file” 철학을 따른다. 모든 리소스를 파일처럼 다룬다는 의미다.

일반적인 파일뿐만 아니라 다음도 모두 파일처럼 접근한다:

  • 하드웨어 장치: GPU, 디스크, 키보드, 마우스 등
  • 프로세스: 실행 중인 프로그램의 정보
  • 네트워크: 소켓을 통한 통신
  • 시스템 정보: CPU, 메모리, 네트워크 상태 등


“파일”의 여러 의미

“Everything is a file”은 설계 철학이지, 모든 것이 완벽하게 파일로만 표현된다는 의미는 아니다.

“파일”이라는 용어는 맥락에 따라 다른 레이어를 가리킨다:

  1. 파일 시스템 경로 (namespace) → “Everything is a file”의 철학적 범위
    • 모든 리소스가 파일 시스템 경로를 가짐
    • /dev/null, /proc/cpuinfo, /sys/class/net/...
  2. 파일 디스크립터 (fd abstraction) → Linux의 실제 구현 범위
    • 모든 리소스가 파일 디스크립터(fd)로 조작 가능
    • 소켓, epoll, signalfd, timerfd 등 경로 없이 fd만으로 존재하는 것도 포함
  3. 바이트 스트림 인터페이스 (data model) → 기본 파일 연산의 범위
    • open(), read(), write(), close() 등의 단순한 인터페이스
    • 순차적 데이터 스트림 읽기/쓰기

“Everything is a file”은 본래 1번(경로)을 지향하는 철학이고, Linux는 2번(fd)으로 이를 더 확장했다. 하지만 3번(바이트 스트림)만으로는 모든 리소스를 표현하기 어렵기 때문에, 뒤에서 살펴볼 전용 시스템 콜로 보완한다.


동일한 방식으로 접근

하드웨어 장치, 프로세스 정보, 네트워크 소켓, 파이프 등 거의 모든 시스템 리소스를 파일로 추상화하여, 모두 같은 방식 방식(cat, echo)으로 다룬다.

# 1. 일반 파일 읽기
cat /tmp/file.txt

# 2. 장치 파일 읽기 (동일한 방식!)
cat /dev/random  # 랜덤 데이터 생성 장치

# 3. 프로세스 정보 읽기 (동일한 방식!)
cat /proc/cpuinfo  # CPU 정보

# 4. 시스템 설정 읽기/쓰기 (동일한 방식!)
cat /sys/class/net/eth0/address  # 네트워크 MAC 주소
echo 1 > /proc/sys/net/ipv4/ip_forward  # IP 포워딩 활성화


파일 종류

Linux에는 7가지 파일 종류가 있다. ls -l 출력의 첫 번째 문자로 파일 타입을 구분할 수 있다.

기호 종류 설명 예시
- 일반 파일 텍스트, 바이너리, 이미지 등 -rw-r--r-- file.txt
d 디렉토리 폴더 drwxr-xr-x /home/
l 심볼릭 링크 다른 파일을 가리키는 링크 lrwxrwxrwx rtc -> rtc0
c 문자 장치 한 바이트씩 스트림 방식 전송 crw-rw-rw- /dev/null
b 블록 장치 블록 단위 랜덤 액세스 brw-rw---- /dev/sda
s 소켓 프로세스 간 네트워크 통신 srwxrwxrwx /run/systemd/notify
p 파이프 프로세스 간 데이터 전달 (FIFO) Named pipe


파일 타입 확인

ls -l 명령으로 파일 타입을 확인할 수 있다.

ls -l /dev/ | head -20

# 출력 예시
brw-rw---- 1 root disk      8,   0 Feb  3 10:00 sda        # 블록 장치
brw-rw---- 1 root disk      8,   1 Feb  3 10:00 sda1       # 블록 장치 (파티션)
crw-rw-rw- 1 root tty       1,   3 Feb  3 10:00 null       # 문자 장치
crw-rw-rw- 1 root tty       1,   9 Feb  3 10:00 urandom    # 문자 장치
lrwxrwxrwx 1 root root          4 Feb  3 10:00 rtc -> rtc0 # 심볼릭 링크
srwxrwxrwx 1 root root          0 Feb  3 10:00 log        # 소켓


철학의 장점

이러한 철학의 장점은 다음과 같다.

  • 단순성: 파일을 다루는 방법(open, read, write, close)만 알면 모든 것을 다룰 수 있음
  • 일관성: 일반 파일, 하드웨어 장치, 프로세스, 네트워크 등을 동일한 인터페이스로 접근
  • 유연성: 파이프(|)와 리다이렉션(>, <)으로 쉽게 조합 가능
  • 투명성: /proc, /sys를 통해 시스템 내부를 쉽게 관찰하고 디버깅 가능


파일 시스템 계층

리눅스 파일 시스템은 FHS(Filesystem Hierarchy Standard)를 따른다. 이 표준 덕분에 배포판(Ubuntu, Rocky, Arch 등)이 달라도 주요 파일들의 위치가 대체로 동일하다.

/
├── bin/      # 필수 명령어
├── dev/      # 장치 파일
├── etc/      # 설정 파일
├── lib/      # 필수 라이브러리, 커널 모듈
├── proc/     # 프로세스/시스템 정보 (가상)
├── sys/      # 커널/장치 설정 (가상)
├── usr/      # 사용자 프로그램, 라이브러리
└── var/      # 가변 데이터 (로그 등)


이 중 “Everything is a file” 철학과 관련된 주요 경로는 다음과 같다.

경로 설명
/dev/ 하드웨어 장치에 접근하는 인터페이스.
실제 장치 파일이 위치
/proc/ 커널이 런타임에 생성하는 가상 파일 시스템.
프로세스 정보(/proc/[pid]/)와 시스템 정보(/proc/cpuinfo, /proc/meminfo 등) 제공
/sys/ 커널 2.6부터 도입된 가상 파일 시스템.
장치와 드라이버 정보를 계층적으로 제공하며, 일부 설정은 쓰기도 가능

참고: 가상 파일 시스템

/proc/sys는 디스크에 실제로 저장되지 않는다. 커널이 런타임에 메모리에서 동적으로 생성하며, 파일을 읽을 때마다 커널이 현재 상태를 조회해서 내용을 반환한다. ls -l로 보면 크기가 0으로 표시되지만, cat으로 읽으면 내용이 있다.

# /proc - 프로세스/시스템 정보
ls -l /proc
dr-xr-xr-x.   9 root  root  0  1        # 프로세스 디렉토리 (PID 1)
dr-xr-xr-x.   9 root  root  0  1126     # 프로세스 디렉토리
-r--r--r--.   1 root  root  0  cpuinfo  # CPU 정보
-r--r--r--.   1 root  root  0  meminfo  # 메모리 정보
-r--r--r--.   1 root  root  0  modules  # 로드된 모듈

# /sys - 커널/장치 설정
ls -l /sys
drwxr-xr-x.   2 root  root  0  block    # 블록 장치
drwxr-xr-x.  41 root  root  0  bus      # 버스 (PCI, USB 등)
drwxr-xr-x.  57 root  root  0  class    # 장치 클래스
drwxr-xr-x.  14 root  root  0  devices  # 장치 트리
drwxr-xr-x. 158 root  root  0  module   # 커널 모듈 파라미터

구체적인 예시

위 장점들이 실제로 어떻게 활용되는지 살펴보자.


1. 프로세스 정보 (/proc)

/proc은 이 철학을 가장 잘 보여주는 예시다. 실제 디스크에 저장되지 않고 커널 메모리의 정보를 파일 형태로 노출한다.

프로세스별 정보

각 프로세스는 /proc/[PID]/ 디렉토리로 표현된다.

# PID 1234 프로세스의 정보
ls /proc/1234/
# cmdline   - 실행 명령어
# status    - 프로세스 상태 (메모리, CPU 등)
# fd/       - 열린 파일 디스크립터들
# maps      - 메모리 맵
# environ   - 환경변수
# cwd       - 현재 작업 디렉토리 (심볼릭 링크)
# exe       - 실행 파일 경로 (심볼릭 링크)

# 실행 명령어 확인
cat /proc/1234/cmdline

# 프로세스가 열고 있는 파일들
ls -l /proc/1234/fd/

# 환경변수 확인 (null로 구분됨)
cat /proc/1234/environ | tr '\0' '\n'


시스템 전체 정보

/proc의 루트에는 시스템 전체 정보가 파일로 제공된다.

cat /proc/cpuinfo     # CPU 정보
cat /proc/meminfo     # 메모리 정보
cat /proc/uptime      # 가동 시간
cat /proc/loadavg     # 시스템 부하
cat /proc/net/dev     # 네트워크 인터페이스 통계
cat /proc/modules     # 로드된 커널 모듈


2. 시스템 설정 (/sys)

커널과 하드웨어 설정도 파일로 접근한다.

# CPU 주파수 확인
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq

# 디스크 스케줄러 변경
echo noop > /sys/block/sda/queue/scheduler

# PCI 장치 목록
ls /sys/bus/pci/devices/


3. 장치 파일 (/dev)

하드웨어 장치에 직접 접근한다.

# 랜덤 데이터 생성
dd if=/dev/urandom of=random.bin bs=1M count=10

# 디스크 직접 읽기
dd if=/dev/sda of=backup.img bs=4M

# 시리얼 포트 통신
echo "AT" > /dev/ttyS0  # 모뎀 명령
cat /dev/ttyS0          # 응답 읽기


한계와 확장

앞에서 살펴본 것처럼, “Everything is a File” 철학은 파일 시스템 경로(1번)로 모든 리소스를 표현하는 것을 지향하고, Linux는 이를 파일 디스크립터(2번)로 더 확장했다. 하지만 실제 구현에서는 아래와 같은 한계가 있다.

바이트 스트림 인터페이스로 부족한 경우

바이트 스트림 인터페이스(3번)만으로는 모든 리소스를 파일로 추상화하기에 부족함이 있다.

파일은 본질적으로 순차적 데이터 스트림이다. 다음과 같은 작업은 read()/write()로 표현하기 어렵다.

1. 복잡한 장치 제어

// GPU 클럭 주파수 설정
// read/write로는 불가능, ioctl() 필요
ioctl(fd, GPU_SET_CLOCK, &clock_freq);

// 터미널 설정 변경
struct termios settings;
ioctl(fd, TCGETS, &settings);  // 현재 설정 읽기
settings.c_cflag = B115200;     // baud rate 변경
ioctl(fd, TCSETS, &settings);   // 설정 적용


2. 구조화된 데이터 교환

네트워크 라우팅 테이블, 복잡한 드라이버 설정 등은 단순 텍스트로 표현하기 어렵다.

// 네트워크 인터페이스 설정
struct ifreq ifr;
ioctl(sockfd, SIOCGIFADDR, &ifr);  // IP 주소 읽기


3. 양방향 통신 패턴

소켓은 파일 디스크립터를 사용하지만, 연결 설정은 별도 시스템 콜이 필요하다.

int sock = socket(AF_INET, SOCK_STREAM, 0);  // 소켓 생성
bind(sock, &addr, sizeof(addr));              // 주소 바인딩
listen(sock, 5);                              // 리스닝 시작
int client = accept(sock, NULL, NULL);        // 연결 수락

// 이후 read/write 사용 가능
write(client, "Hello", 5);


성능 최적화

기본 파일 연산으로 표현은 가능하지만, 성능이 중요한 경우 전용 시스템 콜이 더 효율적이다.

대용량 데이터는 read()/write() 대신 메모리 매핑(mmap())이 훨씬 빠르다.

// read()로 가능하지만 느림
char buffer[1024];
while (read(fd, buffer, sizeof(buffer)) > 0) {
    process_data(buffer);
}

// mmap()으로 직접 메모리 접근 (훨씬 빠름)
void *data = mmap(NULL, file_size, PROT_READ, 
                  MAP_PRIVATE, fd, 0);
process_data(data);  // read() 없이 접근


설계 트레이드오프

Unix/Linux는 다음과 같은 균형을 택했다.

  • 원칙: 가능하면 파일 인터페이스 사용 (단순성)
  • 현실: 부족하면 전용 시스템 콜 추가 (실용성)


참고: ioctl()의 필요성

ioctl()은 복잡한 장치 제어를 위한 실용적 해결책이지만, Unix 철학과는 거리가 있다.

  • 명령어가 표준화되지 않음 (장치마다 제각각)
  • read()/write()만큼 직관적이지 않음
  • 보안 검증이 어려움 (임의의 명령 전달)

이것이 Linux가 /proc, /sys를 통해 가능한 것들은 파일 인터페이스로 노출시키려는 이유다. 예를 들어 CPU 주파수는 ioctl() 대신 /sys/devices/system/cpu/*/cpufreq/*로 제어할 수 있다.


다른 OS의 접근 방식

Windows

Windows는 이 철학을 따르지 않는다.

  • 파일: CreateFile(), ReadFile(), WriteFile()
  • 네트워크: socket(), send(), recv() (별도)
  • 프로세스: CreateProcess(), TerminateProcess() (별도)
  • 레지스트리: RegOpenKey(), RegQueryValue() (별도)

각 리소스마다 전용 API를 사용한다.


macOS

macOS는 Unix 계열이므로 “Everything is a file” 철학을 따른다. 다만 일부 확장이 있다.

# macOS도 /dev, /proc 사용
ls /dev/disk*


정리

“Everything is a file”은 Unix/Linux의 핵심 설계 철학이다.

  • 파일 추상화: 모든 리소스를 파일 디스크립터로 접근
  • 동일한 인터페이스: open(), read(), write(), close()로 일관성 유지
  • 실용적 확장: 부족한 부분은 ioctl(), socket() 등으로 보완


이 철학은 목표이지 완벽한 현실은 아니다. 하지만 가능한 한 그렇게 만들려는 노력이 리눅스의 /proc, /sys 같은 가상 파일 시스템으로 나타난다.


시스템 프로그래밍, 장치 드라이버, 네트워크 프로그래밍, 인프라 관리 등 다양한 영역에서 이 철학을 이해하면 리눅스를 훨씬 쉽게 다룰 수 있다.


참고: 특정 상황에서의 활용

프로덕션 환경에서는 Prometheus, node-exporter, cAdvisor 같은 전문 모니터링 도구를 사용하는 것이 권장된다. 하지만 디버깅, 경량 모니터링, 커스텀 도구 개발 등 특정 상황에서는 /proc, /sys를 직접 읽는 방식을 고려해볼 수 있다.

cgroup 정보 읽기

컨테이너 내부에서 CPU/메모리 제한을 파일로 확인하는 예시다.

package main

import (
    "fmt"
    "os"
    "strconv"
    "strings"
)

// CPU 쿼터 읽기 (파일로!)
func readCPUQuota() (int64, error) {
    data, err := os.ReadFile("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")
    if err != nil {
        return 0, err
    }
    return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
}

// 메모리 제한 읽기 (파일로!)
func readMemoryLimit() (int64, error) {
    data, err := os.ReadFile("/sys/fs/cgroup/memory/memory.limit_in_bytes")
    if err != nil {
        return 0, err
    }
    return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
}

func main() {
    quota, _ := readCPUQuota()
    fmt.Printf("CPU Quota: %d μs\n", quota)
    
    limit, _ := readMemoryLimit()
    fmt.Printf("Memory Limit: %d bytes\n", limit)
}


GPU 메모리 사용량 확인

컨테이너에서 GPU 상태도 파일로 읽을 수 있다.

// GPU 메모리 사용량 확인 (파일로!)
func getGPUMemory(deviceID int) (int64, error) {
    path := fmt.Sprintf("/sys/class/drm/card%d/device/mem_info_vram_used", 
                        deviceID)
    data, err := os.ReadFile(path)
    if err != nil {
        return 0, err
    }
    return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
}

// NVIDIA GPU 온도 확인 (파일로!)
func getGPUTemperature(deviceID int) (int, error) {
    path := fmt.Sprintf("/sys/class/hwmon/hwmon%d/temp1_input", deviceID)
    data, err := os.ReadFile(path)
    if err != nil {
        return 0, err
    }
    temp, err := strconv.Atoi(strings.TrimSpace(string(data)))
    return temp / 1000, err  // milli-celsius → celsius
}


실전 활용 예시

Kubernetes Pod에서 리소스 모니터링

Pod 내부에서 파일을 읽어 리소스 사용량을 계산하는 예시다.

// Pod의 현재 CPU 사용률 계산
func getCurrentCPUUsage() (float64, error) {
    // /proc/stat 파일 읽기
    data, err := os.ReadFile("/proc/stat")
    if err != nil {
        return 0, err
    }
    // CPU 시간 파싱 후 사용률 계산
    // ...
}

// 네트워크 인터페이스 통계
func getNetworkStats(iface string) (rx, tx int64, err error) {
    // /proc/net/dev 파일 읽기
    data, err := os.ReadFile("/proc/net/dev")
    if err != nil {
        return 0, 0, err
    }
    // 파싱 후 rx/tx 바이트 반환
    // ...
}


장치 주입 확인

컨테이너에 GPU가 제대로 주입되었는지 파일로 확인할 수 있다.

# 컨테이너 내부에서
ls -l /dev/nvidia*
# crw-rw-rw- 1 root root 195,   0 Feb  3 10:00 /dev/nvidia0
# crw-rw-rw- 1 root root 195, 255 Feb  3 10:00 /dev/nvidiactl

# GPU 정보 확인 (파일로!)
cat /proc/driver/nvidia/gpus/0000:01:00.0/information

참고: 컨테이너에 GPU를 주입하는 방법은 컨테이너 장치 주입을 참고하자.


이와 같이 “Everything is a File” 철학에 근거해 시스템 정보가 파일로 노출되므로, 특정 상황에서는 이렇게 노출된 /proc, /sys 파일 등을 직접 읽는 방식도 고려해볼 수 있다.





hit count

댓글남기기