Datotečni descriptor v Linuxu s primeri

Enkrat, na enem intervjuju so me vprašali, kaj bi naredil, če bi odkril, da storitev ne deluje, ker je na disku zmanjkalo prostora?

Seveda sem odgovoril, da bom pogledal, kaj zaseda to mesto in če je mogoče, bom sprostil prostor.
Takrat je intervjuvalec vprašal, kaj pa, če na particiji ni prostega mesta, a tudi datotek, ki bi zasedale ves prostor, ne vidiš?

Na to sem rekel, da lahko vedno pogledam odprte opisnike datotek, na primer z ukazom lsof, in razumem, katera aplikacija je zasedla ves razpoložljiv prostor, nato pa lahko ukrepam glede na okoliščine, odvisno od tega, ali so podatki potrebni.

Intervjuvalec me je prekinil pri zadnji besedi in dopolnil vprašanje: „Recimo, da podatki niso pomembni, to je samo log za odpravljanje napak, vendar aplikacija ne deluje, ker ne more zapisati loga za odpravljanje napak“?

„Okej,“ sem odgovoril, „lahko izklopimo logiranje za odpravljanje napak v konfiguraciji aplikacije in jo ponovno zagnamo.“
Intervjuvalec je nasprotoval: „Ne, aplikacije ne moremo znova zagnati, v pomnilniku še vedno hranimo pomembne podatke, pomembni odjemalci so povezani s to storitvijo in ne moremo jih prisiliti, da se ponovno povežejo.“

„No dobro,“ sem rekel, „če aplikacije ne moremo znova zagnati in podatki za nas niso pomembni, lahko preprosto očistimo ta odprt datotečni opisnik, tudi če ga ne vidimo z ukazom ls v datotečnem sistemu.“

Intervjuvalec je bil zadovoljen, jaz pa ne.

Takrat sem pomislil, zakaj oseba, ki preverja moje znanje, ne koplje dlje? Kaj pa, če so podatki kljub vsemu pomembni? Kaj, če ne moremo znova zagnati procesa, medtem ko ta proces piše v datotečni sistem na particijo, kjer ni prostega mesta? Kaj, če ne moremo izgubiti ne le že zapisanih podatkov, temveč tudi podatkov, ki jih ta proces piše ali poskuša zapisati?

Tuzik

Na začetku svoje kariere sem poskušal ustvariti majhno aplikacijo, v kateri je bilo treba shraniti informacije o uporabnikih. Takrat sem razmišljal, kako povezati uporabnika s njegovimi podatki. Recimo, da imam Ivana Ivanoviča, in ima določene podatke, kako jih povezati? Lahko neposredno navedem, da pes po imenu „Tuzik“ pripada temu Ivanu. Ampak kaj, če spremeni ime in postane, na primer, Olja? Takrat bi naša Olja Ivanovna Ivanova več ne imela psa, naš Tuzik pa bi še vedno pripadal neobstoječemu Ivanu. To težavo je rešil sistem za baze podatkov, ki je vsakemu uporabniku dodelil edinstven identifikator (ID), in moj Tuzik se je povezal s tem ID-jem, ki je v bistvu bil le zaporedna številka. Tako je imel lastnik Tuzika ID s številko 2, in v nekem trenutku je ta ID pripadal Ivanu, nato pa je ta ID pripadal Oljai. Težava človeštva in živinoreje je bila skoraj rešljiva.

Datotečni descriptor

Težava datoteke in programa, ki dela s to datoteko, je podobna naši psu in človeku. Predpostavimo, da sem odprl datoteko z imenom ivan.txt in začel vanjo zapisovati besedo tuzik, vendar sem uspel zapisati le prvo črko „t“ v datoteko, nato pa je bila ta datoteka preimenovana, na primer v olya.txt. Vendar je datoteka ostala ista in še vedno želim zapisati svojega tuzika vanjo. Vsakič, ko odprejo datoteko z sistemskim klicem open v katerem koli programskem jeziku dobim edinstven ID, ki mi kaže na datoteko, ta ID je datotečni descriptor. In ni pomembno, kaj in kdo dela s to datoteko naprej, lahko jo izbrišejo, preimenujejo, lahko zamenjajo lastnika ali vzamejo pravice za branje in zapisovanje, še zmeraj imam dostop do nje, ker sem ob odprtju datoteke imel pravice za njeno branje in/ali zapisovanje in sem jo uspel začeti obdelovati, kar pomeni, da moram to nadaljevati.

V Linuxu knjižnica libc odpre 3 datotečne deskriptorje za vsako zagnano aplikacijo (proces), z številkami 0, 1, 2. Več informacij lahko najdete na povezavah man stdio in man stdout

  • Datotečni descriptor 0 se imenuje STDIN in je povezan z vnosom podatkov v aplikacijo.
  • Deskriptor fájl 1 er kallaður STDOUT og er notaður af forritum til að úttak á gögn, til dæmis útgáfu skipana eins og print.
  • Deskriptor fáíl 2 er kallaður STDERR og er notaður af forritum til að úttak á gögn sem tilkynna villur.

Ef þú opnar hvaða skjal sem er til lesa eða skrifa í forritinu þínu, færðu líklega fyrsta frjálst ID, sem verður númer 3.

Listi yfir skjalaskrár getur verið skoðaður hjá hvaða ferli sem er, ef þú veist PID þess.

Til dæmis skulum við opna skipanalínu með bash og skoða PID ferlisins okkar.

[user@localhost ]$ echo $$
15771

Í annarri skipanalínu skulum við keyra.

[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

Deskriptor fáíl með númeri 255 geturðu verið sár í þessari grein, hann var opnaður af bash sjálfu, ekki tengdri bókasafni.

Í dag tengjast allar 3 skjalaskrár tækinu falsa terminal. /dev/pts, en við getum samt stjórnað þeim, til dæmis skulum við keyra í annarri skipanalínu.

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

Og í fyrri skipanalínu munum við sjá.

[user@localhost ]$ hello world

Umleiðing og Rör

Þú getur auðveldlega yfirlýst þessar 3 skjalaskrár í hvaða ferli sem er, þ.m.t. bash, til dæmis í gegnum rör (pipe) sem tengja tvö ferli, sjáum.

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

Þú getur keyrt þessa skipun sjálfur með. strace -f og séð hvað gerist innandyra, en ég mun stuttlega útskýra.

Móðir ferlið okkar bash með PID 15771 parsear skipunina okkar og ákveður hversu margar skipanir við viljum keyra, í okkar tilfelli eru þær tvær: cat og sleep. Bash veit að það þarf að búa til tvö barnið ferli, og sameina þau með einu rör. Þannig verður bash að búa til 2 barnið ferli og eitt rör.

Fyrir foreldri ferlið virðist eins og rör sé þegar til, en barnið ferli er enn ekki til: cevi PID skipun 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

Za starševski proces to izgleda, kot da je cev že prisotna, medtem ko podprocesi še ne obstajajo:

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

Nato z sistemskim klicem clone bash ustvari dva otroška procesa, in naši trije procesi bodo izgledali takole:

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

Ne pozabite, da clone klonira proces skupaj z vsemi datotečnimi deskriptorji, zato bodo v starševskem in otroških procesih enaki. Naloga starševskega procesa z PID 15771 je spremljati otroške procese, zato samo čaka na odgovor od njih.

Zato mu pipe ni potreben, in zapre datotečne deskriptorje z številkami 3 in 4.

V prvem otroškem procesu bash s PID 9004, z sistemskim klicem dup2, spremeni naš STDOUT datotečni deskriptor s številko 1 na datotečni deskriptor, ki kaže na pipe, v našem primeru je to številka 3. Tako vse, kar bo prvotni otroški proces s PID 9004 napisal v STDOUT, avtomatsko pride v buffer pipe-a.

V drugem otroškem procesu s PID 9005 bash s pomočjo dup2 spremeni datotečni deskriptor STDIN s številko 0. Zdaj vse, kar bo bral naš drugi bash s PID 9005, bo bral iz pipe-a.

Po tem se v otroških procesih prav tako zaprejo datotečni deskriptorji z številkami 3 in 4, saj se več ne uporabljajo.

Datotečni deskriptor 255 namerno ignoriram, uporablja se za notranje potrebe samega basha in bo prav tako zaprt v otroških procesih.

Nato v prvem otroškem procesu s PID 9004 bash zažene z sistemskim klicem exec izvedbeno datoteko, ki smo jo navedli v ukazni vrstici, v našem primeru je to /usr/bin/cat.

V drugem otroškem procesu s PID 9005 bash zažene drugo izvedbeno datoteko, ki smo jo navedli, v našem primeru je to /usr/bin/sleep.

Sistemskik poziv exec ne zatvara deskriptore datoteka ako nisu otvoreni s flagom O_CLOEXEC prilikom poziva open. U našem slučaju, nakon pokretanja izvršnih fajlova, svi trenutni deskriptori datoteka će ostati sačuvani.

Proveravamo u konzoli:

[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

Kao što vidite, jedinstveni broj našeg pipe-a u oba procesa se poklapa. Tako imamo vezu između dva različita procesa sa jednim roditeljem.

Za one koji nisu upoznati sa sistemskim pozivima koje koristi bash, toplo preporučujem da pokrenu komande preko strace-a i vide šta se dešava unutra, na primer ovako:

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

Vratimo se našem problemu sa manjkom prostora na disku i pokušajem da sačuvamo podatke bez ponovnog pokretanja procesa. Napisaćemo malu aplikaciju koja će zapisivati na disk otprilike 1 megabajt u sekundi. Ako zbog bilo kakvog razloga ne budemo mogli da zapišemo podatke na disk, jednostavno ćemo to ignorisati i pokušati ponovo za sekundu. U primeru koristim Python, možete koristiti bilo koji drugi programski jezik.

[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

Pokrenimo program i pogledajmo deskriptore datoteka

[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

Kotopućemo da imamo naša 3 standardna opisnika datoteka i još jedan koji smo otvorili. Proverimo veličinu datoteke:

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

Podaci se pišu, pokušavamo promeniti prava na datoteku:

[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

Vidimo da se podaci još uvek pišu, uprkos tome što naš korisnik nema prava da piše u datoteku. Pokušaćemo da je obrišemo:

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

Gde se podaci pišu? I da li se uopšte pišu? Proveravamo:

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

Da, naš opisnik datoteka još uvek postoji, i možemo raditi s tim opisnikom datoteka kao s našom starom datotekom, možemo je čitati, brisati i kopirati.

Gledamo na veličinu datoteke:

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

Veličina datoteke je 19923457. Pokušavamo da očistimo datoteku:

[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

Kao što vidimo, veličina datoteke se samo povećava i naš truncate nije uspeo. Obratićemo se dokumentaciji o sistemskom pozivu. openAko prilikom otvaranja datoteke koristimo flag O_APPEND, tada operativni sistem prilikom svake upisivanja proverava veličinu datoteke i piše podatke na kraj datoteke, a to se radi atomarno. To omogućava više niti ili procesa da pišu u istu datoteku. Ali u našem kodu ne koristimo ovaj flag. Možemo videti drugu veličinu datoteke u lsof nakon truncate-a samo ako otvorimo datoteku za dopisivanje, a to znači da u našem kodu umesto

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

trebalo bi da stavimo

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

Proveravamo s flagom „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

in z zastavico «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

Programiramo že zagnan proces

Programerji pogosto pri ustvarjanju in testiranju programa uporabljajo debagerje (kot je GDB) ali različne ravni beleženja v aplikaciji. Linux omogoča dejansko pisanje in spreminjanje že zagnanega programa, na primer spreminjanje vrednosti spremenljivk, nastavitev breakpointov itd.

Vrnil se bom k izvirnemu vprašanju o pomanjkanju prostora na disku za zapisovanje datoteke in poskusil posimulirati težavo.

Ustvarili bomo datoteko za naš segment, ki ga bomo povezali kot ločen disk:

[user@localhost ~]$ dd if=/dev/zero of=~/tempfile_for_article.dd bs=1M count=10
10+0 zapisov v
10+0 zapisov iz
10485760 bajtov (10 MB) kopiranih, 0.00525929 s, 2.0 GB/s
[user@localhost ~]$

Ustvarimo datotečni sistem:

[user@localhost ~]$ mkfs.ext4 ~/tempfile_for_article.dd
mke2fs 1.42.9 (28-Dec-2013)
/home/user/tempfile_for_article.dd ni blok naprava.
Ali želite nadaljevati? (y,n) y
...
Pisanje superblokov in informacij o datotečnem sistemu: končano
[user@localhost ~]$

Povezujemo datotečni sistem:

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

Ustvarimo direktorij z našim lastnikom:

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

Odprli bomo datoteko samo za zapis v našem programu:

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

Začnemo

[user@localhost ]$ python openforwrite.py 

Počakamo nekaj sekund

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

Torej, imamo težavo, opisano na začetku tega članka. Prostega prostora 0, zasedenega 100%.

Spomnimo se, da poskušamo zapisati zelo pomembne podatke, ki jih ne smemo izgubiti. Hkrati moramo popraviti storitev brez ponovnega zagonjenja procesa.

Recimo, da imamo kljub vsemu prostor na disku, vendar v drugem razdelku, na primer v /home.

Poskusimo "preprogramirati v letu" našo kodo.

Poglejmo PID našega procesa, ki je vzel ves prostor na disku:

[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

Povežemo se na proces preko gdb

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

Poglejmo odprte datotečne descriptorje:

(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

Poglejmo informacije o datotečnem descriptorju s številko 3, ki nas zanima

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

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

#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

Preverimo:

(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. Она создаст псевдофайл на файловой системе, даже если на ней нет свободного места.

Ponovno pokrećemo aplikaciju i proveravamo:

[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

Nemamo prostora na disku, ali uspešno pravimo named 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

Sada treba nekako da obavijemo sve podatke koji dolaze u ovaj pipe na drugi server putem mreže, za to ćemo koristiti netcat.

Na serveru remote-server.example.com pokrećemo

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

Na našem problematičnom serveru pokrećemo u zasebnom terminalu

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

Svi podaci koji uđu u pipe automatski će ući u stdin u netcat-u, koji će ih poslati u mrežu na port 7777.

Sve što treba da uradimo je da počnemo da pišemo naše podatke u ovaj named pipe.

Već imamo pokrenutu aplikaciju:

[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

Od svih flagova, potrebna nam je samo O_WRONLY jer fajl već postoji i ne treba nam da ga brišemo

[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

Preverjamo oddaljeni strežnik remote-server.example.com

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

Podatki tečejo, preverjamo problematični strežnik

[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

Podatki so shranjeni, težava je rešena.

Ob tej priložnosti pošiljam pozdrav kolegom iz podjetja Degiro.
Poslušajte podkaste Radio-T.

Vsem dobro.

Kot domačo nalogo vam predlagam, da pomislite, kaj bo v datoteki opisnikih procesov cat in sleep, če zaženete ta ukaz:

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

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster