Deskryptor pliku w Linux z przykładami

Pewnego razu, na jednym z wywiadów zapytano mnie, co zrobię, jeśli odkryję, że serwis nie działa z powodu braku miejsca na dysku?

Odpowiedziałem, że sprawdzę, co zajmuje to miejsce i jeśli to możliwe, to je zwolnię.
Wtedy interviewer zapytał, a co jeśli na partycji nie ma wolnego miejsca, ale też nie widzisz plików, które zajmowałyby całą przestrzeń?

Na to powiedziałem, że zawsze można sprawdzić otwarte deskryptory plików, na przykład za pomocą polecenia lsof, i zrozumieć, która aplikacja zajęła całe dostępne miejsce, a następnie działać w zależności od okoliczności, w zależności od tego, czy dane są potrzebne.

Interviewer przerwał mi na ostatnim słowie, dodając swoje pytanie: „Załóżmy, że dane nie są nam potrzebne, to tylko log debugowania, ale aplikacja nie działa, ponieważ nie może zapisać debugowania”?

„Dobrze,” odpowiedziałem, „możemy wyłączyć debugowanie w konfiguracji aplikacji i zrestartować ją.”
Interviewer zaprotestował: „Nie, nie możemy zrestartować aplikacji, w pamięci przechowujemy wciąż ważne dane, a do samego serwisu podłączeni są ważni klienci, których nie możemy zmuszać do ponownego połączenia.”

„No dobrze,” powiedziałem, „jeśli nie możemy ponownie uruchomić aplikacji i dane nie są dla nas ważne, to możemy po prostu wyczyścić ten otwarty plik poprzez deskryptor pliku, nawet jeśli nie widzimy go w poleceniu ls w systemie plików.”

Interviewer był zadowolony, a ja nie.

Wtedy pomyślałem, dlaczego człowiek rozważający moją wiedzę nie drąży głębiej? A co jeśli dane jednak są ważne? Co jeśli nie możemy ponownie uruchomić procesu, a ten proces zapisuje do systemu plików w partycji, na której nie ma wolnego miejsca? Co jeśli nie możemy stracić nie tylko już zapisanych danych, ale i tych danych, które ten proces pisze lub próbuje zapisać?

Tuzik

Na początku mojej kariery próbowałem stworzyć małą aplikację, w której trzeba było przechowywać informacje o użytkownikach. Zastanawiałem się, jak dopasować użytkownika do jego danych. Mam na przykład Iwana Iwanowicza, który ma jakieś dane, ale jak je powiązać? Mogę bezpośrednio wskazać, że pies o imieniu „Tuzik” należy do tego Iwana. Ale co się stanie, jeśli zmieni imię i zamiast Iwana stanie się na przykład Olą? Wówczas nasza Ola Iwanowna Iwanowa nie będzie miała psa, a nasz Tuzik nadal będzie należał do nieistniejącego Iwana. Rozwiązanie tej kwestii przyniosła baza danych, która każdemu użytkownikowi nadawała unikalny identyfikator (ID), a mój Tuzik był powiązany z tym ID, które w istocie było tylko numerem porządkowym. W ten sposób właściciel Tuzika miał ID nr 2, a w pewnym momencie pod tym ID był Iwan, a potem pod tym samym ID stała się Ola. Problem ludzkości i hodowli zwierząt był praktycznie rozwiązany.

Deskryptor pliku

Problem pliku i programu, który z nim pracuje, jest podobny do sytuacji naszego psa i człowieka. Załóżmy, że otworzyłem plik o nazwie ivan.txt i zacząłem do niego pisać słowo tuzik, ale zdążyłem zapisać tylko pierwszą literę „t”, a ten plik został przez kogoś przemianowany, na przykład na olya.txt. Ale plik pozostał tym samym, i nadal chcę zapisać w nim mojego Tuzika. Za każdym razem, gdy otwieram plik za pomocą wywołania systemowego open w dowolnym języku programowania otrzymuję unikalny ID, który wskazuje mi na plik, ten ID jest deskryptorem pliku. I zupełnie nieważne, co dalej dzieje się z tym plikiem, mogą go usunąć, mogą go przemianować, mogą zmienić jego właściciela lub odebrać prawa do odczytu i zapisu, nadal będę miał do niego dostęp, ponieważ w momencie otwarcia pliku miałem prawa do jego odczytu i/lub zapisu i zdążyłem zacząć z nim pracować, a więc powinienem kontynuować tę pracę.

W Linuxie biblioteka libc otwiera dla każdej uruchomionej aplikacji (procesu) 3 deskryptory plików, o numerach 0, 1, 2. Więcej informacji znajdziesz pod linkami man stdio i man stdout

  • Deskryptor pliku 0 nazywa się STDIN i jest powiązany z wprowadzaniem danych do aplikacji
  • Deskryptor pliku 1 nazywa się STDOUT i jest używany przez aplikacje do wyjścia danych, na przykład przez polecenia print.
  • Deskryptor pliku 2 nazywa się STDERR i jest używany przez aplikacje do wyjścia danych dotyczących błędów.

Jeśli w swoim programie otworzysz jakikolwiek plik do odczytu lub zapisu, prawdopodobnie otrzymasz pierwsze wolne ID, którym będzie numer 3.

Listę deskryptorów plików można zobaczyć w każdym procesie, jeśli znasz jego PID.

Na przykład otwieramy konsolę z bash i sprawdzamy PID naszego procesu.

[user@localhost ]$ echo $$
15771

W drugiej konsoli uruchomimy

[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

Deskryptor pliku o numerze 255 możesz spokojnie zignorować w ramach tego artykułu, został otwarty dla własnych potrzeb przez bash, a nie przez połączoną bibliotekę.

Obecnie wszystkie 3 deskryptory plików są powiązane z urządzeniem pseudoterminala. /dev/pts, ale wciąż możemy nimi manipulować, na przykład uruchomimy w drugiej konsoli

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

A w pierwszej konsoli zobaczymy

[user@localhost ]$ hello world

Przekierowanie i rura

Możesz łatwo nadpisać te 3 deskryptory plików w dowolnym procesie, w tym w bash, na przykład przez rurę (pipe), łączącą dwa procesy, patrzymy

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

Możesz samodzielnie uruchomić to polecenie z strace -f i zobaczyć, co się dzieje w środku, ale w skrócie opowiem.

Nasz proces rodzicielski bash z PID 15771 analizuje nasze polecenie i rozumie, ile dokładnie poleceń chcemy uruchomić, w naszym przypadku są to dwa: cat i sleep. Bash wie, że musi stworzyć dwa procesy podrzędne i połączyć je jedną rurą. W sumie bash potrzebuje 2 procesów podrzędnych i jednej rury.

Przed stworzeniem procesów podrzędnych bash uruchamia wywołanie systemowe pipe i otrzymuje nowe deskryptory plików dla tymczasowego bufora rury, ale ten bufor jakoś jeszcze nie łączy naszych dwóch procesów podrzędnych.

Dla procesu rodzicielskiego wygląda to tak, jakby rura już istniała, a procesów podrzędnych jeszcze nie było:

PID    polecenie
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

Następnie za pomocą wywołania systemowego clone bash tworzy dwa procesy potomne, a nasze trzy procesy będą wyglądać tak:

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

Nie zapominaj, że clone klonuje proces wraz ze wszystkimi deskryptorami plików, więc w procesie rodzicielskim i w potomnych będą one takie same. Zadanie procesu rodzicielskiego z PID 15771 polega na monitorowaniu procesów potomnych, dlatego po prostu czeka na odpowiedź od potomnych.

W związku z tym pipe nie jest mu potrzebny, dlatego zamyka deskryptory plików o numerach 3 i 4.

W pierwszym procesie potomnym bash z PID 9004, za pomocą wywołania systemowego dup2, zmienia nasz deskryptor pliku STDOUT z numerem 1 na deskryptor pliku wskazujący na pipe, w naszym przypadku jest to numer 3. W ten sposób wszystko, co pierwszy proces potomny z PID 9004 zapisze do STDOUT, automatycznie trafi do bufora pipe.

W drugim procesie potomnym z PID 9005 bash zmienia za pomocą dup2 deskryptor pliku STDIN z numerem 0. Teraz wszystko, co będzie czytał nasz drugi bash z PID 9005, będzie czytane z pipe.

Po tym w procesach potomnych również zamykane są deskryptory plików o numerach 3 i 4, ponieważ nie są już używane.

Deskryptor pliku 255 celowo ignoruję, używa go do wewnętrznych potrzeb samego bash i w procesach potomnych również będzie zamknięty.

Następnie w pierwszym procesie potomnym z PID 9004 bash uruchamia za pomocą wywołania systemowego exec plik wykonywalny, który wskazaliśmy w wierszu poleceń, w naszym przypadku to /usr/bin/cat.

W drugim procesie potomnym z PID 9005 bash uruchamia drugi plik wykonywalny, który wskazaliśmy, w naszym przypadku to /usr/bin/sleep.

Wywołanie systemowe exec nie zamyka deskryptorów plików, jeśli nie zostały one otwarte z flagą O_CLOEXEC podczas wywołania open. W naszym przypadku po uruchomieniu plików wykonywalnych wszystkie bieżące deskryptory plików zostaną zachowane.

Sprawdzamy w konsoli:

[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

Jak widać, unikalny numer naszego pipera zgadza się w obu procesach. W ten sposób mamy połączenie między dwoma różnymi procesami z jednym rodzicem.

Dla tych, którzy nie są zaznajomieni z wywołaniami systemowymi używanymi przez bash, zdecydowanie rekomenduję uruchomienie poleceń za pomocą strace i obserwację, co dzieje się w środku, na przykład tak:

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

Wracając do naszego problemu z brakiem miejsca na dysku i próbą zapisania danych bez ponownego uruchamiania procesu, napiszemy mały program, który będzie zapisywał na dysk około 1 megabajta na sekundę. Jeśli z jakiegoś powodu nie uda nam się zapisać danych na dysku, po prostu zignorujemy to i spróbujemy zapisać dane ponownie za sekundę. W przykładzie używam Pythona, możesz użyć dowolnego innego języka programowania.

[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

Uruchomimy program i przyjrzymy się deskryptorom plików

[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

Jak widzimy, mamy nasze 3 standardowe deskryptory plików i jeden, który otworzyliśmy. Sprawdźmy rozmiar pliku:

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

Dane są zapisywane, spróbujmy zmienić prawa do pliku:

[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

Widzimy, że dane wciąż są zapisywane, chociaż nasz użytkownik nie ma praw do zapisu w pliku. Spróbujmy go usunąć:

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

Gdzie są zapisywane dane? I czy w ogóle są zapisywane? Sprawdzamy:

[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)

Tak, nasz deskryptor pliku wciąż istnieje, i możemy pracować z tym deskryptorem pliku jak z naszym starym plikiem, możemy go czytać, czyścić i kopiować.

Sprawdźmy rozmiar pliku:

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

Rozmiar pliku to 19923457. Spróbujmy wyczyścić plik:

[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

Jak widzimy, rozmiar pliku tylko się zwiększa i nasz truncate nie zadziałał. Odwołajmy się do dokumentacji dotyczącej wywołań systemowych. openJeśli przy otwieraniu pliku używamy flagi O_APPEND, to przy każdym zapisie system operacyjny sprawdza rozmiar pliku i zapisuje dane na końcu pliku, a robi to atomowo. To pozwala wielu wątkom lub procesom zapisywać do tego samego pliku. Jednak w naszym kodzie nie używamy tej flagi. Możemy zobaczyć inny rozmiar pliku w lsof po truncate, tylko jeśli otworzymy plik do dopisywania, a więc w naszym kodzie zamiast

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

powinniśmy wpisać

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

Sprawdzamy z flagą „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

i z flagiem „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

Programujemy już uruchomiony proces

Często programiści przy tworzeniu i testowaniu programów używają debuggerów (np. GDB) lub różnych poziomów logowania w aplikacji. Linux umożliwia faktycznie pisanie i modyfikowanie już uruchomionego programu, na przykład zmienianie wartości zmiennych, ustawianie punktów przerwania itd.

Wracając do pierwotnego pytania o brak miejsca na dysku do zapisania pliku, spróbujemy zasymulować problem.

Stworzymy plik dla naszej sekcji, który podmontujemy jako osobny dysk:

[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 ~]$

Stworzymy system plików:

[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 ~]$

Podmontujemy system plików:

[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

Tworzymy katalog z naszym właścicielem:

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

Otworzymy plik tylko do zapisu w naszej aplikacji:

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

Uruchamiamy

[user@localhost ]$ python openforwrite.py 

Czekamy kilka sekund

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

Tak więc otrzymaliśmy problem opisany na początku tego artykułu. Wolnego miejsca 0, zajęte 100%.

Pamiętamy, że w warunkach zadania próbujemy zapisać bardzo ważne dane, które nie mogą zostać utracone. A przy tym musimy naprawić usługę bez ponownego uruchamiania procesu.

Załóżmy, że mamy jednak miejsce na dysku, ale w innej partycji, na przykład w /home.

Spróbujmy „przeprogramować na bieżąco” nasz kod.

Sprawdzamy PID naszego procesu, który zajmuje całe miejsce na dysku:

[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

Łączymy się z procesem przez gdb

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

Sprawdzamy otwarte deskryptory plików:

(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

Sprawdzamy informacje o deskryptorze pliku z numerem 3, który nas interesuje

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

Pamiętając, jaki systemowy wywołanie wykonuje Python (zobacz wyżej, gdzie uruchomiliśmy strace i znaleźliśmy wywołanie open), przetwarzając nasz kod do otwarcia pliku, robimy to samo osobiście w imieniu naszego procesu, ale bity O_WRONLY|O_CREAT|O_TRUNC musimy zastąpić wartością numeryczną. W tym celu otwieramy źródła jądra, na przykład tutaj i sprawdzamy, do czego odpowiadają poszczególne flagi

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

Łączymy wszystkie wartości w jedną, otrzymujemy 00001101

Uruchamiamy nasze wywołanie z gdb

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

Zatem otrzymaliśmy nowy uchwyt pliku o numerze 4 i nowo otwarty plik na innym systemie plików, sprawdzamy:

(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

Pamiętamy przykład z pipe — jak bash zmienia uchwyty plików i już nauczyliśmy się wywołania systemowego dup2.

Próbujemy zamienić jeden uchwyt pliku na inny

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

Sprawdzamy:

(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

Zamykamy uchwyt pliku 4, ponieważ nie jest już potrzebny:

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

I wychodzimy z 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

Sprawdzamy nowy plik:

[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

Jak widzimy, dane są zapisywane w nowym pliku, sprawdzamy stary:

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

Dane nie zostały utracone, aplikacja działa, logi są zapisywane w nowym miejscu.

Nieco utrudnimy zadanie

Wyobraźmy sobie, że dane są dla nas ważne, ale nie mamy miejsca na dysku w żadnym z systemów plików i nie możemy podłączyć dysku.

Co możemy zrobić, to przekierować nasze dane gdzieś indziej, na przykład do pipe, a dane z pipe z kolei przekierować do sieci przez jakąś aplikację, na przykład netcat.
Możemy stworzyć nazwany pipe poleceniem mkfifo. Stworzy to pseudo-pliki w systemie plików, nawet jeśli nie ma w nim wolnego miejsca.

Restarting the application and checking:

[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

There is no disk space, but we successfully create a named pipe there:

[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

Now we need to somehow wrap all the data coming into this pipe to another server over the network, for this we can use netcat.

On the server remote-server.example.com we start

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

On our problematic server we start in a separate terminal

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

Now all the data that enters the pipe will automatically be sent to stdin in netcat, which will send them over the network to port 7777.

All we have left to do is start writing our data into this named pipe.

We already have an application running:

[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

From all the flags, we only need O_WRONLY since the file already exists and we don't need to clear it.

[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

Sprawdzamy zdalny serwer remote-server.example.com

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

Dane płyną, sprawdzamy problematyczny serwer

[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

Dane zostały zapisane, problem został rozwiązany.

Korzystając z okazji, pozdrawiam kolegów z firmy Degiro.
Słuchaj podcastów Radia-T.

Wszystkiego dobrego.

Jako pracę domową proponuję zastanowić się, co będzie w plikach deskryptorów procesu cat i sleep, jeśli uruchomisz taką komendę:

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

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster