Sunteți pe pagina 1din 21

Anatomia unui apel de sistem ^_n Linux Mihai Budiu | mihaib+@cs.cmu.edu http://www.cs.cmu.

edu/~mihaib 15 decembrie 1997 Subiect: Execut_ia unui apel de sistem urm_arit_a pas cu pas ^_n sistemul de operare Linux; Cuno_stint_e necesare: Not_iuni elementare despre sisteme de operare, limbajul C foarte bine, not_iuni despre setul de instruct_iuni al microprocesoarelor Intel 80x86; Cuvinte cheie: apel de sistem, trap, monitor, cod re-entrant, nucleu. Cuprins 1 Nucleul sistemului de operare 1 1.1 Reentrant_a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 Linux 3 2.1 Arborele de directoare al sursei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 Un apel de sistem: getpid(2) 4 3.1 Apelul funct_iei de bibliotec_a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 3.1.1 Un macro ciudat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 3.2 ^Intreruperea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.2.1 Stiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.3 Poarta de intrare ^_n nucleu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.4 Tabela de dispecerizare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.5 Funct_ia sys getpid() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.5.1 Structura Task . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.6 ^Intoarcerea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.7 Livrarea semnalelor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.8 Sf^_r_situl ^_ntreruperii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.9 Terminarea funct_iei de bibliotec_a . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 4 Rezumat 18 Acest articol este un \studiu de caz" (case study) ^_n sisteme de operare. Vom urm_ari (aproape) pas cu pas operat_iile executate de microprocesor pentru execut_ia unui foarte simplu apel de sistem; cobaiul experimentului nostru (^_nafar_a de cititor) este sistemul de operare Linux. Citirea 1 codului unui sistem de operare \adev_arat" este una dintre cele mai bune metode de a ^_nt_elege cum funct_ioneaz_a m_aruntaiele acestuia. ^In de_nitiv ce poate _ mai concret de at^_ta? 1 Nucleul sistemului de operare ^In aceast_a sect_iune voi revizui pe scurt not_iunea de \nucleu al unui sistem de operare" (kernel); un tratament mai amplu al chestiunii poate _ g_asit ^_ntr-un articol din PC Report din septembrie{ octombrie 1996, a c_arui copie este disponibil_a _si din pagina de web a autorului (ca _si toate celelalte articole ale sale la care face referint__a). Ce este un sistem de operare? Un set de programe care trateaz_a multe din funct_iile cel mai

des utilizate de programele utilizatorilor (cum ar _ accesul la disc) _si permite simultan executarea pe un acela_si calculator a unor programe independente. Cea mai important_a parte a unui sistem de operare este nucleul lui. Acesta este practic o colect_ie de funct_ii (numite \apeluri de sistem" | system calls) care pot _ executate de programele utilizatorilor _si care ^_ndeplinesc funct_iuni utile. Nucleul unui sistem de operare se bucur_a de oarecare privilegii relativ la programele scrise de utilizatorii obi_snuit_i, ^_n sensul c_a anumite operat_ii sunt permise numai nucleului, dar nu _si programelor care bene_ciaz_a de serviciile sale. De pild_a utilizatorii nu pot accesa discul ^_n nici un fel; ei au la dispozit_ie ^_ns_a un set de funct_ii ale nucleului care fac (teoretic) tot ce utilizatorul ar avea nevoie ^_ntr-un mod organizat: creaz_a _si distrug __siere, permit scrierea datelor _si citirea lor ^_n __siere, precum _si accesul controlat la aceste resurse. Motivat_ia pentru care accesul utilizatorului este interzis la disc este ^_n principal legat_a de integritatea discului: dac_a programe diferite ar vrea s_a foloseasc_a _ecare pentru sine discul ^_ntr-un alt fel, ar putea s_a interfereze ^_ntre ele. Nucleul ofer_a un acces limitat la disc, ^_ncerc^_nd s_a garanteze anumite propriet_at_i de consistent__a a datelor: de pild_a dac_a datele scrise ^_n __siere diferite nu au nici o leg_atura unele cu altele, pentru c_a cre_sterea ambelor __siere este supervizat_a atent de nucleu1. Funct_iile nucleului mai sunt ciudate pentru c_a (pe l^_ng_a faptul c_a pot folosi anumite operat_ii privilegiate), ele sunt comune tuturor programelor care se execut_a pe acel calculator, _e c_a programele se execut_a unul dup_a altul sau simultan. De fapt una din misiunile esent_iale ale nucleului este lansarea programelor ^_n execut_ie (_si atunci ele cap_at_a denumirea de \procese") _si controlarea execut_iei lor. Toate nucleele moderne suport_a execut_ia \simultan_a" a mai multor procese (ceea ce se nume_ste \multiprogramare"). Multiprogramarea poate _ \real_a", ^_n cazul ^_n care calculatorul are mai multe procesoare, sau simulat_a prin ceea ce se nume_ste \time-sharing" (punerea ^_n comun a timpului): oprirea unor programe din execut_ie temporar pentru a executa altele. Comutarea de la un proces la altul are numele englezesc de \context switch": comutarea contextului. Rat_iunea principal_a pentru time-sharing este una economic_a: nu toate p_art_ile unui calculator funct_ioneaz_a

cu aceea_si vitez_a, deci dac_a dou_a dintre ele comunic_a cea mai rapid_a trebuie s_a a_stepte dup_a cea mai lent_a (discul de pild_a este de cam un milion de ori mai lent dec^_t procesorul). C^_nd sunt mai multe lucruri de f_acut putem executa unele dintre ele ^_n timp ce altele a_steapt_a dup_a operat_iile lente. 1Ideea aceasta este binecunoscut_a ^_n ingineria program_arii sub numele de \tipuri de date abstracte". L_as_am cititorului sarcina explor_arii similitudinii. 2 1.1 Reentrant_a Nucleul este deci o colect_ie de funct_ii _si de structuri de date care ofer_a utilizatorului o sumedenie de operat_ii utile. Vom vedea c_a nucleul are o singur_a colect_ie de structuri de date pentru toate procesele care se execut_a. Aceste dou_a atribute (multiprogramarea _si unicitatea structurilor de date) puse cap la cap ridic_a o problem_a foarte di_cil_a: s_a presupunem c_a un proces A execut_a un apel de sistem pentru un acces la disc. O astfel de operat_ie este foarte costisitoare (^_n timp), a_sa c_a nucleul roag_a discul s_a-i trimit_a datele, _si pentru c_a are la dispozit_ie timp pentru un milion de instruct_iuni suspend_a procesul A _si porne_ste procesul B. Ce facem ^_ns_a dac_a B face el ^_nsu_si un apel de sistem pentru operat_ii pe __siere ^_n timp ce apelul lui A nu s-a terminat? Poate B _sterge __sierul pe care A tocmai ^_l modi_c_a sau altceva de genul _asta. Un astfel de cod, care se poate executa simultan ^_n contextul mai multor procese se nume_ste cod re-entrant (se poate intra din nou ^_ntr-o funct_ie ^_n timp ce se execut_a). Codul reentrant trebuie proiectat cu foarte mult_a grij_a dintru ^_nceput _si trebuie scris cu mare atent_ie. Nucleele tuturor sistemelor de operare moderne sunt re-entrante. Subiectul este extrem de interesant _si de subtil; toate cursurile universitare despre sisteme de operare ^_i consacr_a o parte relativ important_a. Noi nu ne vom izbi ^_n acest articol explicit de re-entrant__a, de_si ea este de fapt \ascuns_a" undeva, _si o gr_amad_a de funct_ii (despre care nu vom discuta) colaboreaz_a la a ascunde natura re-entrant_a a nucleului. Principala tehnic_a folosit_a pentru a scrie cod re-entrant este regiunea critic_a; aceasta este o regiune de cod care nu poate _ executat_a de mai multe procese simultan. O solut_ie la problema de mai sus a proceselor A _si B ar _ de a

nu permite nici unui proces s_a fac_a operat_ii pe __siere p^_n_a A nu _si-a terminat-o pe a lui (atunci practic toate operat_iile pe __siere ar _ constituit o regiune critic_a). ^In realitate nucleele ^_ncearc_a s_a permit_a c^_t mai mult_a activitate concurent_a, pentru c_a de obicei procesele au nevoie de resurse distincte. De pild_a ar _ p_acat s_a nu-l l_as_am pe B s_a _stearg_a alt __sier dec^_t cel cu care opereaz_a A doar pentru c_a A nu _si-a terminat treaba. Dar _stiu c_a suntet_i anxio_si s_a vedet_i cod, a_sa c_a voi ^_ntrerupe aici discut_ia despre sect_iuni critice. 2 Linux Voi baza discut_ia mea pe sistemul de operare Linux. Linux este un sistem de operare de tip Unix2, scris_a init_ial ^_n 1991 de un student Finlandez pe nume Linus Torvalds. El este ^_n continuare principalul \scriitor" al nucleului Linux, dar nu cantitativ, pentru c_a la cele peste 800 000 linii ale codului au contribuit deja mii de voluntari din ^_ntreaga lume. Trebuie spus c_a sistemul este de o calitate foarte bun_a, rivaliz^_nd cu succes cu produse ale marilor _rme care cost_a bani grei. Diferent_a este c_a Linux este disponibil ^_n surse oricui ^_l dore_ste; poate _ obt_inut contra cost sau gratuit de pe Internet. Linux evolueaz_a foarte rapid; noi versiuni ale nucleului apar la _ecare c^_teva zile. Voi baza discut_ia mea pe versiunea 2.0.30. Aceasta este ultima versiune mare stabil_a a nucleului. Dezvoltarea nucleului se face pe dou_a linii: cele cu un num_ar par dup_a'primul punct sunt versiuni stabile, care sunt recomandate celor care folosesc Linux pentru nevoile lor, iar versiunile cu un num_ar impar (2.1.x) cont_in cod experimental, care nu a fost ^_nc_a ^_ndeajuns testat pentru a _ recomandabil celor care au nevoie de _abilitate. Versiunile impare sunt folosite de cei care dezvolt_a sistemul, sau care au neap_arat_a nevoie de anumite lucruri neimplementate ^_nc_a ^_n celelalte versiuni. 2O scurt_a istorie a evolut_iei Unix-ului, publicat_a mai demult ^_n BYTE Rom^ania, putet_i obt_ine din pagina de web a autorului. 3 Linux este su_cient de bine scris ^_nc^_t poate rula pe calculatoare extrem de diferite; la ora aceasta el merge pe procesoare 80x86/Pentium (de la Intel), Sparc (SUN), Power PC (IBM), Alpha (Digital, cump_arat de cur^_nd de Compaq), MIPS (acum la Silicon Graphics), M68K (Motorola). Noi ne vom referi la versiunea pentru procesoare Intel, pentru c_a este cea mai r_asp^_ndit_a. Principiile care emerg sunt ^_ns_a valabile pentru toate celelalte procesoare.

2.1 Arborele de directoare al sursei Este instructiv s_a arunc_am o scurt_a privire asupra arborelui de directoare care constituie sursele nucleului. De obicei acesta este instalat ^_n directorul /usr/src/linux pe ma_sinile Linux. ^In acest articol voi referi toate c_ar_arile de directoare relativ la acest punct. Subdirectoarele principale sunt: Director Cont_ine Linii de cod fs sisteme de __siere (File System) 68 000 mm memorie (Memory Management) 17 000 init procesul init (nr 1, care porne_ste ma_sina) 4000 kernel funct_ii esent_iale ale nucleului 7200 lib utilitare diverse 1800 include __siere header cu declarat_ii pentru compilarea nucleului _si programelor utilizatorilor 78 000 net protocoalele ret_elelor de calculatoare 56 000 ipc mecanisme de comunicare ^_ntre procese (Inter Process Communication) 2500 drivers programe care m^_nuiesc perifericele 412 000 modules nu cont_ine surse 0 arch cod dependent de procesor 150 0003 Dup_a cum vedet_i mai mult de jum_atate de cod este ^_n drivere. Codul driverelor este ^_ns_a pentru toate pl_acile posibile; un anumit sistem va avea compilate numai driverele pentru hardware-ul instalat. Mult_imea aceasta de drivere se datore_ste popularit_at_ii enorme a hardware-ului de PC, pentru care tot omul fabric_a c^_te o nou_a plac_a. Dou_a cuvinte _si despre unele din subdirectoarele acestor directoare: fs/* Linux suport_a o mult_ime de sisteme de __siere (organiz_ari ale __sierelor pe disc). Lista lor este include sistemul de __siere din MS-DOS, din Amiga _si din OS/2, sistemul de __siere de ret_ea (NFS, Network File System) de la Sun, sistemul vfat al lui Windows NT, sistemele de __siere din Unix-ul original, System V (sysv) _si sistemul de __siere din Berkeley Unix, UFS (numit _si Fast File System, FFS, ^_n literatur_a), _si altele!. Intent_ionez s_a consacru un articol special arhitecturii sistemelor de __siere ^_n nucleele Unix (dou_a articole ^_nrudite despre aceast_a tem_a au ap_arut deja ^_n PC Report), a_sa c_a nu voi divaga ^_n continuare. include/* cont_ine headere cu declarat_iile structurilor de date _si prototipurile funct_iilor publice din nucleu; /drivers/net/* felurite pl_aci de ret_ea; 3Sunt cam 30 000 de linii dependente de arhitectur_a pentru Intel, _si ^_n total 150 000 pentru toate arhitecturile.

4 drivers/block/* toate perifericele tratate de Unix drept colect_ii de blocuri: discuri ^_n special; drivers/char/* majoritatea tuturor celorlalte periferice; drivers/* alte periferice. 3 Un apel de sistem: getpid(2) Am ales pentru vivisect_ia noastr_a un apel de sistem foarte simplu; poate cel mai simplu. Cu toate acestea periplul nostru p^_n_a la el va _ destul de lung, s^_, sper_am, instructiv. Vom discuta despre funct_ia getpid, \GET Process IDenti_er". Nucleul Unix asigneaz_a _ec_arui proces ^_n curs de execut_ie un num_ar unic ^_ntre 0 _si 30000 care poate _ folosit pentru comunicarea ^_ntre procese (semnalele se trimit indic^_nd acest pid). Pagina de manual Unix care descrie apelul de sistem este ^_n sect_iunea 2 a manualului (unde sunt toate celelalte apeluri de sistem); am indicat acest lucru ^_n modul standard, pun^_nd sect_iunea ^_ntre paranteze. Manualul poate _ citit tast^_nd comanda man 2 getpid. Pagina de manual ne spune ca getpid nu are argumente _si ^_ntoarce ca rezultat PID-ul procesului care face apelul. Iat_a mai jos _si un exemplu de folosire: #include <unistd.h> #include <stdio.h> int main(void) { int p = getpid(); printf("Pid = %d\n", p); } Restul acestui articol va explora un singur lucru, _si anume, cum se execut_a prima linie a programului de mai sus. 3.1 Apelul funct_iei de bibliotec_a ^In primul r^_nd trebuie s_a r_aspundem la ^_ntrebarea: unde este codul funct_iei getpid? C^_nd l-a scris _si de unde-l ia programul de mai sus. R_aspunsul este: codul este ^_n biblioteca de funct_ii a limbajului C care vine ^_mpreun_a cu compilatorul de C _si nucleul sistemului. Fiecare apel de sistem are o astfel de funct_ie asociat_a ^_n bibliotec_a. Declarat_ia funct_iei este ^_n __sierul header /usr/include/unistd.h. Funct_ia a fost compilat_a de cei care au scris compilatorul _si legat_a ^_n biblioteca de funct_ii C /lib/libc.a. Corpul funct_iei a fost generat anterior din urm_atoarea surs_a C: #include <linux/unistd.h> _syscall0(int, getpid) 5 3.1.1 Un macro ciudat

Fi_sierul include/linux/unistd.h cont_ine de_nit_ia macro-ului syscall0, care este folosit pentru a genera funct_iile C care cheam_a apeluri de sistem cu 0 argumente. ^In acela_si __sier exist_a _si codul macrourilor syscall1, etc, care genereaz_a corpul apelurilor de system cu mai multe argumente. Iat_a cum arat_a cel care ne intereseaz_a: #define _syscall0(type,name) \ type name(void) \ {\ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name)); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ } Trebuie s_a recunoa_stet_i ca nu vedet_i prea des astfel de cod C, nu? Codul folose_ste mai multe tr_as_aturi mai put_in cunoscute (dar absolut standard) ale preprocesorului de C, plus c_a amestec_a asamblare cu C (ceea ce ^_n standardul C nu exist_a, dar Linux se compileaz_a numai cu compilatorul gcc, a_sa c_a nu conteaz_a prea tare ce zice standardul). Ce e ciudat cu acest macro? _ Este un macro pe mai multe linii, scris folosind caracterul n inainte de sf^_r_situl liniei pentru a indica o continuare pe cea urm_atoare; _ Acest macro nu genereaz_a o expresie c^_nd este expandat, ci corpul unei funct_ii! _ Unul din argumente (type) apare ^_n corpul macro-ului ^_ntr-o pozit_ie ^_n care trebuie s_a apar_a un tip; _ Alt argument (name) apare ^_n pozit_ia unde trebuie s_a apar_a un nume de funct_ie; _ Se folose_ste operatorul ##, care concateneaz_a simbolurile de la st^_nga _si de la dreapta sa. S_a vedem ce iese dup_a pre-procesare din programul de o linie de mai sus: #include <linux/unistd.h> _syscall0(int, getpid) d_a na_stere la (t_in^_nd contul c_a in acel header avem de_nit_ia: #define NR getpid 20, ceea ce ^_nseamn_a c_a num_arul apelului de sistem getpid este 20): 6 int getpid(void) { long __res; __asm__ volatile ("int $0x80"

: "=a" (__res) : "0" (20)); if (__res >= 0) return (int) __res; errno = -__res; return -1; } Asta se traduce cam a_sa: De_nesc funct_ia getpid f_ar_a argumente care d_a ca rezultat un ^_ntreg. Funct_ia va executa ^_nt^_i instruct_iunea int 0x80, care genereaz_a o ^_ntrerupere, av^_nd ^_n registrul 0 num_arul NR getpid, adic_a 20, iar la sf^_r_situl execut_arii lui int 0x80 rezultatul din registrul A trebuie pus ^_n variabila res (a_sa se traduce linia care ^_ncepe cu asm , care este scris_a ^_ntr-un idiom special al compilatorului gcc). Dup_a aceea, dac_a res este pozitiv, acesta este rezultatul funct_iei; altfel rezultatul este -1, iar valoarea lui res este pus_a ^_n variabila global_a a procesului, errno. Deja am ^_nv_at_at un lucru interesant despre apelurile de sistem (dac_a inspectat_i macrourile celelalte, pentru apeluri de sistem cu mai multe argumente, vet_i observa aceea_si comportare): nucleul va ^_ntoarce ^_ntotdeauna un num_ar pozitiv ca r_aspuns la un apel de sistem. O valoare negativ_a reprezint_a codul unei erori. Funct_ia de bibliotec_a ia codul erorii _si ^_l pune ^_ntr-o variabil_a global_a a procesului, errno. R_aspunsul unui apel de sistem ^_n caz de eroare este -1. Valorile pe care le poate lua errno sunt ^_n __sierul header standard errno.h; studiat_i-l, c_aci este interesant. Variabila aceasta mai este folosit_a de funct_ii ca perror(3) sau strerror(3) pentru a tip_ari mesaje de eroare. 3.2 ^Intreruperea Am v_azut deci c_a pentru a invoca serviciile nucleului programele pun un num_ar care descrie serviciul cerut (getpid() ^_n cazul nostru) ^_n registrul 0 (la 386 este registrul EAX), dup_a care execut_a o ^_ntrerupere software, cea cu num_arul 80 ^_n hexazecimal (128). Ce mai e _si cu ^_ntreruperea asta? Dac_a v_a reamintit_i, am spus c_a programele utilizatorului nu au dreptul s_a execute orice operat_ii, pe c^_nd nucleul da. Aceast_a segregare este realizat_a de microprocesor printr-un bit intern de stare, care indic_a dac_a programul curent se execut_a ^_n mod nucleu (kernel mode) (_si atunci este privilegiat), sau ^_n mod utilizator (user mode)4. Microprocesorul trece automat ^_n mod nucleu atunci c^_nd se ^_nt^_mpl_a un eveniment except_ional, cum ar _:

_ Un periferic genereaz_a o ^_ntrerupere; 4De fapt familia x86 are nu 2 ci 4 moduri de privilegiu, dar Linux folose_ste numai 2 dintre ele. 7 _ Un program execut_a o instruct_iune ilegal_a; _ Un program acceseaz_a zone de memorie interzise; _ Un program execut_a o^_ntrerupere software; _ O eroare grav_a este detectat_a (ex: a c_azut curentul); _ etc. Trecerea ^_n mod nucleu ^_nseamn_a nu doar o schimbare a valorii bitului care indic_a modul, ci _si un salt la o adres_a dinainte stabilit_a. Rat_ionamentul este urm_atorul: cel care scrie sistemul de operare scrie pentru _ecare din cazurile de mai sus un program (handler) care ia act_iunile corespunz_atoare pentru a remedia evenimentul except_ional. Aceste programe sunt instalate apoi ^_n memorie la ^_nceput (^_n procesul de boot-are al calculatorului), iar apoi avem garant_ia c_a utilizatorul nu poate face nici o stric_aciune (intent_ionat_a sau nu), pentru c_a orice act_iune except_ional_a va transfera controlul la unul dintre aceste programe scrise dinainte _si ^_n care avem mare ^_ncredere. Pentru a _ _si mai preci_si: _ecare eveniment except_ional are la nivelul microprocesorului un num_ar asociat. Instalarea handler-elor pentru except_ii const_a ^_n construirea unui vector de adrese de proceduri, care indic_a pentru _ecare except_ie ce procedur_a trebuie s-o trateze, cam a_sa5: codul exceptiei | | | --------| 0| |------->procedura pentru tratarea impartirii la 0 | --------\---->1| |------->procedura pentru depanare --------......... --------128| |------->procedura pentru tratarea unui apel de sistem --------vector de exceptii Vectorul de except_ii este construit imediat dup_a pornirea sistemului; funct_ia r_aspunz_atoare de acest lucru este ^_n __sierul arch/i386/kernel/traps.c, _si este numit_a trap init(). Codul esent_ial arat_a cam a_sa: void trap_init(void) {

..... set_trap_gate(0,&divide_error); 5Procesoarele Intel disting mai multe tipuri de evenimente except_ionale, clasi_c^_nd separat ^_ntreruperile generate de hardware, erorile de execut_ie, etc. Diferitele tipuri funct_ioneaz_a ^_ns_a la fel, doar c_a _ecare tip are alt vector de except_ii. 8 set_trap_gate(1,&debug); set_trap_gate(2,&nmi); set_system_gate(3,&int3); /* int3-5 can be called from all */ set_system_gate(4,&overflow); set_system_gate(5,&bounds); set_trap_gate(6,&invalid_op); set_trap_gate(7,&device_not_available); set_trap_gate(8,&double_fault); set_trap_gate(9,&coprocessor_segment_overrun); set_trap_gate(10,&invalid_TSS); set_trap_gate(11,&segment_not_present); set_trap_gate(12,&stack_segment); set_trap_gate(13,&general_protection); set_trap_gate(14,&page_fault); set_trap_gate(15,&spurious_interrupt_bug); set_trap_gate(16,&coprocessor_error); set_trap_gate(17,&alignment_check); for (i=18;i<48;i++) set_trap_gate(i,&reserved); set_system_gate(0x80,&system_call); ..... } Asta e relativ simplu de ghicit ce ^_nseamn_a: except_ia nr 0, care se declan_seaz_a c^_nd se ^_mparte la 0, va _ tratat_a de funct_ia divide error, care este undeva prin nucleu, except_ia 1 de funct_ia debug, etc. C^_t despre codul macro-ului set trap gate(), ^_l putet_i g_asi^_n __sierul include/asm-i386/system.h. Codul este ^_nc^_lcit pentru c_a procesoarele x86 nu cont_in ^_n c_asut_a din vectorul de except_ii doar adresa unei proceduri, ci _si o mult_ime de alte informat_ii, legate de privilegiile pe care le are un program ^_n timp ce execut_a except_ia, de tipul except_iei (except_ie, ^_ntrerupere), etc. Studiul detaliat al tabelei ne-ar ^_ndep_arta de la scopul nostru, _si anume de a vedea cum se execut_a un apel de sistem. Important este de ret_inut: 1. Dup_a executarea ^_ntreruperii software execut_ia sare la o procedur_a speci_cat_a de vectorul de except_ii (system call pentru exemplu nostru concret); 2. Microprocesorul intr_a ^_n mod nucleu;

3. Microprocesorul schimb_a stiva curent_a la cea indicat_a de noul privilegiu. 3.2.1 Stiva Acest ultim punct merit_a o clari_care. Cum se execut_a procedurile? Folosind o stiv_a pentru a-_si p_astra variabilele locale; c^_nd o procedur_a o cheam_a pe alta se contruie_ste un nou cadru de stiv_a (stack frame) pentru procedura nou_a, ^_n care aceasta-_si t_ine variabilele personale, argumentele _si alte lucru_soare. (Pe ^_ndelete despre rolul stivei am scris ^_n PC Report din ianuarie 1997, ^_n articolul \Multithreading".) 9 Nucleul ^_nsu_si este practic o colect_ie de proceduri, care deci au nevoie de o stiv_a pentru a se putea executa. Dar de unde s-o ia pe aceasta? Nucleul nu poate folosi stiva pe care o folose_ste procesul ^_n mod obi_snuit, pentru c_a nu poate avea ^_ncredere ^_n proces. Diferent_a este c_a dac_a procesul manipuleaz_a stiva ^_ntr-un mod eronat, nu poate face r_au dec^_t sie_si, datorit_a faptului c_a mecanismele de memorie virtual_a ^_mpiedic_a un proces s_a acceseze memoria alocat_a altor procese. Cu nucleul lucrurile nu mai stau a_sa: privilegiile lui ridicate i-ar putea permite s_a scrie oriunde, _sterg^_nd orice. Din cauza aceasta, la procesoarele x86, o schimbare de privilegiu a procesorului implic_a automat o schimbare de stiv_a. Cum se face asta? Fiecare proces are o tabel_a cu pointeri c_atre stive, c^_te una pentru _ecare nivel de privilegiu. Acest lucru poate _ v_azut^_n __sierul include/asmi386/processor.h, unde cele 4 stive, corespunz^_nd celor 4 nivele de privilegiu ale familiei x86, sunt indicate ^_n structura numit_a TSS (Task Segment Selector, terminologie Intel): struct thread_struct { unsigned short back_link,__blh; unsigned long esp0; \ unsigned short ss0,__ss0h; | unsigned long esp1; | 3 stive unsigned short ss1,__ss1h; | unsigned long esp2; | unsigned short ss2,__ss2h; / unsigned long cr3; unsigned long eip; unsigned long eflags; unsigned long eax,ecx,edx,ebx; unsigned long esp; | virful stivei curente unsigned long ebp; unsigned long esi; unsigned long edi;

unsigned short es, __esh; | segmentul stivei curente unsigned short cs, __csh; unsigned short ss, __ssh; unsigned short ds, __dsh; unsigned short fs, __fsh; unsigned short gs, __gsh; unsigned short ldt, __ldth; unsigned short trace, bitmap; unsigned long io_bitmap[IO_BITMAP_SIZE+1]; unsigned long tr; unsigned long cr2, trap_no, error_code; /* floating point info */ union i387_union i387; /* virtual 86 mode info */ struct vm86_struct * vm86_info; unsigned long screen_bitmap; unsigned long v86flags, v86mask, v86mode; 10 } C^_nd nucleul creaz_a un proces ^_i aloc_a dou_a stive: una pentru modul utilizator _si una pentru modul nucleu. (Celelalte dou_a stive nu sunt niciodat_a folosite de Linux). C^_nd microprocesorul ^__si schimb_a privilegiul ^__si schimb_a automat _si stiva curent_a. Stiva nucleului ^_n general este mic_a (4K), pentru c_a nucleul este o bucat_a _x_a de cod, care nu cont_ine apeluri recursive de funct_ii, deci consum_a relativ put_in_a stiv_a. Deci funct_ia sys call, chemat_a indirect prin ^_ntrerupere, _si toate funct_iile chemate de ea, se vor executa pe stiva procesului curent care corespunde modului nucleu. 3.3 Poarta de intrare ^_n nucleu S_a vedem ce se ^_nt^_mpl_a mai departe. Codul funct_iei system call este (din p_acate) scris ^_n asamblare. Se g_ase_ste ^_n __sierul arch/i386/kernel/entry.S, _si folose_ste din plin macro-uri foarte simple de_nite ^_n alte p_art_i (cele mai interesante ^_n include/asm-i386/linkage.h), (cum ar _ ENTRY, SYMBOL NAME, SAVE ALL, etc.). Zic \din p_acate", pentru c_a dialectul de asamblare al compilatorului gcc nu este acela_si sintactic cu cel al _rmei Intel, a_sa c_a acela_si program se scrie ^_n feluri diferite folosind cele dou_a limbaje. M_a rog, nu o s_a ne ^_mpiedic_am noi de at^_ta lucru; s_a ^_ncerc_am s_a ne facem o idee despre ce se ^_nt^_mpla ^_n codul urm_ator: ENTRY(system_call) pushl %eax # save orig_eax SAVE_ALL #ifdef __SMP__ ENTER_KERNEL #endif

movl $-ENOSYS,EAX(%esp) cmpl $(NR_syscalls),%eax jae ret_from_sys_call movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax testl %eax,%eax je ret_from_sys_call #ifdef __SMP__ GET_PROCESSOR_OFFSET(%edx) movl SYMBOL_NAME(current_set)(,%edx),%ebx #else movl SYMBOL_NAME(current_set),%ebx #endif andl $~CF_MASK,EFLAGS(%esp) # clear carry - assume no errors movl %db6,%edx movl %edx,dbgreg6(%ebx) # save current hardware debugging status testb $0x20,flags(%ebx) # PF_TRACESYS jne 1f call *%eax movl %eax,EAX(%esp) # save the return value jmp ret_from_sys_call 11 Pa_sii mari sunt urm_atorii: _ Se salveaz_a registrul AX, care cont_ine codul apelului de sistem; _ Se salveaz_a tot_i regi_strii (care au valorile pe care le aveau c^_nd s-a executat ^_ntreruperea 0x80); _ Dac_a calculatorul este un calculator cu mai multe procesoare se execut_a un cod special pentru sincronizarea nucleelor de pe diferitele procesoare6. _ Num_arul apelului de sistem este comparat cu num_arul total de apeluri existente (NR syscalls, un macro de_nit ^_n include/asm-i386/unistd.h). _ Dac_a num_arul este ^_nafara limitelor atunci nucleul se ^_ntoarce imediat la utilizator (prin salt la ret from sys call) cu eroarea ENOSYS (\nu avem un astfel de apel de sistem"); _ Nucleul indexeaz_a cu codul apelului ^_ntr-o tabel_a care cont_ine adresele tuturor funct_iilor care trateaz_a apeluri de sistem (tabela numit_a sys call table este discutat_a ^_n sect_iunea urm_atoare); adresa funct_iei de tratare este pus_a ^_n registrul EAX; _ Dac_a o^_nregistrarea din tabel_a este 0, funct_ia respectiv_a nu exist_a, deci din nou ne ^_ntoarcem la utilizator cu eroare; _ Se fac felurite proces_ari legate de multiprocesoare _si eventuala depanare a procesului curent; le ignor_am; _ Instruct_iunea principal_a este call *%eax, adic_a salt la adresa din registrul EAX. Aceast_a instruct_iune execut_a funct_ia corespunz_atoare apelului de sistem; rezultatul acestei funct_ii este

^_ntors prin convent_ie tot ^_n registrul EAX. _ Rezultatul din registrul EAX este pus pe stiv_a; _ Se sare la eticheta ret from sys call, discutat_a mai jos. 3.4 Tabela de dispecerizare Am v_azut c_a funct_ia de bibliotec_a a pus ^_n registrul EAX un cod de apel de sistem, c_a ^_ntreruperea a comutat privilegiul _si stiva, iar apoi c_a ^_n nucleu s-a indexat ^_ntr-o tabel_a mare cu codul din EAX. Aceast_a tabel_a este construit_a tot ^_n __sierul arch/i386/kernel/entry.S, _si arat_a cam a_sa: .data ENTRY(sys_call_table) .long SYMBOL_NAME(sys_setup) /* 0 */ .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) 6SMP ^_nseamn_a Symmetric Multi Processing, _si este o tehnic_a ^_n care pe un calculator cu mai multe procesoare _ecare procesor execut_a cod at^_t de proces utilizator c^_t _si de nucleu. Scrierea de cod pentru multiprocesoare simetrice este mult mai grea dec^_t scrierea de cod re-entrant, din motive pe care nu avem timp s_a le explor_am acum, dar asupra c_arora sper_am s_a revenim alt_adat_a. Oricum, Linux aici \tri_seaz_a" un pic, nepremit_^_nd unui procesor s_a execute cod nucleu dac_a un alt procesor execut_a deja cod nucleu pentru un alt proces. 12 .long SYMBOL_NAME(sys_read) .long SYMBOL_NAME(sys_write) .long SYMBOL_NAME(sys_open) /* 5 */ ......................... .long SYMBOL_NAME(sys_getpid) /* 20 */ ......................... .space (NR_syscalls-165)*4 /* neimplementate */ Dup_a cum vedet_i ^_n c_asut_a 20 a tabelei se g_ase_ste adresa unei funct_ii, numit_a sys getpid. Aceast_a funct_ie va _ deci executat_a atunci c^_nd codul apelului de sistem este 20. 3.5 Funct_ia sys getpid() Am ajuns ^_n _ne la funct_ia din nucleu care face procesarea corespunz_atoare. Codul ei este ^_n __sierul kernel/sched.c, _si este banal; ^_l reproducem ^_n ^_ntregime: asmlinkage int sys_getpid(void) { return current->pid; } Prin convent_ie compilatorul gcc pune rezultatul unei funct_ii C ^_n registrul EAX; din aceast_a cauz_a valoarea ^_ntoars_a de aceast_a funct_ie poate _ consumat_a de codul de mai sus. Dar cine este current? Este nimeni altul dec^_t \procesul" curent. Cum vine asta? 3.5.1 Structura Task

Pentru a r_aspunde la aceast_a ^_ntrebare trebuie s_a a_am ce este un proces pentru nucleu. Ei bine, pentru nucleu un proces este nimic altceva dec^_t o structur_a de date. Putem vedea aceast_a structur_a de date ^_n __sierul include/linux/sched.h; unul dintre c^_mpurile ei este structura TSS de care am vorbit mai sus. Ea arat_a cam a_sa: struct task_struct { /* these are hardcoded - don't touch */ volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ long counter; long priority; unsigned long signal; unsigned long blocked; /* bitmap of masked signals */ unsigned long flags; /* per process flags, defined below */ int errno; long debugreg[8]; /* Hardware debugging registers */ struct exec_domain *exec_domain; /* various fields */ struct linux_binfmt *binfmt; struct task_struct *next_task, *prev_task; struct task_struct *next_run, *prev_run; 13 unsigned long saved_kernel_stack; unsigned long kernel_stack_page; int exit_code, exit_signal; /* ??? */ unsigned long personality; int dumpable:1; int did_exec:1; /* shouldn't this be pid_t? */ int pid; int pgrp; int tty_old_pgrp; int session; /* boolean value for session group leader */ int leader; int groups[NGROUPS]; /* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->p_pptr->pid) */ struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct wait_queue *wait_chldexit; /* for wait4() */ unsigned short uid,euid,suid,fsuid; unsigned short gid,egid,sgid,fsgid; unsigned long timeout, policy, rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr;

struct timer_list real_timer; long utime, stime, cutime, cstime, start_time; /* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; unsigned long swap_address; unsigned long old_maj_flt; /* old value of maj_flt */ unsigned long dec_flt; /* page fault count of the last time */ unsigned long swap_cnt; /* number of pages to swap on next pass */ /* limits */ struct rlimit rlim[RLIM_NLIMITS]; unsigned short used_math; char comm[16]; /* file system info */ int link_count; struct tty_struct *tty; /* NULL if no tty */ /* ipc stuff */ 14 struct sem_undo *semundo; struct sem_queue *semsleeping; /* ldt for this task - used by Wine. If NULL, default_ldt is used */ struct desc_struct *ldt; /* tss for this task */ struct thread_struct tss; /* filesystem information */ struct fs_struct *fs; /* open file information */ struct files_struct *files; /* memory management info */ struct mm_struct *mm; /* signal handlers */ struct signal_struct *sig; #ifdef __SMP__ int processor; int last_processor; int lock_depth; /* Lock depth. We can context switch in and out of holding a syscall kernel lock... */ } #endif Nucleul manipuleaz_a ^_n principiu dou_a mari clase de structuri de date: _ Structuri de date private care apart_in unui singur proces; de exemplu pid-ul, prioritatea, pointeri spre __sierele deschise, etc. _ Structuri de date globale ^_ntregului sistem: __siere, memorie, procesoare, etc. Practic tot ce este per-proces este t_inut ^_ntr-un array mare de structuri de tipul struct task struct. Un array de pointeri spre aceste structuri este declarat ^_n __sierul kernel/sched.c: struct task_struct * task[NR_TASKS] = {&init_task, };

current este un macro de_nit^_n include/linux/sched.h spre un task struct, care puncteaz_a spre procesul care tocmai se execut_a pe procesorul curent. Plani_catorul (scheduler) are grij_a ca de _ecare dat_a c^_nd comut_a de la procesul curent la un altul s_a schimbe valoarea acestui pointer. 3.6 ^Intoarcerea Gata, am ajuns p^_n_a ^_n \centrul nucleului". Acum trebuie s_a ie_sim la suprafat__a, cu valoarea calculat_a. Credet_i c_a nu poate _ dec^_t mai simplu? Ehe, v_a ^_n_selat_i. Relu_am periplul din __sierul arch/i386/kernel/entry.S; acum trebuie s_a vedem cum se execut_a funct_ia ret from sys call, a c_arei misiune este s_a p_ar_aseasc_a modul privilegiat. Codul este mai complicat dec^_t ne a_stept_am pentru c_a aceast_a funct_ie nu este chemat_a numai la sf^_r_situl unui apel de sistem, ci _si la sf^_r_situl unei ^_ntreruperi hardware. Problema este c_a ^_ntreruperile hardware pot surveni oric^_nd, chiar _si atunci c^_nd se execut_a deja un apel de sistem sau o alt_a ^_ntrerupere hardware. Din cauza asta nucleul trebuie ^_nt^_i s_a veri_ce dac_a trebuie s_a se ^_ntoarc_a la modul utilizator sau trebuie s_a r_am^_n_a ^_n mod nucleu; act_iunile sunt diferite ^_n cele dou_a cazuri. 15 ALIGN .globl ret_from_sys_call ret_from_sys_call: cmpl $0,SYMBOL_NAME(intr_count) jne 2f 9: movl SYMBOL_NAME(bh_mask),%eax andl SYMBOL_NAME(bh_active),%eax jne handle_bottom_half movl EFLAGS(%esp),%eax # check VM86 flag: CS/SS are testl $(VM_MASK),%eax # different then jne 1f cmpw $(KERNEL_CS),CS(%esp) # was old code segment supervisor ? je 2f 1: sti orl $(IF_MASK),%eax # these just try to make sure andl $~NT_MASK,%eax # the program doesn't do anything movl %eax,EFLAGS(%esp) # stupid cmpl $0,SYMBOL_NAME(need_resched) jne reschedule #ifdef __SMP__ GET_PROCESSOR_OFFSET(%eax) movl SYMBOL_NAME(current_set)(,%eax), %eax #else movl SYMBOL_NAME(current_set),%eax #endif

cmpl SYMBOL_NAME(task),%eax # task[0] cannot have signals je 2f movl blocked(%eax),%ecx movl %ecx,%ebx # save blocked in %ebx for signal handling notl %ecx andl signal(%eax),%ecx jne signal_return 2: RESTORE_ALL P^_n_a la eticheta \1:" ^_n programul de mai sus asta se petrece: baz^_ndu-se pe felurite numere, cum ar _ num_arul de ^_ntreruperi ^_n curs de tratare sau num_arul de drivere active ^_n \partea de jos" (bh: bottom half), sau ^_n funct_ie de pozit_ia segmentului de stiv_a al apelantului se poate deduce din ce loc a fost chemat codul curent. De_si este deosebit de instructiv de urmat calea ^_n _ecare din aceste cazuri, noi o s_a pretindem ^_nc_ap_at_^_nat_i c_a tocmai de ^_ntoarcem ^_n spat_iul utilizator. Variabila need reschedule este nenul_a ^_n cazul ^_n care ^_n timpul execut_iei procesului curent ^_n nucleu s-au petrecut evenimente care cer ^_ntreruperea procesului curent _si comutarea la un altul. Hai s_a zicem c_a nu s-a ^_nt^_mplat nimic de acest gen, ca s_a vedem cum ne ^_ntoarcem ^_n spat_iul utilizator. Dar ^_nainte de acest pas se petrece un alt lucru foarte important: se veri_c_a dac_a procesul curent are semnale de primit. 16 3.7 Livrarea semnalelor Semnalele sunt o metod_a simplist_a de comunicat_ie inter-proces ^_n Unix. Un semnal este un eveniment identi_cat printr-un nume _si printr-un num_ar asociat. Un proces poate trimite semnale altui proces folosind apelul de sistem kill(2), cu care indic_a PID-ul _si num_arul semnalului. Semnalele pot _ trimise spontan de nucleu ^_n anumite circumstant_e. Un proces poate react_iona la un semnal ^_n mai multe feluri, _si poate controla ^_ntr-o oarecare m_asur_a livrarea semnalelor folosind o serie de funct_ii de bibliotec_a _si apeluri de sistem (signal, sigsuspend, sigpending, sigaction, etc.). Am v_azut nu demult ^_n PC Report un articol amplu consacrat semnalelor, a_sa ca nu voi discuta despre ce fac. Ce ^_nseamn_a c_a nucleul \transmite un semnal"? Fiecare proces are un array de bit_i, c^_te unul pentru _ecare semnal. C^_nd un proces prime_ste un semnal nucleul nu face altceva dec^_t s_a pun_a bitul corespunz_ator pe 1 _si s_a continue. Adev_arata livrare a semnalului se va face mai t^_rziu, c^_nd

procesul destinatar se execut_a. Din timp ^_n timp un proces veri_c_a dac_a nu i-au fost trimise semnale. De obicei face asta ^_nainte de a se bloca ^_n a_steptarea unei activit_at_i care dureaz_a mult timp, _si ^_ntotdeauna veri_c_a dac_a nu are semnale ^_n momentul c^_nd termin_a executarea unui apel de sistem. Aici am ajuns _si noi cu explicat_iile; codul cu pricina este ^_n __sierul arch/i386/kernel/entry.S: signal_return: movl %esp,%ecx pushl %ecx testl $(VM_MASK),EFLAGS(%ecx) jne v86_signal_return pushl %ebx call SYMBOL_NAME(do_signal) popl %ebx popl %ebx RESTORE_ALL Aici nu se ^_nt^_mpl_a mare lucru; se cheam_a doar funct_ia do signal cu felurite argumente pe stiv_a. Aceast_a funct_ie se ocup_a de tot ce trebuie, livr^_nd unul c^_te unul toate semnalele acumulate ^_ntre timp. Aceste semnale ar putea avea drept efect omor^_rea procesului curent, _si atunci funct_ia do signal nu se mai ^_ntoarce niciodat_a. 3.8 Sf^_r_situl ^_ntreruperii Presupun^_nd c_a do signal() se^_ntoarce, execut_ia^_n mod nucleu se termin_a cu codul lui RESTORE ALL, care extrage regi_strii salvat_i pe stiv_a atunci c^_nd s-a ^_nceput execut_ia ^_n mod nucleu. Codul este tot ^_n __sierul arch/i386/kernel/entry.S. #define RESTORE_ALL \ cmpw $(KERNEL_CS),CS(%esp); \ je 1f; \ GET_PROCESSOR_OFFSET(%edx) \ movl SYMBOL_NAME(current_set)(,%edx), %eax ; ; \ movl dbgreg7(%eax),%ebx; \ 17 movl %ebx,%db7; \ 1: LEAVE_KERNEL \ popl %ebx; \ popl %ecx; \ popl %edx; \ popl %esi; \ popl %edi; \ popl %ebp; \ popl %eax; \ pop %ds; \ pop %es; \

pop %fs; \ pop %gs; \ addl $4,%esp; \ iret Cea mai important_a instruct_iune aici este ultima, iret. Asta ^_nseamn_a \Interrupt RETurn", adic_a \^_ntoarcere din ^_ntrerupere". Aceast_a instruct_iune face exact opusul unei ^_nteruperi, _si anume descre_ste privilegiul, comut_a stivele _si se ^_ntoarce la programul ^_ntrerupt. 3.9 Terminarea funct_iei de bibliotec_a Iat_a cum periplul nostru prin nucleu s-a terminat. Ne-am ^_ntors ^_napoi ^_n corpul funct_iei de bibliotec_a getpid(), av^_nd ^_n registrul EAX valoarea PID-ului pentru procesul curent. Funct_ia aceasta vede dac_a valoarea este negativ_a (nu ar avea nici un motiv s_a _e ^_n cazul nostru), seteaz_a errno dup_a cum am descris mai sus _si se ^_ntoarce la programul apelant. 4 Rezumat Am ^_nc_alecat pe program counter _si am str_ab_atut ^_mpreun_a un periplu ^_n grotele mai super_ciale ale nucleului (alte apeluri de sistem au coduri in_nit mai complexe, cu multe regiuni critice _si cu probleme grele de re-entrant__a). S_a revedem etapele str_ab_atute: 1. Utilizatorul cheam_a o funct_ie de bibliotec_a (getpid(2)); 2. Funct_ia de bibliotec_a ^_mpacheteaz_a num_arul apelului (20) ^_ntr-un registru _si eventualele argumente ^_n alt_i regi_stri; 3. Funct_ia de bibliotec_a genereaz_a o ^_ntrerupere software (0x80); 4. Automat ^_ntreruperea comut_a ^_n mod nucleu, schimb_a stivele _si sare la o procedur_a de intercept_ie (handler); 5. Procedura de intercept_ie extrage num_arul apelului _si indexeaz_a ^_ntr-o tabel_a de apeluri de sistem; 18 6. Se sare la funct_ia care execut_a cu adev_arat apelul (sys getpid()); folosind structurile de date ale nucleului funct_ia calculeaz_a r_aspunsul; 7. Codul de ^_ntoarcere veri_c_a dac_a sunt semnale de livrat procesului curent; dac_a da, acestea sunt procesate ^_nainte de ^_ntoarcerea ^_n mod utilizator; 8. Se execut_a o instruct_iune RETI care termin_a o^_ntrerupere, restaureaz_a privilegiile sc_azute _si comut_a stivele ^_napoi; 9. Funct_ia de bibliotec_a despacheteaz_a r_aspunsul _si dac_a este necesar seteaz_a variabila errno la eroarea survenit_a;

10. Funct_ia de bibliotec_a ^_ntoarce rezultatul primit de la nucleu. Execut_ia apelului de sistem s-a terminat. Cum vi s-a p_arut? 19

S-ar putea să vă placă și