addr_t
84.1.6 84.6.1
arp.h
84.9.3
arp_clean()
84.9.3
arp_index()
84.9.3
arp_init()
84.9.3
arp_reference()
84.9.3
arp_request()
84.9.3
arp_rx()
84.9.3
ata_cmd_identify_device()
84.7.7
ata_cmd_read_sectors()
84.7.7
ata_cmd_write_sectors()
84.7.7
ata_device()
84.7.7
ata_drq()
84.7.7
ata_init()
84.7.7
ata_lba28()
84.7.7
ata_rdy()
84.7.7
ata_read_sector()
84.7.7
ata_reset()
84.7.7
ata_sector_t
84.1.6 84.7.7
ata_t
84.1.6 84.7.7
ata_valid()
84.7.7
ata_write_sector()
84.7.7
blk.h
84.7.2
blk_ata.c
84.7.2
blk_cache_init.c
84.7.2
blk_cache_read.c
84.7.2
blk_cache_save.c
84.7.2
blk_cache_t
84.1.6
cli()
84.3.2
crt0.s
84.2.2
dev.h
84.7.1
dev/dev_tty.c
84.7.1
DEV_CONSOLE
84.7.4
DEV_CONSOLEn
84.7.4
dev_dm.c
84.7.1
DEV_DMmn
84.7.4
dev_io()
84.7.1
dev_io.c
84.7.1
dev_kmem.c
84.7.1
DEV_KMEM_ARP
84.7.4
DEV_KMEM_FILE
84.7.4
DEV_KMEM_INODE
84.7.4
DEV_KMEM_MMP
84.7.4
DEV_KMEM_NET
84.7.4
DEV_KMEM_PS
84.7.4
DEV_KMEM_ROUTE
84.7.4
DEV_KMEM_SB
84.7.4
DEV_MEM
84.7.4
DEV_NULL
84.7.4
DEV_PORT
84.7.4
DEV_TTY
84.7.4
DEV_ZERO
84.7.4
directory_t
84.1.6
dm_t
84.7.6
fd_dup()
84.8.9
fd_reference()
84.8.9
fd_t
84.1.6
file_reference()
84.8.6
file_stdio_dev_make()
84.8.6
file_t
84.1.6 84.8.6
fs.h
84.8
gdt()
84.3.3
gdt_load()
84.3.3
gdt_print()
84.3.3
gdt_segment()
84.3.3
gdt_t
84.1.6
header_t
84.1.6
h_addr_t
84.1.6 84.10
ibm_i386.h
84.3
icmp_rx()
84.11
icmp_tx()
84.11
icmp_tx_echo()
84.11
icmp_tx_unreachable()
84.11
idt()
84.3.4
idtr_t
84.1.6
idt_descriptor()
84.3.4
idt_irq_remap()
84.3.4
idt_load()
84.3.4
idt_print()
84.3.4
idt_t
84.1.6
inode_alloc()
84.8.3
inode_check()
84.8.3
inode_dir_empty()
84.8.3
inode_file_read()
84.8.3
inode_file_write()
84.8.3
inode_free()
84.8.3
inode_fzones_read()
84.8.3
inode_fzones_write()
84.8.3
inode_get()
84.8.3
inode_pipe_make()
84.8.3
inode_pipe_read()
84.8.3
inode_pipe_write()
84.8.3
inode_print()
84.8.3
inode_put()
84.8.3
inode_reference()
84.8.3
inode_save()
84.8.3
inode_stdio_dev_make()
84.8.3
inode_t
84.1.6 84.8.3
inode_truncate()
84.8.3
inode_zone()
84.8.3
in_16()
84.3.1
in_8()
84.3.1
ip.h
84.10
ip_checksum()
84.10
ip_header()
84.10
ip_mask()
84.10
ip_reference()
84.10
ip_rx()
84.10
ip_table[]
84.10.1
ip_tx()
84.10
irq_off()
84.3.2
irq_on()
84.3.2
isr.s
84.4.1
isr_n()
84.3.5
isr_exception_name()
84.3.5
isr_exception_unrecoverable()
84.3.5
isr_irq_clear()
84.3.5
isr_irq_clear_pic1()
84.3.5
isr_irq_clear_pic2()
84.3.5
kbd_isr()
84.7.5.1
kbd_load()
84.7.5.1
kbd_t
84.1.6
kernel.ld
84.2.2
kernel/memory.c
84.6.1
longjmp()
84.4.8
main()
84.2.3
mboot_cmdline_opt()
84.2.1
mboot_save()
84.2.1
mb_alloc()
84.6.1
mb_alloc_size()
84.6.1
mb_clean()
84.6.1
mb_free()
84.6.1
mb_print()
84.6.1
mb_reduce()
84.6.1
mb_reference()
84.6.1
mb_size()
84.6.1
memory.h
84.6.1
MEM_BLOCK_SIZE
84.6.1
MEM_MAX_BLOCKS
84.6.1
multiboot_t
84.1.6
ne2k_check()
84.9.1
ne2k_isr()
84.9.1
ne2k_isr_expect()
84.9.1
ne2k_reset()
84.9.1
ne2k_rx()
84.9.1
ne2k_rx_reset()
84.9.1
ne2k_tx()
84.9.1
net.h
84.9
net_buffer_eth()
84.9.2
net_buffer_lo()
84.9.2
net_eth_ip_tx()
84.9.2
net_eth_tx()
84.9.2
net_index()
84.9.2
net_index_eth()
84.9.2
net_init()
84.9.2
net_rx()
84.9.2
os32.h
84.7
out_16()
84.3.1
out_8()
84.3.1
path_device()
84.8.8
path_fix()
84.8.8
path_full()
84.8.8
path_inode()
84.8.8
path_inode_link()
84.8.8
proc.h
84.4
proc_init()
84.4.4
proc_scheduler()
84.4.6
proc_sig_handler()
84.4.7
proc_t
84.1.6 84.4.2
route_init()
84.10.3
route_remote_to_local()
84.10.3
route_remote_to_router()
84.10.3
route_sort()
84.10.3
sb_inode_status()
84.8.1
sb_mount()
84.8.1
sb_print()
84.8.1
sb_reference()
84.8.1
sb_save()
84.8.1
sb_t
84.1.6 84.8.1
sb_zone_status()
84.8.1
screen_cell()
84.7.5.2
screen_clear()
84.7.5.2
screen_current()
84.7.5.2
screen_init()
84.7.5.2
screen_newline()
84.7.5.2
screen_number()
84.7.5.2
screen_pointer()
84.7.5.2
screen_putc()
84.7.5.2
screen_scroll()
84.7.5.2
screen_select()
84.7.5.2
screen_t
84.1.6
screen_update()
84.7.5.2
setjmp()
84.4.8
stack.s
84.2.2
sti()
84.3.2
sysroutine()
84.4.3 84.4.5
s_chdir()
84.8.8
s_chmod()
84.8.8
s_chown()
84.8.8 84.8.9
s_dup()
84.8.9
s_dup2()
84.8.9
s_fchmod()
84.8.9
s_fcntl()
84.8.9
s_fstat()
84.8.9
s_link()
84.8.8
s_longjmp()
84.4.8
s_lseek()
84.8.9
s_mkdir()
84.8.8
s_mknod()
84.8.8
s_mount()
84.8.8
s_open()
84.8.8
s_pipe()
84.8.9
s_read()
84.8.9
s_setjmp()
84.4.8
s_stat()
84.8.8
s_umount()
84.8.8
s_unlink()
84.8.8
s_write()
84.8.9
tcp()
84.12
tcp_close()
84.12
tcp_connect()
84.12
tcp_rx_ack()
84.12
tcp_rx_data()
84.12
tcp_show()
84.12
tcp_tx_ack()
84.12
tcp_tx_raw()
84.12
tcp_tx_rst()
84.12
tcp_tx_sock()
84.12
tty_console()
84.7.5
tty_init()
84.7.5
tty_read()
84.7.5
tty_reference()
84.7.5
tty_t
84.1.6
tty_write()
84.7.5
udp_tx()
84.12
zno_t
84.1.6
zone_alloc()
84.8.2
zone_free()
84.8.2
zone_print()
84.8.2
zone_read()
84.8.2
zone_write()
84.8.2
_in_16()
84.3.1
_in_8()
84.3.1
_out_16()
84.3.1
_out_8()
84.3.1
os32 è uno studio che applica qualche rudimento relativo ai sistemi operativi, basandosi sull'architettura x86-32 IBM AT, utilizzando come strumenti di sviluppo GNU C, GNU AS e GNU LD, su un sistema GNU/Linux. Il risultato non è un sistema operativo utilizzabile, ma una struttura su cui poter fare esperimenti e di cui è possibile mostrare (in termini tipografici) ed eventualmente descrivere ogni riga di codice.
os32 contiene uno schedulatore banale e molto limitato, un'organizzazione dei processi ad albero e una funzionalità di amministrazione dei segnali, una gestione del file system Minix 1 e delle partizioni in stile Dos (ma solo di quelle primarie), una shell banale e qualche programma di servizio di esempio.
La differenza più importante di os32, rispetto a os16, sta nel fatto che non si utilizzano più le funzioni del BIOS tradizionale, dal momento che si opera in modalità protetta.
Tutti i file di os32 dovrebbero essere disponibili a partire da allegati/os32. In particolare, il file disk.hda
è l'immagine di un vecchio disco PATA, suddiviso in due partizioni: la prima partizione con un file system Dos-FAT, contenente SYSLINUX e il kernel di os32; la seconda contenente un file system Minix 1 con il sistema.
Gli script preparati per os32 prevedono che i file system contenuti nel file-immagine che rappresentano l'unità PATA, in fase di sviluppo si trovino innestati nelle directory /mnt/disk.hda.1/
e /mnt/disk.hda.2/
. Pertanto, se si ricompila os32, tali directory vanno predisposte (oppure vanno modificati gli script con l'organizzazione che si preferisce attuare). Tuttavia, considerato che non è facile lavorare con file-immagine suddivisi in partizioni, altri script aiutano nelle operazioni di manutenzione: fdisk
, format
, mount
, umount
.
Per la verifica del funzionamento del sistema, è previsto l'uso equivalente di Bochs o di Qemu. Per questo scopo sono disponibili gli script bochs
e qemu
(rispettivamente i listati 94.1.2 e 94.1.9), con le opzioni necessarie a operare correttamente.
Per la compilazione del lavoro si usano due script alternativi: makeit.mer
o makeit.sep
(listato 94.1.8). Lo script makeit.mer
conduce una compilazione in cui i file eseguibili degli applicativi sono tali da condividere lo stesso segmento di memoria, sia per il codice, sia per i dati; al contrario, lo script makeit.sep
fa sì che codice e dati siano distinti (il kernel si compila solo in formato ELF). I due script ricreano ogni volta i file-make, basandosi sui file presenti effettivamente nelle varie directory previste; inoltre, alla fine della compilazione, copiano il kernel nella prima partizione del file-immagine del disco PATA (purché risulti innestata come previsto nella directory /mnt/disk.hda.1/
) e nella seconda partizione copiano gli applicativi.
Va osservato che il lavoro si basa su un file system Minix 1 (sezione 68.7) perché è molto semplice, ma soprattutto, la prima versione è quella che può essere utilizzata facilmente in un sistema operativo GNU/Linux (sul quale avviene lo sviluppo di os32). È bene sottolineare che si tratta della versione con nomi da 14 caratteri, ovvero quella tradizionale del sistema operativo Minix, mentre nei sistemi GNU/Linux, la creazione predefinita di un file system del genere produce una versione particolare, con nomi da 30 caratteri.
Gli script descritti nella sezione precedente, si trovano all'inizio della gerarchia prevista per os32. Le directory successive dividono in modo molto semplice le varie componenti per la compilazione:
|
La libreria C non è completa, limitandosi a contenere ciò che serve per lo stato di avanzamento attuale del lavoro. Si osservi che nella directory lib/_gcc/
si collocano file contenenti una libreria di funzioni in linguaggio C, necessaria al compilatore GNU C per compiere il proprio lavoro correttamente con valori da 64 bit.
Nell'ottica della massima semplicità, gli eseguibili degli applicativi di os32 hanno una struttura propria, schematizzata dalla figura successiva. Tale struttura viene ottenuta attraverso i file sorgenti crt0.mer.s
o crt0.sep.s
, e i file di configurazione di GNU LD applic.mer.ld
o applic.sep.ld
. La sigla *.mer.*
individua la compilazione in un solo segmento, sia per il codice, sia per i dati, mentre la sigla *.sep.*
riguarda la situazione opposta, in cui codice e dati si trovano divisi.
Nella figura si mettono a confronto la struttura dell'eseguibile di un applicativo compilato per avere codice e dati nello stesso segmento, rispetto al caso in cui questi sono separati. Nei primi quattro byte c'è un'istruzione di salto al codice che si trova subito dopo l'intestazione, quindi appare un'impronta di riconoscimento che occupa complessivamente otto byte. Tale impronta è la rappresentazione esadecimale della stringa «os32appl», ma spezzata in due e rovesciata a causa dell'architettura little endian (se si legge il file in esadecimale, si vede la sequenza dei caratteri «lppa23so»).
Dopo l'impronta di riconoscimento si trovano, rispettivamente, lo scostamento del segmento dati, espresso in byte, gli indirizzi conclusivi dell'area del codice, dei dati inizializzati e di quelli non inizializzati. Alla fine viene indicata la dimensione richiesta per la pila dei dati. Per distinguere se l'eseguibile è fatto per gestire in un solo segmento codice e dati, oppure se questi devono essere separati, va osservato il valore di data_offset: se questo è zero, significa che il segmento dati parte dall'indirizzo zero, esattamente come il segmento codice, pertanto si trovano nello stesso spazio di indirizzamento. Se invece il valore di data_offset è diverso da zero, allora deve coincidere con il valore di end_text, in quanto i dati inizializzati si trovano nel file a partire dalla fine del codice, ma devono poi collocarsi in un segmento separato, per cui il valore di end data è da intendere riferito all'indirizzo iniziale della zona dei dati, ovvero zero.
Nel file eseguibile, la porzione che contiene i dati inizializzati, parte con un'impronta ulteriore, costituita da «os32data», con gli stessi problemi di inversione già descritti per l'intestazione. Lo scopo di questa impronta è semplicemente quello di evitare che ci possano essere dati che iniziano precisamente dall'indirizzo zero, essendo questo riservato per il puntatore nullo.
Il kernel è in formato ELF, ma nella prima parte del codice viene piazzata un'impronta, secondo le specifiche multiboot (sezione 65.5.1).
|
Nel codice del kernel si utilizzano spesso delle informazioni organizzate in memoria in forma di tabella. Si tratta precisamente di array, le cui celle sono costituite generalmente da variabili strutturate. Queste tabelle, ovvero gli array che le rappresentano, sono dichiarate come variabili pubbliche; tuttavia, per facilitare l'accesso ai rispettivi elementi e per uniformità di comportamento, viene abbinata loro una funzione, con un nome terminante per ..._reference(), con cui si ottiene il puntatore a un certo elemento della tabella, fornendo gli argomenti appropriati. Per esempio, la tabella degli inode in corso di utilizzazione viene dichiarata così nel file kernel/fs/inode_table.c
:
|
Successivamente, la funzione inode_reference() offre il puntatore a un certo inode:
|
Per cercare di dare un po' di uniformità al codice del kernel e a quello della libreria, dove possibile, i nomi delle variabili seguono una certa logica, riassunta dalla tabella successiva.
|
Nel codice del kernel e nella libreria specifica di os32 si usano dei tipi derivati speciali, riassunti nella tabella successiva.
|
Il kernel di os32 è compilato in formato ELF, secondo le specifiche multiboot, in modo da poter essere avviato da un sistema come GRUB 1 o SYSLINUX. Il codice del kernel inizia nel file crt0.s
, dove a un certo punto viene eseguita la funzione kmain(), nella quale si sintetizza il funzionamento del kernel stesso. Il sistema di avvio colloca il kernel a partire dall'indirizzo 10000016 (1 Mibyte), già in modalità protetta, di conseguenza il codice è organizzato per iniziare da tale posizione.
os32 è conforme alle specifiche multiboot per consentirne l'avvio attraverso GRUB 1 o SYSLINUX, senza doversi prendere carico dei problemi relativi al passaggio alla modalità protetta. Perché il file del kernel sia riconosciuto come aderente a tali specifiche, contiene un'impronta di riconoscimento, definita multiboot header, collocata nella parte iniziale, come dichiarato nel file kernel/main/crt.s
, entro i primi 8 Kibyte.
Il primo campo da 32 bit, definito magic, contiene l'impronta di riconoscimento vera e propria, costituita precisamente dal numero 1BADB00216. Il secondo campo da 32 bit, definito flags, contiene degli indicatori con i quali si richiede un certo comportamento al sistema di avvio. Il terzo campo da 32 bit, definito checksum, contiene un numero calcolato in modo tale che la somma tra i numeri contenuti nei tre campi da 32 bit porti a ottenere zero, senza considerare i riporti.
I nomi indicati sono quelli definiti dallo standard e, come si vede, il campo checksum si ottiene calcolando -(magic + flags), dove si deve intendere che i calcoli avvengono con valori interi senza segno e si ignorano i riporti.
Dal momento che il kernel da avviare è in formato ELF, le informazioni che il sistema di avvio necessita per piazzarlo correttamente in memoria e per passare il controllo allo stesso, sono già disponibili e non c'è la necessità di occuparsi di altri campi facoltativi che possono seguire i tre già descritti. Stante questa semplificazione, per quanto riguarda il campo flags, os32 utilizza precisamente il valore 0000000316, con il significato che si vede nella figura successiva.
Il bit meno significativo del campo flags, se impostato a uno, serve a richiedere il caricamento in memoria dei moduli eventuali (assieme al file-immagine principale) in modo che risultino allineati all'inizio di una «pagina» (ovvero all'inizio di un blocco da 4 Kibyte). os32 non prevede moduli, tuttavia richiede ugualmente questa opzione. Il secondo bit del campo flags serve a richiedere al sistema di avvio di passare le informazioni disponibili sulla memoria, le quali poi vengono rese disponibili a partire da un'area a cui punta inizialmente il registro EBX.
Quando il sistema di avvio passa il controllo al kernel, dopo averlo caricato in memoria: il microprocessore è in modalità protetta; il registro EAX contiene il numero 2BADB00216; il registro EBX contiene l'indirizzo fisico, a 32 bit, di una sequenza di campi contenenti informazioni passate dal sistema di avvio (multiboot information structure), come si vede nella figura successiva.
|
Come si può intuire leggendo la tabella che descrive i primi cinque campi, il significato dei bit del campo flags viene attribuito, mano a mano che l'aggiornamento delle specifiche prevede l'espansione della struttura informativa. Per esempio, un campo flags con il valore 1002 sta a significare che esistono i campi fino a cmdline e il contenuto di quelli precedenti non è valido, ma i campi successivi, non esistono affatto. La comprensione di questo concetto dovrebbe rendere un po' più semplice la lettura delle specifiche.
|
Listati 94.1.7, 94.9.2 e 94.9.6.
Il codice del kernel inizia dal file crt.s
; tuttavia, per la sua corretta interpretazione, va considerato prima il file di configurazione di GNU LD (il collegatore, ovvero il linker), costituito dal file kernel.ld
, sintetizzabile così:
|
Si osserva subito che il punto di inizio del codice, descritto successivamente dal file crt.s
, deve corrispondere alla posizione dell'etichetta kstartup e che quel punto deve trovarsi all'indirizzo 10000016, ovvero quello in cui il sistema di avvio lo colloca.
Nel file crt.s
, dopo il preambolo in cui si dichiarano i simboli esterni e quelli interni da rendere pubblici, si parte proprio con l'etichetta kstartup, e da lì si salta a un'altra posizione (start), per lasciare spazio all'intestazione multiboot.
|
L'immagine del kernel in memoria utilizza un solo segmento per codice e dati, suddividendosi nel modo consueto: codice, dati inizializzati, dati non inizializzati e pila. Per individuare le varie componenti, il file kernel.ld
inserisce dei nomi a cui è possibile fare riferimento nel codice; inoltre, viene utilizzato il file stack.s
per definire lo spazio usato per la pila dei dati.
A partire dall'indirizzo corrispondente all'etichetta start, nel file crt0.s
inizia il lavoro preliminare del kernel. Per prima cosa viene attivata la pila dei dati, collocando nel registro ESP l'indirizzo corrispondente alla fine della stessa, ovvero _k_stack_bottom:
|
Quindi si azzera il registro EFLAGS, sfruttando per questo la pila appena attivata:
|
Infine si chiama la funzione kmain() (del file kmain.c
), fornendo come argomenti la firma di riconoscimento del sistema multiboot, contenuta nel registro EAX, e il puntatore alla struttura contenente le informazioni fornite dal sistema multiboot, contenuto nel registro EBX:
|
Se ci dovesse essere una conclusione della funzione kmain(), si passerebbe al codice successivo, il quale si limita a mettere a riposo la CPU:
|
Listato 94.9 e successivi.
Tutto il lavoro del kernel di os32 si sintetizza nella funzione kmain(), contenuta nel file kernel/main/kmain.c
. Per poter dare un significato a ciò che vi appare al suo interno, occorre conoscere tutto il resto del codice, ma inizialmente è utile avere un'idea di ciò che succede, se poi si vuole compilare ed eseguire il sistema operativo.
La funzione si chiama kmain() (e non main()), perché non è conforme allo schema che dovrebbe avere la prima funzione di un programma per sistemi POSIX. Come già accennato a proposito del file crt0.s
, la funzione kmain() prevede come parametri un codice di riconoscimento e il puntatore a delle informazioni, forniti dal sistema di avvio.
|
Dopo la dichiarazione delle variabili si inizializza la gestione del video della console (funzione tty_init()), si verifica che il codice sia stato avviato da un sistema di avvio multiboot e se ne salvano le informazioni (funzione mboot_save()), quindi si mostra un messaggio iniziale, si imposta la dimensione massima della memoria disponibile in base ai dati ottenuti dal sistema multiboot (funzione mb_size()), si configura la tastiera (funzione kbd_load()), si inizializza la gestione della memoria tampone (funzione blk_cache_init()), del file system (funzione fs_init()) e dei processi elaborativi (proc_init()). Fatto tutto questo appare un menù (funzione menu()) e si passa a una fase successiva.
|
A questo punto il kernel ha concluso le sue attività preliminari e, per motivi diagnostici, mostra un menù, quindi inizia un ciclo in cui ogni volta esegue una chiamata di sistema nulla e poi legge un comando dalla tastiera, costituito però da un solo carattere: se risulta selezionato un comando previsto, il kernel esegue quanto richiesto e poi riprende il ciclo. La chiamata di sistema nulla serve a far sì che lo schedulatore ceda il controllo a un altro processo, ammesso che questo esista, consentendo l'avvio di processi ancor prima di avere messo in funzione quel processo che deve svolgere il ruolo di init.
In generale le chiamate di sistema sono fatte per essere usate solo dalle applicazioni; tuttavia, in pochi casi speciali il kernel le deve utilizzare come se fosse proprio un'applicazione. Qui si rende necessario l'uso della chiamata nulla, perché quando è in funzione il codice del kernel non ci possono essere interruzioni esterne e quindi nessun altro processo verrebbe messo in condizione di funzionare. |
Le funzioni principali disponibili in questa modalità diagnostica sono riassunte nella tabella successiva:
|
Premendo [x], il kernel avvia /bin/init
, quindi si mette in un altro ciclo, dove si limita a passare ogni volta il controllo allo schedulatore, attraverso la chiamata di sistema nulla.
|
c abaabaaba p pp p pg T * 0x1000 D * 0x1000 stack id id rp tty uid euid suid usage s addr size addr size pointer name 0 0 0 0000 0 0 0 00.03 R 00000 028e 00000 0000 028eb2c os32 kernel 0 1 0 0000 0 0 0 00.09 r 0051e 000e 0052c 002d 002cf88 /bin/ccc 1 2 0 0000 10 10 10 00.00 s 002bc 000e 002ca 002d 002cf34 /bin/aaa 1 3 0 0000 11 11 11 00.00 s 002f7 000e 00305 002d 002cf34 /bin/bbb ab m Hex mem map, blocks of 1000: 0-28f 2bc-332 51e-559 aabaab_ |
os32 build 20AAMMGGHHmm ram 130048 Kibyte [ata_init] ATA drive 0 size 8064 Kib [ata_drq] ERROR: drive 2 error [dm_init] ATA drive=0 total sectors=16128 [dm_init] partition type=0c start sector=63 total sectors=2961 [dm_init] partition type=81 start sector=3024 total sectors=13104 .------------------------------------------------------------------. | h show this menu .--------------.| | t show internal timer values | all commands || | f fork the kernel | followed by || | m memory map (HEX) | [Enter] || | g|G show GDT table first 21+21 items `--------------'| | i|I show IDT table first 21+21 items | | p process status list | | s super block list | | n list of active inodes | | 1..9 kill process 1 to 9 | | A..F kill process 10 to 15 | | a..c run programs `/bin/aaa' to `/bin/ccc' in parallel | | x exit interaction with kernel and start `/bin/init' | | q quit kernel | `------------------------------------------------------------------' x init os32: a basic os. [Ctrl q], [Ctrl r], [Ctrl s], [Ctrl t] to change console. This is terminal /dev/console0 Log in as "root" or "user" with password "ciao" :-) login: |
Listato 94.6 e successivi.
Il file kernel/ibm_i386.h
e quelli contenuti nella directory kernel/ibm_i386/
, raccolgono il codice del kernel che è legato strettamente all'hardware, escludendo però la gestione dei dispositivi. Tra le altre spiccano particolarmente le funzioni per la gestione dei segmenti di memoria (la tabella GDT), delle interruzioni (la tabella IDT) e l'attivazione delle routine associate alle interruzioni (ISR).
Alcune delle funzioni scritte in linguaggio assemblatore hanno nomi che iniziano con un trattino basso, ma a fianco di queste sono disponibili delle macroistruzioni, con nomi equivalenti senza il trattino basso iniziale, per garantire che gli argomenti della chiamata abbiano il tipo corretto, restituendo un valore intero «normale», quando qualcosa deve essere restituito.
Alcune funzioni e macroistruzioni di questo gruppo sono destinate a facilitare l'input e l'output con le porte interne dell'architettura x86.
|
Alcune funzioni di questo gruppo sono destinate a facilitare il controllo delle interruzioni hardware che raggiungono la CPU.
|
Nel momento in cui il codice del kernel prende il controllo, il microprocessore si trova già a funzionare in modalità protetta, attraverso una tabella GDT già impostata per gestire la memoria in modo lineare, senza particolari accorgimenti. Quando il kernel inizializza la gestione dei processi (funzione proc_init()) costruisce una nuova tabella GDT, nella quale, per ogni processo gestibile, predispone due elementi, per descrivere rispettivamente il segmento codice e il segmento dati di un processo. In pratica, la nuova tabella GDT è composta da una prima voce nulla, obbligatoria, da una coppia di voci che descrivono il segmento codice e dati del kernel, da altre coppie di voci, modificate poi durante il funzionamento, per descrivere i segmenti dei processi.
Tutti i processi vedono la memoria con un indirizzamento che corrisponde a quello reale; tuttavia, disponendo ognuno di una propria coppia di voci nella tabella GDT, è possibile controllarne l'uso in modo da impedire che possano raggiungere aree al di fuori della propria competenza.
La tabella GDT è rappresentata in C dall'array gdt_table[] dichiarato nel file kernel/ibm_i386/gdt_public.c
(listato 94.6.11), composto da elementi di tipo gdt_t (listato 94.6).
|
La tabella viene creata con una quantità di elementi pari al valore della macro-variabile GDT_ITEMS. Sapendo che la prima voce è obbligatoriamente nulla, che se ne usano altre due per il kernel e che ogni processo utilizza due voci della tabella, si possono gestire al massimo (GDT_ITEMS-3)/2 processi.
La struttura di ogni elemento della tabella GDT è molto complessa, pertanto, per scriverci un nuovo valore si usa la funzione gdt_segment() che si occupa di spezzettare e ricollocare i dati come richiesto dal microprocessore (listato 94.6.12)
|
La tabella IDT serve al microprocessore per conoscere quali procedure avviare al verificarsi delle interruzioni. La funzione idt() si occupa di predisporre la tabella e di attivarla, ma prima di ciò si prende cura di posizionare le interruzioni hardware a partire dalla voce 32 (la 33-esima). Le procedure a cui fa riferimento la tabella IDT creata con la funzione idt() sono dichiarate nel file kernel/ibm_i386/isr.s
, descritto però nella sezione successiva.
La tabella IDT è rappresentata in C dall'array idt_table[] dichiarato nel file kernel/ibm_i386/idt_public.c
(listato 94.6.18), composto da elementi di tipo idt_t (listato 94.6).
|
La tabella viene creata con 129 elementi, anche se più della metà non vengono usati; tuttavia, proprio l'ultimo, corrispondente all'interruzione 12810, ovvero 8016, serve per le chiamate di sistema.
La struttura di ogni elemento della tabella IDT è un po' complicata, pertanto, per scriverci un nuovo valore si usa la funzione idt_descriptor() che si occupa di spezzettare e ricollocare i dati come richiesto dal microprocessore (listato 94.6.14)
|
Le interruzioni che individua il microprocessore (eccezioni, interruzioni software e interruzioni hardware) fanno interrompere l'attività normale dello stesso, costringendolo ad accumulare nella pila attuale dei dati lo stato di alcuni registri ed eventualmente di un codice di errore, saltando poi alla posizione di codice indicata nella voce corrispondente nella tabella IDT. Va osservato che, per semplicità, os32 fa lavorare i propri processi nell'anello zero, come il kernel, per cui i dati accumulati nella pila si limitano a quelli della figura successiva, perché non c'è mai un passaggio da un livello di privilegio a un altro.
Le posizioni del codice a cui il microprocessore deve saltare, secondo le indicazioni della tabella IDT, sono contenute tutte nel file kernel/ibm_i386/isr.s
, mentre nel file kernel/ibm_i386.h
vi si fa riferimento attraverso dei prototipi di funzione, benché non si tratti propriamente di funzioni.
|
Listato 94.6.21; listato 94.14 e successivi.
La gestione dei processi è raccolta nei file kernel/proc.h
e kernel/proc/...
; tuttavia, dal file kernel/ibm_i386/isr.s
hanno origine le procedure attivate dalle interruzioni e dalle chiamate di sistema: le chiamate di sistema e le interruzioni provenienti dal temporizzatore interno provocano l'attivazione dello schedulatore.
Con os32, quando un processo viene interrotto per lo svolgimento del compito dell'interruzione, si passa sempre a utilizzare la pila dei dati del kernel. Per annotare la posizione in cui si trova l'indice della pila del kernel si usa la variabile _ksp, accessibile anche dal codice in linguaggio C.
Il codice del kernel può essere interrotto dagli impulsi del temporizzatore, ma in tal caso non viene coinvolto lo schedulatore per lo scambio con un altro processo, così che dopo l'interruzione è sempre il kernel che continua a funzionare; pertanto, nella funzione kmain() è il kernel che cede volontariamente il controllo a un altro processo (ammesso che ci sia) con una chiamata di sistema nulla.
Il file kernel/ibm_i386/isr.s
contiene il codice per la gestione delle interruzioni dei processi. Nella parte iniziale del file, vengono dichiarate delle variabili, alcune delle quali sono pubbliche e accessibili anche dal codice in C.
|
Si tratta di variabili scalari da 32 bit, tenendo conto che: i simboli kticks_lo e kticks_hi compongono assieme la variabile _clock_kernel a 64 bit per il linguaggio C; i simboli tticks_lo e tticks_hi compongono assieme la variabile _clock_time a 64 bit per il linguaggio C.
Dopo la dichiarazione delle variabili inizia il codice vero e proprio, dove i simboli isr_n si riferiscono al codice da usare in presenza dell'interruzione n. Tra tutte, le interruzioni più importanti sono quelle del temporizzatore (isr_32()), il quale produce un impulso a circa 100 Hz; quelle della tastiera (isr_33()) e delle chiamate di sistema (isr_128()).
Il codice per la gestione dei tre tipi di interruzione più importanti ha delle similitudini che conviene analizzare simultaneamente. os32 non cambia mai anello, nel senso che il livello di privilegio dei processi è pari a quello del kernel; pertanto, nel momento in cui si verifica un'interruzione, la pila e il segmento dati in essere sono quelli del processo interrotto. Le procedure che gestiscono le tre interruzioni principali iniziano con il salvataggio dei registri nella pila attuale e il passaggio al segmento dei dati del kernel, lasciando temporaneamente la pila nel segmento dati del processo interrotto; nello stesso modo, terminano con il ripristino del segmento dati originario (al momento dell'interruzione) e il ripristino successivo dei registri, estraendone i valori dalla pila:
|
Il segmento dati del kernel si trova nella terza voce della tabella GDT (la prima è nulla, la seconda è per il codice del kernel, la terza è per i dati del kernel). Sapendo che ogni voce occupa 8 byte (64 bit), per raggiungere l'inizio della terza voce occorre indicare il valore 16 nel registro di segmento.
Durante l'elaborazione di un'interruzione proveniente dal temporizzatore o dalla tastiera, è necessario sapere se è già in corso l'elaborazione di una chiamata di sistema. Se ciò accade, l'impulso del temporizzatore viene recepito, incrementando i contatori, ma non viene fatto altro, mentre l'impulso della tastiera viene semplicemente ignorato.
|
In pratica, quando si presenta una chiamata di sistema, inizialmente viene assegnato il valore uno alla variabile syscall_working, mentre alla fine del suo compito questa variabile viene azzerata:
|
Quando l'interruzione proviene dal temporizzatore e non è in corso l'esecuzione di una chiamata di sistema, oppure quando l'interruzione deriva proprio da una chiamata di sistema, viene attivato lo schedulatore (direttamente o indirettamente, attraverso la funzione che svolge il lavoro richiesto dalla chiamata di sistema), ma per fare questo, è necessario passare alla pila dei dati del kernel, per poi ripristinarla successivamente:
|
Dopo il salvataggio dei registri principali e dopo il cambiamento del segmento dati, rimanendo ancora sulla pila dei dati del processo interrotto, la routine di gestione delle interruzioni del temporizzatore si occupa di incrementare i contatori degli impulsi. Gli impulsi giungono alla frequenza di 100 Hz circa, per cui non c'è la necessità di fare alcun tipo di conversione:
|
A questo punto, se l'interruzione è avvenuta mentre era in corso l'elaborazione di una chiamata di sistema, tutto si conclude con il ripristino dei registri e del PIC1, in modo da consentire la ripresa delle interruzioni. Se invece l'interruzione è avvenuta in una situazione differente, si verifica ancora che non sia stato interrotto il funzionamento del kernel stesso, perché se così fosse, anche in questo caso la procedura termina con il solito ripristino dei registri e del PIC1.
|
Se non è stato interrotto il codice del kernel, viene chiamata la funzione proc_scheduler(), la quale può cambiare i valori delle variabili pubbliche proc_stack_segment_selector e proc_stack_pointer, provocando così la sostituzione del processo interrotto, quando subito dopo si ripristina la pila a cui queste due variabili fanno riferimento.
La pila dei dati al momento dell'interruzione dovuta a una chiamata di sistema, contiene anche le informazioni necessarie a conoscere il tipo di funzione richiesta e gli argomenti di questa, in forma di variabile strutturata, di cui viene trasmesso il puntatore.
Dopo il salvataggio dei registri principali e dopo il cambiamento del segmento dati, rimanendo ancora sulla pila dei dati del processo interrotto, si recuperano dalla pila le informazioni necessarie a ricostruire la funzione richiesta, salvandole in variabili locali:
|
A questo punto, in modo simile a quanto avviene per le interruzioni del temporizzatore, si verifica se la chiamata di sistema è avvenuta durante il funzionamento del kernel, cosa che os32 consente. Tuttavia, la chiamata di sistema viene eseguita ugualmente, solo che si salva l'indice della pila nella variabile _ksp; pertanto, è proprio attraverso una prima chiamata di sistema nulla che os32 inizializza la gestione delle interruzioni.
|
A questo punto viene eseguito il passaggio alla pila del kernel, indipendentemente dal fatto che serva o meno, quindi viene chiamata la funzione sysroutine(), inserendo nella pila attuale i parametri richiesti e salvati precedentemente all'interno di variabili locali:
|
I passi successivi includono il ripristino della pila precedente, secondo quanto annotato nelle variabili globali proc_stack_segment_selector e proc_stack_pointer, e lo stato dei registri dalla nuova pila.
Va osservato che la funzione sysroutine() oltre che prendersi carico di eseguire il compito della chiamata di sistema richiesta, provvede poi a sostituire il processo interrotto, avvalendosi a sua volta della funzione proc_scheduler().
Listato 94.14.
Nel file kernel/proc.h
viene definito il tipo proc_t, con il quale, nel file kernel/proc/proc_public.c
si definisce la tabella dei processi, rappresentata dall'array proc_table[].
|
La tabella successiva descrive il significato dei vari membri previsti dal tipo proc_t. Va osservato che la cosiddetta «u-area» (user area) non viene gestita come un sistema Unix tradizionale e tutti i dati dei processi sono raccolti nella tabella gestita dal kernel. Di conseguenza, dal momento che i processi non dispongono di una tabella personale con i dati della u-area, devono avvalersi sempre di chiamate di sistema per leggere i dati del proprio processo.
|
L'indice della tabella dei processi corrisponde al numero del processo, ovvero il PID, che infatti non è rappresentato al suo interno. Tuttavia, per accedervi più agevolmente, viene usata la funzione proc_reference(), la quale, fornendo il numero PID desiderato, fornisce il puntatore alla voce della tabella che lo descrive(listato 94.14.6).
I processi eseguono una chiamata di sistema attraverso la funzione sys(), dichiarata nel file lib/sys/os32/sys.s
(listato 95.21.7). La funzione in sé, per come è dichiarata, potrebbe avere qualunque parametro, ma in pratica ci si attende che il suo prototipo sia il seguente:
void sys (int syscallnr, void *message, size_t size); |
Il numero della chiamata di sistema, richiesto come primo parametro, si rappresenta attraverso una macro-variabile simbolica, definita nel file lib/sys/os32.h
.
Per fornire dei dati a quella parte di codice che deve svolgere il compito richiesto, si usa una variabile strutturata, di cui viene trasmesso il puntatore (riferito al segmento dati del processo che esegue la chiamata) e la dimensione complessiva.
Nel file lib/sys/os32.h
sono definiti dei tipi derivati, riferiti a variabili strutturate, per ogni tipo di chiamata(listato 95.21). Per esempio, per la chiamata di sistema usata per cambiare la directory corrente del processo, si usa un messaggio di tipo sysmsg_chdir_t:
|
In realtà, la funzione sys(), si limita a produrre un'interruzione software, da cui viene attivata la routine che inizia al simbolo isr_128 nel file kernel/ibm_i386/isr.s
, la quale estrapola le informazioni salienti dalla pila dei dati e poi le fornisce alla funzione sysroutine():
void sysroutine (uint32_t syscallnr, uint32_t msg_off, uint32_t msg_size); |
I parametri della funzione sysroutine() corrispondono in pratica agli argomenti della chiamata della funzione sys(), con la differenza che nei vari passaggi hanno perso l'identità originaria e giungono come numeri puri e semplici. A questo proposito, il secondo parametro cambia nome, in quanto ciò che prima era il puntatore a un'area di memoria, qui va interpretato come lo scostamento rispetto al segmento dati del processo (segment offset).
Listato 94.14.3.
void proc_init (void); |
La funzione proc_init() viene chiamata dalla funzione kmain(), una volta sola, per attivare la gestione dei processi elaborativi. Si occupa di compiere le azioni seguenti:
predisporre la tabella GDT, attraverso la chiamata della funzione gdt();
impostare il temporizzatore in modo da fornire impulsi alla frequenza dichiarata nella macro-variabile CLOCKS_PER_SEC, pari a 100 Hz;
predisporre la tabella IDT, attraverso la chiamata della funzione idt();
azzerare la tabella dei processi, inserendovi però i dati relativi al kernel (il processo zero);
allocare la memoria già utilizzata dal kernel;
attivare selettivamente le interruzioni hardware desiderate;
attivare la gestione delle unità PATA;
innestare il file system principale.
Listato 94.14.28.
La funzione sysroutine() viene chiamata esclusivamente dalla routine attivata dalle chiamate di sistema (tale routine è introdotta dal simbolo isr_128 nel file kernel/ibm_i386/isr.s
) e ha i parametri che si possono vedere dal prototipo:
void sysroutine (uint32_t syscallnr, uint32_t msg_off, uint32_t msg_size); |
Il primo parametro è il numero della chiamata di sistema che ha provocato l'interruzione; gli altri due danno la posizione e la dimensione del messaggio inviato attraverso la chiamata di sistema.
All'inizio della funzione viene dichiarato un puntatore a un'unione di tutti i tipi di messaggio gestibili:
|
Viene quindi calcolata la collocazione del messaggio originale, per poi poter assegnare a msg il puntatore a tale messaggio.
Le chiamate di sistema sono fatte per le applicazioni, ma al kernel è consentito di eseguirne alcune, per motivi particolari. Se però il kernel tenta di eseguire una chiamata differente, si ottiene un messaggio di avvertimento, ma si tenta ugualmente l'esecuzione della richiesta.
Disponendo del puntatore msg, sapendo di quale chiamata di sistema si tratta, il messaggio può essere letto come:
msg->tipo_chiamata |
Per esempio, per la chiamata di sistema SYS_CHDIR, si deve fare riferimento al messaggio msg->chdir; pertanto, per raggiungere il membro ret del messaggio si usa la notazione msg->chdir.ret.
Per distinguere il tipo di chiamata si usa una struttura di selezione:
|
Il messaggio usato per trasmettere i dati della chiamata, può servire anche per restituire dei dati al mittente, pertanto, spesso alcuni contenuti vengono modificati. Ciò succede particolarmente con il membro ret che generalmente rappresenta il valore restituito dalla chiamata di sistema.
Al termine del lavoro, viene chiamata la funzione proc_scheduler().
Listato 94.14.11.
La funzione proc_scheduler() non prevede parametri e riceve le informazioni che le possono servire attraverso variabili pubbliche: _ksp, proc_stack_pointer, proc_stack_segment_selector e proc_current. A sua volta, la funzione aggiorna i valori di queste variabili, per mettere in pratica uno scambio di processi.
void proc_scheduler (void); |
La prima cosa che fa la funzione consiste nel verificare che il valore dell'indice della pila del processo interrotto non superi lo spazio disponibile per la pila stessa. Diversamente il processo viene eliminato forzatamente, con una segnalazione adeguata sul terminale attivo. Si ottiene comunque una segnalazione se l'indice si avvicina pericolosamente al limite.
Successivamente la funzione svolge delle operazioni che riguardano tutti i processi: aggiorna i contatori dei processi che attendono lo scadere di un certo tempo; verifica la presenza di segnali e predispone le azioni relative; raccoglie l'input dai terminali.
|
A quel punto aggiorna il tempo di utilizzo della CPU del processo appena interrotto:
|
Quindi inizia la ricerca di un altro processo, candidato a essere ripreso al posto di quello interrotto. La ricerca inizia dal processo successivo a quello interrotto, senza considerare alcun criterio di precedenza. Il ciclo termina se la ricerca incontra di nuovo il processo di partenza.
|
All'interno di questo ciclo di ricerca, se si incontra un processo pronto per essere messo in funzione, lo si scambia con quello interrotto: in pratica si salva il valore attuale dell'indice della pila, si scambiano gli stati e si aggiornano i valori di proc_current, proc_stack_segment_selector e proc_stack_pointer, in modo da ottenere effettivamente lo scambio all'uscita dalla funzione:
|
Alla fine del ciclo, occorre verificare se esiste effettivamente un processo successivo attivato, perché in caso contrario, si lascia il controllo direttamente al kernel. In fine, si salva il valore accumulato in precedenza dell'indice della pila del kernel, nella variabile _ksp.
Un processo può ricevere un segnale, a seguito del quale può essere interrotto per compiere una certa azione. La maggior parte dei segnali può essere inibita, in modo tale che ricevendoli il processo non venga a essere disturbato, oppure si può associare loro una funzione, da eseguire al momento del ricevimento del tale segnale. Diversamente, in mancanza di tale associazione, il ricevimento di un segnale comporta un'azione predefinita.
L'associazione di una funzione allo scattare di un segnale si ottiene, nel codice dell'applicazione, con la funzione signal() (listato 95.17.3), la quale attraverso una chiamata di sistema fornisce al kernel tutti i dati necessari per la programmazione del segnale.
La vera difficoltà sta nell'esecuzione effettiva della funzione, nel momento in cui scatta il segnale previsto per il processo.
sighandler_t signal (int sig, sighandler_t handler); |
La funzione signal() richiede l'indicazione del numero del segnale da programmare e di un puntatore rappresentato da una funzione che si vuole azionare nel momento in cui scatta il segnale in questione. Il tipo sighandler_t rappresenta il puntatore a una funzione che richiede un parametro di tipo intero, costituito dal numero del segnale ricevuto, e non restituisce alcunché; pertanto, la funzione che si passa come secondo parametro della funzione signal() deve avere la forma seguente:
void handler (int sig); |
La funzione signal(), a sua volta, esegue finalmente la chiamata di sistema, ma oltre al numero del segnale e al puntatore della funzione da azionare, invia il puntatore di un'altra funzione, denominata _sighandler_wrapper(), il cui scopo è quello di avvolgere la chiamata della funzione da azionare, per sistemare in modo appropriato la pila dei dati (listato 95.17.1). In questa fase della descrizione del problema, va osservato che la funzione _sighandler_wrapper() si trova nel codice del processo che riceve il segnale.
La chiamata di sistema, quando raggiunge il kernel, comporta l'aggiornamento dei dati del processo, annotando sia la funzione da azionare, sia la funzione che deve avvolgerla.
Quando arriva un segnale a un processo che prevede l'azionamento di una funzione, attraverso la funzione proc_sch_signals(), chiamata a sua volta dalla funzione proc_scheduler(), attraverso altri passaggi si arriva alla funzione proc_sig_handler() (listato 94.14.15).
void proc_sig_handler (pid_t pid, int sig); |
La funzione proc_sig_handler() ha lo scopo di modificare la pila dei dati del processo pid, in modo da far sì che, nel momento in cui fosse selezionato, prima di riprendere con l'attività sospesa originariamente, esegua la funzione attivata dal segnale sig.
Come si può vedere nella figura, i valori che servono all'istruzione IRET per concludere l'interruzione, vengono modificati in modo da ripartire iniziando con la funzione che avvolge quella da azionare, wrapper, ovvero quella che dal lato dell'applicazione è chiamata _sighandler_wrapper(). D'altro canto, quando quella funzione viene messa in azione, si trova nella pila dei valori che le servono per poter chiamare a sua volta la funzione da azionare effettivamente.
Il codice di _sighandler_wrapper() non corrisponde propriamente a una funzione, in quanto ciò che si trova nella pila non è quello che si prevede di solito. Le figure successive mostrano i cambiamenti della pila del processo, prima e dopo l'esecuzione della funzione incaricata di gestire il segnale ricevuto.
Lo scopo della funzione _sighandler_wrapper() è quello di garantire che sia preservato completamente l'ambiente di lavoro del processo nel momento dell'interruzione, perché non è possibile fare affidamento sul rispetto delle convenzioni di chiamata, dato che la funzione da azionare in corrispondenza dell'interruzione, viene iniettata in una posizione arbitraria del codice.
|
Il linguaggi C prevede la disponibilità di due funzioni, attraverso le quali è possibile salvare il contesto della pila dei dati per poterne recuperare lo stato in un momento successivo. Tuttavia, tale recupero può avvenire solo se dopo il salvataggio il contenuto della pila precedente rimane valido, in quanto il recupero avviene solo in una situazione in cui la pila sia stata incrementata ulteriormente.
Il salvataggio si ottiene con la funzione setjmp() e il recupero con longjmp(). L'effetto della chiamata della funzione longjmp() comporta il riportare il processo alla situazione in cui si trovava dopo l'esecuzione della funzione setjmp(), con la differenza che nel secondo caso, la funzione setjmp() restituisce un valore differente.
Si tratta di un modo pessimo di programmare, tuttavia fa parte dello standard del linguaggio C.
Generalmente, la realizzazione delle funzioni setjmp() e longjmp() avviene nella libreria, senza coinvolgere il kernel in alcun modo. Ma os32 procede diversamente e si avvale invece di chiamate di sistema. Si tratta comunque di una scelta motivata esclusivamente da una più semplice comprensione del codice, facendo rientrare il meccanismo in quello più generale della gestione dei processi.
La funzione setjmp() è realizzata dal file lib/setjmp/setjmp.s
(listato 95.16.2). La funzione svolge sostanzialmente il compito che si può vedere tradotto in linguaggio C nel codice seguente, se il compilatore gestisse la pila dei dati nella forma più compatta e prevedibile:
|
La funzione longjmp() è realizzata invece in C, nel file lib/setjmp/longjmp.c
(listato 95.16.1), perché non c'è la necessità di conoscere esattamente la struttura della sua pila.
La struttura corrispondente al tipo sysmsg_jmp_t si limita a due campi: un puntatore che deve fare riferimento alla memoria in cui viene salvato il contenuto della pila e il valore che deve restituire setjmp() quando rivive attraverso la chiamata di longjmp().
|
Le due chiamate di sistema raggiungono, rispettivamente, le funzioni s_setjmp() e s_longjmp() del kernel (listati 94.8.38 e 94.8.22). La funzione s_setjmp() salva lo stato della pila, a partire dalla chiamata della funzione setjmp(), mentre s_longjmp() lo ripristina, rimettendo anche l'indice della pila allo stato che aveva al momento della chiamata di setjmp().
La funzione setjmp() prevede un argomento di tipo jmp_buf che lo standard prescrive sia come un array:
int setjmp (jmp_buf env); |
In pratica, l'array serve solo a occupare lo spazio necessario a rappresentare il tipo jmp_env_t, i cui membri si vedono rappresentati nella figura già apparsa. La funzione s_setjmp() si occupa si salvare lo stato della pila, dal punto «A» al punto «B» della figura, all'interno di env, secondo la struttura di jmp_env_t, mettendo, oltre al contenuto della pila, il valore del suo indice attuale.
La funzione longjmp() deve portare al ripristino della pila, in una posizione antecedente rispetto a quella attuale.
void longjmp (jmp_buf env, int val); |
Caricare un programma e metterlo in esecuzione è un processo delicato che parte dalla funzione execve() della libreria standard e viene svolto dalla funzione proc_sys_exec() del kernel.
La funzione proc_sys_exec() (listato 94.14.22) del kernel è quella che svolge il compito di caricare un processo in memoria e di annotarlo nella tabella dei processi.
La funzione, dopo aver verificato che si tratti di un file eseguibile valido e che ci siano i permessi per metterlo in funzione, procede all'allocazione della memoria, dividendo se necessario l'area codice da quella dei dati, quindi legge il file e copia opportunamente le componenti di questo nelle aree di memoria allocate.
La realizzazione attuale della funzione proc_sys_exec() non è in grado di verificare se un processo uguale sia già in memoria, quindi carica la parte del codice anche se questa potrebbe essere già disponibile. |
Terminato il caricamento del file viene aggiornata la tabella GDT e quindi viene ricostruita in memoria la pila dei dati del processo. Prima si mettono sul fondo le stringhe delle variabili di ambiente e quelle degli argomenti della chiamata, quindi si aggiungono i puntatori alle stringhe delle variabili di ambiente, ricostruendo così l'array noto convenzionalmente come envp[], continuando con l'aggiunta dei puntatori alle stringhe degli argomenti della chiamata, per riprodurre l'array argv[]. Per ricostruire gli argomenti della chiamata della funzione main() dell'applicazione, vanno però aggiunti ancora: il puntatore all'inizio dell'array delle stringhe che descrivono le variabili di ambiente, il puntatore all'array delle stringhe che descrivono gli argomenti della chiamata e il valore che rappresenta la quantità di argomenti della chiamata.
Fatto ciò, vanno aggiunti tutti i valori necessari allo scambio dei processi, costituiti dai vari registri da rimpiazzare.
Superato il problema della ricostruzione della pila dei dati, la funzione proc_sys_exec() predispone i descrittori di standard input, standard output e standard error, quindi libera la memoria usata dal processo chiamante e ne rimpiazza i dati nella tabella dei processi con quelli del nuovo processo caricato.
I programmi iniziano con il codice che si trova nel file applic/crt0.mer.s
, oppure applic/crt0.sep.s
, a seconda che si compilino in modo da avere codice e dati nello stesso segmento, oppure in segmenti di memoria differenti. Questo file è abbastanza diverso da kernel/main/crt0.s
del kernel; in particolare va osservato che, a differenza del kernel, il codice delle applicazioni viene eseguito in un momento in cui l'indice della pila è già collocato correttamente; inoltre, se la funzione main() delle applicazioni termina e restituisce il controllo a crt0.*.s
, un ciclo senza fine esegue continuamente una chiamata di sistema per la conclusione del processo elaborativo corrispondente.
|
La figura mostra il confronto tra il codice iniziale contenuto nel file applic/crt0.*.s
, senza preamboli e senza commenti, con la dichiarazione del tipo derivato header_t, presente nel file kernel/proc.h
(nel codice si può notare la differenza tra crt0.mer.s
e crt0.sep.s
, relativa al valore assegnato alla variabile doffset). Attraverso questa struttura, la funzione proc_sys_exec() è in grado di estrapolare dal file le informazioni necessarie a caricarlo correttamente in memoria.
Come già accennato, quando viene eseguito il codice di un programma applicativo, la pila dei dati è già operativa. Pertanto, dopo il simbolo startup_code si può già lavorare con questa.
|
Per prima cosa, viene estratto dalla pila il puntatore all'array noto come envp[], per poter assegnare tale valore alla variabile environ, come richiede lo standard della libreria POSIX. Tuttavia, per poter gestire poi le variabili di ambiente, si rende necessario utilizzare un array più «comodo», quando le stringhe vanno sostituite. A tale proposito, nel file lib/stdlib/environment.c
, si dichiarano _environment_table[][] e _environment[]. Il primo è semplicemente un array di caratteri, dove, utilizzando due indici di accesso, si conviene di allocare delle stringhe, con una dimensione massima prestabilita. Il secondo, invece, è un array di puntatori, per localizzare l'inizio delle stringhe contenute nel primo. In pratica, alla fine _environment[] e environ[] devono essere equivalenti. Ma per attuare questo, occorre utilizzare la funzione _environment_setup() che sistema tutti i puntatori necessari.
|
Come si vede dall'estratto del file applic/crt0.*.s
, si vede l'uso della funzione _environment_setup() (il registro ECX contiene già il puntatore a envp[], e viene inserito nella pila proprio come argomento per la funzione). Successivamente viene riassegnata anche la variabile environ in modo da coincidere con _environment. Alla fine, viene ricostruita la pila per gli argomenti della chiamata della funzione main(), ma prima di procedere con quella chiamata, si utilizzano delle funzioni, per inizializzare la gestione dei flussi di file e delle directory, sempre in forma di flussi, e per predisporre la tabella delle funzioni da eseguire alla conclusione del processo.
|
La funzione _stdio_stream_setup(), contenuta nel file lib/stdio/FILE.c
, associa i descrittori standard ai flussi di file standard (standard input, standard output e standard error); la funzione _dirent_directory_stream_setup(), contenuta nel file lib/dirent/DIR.c
, compie un lavoro analogo, limitandosi però a inizializzare un array di flussi di directory; la funzione _atexit_setup(), contenuta nel file lib/stdlib/atexit.c
azzera l'array _atexit_table[], destinato a contenere l'elenco di funzioni da eseguire alla conclusione del processo.
Dopo queste preparazioni, viene chiamata la funzione main(), la quale riceve regolarmente i propri argomenti previsti. Il valore restituito dalla funzione viene poi salvato in corrispondenza del simbolo exit_value.
|
All'uscita dalla funzione main(), dopo aver salvato quanto restituito dalla funzione stessa, ci si introduce nel codice successivo al simbolo halt, nel quale si chiama la funzione sys() (chiamata di sistema), per produrre la chiusura formale del processo. Ciò che si vede è comunque l'equivalente di _exit (exit_value);.
Dal punto di vista del kernel di os32, l'allocazione della memoria riguarda la collocazione dei processi elaborativi nella stessa. Per semplicità si utilizza una mappa di bit per indicare lo stato dei blocchi di memoria, dove un bit a uno indica un blocco di memoria occupato.
Nel file memory.h
viene definita la dimensione di un blocco di memoria e, di conseguenza, la quantità massima che possa essere gestita. Attualmente i blocchi sono da 4 096 byte, pertanto, sapendo che la memoria può arrivare solo fino a 4 Gibyte, si gestiscono al massimo 1 048 576 blocchi.
Per la scansione della mappa si utilizzano interi da 32 bit, pertanto tutta la mappa si riduce a 32 768 di questi interi, ovvero 128 Kibyte. Nell'ambito di ogni intero da 32 bit, il bit più significativo rappresenta il primo blocco di memoria di sua competenza. Per esempio, per indicare che si stanno utilizzando i primi 28 672 byte, pari ai primi 7 blocchi di memoria, si rappresenta la mappa della memoria come «FE0000000...».
Il fatto che la mappa della memoria vada scandito a ranghi di 32 bit va tenuto in considerazione, perché se invece si andasse con ranghi differenti, si incapperebbe nel problema dell'inversione dei byte.
Listato 94.10 e successivi.
Il file kernel/memory.h
, oltre ai prototipi delle funzioni usate per la gestione della memoria, definisce la dimensione del blocco minimo di memoria e la quantità massima di questi, rispettivamente con le macro-variabili MEM_BLOCK_SIZE e MEM_MAX_BLOCKS; inoltre predispone il tipo derivato addr_t, corrispondente a un indirizzo di memoria reale.
Nei file della directory kernel/memory/
viene dichiarata la mappa della memoria, corrispondente a un array di interi a 32 bit, denominato mb_table[]. L'array è pubblico, tuttavia è disponibile anche una funzione che ne restituisce il puntatore: mb_reference(). Tale funzione sarebbe perfettamente inutile, ma rimane per uniformità rispetto alla gestione delle altre tabelle.
|
Listato 94.10 e successivi.
La mappa della memoria si rappresenta (a sua volta in memoria), con un array di interi a 32 bit, dove ogni bit individua un blocco di memoria. Pertanto, l'array si compone di una quantità di elementi pari al valore di MEM_MAX_BLOCKS diviso 32.
Il primo elemento di questo array, ovvero mb_table[0], individua i primi 32 blocchi di memoria, dove il bit più significativo si riferisce precisamente al primo blocco. Per esempio, se mb_table[0] contiene il valore F800000016, ovvero 11111000000000002, significa che i primi cinque blocchi di memoria sono occupati, mentre i blocchi dal sesto al trentaduesimo sono liberi.
Dal momento che i calcoli per individuare i blocchi di memoria e per intervenire nella mappa relativa, possono creare confusione, queste operazioni sono raccolte in funzioni statiche separate, anche se sono utili esclusivamente all'interno del file in cui si trovano. Tali funzioni statiche hanno una sintassi comune:
int mb_block_set1 (int block) int mb_block_set0 (int block) int mb_block_status (int block) |
Le funzioni mb_block_set1() e mb_block_set0() servono rispettivamente a impegnare o liberare un certo blocco di memoria, individuato dal valore dell'argomento. La funzione mb_block_status() restituisce uno nel caso il blocco indicato risulti allocato, oppure zero in caso contrario.
Queste tre funzioni usano un metodo comune per scandire la mappa della memoria: il valore che rappresenta il blocco a cui si vuole fare riferimento, viene diviso per 32, ovvero il rango degli elementi dell'array che rappresenta la mappa della memoria. Il risultato intero della divisione serve per trovare quale elemento dell'array considerare, mentre il resto della divisione serve per determinare quale bit dell'elemento trovato rappresenta il blocco desiderato. Trovato ciò, si deve costruire una maschera, nella quale si mette a uno il bit che rappresenta il blocco; per farlo, si pone inizialmente a uno il bit più significativo della maschera, quindi lo si fa scorrere verso destra di un valore pari al resto della divisione.
Per esempio, volendo individuare il terzo blocco di memoria, pari al numero 2 (il primo blocco corrisponderebbe allo zero), si avrebbe che questo è descritto dal primo elemento dell'array (in quanto 2/32 dà zero, come risultato intero), mentre la maschera necessaria a trovare il bit corrispondente è 001000000000000000000000000000002, la quale si ottiene spostando per due volte verso destra il bit più significativo (due volte, pari al resto della divisione).
Una volta determinata la maschera, per segnare come occupato un blocco di memoria, basta utilizzare l'operatore OR binario:
|
Se invece si vuole liberare un blocco di memoria, si utilizza un AND binario, invertendo però il contenuto della maschera:
|
Va osservato che la rappresentazione dei blocchi nella mappa è invertita rispetto ad altri sistemi operativi, in quanto non sarebbe tanto logico il fatto che il bit più significativo si riferisca invece alla parte più bassa del proprio insieme di blocchi di memoria. La scelta è dovuta al fatto che, volendo rappresentare la mappa numericamente, la lettura di questa sarebbe più vicina a quella che è la percezione umana del problema.
La gestione dei dispositivi fisici, da parte di os32, è limitata ed essenziale. Tutte le operazioni di lettura e scrittura di dispositivi, passano attraverso la gestione comune della funzione dev_io().
Nel file lib/sys/os32.h
(listato 95.21), disponibile sia al kernel, sia alle applicazioni, sono elencate le macro-variabili che descrivono tutti i dispositivi previsti in forma numerica. Queste macro-variabili hanno nomi prefissati dalla sigla DEV_.... Per esempio, DEV_DM_MAJOR corrisponde al numero primario (major) per le unità di memorizzazione di massa, DEV_DM00 corrisponde al numero primario e secondario (major e minor), in un valore unico, della prima unità di memorizzazione di massa complessiva, mentre DEV_DM01 corrisponde alla prima partizione della stessa.
Listati 94.3 e successivi.
Il file kernel/dev.h
incorpora il file lib/sys/os32/os32.h
, per acquisire le macro-variabili della gestione dei dispositivi che sono disponibili anche agli applicativi. Successivamente dichiara la funzione dev_io(), la quale sintetizza tutta la gestione dei dispositivi. Questa funzione utilizza il parametro rw, per specificare l'azione da svolgere (lettura o scrittura). Per questo parametro vanno usate le macro-variabili DEV_READ e DEV_WRITE, così da non dover ricordare quale valore numerico corrisponde alla lettura e quale alla scrittura.
ssize_t dev_io (pid_t pid, dev_t device, int rw, off_t offset, void *buffer, size_t size, int *eof); |
Sono comunque descritte anche altre funzioni, ma utilizzate esclusivamente da dev_io().
La funzione dev_io() si limita a estrapolare il numero primario dal numero del dispositivo complessivo, quindi lo confronta con i vari tipi gestibili. A seconda del numero primario seleziona una funzione appropriata per la gestione di quel tipo di dispositivo, passando praticamente gli stessi argomenti già ricevuti.
Va osservato il caso particolare dei dispositivi DEV_KMEM_.... In un sistema operativo Unix comune, attraverso ciò che fa capo al file di dispositivo /dev/kmem
, si ha la possibilità di accedere all'immagine in memoria del kernel, lasciando a un programma con privilegi adeguati la facoltà di interpretare i simboli che consentono di individuare i dati esistenti. Nel caso di os32, non ci sono simboli nel risultato della compilazione, quindi non è possibile ricostruire la collocazione dei dati. Per questa ragione, le informazioni che devono essere pubblicate, vengono controllate attraverso un dispositivo specifico. Quindi, il dispositivo DEV_KMEM_PS consente di leggere la tabella dei processi, DEV_KMEM_MMAP consente di leggere la mappa della memoria, e così vale anche per altre tabelle.
Per quanto riguarda la gestione dei terminali, attraverso la funzione dev_tty(), quando un processo vuole leggere dal terminale, ma non risulta disponibile un carattere, questo viene messo in pausa, in attesa di un evento legato ai terminali.
os32 gestisce virtualmente tutti i dispositivi come se fossero a caratteri. Tuttavia, nel caso delle unità di memorizzazione di massa il flusso di caratteri, in lettura o in scrittura, viene scomposto in blocchi, sfruttando anche una memoria (cache) per questi. Pertanto, la funzione dev_dm() si avvale di blk_ata().
Listati 94.2 e successivi.
I file contenuti nella directory kernel/blk/
riguardano specificatamente la gestione della memoria cache per i blocchi di dati usati più di frequente, relativamente ai dispositivi di memorizzazione. In pratica, tale gestione riguarda esclusivamente le unità PATA.
La tabella blk_table() è composta da elementi blk_cache_t, ognuno dei quali rappresenta un blocco singolo, con l'indicazione del dispositivo (dell'unità intera e non di una singola partizione) e del numero di blocco a cui si riferisce, assieme a un numero che ne rappresenta l'«età».
Inizialmente, la funzione blk_cache_init(), usata una volta sola all'interno di kmain(), si azzerano le informazioni sul numero di dispositivo e sul numero del blocco di ogni elemento della tabella, quindi si assegna l'età attraverso un numero progressivo, da 0 a BLK_CACHE_MAX_AGE. Il numero più basso rappresenta l'ultimo blocco letto o modificato, mentre quello più alto riguarda il blocco che da più tempo non è stato utilizzato.
Quando la funzione blk_ata() deve leggere un blocco da un'unità PATA, prima, attraverso la funzione blk_cache_read(), controlla all'interno della tabella blk_table() esiste già una copia del blocco; questo viene trovato, la funzione blk_cache_read() ne azzera l'età, incrementando conseguentemente l'età dei blocchi che avevano prima un valore inferiore al suo. Se il blocco viene trovato nella tabella, la funzione non interpella l'hardware PATA e conclude il suo lavoro, altrimenti provvede alla lettura necessaria e al suo salvataggio nella tabella dei blocchi, con l'aiuto di blk_cache_save(), la quale aggiorna il blocco se questo era già presente nella tabella, oppure rimpiazza il blocco di età maggiore, aggiornando di conseguenza l'età, come nel caso della lettura.
Quando la funzione blk_ata() deve scrivere un blocco, la scrittura hardware avviene in ogni caso, seguita dal salvataggio nella tabella dei blocchi.
In pratica, la memoria cache viene usata solo per le letture, pertanto tutte le scritture sono sincrone.
I dispositivi, secondo la tradizione dei sistemi Unix, sono rappresentati dal punto di vista logico attraverso un numero intero, senza segno, a 16 bit. Tuttavia, per organizzare questa numerazione in modo ordinato, tale numero viene diviso in due parti: la prima parte, nota come major, ovvero «numero primario», si utilizza per individuare il tipo di dispositivo; la seconda, nota come minor, ovvero «numero secondario», si utilizza per individuare precisamente il dispositivo, nell'ambito del tipo a cui appartiene.
In pratica, il numero complessivo a 16 bit si divide in due, dove gli 8 bit più significativi individuano il numero primario, mentre quelli meno significativi danno il numero secondario. L'esempio seguente si riferisce al dispositivo che genera il valore zero, il quale appartiene al gruppo dei dispositivi relativi alla memoria:
|
In questo caso, il valore che rappresenta complessivamente il dispositivo è 010416 (pari a 26010), ma si compone di numero primario 0116 e di numero secondario 0416 (che coincidono nella rappresentazione in base dieci). Per estrarre il numero primario si deve dividere il numero complessivo per 256 (010016), trattenendo soltanto il risultato intero; per filtrare il numero secondario si può fare la stessa divisione, ma trattenendo soltanto il resto della stessa. Al contrario, per produrre il numero del dispositivo, partendo dai numeri primario e secondario separati, occorre moltiplicare il numero primario per 256, sommando poi il risultato al numero secondario.
L'astrazione della gestione dei dispositivi, consente di trattare tutti i componenti che hanno a che fare con ingresso e uscita di dati, in modo sostanzialmente omogeneo; tuttavia, le caratteristiche effettive di tali componenti può comportare delle limitazioni o delle peculiarità. Ci sono alcune questioni fondamentali da considerare: un tipo di dispositivo potrebbe consentire l'accesso in un solo verso (lettura o scrittura); l'accesso al dispositivo potrebbe essere ammesso solo in modo sequenziale, rendendo inutile l'indicazione di un indirizzo; la dimensione dell'informazione da trasferire potrebbe assumere un significato differente rispetto a quello comune.
|
Listato 94.4.42 e successivi.
Il terminale offre solo la funzionalità elementare della modalità canonica, dove è possibile scrivere o leggere sequenzialmente. Ci sono al massimo quattro terminali virtuali, selezionabili attraverso le combinazioni di tasti [Ctrl q], [Ctrl r], [Ctrl s] e [Ctrl t] e non è possibile controllare i colori o la posizione del testo che si va a esporre; in pratica si opera come su una telescrivente. Le funzioni di livello più basso, relative al terminale hanno nomi che iniziano per tty_...().
Per la gestione dei quattro terminali virtuali, si utilizza una tabella, in cui ogni voce rappresenta lo stato del terminale virtuale che rappresenta. La tabella è costituita dall'array tty_table[] che contiene TTY_TOTALS elementi. L'array è dichiarato nel file kernel/driver/tty_public.c
, mentre la macro-variabile TTY_TOTALS appare nel file kernel/driver/tty.h
. Gli elementi di tty_table[] sono di tipo tty_t:
|
Il membro attr della voce di un terminale è una variabile strutturata di tipo struct termios, come previsto nel file termios.h
della libreria standard.
L'input del terminale, proveniente dalla tastiera, viene depositato dalla funzione proc_sch_terminals() all'interno del membro line[], annotando in lpw l'indice di scrittura. Quando si legge dal terminale, si ottiene un carattere alla volta da line[], con l'ausilio dell'indice lpr. Quando il terminale virtuale riceve input dalla tastiera, è nello stato definito dalla macro-variabile TTY_INPUT_LINE_EDITING, mentre quando l'inserimento risulta concluso, per esempio perché è stato premuto il tasto [Invio], lo stato è quello di TTY_INPUT_LINE_CLOSED ed è possibile procedere con la lettura del contenuto di line[]: quando la lettura termina perché l'indice lpr ha raggiunto lpw, gli indici vengono azzerati e lo stato ritorna quello di inserimento. Quando un processo tenta di leggere dal terminale, mentre questo è in fase di inserimento, non ancora concluso, viene sospeso e rimane così fino alla conclusione dell'inserimento stesso.
|
Listato 94.4.15 e successivi.
Per la gestione della tastiera, nel file kernel/driver/kbd_public.c
viene dichiarata una variabile strutturata, di tipo kbd_t, contenente le informazioni sullo stato della stessa e sulla mappa di trasformazione da applicare. A differenza della gestione complessiva dei terminali, in cui ogni terminale virtuale ha un proprio insieme di dati, per la tastiera questo è unico.
|
Nella variabile kbd, come si intuisce dai nomi dei suoi membri, viene annotato lo stato di pressione dei tasti delle maiuscole, dei tasti [Ctrl] e [Alt], per poter recepire eventuali combinazioni di tasti; inoltre, il membro echo, se attivo, indica la richiesta di vedere sullo schermo ciò che si digita.
Dalla tastiera viene recepito un solo tasto alla volta: se questo si traduce in un carattere, stampabile o meno che sia, questo viene depositato nel membro key, da dove la funzione proc_sch_terminals() deve provvedere a prelevarlo (per trasferirlo nel membro line[] della voce che descrive il terminale virtuale attivo), azzerando nuovamente key. Fino a quando il membro key ha un valore diverso da zero, non è possibile recepire altro dalla tastiera.
Dalla tastiera è possibile ottenere solo i caratteri ASCII; in particolare, quelli non stampabili si ottengono per combinazione con il tasto [Ctrl], secondo la convenzione tradizionale. Non sono previste altre funzionalità.
|
La funzione proc_scheduler(), il cui scopo principale è quello di alternare i processi in esecuzione, tra le altre cose, avvia ogni volta la funzione proc_scheduler_terminals(). La funzione proc_scheduler_terminals() verifica se nella variabile kbd.key è disponibile un valore diverso da zero e, se c'è, lo acquisisce per conto del terminale attivo. Prima di tutto verifica se si tratta di una combinazione di tasti che richiede lo scambio a un altro terminale virtuale; poi controlla se si tratta di un codice di interruzione (come quello provocato da [Ctrl c]) e, se la configurazione del terminale attivo lo permette, conclude il processo più interno appartenente al gruppo che risulta connesso al terminale stesso; alla fine, dopo altre ipotesi particolari, se si tratta di un carattere «normale» e il terminale si trova in fase di inserimento (TTY_INPUT_LINE_EDITING), questo viene depositato nell'array line[], con il conseguente aggiornamento dell'indice di scrittura al suo interno; ricevendo invece un codice che rappresenta la conclusione dell'inserimento, si rimette il terminale nello stato di conclusione dell'inserimento (TTY_INPUT_LINE_CLOSED).
Listato 94.4.30 e successivi.
Lo schermo di os32 viene gestito secondo quanto prescrive l'hardware VGA (come descritto nella sezione 83.3), per cui ciò che si vuole fare apparire deve essere scritto in memoria a partire dall'indirizzo B800016, usando per ogni carattere 16 bit (8 bit di questo gruppo servono per gli attributi).
Dal momento che si gestiscono dei terminali virtuali, per ognuno di questi occorre tenere una copia dell'immagine dello schermo, così, quando si seleziona un terminale differente, la copia di quel terminale viene usata per sovrascrivere l'area di memoria che rappresenta lo schermo. Per la gestione degli schermi virtuali si usa una tabella, denominata screen_table[], composta da voci di tipo screen_t.
|
All'interno della struttura rappresentata dal tipo screen_t, si vede un array che riproduce la rappresentazione in memoria dello stesso, da copiare a partire dall'indirizzo B800016, quando lo schermo virtuale diventa quello attivo; inoltre si vede il membro position, usato per ricordare la posizione in cui si trova il cursore.
|
Lo standard dei sistemi Unix prescrive che per ogni terminale gestito sia prevista una variabile strutturata, di tipo struct termios, allo scopo di contenere la configurazione dello stesso. os32 gestisce i terminali virtuali soltanto in modalità «canonica», ovvero come se si trattasse di telescriventi, anche se munite di video invece che di carta, pertanto utilizza solo un sottoinsieme delle opzioni previste.
|
Il membro c_cc[] è un array di caratteri di controllo, a cui viene attribuita una definizione.
|
Il membro c_iflag serve a contenere opzioni sull'inserimento, ovvero sul controllo della digitazione.
|
Il membro c_lflag serve a contenere delle opzioni definite come «locali», le quali si occupano in pratica di controllare la visualizzazione della digitazione introdotta e di decidere se l'interruzione ricevuta da tastiera debba produrre l'invio di un segnale di interruzione al processo con cui si sta interagendo. Gli altri due membri della struttura non vengono utilizzati da os32.
|
Listato 94.4 e successivi.
Le unità di memorizzazione vengono viste da os32 attraverso un gruppo di dispositivi astratti, definiti come DEV_DM*, dove la prima unità PATA disponibile ottiene il numero DEV_DM00, la seconda DEV_DM10,...
Il numero del dispositivo che rappresenta queste unità è composto in modo solito per ciò che riguarda la distinzione tra numero primario e numero secondario, ma il numero secondario si scompone ulteriormente in due parti: l'unità intera e la partizione. Per esempio, il numero 081016 individua la seconda unità di memorizzazione per intero, mentre 081116 rappresenta la prima partizione della seconda unità.
Le unità di memorizzazione riconosciute dal sistema sono raccolte in una tabella, denominata dm_table[]. Ogni elemento di questa tabella contiene l'informazione sul tipo di unità, un puntatore per raggiungere un'altra tabella con le informazioni specifiche sull'unità, in base al tipo di questa (attualmente l'unica tabella in questione può essere quella delle unità PATA), le informazioni sulle partizioni esistenti (ma solo quelle primarie).
Inizialmente, all'interno di proc_init(), viene avviata la funzione dm_init(), la quale a sua volta scandisce le unità PATA attraverso ata_init() e ne raccoglie le informazioni nella propria tabella dm_table().
Listato 94.4.3 e successivi.
La gestione delle unità PATA di os32 si limita alla modalità PIO (programmed input-output), con accesso LBA28, senza nemmeno considerare le partizioni. La spiegazione sul come avvenga la gestione di un'unità PATA, secondo le stesse modalità usate da os32 è disponibile nella sezione 83.9.
Per la gestione delle unità PATA, os32 utilizza una tabella, denominata ata_table[], composta da voci di tipo ata_t, ognuna delle quali contiene lo stato di un'unità. L'indice della tabella corrisponde al numero dell'unità, ovvero al parametro drive di varie funzioni. La tabella è dichiarata formalmente nel file kernel/driver/ata/ata_public.c
, mentre il tipo derivato ata_t è descritto nel file kernel/driver/ata.h
. Per comodità, si trova anche il tipo ata_sector_t, usato per descrivere lo spazio di memoria usato per collocare la copia di un settore di dati di un'unità PATA.
|
La gestione del file system è suddivisa in diversi file contenuti nella directory kernel/fs/
, facenti capo al file di intestazione kernel/fs.h
.
Listato 94.5 e successivi.
I file kernel/fs/sb_...
descrivono le funzioni per la gestione dei super blocchi, distinguibili perché iniziano tutte con il prefisso sb_. Tra questi file si dichiara l'array sb_table[], il quale rappresenta una tabella le cui righe sono rappresentate da elementi di tipo sb_t (il tipo sb_t è definito nel file kernel/fs.h
). Per uniformare l'accesso alla tabella, la funzione sb_reference() permette di ottenere il puntatore a un elemento dell'array sb_table[], specificando il numero del dispositivo cercato.
|
Il super blocco rappresentato dal tipo sb_t include anche le mappe delle zone e degli inode impegnati. Queste mappe hanno una dimensione fissa in memoria, mentre nel file system reale possono essere di dimensione minore. La tabella di super blocchi, contiene le informazioni dei dispositivi di memorizzazione innestati nel sistema. L'innesto si concretizza nel riferimento a un inode, contenuto nella tabella degli inode (descritta in un altro capitolo), il quale rappresenta la directory di un'altra unità, su cui tale innesto è avvenuto. Naturalmente, l'innesto del file system principale rappresenta un caso particolare.
|
Nel file system Minix 1, si distinguono i concetti di blocco e zona di dati, con il vincolo che la zona ha una dimensione multipla del blocco. Il contenuto del file system, dopo tutte le informazioni amministrative, è organizzato in zone; in altri termini, i blocchi di dati si raggiungono in qualità di zone.
La zona rimane comunque un tipo di blocco, potenzialmente più grande (ma sempre multiplo) del blocco vero e proprio, che si numera a partire dall'inizio dello spazio disponibile, con la differenza che è utile solo per raggiungere i blocchi di dati. Nel super blocco del file system si trova l'informazione del numero della prima zona che contiene dati, in modo da non dover ricalcolare questa informazione ogni volta.
I file kernel/fs/zone_...
descrivono le funzioni per la gestione del file system a zone.
|
I file kernel/fs/inode_...
descrivono le funzioni per la gestione dei file, in forma di inode. In uno di questi file viene dichiarata la tabella degli inode in uso nel sistema, rappresentata dall'array inode_table[] e per individuare un certo elemento dell'array si usa preferibilmente la funzione inode_reference(). Gli elementi della tabella degli inode sono di tipo inode_t (definito nel file kernel/fs.h
); una voce della tabella rappresenta un inode utilizzato se il campo dei riferimenti (references) ha un valore maggiore di zero.
|
|
L'innesto e il distacco di un file system, coinvolge simultaneamente la tabella dei super blocchi e quella degli inode. Si distinguono due situazioni fondamentali: l'innesto del file system principale e quello di un file system ulteriore.
Quando si tratta dell'innesto del file system principale, la tabella dei super blocchi è priva di voci e quella degli inode non contiene riferimenti a file system. La funzione sb_mount() viene chiamata indicando, come riferimento all'inode di innesto, il puntatore a una variabile puntatore contenente il valore nullo:
|
La funzione sb_mount() carica il super blocco nella tabella relativa, ma trovando il riferimento all'inode di innesto nullo, provvede a caricare l'inode della directory radice dello stesso dispositivo, creando un collegamento incrociato tra le tabelle dei super blocchi e degli inode, come si vede nella figura successiva.
Per innestare un altro file system, occorre prima disporre dell'inode di una directory (appropriata) nella tabella degli inode, quindi si può caricare il super blocco del nuovo file system, creando il collegamento tra directory e file system innestato.
I condotti sono gestiti da os32 nel modo tradizionale, sfruttando nell'inode la poca memoria che altrimenti servirebbe per i riferimenti ai blocchi di dati. In pratica, secondo la struttura del tipo inode_t, si usa direct[], indirect1 e indirect2. Ciò comporta complessivamente la disponibilità di soli 18 byte, cosa che comunque sarebbe insufficiente per lo standard attuale dei sistemi Unix.
Lo spazio di questi 18 byte viene trattato come una coda e scandito attraverso due indici: quello di scrittura e quello di lettura. Durante l'accesso all'inode che rappresenta un condotto si distinguono due stati, individuati dal bit pipe_dir.
La figura mostra un esempio che dovrebbe chiarire il meccanismo di funzionamento del condotto.
Inizialmente gli indici di scrittura e lettura si trovano ad avere lo stesso valore (nella figura si trovano nella posizione zero, ma qualunque altra posizione sarebbe equivalente), mentre il bit pipe_dir indica «scrittura». In questa situazione, si deve procedere con la scrittura, durante la quale l'indice di scrittura non può superare nuovamente quello di lettura.
Nel secondo disegno della figura si vede che è avvenuta una scrittura che ha occupato 13 byte, mentre l'indice di scrittura si trova sul quattordicesimo (quello successivo all'ultimo byte scritto).
A questo punto, si suppone che inizi la lettura del condotto: in tal caso, dato che il bit pipe_dir indica ancora «scrittura», la lettura non può superare la posizione che ha raggiunto l'indice di scrittura. Pertanto si suppone che la lettura raccolga la stessa quantità di byte occupati precedentemente dalla scrittura. Quando l'indice di lettura incontra quello di scrittura, il bit pipe_dir deve essere impostato a «scrittura», perché non c'è altro che si possa leggere. Tale bit era già impostato nel modo corretto e quindi non si notano variazioni.
Nel quarto disegno si vede l'inizio di una nuova fase di scrittura che raggiunge la fine dello spazio dei 18 byte previsti per riprendere dall'inizio. In questa fase di scrittura gli indici non si incontrano e nulla cambia nello stato di pipe_dir.
Nel quinto disegno (all'inizio del lato destro), la scrittura riprende e raggiunge l'indice di lettura (che non può essere superato). Qui lo stato rappresentato da pipe_dir cambia, dal momento che non si può più procedere con la scrittura, adesso indica «lettura».
Nel sesto disegno si vede l'inizio di una lettura. Dopo di questa lettura, potrebbe esserci una fase di scrittura, ma senza poter superare l'indice di lettura. Comunque, questa scrittura non viene eseguita.
Nell'ultimo disegno si vede che la lettura continua, fino a raggiungere l'indice di scrittura, quando così il valore di pipe_dir viene invertito nuovamente.
In pratica, quando gli indici di lettura e scrittura coincidono, per sapere se si può procedere con una scrittura o una lettura, occorre chiederlo a pipe_dir; diversamente, con indici diversi, la scrittura o la lettura può procedere indifferentemente, ma solo fino al raggiungimento dell'altro indice. Poi, se è l'indice di lettura che ha appena raggiunto quello di scrittura, pipe_dir deve essere impostato per richiedere la scrittura; al contrario, quando è l'indice di scrittura che raggiunge quello di lettura, pipe_dir deve richiedere la lettura successiva.
I file kernel/fs/file_...
descrivono le funzioni per la gestione della tabella dei file, la quale si collega a sua volta a quella degli inode. In realtà, le funzioni di questo gruppo sono in numero molto limitato, perché l'intervento nella tabella dei file avviene prevalentemente per opera di funzioni che gestiscono i descrittori.
La tabella dei file è rappresentata dall'array file_table[] e per individuare un certo elemento dell'array si usa preferibilmente la funzione file_reference(). Gli elementi della tabella dei file sono di tipo file_t (definito nel file kernel/fs.h
); una voce della tabella rappresenta un file aperto se il campo dei riferimenti (references) ha un valore maggiore di zero.
Nel membro oflags si annotano esclusivamente opzioni relative alla modalità di apertura del file: lettura, scrittura o entrambe; pertanto si possono usare le macro-variabili O_RDONLY, O_WRONLY e O_RDWR, come dichiarato nel file di intestazione lib/fcntl.h
. Il membro offset rappresenta l'indice interno di accesso al file, per l'operazione successiva di lettura o scrittura al suo interno. Il membro references è un contatore dei riferimenti a questa tabella, da parte di descrittori di file.
La tabella dei file si collega a quella degli inode, attraverso il membro inode, oppure a quella dei socket, attraverso il membro sock. Più voci della tabella dei file possono riferirsi allo stesso inode (o allo stesso socket), perché hanno modalità di accesso differenti, oppure soltanto per poter distinguere l'indice interno di lettura e scrittura. Va osservato che le voci della tabella di inode potrebbero essere usate direttamente e non avere elementi corrispondenti nella tabella dei file.
|
Le tabelle di super blocchi, inode e file, riguardano il sistema nel complesso. Tuttavia, l'accesso normale ai file avviene attraverso il concetto di «descrittore», il quale è un file aperto da un certo processo elaborativo. Nel file kernel/fs.h
si trova la dichiarazione e descrizione del tipo derivato fd_t, usato per costruire una tabella di descrittori, ma tale tabella non fa parte della gestione del file system, bensì è incorporata nella tabella dei processi elaborativi. Pertanto, ogni processo ha una propria tabella di descrittori di file.
Il membro fl_flags consente di annotare indicatori del tipo O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC e O_APPEND, come dichiarato nella libreria standard, nel file di intestazione lib/fcntl.h
. Tali indicatori si combinano assieme con l'operatore binario OR. Altri tipi di opzione che sarebbero previsti nel file lib/fcntl.h
, sono privi di effetto nella gestione del file system di os16.
Il membro fd_flags serve a contenere, eventualmente, l'opzione FD_CLOEXEC, definita nel file lib/fcntl.h
. Non sono previste altre opzioni di questo tipo.
I file kernel/fs/path_...
descrivono le funzioni che fanno riferimento a file o directory attraverso una stringa che ne descrive il percorso.
|
|
Delle funzioni che, per affinità, farebbero parte di questo gruppo, si trovano nella directory kernel/lib_s/
, in quanto servono per attuare delle chiamate di sistema.
|
I file kernel/fs/fd_...
descrivono le funzioni che fanno riferimento a file o directory attraverso il numero di descrittore, riferito a sua volta a un certo processo elaborativo. Pertanto, il numero del processo e il numero del descrittore sono i primi due parametri obbligatori di tutte queste funzioni.
|
Delle funzioni che, per affinità, farebbero parte di questo gruppo, si trovano nella directory kernel/lib_s/
, in quanto servono per attuare delle chiamate di sistema.
|
Il sistema os32 può gestire soltanto interfacce di rete Ethernet e l'interfaccia virtuale locale, nota con il nome loopback. Le interfacce di rete hanno tutte nomi del tipo netn, dove n è un numero intero, a partire da zero, e di norma l'interfaccia virtuale locale coincide con il nome net0.
Il kernel di os32 è in grado di gestire soltanto le interfacce di rete Ethernet NE2000, collocate nel bus PCI: NE2K. Ciò consente di conoscere la porta di I/O necessaria per accedervi, in modo automatico. Le funzioni per la gestione di queste interfacce sono contenute nei file della directory kernel/driver/nic/ne2k/
e fanno capo al file di intestazione kernel/driver/nic/ne2k.h
(listato 94.4.19 e successivi).
Le interfacce di rete NE2000 dispongono di una piccola memoria tampone interna per la ricezione; tuttavia, appena viene individuato un pacchetto ricevuto, os32 lo trasferisce immediatamente in una propria memoria tampone, contenuta nella tabella delle interfacce, descritta nella sezione successiva.
Per una maggiore semplicità progettuale, la trasmissione di un pacchetto avviene mettendo tutto il sistema in attesa, fino a che l'interfaccia dà un responso, positivo o negativo che sia. Tuttavia, ciò comporta anche il rischio di bloccare definitivamente il sistema, nel caso si dovessero manifestare dei problemi all'interfaccia.
|
Nei file kernel/net/net_public.c
[94.12.31] e kernel/net.h
[94.12] viene dichiarata la tabella delle interfacce, corrispondente all'array net_table[], con lo scopo di contenere la memoria tampone delle trame ricevute da ogni interfaccia. La struttura della tabella è definita dal tipo net_t e appare semplificata nella figura successiva.
|
Ogni pacchetto accumulato nella memoria tampone della tabella delle interfacce, oltre al contenuto del pacchetto, include l'orario in cui questo è stato ricevuto (in unità clock_t) e la sua dimensione effettiva.
La scansione della tabella richiede generalmente due indici: il numero che individua l'interfaccia e il numero che rappresenta la trama memorizzata (PDU di livello 2 nel caso di interfaccia Ethernet, oppure di livello 3 nel caso di interfaccia virtuale locale), assieme a delle informazioni accessorie. Per esempio, net_table[0].loopback.buffer[f].clock individua l'orario di ricevimento di un pacchetto con indice f dell'interfaccia locale net0 (loopback), mentre net_table[1].ethernet.buffer[f].size individua la dimensione del pacchetto f dell'interfaccia Ethernet net1.
I pacchetti, a livello della rete fisica, vengono depositati nella memoria tampone della tabella, in corrispondenza dell'interfaccia da cui provengono; da qui, poi, attraverso la funzione net_rx(), i pacchetti vengono passati ai gestori appropriati, cancellandoli dalla tabella originaria.
|
Per mantenere memoria delle corrispondenze tra indirizzi IPv4 e indirizzi Ethernet, si utilizza la tabella ARP, descritta nel file kernel/net/arp.h
[94.12.1] e dichiarata nel file kernel/net/arp/arp_public.c
[94.12.6].
Le voci della tabella sono valide per un tempo limitato, definito dalla macro-variabile ARP_MAX_TIME e periodicamente vengono scandite e cancellate le voci troppo vecchie.
|
|
La gestione di IPv4, da parte di os32, è estremamente limitata, per semplificare il codice e la sua comprensione. In particolare non si considerano le opzioni che potrebbero essere contenute tra l'intestazione minima e il contenuto del pacchetto IPv4.
|
In varie situazioni si usa il tipo h_addr_t, il quale rappresenta un indirizzo IPv4, a 32 bit, espresso però secondo l'architettura dell'elaboratore (host byte order). Questo tipo derivato si contrappone a quello standard, denominato in_addr_t, il quale rappresenta lo stesso indirizzo, ma secondo l'ordinamento adatto alla trasmissione in rete (network byte order).
Quando un pacchetto viene ricevuto ed è riconosciuto dalla funzione net_rx() come riguardante IPv4, questa chiama la funzione ip_rx() che lo copia nella tabella ip_table[], dove rimane fino a quando viene rimpiazzato da un nuovo pacchetto, secondo il criterio per cui i pacchetti più vecchi lasciano il posto a quelli più recenti.
|
Il membro kernel_serviced contiene inizialmente il valore zero, per poi ottenere una copia dell'orario di arrivo del pacchetto, appena questo risulta essere stato considerato dal kernel, ai fini del protocollo ICMP (in quanto il protocollo ICMP viene gestito internamente). Quando il kernel trova un pacchetto che ha l'orario di arrivo uguale a quello di elaborazione, sa così che l'ha già preso in considerazione nella propria gestione interna e non deve farci altro.
Il membro pdu4 contiene un puntatore all'inizio del contenuto del pacchetto IPv4, ovvero a ciò che c'è nel pacchetto, dopo l'intestazione IPv4 e dopo le opzioni eventuali.
La ricezione di un pacchetto IPv4 avviene per opera della funzione ip_rx(), la quale viene avviata da net_rx(), quando si accorge di avere a che fare con un pacchetto di questo tipo.
La funzione ip_rx() riceve due argomenti, n e f, i quali rappresentano, rispettivamente, l'indice dell'interfaccia che ha ricevuto la trama e l'indice della trama stessa. Con questi indici, la funzione ip_rx() è in grado di estrapolare il pacchetto IPv4 dalla tabella net_table[].
Avendo individuato l'inizio del pacchetto IPv4, verifica l'integrità del contenuto dell'intestazione con il codice di controllo relativo: se la verifica ha successo, e se non si tratta di un frammento (in quanto os32 non gestisce pacchetti frammentati), il pacchetto viene accolto e copiato nella prima posizione disponibile della tabella ip_table[], annotando l'orario di arrivo.
Il pacchetto ricevuto in questo modo, dovrebbe risultare destinato a un'interfaccia del proprio sistema. Se però l'indirizzo IPv4 di destinazione non è abbinato ad alcuna interfaccia, viene trasmesso un pacchetto ICMP con il messaggio di destinazione irraggiungibile.
Se il pacchetto ricevuto risulta includere informazioni su porte UDP o TCP, viene verificato se nella tabella sock_table[] è prevista la ricezione nella porta che questo pacchetto dovrebbe raggiungere. Se non è così, viene trasmesso un pacchetto ICMP con il messaggio di porta non raggiungibile.
La tabella sock_table[] è dichiarata nel file |
Alla fine, se il pacchetto risulta essere di tipo ICMP, viene avviata la funzione icmp_rx() perché se ne occupi; diversamente viene semplicemente copiato l'orario di ricevimento del pacchetto nel campo che rappresenta l'elaborazione dello stesso a livello IP.
Nella directory kernel/net/route/
si trovano i file delle funzioni che consentono la gestione degli instradamenti, raccolte nel file di intestazione kernel/net/route.h
(listato 94.12.33 e successivi). Per la limitazione di os32, gli instradamenti servono in pratica solo per la trasmissione, in quanto non è previsto il funzionamento in qualità di router (quindi non si pone il problema di reindirizzare i pacchetti ricevuti).
|
Listato 94.12.10 e successivi.
Quando viene ricevuto un pacchetto IPv4 che contiene un messaggio ICMP, la funzione ip_rx() chiama la funzione icmp_rx() per il trattamento di questa informazione. La funzione icmp_rx() verifica il tipo di messaggio e si comporta di conseguenza: a una richiesta di eco risponde con la trasmissione di un pacchetto appropriato, attraverso la funzione icmp_tx_echo(); a un messaggio di destinazione irraggiungibile, comunica l'informazione nella tabella sock_table[], dopo aver trovato lì dentro la voce di una connessione con le caratteristiche appropriate.
|
Listati 94.5, 94.12.53 e successivi, 94.12.40 e successivi.
Per la gestione dei protocolli UDP e TCP, os32 definisce una tabella in cui ogni voce descrive una connessione. Si tratta della tabella sock_table[] le cui voci sono collegate dalla tabella file_table[]. In pratica, quando si utilizza la funzione socket() per creare una connessione UDP o TCP, si ottiene un descrittore di file che fa riferimento a una voce nella tabella sock_table[], invece che alla tabella degli inode.
La struttura degli elementi della tabella sock_table[] è molto articolata. In generale si distingue una prima parte, comune ai protocolli UDP e TCP, rispetto a una seconda specifica per il protocollo TCP.
|
|
Quando viene ricevuto un pacchetto IPv4 che contiene dati del protocollo UDP, dopo aver verificato che esiste effettivamente una porta UDP in attesa di ricevere nella tabella sock_table[], questo viene semplicemente lasciato nella tabella ip_table[], annotando soltanto che il kernel lo ha già preso in considerazione.
L'acquisizione effettiva del pacchetto UDP avviene attraverso la funzione s_recvfrom(), la quale costituisce la versione interna al kernel di recvfrom(). La funzione s_recvfrom(), chiamata per leggere da un socket UDP, cerca nella tabella ip_table[] un pacchetto corrispondente alle caratteristiche del socket, che non sia già stato preso in considerazione dal socket stesso (il membro read.clock[i], dove i corrisponde all'indice del pacchetto trovato nella tabella ip_table[], contiene l'orario di un pacchetto già letto in quella posizione: se l'orario del pacchetto contenuto nella tabella ip_table[] è più recente, allora deve essere letto). Se il pacchetto viene accettato, si aggiorna nel socket il valore del membro read.clock[i] con l'orario di ricevimento del pacchetto (per evitare di rileggerlo un'altra volta), quindi viene copiato il contenuto del pacchetto nella destinazione specificata dagli argomenti della funzione.
Va osservato che la lettura del pacchetto UDP, così come viene fatta da os32, si limita alla porzione specificata dalla dimensione massima della memoria tampone; se poi si esegue una nuova lettura, si cerca semplicemente un altro pacchetto, senza terminare eventualmente la lettura del precedente.
La trasmissione avviene attraverso la funzione s_send() (corrispondente a send() dal lato utente). Questa funzione, dopo aver determinato che si tratta di un socket UDP, si avvale a sua volta della funzione udp_tx() per costruire e spedire effettivamente il pacchetto. Come nel caso della ricezione, la trasmissione riguarda un solo pacchetto, e le informazioni eccedenti sono semplicemente eliminate.
La gestione del TCP è estremamente più complessa rispetto a UDP, in quanto richiede un proprio sistema di gestione delle connessioni. La funzione tcp() ha il compito di scandire la tabella dei pacchetti ip_table[] alla ricerca di quelli che riguardano il protocollo TCP e che non sono ancora stati considerati, aggiornando lo stato delle connessioni relative. La funzione tcp() è chiamata a ogni interruzione da proc_sch_net().
La funzione tcp(), quando individua nella tabella ip_table[] un pacchetto TCP ancora da prendere in considerazione, deve valutare le caratteristiche del pacchetto trovato in relazione allo stato della connessione eventualmente già in corso, agendo di conseguenza. Ciò significa che la funzione tcp() può trovarsi nella necessità di trasmettere a sua volta pacchetti TCP alla controparte, per il fine della gestione della connessione.
È importante osservare che gli indicatori sock_table[].tcp.can_read e sock_table[].tcp.can_write, necessari a controllare la lettura e la scrittura del socket con le funzioni s_recvfrom() e s_send(), sono aggiornati dalla funzione tcp().
Le funzioni s_recvfrom() e s_send(), se si trovano nell'impossibilità di leggere o scrivere il socket richiesto, in condizioni normali mettono il processo relativo in pausa, in attesa di un cambiamento. Inoltre, la funzione s_recvfrom() deve aggiornare l'indice di lettura interno al socket, in modo che la lettura successiva riprenda da quella posizione. In pratica, lettura e scrittura avvengono qui in modo analogo a quello di un file, in un flusso continuo di byte.
Il risveglio dei processi in attesa di leggere o scrivere un socket avviene per opera della funzione proc_sch_net() dopo aver avviato tcp() per aggiornare lo stato dei socket TCP, in base al fatto che sia stato ricevuto qualcosa o che ci sia motivo di ritenere che sia possibile scrivere attraverso un socket bloccato precedentemente in scrittura.
Esiste un grosso limite di os32, relativo alla gestione del TCP: la chiusura di una connessione elimina le informazioni relative al socket, mentre la controparte potrebbe non essere ancora pronta per recepire tale conclusione. Questa semplificazione serve a far sì che ci sia sempre corrispondenza tra il descrittore di file e il socket, mentre in un sistema reale, il socket deve poter continuare a esistere per un certo tempo, benché chiuso, anche dopo la chiusura del descrittore di file relativo. |
Va poi considerato che os32 gestisce finestre TCP pari a un solo pacchetto, per cui si attende la conferma di ogni singola trasmissione dalla controparte.
«a2» 2013.11.11 --- Copyright © Daniele Giacomini -- appunti2@gmail.com http://informaticalibera.net