← Posts

OS · File Descriptor · Socket

파일·소켓·연결이 다 같은 번호다 - 파일 디스크립터

파일도, 네트워크 소켓도, DB 연결도 프로그램은 작은 정수 하나로 가리킨다. 파일 디스크립터라는 이 번호가 무엇이고, 왜 유한하며, 안 닫으면 왜 새는지 - 연결 풀이 반납에 그토록 예민한 이유가 여기 있다.

목차
  1. 파일도 소켓도 연결도 같은 번호로 가리킨다
  2. 모든 것은 파일이다
  3. fd 테이블은 프로세스마다 있다
  4. 소켓도 fd라서 연결도 fd다
  5. fd는 유한하다
  6. 안 닫으면 샌다
  7. 실무에서: 번호가 새는 걸 잡는다
  8. 정리

프로그램에서 파일을 열면 정수 하나가 돌아온다. 소켓을 열어도, DB에 연결해도 마찬가지다. 이 작은 번호 하나로 온갖 자원을 가리키는 방식이 파일 디스크립터다. 연결 풀이 아끼던 그 “연결”의 밑바닥이기도 하다.

파일도 소켓도 연결도 같은 번호로 가리킨다

파일을 열면 3 같은 정수가 돌아오고, 이후 읽기·쓰기는 그 3으로 한다. 놀라운 건 네트워크 소켓도, 파이프도, 심지어 다른 프로그램과의 통신 통로도 똑같이 정수 하나로 가리킨다는 점이다.

plaintext
파일 열기      →  3
소켓 열기      →  4
DB 연결 열기   →  5   (실은 그 밑이 소켓이다)

종류가 제각각인데 프로그램은 다 같은 방식으로 다룬다 - 번호로 가리키고, 그 번호로 읽고 쓰고, 다 쓰면 그 번호를 닫는다. 이 번호가 파일 디스크립터(file descriptor, fd) 다.

모든 것은 파일이다

이게 가능한 건 유닉스 계열 OS의 오래된 발상 때문이다 - “모든 것을 파일처럼 다룬다.” 진짜 파일이든 네트워크 소켓이든, “열고 · 읽고 · 쓰고 · 닫는다”는 같은 동작으로 취급한다.

덕분에 프로그램은 자원의 종류마다 다른 방식을 배울 필요가 없다. 파일에 쓰듯 소켓에 쓰고, 파일을 읽듯 네트워크를 읽는다. 이 통일 덕에 fd라는 번호 하나가 온갖 자원의 공통 손잡이가 된다.

이건 유닉스·리눅스·맥 계열의 방식이다. 윈도우는 비슷한 걸 “핸들(handle)“이라 부르며 세부가 다르다. 개념 - “자원을 번호/손잡이로 가리킨다” - 은 같다.

fd 테이블은 프로세스마다 있다

그 번호는 어디에 적혀 있나. 프로세스가 받는 세 가지 중 “자원 목록”이 바로 이것이다. 프로세스마다 자기 fd 테이블이 있고, 번호는 그 테이블의 칸을 가리킨다.

plaintext
프로세스 A의 fd 테이블          프로세스 B의 fd 테이블
  3 → 로그 파일                   3 → 소켓
  4 → 소켓                        4 → 설정 파일

그래서 A의 3과 B의 3은 전혀 다른 것을 가리킨다 - 가상 메모리에서 같은 주소가 다른 물리 칸이었던 것과 똑 닮았다. fd는 그 프로세스 안에서만 뜻이 있는 번호다. 프로세스가 죽으면 이 테이블이 통째로 정리되며 열어둔 것들이 다 닫힌다.

소켓도 fd라서 연결도 fd다

네트워크 연결은 소켓이고, 소켓은 fd다. 그러니 connection-pooling 글의 “DB 연결”도 밑을 보면 fd 하나다. DB 연결을 하나 열면, 그 밑에서 소켓 fd 하나가 열려 DB 서버와의 통신 통로를 잡는다.

이 사실이 왜 중요하냐면, “연결”이 추상적인 개념이 아니라 OS가 세는 실물 자원이라는 뜻이기 때문이다. 연결을 아끼라는 말은 곧 fd를 아끼라는 말이다.

fd는 유한하다

fd 테이블은 무한하지 않다. 한 프로세스가 열 수 있는 fd 수에는 상한이 있다(OS가 정하고 조절 가능). 파일·소켓·연결을 계속 열기만 하고 안 닫으면 이 상한에 부딪힌다.

그 순간 나오는 게 그 유명한 “too many open files” 오류다. 새 파일을 못 열고, 새 연결을 못 맺고, 심지어 새 요청을 못 받는다. 자원이 없어서가 아니라 번호를 다 써버려서다.

안 닫으면 샌다

fd도 힙 메모리처럼 스스로 안 사라진다. 열었으면 닫아야 그 번호가 테이블에서 풀려 다시 쓰인다. 안 닫으면 fd가 야금야금 쌓이는 fd 누수가 된다 - 한동안 멀쩡하다 어느 순간 상한에 닿아 “too many open files”로 터진다.

connection-pooling 글이 “빌렸으면 반납해야 한다”고 그토록 강조한 이유가 이것이다. 연결을 안 반납하면 그 밑 fd가 안 닫히고, 풀이 마르는 동시에 fd도 샌다. 반납은 예의가 아니라 유한한 번호를 되돌려주는 일이다.

실무에서: 번호가 새는 걸 잡는다

  • “too many open files”는 fd 누수 신호 - 어딘가에서 파일·소켓·연결을 열고 안 닫는다. 잘 도는 것 같다가 시간이 지나 터지면 이걸 의심한다.
  • 열린 fd 수를 지표로 본다 - 프로세스가 연 fd 수가 시간에 따라 계속 우상향하면 누수다. 평평하게 유지되면 건강하다.
  • 닫기를 코드가 보장하게 - “쓰고 나면 반드시 닫는” 구조(언어의 자동 닫기 구문·풀의 반납 보장)를 써서, 사람이 잊어도 닫히게 한다. 연결 풀이 반납을 관리해 주는 것도 이 때문이다.
  • 상한을 필요만큼 올린다 - 많은 연결을 다뤄야 하는 서버는 fd 상한 자체를 올려야 할 때가 있다. 다만 누수를 상한 올리기로 덮으면 안 된다 - 그건 터지는 시점만 미룬다.

정리

  • 파일·소켓·연결을 프로그램은 전부 작은 정수(fd) 하나로 가리킨다 - “모든 것은 파일” 발상 덕이다.
  • fd 테이블은 프로세스마다 따로라, 같은 번호도 프로세스가 다르면 다른 것을 가리킨다.
  • 연결은 밑을 보면 소켓 fd - 연결을 아끼라는 건 유한한 fd를 아끼라는 말이다.
  • fd는 유한하고 안 닫으면 샌다 - 그래서 “too many open files”가 나고, 반납이 그토록 중요하다.

다음 글은 시리즈를 닫으며, 그 fd를 통한 I/O가 스레드를 어떻게 멈춰 세우는지 - 블로킹 I/O로 밑바닥을 백엔드 개념에 잇는다.