en
Feedback

Don't get caught by a cheater! Telemetrio finds and tags such channels 👉 If you want to see the tag, subscribe

BashTex | Linux

BashTex | Linux

Open in Telegram

Авторский канал для тех, кто хочет глубже погрузиться в мир Linux. Подойдет для разработчиков, системных администраторов и DevOps Реклама: @dad_admin

Show more
2 506
Subscribers
No data24 hours
-47 days
+130 days
Posts Archive
reflink vs обычное копирование: как создать копию файла за секунду Представьте, что нужно скопировать файл размером 50 ГБ. Обычный cp будет читать данные и записывать их в новый файл, а значит, операция потребует времени и дополнительного места на диске. Но некоторые файловые системы Linux умеют создавать копии почти мгновенно. Для этого используется механизм reflink. ▪️Как работает обычное копирование При стандартном копировании создаётся новый файл, в который записываются данные исходного:
cp —reflink=never disk.img disk-copy.img
В результате два файла занимают отдельные блоки на диске. Если исходный файл весит 50 ГБ, для полноценной копии обычно потребуется ещё примерно столько же места. ▪️Что меняет reflink Попробуем другой вариант:
cp —reflink=always disk.img disk-copy.img
На файловых системах с поддержкой reflink, например Btrfs и XFS с включённой соответствующей возможностью, копирование может завершиться практически мгновенно. Вместо дублирования содержимого файловая система создаёт новые метаданные, указывающие на уже существующие блоки данных. Получается:
disk.img ───────┐
                ├── общие блоки
disk-copy.img ──┘
При этом файлы остаются независимыми. ▪️Что произойдёт при изменении копии Допустим, мы изменили несколько мегабайт внутри disk-copy.img. Файловая система использует Copy-on-Write: для изменяемых участков выделяются новые блоки, а неизменённые продолжают использоваться совместно. Поэтому копия постепенно начинает занимать дополнительное место только по мере изменений. ▪️Как проверить экономию места Создадим тестовый файл:
dd if=/dev/urandom of=test.img bs=1M count=100
cp —reflink=always test.img copy.img
Сравним логические размеры:
ls -lh test.img copy.img
А затем посмотрим использование пространства:
du -h test.img copy.img
Важно: du показывает учтённые блоки файлов, но не всегда корректно отражает физическую экономию от совместно используемых экстентов. Для точного анализа нужны инструменты конкретной файловой системы. ▪️Чем reflink отличается от hard link Hard link - это два имени одного inode. Изменение содержимого через одно имя видно и через другое. Reflink создаёт отдельный inode, поэтому изменения одного файла не меняют содержимое второго. Именно поэтому reflink удобен для быстрых копий виртуальных дисков, больших файлов и тестовых окружений. BashTex 📱 #bash #linux

hash: почему Bash запускает команду не из того каталога Вы установили новую версию утилиты, проверили $PATH, но Bash продолжает запускать старую. При этом в другом терминале всё работает правильно. Причина может быть во встроенном механизме hash. Bash запоминает пути к уже найденным исполняемым файлам, чтобы не искать их заново при каждом запуске. ▪️Как Bash ищет команды Когда вы вводите:
mytool
Bash ищет подходящую команду. Если это внешняя программа, оболочка обычно проверяет каталоги из $PATH по порядку. После первого успешного поиска путь может попасть во внутреннюю хеш-таблицу. Посмотреть её содержимое:
hash
Или узнать сохранённый путь конкретной команды:
hash -t mytool
▪️Где возникает проблема Допустим, утилита сначала находилась здесь:
/usr/bin/mytool
Затем вы установили новую версию в:
/usr/local/bin/mytool
Если Bash уже запомнил старый путь, он может продолжать использовать его, даже когда новая программа находится раньше в порядке поиска. Это особенно заметно в долгоживущих сессиях. ▪️Как сбросить кеш Удалить запись для одной команды:
hash -d mytool
Или очистить всю таблицу:
hash -r
После этого Bash заново выполнит поиск внешней команды. Проверить все доступные варианты можно через:
type -a mytool
▪️Важный нюанс При изменении $PATH Bash обычно сам очищает таблицу путей. Поэтому проблема чаще возникает, когда содержимое каталогов изменилось, а сама переменная осталась прежней. Кроме того, hash не переопределяет shell-функции и встроенные команды Bash. Если имя занято функцией или alias, сначала нужно разобраться с порядком разрешения команд. ▪️Почему это важно При обновлении утилит, переключении версий программ и диагностике окружения недостаточно проверить только $PATH. Иногда Bash уже запомнил расположение исполняемого файла, и именно поэтому две одинаковые на первый взгляд сессии запускают разные версии программы. BashTex 📱 #bash #linux

BashTex 📱 #мем
BashTex 📱 #мем

Процесс открыл 50 тысяч файлов: как найти причину Приложение работает несколько дней, а потом внезапно начинает выдавать Too many open files. Перезапуск помогает, но через какое-то время ошибка возвращается. Возможно, процесс постепенно накапливает файловые дескрипторы. Причём это не обязательно обычные файлы: FD используются для сокетов, каналов, eventfd и других объектов. ▪️Сколько дескрипторов открыто Для начала узнаем PID процесса и посчитаем его FD:
ls -U /proc/$PID/fd | wc -l
Посмотреть установленный лимит:
grep 'Max open files' /proc/$PID/limits
Если количество FD постоянно растёт и приближается к лимиту, стоит выяснить, что именно процесс держит открытым. ▪️Смотрим, куда указывают дескрипторы
ls -l /proc/$PID/fd | head -30
В выводе могут встретиться:
15 -> /var/log/app.log
16 -> socket:[123456]
17 -> pipe:[123457]
18 -> anon_inode:[eventpoll]
Это помогает понять, что накапливается: файлы, сетевые соединения, каналы или внутренние объекты ядра. ▪️Что показывает fdinfo Для каждого открытого дескриптора Linux хранит дополнительную информацию:
cat /proc/$PID/fdinfo/15
Например:
pos:    4096
flags:  0100002
mnt_id: 27
ino:    123456
Здесь: • pos - текущая позиция в файле. • flags - флаги открытия и состояния FD в восьмеричном виде. • mnt_id - идентификатор монтирования. • ino - номер inode. Для некоторых объектов появляются дополнительные поля. Например, у epoll-дескриптора можно увидеть отслеживаемые FD, а у eventfd - значение счётчика. ▪️Как найти причину утечки Если подозреваем обычные файлы, можно посмотреть, какие пути встречаются чаще всего:
ls -l /proc/$PID/fd |
awk -F ' -> ' 'NF > 1 {print $2}' |
sort | uniq -c | sort -nr | head
Если сотни или тысячи дескрипторов указывают на один лог, файл или сокеты, это уже хорошая зацепка. Для наблюдения за ростом:
watch -n 2 "ls -U /proc/$PID/fd | wc -l"
▪️Важный нюанс Большое количество FD само по себе не означает утечку. Например, прокси может обслуживать десятки тысяч соединений одновременно. Проблема начинается, когда дескрипторы продолжают накапливаться после завершения соответствующих операций. И ещё: /proc/PID/fdinfo показывает состояние дескрипторов, но не объясняет, какая функция их открыла. Для поиска конкретного участка кода могут понадобиться strace, eBPF или профилирование приложения. BashTex 📱 #bash #linux

BASH_ENV: почему Bash-скрипт выполняет команды, которых в нём нет Допустим, вы запускаете обычный Bash-скрипт, но перед первой строкой его кода неожиданно выполняются дополнительные команды. При этом в самом скрипте нет ни source, ни вызовов сторонних файлов. Причина может быть в переменной окружения BASH_ENV. ▪️Как работает BASH_ENV Когда Bash запускается в неинтерактивном режиме, он проверяет переменную BASH_ENV. Если она задана, Bash раскрывает её значение как имя файла и выполняет его содержимое перед основным скриптом. Например, создадим файл:
echo 'echo "Loaded from BASH_ENV"' > /tmp/bash-init
Теперь запустим:
BASH_ENV=/tmp/bash-init bash -c 'echo "Main script"'
Результат:
Loaded from BASH_ENV
Main script
Команда из /tmp/bash-init выполнилась первой, хотя в основном скрипте её вообще нет. ▪️Зачем это нужно Механизм позволяет автоматически загружать функции, переменные и настройки для неинтерактивных запусков Bash. Например, можно определить общую функцию логирования, которая будет доступна нескольким скриптам. Но есть и обратная сторона: поведение программы начинает зависеть от внешнего окружения. ▪️Где возникает проблема Предположим, скрипт запускается из CI/CD или другого автоматизированного окружения. Если туда попала переменная:
export BASH_ENV=/some/init.sh
каждый подходящий запуск Bash может сначала выполнить этот файл. Причём внутри него могут находиться не только определения функций, но и обычные команды, изменяющие файлы или запускающие процессы. ▪️Как проверить Посмотреть текущее значение:
printf '%s\n' "${BASH_ENV-}"
Запустить Bash без этой переменной:
env -u BASH_ENV bash script.sh
А если нужно исключить влияние остальных переменных окружения, можно использовать env -i, явно передав необходимые значения. ▪️Важный нюанс BASH_ENV работает не во всех режимах запуска оболочки. Например, интерактивный Bash обычно использует ~/.bashrc, а при запуске в режиме sh действуют другие правила. Кроме того, Bash игнорирует BASH_ENV в привилегированном режиме. Поэтому при расследовании неожиданного поведения скрипта важно проверять не только его содержимое, но и способ запуска. BashTex 📱 #bash #linux

close_range(): как Linux закрывает тысячи файловых дескрипторов одним вызовом Когда процесс запускает дочернюю программу, вместе с ней могут оказаться открытыми файловые дескрипторы родителя: сокеты, файлы, каналы и другие ресурсы. Иногда это приводит к неожиданным проблемам. Например, дочерний процесс продолжает держать сокет открытым, хотя основная программа давно его закрыла. ▪️Почему обычного close() недостаточно Допустим, приложение открыло несколько тысяч дескрипторов. Перед запуском другой программы нужно закрыть все лишние. Классический подход - пройтись по диапазону:
for (int fd = 3; fd < max_fd; fd++) {
    close(fd);
}
Но если максимальный номер дескриптора большой, программа выполнит множество системных вызовов, в том числе для уже закрытых FD. ▪️Как работает close_range() Начиная с Linux 5.9 доступен системный вызов:
close_range(3, ~0U, 0);
Он закрывает все открытые дескрипторы начиная с номера 3. При этом стандартные потоки остаются доступными:
0 → stdin
1 → stdout
2 → stderr
Ядро обрабатывает диапазон за один системный вызов, без необходимости вызывать close() для каждого номера отдельно. ▪️А что происходит при fork() и exec()? Здесь есть важное различие. После fork() дочерний процесс наследует копии дескрипторов родителя. Они ссылаются на те же открытые файловые объекты. При execve() дескрипторы по умолчанию сохраняются, если для них не установлен флаг FD_CLOEXEC. Поэтому случайно унаследованный FD может пережить даже запуск совершенно другой программы. ▪️Можно не закрывать, а установить CLOEXEC У close_range() есть дополнительный режим:
close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);
Он устанавливает FD_CLOEXEC для дескрипторов в диапазоне. В результате они останутся открытыми до execve(), но будут автоматически закрыты при успешной замене образа процесса. Это удобно, когда нужно подготовить окружение дочерней программы. ▪️Почему это важно Лишние унаследованные дескрипторы могут удерживать файлы, мешать закрытию соединений и давать дочерним программам доступ к ресурсам, которые им не нужны. close_range() позволяет быстро закрыть целый диапазон FD или подготовить их к закрытию при execve(). Но есть нюанс: в многопоточном процессе таблица дескрипторов может быть общей для нескольких потоков. Если нужна отдельная таблица перед изменением FD, предусмотрен флаг CLOSE_RANGE_UNSHARE. BashTex 📱 #bash #linux

O_APPEND: почему несколько процессов могут писать в один файл Допустим, два процесса одновременно записывают события в один лог. Оба открывают файл, находят его конец и добавляют новую строку. Но что произойдёт, если они сделают это одновременно? Один процесс может перезаписать данные другого. В Linux для таких случаев есть флаг O_APPEND. ▪️Что происходит без O_APPEND Предположим, оба процесса выполняют:
lseek(fd, 0, SEEK_END);
write(fd, data, size);
Между этими вызовами другой процесс может изменить файл. Получается ситуация:
Process A → lseek() → offset 100
Process B → lseek() → offset 100
Process A → write()
Process B → write()
Оба процесса начинают запись с одной позиции. Часть данных может быть перезаписана. ▪️Как работает O_APPEND Открываем файл с дополнительным флагом:
int fd = open("app.log",
              O_WRONLY | O_APPEND);
Теперь перед каждым write() ядро устанавливает позицию записи в конец файла. Причём выбор позиции и сама запись выполняются как одна атомарная операция относительно конкурирующих записей. Поэтому два процесса не смогут одновременно выбрать один и тот же конец файла и перезаписать друг друга из-за этой гонки. ▪️Можно проверить через Bash Оператор >> открывает файл в режиме добавления:
echo "First event" >> app.log
Запустим несколько фоновых процессов:
for i in {1..100}; do
    printf 'Event %s\n' "$i" >> app.log &
done

wait
Каждый printf открывает файл с O_APPEND, поэтому записи не должны перезаписывать друг друга из-за совпавшей позиции. ▪️Но есть ограничение O_APPEND не превращает последовательность нескольких write() в одну атомарную транзакцию. Если приложение записывает сообщение частями, между ними могут появиться данные другого процесса. Кроме того, на некоторых сетевых файловых системах, например NFS, добавление в конец может реализовываться неатомарно. ▪️Почему это важно Для логов, журналов событий и других файлов, в которые одновременно пишут несколько процессов, O_APPEND устраняет распространённую гонку между поиском конца файла и записью. Но если требуется гарантировать целостность целого сообщения, состоящего из нескольких операций, одного O_APPEND недостаточно. BashTex 📱 #bash #linux

BashTex 📱 #мем
BashTex 📱 #мем

fsync() vs fdatasync(): когда данные действительно попадают на диск Программа записала данные в файл, write() вернул успешный результат. Кажется, всё сохранилось. Но если в этот момент отключить питание, часть данных может исчезнуть. Причина в том, что успешный write() ещё не означает запись на физический носитель. ▪️Куда попадают данные после write() Обычно Linux сначала помещает изменения в page cache - область оперативной памяти, используемую для кеширования файлов.
write()
   ↓
page cache
   ↓
writeback
   ↓
накопитель
Ядро самостоятельно решает, когда отправлять изменённые страницы на диск. Это ускоряет работу, но создаёт промежуток между успешной записью и её сохранением на носителе. ▪️Что делает fsync() Вызов:
fsync(fd);
запрашивает запись изменённых данных файла и связанных с ним метаданных, необходимых для синхронизации. Например, программа обновила файл конфигурации. Чтобы не полагаться на фоновый writeback, она может вызвать fsync() после записи.
write(fd, data, size);

if (fsync(fd) == -1) {
    perror("fsync");
}
Если вызов завершился успешно, ядро выполнило требуемую синхронизацию файла с устройством хранения. ▪️Чем отличается fdatasync()
fdatasync(fd);
Этот вызов тоже синхронизирует данные, но не обязан записывать все изменения метаданных. Например, изменение времени модификации файла само по себе не требует синхронизации через fdatasync(). Но если файл увеличился, новый размер необходимо сохранить: без него записанные данные могут оказаться недоступными после сбоя. Поэтому fdatasync() не означает «записать только содержимое и забыть обо всём остальном».
▪️Что быстрее?

fdatasync() потенциально быстрее, потому что может выполнять меньше операций с метаданными.

Но разница зависит от файловой системы, накопителя и характера записи. Иногда оба вызова приводят практически к одинаковой работе.

▪️Важный нюанс с rename()

Допустим, приложение безопасно обновляет конфигурацию:

1. Создаёт config.tmp
2. Записывает новые данные
3. Вызывает fsync(config.tmp)
4. Переименовывает в config.json
Этого недостаточно, чтобы гарантировать сохранность самого переименования после сбоя. Для этого обычно требуется дополнительно вызвать fsync() на файловом дескрипторе родительского каталога. И ещё: даже успешный fsync() зависит от того, насколько корректно накопитель выполняет команды сброса кеша. ▪️Почему это важно write() подтверждает, что данные приняты системой, а не обязательно сохранены на носителе. fsync() синхронизирует данные и связанные метаданные файла, а fdatasync() может пропустить метаданные, не нужные для последующего чтения данных. Разница особенно важна для баз данных, журналов транзакций и приложений, которым нельзя терять подтверждённые записи. BashTex 📱 #bash #linux

coproc: как Bash общается с фоновым процессом в обе стороны Обычно 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 📱 #bash #linux

openat2(): как Linux защищает от выхода за пределы каталога Допустим, приложение должно читать файлы только из /srv/uploads. Пользователь передаёт имя файла, а программа открывает его. На первый взгляд достаточно проверить путь. Но если внутри каталога есть символические ссылки, всё становится сложнее. ▪️В чём проблема Например, пользователь передаёт:
documents/report.pdf
Приложение проверяет, что путь находится внутри /srv/uploads, а затем вызывает open(). Но между проверкой и открытием файла кто-то может заменить один из каталогов символической ссылкой на /etc. Получается классическая TOCTOU-проблема: проверили один объект, а открыли уже другой. ▪️Что меняет openat2() В 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 📱 #bash #linux

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 📱 #bash #linux

BashTex 📱 #мем
BashTex 📱 #мем

Bridge FDB - как Linux bridge узнаёт, где находится MAC Linux bridge не знает заранее, за каким интерфейсом находится конкретный MAC-адрес. Он изучает это динамически, наблюдая за входящими кадрами. Для этого используется FDB - Forwarding Database. ▪️Сначала bridge учится Допустим:
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 📱 #bash #linux

/dev/tcp - сетевое соединение средствами Bash В Bash есть малоизвестная возможность работать с TCP через псевдоустройства /dev/tcp. Никакого nc, telnet или отдельного сетевого клиента не требуется. ▪️Проверить, доступен ли TCP-порт
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 📱 #bash #TCP

Ephemeral ports - почему заканчиваются исходящие TCP-порты Когда приложение устанавливает исходящее 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 📱 #bash #utils

name_to_handle_at() - как получить файловый объект без обычного пути В Linux обычно обращаются к файлу через путь:
/etc/hosts
Но ядро умеет работать с файловым объектом иначе. Системный вызов name_to_handle_at() позволяет получить специальный file handle, связанный с объектом filesystem. Это используется низкоуровневыми инструментами, которым нужно сохранить ссылку на объект, а не его текстовый путь. ▪️Получаем handle Сам системный вызов выглядит примерно так:
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 📱 #bash #utils

madvise() - как процесс подсказывает ядру, как он собирается использовать память Когда процесс работает с большой областью памяти, ядро не всегда знает, как именно приложение будет обращаться к этим страницам. 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(). Виртуальный адрес всё ещё может оставаться отображённым, но физические страницы могут быть освобождены и восстановлены при следующем обращении. ▪️Есть и подсказка WILLNEED
madvise(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 📱 #bash #utils

flock vs fcntl - почему две блокировки одного файла могут вести себя совершенно по-разному В 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
И наоборот. ▪️Обе блокировки advisory Ни 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 📱 #bash #utils

DEBUG trap - как Bash выполняет код перед каждой командой В Bash есть специальный DEBUG trap, который позволяет выполнить собственный код непосредственно перед выполнением команды. Это удобно не только для отладки. Через него можно посмотреть, какие команды реально выполняет скрипт, с какими аргументами и в каком контексте. ▪️Самый простой пример
trap 'echo "CMD: $BASH_COMMAND"' DEBUG

echo "hello"
mkdir /tmp/test
rm -rf /tmp/test
Перед каждой командой Bash вызовет trap:
CMD: echo "hello"
CMD: mkdir /tmp/test
CMD: rm -rf /tmp/test
$BASH_COMMAND содержит команду, которую Bash собирается выполнить. ▪️Можно добавить PID и функцию
trap 'printf "[%s] %s: %s\n" "$$" "${FUNCNAME[1]:-main}" "$BASH_COMMAND"' DEBUG
Теперь при выполнении функций можно получить что-то вроде:
[4217] main: prepare
[4217] deploy: mkdir -p /srv/app
[4217] deploy: cp app.conf /srv/app/
Это уже превращается в простой трассировщик Bash-скрипта. ▪️Но DEBUG не означает буквально «перед каждой строкой» Trap срабатывает перед выполнением простых команд, for, case, некоторых условных конструкций и других элементов shell execution. Поведение также зависит от того, включено ли наследование trap внутри функций и subshell. Например:
trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

foo() {
    echo "inside"
}

foo
Чтобы DEBUG распространялся внутрь функций, часто используют:
set -T
или:
shopt -s functrace
▪️У trap есть интересный побочный эффект Сам код внутри DEBUG тоже выполняется Bash, поэтому слишком сложный обработчик может сам стать источником неожиданных эффектов. Для диагностики лучше держать его простым:
trap 'printf "%s\n" "$BASH_COMMAND" >> /tmp/bash-debug.log' DEBUG
А после диагностики обязательно убрать:
trap - DEBUG
▪️Практический сценарий Есть большой deployment-скрипт, который запускает десятки функций и команд. Лог показывает только:
deployment failed
Вместо того чтобы расставлять echo по всему скрипту, временно включаем:
trap 'printf "[%s] %s\n" "$SECONDS" "$BASH_COMMAND"' DEBUG
и получаем последовательность реально выполнявшихся команд. Важно: DEBUG предназначен прежде всего для трассировки. Для полноценного production-аудита он неудобен: trap может влиять на поведение скрипта и генерировать очень много вывода. BashTex 📱 #bash #utils

BashTex | Linux - Statistics & analytics of Telegram channel @bash_tex