Python os.memfd_create()로 이름 없는 메모리 파일 다루기

프로세스 간에 잠깐 쓰고 버릴 데이터를 주고받을 때 /tmp에 실제 파일을 만들면 디스크(또는 tmpfs) 경로가 남고, 정리를 깜빡하면 흔적이 계속 쌓인다. memfd_create()는 파일시스템 어디에도 나타나지 않는 익명 메모리 파일을 만들어 이 문제를 없앤다 — 파일 디스크립터 하나로 접근하는 순수 메모리 객체라고 보면 된다. 이 글에서는 Python os.memfd_create()의 기본 사용법과 mmap 결합, seal(봉인)로 불변 객체를 만드는 방법, fork() 자식 프로세스와 fd를 공유하는 방법까지 실제로 실행해서 정리한다.

기본 사용법

os.memfd_create()(Python 3.8+, Linux 전용)는 일반 파일 디스크립터를 반환한다. ftruncate()로 크기를 잡고 write()/read()로 다루는 것도 일반 파일과 동일하다.

import os

fd = os.memfd_create("demo", os.MFD_CLOEXEC)
os.ftruncate(fd, 4096)
os.write(fd, b"hello memfd")

os.lseek(fd, 0, os.SEEK_SET)
print(os.read(fd, 20))
os.close(fd)
$ python3 memfd_basic.py
b'hello memfd\x00\x00\x00\x00\x00\x00\x00\x00\x00'

ftruncate()로 확보한 4096바이트 중 쓰지 않은 나머지는 0으로 채워진다. /proc/self/fd/<fd번호>를 열어보면 실제 경로 없이 memfd:demo (deleted) 형태로 표시된다.

mmap과 함께 쓰기

fd를 그대로 mmap.mmap()에 넘기면 memfd를 바이트 배열처럼 슬라이싱해서 다룰 수 있다.

import os, mmap

fd = os.memfd_create("demo")
os.ftruncate(fd, 4096)
os.write(fd, b"hello memfd")

os.lseek(fd, 0, os.SEEK_SET)
m = mmap.mmap(fd, 4096)
m[0:5] = b"WORLD"
print(m[0:11])
m.close()
os.close(fd)
$ python3 memfd_mmap.py
b'WORLD memfd'

seal로 봉인해 불변 객체 만들기

memfd는 fcntl(F_ADD_SEALS)로 이후 쓰기·크기 변경을 원천 차단하는 seal을 걸 수 있다. 다만 memfd_create() 호출 시 MFD_ALLOW_SEALING 플래그를 주지 않으면 생성 시점에 이미 F_SEAL_SEAL(더 이상 seal 추가 자체를 금지)이 걸려 있어 seal 요청이 EPERM으로 실패한다 — 아래는 이 플래그를 빠뜨렸을 때와 넣었을 때를 함께 실행한 결과다.

import os, fcntl

F_ADD_SEALS = 1033
F_GET_SEALS = 1034
F_SEAL_WRITE = 0x0008
F_SEAL_SHRINK = 0x0002
F_SEAL_GROW = 0x0004

def try_seal(allow_sealing):
	flags = os.MFD_ALLOW_SEALING if allow_sealing else 0
	fd = os.memfd_create("sealed_demo", flags)
	os.ftruncate(fd, 4096)
	os.write(fd, b"immutable data")
	try:
		fcntl.fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW)
		print(f"allow_sealing={allow_sealing}: seal 성공, seals={bin(fcntl.fcntl(fd, F_GET_SEALS))}")
	except OSError as e:
		print(f"allow_sealing={allow_sealing}: seal 실패 - {e}")
	os.close(fd)

try_seal(False)
try_seal(True)
$ python3 memfd_seal.py
allow_sealing=False: seal 실패 - [Errno 1] Operation not permitted
allow_sealing=True: seal 성공, seals=0b1110

seal이 걸린 fd에 쓰기·크기 변경을 시도하면 마찬가지로 EPERM이 나고, 읽기는 그대로 동작한다.

try:
	os.write(fd, b"nope")
except OSError as e:
	print("write:", repr(e))

try:
	os.ftruncate(fd, 8192)
except OSError as e:
	print("ftruncate:", repr(e))

print("pread:", os.pread(fd, 14, 0))
$ python3 memfd_seal_write.py
write: PermissionError(1, 'Operation not permitted')
ftruncate: PermissionError(1, 'Operation not permitted')
pread: b'immutable data'

fork 자식 프로세스와 fd 공유하기

일반 익명 mmap()과 달리 memfd는 fd 자체로 존재하므로, fork() 후 자식이 상속받은 같은 fd로 부모와 곧바로 데이터를 주고받을 수 있다.

import os, time

fd = os.memfd_create("shared_demo")
os.ftruncate(fd, 4096)

pid = os.fork()
if pid == 0:
	time.sleep(0.3)
	os.lseek(fd, 0, os.SEEK_SET)
	os.write(fd, b"written-by-child")
	os._exit(0)
else:
	time.sleep(0.6)
	os.lseek(fd, 0, os.SEEK_SET)
	print("parent sees:", os.read(fd, 30))
	os.waitpid(pid, 0)
	os.close(fd)
$ python3 memfd_fork.py
parent sees: b'written-by-child\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

fork 없이 별개 프로세스끼리 fd를 넘기려면 multiprocessing.reduction이나 유닉스 소켓의 SCM_RIGHTS로 fd를 전달해야 한다 — memfd 자체는 fd 전달 메커니즘을 대체하지 않는다.

주의사항

  • seal 플래그 순서: MFD_ALLOW_SEALING 없이 만든 memfd는 이미 F_SEAL_SEAL이 걸려 있어 나중에 seal을 추가로 걸 수 없다. seal을 쓸 계획이면 생성 시점에 반드시 플래그를 넣어야 한다.
  • Linux 전용: memfd_create(2) 시스템 콜은 Linux 커널 3.17+에서만 동작한다. macOS/Windows에서는 os.memfd_create 자체가 없다.
  • 메모리 압박: memfd는 디스크가 아니라 페이지 캐시(사실상 tmpfs)를 점유한다. 큰 파일을 memfd로 만들면 그만큼 RAM을 직접 소비하므로 대용량 임시 데이터에는 부적합하다.
  • CLOEXEC 기본값: 플래그 없이 만들면 exec() 이후에도 fd가 살아남는다. 자식 프로세스로 fd를 넘기고 싶지 않다면 MFD_CLOEXEC를 명시해야 한다.

마무리

memfd_create는 임시 파일을 흔적 없이 다루거나, seal로 한 번 쓰면 바뀌지 않는 공유 버퍼가 필요하거나, fork한 자식과 대용량 데이터를 파이프 없이 주고받아야 할 때 유용하다. 다만 셋 중 어느 경우든 결국 fd 하나로 접근하는 메모리라는 본질은 같으므로, 앞서 다룬 eventfd·mmap 글과 함께 보면 Linux의 “모든 것은 fd다”라는 설계가 더 뚜렷하게 보인다.

참고

답글 남기기