Operating Systems: Three Easy Pieces
Dépôt github à cloner :
https://github.com/remzi-arpacidusseau/ostep-code
git clone https://github.com/remzi-arpacidusseau/ostep-code
cd ostep-code
cd intro
maketopps : liste les processus ps,
ps aux, ps afxkill PID / kill -9 PIDkillall nomduprogramme/proc : système de fichiers processus : informations du
noyau sur les processus
/proc/cpuinfo/proc/meminfo/proc/PID/.../sys : informations de l’arbre des périphériquesint main(int argc, char *argv[])
{
char *str = argv[1];
while (1) {
printf("%s\n", str);
Spin(1);
}
return 0;
}cat /proc/cpuinfo./cpu 1 & ./cpu 2 & ./cpu 3 & ./cpu 4 &
# résultat de l'exécution...
# Les programmes s'exécutent "ensemble"
# 1 2 3 4 1 3 2 4 1 3 2 4 1 3 4 2 ..../cpu 1 & ./cpu 2 & ./cpu 3 & ./cpu 4 &\
./cpu 5 & ./cpu 6 & ./cpu 7 & ./cpu 8 &\
./cpu 9 & ./cpu A & ./cpu B & ./cpu C &\
./cpu D &
# résultat de l'exécution...
# Les programmes s'exécutent "ensemble"
# On a des variations de l'ordre des sortiesPour “tuer” ces processus : kill PID1 PID2 ...
sudo apt update
sudo apt install psmisckillall cpucat /proc/cpuinfo | grep processor | wc -l donne le
nombre de “processeurs” (coeurs) du système
On a été capables de faire tourner plus de programmes en même temps qu’il n’y a de processeurs. Le système a donc une méthode pour “partager” le processeur entre les différents programmes.
int main(int argc, char *argv[]) {
int *p;
p = malloc(sizeof(int));
assert(p != NULL);
printf("(%d) addr pointed to by p: %p\n", (int) getpid(), p);
*p = atoi(argv[1]); // assign value to addr stored in p
while (1) {
Spin(1);
*p = *p + 1;
printf("(%d) value of p: %d\n", getpid(), *p);
}
return 0;
}$ ./mem 100 & ./mem 200
# (2710) addr pointed to by p: 0x555555756260
# (2711) addr pointed to by p: 0x555555756260
# (2710) value of p: 101
# (2711) value of p: 201Dans chaque processus, le pointeur mémoire p a la même
adresse 1 en mémoire, or les valeurs pointées
par p dans chaque programme sont différentes.
Le système a donc une méthode pour “virtualiser” la mémoire pour les différents programmes.
volatile int counter = 0;
int loops;
void *worker(void *arg) {
int i;
for (i = 0; i < loops; i++) {
counter++;
}
return NULL;
}
int main(int argc, char *argv[]) {
loops = atoi(argv[1]);
pthread_t p1, p2;
printf("Initial value : %d\n", counter);
Pthread_create(&p1, NULL, worker, NULL);
Pthread_create(&p2, NULL, worker, NULL);
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
printf("Final value : %d\n", counter);
return 0;
}
./threads 100
# Initial value : 0
# Final value : 200./threads 100000
# Initial value : 0
# Final value : 141148 (variable)Les deux processus tournent simultanément en accédant à une ressource
partagée (l’espace mémoire counter).
La valeur de counter est lue, incrémentée et réécrite de
manière non atomique. Dans ce cas on peut avoir des
situations de compétition (race conditions).
counter contient 20
P1: lecture (registre = 20)
P2: lecture (registre = 20)
P1: incrémentation (registre = 21)
P2: incrémentation (registre = 21)
P1: écriture (counter = 21)
P2: écriture (counter = 21)
La solution est d’utiliser des méthodes d’exclusion mutuelle (mutex) ou des opérations atomiques.
Un programme est un ensemble d’instructions machine et de données. Un processus est un programme en cours d’exécution
Deux appels systèmes :
fork : appel système permettant la création d’un nouveau processus (fils) copie conforme du processus appelant (père). Valeur de retour : PID dans le processus père, 0 dans le processus fils, -1 si erreur.
exec : charge le code et les données statiques d’un programme et remplace le segment de code courant par celui-ci.
On a donc un arbre des processus, avec des relations père/fils, des processus frères…
init, qui crée
ensuite les processus nécessaires au bon fonctionnement du systèmeVoir l’arbre des processus :
ps afxLorsque plusieurs processus doivent tourner sur un processeur, comment organiser leur exécution ?
Sans interruption du processus :
Avec interruption du processus : ordonnancement préemptif utilisant context switch / timer interrupts
L’OS est responsable des changements d’état des processus (élection, bloquage…)
# Utiliser la commande top pour suivre les processus en cours
topLa décision d’élection de l’ordonnanceur des tâches
fait aussi intervenir un niveau de priorité :
nice
Si plusieurs processus sont en concurrence pour l’élection, le processus le moins “nice” bénéficiera de la plus grande quantité de processeur.
man nice
man reniceimport sys
import datetime
print("Waiting to start")
i=0
while i < 10000000:
i+=1
i=0
start = datetime.datetime.now()
while i < 100000000:
i+=1
if i%1000000 == 0:
print(datetime.datetime.now(),sys.argv[1], i)
print("Done ", sys.argv[1], "in", datetime.datetime.now() - start)# Exécution à deux priorités différentes
nice -n 0 python3 loop.py A & nice -n 20 python3 loop.py B &
# Done A in 0:00:18.003129
# Done B in 0:00:18.680570Un processus accédant à une ressource non partageable peut la bloquer
(acquisition d’un mutex)
https://fr.wikipedia.org/wiki/Interblocage
Deux mutex (M1 et M2), deux processus légers (P1 et P2) et la séquence d’opération suivante :
Dans cette situation, les deux processus sont définitivement bloqués.
Solution :
En situation complètement réelle, les processus n’ont
pas exactement la même adresse, pour éviter certains types d’attaques,
les adresses mémoires sont aléaoires. On peut désactiver ce comportement
avec echo 0 | sudo tee /proc/sys/kernel/randomize_va_space,
ce qui a été fait pour cette démo.↩︎