Introduction

Référence

Le livre

Operating Systems: Three Easy Pieces

Le code

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
make

Les outils

Ligne de commande

Les systèmes de fichiers virtuels (VFS)

Exécution simultanée

Le programme

int main(int argc, char *argv[])
{
    char *str = argv[1];

    while (1) {
      printf("%s\n", str);
      Spin(1);
    }
    return 0;
}

Exécution simultanée

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 sorties

Pour “tuer” ces processus : kill PID1 PID2 ...

Killall (à installer)

sudo apt update
sudo apt install psmisc
killall cpu

Conclusion

cat /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.

Espace mémoire

Le code

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;
}

Exécution

$ ./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: 201

Conclusion

Dans 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.

Simultanéité

Le code

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;
}

Exécution


./threads 100
# Initial value : 0
# Final value   : 200
./threads 100000
# Initial value : 0
# Final value   : 141148 (variable)

Conclusion

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.

Gestion des processus

Notion de processus

Définition

Un programme est un ensemble d’instructions machine et de données. Un processus est un programme en cours d’exécution

Lancement d’un processus

Deux appels systèmes :

On a donc un arbre des processus, avec des relations père/fils, des processus frères…

Le premier processus

Voir l’arbre des processus :

ps afx

Ordonnancement

Stratégie

Lorsque 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

Les états d’un processus

L’OS est responsable des changements d’état des processus (élection, bloquage…)

# Utiliser la commande top pour suivre les processus en cours
top

Priorité

La 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 renice

Exemple

import 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.680570

Partage de ressources

Mécanisme de bloquage

Un processus accédant à une ressource non partageable peut la bloquer (acquisition d’un mutex)

Interblocage

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 :


  1. 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.↩︎