Файл дескриптор у Linux із прикладами

Якось, на одному інтерв'ю мене запитали, що ти робитимеш, якщо виявиш непрацюючий сервіс через те, що на диску закінчилося місце?

Звичайно ж я відповів, що подивлюся, чим зайняте це місце і, якщо можливо, то почищу місце.
Тоді інтерв'юер запитав, а якщо на розділі немає вільного місця, але й файлів, які б займали все місце, ти теж не бачиш?

На це я сказав, що завжди можна подивитися відкриті файли дескриптори, наприклад командою lsof і зрозуміти, який додаток зайняв все доступне місце, а далі можна діяти за обставинами, залежно від того, чи потрібні дані.

Інтерв'юер перервав мене на останньому слові, доповнивши своє запитання: «Припустимо, що дані нам не потрібні, це просто дебаг лог, але програма не працює через те, що не може записати дебаг»?

"окей", - відповів я, "ми можемо вимкнути дебаг в конфізі програми і перезапустити його".
Інтерв'юер заперечив: "Ні, програму ми перезапустити не можемо, у нас у пам'яті все ще зберігаються важливі дані, а до самого сервісу підключені важливі клієнти, яких ми не можемо змушувати перепідключатися заново".

"ну добре", сказав я, "якщо ми не можемо перезапускати додаток і дані нам не важливі, то ми можемо просто очистити цей відкритий файл через файл дескриптор, навіть якщо ми не бачимо його в команді ls на файловій системі".

Інтерв'юер залишився задоволеним, а я ні.

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

Тузик

На початку моєї кар'єри я намагався створити невелику програму, в якій потрібно було зберігати інформацію про користувачів. І тоді я думав, як мені зіставити користувача до його даним. Є, наприклад, у мене Іванов Іван Іванович, і має якісь дані, але як їх подружити? Я можу вказати прямо, що собака на ім'я «Тузик» належить цьому самому Іванові. Але що, якщо він змінить ім'я і замість Івана стане, наприклад, Олей? Тоді вийде, що наша Оля Іванівна Іванова більше не матиме собаки, а наш Тузик все ще належатиме неіснуючому Іванові. Вирішити цю проблему допомогла база даних, яка кожному користувачеві давала унікальний ідентифікатор (ID), і мій Тузік прив'язувався до цього ID, який був просто порядковим номером. Таким чином господар у тузика був із ID під номером 2, і на якийсь момент часу під цим ID був Іван, а потім під цим ID стала Оля. Проблема людства та тваринництва була практично вирішена.

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

Проблема файлу та програми, що працює з цим файлом, приблизно така ж як нашого собаки та людини. Припустимо я відкрив файл під ім'ям ivan.txt і почав записувати слово tuzik, але встиг записати тільки першу літеру «t» у файл, і цей файл був кимось перейменований, наприклад в olya.txt. Але файл залишився тим самим, і я все ще хочу записати в нього свого тузика. Щоразу при відкритті файлу системним викликом відкрити у будь-якій мові програмування я отримую унікальний 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 вже є, а дочірніх процесів ще немає:

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

Потім за допомогою системного виклику клон 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, оскільки вони більше не використовуються.

я навмисно ігнорую, він використовує для внутрішніх потреб самого 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

Як бачимо, розмір файлу тільки збільшується і наш транкейт не спрацював. Звернімося до документації із системного виклику відкрити. Якщо при відкритті файлу ми використовуємо прапорець 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

Джерело: habr.com

Додати коментар або відгук