티스토리 뷰

시분할(Time Sharing)은 하나의 컴퓨터 자원을 여러 사용자가
시간을 나눠가며 동시에 사용하는 것처럼 보이게 만드는 기술입니다.
혹은, 한 사용자가 여러 작업을 동시에 수행하는 것처럼 보이게 만들기도 합니다.

 
예를 들어, 컴퓨터로 노래를 들으면서 게임을 하고 있는데, 그 와중에 친구에게 카카오톡 메시지가 도착했다고 해봅시다.
사실 컴퓨터는 한 번에 하나의 작업만 처리할 수 있는 구조입니다.
하지만 CPU는 각 작업에 아주 짧은 시간(몇 밀리초)을 번갈아가며 배정해주기 때문에 사용자는 마치 노래, 게임, 메신저가 동시에 작동하는 것처럼 느끼게 됩니다.
이처럼 짧은 시간 단위로 작업을 나누어 번갈아 실행하는 방식이 바로 시분할(Time Sharing)입니다.
 
UNIX는 이 시분할(Time Sharing) 개념을 실용적으로 구현한 최초의 운영체제 중 하나입니다.
하나의 컴퓨터를 여러 사용자가 동시에 사용하는 것처럼 보이게 만들고,
한 사용자의 여러 작업이 병렬적으로 실행되는 것처럼 처리할 수 있도록 설계되었습니다.
 

이 글에서는 UNIX가 프로세스와 메모리구조시스템 콜파일 시스템을 통해
시분할 기반의 멀티태스킹 시스템을 어떻게 구현했는지 단계별로 살펴보겠습니다.

 

목차

1. 개요: UNIX란 무엇인가?
2. 프로세스와 메모리 구조
3. 시스템 콜 개요: UNIX의 모든 동작은 여기서 시작된다
    3.1. 파일 입출력: 모든 것을 파일처럼 다룬다 (open, read, write, seek, close)
    3.2. 프로세스와 실행 흐름 (fork, exec, wait, exit)
    3.3. 프로세스 간 통신 (pipe)
    3.4. 시스템 콜 예시
    3.5. 시스템 콜 요약
4. 파일 시스템 구조와 i-node
    4.1 디렉토리와 i-node
    4.2 파일 열기와 파일 디스크럽터
    4.3 파일 생성, 링크 삭제의 동작 방식
    4.4 디스크 구조와 파일 크기
    4.5 특수 파일(special file)의 처리
    4.6. 파일 시스템 구조와 i-node 요약
5. 파일 시스템 계층 구조와 mount
    5.1 계층적 디렉토리 구조란?
    5.2 특수한 두 항목: .와 ..
    5.3 mount: 다른 장치를 파일 시스템에 붙이기
    5.4 mount 내부 동작
    5.5 사용자 입장에서의 이점
    5.6 mount후 consistency check: 일관성을 지키는 UNIX의 방식
    5.7 mount의 철학적 의미
    5.8 계층적 디렉토리 구조와 mount 요약
6. UNIX, Linux 비교
    6.1 UNIX 철학 요약
    6.2 UNIX와 Linux 공통점
    6.3 UNIX와 Linux 차이점
    6.4 UNIX와 Linux 비교 요약
7. 참고자료


1. 개요: UNIX란 무엇인가?

유닉스(UNIX)는 1969년, AT&T 벨 연구소에서 만든 운영체제입니다.
운영체제는 컴퓨터가 프로그램을 실행하고 파일을 관리할 수 있게 도와주는 컴퓨터의 관리자 같은 존재입니다.
 
유닉스는 다음과 같은 특징을 가지고 있습니다.
- 여러 사람이 동시에 한 컴퓨터를 사용할 수 있게 해주는 다중 사용자 시스템입니다.
- 한 사람이 여러 작업을 동시에 할 수 있게 해주는 다중 작업 시스템이기도 합니다.
 
UNIX는 “모든 것을 파일처럼 다룬다”는 철학을 중심으로 설계되었습니다.
즉, 텍스트 파일뿐 아니라, 프린터, 키보드, 터미널, 디스크, 네트워크 장치, 실행 중인 프로그램까지도 모두 파일처럼 동일한 방식으로 접근하고 제어할 수 있습니다. 예를 들어, 키보드에서 입력을 받을 때도 /dev/tty와 같은 파일을 읽는 것처럼 처리되고, 프린터에 출력을 보낼 때도 특정 파일에 데이터를 쓰는 것처럼 동작합니다.
 
이러한 추상화 덕분에, 개발자는 장치의 종류에 상관없이 ‘파일 입출력’만 다룰 줄 알면 대부분의 자원과 상호작용할 수 있습니다.
이처럼 단순하고 일관된 구조는 UNIX 시스템을 강력하면서도 우아하게 만든 핵심 원리 중 하나입니다.


2. 프로세스와 메모리 구조

UNIX에서 실행 중인 프로그램은 프로세스(Process)라고 부르며,
그 프로세스가 담고 있는 전체 상태(메모리, 레지스터, 열린 파일 등)이미지(Image)라고 합니다.
 
즉, 이미지는 가상 컴퓨터의 현재 상태 전체를 의미하고, 프로세스는 그 이미지를 실행 중인 행위입니다.
프로세스가 실행되는 동안, 그 이미지(상태)는 반드시 주기억장치(core memory = 현대의 RAM)에 올라와 있어야 합니다.
다른 프로세스가 CPU를 사용하는 동안에도, 우선순위가 낮지 않다면 이미지가 메모리에 유지되지만, 더 높은 우선순위의 프로세스가 등장하면 디스크로 스왑(swap out)될 수 있습니다.
 

UNIX 프로세스의 메모리 구조

UNIX의 사용자 메모리 공간은 다음의 세 가지 논리적 세그먼트로 나뉩니다.

참고: https://www.researchgate.net/figure/Unix-process-memory-layout_fig1_2332255

세그먼트 역할 특징
Stack Segment 함수 호출, 지역 변수 - 가장 높은 주소에서 시작
- 호출에 따라 아래 방향으로 자동 확장
Data Segment 전역 변수, 동적 메모리 - Text 위쪽 8KB 경계에서 시작
- 프로세스마다 독립적
- sbrk() 같은 시스템 콜로 동적 확장 가능, 현대는 mmap()
Text Segment 프로그램 코드 - 가상 주소 0번지부터 시작
- 읽기 전용
- 동일 프로그램 실행 시 여러 프로세스 간 공유 가능

 
이 구조 덕분에 UNIX는 다음과 같은 장점을 가집니다.

  • 효율적인 메모리 분리: 코드와 데이터, 스택을 논리적으로 구분하여 안전성 확보
  • 텍스트 공유: 동일한 프로그램 실행 시 코드(Text)는 공유, 나머지는 분리 → 메모리 절약 + 멀티유저 환경에서의 성능 향상

 

sbrk(): 동적 메모리 확장의 고전적인 방식

UNIX에서 힙 영역(Heap) 은 전역 변수 외에 실행 중에 동적으로 필요한 데이터를 저장하기 위한 공간입니다.
이 힙 영역은 Data Segment의 일부이며, 실행 중에 유동적으로 확장될 수 있도록 설계되어 있습니다.
이때 사용되는 시스템 콜이 바로 sbrk() 입니다.

void *sbrk(intptr_t increment);
  • sbrk(n)은 프로세스의 힙 영역 끝을 n 바이트만큼 확장(또는 음수일 경우 축소)합니다.
  • 반환값은 이전 힙의 끝 주소이며, 여기서부터 새로운 동적 메모리 공간이 제공됩니다.
  • C 표준 라이브러리의 malloc() 함수는 내부적으로 이 sbrk()를 사용하여 메모리를 확보합니다.

 

현대 시스템의 메모리 할당: sbrk() vs mmap()

현대의 리눅스/유닉스 시스템에서 C 언어의 malloc()은 동적 메모리 요청 크기에 따라 서로 다른 방식으로 메모리를 할당합니다.

  • 작은 요청 (< 약 128KB) → 기존 힙 영역을 확장 (sbrk() 기반)
  • 큰 요청 (수백 KB ~ MB) → 가상 주소 공간에 새 페이지 매핑 (mmap() 기반)

둘 다 동적 메모리를 할당하는 방법이지만,
sbrk()는 단순한 힙 확장 방식이고,
mmap()은 가상 메모리의 강점을 살려 더 유연하고 안전하게 메모리를 다룹니다.
즉, mmap은 메모리를 원하는 위치에, 원하는 속성으로, 정확히 필요한 만큼 매핑할 수 있습니다.
 
이 차이는 가상 메모리와 페이지 테이블이 지원하는 유연한 주소 매핑 구조 덕분이며,
이후 메모리 보호와 프로세스 간 메모리 공유 같은 기능의 기반이 됩니다.
 
sbrk()로 이전 힙의 끝 영역을 단순하게 확장하는 방식은 다음과 같은  문제점이 있어 mmap()를 선호한다고 합니다.
→ 메모리 해제(free)가 어려움
→ 조각화(fragmentation) 발생
→ 다중 스레드나 동시 요청에 부적합
→ 충돌 위험 존재 (stack ↕ heap)
 


3.  시스템 콜 개요: UNIX의 모든 동작은 여기서 시작된다

UNIX에서 사용자 프로그램이 운영체제의 기능을 사용하려면, 시스템 콜(System Call)을 통해 커널과 통신해야 합니다.
파일을 열거나 읽고 쓰는 작업, 새로운 프로세스를 만드는 일, 프로그램을 실행하거나 종료하는 일 등 모든 주요 기능은 이 시스템 콜을 통해 수행됩니다.
 
UNIX의 시스템 콜은 작고 단순한 기능을 수행하지만, 필요한 작업들을 조합하면 매우 유연하고 강력한 처리 흐름을 만들 수 있다는 점에서 UNIX 철학을 잘 보여줍니다.
 
시스템 콜을 조합하여 강력한 처리흐름을 만드는 예시는 시스템 콜 마지막 부분에서 다룹니다.
 

3-1. 파일 입출력: 모든 것을 파일처럼 다룬다 (open, read, write, seek, close)

UNIX는 입출력(I/O) 시스템을 설계할 때, 가능한 한 모든 장치와 파일에 대해 동일한 방식으로 접근할 수 있도록 했습니다.
즉, 텍스트 파일이든, 하드디스크든, 프린터든, 터미널이든 모두 같은 시스템 콜(open, read, write 등)을 통해 다룰 수 있게 설계했습니다.
 

UNIX I/O의 핵심 개념

- 순차 접근(sequential access)과 랜덤 접근(random access)의 구분이 없다
     → 파일을 처음부터 차례대로 읽든, 특정 위치로 이동해 읽든, 모두 같은 시스템 콜(read, seek)로 처리합니다.
 
- 고정된 레코드 크기(logical record size)를 강제하지 않는다
     → 사용자는 원하는 만큼의 바이트를 읽고 쓸 수 있으며, 시스템이 미리 정한 크기 단위는 없습니다.
 
- 파일 크기를 미리 정하지 않는다
     → 쓰기가 발생한 마지막 바이트 위치가 파일의 크기가 됩니다. 즉, write하면서 파일크기는 증가하거나 감소합니다.
 
이런 설계는 개발자에게 자유롭고 유연한 파일 처리 방식을 제공하며, 기계마다 달랐던 장치 제어 방식의 복잡성을 크게 줄였습니다.

 

주요 파일 시스템 콜

시스템 콜 설명
open(name, flag) 파일 열기. 읽기/쓰기 모드 지정. 디스크립터 반환
read(filep, buffer, count) 파일 읽기. 파일에서 최대 count 바이트 읽기
write(filep, buffer, count) 파일 쓰기. 버퍼 내용을 파일에 쓰기
seek(filep, base, offset) 파일 내 오프셋 이동. 파일 포인터를 이동시켜 임의 위치 접근
close(filep) 파일 닫기.  열린 파일 닫기

 

파일 디스크립터(file descriptor)는 시스템이 내부적으로 관리하는 정수 번호로, 열린 파일을 식별하는 데 사용되며 이후 모든 read/write/seek 작업에 사용됩니다.
 

포인터 기반 동작

-  파일을 열면, 시스템은 그 파일에 대해 읽기/쓰기 포인터를 유지합니다.
-  데이터를 읽거나 쓰면 해당 포인터는 자동으로 이동합니다.
-  seek()으로 포인터를 원하는 위치로 이동하면, 랜덤 접근도 간단히 구현됩니다.

 

파일 끝(EOF)에 도달하면 read()는 0을 반환합니다.
파일을 쓰는 도중, 포인터가 파일 끝을 넘으면 자동으로 파일 크기를 늘립니다.
 

잠금은 없다, 책임은 사용자에게

UNIX에는 사용자 눈에 보이는 잠금(locking) 기능이 없습니다.
-  여러 사용자가 동시에 같은 파일을 열어 읽거나 쓸 수 있습니다.
-  이러면 충돌이 날 수는 있지만, UNIX는 그것을 시스템이 막는 대신 사용자가 책임지고 관리하도록 설계되었습니다.

 

왜 잠금을 제공하지 않았을까?

1. 당시 UNIX 환경은 독립적으로 동작하는 대형 DB 시스템이 아닌 연구/개인 중심의 환경이었고,
2. 잠금이 있다고 해도, 복사 후 편집하는 방식의 편집기(예: ed, vi)는 잠금만으로 경합을 방지할 수 없기 때문입니다.
    → 즉, UNIX는 “잠금은 필요하지도, 충분하지도 않다”는 입장이었다고 합니다.

 
vi는 "원본 파일을 직접 수정"하지 않습니다.

vi는 파일을 편집할 때 원본 파일을 바로 변경하지 않고, 다음과 같은 구조로 동작합니다.
 
1. vi file.txt 실행 시
- 파일 내용을 메모리 버퍼(임시 공간)에 읽어옵니다.
- 사용자는 이 메모리 버퍼 상에서 편집을 진행합니다.

 

2. 편집 중에는
- 실제 file.txt에는 아무런 변경이 없습니다.
- 대부분 ~로 끝나는 백업 파일이나 .swp 파일이 같은 디렉토리에 생성됩니다.

 

3. :w 또는 :wq 입력 시
- 편집된 내용을 전체 통째로 덮어쓰기(write)하여 file.txt에 저장합니다.
- 즉, 원본 파일은 이 시점에 완전히 새 파일로 대체됩니다.

 
경합이 발생하는 이유

- 사용자 A와 B가 동시에 vi file.txt로 같은 파일을 편집할 경우, 각자 자신만의 메모리 버퍼에서 독립적으로 편집합니다.
- 둘 다 :wq로 저장하면, 마지막으로 저장한 사람의 내용이 최종 결과가 됩니다.
   → 이 과정에서 서로의 변경 사항이 덮어씌워져 손실될 수 있습니다.
- UNIX는 이 상황에서 강제 잠금을 걸어 방지하지 않으며, vi는 .swp 파일을 통해 "누군가 이 파일을 편집 중"이라는 경고만 표시합니다.
 

vi가 사용하는 보호 수단: swap 파일

 
- vi는 파일을 열면 file.txt.swp라는 스왑 파일을 생성합니다.
- 다른 사용자가 같은 파일을 vi로 열면 다음과 같은 경고가 뜹니다.

E325: ATTENTION
Found a swap file by the name ".file.txt.swp"

→ 사용자는 이때 "계속 열기 / 강제 종료 / 복구 시도" 등을 선택할 수 있지만, 시스템이 무조건적인 접근을 막지는 않습니다.
 
 
"잠금만으로는 충돌을 완전히 방지할 수 없다" → 편집기 구조상, 사용자 간 충돌은 피할 수 없고 → 그렇다면 시스템이 강제 제어하기보다는, 사용자에게 경고만 하고 책임을 맡기자 이게 UNIX 철학이고, vi가 대표적인 사례입니다.
 

3-2. 프로세스와 실행 흐름 (fork, exec, wait, exit)

프로세스 생성: fork()

새로운 프로세스를 만들기 위해 fork() 시스템 콜을 사용합니다.
fork()는 현재 실행 중인 프로세스를 그대로 복사해서 자식 프로세스를 만듭니다.

 

- 부모와 자식은 서로 독립적인 메모리 공간(image)을 가집니다.
- 그러나 열린 파일 디스크립터는 공유됩니다.
- fork()는 부모에겐 자식의 PID를, 자식에겐 0을 반환합니다.
    → 이 차이를 통해 두 프로세스는 반환값을 기준으로 서로를 구분할 수 있습니다.

이러한 구조 덕분에 fork() 이후, 동일한 코드 흐름에서 병렬 실행이 가능합니다.

 

새로운 프로그램 실행: exec()

exec(file, arg1, arg2, ...)는 현재 프로세스를 완전히 새로운 프로그램으로 대체합니다.
- 기존의 코드(text), 데이터(data), 스택(stack)은 모두 제거됩니다.
- 열린 파일, 현재 디렉토리 등은 유지됩니다.
- 실행에 실패하지 않는 이상, exec()는 절대 반환되지 않고 새 프로그램의 코드가 실행됩니다.
- 마치 서브루틴 호출이 아니라, 프로그램 점프(jump)처럼 작동합니다.

 

프로세스 제어: wait() & exit()

- wait()은 부모 프로세스가 자식 프로세스가 종료될 때까지 대기하게 합니다. 자식이 종료되면 PID와 상태값을 반환받습니다.
- exit(status)는 프로세스를 종료하고, 열린 파일들을 정리하며 부모에게 종료 사실을 전달합니다. 부모가 없다면, 시스템의 상위 프로세스가 상태를 이어받습니다.

 

3-3. 프로세스 간 통신 (pipe)

UNIX는 pipe() 시스템 콜을 통해 부모-자식 간 데이터 통신 채널을 제공합니다.
- pipe()는 읽기/쓰기 가능한 파일 디스크립터를 생성합니다.
- fork()로 자식 프로세스를 만들면, 파이프 디스크립터는 공유됩니다.
    → fork()를 통한 부모와 자식 프로세스는 별도의 명시적 동기화 없이도 데이터를 주고받을 수 있습니다.
- 한쪽이 write(), 다른 쪽이 read() 하면 데이터가 전달됩니다.
- 읽는 쪽은 상대방이 데이터를 쓸 때까지 자동으로 대기합니다.
    → 간단한 동기화 수단으로도 활용됩니다.

단, pipe()는 공통 조상 프로세스를 통해 생성되어야 하므로, 완전히 일반적인 통신 수단은 아닙니다.

 

3.4. 시스템 콜 예시

file.txt에서 "error"가 포함된 줄 수를 출력하는 명령어 조합입니다.

cat file.txt | grep "error" | wc -l: file.txt

 
이 명령은 다음과 같은 시스템 콜들을 호출합니다.
 
1. cat file.txt
- open("file.txt", O_RDONLY) ← 파일 열기
- read(fd, buffer, size) ← 파일 내용 읽기
- write(1, buffer, size) ← stdout(파이프의 입력)으로 내보내기
 
2. grep "error"
- read(0, buffer, size) ← stdin(파이프로부터 받기)
- 버퍼에서 "error" 포함된 줄 필터링
- write(1, buffer, size) ← stdout(다음 파이프로 넘기기)
 
3. wc -l
- read(0, buffer, size) ← grep 결과 받기
- 줄 개수 세기
- write(1, buffer, size) ← 결과를 출력
 
그리고 이들 사이를 연결하는 | (파이프)
- 셸(Shell)은 pipe() 시스템 콜로 파이프를 생성하고,
- fork()로 각 명령어를 별도 프로세스로 분기한 뒤,
- dup2()를 사용해 표준 입력/출력을 파이프에 연결합니다.

[Shell]
 ├─ pipe() → 파이프 생성
 ├─ fork() → cat 실행
 │   └─ open + read + write
 ├─ fork() → grep 실행
 │   └─ read + write
 ├─ fork() → wc 실행
 │   └─ read + write
 └─ wait() → 자식 프로세스 종료 대기

 
이처럼 겉으로는 단순한 명령어 조합처럼 보여도, 그 내부에서는 수많은 UNIX 시스템 콜들이 연결되어 작동하고 있습니다.
이것이 UNIX 철학의 핵심입니다.

"작은 도구와 단순한 시스템 콜을 조합해, 복잡한 작업을 간결하게 해결한다."

 

3.5 시스템 콜 요약

- UNIX는 파일과 장치를 동일한 방식으로 처리하기 때문에, 시스템 콜 인터페이스도 단순하고 일관적입니다.
- 파일 크기나 구조에 대한 사전 제약 없이 자유롭게 접근이 가능하며,
- 파일 디스크립터 기반의 시스템 콜(open/read/write/seek)만으로 대부분의 작업을 수행할 수 있습니다.
- 동시 접근에 대해서는 운영체제가 강제 통제를 하지 않고, 개발자와 사용자의 책임에 맡기는 방향을 취했습니다.
- 이런 구조 덕분에 UNIX는 시분할 시스템에 적합한 안정적 멀티태스킹 운영체제로 자리 잡았습니다.
 


4. 파일 시스템 구조와 i-node

UNIX 파일 시스템 구조, 출처: http://www.mykit.com/kor/ele/data_22/unix_.htm

 
앞에서 UNIX는 "모든 것을 파일처럼 다룬다"는 철학을 중심으로 설계되었다고 이야기했습니다.
그렇다면, 그 파일은 시스템 내부에서 어떻게 구현되어 있을까요?
 

4.1 디렉토리와 i-node

UNIX에서 디렉토리는 단순히 “파일의 이름”과 해당 파일을 가리키는 “숫자(i-number)”를 저장하는 리스트일 뿐입니다.
이 숫자, 즉 i-number(index number) 는 시스템 내부에 존재하는 i-node 테이블(i-list)의 인덱스입니다.
이 i-node에는 해당 파일에 대한 실제 정보들이 담겨 있습니다.
 
i-node가 담고 있는 정보는 다음과 같습니다.

  1. 파일 소유자
  2. 접근 권한 (protection bits)
  3. 실제 파일 내용이 저장된 디스크 블록 주소들
  4. 파일 크기
  5. 최종 수정 시각
  6. 해당 파일에 연결된 링크 수 (즉, 몇 개의 디렉토리에서 참조되고 있는지)
  7. 이 파일이 디렉토리인지 여부
  8. 특수 파일인지 여부 (예: 디바이스)
  9. 파일 크기 구분 (작은 파일 / 큰 파일)

즉, 디렉토리는 이름과 위치표시만, 실제 내용은 i-node가 다루는 구조입니다.

4.2 파일 열기와 파일 디스크립터

UNIX에서 사용자가 open() 또는 creat() 시스템 콜을 호출하면, 시스템은 다음과 같은 과정을 거쳐 파일을 엽니다.

 

1. 경로명 탐색: /home/user/a.txt처럼 입력된 경로를 따라가며 각 디렉토리를 순서대로 열고, 최종 파일의 i-number를 찾아냅니다.
2. i-node 참조: 해당 i-number를 이용해 i-node 테이블(i-list)에서 파일의 메타데이터(소유자, 권한, 블록 위치 등)를 읽어옵니다.
3. 파일 테이블 등록: 커널은 열린 파일에 대해 내부의 Open File Table에 정보를 등록합니다.

  • 파일의 i-node 참조
  • 읽기/쓰기 포인터 위치
  • 접근 권한 플래그 등

4. 파일 디스크립터 반환: 이 파일 테이블 항목에 연결된 정수 번호(=파일 디스크립터) 를 사용자에게 반환합니다.
사용자는 이후 이 번호를 통해 read(fd, ...), write(fd, ...), close(fd) 등의 작업을 수행할 수 있습니다.
 
즉, 파일 디스크립터(File Descriptor)는 내부적으로 다음 정보를 간접적으로 가리킵니다.

[fd] → [Open File Table Entry] → [i-node] + [읽기/쓰기 포인터] + [장치 정보]

* 하나의 파일을 여러 번 열면 각각의 파일 디스크립터가 생성되며, 같은 파일이더라도 읽기 포인터는 독립적으로 관리됩니다.
 

4.3 파일 생성, 링크, 삭제의 동작 방식

UNIX 파일 시스템은 i-node 중심 구조이기 때문에, 파일의 이름과 파일의 내용(데이터)은 서로 분리되어 관리됩니다.
즉, 디렉토리는 단지 파일 이름 → i-number 로 연결되는 리스트일 뿐입니다.
 

새 파일 생성

1. creat("a.txt") 호출
2. 새 i-node를 할당하고, 디스크 블록 준비
3. 디렉토리 엔트리에 "a.txt" → 새 i-number 등록
4. 이후 read/write는 이 i-node를 통해 수행
 

링크(link) 생성

1. link("a.txt", "backup.txt") 호출
2. backup.txt라는 이름이 같은 i-number를 가리키도록 디렉토리에 추가
3. i-node의 링크 수(link count) 가 +1 증가
이렇게 되면 하나의 파일 내용이 둘 이상의 이름으로 접근 가능해집니다.
 

파일 삭제

1. unlink("a.txt") 호출
2. 해당 디렉토리 엔트리 제거
3. i-node의 링크 수(link count)를 1 줄임
    링크 수가 0이 되면: node 제거 및 데이터 블록 반환
 
→ 즉, 실제 데이터는 모든 링크가 제거된 후에야 삭제됩니다.
→ 디렉토리 엔트리 삭제 → i-node 링크 수 감소
링크 수가 0이 되면 디스크 블록을 해제하고 i-node도 제거됨
 
이러한 방식 덕분에 하나의 파일은 여러 경로에서 접근 가능하며, 디스크 공간도 효율적으로 관리할 수 있습니다.
 
i-node(stat() 함수 등)와 링크(심볼릭, 하드) 관련 코드 실습은 인터넷에 많이 있습니다.
 

4.4 디스크 구조와 파일 크기

UNIX 파일 시스템은 파일 내용을 512바이트 단위의 블록(block)으로 저장합니다.
파일이 크든 작든, 결국은 여러 개의 블록에 나뉘어 저장되며, 이 블록들이 어디에 있는지를 i-node가 기억하고 있어야 합니다.
하지만 i-node가 가지고 있는 공간은 제한되어 있어, 모든 블록 주소를 직접 저장할 수는 없습니다.
 

직접 블록 (Direct Block)

i-node 안에는 최대 8개의 디스크 블록 주소를 직접 저장할 수 있는 슬롯이 있습니다.
이 슬롯들이 가리키는 블록에는 파일의 실제 내용이 들어 있습니다.
 
따라서, 파일 내용을 512바이트 단위의 블록으로 저장하고, 이 블록을 저장할 수 있는 슬롯이 8개가 있으므로
1개 직접 블록 저장 최대 크기: 512 바이트
8개 직접 블록 저장 최대 크기: 512바이트 x 8블록 = 4KB
 
즉, 파일 크기가 작아서 최대 8개 블록(4KB) 안에 들어간다면, 파일 시스템은 1개의 i-node만으로 바로 접근이 가능합니다.
 

간접 블록 (Indirect Block)

파일이 4KB보다 커지면? i-node의 직접 슬롯만으로는 부족해집니다.
 
이때 UNIX는 간접 블록이라는 중간 단계를 사용합니다.
i-node는 이제 직접 파일 내용을 가리키지 않고, "블록 주소만 모아둔 블록"(= 간접 블록)을 가리킵니다.
그리고 그 간접 블록 안에는 실제 파일 내용이 저장된 블록들의 주소 리스트가 들어 있습니다.
 
하나의 간접 블록에는 256개의 블록이 담긴 주소를 담을 수 있습니다.
즉, 간접 블록에 저장된 주소에 접근하면 256개의 블록이 있고, 각각의 블록 주소가 512바이트짜리 직접 블록을 가리킵니다.
간접 블록 저장 최대 크기: 256블록 x 512바이트 = 128KB
 
이러한 구조 덕분에 UNIX는 작은 파일은 빠르게 처리하고, 큰 파일은 별도 인덱스를 통해 유연하게 확장할 수 있습니다.
다만, 이 구조는 단일 간접 블록까지만 지원하기 때문에
수백 MB~GB 크기의 파일을 다루기 위해서는 이후 버전에서 이중/삼중 간접 블록 개념이 도입됐습니다 (예: UFS, ext 등).
 

직접/간접 블록 비유

 
직접 블록: 마치 사서가 "OS 공룡책은 3층 A열 2번 책꽂이에 있어요"처럼 정확한 위치를 바로 알려주는 것과 같습니다.
→ i-node가 실제 데이터가 저장된 블록의 주소를 직접 알고 있는 경우입니다.
 
간접 블록: 사서가 "OS 관련 책들은 저기 인덱스 목록에 있어요. 거기에서 찾아보세요"라고 말하는 것과 같습니다.
→ i-node는 먼저 간접 블록(= 인덱스 목록) 을 가리키고, 그 블록 안에 적힌 주소들을 따라가야 실제 파일 데이터를 찾을 수 있습니다.
 
이처럼 UNIX는 작은 파일은 빠르게, 큰 파일은 유연하게 처리할 수 있도록 구조를 설계했습니다.
 

4.5 특수 파일(special file)의 처리

UNIX의 큰 특징 중 하나는 장치도 파일처럼 다룬다는 점입니다.
키보드, 터미널, 디스크, 프린터 같은 하드웨어 장치들도 '/dev/' 디렉토리 아래에 있는 특수 파일(special file) 로 표현됩니다.
장치를 파일처럼 다루는 방법(mount 개념) "5. 계층적 디렉토리 구조와 mount"에서 더 자세하게 다룹니다.
 
특수 파일의 경우, i-node의 일부 필드는 의미가 없고 대신 두 바이트로 디바이스 타입과 하위 디바이스 번호를 저장합니다.

바이트 의미
상위 바이트 디바이스 타입 (예: 프린터, 터미널, 디스크 등)
하위 바이트 서브디바이스 번호 (예: 첫 번째 프린터 = 0)

이러한 방식으로 키보드, 프린터, 터미널까지도 하나의 “파일”처럼 접근할 수 있게 된 것입니다.
 

일반 파일과 특수 파일 차이점

항목 일반 파일 특수 파일
i-node가 가리키는 것 데이터 블록 주소 장치 타입 + 디바이스 번호 (2바이트)
실제 저장 위치 디스크 없음 (장치와 연결됨)
read/write 의미 디스크에서 읽고 쓰기 - (파일 데이터) 장치로(프린터 등) 입출력 - (장치 드라이버)
예시 /home/user/note.txt /dev/tty, /dev/sda1, /dev/null

 

특수 파일의 기능

일반 파일은 말 그대로 파일로서 데이터를 저장합니다. 그 자체가 내용을 가지고 있으며, 디스크 블록에 실제 데이터를 기록합니다.
하지만 특수 파일은 데이터를 저장하지 않습니다. 대신, 장치(프린터, 터미널, 디스크 등)와 연결된 통신 경로 역할을 수행합니다.
 

특수 파일 예시

프린터를 사용하는 상황을 가정해봅시다. UNIX에서는 프린터 장치도 다음과 같은 특수 파일로 표현됩니다.

/dev/lp0   ← 첫 번째 프린터 (line printer)
/dev/lp1   ← 두 번째 프린터

사용자는 프린터에 직접 명령을 내리지 않고도, 마치 파일에 데이터를 쓰듯 아래와 같이 명령을 실행합니다.

echo "Hello, printer!" > /dev/lp0

이 명령은 정말 프린터와 통신을 하는 걸까요?
정답은 그렇다입니다. 하지만 간접적으로, 파일 시스템 인터페이스를 통해 이루어집니다.

  • /dev/lp0는 실제 파일이 아니라, 프린터 장치를 가리키는 i-node 기반의 인터페이스
  • write() 시스템 콜을 통해 전달된 문자열은 커널 내부에서 프린터 드라이버에 전달
  • 드라이버는 해당 내용을 프린터 하드웨어로 전송하여 출력 수행

이 외에도 다양한 특수 파일들이 있습니다.

특수 파일 경로 의미
/dev/tty 현재 터미널. 여기에 write하면 화면에 출력됨
/dev/null 쓰면 사라지고, 읽으면 아무 것도 없음 (EOF)
/dev/sda1 디스크 장치. 여기에 파일 시스템이 설치됨
/dev/zero 무한한 0 바이트를 제공하는 가상 장치

 
이러한 구조 덕분에 UNIX는 모든 자원에 대해 일관된 인터페이스를 유지할 수 있고,
장치 입출력을 일반 파일 입출력처럼 쉽게 다룰 수 있습니다.
 
macOs 이신 분들은, echo "hello" > /dev/tty 명령어를 입력하면 터미널에 "hello"가 출력되는 것을 확인할 수 있습니다. 
 

4.6 파일 시스템 구조와 i-node 요약

항목 설명
디렉토리와 i-node 디렉토리는 (파일 이름, i-number) 리스트일 뿐이며, 파일의 실제 정보는 i-node가 관리함
i-node 정보 소유자, 접근 권한, 디스크 블록 주소, 크기, 수정 시각, 열견된 링크 수, 디렉토리 여부, 특수 파일 여부, 크기 구분
파일 열기 open() 시 경로를 따라가 i-number를 찾고, 해당 i-node를 열어 파일 디스크럽터(file descriptor)를 반환
파일 디스크럽터 이후 read()/write() 시 사용하는 핸들. 내부적으로 (장치, i-number, 포인터) 정보를 참조
파일 생성 새 i-node 할당 후 디렉토리에 (이름, i-number) 등록
링크 생성 새로운 디렉토리 엔트리에 기존 i-number 등록, i-node 링크 수 증가
파일 삭제 디렉토리 엔트리 제거 후 i-node 링크 수 감소 → 0이면 디스크 블록 및 i-node 제거
디스크 구조 파일은 512바이트 블록 단위로 저장됨. i-node는 최대 8개 블록 직접 참조, 그 이상은 간접 블록 사용
최대 파일 크기 직접 블록: 8(단일 슬롯=8블록: 블록 개수) * 512바이트 = 4KB,
단일 간접 블록: 1(단일 슬롯) * 256(블록 개수) * 512바이트 = 약 128KB
특수 파일 처리 장치도 파일처럼 다루며, i-node는 디바이스 타입과 번호만 저장. ex) 키보드, 프린터 등도 파일로 인식
mount 구현 특정 디렉토리에 장치를 연결하면 해당 i-number를 새로운 장치의 루트 i-number(1번)로 대체하여 경로 탐색을 전환함

5. 파일 시스템 계층 구조와 mount

앞서 살펴봤듯, UNIX의 파일 시스템은 모든 자원을 파일처럼 추상화하고, 그 파일은 i-node로 관리됩니다.
 
그렇다면 이런 파일들이 어떻게 디렉토리 구조로 연결되고, 다양한 디바이스를 하나의 트리로 통합할 수 있는 걸까요?그 중심에는 바로 계층적 디렉토리 구조mount 시스템 콜이 있습니다.
 

5.1 계층적 디렉토리 구조란?

UNIX는 파일을 단순히 나열하는 방식이 아니라, 트리(tree) 형태로 구성합니다.
가장 상위 디렉토리는 / (루트 디렉토리)이며, 모든 파일과 디렉토리는 여기에 연결된 하위 노드입니다.
 

예시

/
├── bin/
│   └── ls
├── home/
│   └── minseo/
│       ├── hello.txt
│       └── project/
│           └── main.c
└── dev/
    └── sda1
  • 모든 경로는 /부터 시작하는 절대 경로(absolute path) 입니다.
  • 각각의 디렉토리는 단순히 또 다른 파일의 목록입니다.
    그 목록은 (이름, i-number)의 쌍으로 이루어진 배열이며,
    해당 이름이 어떤 파일(i-node)과 연결되는지를 나타냅니다.

즉, 디렉토리는 '이름 → i-number'로 구성된 파일 시스템의 주소록입니다.
 

5.2 특수한 두 항목: ...

모든 디렉토리는 반드시 두 개의 특수 항목을 포함합니다.
 
. → 자기 자신
.. → 상위 디렉토리
 
이 덕분에 사용자는 cd ..으로 쉽게 상위 디렉토리로 이동할 수 있고,
파일 시스템도 경로를 추적하면서 돌아가는 것이 가능합니다.
 

5.3 mount: 다른 장치를 파일 시스템에 붙이기

지금까지 설명한 계층 구조는 단일 디스크 안에서만 유지되는 게 아닙니다.
UNIX는 여러 저장장치하나의 디렉토리 트리 아래에 붙여버리는 기능을 제공합니다.
이게 바로 mount 시스템 콜입니다.

 
예시

mount /dev/sdb1 /mnt/usb
  • /dev/sdb1 → 외장 USB 저장장치
  • /mnt/usb → 기존 루트 트리 안의 빈 디렉토리

위 명령은 USB 장치의 루트 디렉토리를 /mnt/usb 경로에 연결합니다.
이제 사용자는 /mnt/usb/photo.jpg처럼 기존 트리의 일부인 것처럼 접근할 수 있습니다.
 

5.4 mount의 내부 동작

  1. 사용자가 open("/mnt/usb/photo.jpg")를 요청하면,
  2. 커널은 /mnt/usb 디렉토리를 열며 해당 경로가 mount point인지 확인합니다.
  3. 만약 mount된 장치가 있다면
    •  
    • 디바이스명을 mount된 새로운 외부 장치로 교체
    • 현재 i-number를 루트 i-number(보통 1) 로 대체
  4. 이후부터는 해당 장치의 파일 시스템을 따라 경로를 해석합니다.

즉, mount는 다음과 같은 쌍을 시스템 내부에 저장해 둡니다.

(기존 장치, i-number = 123) → (새 장치, i-number = 1)

이 정보를 기반으로 경로 탐색 중에 자동으로 장치를 전환해주는 것입니다.
 

왜 i-number가 1로 대체될까?

모든 파일 시스템은 자기 내부적으로 루트 디렉토리의 i-number를 1번으로 고정합니다.
즉, 외부 장치를 mount했다면 그 안의 탐색을 시작할 기준은 (해당 장치, i-number 1) 이 되는 겁니다.

예시

mount 전:
  /home/minseo/usb  →  (내부 디스크, i-number 123)

mount 후:
  /home/minseo/usb  →  (USB 디스크, i-number 1)


→ 이제 이 경로는 내부 디스크가 아니라 USB 디스크의 루트 디렉토리를 가리키게 됩니다.

즉, mount된 디렉토리에 접근할 때는
원래 디렉토리의 i-number(예: 123)를 무시하고, mount된 장치의 루트 디렉토리 i-number(보통 1) 를 사용합니다.

 

외부 장치가 여러 개인 경우는?

만약 외부 장치인 USB가 한개가 아니라 여러개를 마운트하면 어떻게 될까요?
UNIX에서는 각 장치마다 독립된 i-node 테이블(i-list) 을 가지고 있기 때문에,
모든 장치에서 루트 디렉토리의 i-number는 1번입니다.

 
장치 루트 디렉토리의 i-number
/dev/sda1 1
/dev/sdb1 1
/dev/sdc1 1

 

→ 즉, 다른 장치 = 서로 다른 i-list
그래서 i-number는 장치 내에서만 고유하고
장치명이 다르기 때문에 시스템은 다음과 같이 구분합니다.

(/dev/sdb1, i-number 1)  → USB1의 루트
(/dev/sdc1, i-number 1)  → USB2의 루트

이처럼 mount 시스템 콜은 단순히 다른 디렉토리를 "끼워넣는" 것이 아니라,
경로 탐색의 시작점과 장치 정보를 바꾸는 매우 핵심적인 작업입니다.
 

5.5 사용자 입장에서의 장점

장치가 여러 개 있어도, 사용자 입장에서는 하나의 통합된 디렉토리 트리처럼 보입니다.
예를 들어 /, /home, /dev, /mnt, /media 등으로 구조가 나뉘어 있지만, 그 아래에는 내부 디스크, 외장 USB, 네트워크 드라이브(NFS), CD-ROM 등 다양한 저장소동일한 방식으로 연결(mount) 되어 있습니다.
 
즉, 사용자는 장치의 물리적 위치나 종류에 상관없이, 경로만 알면 언제든 일관된 방식으로 접근할 수 있습니다.
 

5.6 mount 후 consistency check: 일관성을 지키는 UNIX의 방식

파일 시스템은 단순한 파일 목록이 아니라, 수많은 블록, i-node, 디렉토리 구조가 얽힌 복잡한 데이터 구조입니다.
만약 디스크가 정상적으로 언마운트되지 않거나, 갑작스러운 전원 종료, 시스템 오류가 발생하면 파일 시스템의 일관성이 깨질 수 있습니다.
그렇기 때문에 UNIX에서는 파일 시스템을 mount할 때, 그 구조가 일관적인지 검사(consistency check) 하는 과정이 필요합니다.

 

파일 시스템이 깨진다는 건?

  • 파일을 지웠는데 디스크 블록은 여전히 사용 중인 상태로 남아 있음
  • 하나의 블록이 여러 파일에 의해 중복 참조됨
  • 링크 수는 2인데 실제 디렉토리에선 한 번만 나타남
  • 디렉토리가 존재하지만 가리키는 i-node가 없음

이런 오류가 쌓이면 결국 파일 유실, 디스크 공간 낭비, 시스템 충돌로 이어질 수 있습니다.

 

UNIX는 어떻게 검사할까?

UNIX는 모든 파일을 디렉토리 구조와 별개로 i-node 리스트(i-list) 를 통해 관리합니다.
이 덕분에 consistency check를 할 때 디렉토리를 일일이 순회할 필요 없이 i-list만 선형적으로 훑으며 빠르게 검사할 수 있습니다.

 
검사 항목 예시:

  • 모든 i-node가 가리키는 블록이 서로 겹치지 않는지
  • 유효한 블록 범위 내에서만 접근하는지
  • 각 i-node의 링크 수가 실제 디렉토리 등장 횟수와 일치하는지
  • 디스크에 남은 블록과 사용 중인 블록이 정확히 구분되어 있는지

macOS나 Linux에서는 fsck, fsck_hfs 등의 명령어로 수동 검사도 할 수 있습니다.

sudo fsck /dev/disk2s1
  • 시스템이 자동으로 실행하는 경우도 많지만,
  • 전원이 비정상적으로 꺼진 경우에는 수동으로 fsck를 돌려야 안전합니다.

 

5.7 mount의 철학적 의미

UNIX의 mount는 단순한 장치 연결이 아닙니다. 이질적인 공간을 하나의 논리적 트리로 통합한다는 철학입니다.

  • 디스크이든
  • 네트워크 드라이브이든
  • 심지어 특수한 장치 파일이든

→ 모두 파일처럼 접근할 수 있고
경로(path) 만 알면 언제든 동일한 방식으로 다룰 수 있습니다.
 

5.8 계층적 디렉토리 구조와 mount 요약

항목 설명
계층적 디렉토리 구조 UNIX는 / 루트를 기준으로 트리 형태의 디렉토리 구조를 가짐.
모든 경로는 /부터 시작하며, 트리의 각 노드는 파일 또는 디렉토리
특수 디렉토리 .: 현재 디렉토리,
..: 상위 디렉토리. 경로 탐색과 상대경로 처리에 사용됨
mount 개념 외부 장치(USB, CD, 네트워크 드라이브 등)를 특정 디렉토리(예: /mnt/usb)에 연결하여,
그 위치에서 다른 파일 시스템을 이어 붙이는 작업
mount 동작 원리 경로 탐색 중 mount point(i-number)를 만나면,
이를 해당 장치의 루트 i-node (번호 1) 로 대체하고 장치도 전환하여 탐색을 이어감
사용자 입장 여러 장치가 연결되어 있어도 마치 하나의 거대한 디렉토리처럼 보임.
/, /home, /dev, /mnt, /Volumes 등에 다양한 장치를 일관되게 통합
장치 간 i-number 중복 각 장치마다 별도의 i-node 테이블(i-list)을 가지므로,
모든 장치에서 루트 i-number는 1번이며. (장치가 다르면 중복 i-number 허용됨)
파일 고유 식별 방식 단순히 i-number만이 아니라 (장치 이름, i-number) 쌍으로 식별함
mount 후 경로 변화 예시 /home/minseo/usb의 i-number가 원래 123이었다면,
mount 후에는 (USB 장치, i-number 1)로 탐색 시작 지점이 바뀜
mount 후 consistency check 비정상적으로 언마운트된 장치는 손상 위험이 있어 mount 전 consistency check 필요.
UNIX에서 모든 파일은 디렉토리 처럼 트리 구조가 아니라, i-node list(i-list)로 관리 하므로,
i-list를 선형탐색하여 일관성 검사가 가능함
fsck 관련 유틸리티 macOS에서는 파일 시스템 종류에 따라 fsck_hfs, fsck_apfs 등을 사용.
검사 전에는 반드시 디스크를 unmount 해야 함

 


6. UNIX와 Linux 비교

UNIX 철학 요약

원칙 설명
1. 하나의 일에 집중하라 하나의 프로그램은 하나의 기능만 수행하고, 그것을 잘 하도록 설계한다.
2. 텍스트를 인터페이스로 사용하라 프로그램 간의 연결을 위해 사람이 읽을 수 있는 텍스트 형식을 사용한다.
3. 프로그램을 조합하라 여러 개의 작은 프로그램을 파이프(pipe)로 조합한다.
4. 빠르게 만들고 필요하면 버려라 완벽하게 시작하기보다 빠르게 만들고 개선하며, 필요하면 과감히 다시 만든다.
5. 상호작용보다는 자동화를 지향하라 사용자 입력을 요구하는 대신, 파일이나 명령어로 제어되도록 설계한다.

 

UNIX 철학 한줄 요약: 작은 도구와 단순한 시스템 콜을 조합해, 복잡한 작업을 간결하게 해결한다.

 

6.2 UNIX와 Linux 공통점

항목 설명
시분할 기반 멀티태스킹 여러 작업/사용자를 동시에 실행하는 것처럼 보이게 함 (시간 분할로 CPU 자원 배분)
시스템 콜 인터페이스 open(), read(), write(), fork(), exec() 등 거의 동일한 API 구조 사용
프로세스 관리 방식 fork()로 복제, exec()로 대체, wait()로 동기화하는 방식 동일
파일 시스템 구조 i-node 기반 구조 사용: 디렉토리는 이름과 i-number 매핑, 실제 정보는 i-node에 저장
장치 추상화 철학 "모든 것을 파일처럼 다룬다": /dev/tty, /dev/null 등 장치도 파일로 취급
계층적 디렉토리 구조 / 루트를 중심으로 하는 트리형 파일 시스템 구조
셸 환경 사용 텍스트 기반 명령어 인터페이스 사용 (sh, bash, 등)
유닉스 철학 계승
 

 

6. 3 UNIX와 Linux 차이점

항목 UNIX Linux
개발 주체 AT&T 벨 연구소 (1969년),
이후 상용 유닉스(AIX, HP-UX, Solaris 등)
리누스 토르발스 (1991년),
전 세계 커뮤니티 주도 오픈소스 개발
라이선스 폐쇄형, 상용 라이선스 (System V 기반) GPL (오픈소스), 누구나 사용·수정·배포 가능
커널 구조 모놀리식 커널, 정적 구성 모놀리식 커널이지만,
커널 모듈 동적 로딩 지원 (insmod, modprobe 등)
장치 드라이버 관리 정적으로 커널에 컴파일 장치 연결 시 자동 인식 및 모듈 로딩 (udev 시스템 등)
동적 메모리 관리 방식 sbrk() 기반 힙 확장 방식 sbrk() + mmap() 혼합 사용 (요청 크기에 따라 달라짐)
지원 플랫폼 주로 대형 서버/메인프레임 환경 데스크탑, 서버, 클라우드, 모바일,
IoT, 슈퍼컴퓨터 등 다양한 환경
배포 방식 단일 OS 형태 (AIX, Solaris 등) 다양한 배포판 존재 (Ubuntu, Debian, Fedora, Arch 등)
기본 셸 환경 초기에는 sh, csh 등 bash, zsh, fish, dash 등 다양하고 모던한 셸 지원
기술 발전 속도 비교적 느리고 안정성 중심 빠른 변화와 새로운 기술 반영 (컨테이너, systemd, cgroups 등)
사용자층 기업, 연구소 중심 일반 사용자부터 개발자, 서버 관리자, 교육, 임베디드까지 폭넓음

 

6.4 UNIX와 Linux 비교 요약

UNIX는 운영체제 철학과 구조의 뿌리를 제공한 선구자이며,
Linux는 이를 계승해 오픈소스 생태계와 현대 기술로 확장해 나간 진화형 운영체제입니다.


7. 참고자료

Ritchie & Thompson, The UNIX Time-Sharing System, CACM, 1974.
 
http://www.mykit.com/kor/ele/data_22/unix_.htm
 

'컴퓨터과학 > 운영체제' 카테고리의 다른 글

Zero-Copy (Kafka가 빠른 이유)  (6) 2025.05.31