Miki TFC - http://www.miki.cat
 

3.2. Fases de la implementació

3.2.1. Fase 1

En aquesta fase, l'esforç es centra bàsicament en la implementació del protocol HTTP. Tot i així, en un primer moment vaig tenir certs problemes amb els includes, ja que el fet de tenir-los repartits feia que sorgissin problemes al compilar, per això finalment s'obta per tenir-los tots centralitzats al fitxer main.h, de manera que només cal incloure aquesta referència en tots els fitxers font i tot aclarit.

En aquest moment no em va semblar adient ni necessari d'utilitzar el port 80 per les primeres proves, ja que això implica tenir permisos de superusuari (de fet per qualsevol per sota del 1024), i sempre es pot escapar alguna coseta (mai millor dit) que provoqui algun efecte no desitjat a la màquina, així que en un primer moment se n'utilitza un d'aleatori, i després un per sobre del 5000 ja que, recordem que el sistema gestiona automàticament el rang 1024-5000 i no és gaire aconsellable interferir-hi.

L'altra plat nou per mi era obtenir la informació que necessitava dels fitxers, i comprovar-ne els permisos d'accés, ja que calia preparar les capçaleres i la resposta, però no volia tenir el fitxer obert mentre feia això, sobretot si es tractava de la resposta a un HEAD on necessito tota la informació però no em cal obrir el fitxer per res. Coneixia una funció anomenada acces() que em resolia el primer problema de permisos, però continuava necessitant alguna altra crida per la mida i la data de l'última modificació, així que després de passejar-me per diferents planes del manual sobre el tractament de fitxers, en una d'elles vaig trobar la referència a la crida stat(), en obrir-la vaig trobar el que buscava: recollir la informació del fitxer d'un sol cop, junt als permisos i si la crida fallava només em calia fer un cop d'ull a la variable errno per saber-ne el motiu. La operació és tan senzilla com això:

Després de les primeres compilacions, i proves amb el telnet sembla que el XicHttpd contesta correctament, i retorna el recurs o els errors HTTP quan toca, així que em decideixo a fer la primer prova de foc, i obro el meu estimat mozilla, per fer-ho utilitzo la versió HTML d'aquest document ja que inclou varies pàgines i algunes amb imatges. L'índex s'obre correctament, portat per l'entusias-me començo a seguir els enllaços fins que arribo a una plana que inclou dues imatges, una de les quals no es carrega. En un primer moment no me'n preocupo gaire, i segueixo endavant... sembla que vagi bé, reinicio el servidor i ho torno a provar i ja funciona però no em convenç, així que netejo la memòria cau del mozilla i recarrego la plana, altre cop la imatge no es carrega... Després de mirar i remirar el codi i de múltiples printf no sé trobar el problema, fins que se m'encen la llumeta i faig una sessió amb l'ethereal[1] activat, i descobreixo que el mozilla, igual que altres navegadors que he provat més endavant, obre més d'una connexió quan li convé, el que em molesta una mica perquè un mateix client m'ocuparà varies connexions, però que hi farem. Això sí, des d'aquest moment fins al final del projecte l'ethereal es converteix en el meu company inseparable a cada prova.

La primera prova amb l'Internet Explorer de Microsoft també va fallar, però amb la nova eina vaig detectar de seguida que ves a saber per quins sets ous enviava salts de línia addicionals en les peticions, els descartem i problema solucionat. Es dona aquesta fase per finalitzada, i anem per la següent.

3.2.2. Fase 2

El primer que calia era veure quines de les variables globals que necessitaven els threads podien provocar incoherències per tal de bloquejar-ne l'accés, després de donar-li moltes voltes vaig veure que de fet no tenia cap regió crítica a protegir, ja que tots els accessos a aquestes variables eren de lectura i prou. Les estructures de les connexions dels clients, es troben en una taula global, però de nou cada thread únicament accedeix a la posició que té assignada de manera que tampoc hauria d'haver-hi problemes, ja que si hi escriu algú altre, aquest és el pare just abans de crear (o assignar) el fil. Únicament en la manera que el pare obté els identificadors (posició lliure de la taula) pot donar algun problema, ja que es fa un recorregut per la taula de clients i retorna la primera posició en que el terme de l'estructura que fa referència al socket té el valor 0, però com a molt el que passarà és que en una situació de càrrega màxima, mentre es fa el recorregut algun dels threads anteriors acabi, amb el que el pare òbviament no ho sabrà i refusarà la connexió del client. Per tant, s'obta per no bloquejar res conscient de que si més endavant alguna cosa no funciona serà força difícil de trobar el problema, però com a bon Snowboarder que sóc sé que sense risc no hi ha diversió, així que m'hi llanço de cap (actitud que m'ha costat més d'una costella ho reconec...).

En un primer moment no tenia molt clar com controlar els threads estàtics, així que començo per lo "fàcil", i quan funcioni el servidor multi-fil únicament amb threads dinàmics, ja em dedicaré a triar una de les solucions possibles que hi ha.

3.2.2.1. Threads dinàmics

La idea és senzilla, quan s'accepta una nova connexió, obtenim un identificador que farà d'índex de la taula de clients, deixem el descriptor del socket en aquesta posició, i creem el nou thread per tal que executi la funció http_control() ja comentada, i li passem l'índex de la taula on hi ha l'estructura sobre la que treballarà amb el nou socket a punt. Quan s'acabi la sessió HTTP ja sigui per un tancament explícit, perquè salti el timeout o bé per algun error, caldrà que el propi thread deixi el terme socket amb el valor 0, per marcar la connexió com a lliure després de ressetejar-ne els altres valors, i finalment finalitzi la seva execució, sense esperar cap resposta per part del pare:

En aquest punt, tot i poder provar el multithreading amb la doble connexió del mozilla, es fa necessari l'ús d'alguna altra eina que em generi una càrrega prou alta per veure si el XicHttpd aguanta. Es poden veure les eines que s'utilitzaran per generar les càrregues d'aquí en endavant en el següent apartat referent als Jocs de Proves i anàlisi de rendiment.

En començar doncs a generar càrrega, tot i que la majoria de peticions es resolen bé, es comencen a rebre aleatòriament senyals de tipus SIGPIPE (Pipe trencada) i SIGSEGV (Violació de segment). Per tal de trobar-ne l'origen començo per utilitzar el gdb[2] tot i que sense èxit, ja que no sé per quin motiu, però tots els threads que finalitzaven la seva tasca es quedaven en estat zombie, de manera que tot i no ocupar espai de memòria si que ocupen un descriptor, i quan el sistema té massa processos envia un SIGTERM a aquells que, per dir-ho així, li molesten més, i naturalment amb més de 300 instàncies del xic en estat defunct em tocava el rebre a mi. Així doncs que amb el gdb no vaig poder treure cap conclusió. Tot i això en una de les proves si que vaig rebre els SIGPIPE abans del SIGTERM comentat, així que vaig intentar treure'n informació:

Així doncs cal alguna altra eina per intentar veure el que passa, i buscant una mica vaig descobrir l'strace

Cal notar que l'strace ressegueix les crides al sistema, però si el SIGSEGV pot aparèixer en qualsevol lloc, un SIGPIPE quasi segur que passa en fer alguna crida sobre un descriptor, així que l'strace ens serveix. Un cop rebut doncs, executo la comanda $ rgrep "SIGPIPE" * dins el directori on hi ha les traces i així sé quines traces han rebut aquest senyal i n'obro els fitxers, després d'una primera observació, repeteixo la prova varies vegades. Curiosament totes les vegades, tots els threads el rebien mentre estaven en la crida select excepte un que estava fent una serie de recv

A Google linux hi vaig trobar un post[3] que en parlava, així que man recv, i per fi descobreixo com detectar que l'altre extrem tanca la connexió (la falta d'experiència ja les té aquestes coses, però bé, uns quants cops de cap contra la paret i seguim). Tot i tenir varies opcions per tractar-ho, em va semblar que el més adient és ignorar aquest senyal de manera que quan els recv (i els send també a partir d'ara) fallin sigui per un SIGPIPE o per qualsevol altre motiu sempre retornen -1, amb el que XicHttpd tanca la seva banda i allibera el thread.

3.2.2.2. Threads estàtics

Per als threads estàtics el primer que ens cal és trobar un mecanisme per tal que estiguin pausats, esperant a que el pare els hi assigni una connexió. Tenia varies opcions, com ara el polling (Consultar una variable fins que pren cert valor), que els fils capturin els SIGUSR1 i facin un pause() i el pare els hi envii en rebre una connexio, utilitzar semàfors, però la pròpia llibreria pthread ja té les crides per aquestes situacions, com són l'ús de variables mutex i les variables condicionals, el que passava és que amb tota la documentació que havia mirat (veure Bibliografia) no m'acabava de quedar clar com adaptar-ho al meu cas, ja que tots parlen de l'accés a regions crítiques, però en el meu cas només em calia esperar i prou. Aquí agraeixo l'ajuda d'en David Carrera (el meu Consultor) que em va mostrar un exemple de l'ús de les variables condicionals, a més de remetrem al que ha sigut potser el Tutorial de Pthreads més aclaridor de tots els que he vist.

Veiem doncs com es pot fer per tenir una serie de threads en espera, i com el pare els pot despertar, sense disposar realment de cap regió crítica ni variable compartida a consultar.

Les crides pthread_cond_wait i pthread_cond_signal internament el primer que fan és alliberar el bloqueig de la regió crítica en la que estan, i lo últim és tornar-lo a posar, de manera que ens cal l'ús de les funcions pthread_mutex_lock i pthread_mutex_unlock ja que d'altre manera sempre ens trobaríem l'accés bloquejat.

3.2.3. Fase 3

Anem a veure doncs com s'han implementat les diferents funcionalitats addicionals:

3.2.3.2. Enviament de dades comprimides amb gzip

La llibreria zlib redefineix els mètodes d'entrada/sortida sobre fitxers per als fitxers gzip amb el prefix gz així per exemple trobem gzopen, gzwrite, gzread, etc. De fet les operacions són molt senzilles, només hi ha un problema, i és que treballa sobre fitxers, quan lo ideal per al servidor Web fora que realitzés la compressió en memòria, nosaltres en llegim la mida, omplim la capçalera adient, i n'enviem el resultat. La única manera de fer això amb aquestes llibreries seria fer una implementació pròpia de les rutines de compressió utilitzant com a model les fonts de les zlib (Concretament les del fitxer gzio.c), però això s'allunya molt dels objectius marcats, per tant utilitzarem les funcions gz*.

Així doncs tenim dues necessitats, la primera és la creació d'un fitxer temporal sobre el qual poder fer la compressió, i la segona és obtenir la mida del fitxer comprimit abans d'enviar-lo per tal d'omplir la capçalera Content-Length.

Resultat final. En el següent exemple es mostra el resultat prescindint del control d'errors i altre codi que pugui despistar.

3.2.3.3. CGI

El XicHttpd suporta el processament de dades per mitjà d'scripts o programes CGI, però de moment no es permet l'accés directament a aquests scripts per generar planes dinàmicament en el servidor. Ho he decidit així perquè penso que hi ha maneres molt més eficients de generar contingut dinàmic, aquell qui disposi de l'Apache, PHP i el mòdul PHP per l'Apache, pot provar a configurar l'execució dels PHP com a mòdul o com a CGI i comparar per ell mateix.

Per l'execució dels CGI cal que el servidor Web faci com de passarel·la entre el client i el CGI, per la seva banda l'script en qüestió espera trobar certs valors en certes variables d'entorn, un cop ha fet la feina retorna una sortida que el servidor envia directament al client:

Per al pas de paràmetres per mitjà del mètode GET el procés és el mateix però sense la línia "Dades POST". Anem a veure doncs quines són aquestes variables d'entorn mínimes que li calen als CGI, i que de moment són les úniques que utilitza el XicHttpd:

Per a l'execució del CGI s'utilitza l'omnipotent crida fork() de Unix la qual després de reassignar els canals d'entrada/sortida cap a dues pipes prèviament creades, omple les variables d'entorn i fa un canvi d'imatge amb la crida execl per executar el CGI. Vejem-ne el funcionament en el següent exemple en pseudo-codi:

3.2.3.4. Sistema de log

En aquesta part apareix per primer cop l'accés concurrent d'escriptura per part dels threads sobre un mateix recurs, en aquest cas els fitxers de log, però de fet tractant-se de fitxers, el Sistema Operatiu s'encarrega dels bloquejos, i per tant no cal preocupar-se'n. Ara bé, el que si és imprescindible és fer una sola crida a la funció d'escriptura per cada entrada al log, ja que d'altra manera el resultat és impredictible, per tant cal que cada thread disposi d'un buffer intern on preparar el missatge per posteriorment fer la crida a la funció d'escriptura.

Mostrem a continuació les errors que pot prendre la constant XMISSATGE de l'exemple anterior:

3.2.3.5. Últims detalls importants

La compilació de programes que utilitzin la llibreria pthread és tan senzill com ficar l'include adient (pthread.h) i dir-li al compilador que l'enllaci (opció -lpthread), però hi ha algunes coses més a fer si utilitzem les llibreries glibc (...). Pensem per exemple amb la variable errno la qual és global a tot el procés i s'actualitza quan alguna crida falla, aleshores, si els diferents threads que hem creat comparteixen l'espai de memòria i dos d'ells fallen, és molt possible que quan en recollim el valor per fer alguna comprovació (com ara ENOMEM o EACCES) llegim el valor que no toqui. A més hi poden haver certes funcions que també tinguin un comportament indefinit quan treballem amb threads donat que internament defineixen variables estàtiques. Per evitar aquestes situacions cal que sempre que compilem programes multi-fil utilitzem:

#define _REENTRANT
(o bé gcc ... -D_REENTRANT)

La creuada no s'acaba aquí, donat que algunes funcions a les que podríem estar acostumats disposen de les equivalents però segures per als threads (Thread-Safe), com és el cas de la funció strtok, l'equivalent de la qual s'anomena strtok_r a la qual li hem de passar un nou paràmetre corresponent a l'adreça d'una variable local per tal que emmagatzemi els resultats intermitjos[4]

Una altra qüestió important són els permisos d'execució del servidor Web ja que els ports per sota del 1024 estan reservats, de manera que si els volem utilitzar calen permisos especials. Sempre el podem executar com a usuari root però mai és una bona idea fer això, l'altre opció que tenim és fer l'executable SUID root, amb el que el podrem executar com a un usuari normal però si ens hi fixem el procés tindrà privilegis del superusuari per tant si es produeix algun desbordament no desitjat pot comprometre la seguretat de tot el sistema.

Per evitar aquesta situació de perill el XicHttpd fa el canvi d'usuari internament: Entre tots els identificadors coneguts pel procés, els que ens interessen són l'EUID (usuari efectiu) que correspon al propietari, i l'UID que és l'usuari que l'executa, així en arrencar el servidor amb SUID root des d'un usuari normal es canvia l'EUID per l'UID i només es restaura l'EUID en aquelles funcions en que els privilegis són estrictament necessaris. Anem a veure el següent exemple on es veurà més clar el procediment.

Exemple 3-13. Exemple de canvi del nivell de privilegis[5]

uid_t e_uid_inicial;
uid_t r_uid;
...
void init ()
{
	e_uid_inicial = geteuid ();
	r_uid = getuid ();

	/* Canviem els privilegis d'execució */
	seteuid (r_uid);
	...
	crea_socket ();
}
int crea_socket ()
{
	...
	/* Restaurem els privilegis originals */
	seteuid (e_uid_inicial);

	/* Crida(es) que necessita de privilegis */
	bind (server_sock,  (struct sockaddr *) &config.addr, sizeof (struct sockaddr));

	/* Tornem als privilegis de l'usuari */
	seteuid (r_uid);
	...
}

Per acabar es mostra com fer que un procés es converteixi en daemon per tal que s'executi com un servei més del sistema. Fixem-nos que ens calen dues coses, en primer lloc cal executar el procés en background i per fer-ho només tenim que "clonar" el procés amb la crida fork() i fer que el pare acabi la seva execució, ara bé el fill, igual que el pare quan s'ha creat, dependrà del terminal on s'ha cridat a la seva execució, de manera que si tanquéssim el terminal el procés acabaria. Per tant la segona cosa que cal fer és crear una nova sessió i així deslligar-nos de qualsevol terminal convertint-nos en el nou cap del grup de processos.

Notes

[1]

L'ethereal és un sniffer per les X (http://www.ethereal.com). Té una funció que m'ha estat especialment útil al provar els threads que s'anomena Follow TCP Stream del menú que apareix amb el botó dret del ratolí quan seleccionem una trama , així es pot veure tota la conversa d'una connexió TCP concreta.

[2]

gdb és l'acrònim de GNU Debugger

[3]

POST: http://www.ussg.iu.edu/hypermail/linux/kernel/0006.3/0209.html

[4]

Es pot trobar una bona explicació d'aquests detalls a http://www.linuxjournal.com/article.php?sid=1363.

[5]

Article interessant sobre programació segura: http://www.tldp.org/linuxfocus/Castellano/January2001/article182.shtml