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 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 i
- 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. , 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 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 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 , 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 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. Jeś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 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
