Vyšlo v týdeníku Computerworld č. 17/93 v roce 1993
Vytištěno z adresy: http://www.earchiv.cz/a93/a317c110.php3

Transportní rozhraní - Streams a TLI

V minulých dílech seriálu jsme se začali zabývat tím, jak je v prostředí operačního systému Unix implementováno transportní rozhraní k přenosovým protokolům TCP/IP, neboli rozhraní mezi službami transportní vrstvy a jednotlivými aplikacemi. Řekli jsme si, že protokoly TCP/IP se z historických důvodů prosadily nejprve v tzv. BSD větvi Unixu, kde byl pro jejich zpřístupnění vytvořen nový mechanismus - sockets. Dnes je na řadě druhá hlavní větev Unixu, vyvíjená a šířená firmou AT&T (nejprve tzv. Bellovými laboratořemi firmy AT&T, dnes specializovanou dceřinnou organizací USL, neboli Unix System Laboratories).

Pro správné pochopení myšlenek, na kterých je založena implementace protokolů TCP/IP v prostředí AT&T Unixu, je vhodné si nejprve upřesnit časový sled jednotlivých událostí. Mechanismus "sockets", kterým jsme se zabývali minule, byl vyvinut pro verzi 4 BSD Unixu, která se poprvé objevuje v roce 1981. Do AT&T Unixu se obdobný mechanismus dostává až ve verzi System V Release 3 (zkráceně SVR3), kterou firma AT&T ohlásila v roce 1987. Rozdíl šesti let je hodně dlouhou dobou, za kterou se zcela zákonitě musely projevit různé vývojové trendy, nové koncepce i nové požadavky na operační systém Unix jako takový.

Jedním z těchto požadavků byl tlak na větší modularitu a flexibilitu celého vstupně/výstupního subsystému. Operační systém Unix je totiž koncipován tak, že jeho jádro (kernel) obsahuje základní část, nezávislou na konkrétním hardwaru, a dále pak různé ovladače (drivers), které základní části jádra zprostředkovávají vše, co již je závislé na konkrétním hardwaru daného počítače. Tyto ovladače se sice navenek "tváří" jako speciální soubory (a jako s takovými se s nimi také pracuje), ve skutečnosti jsou ale součástí jádra. To však mj. znamená, že při přidání nového ovladače či jakékoli jiné změně je třeba minimálně znovu sestavit celé jádro.

Kromě zřejmé nepružnosti má tato strategie i další nevýhodu, která se nejvíce projevila právě u implementace síťových protokolů. Souvisí s tím, že takto řešené ovladače často znovu implementují funkce, které jsou současně implementovány i v jiných ovladačích. Celková koncepce operačního systému a jeho jádra přitom nevychází příliš vstříc možnosti realizovat tyto společné funkce jen jednou, a příslušné moduly sdílet. Myšlenka řešit síťový software formou ovladačů (drivers), tedy "v jednom balíku", také není příliš v souladu s vrstevnatou koncepcí síťového softwaru, která si přímo říká o modulární implementaci.

Počátkem osmdesátých let, kdy byly protokoly TCP/IP začleňovány do BSD Unixu, nebyl ještě tlak na modularitu a flexibilitu vstupně/výstupního subsystému Unixu takový, aby si vynutil i nový způsob implementace samotných síťových protokolů. V BSD Unixu jsou tyto protokoly stále chápány a implementovány jako ovladače (ovladače speciálních zařízení), a nově je řešen pouze způsob práce s nimi. "Sockets" jsou z tohoto pohledu jen tenkou "slupkou", která ovladače překrývá, a zajišťuje takový způsob přístupu k nim, jaký vyhovuje i nejrůznějším síťovým aplikacím.

Do AT&T Unixu se protokoly TCP/IP dostávají již v době, kdy si nepružnost dosavadní koncepce V/V subsystému sama vynutila změnu. Zhruba v roce 1984 přichází jeden ze spoluautorů Unixu, Dennis Ritchie, s myšlenkou nového mechanismu, příznačně nazvaného streams (doslova: proudy). Ten pak byl také použit i pro implementaci TCP/IP protokolů v prostředí AT&T Unixu, a pro zpřístupnění těchto protokolů byla vytvořena nová překrývající "slupka", označovaná jako TLI neboli Transport Layer Interface (rozhraní transportní vrstvy).

Co jsou proudy?

Obrázek 63.1.
Obr. 63.1.: Představa přístupu k ovladači
a/ bez proudů
b/ s proudy
Základní myšlenka proudů (streams) je taková, že mezi ovladač zařízení a proces, který s tímto ovladačem komunikuje, se dynamicky vkládají různé programové moduly (bez nutnosti znovu sestavovat jádro při každém vložení či vyjmutí modulu), a veškerá komunikace mezi procesem a ovladačem pak prochází "skrz" tyto moduly. Představu proudu ilustruje obr. 63.1. b/.

Dynamickým vkládáním jednotlivých modulů do proudu vzniká celý řetězec, na jehož jednom konci stojí ovladač (označovaný také jako stream end, neboli koncový modul proudu), a na druhém konci tzv. hlavička proudu (stream head). Ta zajišťuje potřebné přizpůsobení mezi procesem, který s proudem pracuje, a proudem jako takovým. Mezi hlavičku a ovladač jsou zařazovány moduly, které mohou provádět v podstatě jakékoli akce s daty, které skrz ně prochází. Celý proud přitom pracuje v plně duplexním režimu - data tedy mohou procházet proudem oběma směry současně. Konkrétní způsob komunikace jednotlivých modulů v rámci proudu je takový, že každý vždy zpracuje data, převzatá od svého bezprostředního souseda z jedné strany, a výsledek zpracování zase předá svému sousedovi z druhé strany.

Podstatný je ovšem také způsob, jakým proud vzniká a utváří se. Na počátku je každý proud vytvářen jen jako dvouprvkový, a obsahuje tedy jen vlastní ovladač (stream end) a svou hlavičku (stream head). Zařazování jednotlivých modulů "doprostřed" proudu (konkrétně bezprostředně za hlavičku) se pak provádí dynamicky, na základě skutečné potřeby. Slouží k tomu operace, označovaná příznačně jako push (vložení nového modulu do proudu, bezprostředně za hlavičku). Stejně tak je možné jednotlivé moduly z proudu dynamicky vyjímat, a to operací pop (která vyjme modul bezprostředně za hlavičkou).

Tímto způsobem je možné zařazovat do proudu moduly, zajišťující nejrůznější funkce - od jednoduchých filtrů a převodníků až po mnohem složitější transformace. Pro potřeby síťového softwaru se přímo vnucuje myšlenka realizovat přenosové protokoly jednotlivých vrstev ve formě takovýchto modulů, a pak z nich dynamicky sestavovat celé hierarchické sestavy protokolů (protocol stacks). Tedy například transportní protokoly TCP a UDP realizovat jako dva (alternativní) moduly, síťový protokol IP jako další modul, a na ovladači (např. ovladači síťového adaptéru) ponechat přímé ovládání hardwaru (resp. protokoly, spadající do vrstvy síťového rozhraní).

Transport Layer Interface

Právě popsaný mechanismus proudů (streams) je však stále jen pouhým zdokonalením ovladačů v prostředí AT&T Unixu verze System V. Není určen jen pro implementaci síťových protokolů, ale používá se i k jiným účelům - například k obsluze terminálů apod.

Pro nás je ovšem podstatné, že výsledný proud (stream) se nadále "tváří" jako speciální soubor, a způsob jeho implementace (pomocí proudu) tudíž není pro aplikační procesy příliš relevantní. Pro ně je naopak zapotřebí obdobná "slupka", jakou je BSD Unixu rozhraní socket interface (viz minule), která by překryla příslušné proudy, a zpřístupnila transportní služby takovým způsobem, jaký vyhovuje aplikačním procesům.

V BSD Unixu je zmíněná "slupka" realizována formou systémových volání, což jsou ve skutečnosti vstupní doby do jádra, a teprve to pak zajišťuje veškeré požadované služby. Datové struktury, které se v této souvislosti používají (tj. vlastní sockety) jsou vytvářeny v paměti, která také patří jádru, a to si zabezpečuje vše potřebné pro alokaci a následné uvolňování této paměti.

U AT&T Unixu jsou analogií systémových volání v BSD Unixu volání knihovních rutin, které se přilinkovávají k aplikačním procesům, a jsou tudíž provozovány v adresovám prostoru těchto procesů. Stejně tak jsou v adresovém prostoru aplikačních procesů alokovány i veškeré datové struktury, které jsou těmito knihovními rutinami využívány.

Rozhraní k aplikačním procesům, které tyto knihovní rutiny vytváří, je v AT&T Unixu verze System V označováno jako Transport Layer Interface (zkráceně: TLI, doslova: rozhraní transportní vrstvy).

Obrázek 63.2.
Obr. 63.2.: Sockets (a/) vs. transport endpoints (b/)
Operace, které toto rozhraní nabízí, jsou v mnohém obdobné operacím, které se používají pro práci se sockety v BSD Unixu. Významnějším rozdílem je však povaha objektů na rozhraní mezi transportní a aplikační vrstvou. Zatímco v BSD Unixu je tímto objektem datová struktura (tj. socket), v případě TLI hraje tutéž roli spojení dvou entit - uživatele služby (který sídlí v aplikační vrstvě), a poskytovatele služby (který sídlí ve vrstvě transportní), viz obr. 63.2.b/. Toto lokální spojení je v terminologii TLI označováno jako transport endpoint (doslova: koncový bod transportního spojení), a je tedy analogií socketu v BSD Unixu. Zřizuje se pomocí rutiny t_open, která je obdobou systémového volání open v BSD Unixu, a teprve následně s ním musí být sdruženo i vhodné číslo portu (jako adresa transportního spojení), rutinou t_bind (která je analogická systémovému volání bind v BSD Unixu).

Obrázek 63.3.
Obr. 63.3.: Představa práce s rozhraním TLI při spojované komunikaci
Další postup na straně serveru ukazuje obrázek 63.3.: po volání rutiny t_bind si příslušný aplikační proces v roli serveru sám alokuje paměť pro datové struktury, které bude při komunikaci používat, a pak volá rutinu t_listen. Ta čeká na volání od klienta, a vrací řízení zpět v okamžiku, kdy takovéto volání přijde. Klient naopak využije k navázání spojení rutinu t_connect, s obdobným efektem jako v BSD Unixu.

Pokud je server ochoten akceptovat přicházející volání (které představuje výzvu k navázání spojení), volá rutinu t_accept, která volání přijme, a zajistí navázání spojení s klientem. Pak již může následovat vlastní přenos dat (rutinami t_rcv a t_snd, nebo přímo operacemi read a write), a ukončení spojení (které může být realizováno buď jako "řádné", zaručují doručení všech již odeslaných dat, nebo jako ukončení "okamžité", které toto nezaručuje).

Obrázek 63.4.
Obr. 63.4.: Představa práce s rozhraním TLI při nespojované komunikaci
Případ nespojovaného způsobu komunikace mezi klientem a serverem ukazuje obrázek 63.4.: zde po alokaci paměti pro datové struktury ihned následuje vlastní přenos dat.

Zajímavou odlišností AT&T Unixu a BSD Unixu je to, zda server může odmítnou volání klienta, či nikoli. V BSD Unixu to možné není - systémové volání accept zde čeká na jakékoli volání, a to vždy přijme. Teprve pak se server dozvídá (z výstupních parametrů systémového volání accept), kdo mu vlastně zavolal, a pouze následně může již navázané spojení zase zrušit. Naproti tomu v AT&T Unixu má server možnost odmítnout určitého klienta, a spojení s ním vůbec nenavázat. Na volání klienta totiž čeká rutina t_listen, která s ním však nenaváže spojení, ale pouze vrátí potřebné informace o tom, kdo vlastně volá. Server se podle nich rozhodne buď volání přijmout (tj. navázat spojení, pomocí rutiny t_accept), nebo jej odmítnout (v tomto případě volá rutinu t_snddis).