Однажды, на одном интервью меня спросили, что ты будешь делать, если обнаружишь неработающий сервис из-за того, что на диске закончилось место?

Конечно же я ответил, что посмотрю, чем занято это место и если возможно, то почищу место.
Тогда интервьюер спросил, а что если на разделе нет свободного места, но и файлов, которые бы занимали все место, ты тоже не видишь?

На это я сказал, что всегда можно посмотреть открытые файл дескрипторы, например командой lsof и понять какое приложение заняло все доступное место, а дальше можно действовать по обстоятельствам, в зависимости от того, нужны ли данные.

Интервьюер прервал меня на последнем слове, дополнив свой вопрос: «Предположим, что данные нам не нужны, это просто дебаг лог, но приложение не работает из-за того, что не может записать дебаг»?

«окей», — ответил я, «мы можем выключить дебаг в конфиге приложения и перезапустить его».
Интервьюер возразил: «Нет, приложение мы перезапустить не можем, у нас в памяти все еще хранятся важные данные, а к самому сервису подключены важные клиенты, которых мы не можем заставлять переподключаться заново».

«ну хорошо», сказал я, «если мы не можем перезапускать приложение и данные нам не важны, то мы можем просто очистить этот открытый файл через файл дескриптор, даже если мы его не видим в команде ls на файловой системе».

Интервьюер остался доволен, а я нет.

Тогда я подумал, почему человек, проверяющий мои знания, не копает глубже? А что, если данные все-таки важны? Что если мы не можем перезапускать процесс, и при этом этот процесс пишет на файловую систему в раздел, на котором нет свободного места? Что если мы не можем потерять не только уже записанные данные, но и те данные, что этот процесс пишет или пытается записать?

Тузик


В начале моей карьеры я пытался создать небольшое приложение, в котором нужно было хранить информацию о пользователях. И тогда я думал, а как мне сопоставить пользователя к его данным. Есть, например, у меня Иванов Иван Иваныч, и есть у него какие-то данные, но как их подружить? Я могу указать напрямую, что собака по имени «Тузик» принадлежит этому самому Ивану. Но что, если он сменит имя и вместо Ивана станет, например, Олей? Тогда получится, что наша Оля Ивановна Иванова больше не будет иметь собаки, а наш Тузик все еще будет принадлежать несуществующему Ивану. Решить эту проблему помогла база данных, которая каждому пользователю давала уникальный идентификатор (ID), и мой Тузик привязывался к этому ID, который, по сути, был просто порядковым номером. Таким образом хозяин у тузика был с ID под номером 2, и на какой-то момент времени под этим ID был Иван, а потом под этим же ID стала Оля. Проблема человечества и животноводства была практически решена.

Файл дескриптор


Проблема файла и программы, работающей с этим файлом, примерно такая же как нашей собаки и человека. Предположим я открыл файл под именем ivan.txt и начал в него записывать слово tuzik, но успел записать только первую букву «t» в файл, и этот файл был кем-то переименован, например в olya.txt. Но файл остался тем же самым, и я все еще хочу записать в него своего тузика. Каждый раз при открытии файла системным вызовом open в любом языке программирования я получаю уникальный ID, который указывает мне на файл, этот ID и есть файл дескриптор. И совершенно не важно, что и кто делает с этим файлом дальше, его могут удалить, его могут переименовать, ему могут поменять владельца или забрать права на чтение и запись, я все равно буду иметь к нему доступ, потому что на момент открытия файла у меня были права для его чтения и/или записи и я успел начать с ним работать, а значит должен продолжать это делать.

В Linux библиотека libc открывает для каждого запущенного приложения(процесса) 3 файл дескриптора, с номерами 0,1,2. Больше информации вы можете найти по ссылкам man stdio и man stdout

  • Файл дескриптор 0 называется STDIN и ассоциируется с вводом данных у приложения
  • Файл дескриптор 1 называется STDOUT и используется приложениями для вывода данных, например командами print
  • Файл дескриптор 2 называется STDERR и используется приложениями для вывода данных, сообщающих об ошибке

Если в вашей программе вы откроете какой-либо файл на чтение или запись, то скорее всего вы получите первый свободный ID и это будет номер 3.

Список файл дескрипторов можно посмотреть у любого процесса, если вы знаете его PID.

Например, откроем консоль с bash и посмотрим PID нашего процесса

[user@localhost ]$ echo $$
15771

Во второй консоли запустим

[user@localhost ]$ ls -lah /proc/15771/fd/
total 0
dr-x------ 2 user user  0 Oct  7 15:42 .
dr-xr-xr-x 9 user user  0 Oct  7 15:42 ..
lrwx------ 1 user user 64 Oct  7 15:42 0 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 1 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 2 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 255 -> /dev/pts/21

Файл дескриптор с номером 255 можете смело игнорировать в рамках данной статьи, он был открыт для своих нужд уже самим bash, а не прилинкованной библиотекой.

Сейчас все 3 файл дескриптора связаны с устройством псевдотерминала /dev/pts, но мы все равно можем ими манипулировать, например запустим во второй консоли

[user@localhost ]$ echo "hello world" > /proc/15771/fd/0

И в первой консоли мы увидим

[user@localhost ]$ hello world

Redirect и Pipe


Вы можете легко переопределить эти 3 файл дескриптора в любом процессе, в том числе и в bash, например через трубу(pipe), соединяющую два процесса, смотрим

[user@localhost ]$ cat /dev/zero | sleep 10000

Вы можете сами запустить эту команду с strace -f и увидеть, что происходит внутри, но я вкратце расскажу.

Наш родительский процесс bash с PID 15771 парсит нашу команду и понимает сколько именно команд мы хотим запустить, в нашем случае их две: cat и sleep. Bash знает что ему нужно создать два дочерних процесса, и объединить их одной трубой. Итого bash потребуется 2 дочерних процесса и один pipe.

Перед созданием дочерних процессов bash запускает системный вызов pipe и получает новые файл дескрипторы на временный буфер pipe, но этот буфер никак пока не связывает наши два дочерних процесса.

Для родительского процесса это выглядит так будто pipe уже есть, а дочерних процессов еще нет:

PID    command
15771  bash
lrwx------ 1 user user 64 Oct  7 15:42 0 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 1 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 2 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 3 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:42 4 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:42 255 -> /dev/pts/21

Затем с помощью системного вызова clone bash создает два дочерних процесса, и наши три процесса будут выглядеть так:

PID    command
15771  bash
lrwx------ 1 user user 64 Oct  7 15:42 0 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 1 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 2 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 3 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:42 4 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:42 255 -> /dev/pts/21
PID    command
9004  bash
lrwx------ 1 user user 64 Oct  7 15:57 0 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:57 1 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:57 2 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:57 3 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:57 4 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:57 255 -> /dev/pts/21
PID    command
9005  bash
lrwx------ 1 user user 64 Oct  7 15:57 0 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:57 1 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:57 2 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:57 3 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:57 4 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:57 255 -> /dev/pts/21

Не забываем, что clone клонирует процесс вместе со всеми файл дескрипторами, поэтому в родительском процессе и в дочерних они будут одинаковые. Задача родительского процесса с PID 15771 следить за дочерними процессами, поэтому он просто ждет ответ от дочерних.

Следовательно pipe ему не нужен, и он закрывает файл дескрипторы с номерами 3 и 4.

В первом дочернем процессе bash с PID 9004, системным вызовом dup2, меняет наш STDOUT файл дескриптор с номером 1 на файл дескриптор указывающий на pipe, в нашем случае это номер 3. Таким образом все, что первый дочерний процесс с PID 9004 будет писать в STDOUT, будет автоматически попадать в буфер pipe.

Во втором дочернем процессе с PID 9005 bash меняет с помощью dup2 файл дескриптор STDIN с номером 0. Теперь все, что будет читать наш второй bash с PID 9005, будет читать из pipe.

После этого в дочерних процессах так же закрываются файл дескрипторы с номерами 3 и 4, так как они более не используются.

Файл дескриптор 255 я намеренно игнорирую, он использует для внутренних нужд самого bash и в дочерних процессах будет также закрыт.

Далее в первом дочернем процессе с PID 9004 bash запускает с помощью системного вызова exec исполняемый файл, который мы указали в командной строке, в нашем случае это /usr/bin/cat.

Во втором дочернем процессе с PID 9005 bash запускает второй исполняемый файл, который мы указали, в нашем случае это /usr/bin/sleep.

Системный вызов exec не закрывает файл дескрипторы, если они не были открыты с флагом O_CLOEXEC во время выполнения вызова open. В нашем случае после запуска исполняемых файлов все текущие файл дескрипторы сохранятся.

Проверяем в консоли:

[user@localhost ]$ pgrep -P 15771
9004
9005
[user@localhost ]$ ls -lah /proc/15771/fd/
total 0
dr-x------ 2 user user  0 Oct  7 15:42 .
dr-xr-xr-x 9 user user  0 Oct  7 15:42 ..
lrwx------ 1 user user 64 Oct  7 15:42 0 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 1 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 2 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:42 255 -> /dev/pts/21
[user@localhost ]$ ls -lah /proc/9004/fd
total 0
dr-x------ 2 user user  0 Oct  7 15:57 .
dr-xr-xr-x 9 user user  0 Oct  7 15:57 ..
lrwx------ 1 user user 64 Oct  7 15:57 0 -> /dev/pts/21
l-wx------ 1 user user 64 Oct  7 15:57 1 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:57 2 -> /dev/pts/21
lr-x------ 1 user user 64 Oct  7 15:57 3 -> /dev/zero
[user@localhost ]$ ls -lah /proc/9005/fd
total 0
dr-x------ 2 user user  0 Oct  7 15:57 .
dr-xr-xr-x 9 user user  0 Oct  7 15:57 ..
lr-x------ 1 user user 64 Oct  7 15:57 0 -> pipe:[253543032]
lrwx------ 1 user user 64 Oct  7 15:57 1 -> /dev/pts/21
lrwx------ 1 user user 64 Oct  7 15:57 2 -> /dev/pts/21
[user@localhost ]$ ps -up 9004
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
user  9004  0.0  0.0 107972   620 pts/21   S+   15:57   0:00 cat /dev/zero
[user@localhost ]$ ps -up 9005
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
user  9005  0.0  0.0 107952   360 pts/21   S+   15:57   0:00 sleep 10000

Как видите уникальный номер нашего pipe у нас в обоих процессах совпадает. Таким образом у нас есть связь между двумя разными процессами с одним родителем.

Для тех, кто не знаком с системными вызовами, которые использует bash, крайне рекомендую запустить команды через strace и посмотреть, что происходит внутри, например, так:

strace -s 1024 -f bash -c "ls | grep hello"

Вернемся к нашей проблеме с нехваткой места на диске и попыткой сохранить данные без перезапуска процесса. Напишем небольшую программу, которая будет записывать на диск примерно 1 мегабайт в секунду. При этом если по какой-либо причине мы не смогли записать данные на диск, мы будем просто игнорировать это и пытаться записать данные вновь через секунду. В примере я использую Python, вы можете использовать любой другой язык программирования.

[user@localhost ]$ cat openforwrite.py 
import datetime
import time

mystr="a"*1024*1024+"\n"
with open("123.txt", "w") as f:
    while True:
        try:
            f.write(str(datetime.datetime.now()))
            f.write(mystr)
            f.flush()
            time.sleep(1)
        except:
            pass

Запустим программу и посмотрим на файл дескрипторы

[user@localhost ]$ python openforwrite.py &
[1] 3762
[user@localhost ]$ ps axuf | grep [o]penforwrite
user  3762  0.0  0.0 128600  5744 pts/22   S+   16:28   0:00  |   \_ python openforwrite.py
[user@localhost ]$ ls -la /proc/3762/fd
total 0
dr-x------ 2 user user  0 Oct  7 16:29 .
dr-xr-xr-x 9 user user  0 Oct  7 16:29 ..
lrwx------ 1 user user 64 Oct  7 16:29 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  7 16:29 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  7 16:29 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  7 16:29 3 -> /home/user/123.txt

Как видим у нас есть наши 3 стандартные файл дескрипторы и еще один, который мы открыли. Проверим размер файла:

[user@localhost ]$ ls -lah 123.txt 
-rw-rw-r-- 1 user user 117M Oct  7 16:30 123.txt

данные пишутся, пробуем поменять права на файл:

[user@localhost ]$ sudo chown root: 123.txt
[user@localhost ]$ ls -lah 123.txt 
-rw-rw-r-- 1 root root 168M Oct  7 16:31 123.txt
[user@localhost ]$ ls -lah 123.txt 
-rw-rw-r-- 1 root root 172M Oct  7 16:31 123.txt

Видим, что данные все еще пишутся, хотя наш пользователь не имеет права писать в файл. Попробуем его удалить:

[user@localhost ]$ sudo rm 123.txt 
[user@localhost ]$ ls 123.txt
ls: cannot access 123.txt: No such file or directory

Куда пишутся данные? И пишутся ли вообще? Проверяем:

[user@localhost ]$ ls -la /proc/3762/fd
total 0
dr-x------ 2 user user  0 Oct  7 16:29 .
dr-xr-xr-x 9 user user  0 Oct  7 16:29 ..
lrwx------ 1 user user 64 Oct  7 16:29 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  7 16:29 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  7 16:29 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  7 16:29 3 -> /home/user/123.txt (deleted)

Да, наш файл дескриптор все еще существует, и мы можем работать с этим файл дескриптором как с нашим старым файлом, мы можем его читать, очищать и копировать.

Смотрим на размер файла:

[user@localhost ]$ lsof | grep 123.txt
python    31083             user    3w      REG                8,5   19923457   2621522 /home/user/123.txt

Размер файла 19923457. Пробуем очистить файл:

[user@localhost ]$ truncate -s 0 /proc/31083/fd/3
[user@localhost ]$ lsof | grep 123.txt
python    31083             user    3w      REG                8,5  136318390   2621522 /home/user/123.txt

Как видим размер файла только увеличивается и наш транкейт не сработал. Обратимся к документации по системному вызову open. Если при открытии файла мы используем флаг O_APPEND, то при каждой записи операционная система проверяет размер файла и пишет данные в самый конец файла, причем делает это атомарно. Это позволяет нескольким тредам или процессам писать в один и тот же файл. Но в нашем коде мы не используем этот флаг. Мы можем увидеть другой размер файла в lsof после транкейт только если откроем файл для дозаписи, а значит в нашем коде вместо

with open("123.txt", "w") as f:

мы должны поставить

with open("123.txt", "a") as f:

Проверяем с «w» флагом

[user@localhost ]$ strace -e trace=open python openforwrite.py 2>&1| grep 123.txt
open("123.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3

и с «a» флагом

[user@localhost ]$ strace -e trace=open python openforwrite.py 2>&1| grep 123.txt
open("123.txt", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3

Программируем уже запущенный процесс


Часто программисты при создании и тестировании программы используют дебагеры (например GDB) или различные уровни логирования в приложении. Linux предоставляет возможность фактически писать и менять уже запущенную программу, например менять значения переменных, устанавливать breakpoint и тд и тп.

Возвращаясь к оригинальному вопросу с нехваткой места на диске для записи файла, попробуем сэмулировать проблему.

Создадим файл для нашего раздела, который мы подмонтируем как отдельный диск:

[user@localhost ~]$ dd if=/dev/zero of=~/tempfile_for_article.dd bs=1M count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB) copied, 0.00525929 s, 2.0 GB/s
[user@localhost ~]$

Создадим файловую систему:

[user@localhost ~]$ mkfs.ext4 ~/tempfile_for_article.dd
mke2fs 1.42.9 (28-Dec-2013)
/home/user/tempfile_for_article.dd is not a block special device.
Proceed anyway? (y,n) y
...
Writing superblocks and filesystem accounting information: done
[user@localhost ~]$

Подмонтируем файловую систему:

[user@localhost ~]$ sudo mount ~/tempfile_for_article.dd /mnt/
[sudo] password for user: 
[user@localhost ~]$ df -h | grep mnt
/dev/loop0      8.7M  172K  7.9M   3% /mnt

Создаем директорию с нашим владельцем:

[user@localhost ~]$ sudo mkdir /mnt/logs
[user@localhost ~]$ sudo chown user: /mnt/logs

Откроем файл только на запись в нашей программе:

with open("/mnt/logs/123.txt", "w") as f:

Запускаем

[user@localhost ]$ python openforwrite.py 

Ждем несколько секунд

[user@localhost ~]$ df -h | grep mnt
/dev/loop0      8.7M  8.0M     0 100% /mnt

Итак, мы получили проблему, описанную в начале этой статьи. Свободного места 0, занятого 100%.

Мы помним, что по условиям задачи мы пытаемся записать очень важные данные, которые нельзя потерять. И при этом нам нужно починить сервис без перезапуска процесса.

Допустим, у нас все же есть место на диске, но в другом разделе, например в /home.

Попробуем «перепрограммировать на лету» наш код.

Смотрим PID нашего процесса, который съел все место на диске:

[user@localhost ~]$ ps axuf | grep [o]penfor
user 10078 27.2  0.0 128600  5744 pts/22   R+   11:06   0:02  |   \_ python openforwrite.py

Подключаемся к процессу через gdb

[user@localhost ~]$ gdb -p 10078
...
(gdb) 

Смотрим открытые файл дескрипторы:

(gdb) shell ls -lah /proc/10078/fd/
total 0
dr-x------ 2 user user  0 Oct  8 11:06 .
dr-xr-xr-x 9 user user  0 Oct  8 11:06 ..
lrwx------ 1 user user 64 Oct  8 11:09 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:09 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:06 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:09 3 -> /mnt/logs/123.txt

Смотрим информацию о файл дескрипторе с номером 3, который нас интересует

(gdb) shell cat /proc/10078/fdinfo/3
pos:    8189952
flags:  0100001
mnt_id: 482

Помня о том, какой системный вызов делает Python (смотрите выше, где мы запускали strace и находили вызов open), обрабатывая наш код для открытия файла, мы делаем то же самое самостоятельно от имени нашего процесса, но биты O_WRONLY|O_CREAT|O_TRUNC нам нужно заменить на числовое значение. Для этого открываем исходники ядра, например тут и смотрим какие флаги за что отвечают

#define O_WRONLY 00000001
#define O_CREAT 00000100
#define O_TRUNC 00001000

Объединяем все значения в одно, получаем 00001101

Запускаем наш вызов из gdb

(gdb) call open("/home/user/123.txt", 00001101,0666)
$1 = 4

Итак мы получили новый файл дескриптор с номером 4 и новый открытый файл на другом разделе, проверяем:

(gdb) shell ls -lah /proc/10078/fd/
total 0
dr-x------ 2 user user  0 Oct  8 11:06 .
dr-xr-xr-x 9 user user  0 Oct  8 11:06 ..
lrwx------ 1 user user 64 Oct  8 11:09 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:09 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:06 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:09 3 -> /mnt/logs/123.txt
l-wx------ 1 user user 64 Oct  8 11:15 4 -> /home/user/123.txt

Мы помним пример с pipe — как bash меняет файл дескрипторы, и уже выучили системный вызов dup2.

Пробуем подменить один файл дескриптор другим

(gdb) call dup2(4,3)
$2 = 3

Проверяем:

(gdb) shell ls -lah /proc/10078/fd/
total 0
dr-x------ 2 user user  0 Oct  8 11:06 .
dr-xr-xr-x 9 user user  0 Oct  8 11:06 ..
lrwx------ 1 user user 64 Oct  8 11:09 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:09 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:06 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:09 3 -> /home/user/123.txt
l-wx------ 1 user user 64 Oct  8 11:15 4 -> /home/user/123.txt

Закрываем файл дескриптор 4, так как нам он не нужен:

(gdb) call close (4)
$1 = 0

И выходим из gdb

(gdb) quit
A debugging session is active.

    Inferior 1 [process 10078] will be detached.

Quit anyway? (y or n) y
Detaching from program: /usr/bin/python2.7, process 10078

Проверяем новый файл:

[user@localhost ~]$ ls -lah /home/user/123.txt
-rw-rw-r-- 1 user user 5.1M Oct  8 11:18 /home/user/123.txt
[user@localhost ~]$ ls -lah /home/user/123.txt
-rw-rw-r-- 1 user user 7.1M Oct  8 11:18 /home/user/123.txt

Как видим, данные пишутся в новый файл, проверяем старый:

[user@localhost ~]$ ls -lah /mnt/logs/123.txt 
-rw-rw-r-- 1 user user 7.9M Oct  8 11:08 /mnt/logs/123.txt

Данные не потеряны, приложение работает, логи пишутся в новое место.

Немного усложним задачу


Представим, что данные нам важны, но места на диске у нас нет ни в одном из разделов и подключить диск мы не можем.

Что мы можем сделать, так это перенаправить куда-то наши данные, например в pipe, а данные из pipe в свою очередь перенаправить в сеть через какую-либо программу, например netcat.
Мы можем создать именованный pipe командой mkfifo. Она создаст псевдофайл на файловой системе, даже если на ней нет свободного места.

Перезапускаем приложение, и проверяем:

[user@localhost ]$ python openforwrite.py 
[user@localhost ~]$ ps axuf | grep [o]pen
user  5946 72.9  0.0 128600  5744 pts/22   R+   11:27   0:20  |   \_ python openforwrite.py
[user@localhost ~]$ ls -lah /proc/5946/fd
total 0
dr-x------ 2 user user  0 Oct  8 11:27 .
dr-xr-xr-x 9 user user  0 Oct  8 11:27 ..
lrwx------ 1 user user 64 Oct  8 11:28 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:28 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:27 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:28 3 -> /mnt/logs/123.txt
[user@localhost ~]$ df -h | grep mnt
/dev/loop0      8.7M  8.0M     0 100% /mnt

Места на диске нет, но мы успешно создаем там именованный pipe:

[user@localhost ~]$ mkfifo /mnt/logs/megapipe
[user@localhost ~]$ ls -lah /mnt/logs/megapipe 
prw-rw-r-- 1 user user 0 Oct  8 11:28 /mnt/logs/megapipe

Теперь нам надо как-то завернуть все данные, что попадают в этот pipe на другой сервер через сеть, для этого подойдет все тот же netcat.

На сервере remote-server.example.com запускаем

[user@localhost ~]$ nc -l 7777 > 123.txt 

На нашем проблемном сервере запускаем в отдельном терминале

[user@localhost ~]$ nc remote-server.example.com 7777 < /mnt/logs/megapipe 

Теперь все данные, которые попадут в pipe автоматически попадут на stdin в netcat, который их отправит в сеть на порт 7777.

Все что нам осталось сделать это начать писать наши данные в этот именованный pipe.

У нас уже есть запущенное приложение:

[user@localhost ~]$ ps axuf | grep [o]pen
user  5946 99.8  0.0 128600  5744 pts/22   R+   11:27 169:27  |   \_ python openforwrite.py
[user@localhost ~]$ ls -lah /proc/5946/fd
total 0
dr-x------ 2 user user  0 Oct  8 11:27 .
dr-xr-xr-x 9 user user  0 Oct  8 11:27 ..
lrwx------ 1 user user 64 Oct  8 11:28 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:28 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:27 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:28 3 -> /mnt/logs/123.txt

Из всех флагов нам нужен только O_WRONLY так как файл уже существует и очищать нам его не нужно

[user@localhost ~]$ gdb -p 5946
...
(gdb) call open("/mnt/logs/megapipe", 00000001,0666)
$1 = 4
(gdb) shell ls -lah /proc/5946/fd
total 0
dr-x------ 2 user user  0 Oct  8 11:27 .
dr-xr-xr-x 9 user user  0 Oct  8 11:27 ..
lrwx------ 1 user user 64 Oct  8 11:28 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:28 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:27 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:28 3 -> /mnt/logs/123.txt
l-wx------ 1 user user 64 Oct  8 14:20 4 -> /mnt/logs/megapipe
(gdb) call dup2(4,3)
$2 = 3
(gdb) shell ls -lah /proc/5946/fd
total 0
dr-x------ 2 user user  0 Oct  8 11:27 .
dr-xr-xr-x 9 user user  0 Oct  8 11:27 ..
lrwx------ 1 user user 64 Oct  8 11:28 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:28 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:27 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:28 3 -> /mnt/logs/megapipe
l-wx------ 1 user user 64 Oct  8 14:20 4 -> /mnt/logs/megapipe
(gdb) call close(4)
$3 = 0
(gdb) shell ls -lah /proc/5946/fd
total 0
dr-x------ 2 user user  0 Oct  8 11:27 .
dr-xr-xr-x 9 user user  0 Oct  8 11:27 ..
lrwx------ 1 user user 64 Oct  8 11:28 0 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:28 1 -> /dev/pts/22
lrwx------ 1 user user 64 Oct  8 11:27 2 -> /dev/pts/22
l-wx------ 1 user user 64 Oct  8 11:28 3 -> /mnt/logs/megapipe
(gdb) quit
A debugging session is active.

    Inferior 1 [process 5946] will be detached.

Quit anyway? (y or n) y
Detaching from program: /usr/bin/python2.7, process 5946

Проверяем удаленный сервер remote-server.example.com

[user@localhost ~]$ ls -lah 123.txt 
-rw-rw-r-- 1 user user 38M Oct  8 14:21 123.txt

Данные идут, проверяем проблемный сервер

[user@localhost ~]$ ls -lah /mnt/logs/
total 7.9M
drwxr-xr-x 2 user user 1.0K Oct  8 11:28 .
drwxr-xr-x 4 root     root     1.0K Oct  8 10:55 ..
-rw-rw-r-- 1 user user 7.9M Oct  8 14:17 123.txt
prw-rw-r-- 1 user user    0 Oct  8 14:22 megapipe

Данные сохранились, проблема решена.

Пользуясь случаем, передаю привет коллегам из компании Degiro.
Слушайте подкасты Радио-Т.

Всем добра.

В качестве домашнего задания предлагаю подумать, что будет в файл дескрипторах процесса cat и sleep если запустить такую команду:

[user@localhost ~]$ cat /dev/zero 2>/dev/null| sleep 10000

Комментарии (16)


  1. splinehip
    10.10.2019 23:08
    +3

    Прочитал с удовольствием. Спасибо!


  1. mkevac
    11.10.2019 00:27

    Вы мне не подходите, интервьюер. Вы слишком легко сдались!

    Классная статейка!


  1. agic
    11.10.2019 11:57

    хорошая статья, спасибо.


  1. tnt4brain
    11.10.2019 12:17

    Замечательная статья, спасибо автору. Теперь лично мои знания по использованию дескриптор стали более упорядоченными.


  1. testor123
    11.10.2019 13:40

    Спасибо!


  1. disaipe
    11.10.2019 13:40

    Спасибо, вполне доступно и очень познавательно.


  1. lurkr
    11.10.2019 13:40

    Спасибо.


  1. Wolf4ara
    11.10.2019 13:40

    шикардос =)


  1. ElegantBoomerang
    11.10.2019 14:56

    Красиво) Но mkfifo может упасть всё-таки: если качественно забить диск, то придётся искать папку, у которой есть свободное место в блоке, описывающем её содержимое. Иначе ENOSPC.


  1. a-tk
    11.10.2019 15:25

    Ох уж эти костыли и закат вручную! Но прикольно, да!


  1. gpyra
    11.10.2019 16:11

    Но что, если он сменит имя и вместо Ивана станет, например, Олей

    Не пугайте так. У нас не Европа все-таки


  1. ScreamPassion
    11.10.2019 16:26

    Отличная статья, хорошая подача материала, определенно в закладки, спасибо большое.


  1. IkaR49
    11.10.2019 19:43
    +3

    Сударь, вы волшебник! Прочитал с упоением и, что удивительно, практически всё понял.


  1. glowingsword
    11.10.2019 21:33

    Шикарная статья. Про дескрипторы и то, как дотянуться до уже удалённого, но ещё открытого процессом файла знал и ранее, а про то как с помощью dup2 в gdb подменять дескрипторы и сохранять выхлоп лога в другое место при закончившемся пространстве — не знал, даже не думал о таком хаке как-то… Спасибо!


  1. kai3341
    11.10.2019 23:29

    Спасибо за статью


  1. ne_zabudka
    13.10.2019 13:12

    У меня в gdb вылезла ошибка:

    (gdb) call dup2(4,3)
    'dup2' has unknown return type; cast the call to its declared return type

    Исправила:
    (gdb) call (int)dup2(4,3)
    $2 = 3