ridirezionamento di i/o con bash: un breve approfondimento

9
BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 Uno sguardo approfondito alla redirezione dell'I/O con Bash di Roberto Polli Redirigere l’I/O La redirezione dell'I/O è il processo atto a controllare l'Input/Output di un programma. Quando si scrive un programma o uno script è possibile leggere dalla console e scrivere sullo schermo. I file standard usati per l'I/O sono tre: il programma legge da standard input, scrive il risultato delle operazioni su standard output e invia messaggi di errore a standard error. Su un sistema Unix, i processi identificano ogni file con un numero intero progressivo: il file descriptor. Questo numero è univoco all'interno di ogni processo. Quando un processo apre un file, gli assegna un nuovo file descriptor. STDIN, STDOUT e STDERR sono associati rispettivamente ai primi tre interi: 0, 1 e 2. Su Linux è possibile trovare i file descriptor di un processo tramite il filesystem /proc. Ad esempio elencando i file descriptor del processo con pid 1201 si può notare che oltre a quelli di default ce n'è un altro: crond.pid. Ciò significa che il processo con pid 1201 ha aperto il file /var/run/crond.pid. # ls /proc/1201/fd/ -l total 0 lr-x------ 1 root root 64 2011-06-10 08:43 0 -> /dev/null l-wx------ 1 root root 64 2011-06-10 08:43 1 -> /dev/null l-wx------ 1 root root 64 2011-06-10 08:43 2 -> /dev/null lrwx------ 1 root root 64 2011-06-10 08:43 3 -> /var/run/crond.pid L'unicità dei file descriptor è relativa al processo: se un altro processo apre lo stesso file (ex. /var/run/crond.pid) gli potrà assegnare un altro intero (es. 5).

Upload: babel

Post on 13-Jun-2015

441 views

Category:

Technology


0 download

DESCRIPTION

In questo articolo, il TechAdvisor Babel Roberto Polli esplora la funzionalità di ridirezionamento dell'Input/Output con Bash (Bourne Again Shell), la più diffusa shell per sistemi GNU/Linux. La guida, rivolta ai sistemisti junior, propone una panoramica sulla gestione dei tre file standard assegnati da GNU/Linux ad ogni processo: input, output ed error. Particolare attenzione è dedicata a strace, uno strumento molto utile che permette di tenere sotto controllo le chiamate di sistema, utilizzato in questo caso per svelare il funzionamento di Bash.

TRANSCRIPT

Page 1: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

Uno sguardo approfondito alla

redirezione dell'I/O con Bash

di Roberto Polli

Redirigere l’I/O

La redirezione dell'I/O è il processo atto a controllare l'Input/Output di un programma. Quando

si scrive un programma o uno script è possibile leggere dalla console e scrivere sullo schermo.

I file standard usati per l'I/O sono tre: il programma legge da standard input, scrive il risultato

delle operazioni su standard output e invia messaggi di errore a standard error.

Su un sistema Unix, i processi identificano ogni file con un numero intero progressivo: il file

descriptor. Questo numero è univoco all'interno di ogni processo. Quando un processo apre un

file, gli assegna un nuovo file descriptor. STDIN, STDOUT e STDERR sono associati rispettivamente

ai primi tre interi: 0, 1 e 2.

Su Linux è possibile trovare i file descriptor di un processo tramite il filesystem /proc.

Ad esempio elencando i file descriptor del processo con pid 1201 si può notare che oltre a quelli

di default ce n'è un altro: crond.pid. Ciò significa che il processo con pid 1201 ha aperto il file

/var/run/crond.pid.

# ls /proc/1201/fd/ -l

total 0

lr-x------ 1 root root 64 2011-06-10 08:43 0 -> /dev/null

l-wx------ 1 root root 64 2011-06-10 08:43 1 -> /dev/null

l-wx------ 1 root root 64 2011-06-10 08:43 2 -> /dev/null

lrwx------ 1 root root 64 2011-06-10 08:43 3 -> /var/run/crond.pid

L'unicità dei file descriptor è relativa al processo: se un altro processo apre lo stesso file (ex.

/var/run/crond.pid) gli potrà assegnare un altro intero (es. 5).

Page 2: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

Es: questo comando mostra tutti i processi che usano /etc/passwd mostrando il loro pid e i

corrispondenti file descriptor. Consultare #man find per ulteriori informazioni sul comando find.

#sudo find /proc/ -lname /etc/passwd -a ! -path \*task\* 2>/dev/null

/proc/2374/fd/14

/proc/5848/fd/43

La redirezione dell'I/O si verifica quando si dirotta il flusso di dati su file differenti. Il comando

precedente ne contiene un esempio. L'ultima parte “2>/dev/null” indica a Bash di redirigere i

messaggi di errore di #find su /dev/null.

Flusso standard di Bash

Il flusso I/O standard di Bash è verificabile nel lavoro quotidiano, ad es. lanciando il comando

#ls.

#ls -l /etc/passwd unexistent.txt

ls: cannot access unexistent.txt: No such file or directory

-rw-r--r-- 1 root root 1953 2011-04-18 18:45 passwd

Le righe di output inviate alla console sono parte di due flussi differenti: il primo – contenente un

messaggio di errore – è inviato a STDERR, mentre il secondo – essendo il vero output del

comando – è inviato a STDOUT. Su un terminale, sia STDOUT che STDERR sono inviati solitamente

allo stesso schermo.

Piping e redirezione dell’output

Nel lavoro quotidiano è consuetudine “mettere in pipe” i comandi, ad esempio:

# cat /etc/passwd | grep “wheel”

Il piping è una forma di redirezione dell'I/O: in questo caso l'output di #cat viene inviato all'input

di #grep – o più precisamente associa lo STDIN di #grep allo STDOUT di #cat.

Un'altra modalità spesso utilizzata è la redirezione verso un file. Questa può essere fatta in

modalità APPEND (accodamento) o TRUNCATE (troncamento). Il troncamento si effettua con

Page 3: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

“>”. L'output di #grep è scritto su /tmp/grep.out. Se il file non esiste, verrà creato, se esiste sarà

invece sovrascritto.

# grep wheel /etc/group > /tmp/grep.out

oppure – poiché “1” rappresenta il file descriptor STDOUT:

# grep wheel /etc/group 1> /tmp/grep.out

La modalità APPEND usa l'operatore “>>”. In questo caso l'output è accodato al file in

questione:

# grep users /etc/group >> /tmp/grep.out

Le ultime versioni di Bash (con le vecchie versioni non funziona) permettono di redirigere

contemporaneamente STDOUT e STDERR in due modi:

mediante l'operatore >&

# find /proc >& /dev/null

mediante l'operatore |&

#find /proc |& grep task

Andando in profondità

Detto ciò sembra che la redirezione sia una cosa immediata ma in realtà non è proprio così.

Analizzando cosa accade al seguente comando si ha che:

#ls -d /etc/ unexisting.txt > /tmp/ls.out

ls: cannot access unexisting.out: No such file or directory

Come previsto STDOUT finisce nel file /tmp/ls.out, mentre STDERR viene stampato a schermo

segnalando che il file unexisting.txt non esiste.

Ora facciamo pulizia nel nostro ambiente e modifichiamo un po' il comando. Proviamo a

indovinare il risultato: quale sarà il contenuto di STDOUT/STDERR?

#rm -f unexisting.out;

#ls -l unexisting.out > unexisting.out

Page 4: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

Cos'è accaduto? Perché il risultato è diverso dal comando precedente? Sembra che la

redirezione dell'I/O con Bash non sia completamente trasparente. Infatti essa è soggetta

all'implementazione di Bash. Usando un tool come #strace, possiamo analizzare le operazioni

effettuate da Bash.

Strace world

Strace è un potente tool per tracciare le system call. Mostra cosa fa un programma e può

essere utilizzato per capire cos'è accaduto durante l'esecuzione di un comando.

Il primo tentativo porterebbe ad utilizzarlo nel seguente modo:

# strace ls unexisting.out > unexisting.out

Questo tipo di utilizzo è sbagliato, in quanto staremmo semplicemente redirigendo l'output di

# strace ls unexisting.out

nel file unexisting.out.

E' comunque interessante vedere il risultato del comando precedente. L'output a schermo è

molto “verboso” – circa 188 linee. Quanto basta per imparare che anche un comando

semplice come #ls non è poi così semplice.

La prima call esegue il comando ls:

execve("/bin/ls", ["ls", "unexisting.out"], [/* 46 vars */]) = 0

brk(0) = 0x7f9000

seguono le syscall contenute in ls per caricare le librerie condivise…

access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or

directory)

mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =

0x7f32158f6000

access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or

directory)

Page 5: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

open("/etc/ld.so.cache", O_RDONLY) = 3

fstat(3, {st_mode=S_IFREG|0644, st_size=126913, ...}) = 0

mmap(NULL, 126913, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f32158d7000

close(3) = 0

...

… per usare SELinux e altra roba…

open("/lib/libselinux.so.1", O_RDONLY) = 3

mprotect(0x7f3214905000, 4096, PROT_READ) = 0

mprotect(0x7f3214b09000, 4096, PROT_READ) = 0

...

open("/proc/filesystems", O_RDONLY) = 3

… per usare le librerie di localizzazione…

open("/usr/lib/locale/locale-archive", O_RDONLY) = -1 ENOENT (No such file or

directory)

open("/usr/share/locale/locale.alias", O_RDONLY) = 3

open("/usr/lib/locale/en_GB.utf8/LC_IDENTIFICATI/ON", O_RDONLY) = 3

...

…infine (e solo infine) per operare su STDOUT scrivendo il risultato della system call stat() - quella

usata da #ls.

stat("unexisting.out", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0

lstat("unexisting.out", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0

fstat(1, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =

0x7f3215774000

write(1, "unexisting.out\n", 15) = 15

L’ultima operazione prevede la chiusura di tutti i file descriptor.

close(1) = 0

close(2) = 0

Page 6: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

Per avere un output più breve è possibile evitare il caricamento di tutte le librerie di

localizzazione e salvare circa 70 syscall() impostando una variabile di ambiente:

#export LANG=C

Suggerimento: meno syscall() vuol dire comandi più veloci: se si sta parsando un file ASCII di

qualche Gb, è bene ricordarsi di fare #export LANG=C!

Strace su una shell

Ora che si è compreso l'output di strace, si può procedere applicandolo su Bash. Apriamo un

nuovo terminale e tracciamo le syscall utilizzate dal processo Bash:

terminalA# echo "Bash pid is $$"

Bash pid is 2043

terminalB# strace -p 2043

Process 2043 attached - interrupt to quit

read(0,

Ora terminalB traccerà ogni tasto che si preme sul terminalA, così è possibile saltare un sacco di

righe tipo la seguente:

rt_sigprocmask(SIG_BLOCK, [INT], [], 8) = 0

ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig -icanon -echo

...}) = 0

Per farlo ovviamente si redirige l'output di strace sul comando grep, tagliando le righe che non

interessano utilizzando il nuovo operatore |&:

# strace -p 2043 |& egrep -v 'rt_sig|ioctl'

Ecco l'output ripulito:

pipe([3, 4]) = 0

clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,

child_tidptr=0x7f1347e1c9d0) = 4381

setpgid(4381, 4381) = 0

Page 7: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

close(3) = 0

close(4) = 0

wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WSTOPPED|WCONTINUED, NULL)

= 4381

--- SIGCHLD (Child exited) @ 0 (0) ---

wait4(-1, 0x7fff4cf1bc5c, WNOHANG|WSTOPPED|WCONTINUED, NULL) = -1 ECHILD (No

child processes)

write(2, "\33]0;rpolli@rpolli: /tmp\7rpolli@r"..., 47) = 47

Fino a qui non è apparso nessun riferimento al comando ls, né ad unexisting.out! E’ comunque

evidente che Bash:

1. crea una pipe() con i file descriptor 3 e 4;

2. forka() creando un nuovo processo figlio;

3. aspetta che il figlio ritorni con la syscall wait();

4. ritorna al prompt.

Il figlio di una shell

Tracciamo ancora più in profondità, dicendo a #strace di seguire (follow) i processi figli della

shell. Per farlo bisogna usare l'opzione “-f”:

# strace -f -p 2043 |& egrep -v 'rt_sig|ioctl'

Vediamo quindi che il processo figlio fa qualcosa in più rispetto a prima – quando si limitava ad

eseguire la exec() del comando ls:

[pid 4469] open("unexisting.out", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3

[pid 4469] dup2(3, 1) = 1

[pid 4469] close(3) = 0

[pid 4469] execve("/bin/ls", ["ls", "--color=auto", "unexisting.out"], [/*

46 vars */]) = 0

[pid 4469] brk(0) = 0xeeb000

In pratica:

1. crea il file unexisting.out - spiegando così perchè #ls trova il file;

2. maneggia i file descriptor 3 (associato ad unexisting.out) ed 1 (che è STDOUT) con

l'operatore dup2();

Page 8: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

3. prosegue come le analisi precedenti.

Maneggiare i file descriptor

Guardando la #man di dup2():

int dup2(int oldfd, int newfd);

..

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary...

Questo vuol dire che dopo il fork(), il processo figlio crea un nuovo file unexisting.out, gli assegna

il file descriptor numero 3 e invoca la system call dup2() che:

1. chiude il file descriptor #1 del processo figlio – standard output;

2. duplica il file descriptor #3 associandolo al file descriptor #1: da quel momento ogni

chiamata che utilizzerà il file descriptor #1 verrà effettuata sul file associato al file

descriptor #3 – ossia unexisting.out;

3. chiude il file descriptor #3 poiché il file unexisting.out è oramai associato allo STDOUT (file

descriptor #1).

Page 9: Ridirezionamento di I/O con Bash: un breve approfondimento

BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680

E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it

Reg. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v.

Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015

Licenza d'uso “Attribuzione - Non commerciale - Non opere derivate”, secondo i criteri

internazionali Creative Commons (http://creativecommons.org/licenses/by-nc-nd/2.5/it/)