petak, 1. lipnja 2007.

Koprocesi

UNIX sistemski filter jest bilo program koji čita sa standardnog ulaza i piše na standardni izlaz. Obično se ulančavaju cjevovodima u shellu (koji su pri tome, kao što je Oleg Kiselyov jednoć pokazao, pri tome monadi!), filtar se nazivno promovira u koproces u trenutku kada isti program generira filtrov ulaz i čita mu izlaz, efektivno ga utilizirajući kao subsidijalnu računsku jedinku.

Iako popularniji shellovi ne podržavaju izravno koprocese (pingvinski bash/sh/csh), neki kao ksh jesu, i to uz poprilično gadnu sintaksu. No svejedno, sa stajališta sistemskih programa, koprocesi su jako korisna koncepcija. Obično ih ostvarujemo kreiranjem po para jednosmjernih (unidirekcionalnih, half-duplex - iako POSIX.1 dozvoljava ali ne zahtijeva full-duplex) cjevovoda (pipes), koje u roditelju otvaramo za čitanje/pisanje, dok u forkanom djetetu u kojem exec*-amo koproces komplementarno za pisanje/čitanje.

U 5. labosu iz kolegija Operacijski Sustavi 1, koncept se koprocesa rabi za provjeru ispravnosti korisničkih aritmetičkih izraza, projvjeravajući njihovu validnost i krajnji rezultat transparentno preko bc(1) koprocesa. Sva rješenja dotičnog zadatka koja sam ja vidio od svojih kolega, a bome i primjeri koji su asistenti-pingvini dali na većspomenutim stranicama, imaju jedan kritičan race condition.

Naime, problem leži u standardnoj I/O buffering polisi, koja se programski dade kontrolirati (sa setvbuf(3), odnosno eksplicitnim fflush(3)-anjem nakon svakog pisanja), no to postaje malo nedohvatljivo u slučaju pretkompiliranog binarya kojeg ne možemo izmjeniti (bar ne koristeći neki platformski-nezavisan POSIX-kompatibilan način). ISO C zahtijeva sljedeće buffering karakteristike:

  • Standardni ulaz i standardni izlaz su u potpunosti buffered, akko se ne odnose na interaktivni uređaj.
  • Standardni izlaz za greške nikad nije u potpunosti buffered.

Ovo, međutim, ne kaže mogu li stdin/stdout biti unbuffered ili linijski bufferirani ukoliko su vezani za interkativni uređaj, kao i treba li stderr biti unbuffered ili linijski buffered. Većina implementacija (POSIX je specifikacija, ne implementacija!) po defaultu koriste sljedeće tipove:

  • Standardni izlaz za greške je uvijek unbuffered.
  • Svi su drugi streamovi linijski bufferirani ako se odnose na terminal, inače su fully buffered (obično po 1024B).

Standardni ulaz i izlaz koprocesa evidentno nisu vezani za terminal - i tu leži problem. Oni read(2)-ovi na izlaz iz koprocesa mogu potencijalno zauvijek blokirati pozivatelja koji čita sve dok ne dobije '\n' (bez obzira čak i ako koristimo neblokirajuće čitanje u petlji - još gore, sad imamo livelock!). Pozivatelj čeka na izlaz iz koprocesa, koproces čeka na ulaz iz pozivatelja, I/O podsustav čeka da u potpunosti bufferirani izlaz iz koprocesa pređe predefinirani prag popunjenja pa da ga može flushati i poslati pozivatelju - deadlock smrdi na kilometar.

Teži način rješavanja ovog potencijalno cirkularnog čekanja jest pokretanje koprocesa pod pseudoterminalom kojeg ustvari exec*-amo i koji najposlije pokreće koproces. Pisanje drivera za vlastiti pseudoterminalni uređaj je netrivijalno (dobro ne baš toliko teško, ali u svakom slučaju previše za nekoga tko samo želi naučiti elementarne IPC koncepte, a osim toga postoje i začkoljice o tome radi li se o BSD ili SystemV...), iako dakako postoje već gotova rješenja na netu.

Drugi je, lakši i hax0rskiji način, redefinirati isatty(2) tako da uvijek vraća istinu:

_isatty(int fd) { return 1;}

cc -G moj_isatty.c -o moj_isatty.so -Kpic

i potom na Solaris kanti na pinusu (naziv studentskog stroja) dinamički linkati na tu verziju podešujući okolinu koprocesa (odmah nakon forkanja):


LD_PRELOAD=/path/to/moj_isatty.so

Nema komentara: