flock vs fcntl - почему две блокировки одного файла могут вести себя совершенно по-разному
В Linux есть несколько механизмов блокировки файлов. Наиболее часто встречаются
На первый взгляд они делают одно и то же: не дают нескольким процессам одновременно работать с одним файлом.
Но внутри ядра это разные механизмы, поэтому блокировки могут вообще не замечать друг друга.
▪️
Простейший вариант:
Пока первый процесс держит блокировку, второй:
получит ошибку и сразу завершится из-за
Часто этот механизм используют для защиты cron-задач:
▪️
Здесь можно блокировать не только весь файл, но и конкретный диапазон байтов.
На уровне C:
Можно заблокировать первые 100 байт, а другой процесс при этом сможет работать с другим диапазоном.
▪️ Главная ловушка
Процесс A:
Процесс B использует
Они не обязаны конфликтовать.
Причина в том, что
То есть наличие:
не означает автоматически:
И наоборот.
▪️ Обе блокировки advisory
Ни
Если приложение вообще не проверяет блокировку, оно может спокойно сделать:
Поэтому механизм работает только тогда, когда все участники системы договариваются его соблюдать.
▪️ Есть ещё одна важная разница
Для
У
Из-за этого без понимания модели владения FD легко получить ситуацию, когда один
▪️ Практический вывод
Если нужно просто гарантировать, что cron или несколько экземпляров shell-скрипта не выполняются одновременно:
обычно достаточно.
Если приложение должно блокировать отдельные диапазоны файла или ему нужна POSIX-модель блокировок, используют
И главное: нельзя считать
BashTex📱 #bash #utils
В Linux есть несколько механизмов блокировки файлов. Наиболее часто встречаются
flock() и fcntl().На первый взгляд они делают одно и то же: не дают нескольким процессам одновременно работать с одним файлом.
Но внутри ядра это разные механизмы, поэтому блокировки могут вообще не замечать друг друга.
flock блокирует сам файлПростейший вариант:
flock /tmp/app.lock -c 'echo "working"; sleep 10'
Пока первый процесс держит блокировку, второй:
flock -n /tmp/app.lock -c 'echo "working"'
получит ошибку и сразу завершится из-за
-n.Часто этот механизм используют для защиты cron-задач:
flock -n /run/myjob.lock /usr/local/bin/myjob
fcntl работает через диапазоны файлаЗдесь можно блокировать не только весь файл, но и конкретный диапазон байтов.
На уровне C:
struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 100
};
fcntl(fd, F_SETLK, &lock);Можно заблокировать первые 100 байт, а другой процесс при этом сможет работать с другим диапазоном.
Процесс A:
flock /tmp/test.lock
Процесс B использует
fcntl() для того же файла.Они не обязаны конфликтовать.
Причина в том, что
flock и традиционные POSIX advisory locks через fcntl исторически используют разные пространства блокировок.То есть наличие:
flock → locked
не означает автоматически:
fcntl → blocked
И наоборот.
Ни
flock, ни fcntl сами по себе не запрещают процессу открыть и изменить файл.Если приложение вообще не проверяет блокировку, оно может спокойно сделать:
echo "data" >> /tmp/file
Поэтому механизм работает только тогда, когда все участники системы договариваются его соблюдать.
Для
flock блокировка связана с открытым file description. При fork() дочерний процесс наследует соответствующую блокировку.У
fcntl семантика другая: POSIX locks связаны с процессом и имеют собственные правила поведения при close().Из-за этого без понимания модели владения FD легко получить ситуацию, когда один
close() неожиданно освобождает блокировку.Если нужно просто гарантировать, что cron или несколько экземпляров shell-скрипта не выполняются одновременно:
flock -n /run/myjob.lock /usr/local/bin/myjob
обычно достаточно.
Если приложение должно блокировать отдельные диапазоны файла или ему нужна POSIX-модель блокировок, используют
fcntl().И главное: нельзя считать
flock и fcntl взаимозаменяемыми только потому, что оба называются file locking.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
madvise() - как процесс подсказывает ядру, как он собирается использовать память
Когда процесс работает с большой областью памяти, ядро не всегда знает, как именно приложение будет обращаться к этим страницам.
Linux предоставляет
Это особенно интересно для
▪️ Последовательный доступ
Если программа собирается читать память последовательно:
ядро получает подсказку, что страницы будут использоваться примерно по порядку.
Это позволяет иначе управлять page cache и упреждающим чтением.
Например, обработчик большого файла может пройти по нему от начала до конца вместо случайных обращений.
▪️ Случайный доступ
Для случайного доступа есть:
Если приложение читает страницы в произвольном порядке, агрессивное read-ahead может оказаться бесполезным.
Подсказка позволяет ядру учитывать такой сценарий.
▪️ Можно сообщить, что страницы больше не нужны
Особенно интересен:
Процесс говорит ядру, что содержимое этой области ему сейчас не требуется.
Для анонимной памяти это может позволить освободить физические страницы. Для файлового отображения поведение связано с отображёнными страницами и page cache.
Важно: это не то же самое, что
Виртуальный адрес всё ещё может оставаться отображённым, но физические страницы могут быть освобождены и восстановлены при следующем обращении.
▪️ Есть и подсказка
Она сообщает ядру, что страницы, вероятно, скоро понадобятся.
Это может помочь подготовить данные заранее, например перед обработкой большого memory-mapped файла.
Но
▪️ Практический сценарий
Представим программу, которая через
Вместо случайного поведения с page cache она может сделать:
Теперь приложение явно сообщает ядру характер будущего доступа.
А когда большой участок больше не нужен:
Это может уменьшить давление на память, не требуя немедленно уничтожать само отображение.
▪️ Важный нюанс
Эффект зависит от конкретного флага, типа mapping, версии ядра и сценария доступа.
И особенно важно не путать
То есть
BashTex📱 #bash #utils
Когда процесс работает с большой областью памяти, ядро не всегда знает, как именно приложение будет обращаться к этим страницам.
Linux предоставляет
madvise() - системный вызов, через который процесс может сообщить ядру предполагаемый характер использования памяти.Это особенно интересно для
mmap() и больших memory-mapped файлов.Если программа собирается читать память последовательно:
madvise(addr, length, MADV_SEQUENTIAL);
ядро получает подсказку, что страницы будут использоваться примерно по порядку.
Это позволяет иначе управлять page cache и упреждающим чтением.
Например, обработчик большого файла может пройти по нему от начала до конца вместо случайных обращений.
Для случайного доступа есть:
madvise(addr, length, MADV_RANDOM);
Если приложение читает страницы в произвольном порядке, агрессивное read-ahead может оказаться бесполезным.
Подсказка позволяет ядру учитывать такой сценарий.
Особенно интересен:
madvise(addr, length, MADV_DONTNEED);
Процесс говорит ядру, что содержимое этой области ему сейчас не требуется.
Для анонимной памяти это может позволить освободить физические страницы. Для файлового отображения поведение связано с отображёнными страницами и page cache.
Важно: это не то же самое, что
free().Виртуальный адрес всё ещё может оставаться отображённым, но физические страницы могут быть освобождены и восстановлены при следующем обращении.
WILLNEEDmadvise(addr, length, MADV_WILLNEED);
Она сообщает ядру, что страницы, вероятно, скоро понадобятся.
Это может помочь подготовить данные заранее, например перед обработкой большого memory-mapped файла.
Но
madvise() именно подсказывает, а не заставляет ядро выполнить конкретную стратегию.Представим программу, которая через
mmap() обрабатывает несколько гигабайт файла строго последовательно.Вместо случайного поведения с page cache она может сделать:
void *p = mmap(NULL, size,
PROT_READ,
MAP_PRIVATE,
fd, 0);
madvise(p, size, MADV_SEQUENTIAL);
Теперь приложение явно сообщает ядру характер будущего доступа.
А когда большой участок больше не нужен:
madvise(p, size, MADV_DONTNEED);
Это может уменьшить давление на память, не требуя немедленно уничтожать само отображение.
madvise() не является универсальной кнопкой «ускорить память».Эффект зависит от конкретного флага, типа mapping, версии ядра и сценария доступа.
И особенно важно не путать
MADV_DONTNEED с гарантированным физическим освобождением каждой страницы: это рекомендация ядру о том, что содержимое региона можно считать ненужным.То есть
madvise() - это интерфейс, через который userspace сообщает kernel memory manager: «я примерно знаю, что собираюсь делать с этой памятью».BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥1
name_to_handle_at() - как получить файловый объект без обычного пути
В Linux обычно обращаются к файлу через путь:
Но ядро умеет работать с файловым объектом иначе. Системный вызов
Это используется низкоуровневыми инструментами, которым нужно сохранить ссылку на объект, а не его текстовый путь.
▪️ Получаем handle
Сам системный вызов выглядит примерно так:
Например, программа может передать:
и получить структуру с handle и идентификатором mount.
Важно: это не файловый дескриптор. Handle не позволяет просто сделать
▪️ Зачем тогда он нужен
Основная идея - получить устойчивое представление объекта внутри filesystem, которое затем можно использовать для повторного обращения к нему.
Для этого существует парный системный вызов:
Схема получается такой:
То есть сначала filesystem выдаёт идентификатор объекта, а позже по нему можно снова получить FD.
▪️ Почему это отличается от обычного пути
Представим:
Путь зависит от directory entries. Файл могут переименовать:
Сам inode при этом остаётся тем же объектом.
File handle предназначен именно для работы с объектом filesystem, а не с конкретным именем в каталоге.
▪️ Но есть важное ограничение
Handle не является универсальным идентификатором любого файла на всех filesystem.
Его формат зависит от filesystem, а поддержка механизма должна быть реализована самой filesystem.
Кроме того,
▪️ Где это реально встречается
Механизм особенно интересен для низкоуровневых инструментов резервного копирования, мониторинга и работы с filesystem, где обычный путь может быть неудобен или недостаточно надёжен.
И главное:
Он просит конкретную filesystem предоставить handle для объекта, а затем этот handle может быть использован через соответствующий kernel API.
BashTex📱 #bash #utils
В Linux обычно обращаются к файлу через путь:
/etc/hosts
Но ядро умеет работать с файловым объектом иначе. Системный вызов
name_to_handle_at() позволяет получить специальный file handle, связанный с объектом filesystem.Это используется низкоуровневыми инструментами, которым нужно сохранить ссылку на объект, а не его текстовый путь.
Сам системный вызов выглядит примерно так:
int name_to_handle_at(
int dirfd,
const char *path,
struct file_handle *handle,
int *mount_id,
int flags
);
Например, программа может передать:
/etc/hosts
и получить структуру с handle и идентификатором mount.
Важно: это не файловый дескриптор. Handle не позволяет просто сделать
read().Основная идея - получить устойчивое представление объекта внутри filesystem, которое затем можно использовать для повторного обращения к нему.
Для этого существует парный системный вызов:
open_by_handle_at()
Схема получается такой:
path
↓
name_to_handle_at()
↓
file handle + mount ID
↓
open_by_handle_at()
↓
file descriptor
То есть сначала filesystem выдаёт идентификатор объекта, а позже по нему можно снова получить FD.
Представим:
/var/data/report
Путь зависит от directory entries. Файл могут переименовать:
mv /var/data/report /var/archive/report
Сам inode при этом остаётся тем же объектом.
File handle предназначен именно для работы с объектом filesystem, а не с конкретным именем в каталоге.
Handle не является универсальным идентификатором любого файла на всех filesystem.
Его формат зависит от filesystem, а поддержка механизма должна быть реализована самой filesystem.
Кроме того,
open_by_handle_at() требует соответствующих привилегий. Обычное приложение не получает возможность произвольно открывать объекты по filesystem handles.Механизм особенно интересен для низкоуровневых инструментов резервного копирования, мониторинга и работы с filesystem, где обычный путь может быть неудобен или недостаточно надёжен.
И главное:
name_to_handle_at() не «обходит filesystem по inode».Он просит конкретную filesystem предоставить handle для объекта, а затем этот handle может быть использован через соответствующий kernel API.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Ephemeral ports - почему заканчиваются исходящие TCP-порты
Когда приложение устанавливает исходящее TCP-соединение, порт выбирается автоматически. Это ephemeral port, временный исходный порт клиента.
Например:
Если приложение создаёт тысячи соединений, диапазон таких портов может стать узким местом.
▪️ Посмотреть доступный диапазон
В Linux его задаёт:
Например:
Это около 28 тысяч портов для исходящих соединений с одного локального IP.
▪️ Но количество соединений не всегда ограничено этим числом
TCP-соединение определяется не одним портом, а набором:
То есть:
это разные соединения.
Но тот же локальный порт теоретически может использоваться снова, если меняется destination tuple.
Поэтому несколько destination IP/портов значительно расширяют пространство комбинаций.
▪️ Как увидеть исходящие соединения
А чтобы посмотреть локальные порты:
Если приложение создаёт огромное количество короткоживущих TCP-соединений, дополнительно стоит посмотреть
▪️ Почему
После закрытия TCP-соединения локальный порт не всегда сразу можно использовать для нового соединения.
Это нужно для защиты TCP от старых сегментов предыдущего соединения.
При большом количестве коротких запросов можно получить ситуацию:
В итоге новое
▪️ Практический сценарий
Проблема часто появляется у прокси, API-клиентов, NAT-шлюзов и сервисов, которые вместо переиспользования соединений постоянно создают новые.
Поэтому сначала стоит проверить:
А уже потом решать проблему: расширять диапазон, использовать connection pooling или уменьшать количество коротких TCP-соединений.
Важно: увеличение
BashTex📱 #bash #utils
Когда приложение устанавливает исходящее TCP-соединение, порт выбирается автоматически. Это ephemeral port, временный исходный порт клиента.
Например:
10.0.0.10:49152 → 142.250.74.14:443
Если приложение создаёт тысячи соединений, диапазон таких портов может стать узким местом.
В Linux его задаёт:
cat /proc/sys/net/ipv4/ip_local_port_range
Например:
32768 60999
Это около 28 тысяч портов для исходящих соединений с одного локального IP.
TCP-соединение определяется не одним портом, а набором:
source IP
source port
destination IP
destination port
protocol
То есть:
10.0.0.10:50000 → 1.1.1.1:443
10.0.0.10:50001 → 1.1.1.1:443
это разные соединения.
Но тот же локальный порт теоретически может использоваться снова, если меняется destination tuple.
Поэтому несколько destination IP/портов значительно расширяют пространство комбинаций.
ss -tan state established
А чтобы посмотреть локальные порты:
ss -tan | awk 'NR>1 {print $4}' | sort | uniq -c | sort -nr | headЕсли приложение создаёт огромное количество короткоживущих TCP-соединений, дополнительно стоит посмотреть
TIME-WAIT:ss -tan state time-wait | wc -l
TIME_WAIT особенно неприятенПосле закрытия TCP-соединения локальный порт не всегда сразу можно использовать для нового соединения.
Это нужно для защиты TCP от старых сегментов предыдущего соединения.
При большом количестве коротких запросов можно получить ситуацию:
requests
↓
тысячи TCP connections
↓
close()
↓
TIME_WAIT
↓
исчерпание доступных комбинаций
В итоге новое
connect() начинает получать ошибки вроде:Cannot assign requested address
Проблема часто появляется у прокси, API-клиентов, NAT-шлюзов и сервисов, которые вместо переиспользования соединений постоянно создают новые.
Поэтому сначала стоит проверить:
ss -s
cat /proc/sys/net/ipv4/ip_local_port_range
ss -tan state time-wait | wc -l
А уже потом решать проблему: расширять диапазон, использовать connection pooling или уменьшать количество коротких TCP-соединений.
Важно: увеличение
ip_local_port_range не создаёт новые IP-адреса и не отменяет остальные ограничения TCP.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
/dev/tcp - сетевое соединение средствами Bash
В Bash есть малоизвестная возможность работать с TCP через псевдоустройства
Никакого
▪️ Проверить, доступен ли TCP-порт
Bash сам пытается выполнить TCP
▪️ Можно получить обычный файловый дескриптор
Теперь FD
То есть сетевой сокет можно использовать почти как файл.
▪️ Почему это удобно в скриптах
Например, простая проверка нескольких сервисов:
Это удобно для минимальных recovery/health-check скриптов, когда не хочется зависеть от
▪️ Но есть важный нюанс
Поэтому:
не покажет какой-то TCP-интерфейс.
И работает это именно как возможность Bash, а не универсальная функция любого
Например, в Bash:
а поведение другого shell может быть совершенно другим.
BashTex📱 #bash #TCP
В Bash есть малоизвестная возможность работать с TCP через псевдоустройства
/dev/tcp.Никакого
nc, telnet или отдельного сетевого клиента не требуется.if (echo > /dev/tcp/127.0.0.1/5432) 2>/dev/null; then
echo "порт открыт"
else
echo "соединение не установлено"
fi
Bash сам пытается выполнить TCP
connect() к указанному адресу.exec 3<>/dev/tcp/example.com/80
Теперь FD
3 связан с TCP-соединением:printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3
cat <&3
exec 3>&-
То есть сетевой сокет можно использовать почти как файл.
Например, простая проверка нескольких сервисов:
for port in 22 80 443 8080; do
if (echo > /dev/tcp/127.0.0.1/$port) 2>/dev/null; then
echo "$port: open"
else
echo "$port: closed"
fi
done
Это удобно для минимальных recovery/health-check скриптов, когда не хочется зависеть от
nc./dev/tcp - не настоящий файл устройства. Bash распознаёт специальный путь и превращает обращение к нему в сетевой системный вызов.Поэтому:
ls -l /dev/tcp
не покажет какой-то TCP-интерфейс.
И работает это именно как возможность Bash, а не универсальная функция любого
/bin/sh.Например, в Bash:
bash -c 'echo > /dev/tcp/127.0.0.1/22'
а поведение другого shell может быть совершенно другим.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Bridge FDB - как Linux bridge узнаёт, где находится MAC
Linux bridge не знает заранее, за каким интерфейсом находится конкретный MAC-адрес.
Он изучает это динамически, наблюдая за входящими кадрами.
Для этого используется FDB - Forwarding Database.
▪️ Сначала bridge учится
Допустим:
PC-A отправляет кадр с:
Bridge видит, что этот MAC пришёл через
он не может выбрать конкретный порт.
Поэтому кадр отправляется через все подходящие порты, кроме того, откуда он пришёл.
Это unknown unicast flooding.
Если MAC назначения уже есть в FDB:
bridge отправляет кадр только через eth1.
Получается:
▪️Запись не вечная
FDB содержит динамические записи, и они имеют lifetime.
Посмотреть подробнее:
Удалить конкретную запись можно, например:
После этого bridge снова должен будет изучить расположение MAC.
▪️Почему это важно
Если FDB постоянно заполняется, очищается или содержит неожиданные MAC, это уже может быть полезным диагностическим сигналом.
Например:
может показать, какой MAC bridge сейчас связывает с каждым портом.
А если один MAC начинает «прыгать» между интерфейсами, появляется MAC flapping. Такое бывает при петлях, неправильной коммутации или некоторых схемах виртуализации.
FDB - одна из причин, почему обычный Ethernet-коммутатор не рассылает каждый кадр на все порты: после обучения он знает, куда отправить unicast напрямую.
BashTex📱 #bash #linux
Linux bridge не знает заранее, за каким интерфейсом находится конкретный MAC-адрес.
Он изучает это динамически, наблюдая за входящими кадрами.
Для этого используется FDB - Forwarding Database.
Допустим:
eth0 ── PC-A
eth1 ── PC-B
PC-A отправляет кадр с:
src MAC = aa:aa:aa:aa:aa:aa
Bridge видит, что этот MAC пришёл через
eth0, и запоминает:aa:aa:aa:aa:aa:aa → eth0
Посмотреть таблицу:
bridge fdb show
▪️Что происходит с первым кадром
Если bridge ещё не знает MAC назначения:
PC-A → bridge → неизвестный MAC
он не может выбрать конкретный порт.
Поэтому кадр отправляется через все подходящие порты, кроме того, откуда он пришёл.
Это unknown unicast flooding.
Если MAC назначения уже есть в FDB:
aa:aa:aa:aa:aa:aa → eth1
bridge отправляет кадр только через eth1.
Получается:
unknown MAC
↓
flooding
↓
bridge learns MAC
↓
known MAC
↓
unicast
▪️Запись не вечная
FDB содержит динамические записи, и они имеют lifetime.
Посмотреть подробнее:
bridge -d fdb show
Удалить конкретную запись можно, например:
bridge fdb del aa:aa:aa:aa:aa:aa dev eth0
После этого bridge снова должен будет изучить расположение MAC.
▪️Почему это важно
Если FDB постоянно заполняется, очищается или содержит неожиданные MAC, это уже может быть полезным диагностическим сигналом.
Например:
bridge fdb show br br0
может показать, какой MAC bridge сейчас связывает с каждым портом.
А если один MAC начинает «прыгать» между интерфейсами, появляется MAC flapping. Такое бывает при петлях, неправильной коммутации или некоторых схемах виртуализации.
FDB - одна из причин, почему обычный Ethernet-коммутатор не рассылает каждый кадр на все порты: после обучения он знает, куда отправить unicast напрямую.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
xargs против while read: где быстрее и безопаснееОбе конструкции позволяют обработать список файлов или строк, но работают они по-разному. Из-за этого одна может быть заметно быстрее, а другая - безопаснее.
xargsДопустим, нужно удалить все
.log-файлы:find . -name "*.log" | xargs rm
xargs собирает несколько аргументов и передаёт их одной команде, поэтому вместо сотен запусков rm будет всего несколько.Для большого количества файлов разница в производительности может быть существенной.
По умолчанию
xargs разделяет вход по пробелам и переводам строки.Если встретится файл:
my file.log
или
backup
2025.log
команда отработает некорректно.
Безопасный вариант:
find . -name "*.log" -print0 | xargs -0 rm
Здесь разделителем становится символ
NULL, поэтому пробелы и спецсимволы больше не проблема.while readЕсли над каждой строкой нужно выполнить несколько действий:
find . -name "*.log" -print0 |
while IFS= read -r -d '' file; do
echo "Deleting: $file"
rm "$file"
done
Такой код легче расширять: добавить проверки, логирование, условия или обработку ошибок.
Для простого запуска одной команды обычно выигрывает
xargs.Для сложной логики, где каждая строка проходит несколько этапов обработки, удобнее использовать
while read.Многие используют
xargs и while read как взаимозаменяемые инструменты. На практике выбор зависит не только от скорости, но и от формата входных данных. Если есть вероятность встретить пробелы, переносы строк или необычные символы в именах файлов, безопасные варианты - xargs -0 или while read -d ''.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥2
openat2(): как Linux защищает от выхода за пределы каталога
Допустим, приложение должно читать файлы только из
На первый взгляд достаточно проверить путь. Но если внутри каталога есть символические ссылки, всё становится сложнее.
▪️ В чём проблема
Например, пользователь передаёт:
Приложение проверяет, что путь находится внутри
Но между проверкой и открытием файла кто-то может заменить один из каталогов символической ссылкой на
Получается классическая TOCTOU-проблема: проверили один объект, а открыли уже другой.
▪️ Что меняет openat2()
В Linux есть системный вызов
Например:
Здесь два важных ограничения:
▪️ Как открыть файл
Сначала приложение получает дескриптор каталога:
Затем открывает файл относительно него:
Теперь ядро проверяет ограничения непосредственно при разрешении пути.
Если один из компонентов окажется символической ссылкой, операция завершится ошибкой.
▪️ А если символические ссылки нужны?
Полностью запрещать их необязательно.
Можно использовать:
В этом режиме указанный каталог становится временным корнем для разрешения пути. Даже абсолютные пути и переходы через
Это полезно, когда приложению нужно работать с деревом файлов, не позволяя обращаться к объектам за его пределами.
▪️ Почему это важно
Обычная проверка строки пути не гарантирует безопасность: файловая система может измениться между проверкой и открытием.
BashTex📱 #bash #linux
Допустим, приложение должно читать файлы только из
/srv/uploads. Пользователь передаёт имя файла, а программа открывает его.На первый взгляд достаточно проверить путь. Но если внутри каталога есть символические ссылки, всё становится сложнее.
Например, пользователь передаёт:
documents/report.pdf
Приложение проверяет, что путь находится внутри
/srv/uploads, а затем вызывает open().Но между проверкой и открытием файла кто-то может заменить один из каталогов символической ссылкой на
/etc.Получается классическая TOCTOU-проблема: проверили один объект, а открыли уже другой.
В Linux есть системный вызов
openat2(), который позволяет задать ограничения прямо во время разрешения пути.Например:
struct open_how how = {
.flags = O_RDONLY,
.resolve = RESOLVE_BENEATH |
RESOLVE_NO_SYMLINKS
};Здесь два важных ограничения:
RESOLVE_BENEATH - запрещает разрешение пути за пределами указанного каталога.RESOLVE_NO_SYMLINKS - запрещает проходить через символические ссылки в любом компоненте пути.Сначала приложение получает дескриптор каталога:
int dirfd = open("/srv/uploads",
O_PATH | O_DIRECTORY);Затем открывает файл относительно него:
int fd = syscall(SYS_openat2,
dirfd,
"documents/report.pdf",
&how,
sizeof(how));
Теперь ядро проверяет ограничения непосредственно при разрешении пути.
Если один из компонентов окажется символической ссылкой, операция завершится ошибкой.
Полностью запрещать их необязательно.
Можно использовать:
.resolve = RESOLVE_IN_ROOT
В этом режиме указанный каталог становится временным корнем для разрешения пути. Даже абсолютные пути и переходы через
.. интерпретируются относительно него.Это полезно, когда приложению нужно работать с деревом файлов, не позволяя обращаться к объектам за его пределами.
Обычная проверка строки пути не гарантирует безопасность: файловая система может измениться между проверкой и открытием.
openat2() позволяет перенести ограничения на уровень ядра и избежать многих ошибок при работе с недоверенными путями. При этом это не полноценная песочница: ограничения действуют на конкретное открытие файла, а не на все действия процесса.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
coproc: как Bash общается с фоновым процессом в обе стороны
Обычно Bash запускает фоновый процесс через
Можно использовать временные файлы или именованные каналы, а можно обойтись встроенным
Он запускает процесс в фоне и создаёт два канала: один для записи в его stdin, другой для чтения stdout.
▪️ Запускаем фоновый процесс
Например, создадим простой обработчик строк:
Bash создаст массив
При этом основной скрипт продолжит выполняться.
▪️ Передаём данные и получаем ответ
Отправим строку обработчику:
Теперь прочитаем результат:
Получим:
Процесс не пришлось запускать повторно. Он продолжает работать и ждать следующие строки.
▪️ Зачем это нужно
Представим, что скрипт должен отправить тысячу запросов одной программе.
При обычном подходе:
Программа запускается заново для каждого элемента.
С
Это особенно полезно, если запуск программы дорогой или она поддерживает постоянный обмен сообщениями через stdin/stdout.
▪️ Есть важный нюанс
Не каждая программа сразу отправляет данные в stdout. Некоторые буферизуют вывод, когда он подключён к каналу, а не к терминалу.
В таком случае
Ещё одна особенность: дескрипторы
▪️ Как завершить работу
Закрываем канал записи и ждём завершения обработчика:
После EOF цикл внутри обработчика завершится самостоятельно.
BashTex📱 #bash #linux
Обычно Bash запускает фоновый процесс через
&, но передавать ему команды и получать ответы уже сложнее. Можно использовать временные файлы или именованные каналы, а можно обойтись встроенным
coproc.Он запускает процесс в фоне и создаёт два канала: один для записи в его stdin, другой для чтения stdout.
Например, создадим простой обработчик строк:
coproc WORKER {
while IFS= read -r line; do
printf 'Processed: %s\n' "$line"
done
}Bash создаст массив
WORKER с двумя файловыми дескрипторами:WORKER[0] # читаем stdout процесса
WORKER[1] # пишем в stdin процесса
При этом основной скрипт продолжит выполняться.
Отправим строку обработчику:
printf 'hello\n' >&"${WORKER[1]}"Теперь прочитаем результат:
IFS= read -r result <&"${WORKER[0]}"
echo "$result"Получим:
Processed: hello
Процесс не пришлось запускать повторно. Он продолжает работать и ждать следующие строки.
Представим, что скрипт должен отправить тысячу запросов одной программе.
При обычном подходе:
for item in "${items[@]}"; do
processor "$item"
doneПрограмма запускается заново для каждого элемента.
С
coproc можно один раз запустить обработчик, передавать ему данные и читать ответы через открытые каналы.Это особенно полезно, если запуск программы дорогой или она поддерживает постоянный обмен сообщениями через stdin/stdout.
Не каждая программа сразу отправляет данные в stdout. Некоторые буферизуют вывод, когда он подключён к каналу, а не к терминалу.
В таком случае
read может ждать ответ, хотя программа уже обработала запрос.Ещё одна особенность: дескрипторы
coproc не всегда доступны командам, запущенным как отдельные дочерние процессы. Для таких сценариев их можно предварительно продублировать через exec.Закрываем канал записи и ждём завершения обработчика:
exec {WORKER[1]}>&-
wait "$WORKER_PID"После EOF цикл внутри обработчика завершится самостоятельно.
coproc удобен, когда нужен постоянный двусторонний обмен с процессом без сокетов, временных файлов и повторных запусков программы.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥2
fsync() vs fdatasync(): когда данные действительно попадают на диск
Программа записала данные в файл,
Но если в этот момент отключить питание, часть данных может исчезнуть. Причина в том, что успешный
▪️ Куда попадают данные после write()
Обычно Linux сначала помещает изменения в page cache - область оперативной памяти, используемую для кеширования файлов.
Ядро самостоятельно решает, когда отправлять изменённые страницы на диск.
Это ускоряет работу, но создаёт промежуток между успешной записью и её сохранением на носителе.
▪️ Что делает fsync()
Вызов:
запрашивает запись изменённых данных файла и связанных с ним метаданных, необходимых для синхронизации.
Например, программа обновила файл конфигурации. Чтобы не полагаться на фоновый writeback, она может вызвать
Если вызов завершился успешно, ядро выполнило требуемую синхронизацию файла с устройством хранения.
▪️ Чем отличается fdatasync()
Этот вызов тоже синхронизирует данные, но не обязан записывать все изменения метаданных.
Например, изменение времени модификации файла само по себе не требует синхронизации через fdatasync().
Но если файл увеличился, новый размер необходимо сохранить: без него записанные данные могут оказаться недоступными после сбоя.
Поэтому fdatasync() не означает «записать только содержимое и забыть обо всём остальном».
Этого недостаточно, чтобы гарантировать сохранность самого переименования после сбоя.
Для этого обычно требуется дополнительно вызвать
И ещё: даже успешный
▪️ Почему это важно
Разница особенно важна для баз данных, журналов транзакций и приложений, которым нельзя терять подтверждённые записи.
BashTex📱 #bash #linux
Программа записала данные в файл,
write() вернул успешный результат. Кажется, всё сохранилось.Но если в этот момент отключить питание, часть данных может исчезнуть. Причина в том, что успешный
write() ещё не означает запись на физический носитель.Обычно Linux сначала помещает изменения в page cache - область оперативной памяти, используемую для кеширования файлов.
write()
↓
page cache
↓
writeback
↓
накопитель
Ядро самостоятельно решает, когда отправлять изменённые страницы на диск.
Это ускоряет работу, но создаёт промежуток между успешной записью и её сохранением на носителе.
Вызов:
fsync(fd);
запрашивает запись изменённых данных файла и связанных с ним метаданных, необходимых для синхронизации.
Например, программа обновила файл конфигурации. Чтобы не полагаться на фоновый writeback, она может вызвать
fsync() после записи.write(fd, data, size);
if (fsync(fd) == -1) {
perror("fsync");
}
Если вызов завершился успешно, ядро выполнило требуемую синхронизацию файла с устройством хранения.
fdatasync(fd);
Этот вызов тоже синхронизирует данные, но не обязан записывать все изменения метаданных.
Например, изменение времени модификации файла само по себе не требует синхронизации через fdatasync().
Но если файл увеличился, новый размер необходимо сохранить: без него записанные данные могут оказаться недоступными после сбоя.
Поэтому fdatasync() не означает «записать только содержимое и забыть обо всём остальном».
▪️Что быстрее?
fdatasync() потенциально быстрее, потому что может выполнять меньше операций с метаданными.
Но разница зависит от файловой системы, накопителя и характера записи. Иногда оба вызова приводят практически к одинаковой работе.
▪️Важный нюанс с rename()
Допустим, приложение безопасно обновляет конфигурацию:
1. Создаёт config.tmp
2. Записывает новые данные
3. Вызывает fsync(config.tmp)
4. Переименовывает в config.json
Этого недостаточно, чтобы гарантировать сохранность самого переименования после сбоя.
Для этого обычно требуется дополнительно вызвать
fsync() на файловом дескрипторе родительского каталога.И ещё: даже успешный
fsync() зависит от того, насколько корректно накопитель выполняет команды сброса кеша.write() подтверждает, что данные приняты системой, а не обязательно сохранены на носителе.fsync() синхронизирует данные и связанные метаданные файла, а fdatasync() может пропустить метаданные, не нужные для последующего чтения данных.Разница особенно важна для баз данных, журналов транзакций и приложений, которым нельзя терять подтверждённые записи.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3👍1