ru
Feedback

Не попадитесь на канал ботавода! Telemetrio находит и помечает такие каналы 👉 Хотите видеть метку — оформите подписку

BashTex | Linux

BashTex | Linux

Открыть в Telegram

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

Больше
2 506
Подписчики
Нет данных24 часа
-47 дней
+130 дней

Загружаем данные

Похожие каналы
Нет данных
Возникли проблемы? Пожалуйста, обновите страницу или обратитесь к нашему support-менеджеру .
Входящие и исходящие упоминания
---
---
---
---
---
---
Привлечение подписчиков
окт. '26
октябрь '26
+24
в 1 каналах
сентябрь '26
+7
в 0 каналах
Get PRO
август '26
+54
в 11 каналах
Get PRO
июль '26
+16
в 1 каналах
Get PRO
июнь '26
+38
в 5 каналах
Get PRO
май '26
+8
в 0 каналах
Get PRO
апрель '26
+14
в 0 каналах
Get PRO
март '26
+22
в 0 каналах
Get PRO
февраль '26
+18
в 0 каналах
Get PRO
январь '26
+85
в 5 каналах
Get PRO
декабрь '25
+122
в 19 каналах
Get PRO
ноябрь '25
+93
в 2 каналах
Get PRO
октябрь '25
+107
в 7 каналах
Get PRO
сентябрь '25
+197
в 20 каналах
Get PRO
август '25
+16
в 1 каналах
Get PRO
июль '25
+130
в 13 каналах
Get PRO
июнь '25
+57
в 4 каналах
Get PRO
май '25
+70
в 1 каналах
Get PRO
апрель '25
+728
в 23 каналах
Get PRO
март '25
+129
в 6 каналах
Get PRO
февраль '25
+235
в 9 каналах
Get PRO
январь '25
+497
в 19 каналах
Get PRO
декабрь '24
+134
в 8 каналах
Get PRO
ноябрь '24
+819
в 17 каналах
Дата
Привлечение подписчиков
Упоминания
Каналы
27 октября+1
26 октября0
25 октября0
24 октября0
23 октября0
22 октября0
21 октября0
20 октября0
19 октября0
18 октября+1
17 октября0
16 октября0
15 октября0
14 октября+1
13 октября+1
12 октября0
11 октября+1
10 октября0
09 октября0
08 октября+1
07 октября0
06 октября+4
05 октября+14
04 октября0
03 октября0
02 октября0
01 октября0
Посты канала
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

2
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
207
3
BashTex 📱 #мем
BashTex 📱 #мем
374
4
Процесс открыл 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
371
5
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
351
6
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
308
7
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
337
8
BashTex 📱 #мем
BashTex 📱 #мем
528
9
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
458
10
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
381
11
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
337
12
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
427
13
BashTex 📱 #мем
BashTex 📱 #мем
626
14
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
600
15
/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
564
16
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
465
17
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
461
18
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
514
19
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
516
20
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
437