Pillole Linux – grep

Come selezionare con una regex (espressione regolare) tutte le righe di uno stream che non contengono una certa stringa?

% cat file | grep -v "some string"

Et voilà.

php-curl una storia infinita

Dopo l’ultimo aggiornamento di php-curl ho nuovamente il fastidio di emendare l’errore che stavolta si presenta in una veste diversa, a volte Warning

$ php -m | grep curl
PHP Warning:  PHP Startup: Unable to load dynamic library 'curl.so' (tried: /usr/lib/php/20220829/curl.so (/usr/lib/php/20220829/curl.so: undefined symbol: curl_mime_addpart, version CURL_OPENSSL_4), /usr/lib/php/20220829/curl.so.so (/usr/lib/php/20220829/curl.so.so: cannot open shared object file: No such file or directory)) in Unknown on line 0

e a volte Fatal

PHP Fatal error:  Uncaught Error: Call to undefined function curl_init() in /home/marcob/IdeaProjects/PHP/code/vendor/jasig/phpcas/source/CAS/Request/CurlRequest.php:107
Stack trace:
#0 ...

Senza riportare tutta la ricerca fatta, vado al punto: al solito, un’aggiornamento di PHP ha nuovamente modificato un link simbolico facendolo puntare ad una versione non compatibile della libreria libcurl

marcob@jsbach:/usr/local/lib$ ll
totale 170048
drwxr-xr-x 10 root root       4096 gen  5 17:18 ./
drwxr-xr-x 19 root root       4096 set 24  2021 ../
-rw-r--r--  1 root root       1957 set  2 17:07 curl.txt
-rw-r--r--  1 root root    1006444 mag 23  2017 libcurl.a
-rwxr-xr-x  1 root root       1042 mag 23  2017 libcurl.la*
lrwxrwxrwx  1 root root         16 gen  5 17:18 libcurl.so.4 -> libcurl.so.4.4.0*

Serve rifare il link simbolico:

marcob@jsbach:/usr/local/lib$ sudo ln -s /usr/lib/x86_64-linux-gnu/libcurl.so.4
marcob@jsbach:/usr/local/lib$ ll
totale 170048
drwxr-xr-x 10 root root       4096 gen 13 10:52 ./
drwxr-xr-x 19 root root       4096 set 24  2021 ../
-rw-r--r--  1 root root       1957 set  2 17:07 curl.txt
-rw-r--r--  1 root root    1006444 mag 23  2017 libcurl.a
-rwxr-xr-x  1 root root       1042 mag 23  2017 libcurl.la*
lrwxrwxrwx  1 root root         38 gen 13 10:52 libcurl.so.4 -> /usr/lib/x86_64-linux-gnu/libcurl.so.4
...
$ php -v
PHP 8.2.1 (cli) (built: Jan  6 2023 15:18:43) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.1, Copyright (c) Zend Technologies
    with Zend OPcache v8.2.1, Copyright (c), by Zend Technologies
    with Xdebug v3.2.0, Copyright (c) 2002-2022, by Derick Rethans

Il Warning scompare ma il Fatal c’è ancora: basta riavviare Apache

$ sudo apachectl restart

SSH headache

ssh
ssh

Stamattina non riesco più a stabilire una connessione SSH con un server in cui ho lavorato l’ultima volta un po’ di tempo fa. Facciamo che questo host si chiami web.mysecretserver.it come FQNS.

Anche oggi ho la mia occasione per sentirmi stupido.

Primo tentativo

Al primo tentativo il protocollo mi dice che non ha trovato tra quelli a disposizione del client, alcun metodo di scambio chiavi crittografiche supportate dal server:

$ ssh web.mysecretserver.it
Unable to negotiate with 184.17.4.115 port 22: no matching key exchange method found. Their offer: diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1

Secondo tentativo

Ho quindi specificato uno degli algoritmi supportati dal server

$ ssh -oKexAlgorithms=diffie-hellman-group-exchange-sha1 web.mysecretserver.it
Unable to negotiate with 184.17.4.115 port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss

ma stavolta i tipi di chiave supportati dal server sono diversi da quelli a mia disposizione (è inutile: ho una versione client di SSH molto più aggiornata del vecchio mysecretserver).

Terzo tentativo

Specifico allora un tipo di chiave supportato dal server:

$ ssh -oKexAlgorithms=diffie-hellman-group-exchange-sha1 -c ssh-dss -oHostKeyAlgorithms=+ssh-dss web.mysecretserver.it
Unknown cipher type 'ssh-dss'

Non serve specificare la cifratura (ssh-dss), basta il parametro -oHostKeyAlgorithms.

Quarto tentativo

$ ssh -oKexAlgorithms=diffie-hellman-group-exchange-sha1 -oHostKeyAlgorithms=+ssh-dss web.mysecretserver.it
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the DSA key sent by the remote host is
SHA256:iDMXs5dTRKtmP9VpVRtuPpJ1/bMja3ZSLnwuuEz5jAo.
Please contact your system administrator.
Add correct host key in /home/marcob/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/marcob/.ssh/known_hosts:23
  remove with:
  ssh-keygen -f "/home/marcob/.ssh/known_hosts" -R "web.mysecretserver.it"
Host key for web.mysecretserver.it has changed and you have requested strict checking.
Host key verification failed.

Ci siamo! Stavolta la connessione è possibile solo che la chiave dell’host è cambiata rispetto a quella contenuta nel mio portachiavi. Quindi

Elimino l’host

Aggiorno il file .ssh/known_hosts togliendo la chiave dell’host desiderato (il programma crea atuonomamente una copia di backup).

$ ssh-keygen -f "/home/marcob/.ssh/known_hosts" -R "web.mysecretserver.it"
# Host web.mysecretserver.it found: line 23
/home/marcob/.ssh/known_hosts updated.
Original contents retained as /home/marcob/.ssh/known_hosts.old

Trovo la soluzione

Dopo aver eliminato il vecchio host, il server SSH mi presenta il suo fingerprint (l’impronta) e mi chiede se sono disposto a fidarmi:

$ ssh -oKexAlgorithms=diffie-hellman-group-exchange-sha1 -oHostKeyAlgorithms=+ssh-dss web.mysecretserver.it
The authenticity of host 'web.mysecretserver.it (184.17.4.115)' can't be established.
DSA key fingerprint is SHA256:iDMXs5dTRKtmP9VpVRtuPpJ1/bMja3ZSLnwuuEz5jAo.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'web.mysecretserver.it' (DSA) to the list of known hosts.
         * * * * * * * * * * * * * W A R N I N G * * * * * * * * * * * * *
         THIS SYSTEM IS RESTRICTED TO AUTHORIZED USERS FOR AUTHORIZED USE
         ONLY. UNAUTHORIZED ACCESS IS STRICTLY PROHIBITED AND MAY BE
         PUNISHABLE UNDER THE COMPUTER FRAUD AND ABUSE ACT OF 1986 OR
         OTHER APPLICABLE LAWS. IF NOT AUTHORIZED TO ACCESS THIS SYSTEM,
         DISCONNECT NOW. BY CONTINUING, YOU CONSENT TO YOUR KEYSTROKES
         AND DATA CONTENT BEING MONITORED. ALL PERSONS ARE HEREBY
         NOTIFIED THAT THE USE OF THIS SYSTEM CONSTITUTES CONSENT TO
         MONITORING AND AUDITING.
         * * * * * * * * * * * * * W A R N I N G * * * * * * * * * * * * *
(webportal@web.mysecretserver.it) Password: 
Last login: Wed Nov 16 10:13:25 2022 from az931356.mysecretserver.it
-bash-3.2$ 

Eureka!

Ultima chicca: mettere tutte queste opzioni in un file di configurazione per accorciare il comando:

$ nano .ssh/config
Host websecret
	Hostname web.mysecretserver.it
	User webuser
	HostKeyAlgorithms=+ssh-dss
	KexAlgorithms=diffie-hellman-group-exchange-sha1
$ 

Quindi posso collegarmi senza dover ricordare tutto lo stringone, semplicemente con:

$ ssh websecret
         * * * * * * * * * * * * * W A R N I N G * * * * * * * * * * * * *
         THIS SYSTEM IS RESTRICTED TO AUTHORIZED USERS FOR AUTHORIZED USE
         ONLY. UNAUTHORIZED ACCESS IS STRICTLY PROHIBITED AND MAY BE
         PUNISHABLE UNDER THE COMPUTER FRAUD AND ABUSE ACT OF 1986 OR
         OTHER APPLICABLE LAWS. IF NOT AUTHORIZED TO ACCESS THIS SYSTEM,
         DISCONNECT NOW. BY CONTINUING, YOU CONSENT TO YOUR KEYSTROKES
         AND DATA CONTENT BEING MONITORED. ALL PERSONS ARE HEREBY
         NOTIFIED THAT THE USE OF THIS SYSTEM CONSTITUTES CONSENT TO
         MONITORING AND AUDITING.
         * * * * * * * * * * * * * W A R N I N G * * * * * * * * * * * * *
(webuser@web.mysecretserver.it) Password: 
Last login: Wed Nov 16 15:35:22 2022 from 10.196.82.233
-bash-3.2$ 

Voilà.

Riferimenti Web

Quantum Computing

Il mantra avveniristico più in voga degli ultimi due anni è sicuramente lui, secondo me anche più dell’Intelligenza Artificiale: il Quantum Computing, calcolo quantistico.

È anche vero che da un po’ l’attributo Quantum va comunque di moda anche in ambiti non strettamente scientifici (coscienza quantica, mente quantica e via discorrendo), però di sicuro una delle innovazioni da cui ci si aspettano progressi sconvolgenti è proprio questa branca della ricerca che incrocia Fisica Teorica, Fisica Sperimentale e Computer Science.

Breve storia del Quantum Computing

La nascita della Fisica dei quanti è dovuta a Max Planck che, in un suo articolo del 1901, trovò la quadra per una giustificazione teorica della curva del corpo nero (ho tradotto l’articolo originale, per chi fosse interessato) un oggetto ideale che non riflette alcuna luce, a cui i fisici di fine ‘800 non riuscivano a dare una spiegazione in termini di Elettromagnetismo e Termodinamica.

A coniare il termine “quanto” fu Albert Einstein nel suo articolo sull’effetto fotoelettrico del 1905 (il primo dei tre articoli del suo annus mirabilis), uno dei primi contributi ad utilizzare con profitto l’idea di elemento di energia (come lo chiamava Planck).

L’idea di utilizzare la Meccanica Quantistica per costruire i computer si può far risalire a Richard Feynman che la descrisse nella sua celebre lezione al Caltech per l’American Physical Society il 29 dicembre 1959: There’s Plenty of Room at the Bottom.

Attenzione: già i computer attuali sono basati sulla Meccanica Quantistica (il funzionamento dei semiconduttori si spiega solo con la Meccanica Quantistica), ma per questa nuova generazione la differenza è questa: affidare la codifica dell’informazione ad uno stato di un singolo sistema quantistico. Attualmente non è così: 0 o 1 fisicamente vengono codificati con il fatto che una strisciolina di silicio lasci passare o meno una corrente, che è una informazione dell’elettromagnetismo classico.

Paradosso EPR e teorema di Bell

Fin dalle origini (Planck e Einstein, anni Zero), passando per i lavori di Bohr e Sommerfeld (anni Dieci) e poi ancora per quelli di Heisenberg, Schrödinger, Jordan, Born e Dirac (anni Venti) – ma ne ho lasciati fuori tanti – non ci si affrancò mai abbastanza da essere sicuri di dove questa teoria ci avrebbe condotto. Era strana. Molto più strana anche della stessa Relatività di Einstein. Occorreva capirla ancora, sempre meglio. Che fosse difficile da capire lo ammisero tutti dall’inizio.

Bohr nel suo tentativo di argomentare contro l’articolo EPR nel 1935 ammise “Stiamo cominciando a capirci qualcosa”. Erano passati vent’anni dal suo primo articolo.

In un’altra lezione del 1965 Feynman lo disse fuori dai denti: “I think I can safely say that nobody understands Quantum Mechanics.”

Nel 1935 Einstein fece un lavoro che partì in sordina – le citazioni per vent’anni rimasero molto poche, poi all’inizio degli anni Sessanta fino ad oggi crebbero enormemente. Egli continuò a rimuginare sulla teoria quantistica per tutta la vita senza riuscire a digerirla. Il famoso articolo scritto quand’era già a Princeton dopo essere fuggito dalla Germania nazista, presenta il famoso paradosso di EPR (dai nomi degli autori: Einstein, Podolsky e Rosen), nel quale si sosteneva che la Meccanica Quantistica non può essere completa, attraverso un esperimento mentale grazie al quale si dimostrava che si poteva violare il principio di indeterminazione. Detto in parole semplici, secondo l’articolo di EPR la Meccanica Quantistica è probabilistica perché ci nasconde qualcosa.

Molti tentativi di confutare questa conclusione furono fatti – in primis da Bohr al quale avevano toccato il suo giocattolo – ma non convinsero mai completamente tutta la comunità scientifica. Una svolta si ebbe con il Teorema di Bell, dimostrato dal fisico irlandese John Bell nel 1964, nel quale si provava che erano fondate le preoccupazioni espresse da Einstein. In effetti Bell ci mostra che la Meccanica Quantistica non è reale né locale: la peggior paura di Einstein, in quanto pare che le informazioni possano propagarsi a velocità infinita (la spooky action at a distance detta poi entanglement) e che la Luna non esista quando la si guarda, cioè lo stato quantistico non sia definito prima della misura. In realtà le cose non stanno esattamente così nel mondo macroscopico, ma in quello subatomico sì.

Tecnicamente il teorema di Bell afferma che se la Meccanica Quantistica è una teoria a variabili nascoste (cioè è probabilistica perché ci nasconde qualcosa) allora deve valere una certa diseguaglianza. Ma se la Meccanica Quantistica è quello che dice di essere, questa diseguaglianza deve venire violata.

La violazione della diseguaglianza di Bell viene sistematicamente verificata dagli stati quantistici, in altre parole: la Meccanica Quantistica non è casuale grazie a variabili nascoste, è proprio dispettosa di suo. Questo fu provato sperimentalmente da Alain Aspect nel 1983 e da molti altri esprimenti condotti nei modi più fantasiosi per evitare che la Natura potesse aggirare l’esperimento (i cosiddeti loopholes), anche se la prova definitiva risale solo al recente 2018.

Alain Aspect, Anton Zeilinger e John Clauser sono stati insigniti del premio Nobel per la Fisica quest’anno, il 2022, proprio per questi lavori.

Cosa entra questo con il calcolo quantistico?

Quantum Computing
Quantum Computing – il qubit

La manipolazione degli stati effettuata per avere una conferma sperimentale del teorema di Bell, diede forza al concetto di qubit: un modo di codificare una informazione binaria attraverso lo stato di un sistema quantistico che però ha la bella proprietà di trovarsi in infinite combinazioni lineari di stati elementari prima di venire misurato. Il valore dello stato non esiste prima della misura, è questo che si intende con la locuzione la MQ non è reale.

Al contrario, il bit classico ha un valore anche prima di essere misurato, solo che lo ignoriamo ma è lo stesso che ha anche dopo che lo abbiamo misurato. La differenza è sostanziale ed è alla base del calcolo parallelo consentito dal questo tipo di dispositivi.

Nel 2015 ho seguito il corso di Quantum Information nell’ambito il Dottorato in Ingegneria delle Telecomunicazioni dal DEI – Università di Padova, tenuto dal prof. Paolo Villoresi, mio insegnante di Elettronica Quantistica quando studiavo Ingegneria Elettronica che, saputo del mio interesse sull’argomento, mi ha accolto affettuosamente nel suo gruppo di dottorandi.

In quel corso effettuammo un esperimento che era un po’ il rifacimento dell’esperimento di Aspect che mostrava la conferma sperimentale del teorema di Bell: una diseguaglianza che in teoria deve venire violata dalla MQ se è non reale o non locale – viene violata per davvero. Ne nacque questo mio lavoretto che mi divertii sommamente a scrivere, nel quale affronto un po’ di armamentario teorico per manipolare con una certa confidenza l’argomento, studio in dettaglio il paradosso EPR e infine descrivo il teorema di Bell e l’esperimento che da’ una conferma sperimentale che la disuguaglianza di Bell è davvero violata nella realtà.

Vedo, dai log del mio Google Analytics, che ultimamente il download di questo mio articolo ha avuto una rapida impennata, lo metto volentieri in evidenza per chi volesse leggerlo.

Misura della violazione della disuguaglianza di Bell

Bibliografia

È quella che trovate all’interno dell’articolo.

Applicare una patch

Tux patch
Tux patch

Dimentichiamoci per un attimo che esista Git e, più in generale, i software di gestione del versionamento. All’origine dei tempi, come si applicavano le modifiche ai file quando si faceva sviluppo in team? Ovviamente quanto segue è comunque utile e può essere utilizzato in sistuazioni in cui dobbiamo emendare singoli file.

Unix ha tuttora, compreso Linux, un comando di utilità molto potente: il comando diff, che è un utilissimo strumento per confrontare file di testo. È utile per esempio per individuare i punti in cui un programma è stato modificato rispetto ad una versione iniziale, magari introducendo dei bug non intenzionali, come capita spesso.

Meno comune è il suo uso per applicare velocemente le modifiche di una nuova versione ad uno stesso file di una versione diversa.

Potremmo semplicemente guardarci il file vecchio con un occhio, il file nuovo con un altro e applicare a mano le novità presenti nel nuovo dentro al file vecchio.

Ma l’essere umano è naturalmente fallibile ed è consigliabile operare in altro modo.

In Linux/Unix esiste un comando gemello di diff che è patch. Patch prende un file di differenze e modifica il file argomento applicando le modifiche contenute nel file di differenze.

Il file di differenze è l’output del comando diff. Vediamo un esempio.

Scrivo un bellissimo programma

Il bellissimo programma è questo, si chiama, tanto per non discostarci troppo dalla tradizione, hello.c:

#include <stdio.h> 

int main() {
	printf("Hello World\n");
}

Un collega scrive una versione più completa

Un collega che sta lavorando sul mio stesso progetto, fa una modifica al file e produce il nuovo file hello_new.c: (ovviamente al giorno d’oggi nessuno più si sogna di lavorare così, ma riportiamoci per un attimo nel passato, come abbiamo detto ll’inizio):

#include <stdio.h>

int main(int argc, char *argv[]) {
	printf("Hello World\n");
	return 0;
}

Il suo programma è più figo perché prevede che gli si possano passare dei parametri in ingresso.

Ora devo aggiornare la mia copia

Come faccio da aggiornare la mia copia senza fare errori? Ovviamente, non riportando a mano (e a occhio) le modifiche.

Uso invece il file differenza, un cerotto, detto patch: non è altro che l’output del comando diff:

$ diff hello.c hello_new.c 
1c1
< #include <stdio.h> 
---
> #include <stdio.h>
3c3
< int main() {
---
> int main(int argc, char *argv[]) {
4a5
> 	return 0;

Ora questo ouput lo mettiamo su un file che chiamiamo hello.patch

$ diff hello.c hello_new.c > hello.patch

L’applicazione del cerotto si fa con il comando:

$ patch [file da sanare] [cerotto]

Nel nostro caso, prima mi faccio un backup del file da sanare:

$ cp hello.c hello.orig.c

e poi applico la patch

$ patch hello.c hello.patch

A questo punto il file hello.c è cambiato, recependo gli aggiornamenti:

$ cat hello.c
#include <stdio.h>

int main(int argc, char *argv[]) {
	printf("Hello World\n");
	return 0;
}

È possibile creare la copia di backup contestualmente con l’applicazione della patch, senza cioè farlo a mano con un secondo comando come abbiamo fatto noi: specificando l’opzione -b, patch prima crea la copia di backup e poi applica le modifiche:

$ patch -b hello.c hello.patch 
patching file hello.c
$ ll
totale 28
drwxrwxr-x 2 marcob marcob 4096 ott 25 11:58 ./
drwxrwxr-x 3 marcob marcob 4096 ott 25 10:52 ../
-rw-rw-r-- 1 marcob marcob  173 ott 25 10:54 diagram.txt
-rw-rw-r-- 1 marcob marcob   94 ott 25 11:58 hello.c
-rw-rw-r-- 1 marcob marcob   62 ott 25 11:56 hello.c.orig <---- creato grazie all'opzione -b
-rw-rw-r-- 1 marcob marcob   94 ott 25 10:54 hello_new.c
-rw-rw-r-- 1 marcob marcob  128 ott 25 11:57 hello.patch

È bello Nautilus (il file manager di GNOME Linux) che mostra delle icone azzeccate:

patch
patch: Nautilus mostra icone constestuali 🙂

… e Git?

Questo comando è integrato in Git, ad esempio quando eseguiamo il comando di merge di due linee di sviluppo (branch). Diciamo che il bello di questo comando è il principio ma oggigiorno è annegato in strumenti ben più complessi per la gestione delle versioni dei file.

Riferimenti

Convertire xlsx in csv

Convert XLS to CSV
Convert XLS to CSV

Per soli utenti Linux, in due righe di comando:

$ sudo apt install gnumeric
$ ssconvert --export-type=Gnumeric_stf:stf_csv file.xlsx file.csv

Fine.

Riferimenti

Oracle: update – select

Ovvero come scrivere un’istruzione SQL per aggiornare i campi di una tabella prendendo i valori da un’altra in join.

La soluzione è valida per il dialetto di Oracle, con qualche modifica si fa girare per altri RDBMS.

Me lo dimentico sempre e ogni volta devo andare a googlare per trovarlo. Basta! Me lo riporto qui per comodità

UPDATE dest_tab tt
SET    (tt.code, tt.description) = (SELECT st.code, st.description
                                    FROM   source_tab st
                                    WHERE  st.id = tt.id)
WHERE EXISTS (SELECT 1
               FROM   source_tab
               WHERE  id = tt.id);

Risorse

Totem non riproduce più i video con Ubuntu 22.04

totem
Il logo di Totem

Conosciuto anche come Videos, Totem è un riproduttore di film progettato per GNOME [3].

Dopo aver avanzato il mio OS alla versione di Ubuntu 22.04 ho notato un problema (nel senso: da un po’ non vedevo i video e facendone girare uno delle vacanze estive mi sono accorto del probema che solo poi ho trovato essere legato alla nuova release di Ubuntu).

Facendo doppio click su un video, si apre il programma ma, invece di avviarsi il video, appare una finestra di alert con scritto

Il file non è stato trovato

Ho trovato la descrizione del problema nel forum [1] assieme alla soluzione, meglio spiegata in [2].

In breve, con la versione 22.04 di Ubuntu è stata introdotta una incompatibilità con GStreamer, un framework multimediale per gestire media file [4]. Con Ubuntu 22.04 non è più necessario per cui si può eliminare ed elimare con esso il problema:

$ sudo apt remove gstreamer1.0-vaapi

Problema risolto, i video si vedon di nuovo.

Riferimenti

Appunti di Sistemi Operativi (OS)

Sistemi operativi
Sistemi operativi

Un piccolo breviario con la spiegazione succinta dei principali concetti riguardante i sistemi operativi (operating systems, OS).

Sistemi Operativi

Sono i software fondamentali per poter utilizzare di una macchina di tipo Von Neumann (quella col processore e la memoria per dati/programmi). Consentono di usare il processore, la memoria, il network, l’I/O, di gestire l’allocazione in memoria dei programmi e dei processi e di regolare il loro accesso alle risorse e le interazioni tra di loro.

Come diceva la mia prima insegnante di programmazione, un computer senza sistema operativo è solo ferraglia (hardware, in inglese).

Kernel

Il kernel è il sottoinsieme di programmi del sistema operativo che sovraintende all’accesso delle risorse hardware (processore, RAM, memoria di massa, rete e I/O). I programmi “utente” quelli che servono ad eseguire le azioni della vita quotidiana – far girare un sito web, gestiore un foglio di calcolo, montare un video, mantenere un database – accedono alle risorse hardware solamente per mezzo del sistema operativo (e se lo saltano la circostanza è eccezionale e si fa solo in casi estremi).

Quali sono i principali compiti svolti dal kernel dei sistemi operativi?

Sintetizzando al massimo si possono individuare nel kernel le seguenti 2 attività:

  1. Allocazione delle risorse (memoria e I/O)
  2. Pianificazione e gestione dei processi (scheduling, ciclo di vita, segnalazioni tra processi, arbitraggio)

Allocazione delle risorse (RAM e I/O)

Spazio kernel e spazio utente

I computer moderni dividono la memoria in spazio del kernel e spazio utente. Lo spazio utente è il luogo in cui viene eseguito il software applicativo, mentre lo spazio del kernel è dedicato al lavoro dietro le quinte necessario per far funzionare un computer, come l’allocazione della memoria e la gestione dei processi. A causa di questa separazione tra spazio kernel e spazio utente, il lavoro svolto dal kernel non è in genere visibile all’utente.

Il sistema operativo è responsabile dell’occupazione/liberazione dello spazio di memoria per i processi. Mantiene le mappature dalla memoria virtuale a quella fisica (che sono archiviate nelle tabelle delle pagine). Decide anche quanta memoria allocare a ciascun processo e quando un processo deve essere rimosso dalla memoria.

L’accesso alla memoria RAM è un processo molto dispendioso anche in termini di tempo. Per eseguire operazioni di I/O in memoria infatti il microprocessore si avvale di un dispositivo hardware di controllo dedicato detto Memory Controller. Il MC gestisce l’individuazione delle locazioni di memoria attraverso l’indirizzamento e l’I/O dei dati da e verso il processore attraverso l’infrastruttura del bus dati (data bus). Nei processori di ultima generazione il memory controller è stato integrato nei microprocessori (infatti si chiama IMC – Integrated Memory Controller) ma l’infrastruttura fisica del bus (con i suoi clock) e la RAM rimangono pur sempre periferici e quindi introducono latenze.

Se un programma deve inizalizzare delle variabili non va a prendersi la RAM direttamente, ma se la fa allocare dal kernel. Ogni processo ha a disposizione una memoria virtuale visibile come heap (“mucchio”, area di memoria utilizzata per variabili globali) o come stack (“pila”, area di memoria utilizzata per chiamate a funzione, ricorsione e variabili locali): per ogni processo il sistema operativo gli presenta quello che a lui sembra essere l’intervallo di memoria completamente indirizzabile. Quindi su una macchina a 32 bit, ogni processo “pensa” di avere a sua disposizione 4 GB di memoria contigua.

In realtà, il sistema operativo, dietro le quinte, è impegnato a mappare le allocazioni di memoria virtuale su blocchi reali di memoria fisica. Quindi, ad esempio, un’allocazione di memoria virtuale di 400 byte viene mappata su 100 blocchi fisici da 4 byte. Quei blocchi fisici non devono essere contigui (e quasi mai lo sono – nulla impedisce che accada, ma su una macchina che esegue qualsiasi tipo di lavoro, è altamente improbabile) ma l’allocazione della memoria virtuale deve essere contigua.

Memoria Virtuale

La memoria virtuale è una tecnica che dà a un programma applicativo l’impressione di avere una memoria di lavoro RAM non frammentata, mentre in realtà può essere fisicamente frammentata e si può persino estendere nello spazio di archiviazione su disco. I sistemi che utilizzano questa tecnica semplificano la programmazione di applicazioni di grandi dimensioni e utilizzano la memoria fisica reale (ad es. RAM) in modo più efficiente rispetto a quelli senza memoria virtuale.

Attenzione che la “memoria virtuale” non è solo “l’utilizzo dello spazio su disco per estendere le dimensioni della memoria fisica”. L’estensione della memoria è una normale conseguenza dell’utilizzo di tecniche di memoria virtuale, che può essere gestita con le sovrapposizioni (overlay) o lo scambio completo di programmi e relativi dati (swap) su disco mentre questi sono inattivi.

Per “memoria virtuale” si intende più precisamente l’inganno che viene fatto ai programmi facendo credere loro di utilizzare grandi blocchi di indirizzi contigui.

malloc

Facciamo un esempio. Un programma C alloca dinamicamente una locazione di memoria. Cioè lo fa a runtime, non viene riservata una memoria iniziale dal compilatore (ad esempio come quando dichiaro una variabile int) ma lo fa in corsa per esempio per dichiarare un array di dimensione arbitraria che lo user si inventa al momento, mentre il programma sta già girando.

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
 
    // Questo puntatore contiene l'indirizzo base del blocco creato
    int* ptr;
    int n, i;
 
    printf("Scrivi il numero di elementi dell'array:");
    scanf("%d",&n);
 
    // Alloca dinamicamente la memoria usando malloc()
    ptr = (int*)malloc(n * sizeof(int));
...

In questo esempio al momento della compilazione non sappiamo quanta memoria servirà al programma perché è l’utente che lo decide dopo averlo avviato.

L’istruzione malloc() riserverà al processo, che rappresenta la realizzazione fisica del programma C, un’area di memoria (virtuale) n volte la dimensione di una variabile di tipo intero. Per esempio se la variabile intera è rappresentata in 32 bit (4 byte) e n=100, il processo dovrà allocare 400 byte.

Dove sono realmente questi 400 byte? Il programma non lo sa (il programmatore ancora meno).

Il kernel gli assegnerà 400 byte che lui – il programma – vedrà come contigui, ma dietro le quinte il kernel sa esattamente nella RAM fisica dove si trovano questi 400 byte che possono anche essere sparpagliati. Per mantenere questa associazione il kernel usa una tecnica chiamata paginazione (paging) che ha il compito non soltanto di fornire una mappatura tra indirizzi virtuali e fisici ma anche di segregare i processi in aree di memoria stagne e di fornire i puntamenti ai blocchi di RAM non contigui in modo tale che la memoria virtuale risulti non frammentata.

Gestione dei processi

Un programma si può definire come una sequenza (“passiva”) di istruzioni scritte in un determinato linguaggio che ha lo scopo di risolvere un problema o, più in generale, di effettuare un’attività.

Processo

È l’effettiva esecuzione delle istruzioni che definiscono il programma. Un programma può essere suddiviso in più processi; per esempio se un programma apre più finestre, ogni finestra è un processo separato. I processi sono definiti e gestiti dal sistema operativo.

Un singolo processore del computer esegue una o più istruzioni alla volta (per ciclo di clock), una dopo l’altra. Per consentire agli utenti di eseguire più programmi contemporaneamente (ad esempio, in modo che il tempo del processore non venga sprecato in attesa di input da una risorsa), i sistemi informatici a processore singolo possono eseguire la divisione del tempo o time sharing.
La condivisione del tempo consente ai processi di passare da uno stato di esecuzione ad uno stato di attesa per continuare ad essere eseguiti.
Nella maggior parte dei casi ciò avviene molto rapidamente, fornendo l’illusione che diversi processi vengano eseguiti “contemporaneamente”. Questo è noto come concorrenza o multiprogrammazione.

Pianificazione e gestione dei processi.

Il sistema operativo mantiene separati i suoi processi e alloca le risorse di cui hanno bisogno in modo che abbiano meno probabilità di interferire tra loro e causare errori di sistema (ad esempio deadlock – la situazione in cui due processi aspettano a vicenda che l’altro acceda ad una locazione di memoria – o thrashing – situazione in cui il processo spende più tempo a paginare la memoria che ad eseguire le istruzioni). Il kernel può anche fornire meccanismi per la comunicazione tra processi per consentire ai processi di interagire in modi sicuri e prevedibili.

Arbitraggio dei processi

Quando il kernel decide che bisogna parcheggiare un processo (P1) e servirne un altro (P2) in base a un qualche criterio, avviene il salvataggio in una particolare area della RAM del set di registri e flag del processore, del program counter (il registro che contiene l’indirizzo della prossima istruzione del programma da caricare nel processore) e dello stack (che rappresenta la nidificazione delle chiamate a funzione) relativi al processo P1 e preleva dalla stessa area, in una locazione diversa destinata al processo P2, i diversi valori per lo stesso set di registri e dello stack per il processo P2 e li carica nel processore per poi avviarne l’esecuzione. Questa sequenza scrittura – lettura si chiama commutazione del contesto (context switching).

Sincronizzazione dei processi concorrenti.

Se ci sono processi che concorrono a scrivere in un stessa locazione di memoria (ad esempio due utenti che condividono un conto bancario ed eseguono operazioni allo stesso momento), il sistema operativo deve orchestrare le operazioni su questa locazione di memoria perché questa rappresenti in ogni istante il risultato che tutti si attendono.

Esempio

Alice e Bob aprono un conto bancario condiviso. Il loro saldo iniziale è di 0 €. Ognuno di loro deposita 100 €. Ci aspettiamo che il saldo finale debba essere di 200 €.

Ma consideriamo la seguente sequenza di operazioni:

  • 1) Alice legge Saldo (legge 0 €),
  • 2) Alice incrementa Saldo ma non lo salva ancora: Saldo + 100 €: a questo proposito si consideri che l’istruzione seguente
    s = s + 100
    viene eseguita in più passaggi dal processore (eax, eay sono registri interni del processore – il processore esegue tutte le operazioni aritmetiche internamente perché così opera in modo enormemente più veloce – e s la locazione di memoria virtuale che contiene il Saldo)
    mov     eay, 0
    mov     eax, 100
    add     eax, eay
    push    s
  • 3) Bob legge il Saldo (legge anche lui 0 €, poiché Alice non ha ancora scritto il nuovo Saldo, non è ancora arrivata a push s),
  • 4) Bob incrementa il Saldo e neanche lui lo sovrascrive: Saldo + 100 €,
  • 5) Alice scrive il Saldo (scrive 100 €),
  • 6) Bob scrive Saldo (scrive anche lui 100 €).

Questa sequenza porta al saldo finale di 100 € (che non è corretto). Questo esempio illustra chiaramente l’importanza della sincronizzazione tra i processi di Alice e Bob. In particolare, la seguente sequenza (se applicata) produrrebbe risultati corretti:

  1. Alice legge,
  2. Alice incrementa,
  3. Alice scrive,
  4. Bob legge,
  5. Bob incrementa,
  6. Bob scrive .

Potrebbero esserci altre sequenze di letture e scritture che producono ugualmente risultati corretti, però l’importante è vedere come il sistema operativo esegua l’arbitraggio dei due processi affinché venga eseguito prima completamente il processo di Alice – che è partito prima – e poi quello di Bob.

Commutazione del contesto

Un altro dei compiti del kernel è fare in modo che ogni processo abbia accesso alla CPU e alle sue risorse per un certo periodo limitato di tempo. Questo è dovuto al principio che un processore serve un processo alla volta. Ultimamente – negli ultimi 20 anni – con l’avvento dei processori multicore abbiamo effettivamente la possibilità di eseguire più processi in contemporanea moltiplicando il numero di CPU. Attualmente per i processori commerciali siamo a 8 core per cui si possono eseguire 8 processi contemporaneamente. Ma il principio è sempre quello: una CPU (un core) esegue un solo processo alla volta. La tecnica del multitasking prevede un uso a divisione di tempo della singola CPU (time sharing).

Risorse web

Funzione anonima↔Espressione lambda

Espressione lambda
Espressione lambda

C’è un concetto che per tanti anni mi è apparso misterioso, quello di espressione lambda o lambda expression in inglese – che poi mi è risultato analogo a quello di funzione anonima – da quando me l’hanno raccontato nel corso di Automi e Linguaggi Formali all’Università nell’ambito del corso di LISP moltissimi anni fa.

Sì, faccio pubblicamente ammenda di questa cosa, dovrei saperlo e non lo so.

Non è in ogni caso un concetto particolarmente complicato.

Espressione lambda

Una espressione lambda è una funzione senza nome (o anonima) in cui descrivo solo quali sono gli argomenti e i risultati della funzioni.

Una sintassi generale non legata ad alcun linguaggio di programmazine potrebbe essere:

argomenti -> operazioni

Per chi ha qualche confidenza con la matematica, una definizione di funzione di solito si scrive così:

\begin{gathered}
	f: \mathbb{R} \rightarrow \mathbb{R} \\
	x     \mapsto    2x 
\end{gathered}

Ecco: l’epressione nella seconda riga è esattamente la lambda expression.

Definizione di una espressione lambda in Python

Vediamo come si definiscono e si utilizzano le espressioni lambda in vari linguaggi di programmazione.

In Python le funzioni ordinarie si definiscono con la keyword def

def double(x):
  return 2*x

Quindi c’è la parola chiave def, poi il nome della funzione, l’elenco degli argomenti – tra parentesi tonde – e infine il corpo della funzione, complicato quanto si vuole.

Invece le espressioni lambda si definiscono con la keyword lambda, non è necessario dare loro un nome e occupano una sola riga:

lambda x: 2*x

quindi in generale

lambda argomenti: espressione

Poi per poterle invocare un nome bisogna comunque darglielo, ad esempio per l’espressione lambda precedente:

double = lambda x: 2*x

y = double(5)

Funzioni anonime in PHP

Le funzioni anonime in PHP sono l’equivalente delle espressioni lambda per Python:

<?php
$double = function($x) {
    return 2*$x;
}

$y = $double(5);

Ora, si potrebbe obiettare che comunque diamo sempre un nome a queste funzioni anonime, per cui: sono anonime o no?

L’uso più appropriato che da il nome di funzione anonima è la definzione di una callback, ossia una funzione che deve venire invocata quando si verifica un determinato evento.

Utilizzo in Javascript

In Javascript possiamo fare questo esempio: una funzione viene chamata quando si verifica un evento (un click, del mouse, la pressione su un link, la digitazione di un tasto della tastiera e così via). La funzione da invocare (che viene detta callback) è definita e invocata sul posto e quindi non ha alcuna necessità di avere un nome:

document.queryselector("#callback-btn")
    .addEventListener("click", function() {    
      console.log("User has clicked on the button!");
});

In questo esempio la funzione di callback viene passata alla funzione addEventListener() che ha lo scopo di agganciare l’evento “click” ad una funzione che svolge un determinato compito. In questo caso specifico, quando questo click si verifica, il programma va a scrivere una riga di log. Non c’è alcuna necessità di invocarle altrove questa funzione per cui va bene anche non darle un nome, forma a cui si perviene con la sintassi function() (senza il nome tra la keyword e le parentesi come si fa di solito).

Risorse web