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

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

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

    На это я сказал, что всегда можно посмотреть открытые файл дескрипторы, например командой 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
    
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 18

      +11
      Прочитал с удовольствием. Спасибо!
        +2
        Вы мне не подходите, интервьюер. Вы слишком легко сдались!

        Классная статейка!
          +1
          хорошая статья, спасибо.
            +3

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

              0
              Спасибо!
                +2
                Спасибо, вполне доступно и очень познавательно.
                  0
                  Спасибо.
                    0
                    шикардос =)
                      +1

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

                        0
                        Ох уж эти костыли и закат вручную! Но прикольно, да!
                          –7
                          Но что, если он сменит имя и вместо Ивана станет, например, Олей

                          Не пугайте так. У нас не Европа все-таки
                            +3
                            Отличная статья, хорошая подача материала, определенно в закладки, спасибо большое.
                              +6

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

                                +4
                                Шикарная статья. Про дескрипторы и то, как дотянуться до уже удалённого, но ещё открытого процессом файла знал и ранее, а про то как с помощью dup2 в gdb подменять дескрипторы и сохранять выхлоп лога в другое место при закончившемся пространстве — не знал, даже не думал о таком хаке как-то… Спасибо!
                                  +1
                                  Спасибо за статью
                                    0
                                    У меня в 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
                                      0
                                      Прикольно, спасибо!
                                        +1
                                        Вот только мне покажется, что я вроде начал неплохо разбираться в ОС семейства Linux, и выходит такая статья, которая заколачивает гвозди в плинтус над моей головой. Спасибо за статью! Пишите еще!

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое