Hard links и symlinks: ln
Linux позволяет дать файлу два имени — это link (ссылка). Есть два типа: hard link и symbolic link (symlink). Они работают по-разному, и понимание разницы критично для Junior DE: вы будете использовать симлинки для версионирования (current -> 2026-05-13), для хитрого мердж конфигов, для дедупликации в архивах.
В этом уроке разберём оба типа, с механикой на уровне inode.
Inode — что хранится про файл
Inode: структура метаданных файла на уровне ядраПеред links нужно понять inode. В Linux каждый файл — это:
- Inode (index node) — структура с метаданными: размер, права, владелец, временные метки, указатели на блоки данных.
- Имя файла — запись в директории, которая мапит «имя -> inode-номер».
То есть, файл и имя — это отдельно. Имя — это «бирка», прикреплённая к inode.
Inode-номер можно увидеть:
$ ls -i
12345 report.csv
12346 notes.txt
67890 projects/
$ stat report.csv
File: report.csv
Size: 10240 Blocks: 24 IO Block: 4096 regular file
Device: 8,1 Inode: 12345 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ user) Gid: ( 1000/ user)
Access: 2026-05-13 14:00:00
Modify: 2026-05-13 14:00:00
Change: 2026-05-13 14:00:00
Links: 1 — это количество имён, которые ссылаются на этот inode. По умолчанию — 1.
Hard link: второе имя для inode
ln source target создаёт hard link — второе имя для того же inode:
$ echo "original content" > original.txt
$ ls -i original.txt
12345 original.txt
$ ln original.txt copy.txt
$ ls -i
12345 copy.txt 12345 original.txt
# Один и тот же inode!
$ stat original.txt
...
Inode: 12345 Links: 2 # ← теперь 2
$ cat copy.txt
original content # тот же контент
$ echo "modified" >> copy.txt
$ cat original.txt
original content
modified # ← изменение через copy.txt видно в original.txt
Это потому, что физически это один файл с двумя именами. Блоки на диске — одни.
Удаление hard link
Когда удаляете один из hard links (rm copy.txt), inode НЕ удаляется — Links декрементится с 2 до 1. Файл остаётся доступен через другое имя.
Inode удаляется только когда Links достигает 0 (все имена удалены) И ни один процесс не держит файл открытым.
$ ln original.txt copy.txt
$ stat copy.txt | grep Links
Links: 2
$ rm copy.txt
$ stat original.txt | grep Links
Links: 1
$ cat original.txt
original content
modified # все данные на месте
Ограничения hard links
- Только в пределах одного filesystem. Hard link не может пересекать mount points (потому что inode-номера разные на разных fs).
- Только для файлов, не для директорий. Запрещено системой (иначе можно создать циклы в графе FS).
- Размер не дублируется. Это главная фишка: 100GB файл с hard link не съест 200GB.
# Попытка создать hard link на директорию
$ ln /etc/ /tmp/etc-link
ln: /etc/: hard link not allowed for directory
# Между разными fs — не работает
$ ln /home/user/file.txt /tmp/file.txt # если /tmp и /home на разных fs
ln: failed to create hard link: Invalid cross-device link
Hard links и symlinks на уровне ядра: link(), symlink(), unlink()
Symbolic link (symlink)
Symbolic link — это файл-указатель на путь. Не на inode, а на путь (строку).
$ ln -s original.txt symlink.txt
$ ls -la
-rw-r--r-- 1 user user 25 May 13 14:00 original.txt
lrwxrwxrwx 1 user user 12 May 13 14:00 symlink.txt -> original.txt
# l в начале — это symlink
# -> показывает путь, на который указывает
Inode разные:
$ ls -i
12345 original.txt
67890 symlink.txt # Свой inode
Symlink — это маленький файл, содержащий имя цели. Когда программа обращается к symlink, ядро резолвит цель и идёт по ней.
Работа с symlinks
# Создать symlink
$ ln -s /var/data/raw/2026-05-13 ~/current-data
# Содержимое цели доступно через имя symlink
$ ls ~/current-data
file1.csv file2.csv
# Что внутри symlink (на что указывает)
$ readlink ~/current-data
/var/data/raw/2026-05-13
# Полностью развернуть (для chains)
$ readlink -f ~/current-data
/var/data/raw/2026-05-13 # после резолва всех вложенных symlinks
Что происходит при удалении цели
$ ln -s original.txt symlink.txt
$ cat symlink.txt
original content
$ rm original.txt
$ cat symlink.txt
cat: symlink.txt: No such file or directory
# Symlink остался, но цель пустая -> битый symlink
$ ls -la symlink.txt
lrwxrwxrwx 1 user user 12 May 13 14:00 symlink.txt -> original.txt
# Symlink на месте, но битый
«Битый» symlink (dangling symlink) — это symlink, цель которого больше не существует. Их можно найти:
$ find /etc -xtype l # все битые symlinks
Hard link vs Symbolic link — сравнение
DE-сценарии для symlinks
Сценарий 1: версионирование current
Классика: ваш ETL генерирует данные в datestamp-папки. Приложение должно читать «текущие» данные — но имя папки меняется ежедневно.
# Структура
/var/data/2026-05-11/
/var/data/2026-05-12/
/var/data/2026-05-13/ # сегодня
/var/data/current -> 2026-05-13 # symlink
# Приложение читает /var/data/current/file.csv
# Не нужно знать дату — symlink резолвится в текущую
ETL-скрипт после успешной генерации обновляет symlink атомарно:
$ ln -sf "/var/data/$(date +%Y-%m-%d)" /var/data/current
-f (force) — перезаписать, если symlink уже существует. Это атомарная операция — приложение не увидит «промежуточного» состояния.
Сценарий 2: версии бинарников
Python, Node.js, Java — у вас часто несколько версий:
$ ls -la /usr/bin/python*
lrwxrwxrwx 1 root root 9 May 1 10:00 /usr/bin/python -> python3
lrwxrwxrwx 1 root root 11 May 1 10:00 /usr/bin/python3 -> python3.13
-rwxr-xr-x 2 root root 5M May 1 10:00 /usr/bin/python3.13
python -> python3 -> python3.13. Можно «переключиться» на python3.12, переставив symlink:
$ sudo ln -sf /usr/bin/python3.12 /usr/bin/python3
(На самом деле в Ubuntu используется update-alternatives для этого, но идея — symlinks.)
Сценарий 3: dotfiles
DE-разработчики часто держат свои конфиги в git-репо (~/dotfiles/) и symlink-ят их в HOME:
$ ls ~ -la | head
.bashrc -> /home/user/dotfiles/.bashrc
.vimrc -> /home/user/dotfiles/.vimrc
.config/starship.toml -> /home/user/dotfiles/.config/starship.toml
Менеджеры типа stow это автоматизируют:
$ cd ~/dotfiles
$ stow bash # создаст symlinks для всех файлов из ~/dotfiles/bash в ~/
DE-сценарии для hard links
Hard links используются реже, но есть характерные сценарии:
Сценарий 1: дедупликация в архивах
rsync --link-dest: incremental backups через hard links.
# Первый бэкап — полный
$ rsync -a /var/data/ /backup/2026-05-11/
# Следующий — только изменённые файлы, остальные как hard links
$ rsync -a --link-dest=/backup/2026-05-11 /var/data/ /backup/2026-05-12/
В /backup/2026-05-12/ все НЕизменённые файлы — это hard links на файлы из 2026-05-11. Disk usage минимален: только изменения занимают новое место. Каждый день — «полный» бэкап, но компактный.
Подробнее в модуле 12 про rsync.
Сценарий 2: атомарная замена файла
Атомарная замена через ln + rename:
# Прочитать конфиг, обновить, написать атомарно
$ cp /etc/myapp.conf /tmp/myapp.conf.new
$ vim /tmp/myapp.conf.new
$ mv /tmp/myapp.conf.new /etc/myapp.conf # rename — атомарная операция
mv под капотом — это rename(), который атомарен на одном fs. Это значит, что любой процесс, читающий /etc/myapp.conf, либо увидит старую версию полностью, либо новую — но не половинку.
Это важно для production: вы не хотите, чтобы сервис прочитал недописанный конфиг и упал.
Опции ln
$ ln source target # hard link
$ ln -s source target # symbolic link
$ ln -sf source target # force (перезаписать существующий)
$ ln -v source target # verbose
$ ln -r -s source target # relative path в symlink (вместо абсолютного)
-r особенно полезно — создаёт symlink с относительным путём:
# Без -r — абсолютный путь
$ ln -s /var/data/2026-05-13 /var/data/current
$ readlink /var/data/current
/var/data/2026-05-13 # абсолютный
# С -r — относительный
$ ln -sr /var/data/2026-05-13 /var/data/current
$ readlink /var/data/current
2026-05-13 # относительно расположения symlink
Относительные symlinks устойчивее к перемещению: если папку /var/data переместить, абсолютные symlinks сломаются, а относительные продолжат работать (потому что они «соседи»).
Попробуй сам
$ mkdir -p ~/linux-sandbox/lesson-links
$ cd ~/linux-sandbox/lesson-links
# Создание hard link
$ echo "original" > file1.txt
$ ls -li file1.txt
12345678 -rw-r--r-- 1 user user 9 May 13 14:00 file1.txt
$ ln file1.txt file2-hard.txt
$ ls -li
12345678 -rw-r--r-- 2 user user 9 May 13 14:00 file1.txt
12345678 -rw-r--r-- 2 user user 9 May 13 14:00 file2-hard.txt
# Same inode, Links count = 2
# Изменение через одно имя видно в другом
$ echo "modified" >> file1.txt
$ cat file2-hard.txt
original
modified
# Создание symlink
$ ln -s file1.txt file3-sym.txt
$ ls -li
12345678 -rw-r--r-- 2 user user 18 May 13 14:00 file1.txt
12345678 -rw-r--r-- 2 user user 18 May 13 14:00 file2-hard.txt
99999999 lrwxrwxrwx 1 user user 9 May 13 14:00 file3-sym.txt -> file1.txt
# Разный inode, тип l
$ cat file3-sym.txt
original
modified
# Удалить оригинал
$ rm file1.txt
# Hard link продолжает работать
$ cat file2-hard.txt
original
modified
# Symlink — нет (битый)
$ cat file3-sym.txt
cat: file3-sym.txt: No such file or directory
# Найти битые symlinks
$ find . -xtype l
./file3-sym.txt
DE-симуляция: версионирование current:
$ mkdir 2026-05-{11,12,13}
$ touch 2026-05-13/data.csv
$ ln -sf 2026-05-13 current
$ ls current/
data.csv
# Завтра — обновляем
$ mkdir 2026-05-14
$ touch 2026-05-14/data.csv
$ ln -sfn 2026-05-14 current # -n чтобы не создать current/2026-05-14 если current — папка
$ ls -la current
lrwxrwxrwx 1 user user 10 May 13 14:00 current -> 2026-05-14