PRINCIPLES OF OPERATING SYSTEMS PRINCIPLES OF
Transcription
PRINCIPLES OF OPERATING SYSTEMS PRINCIPLES OF
PRINCIPLES OF OPERATING SYSTEMS L. BENTABET, Bishop’s University, Winter 2007 Notes par Philippe Giabbanelli I. Definitions and history 1) What is a Computer System ? It is a set of users, system and applications, Operating System and resources. The OS (i.e. the kernel that is running all the time on the computer) controls the use of the hardware for the different applications, which are obviously used to solve some problems. The requirement depends on users and applications. ● Needs of users Convenience → Monopolized by a single user → Run multiple programs → GUI for ease of use → Resources utilisations is not an issue ♦ Example : Linux, windows, Mac OS, for PC. Efficiency → Large, shared multi user systems → OS designed to optimize the use of resources → Fair amount of resources for every user → Security and protection (on memory !) → Efficiency Trade-off between efficiency and convenience Nothing ♦ Example : mainframe system, Unix/Irix/Vax. → Workstations connected through a network → Sharing of devices and Network ♦ Example : Windows NT, Linux. ♦ Embedded systems (input are sensors !). ● Needs of the system → Only resources allocation. 2) Evolution of Operating Systems ● Mainframe Systems Batch Systems ♦ Rudimentary OS without concept of process but a ‘job’, composed of code+data+control information. ♦ No interaction with the user. ♦ Input : punched cards or magnetic tapes. ♦ Output : line printer. ♦ A batch is a set of jobs. The execution is sequential. Multiprogram ♦ Several jobs in memory Batch Systems ♦ A job has the CPU as long at it is not waiting for an I/O ; it is the CPU management (scheduling). Magnetic disk Job 1 Job 2 OS load job memory Job 3 Job 1 Job 2 Job 3 Job 4 Job Selection Virtual memory managment. OS Job 1 CPU Job 2 Job 3 CPU multiplexing ● Timesharing Systems ♦ Interaction with the user ♦ Support multi-tasking ♦ The CPU is multiplexed between the tasks ♦ For the scheduling of processes, switching is performed on I/O and with a timer. ♦ We say “process” instead of “job”. ● Desktop Systems ♦ Use of timesharing ♦ One user ♦ We focus more on convenience than efficiency ● Multiprocessors Systems CPU 1 CPU 2 Memory CPU n ♦ Use of timesharing bus ♦ Reliability (fault tolerant) ♦ Economical ♦ n processors : speed by k.n, where k < 1 depends on the cost of managements of the network. ♦ Two configurations : ♦ Symmetric (solaris 5) : all CPU identical from the point of view of responsibility. User responsible for distributing to the CPU. ♦ Asymmetric (solaris 4) : 1 master CPU. The OS is responsible for dispatching, instead of user. Le switch entre les processus est rapide car on laisse toutes leurs données en mémoire. On gaspille ainsi de la mémoire en la laissant allouée à quelque chose qui ne l’utilise pas, mais en revanche le switch devient très rapide puisqu’on change uniquement les pointeurs. C’est pour cela qu’on arrive à un switch en millisecondes avec des timer extrêmement réduits. II. Computer Organization disks CPU monitor (écran) Disk Controller Graphics Adapter printer Memory keyboard mouse USB Controller bus … 1) CPU Organization 3046 3050 a Execute the code located at this part of the memory R1 R2 R3 3054 PC 3050 IR load R1,a ........ load R1, a 101…. Translated load R2, b add R1, R2 into machine 011…. code store R3, a 110…. CPU Les registres du CPU sont manipulés par l’ALU (Arithmetic Logic Unit). IR est le Instruction Register, qui contient l’instruction courant. PC (Program Counter) pointe vers la prochaine instruction. Il y a un autre registre DR (Data Register), utilisé lorsqu’on veut sauver quelque chose en mémoire. 2) Controllers Organization ● Similar to CPU’s. A controller is in charge (responsible) for the input/output devices ; it will transfer the data from the buffer to the input/output device (or vice versa). ● Controls the Input/Output operations between their own buffers and external devices. ● The communication controllers/CPU is done through the bus. At some points, the controller will need the bus; they will have to wait until the bus becomes free. Therefore, the controllers will execute concurrently with the CPU (il y a une concurrence pour l’accès au bus; sinon, on parlerait d’exécution simultanée). Le device status a 2 bits importants : Busy et Done. Le code pour l’erreur survenue est mis dans les bits restants. Le command register a 2 valeurs : input (external device → buffer), ou output (buffer → external device). Command register Device Status Register LOGIC CIRCUITS (control unit) BUFFER (internal memory) Busy 0 0 1 1 Done 0 1 0 1 Description Idle (ready for use) I/O finished Working Undefined (error…) Example : Save f.out to the disk, and then load f.in from the disk. ♦ Save f.out - OS (CPU) checks if the device is idle (i.e. ready for use). - Copy partially the data from the memory to the buffer. Les données sont rarement copiées en totalité car le buffer a une taille limitée, même s’il arrive maintenant jusqu’à 16 Mo. - Set output in the command buffer. - Status of the device : working. Move data from the buffer to the disk. - Status of the device : finished. We tell it to the CPU through the BUS. The controller sends interrupts to the CPU ; the CPU is stopped in what it was doing, it is an interruption. ♦ Load f.in - Check idle - Command register : input - Device : working - Copy from disk to buffer - Finished → interrupts the CPU For each device, we have a queue (file d’attente). These queues are all Output 3 put together in a data structure Input 1 Output 1 Output 2 Input 2 called device status table, implemented in the OS. 3) Interrupts Mechanism read(x) y=x+2 load R1, a load R2, b add R1, R2 store R3, c 1 2 1 will be waiting for the controller. 2 will be using the CPU (i.e. running). 1 aura fini après le ‘load R2, b’. Il fait un interrupt : interrompt les processus. Pour intervertir, on sauvegarde le contenu (content/context) du CPU, c’est-àdire les registres, et on charge le nouveau processus. Il faut avoir un moyen d’identifier le périphérique qui a envoyé l’interrupt. Nous allons voir 2 moyens. ● Le périphérique envoie un interrupt, qui n’est qu’un seul bit avec 0 ou 1. On fait un sondage linéaire (polling : check sequentialy) pour trouver le premier périphérique qui a comme statut finished. Cela nécessite de définir une séquence de recherche, et donc d’assigner certaines priorités aux périphériques. ● Le périphérique inclus son identité dans l’interrupt qu’il envoit : vectored interrupt system, car c’est un vecteur de bits. Ce système est illustré ci-dessous : Le périphérique 0 génère l’interrupt Finished ? k n = 2 entrées 000…1. On utilise le … vecteur de bits tout à 0 pour dire qu’il n’y a k pas d’interrupt, donc 2 → k Encoder les périphériques sont nommés à partir de 0 k sorties mais le code commence à 1. 4) I/O Handling (gestion des entrées/sorties) On cherche par exemple à exécuter la routine associée à read(x), demandé par le périphérique 1. Chaque périphérique a une routine propre, i.e. sa façon de fonctionner. La routine est stockée en mémoire, à un emplacement déterminé par l’adresse [offset+binary code] ; l’offset est déterminé par le système d’exploitation, et le code binaire est donné par l’API lorsque l’utilisateur appelle une fonction. Adresse de la routine pour le 0 …… 100 address 0 101 address 1 Address 1 Routine for the device 1 Exemple : copy x du buffer vers la mémoire Dans l’exemple ci-contre, l’offset est 100. On donne le code binaire 0 : on regarde l’adresse dans la table de pointeurs (map), on exécute la routine concernée. On peut alors finir l’interrupt en chargeant la contexte sauvegardé et en le relançant. Notons que IRQ : priorité des interrupts. 5) DMA (Direct Memory Access) Le CPU n’est pas utilisé pour faire les transferts avec les périphériques. Au lieu de cela, on utilise le DMA. Le problème est que le CPU travaille et a besoin d’accès à la mémoire : le DMA et le CPU entrent en compétition pour le bus. Dans certaines architectures, chacun a son bus (dual-bus architecture). microcontroller Main Memory DMA I/O device buffer CPU Process 6) Hardware Protection ♦ Several processes and the OS are running concurrently ♦ All are sharing the hardware resources ♦ If there exist a fatal error in one process, we don’t want it to disturb the execution of the other processes; we need to isolate it. Examples : Overwriting the data of other processes, involves the memory. Infinite loop, involves the CPU. Multiple Input/Output operation of the same device, involves the devices. We need a protection mechanism implemented at hardware level to ensure that it cannot be broken. Dual Execution Mode. A bit is added to the CPU : the mode bit. It defines 2 modes of execution : - User mode (1). It is a user program that is running : execution of behalf of the user. It cannot execute instructions that may harm the system. - Monitor mode (2), or supervisor mode, done on behalf of the OS. Instructions may harm the system, but we hope that they won’t because they are written in the OS. Example : cin<<, fopen, … User Mode (bit = 1) fopen : trap (call to the OS) User Program OS Abnormal Termination finished Hardware Check the mode bit : it is 1. Generates an interrupt to create a fatal error. OS Le switch du mode utilisateur au mode superviseur se fait en générant une interrupt (aussi dit trap). En pratique, cette interrupt est générée par l’API, lors de l’appel d’une fonction. Toute action qui peut bloquer le système doit être faite en passant par une fonction de l’API, comme l’ouverture d’un fichier. Si le programme tente de communiquer directement avec le matériel, celui-ci verra que le mode bit est toujours à 1, et que l’accès est interdit ; il fera une interrupt et l’OS fermera le programme. I/O protection. I/O routines must execute in supervisor mode. L’API se charge de faire la correspondance lors de l’appel : elle change le mode et trouve le n à utiliser dans la table de pointeurs. Si les paramètres à passer sont trop gros, on les sauve dans le user space et on place juste un pointeur dans le CPU ; sinon on fait un passage direct dans les registres. System Call n (user) API Switch bit mode 0 Va à l’adresse offset+n de la routine Exécution de la routine Switch bit mode 1 Si on arrive à mettre notre adresse dans l’espace de l’OS, et que cette adresse pointe vers notre programme, alors on met notre bit mode à 0 de façon permanente, et on exécute tout le programme en mode supervisor : tout le mécanisme de sécurité tombe à l’eau. Il faut donc une protection de la mémoire. Memory Protection. We add 2 registers to the CPU : base register (smallest address of the process), limit register (range of addresses allocated) ; they can be accessed only in supervisor mode. OS Process 1 Process 2 Process 3 Base value 0 256000 300040 420540 880000 CPU address >= Base + limit yes no Trap to OS for fatal error < yes Access OK En pratique, la mémoire est fragmentée, ce n’est pas un bloc continu. Utilise le MMU (Memory Mamagnment Unit). CPU Protection. Si un processus est en exécution dans une boucle infini, il ne va pas rendre le CPU : on protège donc le CPU avec un timer qui génère un interrupt tous les ∆t. Le timer est un compteur qui va de n à 0, branché sur l’horloge interne (déclenchement sur front). ∆t est une variable gérée par l’OS (voir scheduling). 7) The storage structure Main Memory. The only media that the CPU can access through address is the main memory. A program must be in main memory in order to be executed. This memory is volatile. Access are done at the hardware level, using machine instructions (e.g. load, store, …). R/W RAM Cache Memory Controllers Registers Managed at hardware level I/O Mapped Memory Les contrôleurs ont des adresses et peuvent être accédés par load et store. Il y a une plage d’adresses contrôleurs. We use high speed memory (cache memory) to hold recently used data. If the data is in the cache, we use the cache; otherwise it is in the RAM, and we copy it to the cache, and then we use it. It increases the performances of the system. We try to place 80% of the data needed in cache instead of the RAM. Since the cache memory is small, we need a data replacement policy. On fait passer un des blocs du Cache (full) RAM cache dans la RAM, par exemple « remove the least CPU recently used block ». Après, on copie et on utilise le bloc We need this block demandé par le processeur. Time neighbourhood : data used non only once; several access to the memory are expected. Location/space neighbourhood : we move some blocks of data instead of only 1 or 2 bytes. Secondary Storage. Magnetic disks, non volatile, accessed through the OS. load load Magnetic Disk Main Memory (RAM) Cache update Update when we move the block CPU III. Cache Main Memory (RAM) Cache CPU load CPU registers update Dans la situation de deux processeurs utilisant la même mémoire, on établit une communication entre les deux caches. OS Structure 1) OS Components ♦ Process = Running program + ressources (CPU time, files, messages…). Process creation, deletion, suspension (operation d’I/O ou à cause du timer : demande de resources non encore disponibles), resumption by the OS. Interprocess communication and synchronization (partage de variables). ♦ Memory Manager. Allocate memory to processes, and keep track of it (e.g. use of limit + base registers), and deallocate. ♦ File Manager. Mapping between the logical files and the secondary storage (où le fichier n’est pas un bloc continu mais fragmenté). Manipulation of files and directories. ♦ Protection : CPU, I/O, Memory. ♦ Interfacing : On lit une commande. L’OS test la validité: si non, message d’erreur ; sinon trigger OS call. 2) System Calls : interface between processes and OS indirect Routines implemented in run-time library Processes Direct Call : fork(), read(), write(). Norme POSIX. Readfile(), write() : win32API (windows). OS Read, write : POSIX or win32API ♦ Use of registers to pass parameters Load x OS call 13 Routine B Use x x On met les paramètres dans les registres et on appelle la routine, qui va aller les chercher. ♦ Use of memory to pass parameters x Parameters Load x OS call 13 ♦ Use of the stack (linux) Calling process : push OS process : pop x Read x Load param 3) Useful OS calls ♦ Process Controlling A new command Valid ? Create new process. Fork(), exec sur la commande. Terminate the process : exit(). On a deux modes : wait() pour une exécution en foreground, ou rien si on veut du background. Shell process ♦ File Management (POSIX) Tous les périphériques d’entrées/sorties sont considérées comme des fichiers dans la norme POSIX : open() retourne un fid, close(), read(), write(), lseek(), fcntl() envoie une requête spécifique comme « don’t block the calling process. » 4) Device Managment // user program cout >> « x ? » ; cin << x ; L’utilisateur appelle des fonctions de la bibliothèque, qui font des appels au noyau. L’OS fait le lien entre l’appel et le driver, et le lance en mode superviseur. Stdio.h Write(deviceId, …). deviceId est un entier identifiant le périphérique : disque dur en 3, floppy en 2 (major number), puis numéro : 1er disque dur (numéro mineur 1), 2nd disque dur (numéro min. 2), etc. Read(deviceId, …) Device dependant drivers (they are outside of the kernel, as we do nowadays to keep the kernel as small as possible). OS call to the kernel. Switch in supervisor mode. ♦ Check value of the decice Id (switch…) ♦ Call the routine which knows how to write to the right device. This routine is responsible for error handling. Rappelons qu’en POSIX, tout périphérique est vu comme un fichier. Quand on fait un cout, le périphérique est l’écran : WONLY (write only) ; quand on fait un cin, c’est le clavier : RONLY (read only). 5) Interprocess Communication Les processus doivent partager certaines données, et des problèmes de synchronisation se posent. L’implémentation la plus P1 courante consiste à autoriser update retrieve x le partage de mémoire dans le Pr1 Pr2 P2 mappage des adresses des update y processus. On peut aussi P1base : 1000 use utiliser des pipes (se gère P1limit : 1000 Exemple : un processus produit des données et comme des fichiers avec write P2base : 1900 un autre les utilise. P2limit : 2100 et read dans le pipeId) ; par contre ils entrainent un grand nombre de context switching : P1 Information to share P1 Copy of the info Pipe ID time consuming ! 6) OS Organization MSDOS a été conçu pour fournir le plus de services dans le moins possibles d’espaces. Il adoptait une architecture en couches, mais ne la faisait pas respecter : une application peut accéder directement au matériel (c.f. flèches rouges). Il y a donc de graves problèmes de protection. Ceci se comprend car la 1ère version était implémentée dans le processeur 8088 qui n’avait pas de mode bit. Application programs Resident Program (toujours en mémoire) Drivers Hardware Kernel Les anciennes versions d’UNIX adoptaient aussi une structure en couche, mais la faisaient respecter. Le problème était que tout était dans le noyau, il n’y avait aucun module : on parle d’une structure monolithique, difficile à débuguer. De plus, chaque fois qu’on a un nouveau driver, il faut recompiler le noyau ce qui fait augmenter ça taille et prend du temps. Le fonctionnement, lui, est rapide car peu de calls. ♦ memory manager Using System The ♦ CPU/file manager system Call Hardware users programs ♦ drivers Interface Dans le micronoyau (Micro Kernel), on veut garder le noyau aussi petit que possible : la gestion de la mémoire, du CPU, les communications et les drivers sont en dehors du noyau. La structure en couche doit être respectée, mais le nombre d’appels est plus important. On va de l’user space (programme) dans le monitor space (noyau) à l’user space (prendre les drivers) : 2 context switches. Ainsi, la 1ère version de Windows NT (pour les serveurs) utilisait ce système et était plus lente que Windows 95 (paradoxe…). Le système est plus stable, portable et facile à débuguer en conservant un petit noyau certifié (Linux). La dernière architecture est celle à ‘modules’. L’OS est fait d’un micro kernel et d’un ensemble de modules chargeables dynamiquement (runtime : quand on les appelle), et/ou au démarrage. Si un module est dans le noyau, on l’exécute en mode superviseur. C’est l’architecture de certains Solaris où on peut choisir ses modules. IV. Process Managment 1) Déroulement général et relations père-fils A process is all what a program needs in order to be executed : text segment (program code), program counter (address of the next instruction), data segment (global data defined within the program), stack (memory ressource allocated to thje process in order to save temporary variables such as return addresses), heap (memory that is allocated dynamically). Only one process new admitted ready Just created timer complete In memory, waiting for the CPU scheduled waiting terminated Deallocation. The OS will release the process ressources. running Request I/O or waiting for event There is a need for an efficient identification of each process, to know exactly its state, what it is using and what it’s requiring. We use the PCB (Process Control Block) : an OS data structure that identifies each process in the system (i.e. chaque processus a son PCB). It is updated on change of state. It contains : - Process State - Process ID (integer) - PC and all the registers from the CPU. This is not updated during the execution. - CPU scheduling information : priority, address of the next process. - Memory Information. Everything related to the mapping, e.g. base and limit registers. - Accounting information (used CPU time, time limit, …) for real time application where there is a time deadline. - List of files that are opened by this process, stored as a set of file identifiers (fid). Head Tail La file contient les PCB, chacun ayant l’adresse du PCB suivant. Une valeur spéciale signifie « end of the tail ». Le scheduling est en charge de la migration des processus entre les files d’attentes (queues). Il peut y avoir plusieurs ready queue, dépendantes des priorités ; de même, il y a plusieurs waiting queue car une pour chaque périphérique sollicitée. Ready et waiting ne sont pas deux files mais plutôt deux familles de files. Chaque file est généralement implémentée par une liste chaînée (linked list). Lorsqu’un parent fait un fork, Create new process finish le parent ou (exclusif) le fils Ready queue CPU doit aller dans la ready queue. The parent goes to Quand le segment de temps the ready queue Request I/O Time slice finished alloué à un programme vient à expires expiration et qu’on manque de I/O queue mémoire, on met le processus dans le disque et on le Is there enough place Fork a child rechargera quand on aura de la in memory to store the place ; on fait ça quand ils ont The child execute. process back in the une basse priorité, ce qu’on ready queue ? nomme le medium term yes Swap out scheduler. Celui ci-contre est Ready queue no : swap in le short time scheduler, car il Disk to the disk est invoqué souvent. P1 Save PCB0 … … Copy of PCB1 resume … Save PCB0 resume … Copy of PCB1 interrupt CPU Idle interrupt Context Switching CPU Idle P0 Il faut généralement minimiser les context switching car ils utilisent le CPU. Sur certains systèmes, le context switching est supporté par le matériel : - Machines de Sun Microsystem, où le CPU a plusieurs ensembles de registres et chaque PCB a son propre ensemble. - Une instruction spécifique de bas niveau, au lieu d’une pléiade de copy et load. Les processus sont organisés sous forme d’arbre, basé sur une relation parent-fils. Le processus faisant le fork est le parent, et celui en résultant est le fils. Shell > ls Parent process Le fils peut partager tout ou une partie des ressources du parent, ou avoir de nouvelles ressources allouées par l’OS. La définition de l’OS doit permettre ces 3 possibilités (policy). L’exécution peut se faire en foreground (on ne détache pas le processus, on attend qu’il ait fini avant de reprendre la main) : parent will wait for child termination. L’exécution peut aussi se faire en background : les processus sont en concurrences, considérés comme au même niveau, et c’est au scheduler de s’en charger (execute concurrently). Child process that will execute the command ls Execution 1. P0 is running. Fork() P1 2. P1 is ready 3. P0 goes in waiting 4. P1 starts executing 5. P0 waits for P1 to resume execution (ready list) Fork() éxecute une copie des données du parent. Fork() + execv charge un nouveau programme avec ses propres données dans l’address space du fils. pid = fork() my pid is 214 (le parent, car il a exécute l’instruction) scheduler printf(« my pid is %d\n »,pid) ; my pid is 0 (le fils, car il n’a pas fait pid = fork() !) Le scheduler décide de l’ordre de l’exécution. Ainsi, dans l’exemple précédent, l’ordre des messages est indéterminé. Pour terminer un processus, exit() l’arrête et l’OS désalloue les ressources, ou avec abord() le parent va terminer le processus fils. Si le processus parent termine, soit tous les fils s’arrêtent, soit on tente de régler le problème en mettant un nouveau parent à la racine de l’arbre. 2) CPU I/O Burst Cycles ♦ A process is CPU bound if it spends most of its time using the CPU (i.e. process uses the CPU a lot). Example : a program that calculates primes numbers; we do not need interaction with the user. It is most often in the running state, and does not have to wait for any input or access to devices. ♦ A process is I/O bound if it spends most of its time in access read/write. It is most often in waiting state. → A well balanced program alternates between CPU bound and I/O bound. CPU bound I/O bound Une importante invocation du scheduler se fait sur front haut, lorsqu’un processus a fini son attente et demande à nouveau un accès au CPU 3) Scheduling Mechanism and Criteria Généralement, le scheduler est invoqué dans 4 cas : Dans les vieux systèmes, on invoquait le scheduler - terminaison d’un processus juste sur les 2 premières situations : non-preemptive - switch de l’état running à waiting scheduling. Windows 3.x par exemple. - switch de l’état running à l’état ready - switch de l’état waiting à l’état ready dispatcher loader Les systèmes d’exploitations modernes utilisent ces 4 situations, on parle de preemptive scheduling. Les critères ont de gros impacts sur les performances du système. Context Switch : ♦ Maximiser l’usage du CPU (minimiser le idle time). ♦ Switch to supervisor mode. ♦ Maximiser le throughput (nombre de processus terminés par ♦ Save PCB1 (free CPU registers) unités de temps, par exemples 30 processus à la seconde). ♦ Load dispatcher ♦ Minimiser le turn around (temps total nécessaire pour exécuter ♦ Select a process from ready list un processus, i.e. celui passé en ready/waiting/running). ♦ Switch to user mode ♦ Minimiser le temps de réponse (entre création et 1ère exécution) ♦ Load PCB2 into the CPU ♦ Minimiser le temps d’attente (passé dans la waiting queue) Tous les critères ne peuvent être satisfaisants en même temps. En général, on n’en cherche qu’un. CPU P1 Scheduler invoked CPU Scheduler Select a process from the ready queue CPU P2 4) Scheduling Policy : First Come, First Served (FCFS) Processus CPU Burst Time Rappel : le CPU Burst Time est le temps nécessaire à un processus 24 P1 pour exécuter sa partie s’il dispose d’un plein accès au CPU. P2 3 P3 3 CPU : P1 P2 P3 Average Waiting Time : 24 27 30 0 pour process 1 24 pour process 2 Moyenne 17 23 pour Process 3 Average Response Time : (0 + 24 + 27)/3 = 17. Pareil que le temps d’attente car non-preemptive (une seule exécution pour chaque processus). Average Turn Around Time : On suppose que tous les processus ont été créées au même temps T = 0. On a alors (24 + 27 + 30)/3 = 27. Si on inverse l’ordre d’arrivée des processus, on a : Average Waiting Time (0+3+6)/3 = 3 Average Turn Around Time : (3+6+30)/3 = 13 C’est donc un système très sensible à l’ordre. On remarque de meilleurs performances en exécutant les plus courts processus d’abord, d’où l’idée de l’algorithme que nous allons voir maintenant. 5) Scheduling Policy : Shortest Job First (SJF) Chaque processus est associé avec le prochain CPU burst time. Comme on ne le connaît pas, on fait une prédiction à partir des précédents. En général, on ne conserve que les 10 dernières valeurs (on affirme que le comportement sera le même que dans une période de temps récente). On peut prendre la médiane ou la moyenne. Pour l’implémentation, il suffit de faire un tableau de taille 10 et l’ajout en (i++)%10 ; d’abord on lance FCFS le temps de recueillir assez de valeurs pour les prédictions, puis on passe en SJF. SJF réduit le average waiting time. C’est une méthode non-preemptive. 6) Scheduling Policy : Shortest Remaining Time First Si un processus Pj entre dans la ready queue avec un CPU burst time tj < temps restant ti du processus courant, alors on switch pour lancer Pj. Si un nouveau processus peut-être fini plus vite, on le lancera lui. Process P1 P2 P3 P4 Arrival Time 0 1 2 3 Predicted Burst Time 8 4 P1 9 5 0 1 CPU Chart : P2 P4 5 P1 10 P3 17 Average Waiting Time : [0 + (10 – 1) + (1 – 1) + (5 – 3) + (17 – 2)]/4 = 6,5 -----P1------- ---P2--- ---P4--- ---P3--Pour chaque processus, on regarde le temps qu’il a passé à attendre; on divise par le nombre de processus. Cet algorithme minimise le temps d’attente moyen. SJF sur le même exemple donne 7,75. Par contre, SJF fait moins de context switching : dans notre calcul du temps d’attente moyen, on considère les changements de contexte comme négligeables alors que par grands nombres ils commencent à coûter. 7) Première étude comparative Imaginons que l’on ait les trois applications suivantes lancées en même temps : - (1) traitement de texte - (2) jeu 3D - (3) calcul des nombres premiers SJF va donner une grosse priorité au jeu 3D car le burst time est très bas : un jeu 3D s’arrête très fréquemment pour les entrées/sorties (souris, clavier…) et fait rarement un moment de calcul pur. En revanche, le calcul des nombres premiers sera pénalisé par SJF alors qu’il faudrait peut être allouer plus. Par conséquent, SJF donne davantage de priorité aux processus I/O bound qu’à ceux CPU bound. L’ordre d’exécution sera 213. Si on souhaite allouer au processus qui a le plus besoin de ressources en calcul, on prend la politique longest job first (on remarque au passage qu’elle n’optimise aucun de nos ‘critères)’. 8) Principles of Priority scheduling Un nombre entier est associé à chaque processus. Cela peut représenter la priorité de façon croissante ou décroissante, cela dépend du système. On le détermine de deux façons : - Externe. Dépend juste de la nature du processus : OS, interaction, real time (temps limite auquel l’exécution doit avoir été faite), batch (aucune intéraction). - Interne. On prend des critères comme le nombre de requêtes pour la mémoire, le nombre de fichiers, ou la moyenne (I/O time)/(CPU time). Le scheduling peut se faire de deux façons : - Pre-emptive. Un nouveau processus dans la ready queue avec une plus grande priorité que celui du CPU va dégager celui du CPU. - Non-preemptive : je suis dans le CPU, j’y reste jusqu’au bout. Les processus avec une priorité basses risquent de souffrir de starvation (‘mort de faim’) car ils ne vont jamais accéder aux ressources qu’ils demandent. La solution fournie par la plupart des O/S est de faire une priorité dynamique, qui va changer durant l’exécution : c’est l’aging. La priorité des processus augmente avec le temps, pour être certain qu’ils passent un jour. Sous Linux, on parle de ‘crédits’ au lieu de priorités, mais ils sont aussi mis à jour de façon dynamique. SJF est une politique de priorité où on calcule la priorité sur la base du temps nécessaire pour la prochaine exécution (un critère interne). 26 9) Round-Robin (RR) Scheduling Politique bonne pour l’intéractivité : on se focalise sur le temps de réponse. RR a été développé pour les systèmes à partage de temps (timesharing). La ready queue est circulaire sur le principe FIFO (premier entré, premier servi). Si un processus est scheduled (i.e. qu’il utilise le CPU), on démarre un timer. Notons bien que c’est la première fois qu’on utilise un timer dans nos politiques de gestion. Après un temps ∆t (time quantum), généralement 10ms ≤ ∆t ≤ 100ms, le processus en cours est interrompu. Tous les ∆t le timer envoie un interrupt, et le processus arrêté va dans la queue de la file d’attente (à la fin). Le processus de tête est alors lancé. Process P1 P2 P3 P4 CPU Burst Time 53 P1 17 68 0 20 24 Average Waiting Time : Average Response Time : CPU Chart (∆t = 20ms) : P2 P3 37 P4 57 Timer reset P1 (0+77-20+121-97) P2 (20-0) P3 (33-0+97-57+134-117) P1 0 P2 20 P1 77 P3 37 P3 97 P4 117 P1 121 P3 134 154 P4 57 (très bas) RR fait beaucoup de context swtiching, par exemple davantage que SJF. On peut vouloir prendre en compte le temps nécessaire pour faire le changement de contexte quand on dessine la charge. Il faut en tout cas savoir qu’à peu prêt 10% du temps est passé à faire des changements de contextes. Si ∆t augmente il y a moins de context swtiching, mais cela ne sert alors plus à grand chose (si ∆t est trop haut c’est équivalent à la méthode FCFS). Avec les nouveaux processeurs, ∆t est en ms et un changement de contexte peut se faire en µs. 10) Comparaison des performances Nom de la méthode FCFS SJF RR Average Waiting Time + - Average Turn Around Time + - Average Response Time + FCFS n’est bon nul part (naïf). SJF est bon pour les processus CPU bound. RR est bon pour les processus I/O bound (i.e. interactif). La solution retenue dans la réalité n’est pas une méthode mais plusieurs : on utilise plusieurs files d’attentes avec chacun son scheduler correspondant à une méthode. 11) Multi-level queues La ready queue est divisée en plusieurs sous-queues. Par exemple : ♦ Les processus intéractifs (I/O bound, small CPU burst time) ont besoin d’un bon temps de réponse. C’est la Foreground Queue, gérée par Round Robin. ♦ Les autres processus (CPU bound, large CPU burst time) sont dans la background queue, gérée SJF. Foreground (Round Robin) Background (SJF) Mechanism for queue scheduling CPU Pour gérer les priorités des queues, il serait stupide de faire passer tout le foreground avant par exemple (problème de starvation). On fait un RR mixé avec les priorités. Remarques : dans ce modèle les processus ne sont pas autorisés à passer d’une file à l’autre, c’est peu flexible. En réalité, les processus sautent entre les queues et il faut savoir à quelle queue un processus est associée (dans windows 16 ou 32 queues). La solution la plus élaborée est le multi-level feedback queues: on peut aller up et down dans les files d’attentes, et on a un point d’entrée à la création du processus. 12) Multi-level feedback queues Un processus peut bouger entre les files. Une Multi-Level Feedback Queues (MFQ) se caractérise par : - le nombre de files - le scheduling policy pour chaque file (la façon de les gérer : round robin, SJF, …) - quand un processus peut aller en haut ou en bas dans la hiérarchie - dans quelle file un processus entre lorsqu’il a besoin du CPU Par exemple, la configuration suivant défini de façon complète une MFQ : - On a 3 files Q0, Q1, Q2. Q0 : RR (∆t = 8), Q1 : RR (∆t = 16), Q2 : FCFS. - Un processus ne va jamais en haut. - Un processus va en bas : Q0 → Q1 s’il n’a pas été terminé après ∆t = 8 Q1 → Q2 s’il n’a pas été terminé après ∆t = 16 - les processus entrent tous dans Q0 Q0 (∆t = 8) C P U Q1 (∆t = 16) Q2 (FCFS) Les processus interactifs vont rester dans Q0 voire Q1 car ils ne sont pas longs. Les processus non-interactifs vont aller dans Q3. On met des priorités entre les files, comme Q0 > Q1 > Q2 afin de gérer l’accès au CPU. On a toujours les deux gestions possibles : preemptive (relâcher le CPU pour un autre processus : problème de starvation) et non-preemptive (le processus dans le CPU se finira). Processes P1 P2 P3 P4 P5 P6 Arrival Time 0 1 2 3 4 5 CPU Burst Time 31 5 9 10 6 17 P1 (from Q0) 0 8 P5 (termine) 29 V. P2 (termine) P3 (from Q0) 13 P6 (from Q0) 35 P4 (from Q0) 21 P1 (from Q1) 43 P3 (from Q1) 59 60 Processes Synchronization 1) Exemple de problème : Bounded Buffer Des processus coopératifs doivent partager des données, donc il y a un besoin de synchronisation. Un exemple classique est celui du Bounded Buffer. Un objet produit des données pendant qu’un autre veut les retirer, le tout par l’intermédiaire d’un tableau de taille n. out Buffer Producer add Consumer The producer : while(1){ while(count==N} // wait in Problème : count est une variable partagée. } The consumer : while(1){ while(count==0} // wait // produce item // grab an item buffer[in] = item ; in = (in+1)%N ; count++ ; item = buffer[out] ; out = (out+1)%N ; count-- ; } Soit count = 5. Le producer et le consumer vont exécuter count++ et count—de façon concurrente. Quelle est la nouvelle valeur de count ? Cela peut-être aussi bien 4, 5 ou 6. Comme ce ne sont pas des instructions atomiques, il est possible qu’elles soient interrompues. Incrément et décrément se fait avec : Incrémenter : R1 = count Décrémenter : R2 = count R1 = R1 + 1 R2 = R2 – 1 count = R1 count = R2 Détaillons ce qui peut se produire lors de l’exécution du programme pour comprendre l’importance d’avoir des instructions atomiques (qui ne peuvent pas être interrompues par timer ou quoi que ce soit) : 1. P1 R1 = count [5] 2. P1 R1 = R1 + 1 [ 6 ] timer interrupt 3. P2 R2 = count [5] 4. P2 R2 = R2 – 1 [ 4 ] timer interrupt 5. P1 count = R1 [ 6 ] timer interrupt 6. P2 count = R2 [4] On arrive à count = 4, et on voit qu’avec des interrupts au timer différentes alors le résultat serait différent. 2) Solutions matérielles Pour synchroniser, un bloque un des processus si l’autre est en train d’utiliser une variable partagée ; on parle de Critical Section pour désigner la partie du code où un programme manipule des variables partagées. Un seul processus peut-être dans sa Critical Section à un moment donné. S’il y a partage de variables sans synchronisation, on parle de race condition : les processus se font la course. La première idée et d’activer et désactiver les interruptions avec un bit dans le CPU. Cela donnerait : Disable interrupts Disable interrupts Sauf si on veut donner des Count ++ // critical section Count -// critical section priorités, les solutions de Enable interrupts Enable interrupts synchro. sont symétriques Cependant, s’il y a une boucle infinie dans la critical section, alors le CPU n’est plus protégé, on crash. L’alternative est ‘test and set instruction’. Soit TS(m) avec m un emplacement mémoire, ou TS(R,m) avec R un registre et m un emplacement. On ajoute cette instruction, qui consiste à faire 3 opérations : Load R with the content of m Test the content of R (i.e. return it) Set m = true Ce qui est capital c’est que ceci est géré comme une seule instruction, elle est atomique. Un exemple : R m false Before executing TS(r,m) Exemple : false } lock = false ; P1 P1 P1 P2 P2 P2 P1 P1 // put lock back to false m true After executing TS(r,m) while(true){ While(TS(lock)) // these two lines disable interrupt P1.yield() // critical section Trace : R La Critical Section est comme une pièce fermée et le lock en est la clé : on la demande, on rentre et on ferme derrière soit ; on rend la clé en ressortant. lock → false TS(lock) ← true enter the critical section Timer interrupt TS(lock) → true lock → true yield() // relâche le CPU et on passe en ready queue lock → false CS exécutée Il y autant de critical sections que de variables partagées : ici, il y en a 2 puisque le lock en lui-même est une variable partagée. Ainsi créer des solutions à des problèmes de synchronisation peut faire intervenir d’autres CS. La dernière possibilité est l’instruction machine swap, qui peut remplacer le TS. On a swap(m1,m2) qui échange les contenus des emplacements mémoire. On prend m2 pour le lock partagé et m1 pour le lock local (pour chaque processus on a un m1). Cela nous donne la gestion suivante : Processus P1 : boolean key1 = true ; // local key While(true){ Key1 = true; do{ swap(Key1, lock) }while(key1 = true) // ces deux lignes sont à la place du yield // critical section… lock = false; } Trace : P1 is executing. Key1 = true Key2 = false Lock = true Timer interrupt P2 is executing Key2 = true Lock = true Timer interrupt P1 exits the critical section, puts lock to false Timer interrupt P2 Key2 = false, enter the critical section 3) Solution logicielle : sémaphore Un sémaphore S (aussi dit mutex, ou mutual exclusion) est une variable de type entier accessible uniquement par deux opérations atomiques : acquire (a lock : wait instruction) noté P(s), et release V(s) : Modèle pour acquire Implémentation de la boucle While(s ≤ 0){ ; } // busy wait loop while(s ≤ 0){ s-- ; // atomique EnableInterrupt() ; // des interrupts sont autorisés dans la boucle DisableInterrupt() ; } L’instruction release, notée V(S) est uniquement s++. On la nomme signal instruction. Un exemple : Semaphore mutex = 1 ; P1 : while(true){ P2 : while(true){ Acquire(mutex); // mutex.P() Acquire(mutex); } // critical section // critical section Release(mutex); // mutex.V() Release(mutex); } Si l’on écrit le code dans un ordre différent, on fait des priorités. Par exemple, A s’éxécutera avant B : Semaphore mutex = 0 ; P1 : A P2 : Acquire(mutex) Release(mutex) B Si les processus s’attendent l’un l’autre, il y a blocage (deadlock). C’est le problème le plus classique lorsqu’on veut gérer la concurrence. Le problème devient encore plus compliqué lorsqu’il y a deux classes, chacune écrite par un programmeur différent. On ne peut pas savoir qui met quoi dans quel ordre. P1 : Semaphore s = 1, q = 1 ; Acquire(s) Acquire(q) P2 : // critical section // critical section Release(s) Release(q) Acquire(q) Acquire(s) Release(q) Release(s) Dans cette configuration là, la seule solution est d’utiliser un AND qui fait l’opération d’acquisition simultanément sur plusieurs sémaphores. Ainsi, chaque programmeur n’a pas à se soucier de l’ordre dans lequel son collègue a écrit sa classe. Si s2{0, 1} alors on parle de sémaphore binaire. Si s2N, c’est un counting semaphore. L’implémentation est exactement la meme. La difference en terme d’application est que plusieurs personnes peuvent avoir accès à la section critique, alors que le binaire n’en autorise qu’une. Exemple : out Buffer Producer add Semaphore mutex = 1 ; Semaphore full = 0 ; Semaphore empty = n ; Producer : in Consumer Le buffer est une critical section pour lequel on utilise un sémaphore binaire. On utilise aussi un counting semaphore count pour le nombre d’objets en buffer. If(count==0) BlockConsumer() // semaphore empty Else if(count == n) BlockProducer() // semaphore full // le buffer est libre // counting semaphore : au départ aucune cellule n’est pleine // un autre counting semaphore while(true){ Produce nextItem Acquire empty // n – 1 Acquire mutex Add NextItem to buffer Release Mutex Release Full } Consumer : while(true){ Acquire Full Acquire mutex Consume item Release mutex Release Empty } Un problème classique est celui des philosophes qui dînent, pour illustrer les deadlock. On a 5 philosophes et 5 baguettes. Chaque philosophe a besoin de deux baguettes pour manger ses nouilles, puis il les repose pour qu’un autre les utilise, et pendant ce temps-là il pense. Le but est de synchroniser… Les philosophes sont des processus, les baguettes des sémaphores et les nouilles la critical section. Chaque philosophe a une baguette à sa gauche et une à sa droite. La table est un cercle. Ainsi le philosophe 1 a accès aux baguettes 1 et 2, le philosophe 2 aux baguettes 2 et 3, …, le philosophe 5 aux baguettes 5 et 1. Le code pour résoudre le problème est le suivant : While(true){ AND(acquire(chopstick[i]),chopstick[(i+1)%5])) ; // eat Release(chopstick[i]); Release(chopstick[(i+1)%5]); // think } Le problème des readers/writers : on a un fichier qui est une ressource, accédée par certains processus en lecture et en écriture. On autorise plusieurs readers à accéder le fichier, mais s’il y a un processus en train d’écrire dans le fichier alors personne ne doit pouvoir y accéder. On a donc deux situations : plusieurs readers ou un seul writer. Mettons en place une 1ère approche par sémaphores. On a deux types de processus, chacun avec son code: Semaphore write_block = 1 ; // initially we are not doing any block at all int read_count = 0; // count how many readers we have Writer : while(true){ Reader : while(true){ acquire(write_block) read_count++; // write to the file if(read_count == 1) acquire(write_block); release(write_block) // read from the file } read_count--; if(read_count==0) release(write_block); } On veut savoir si ce qu’on a écrit est bon. Est-ce que toutes les variables partagées sont correctement gérées ? Non : read_count est une variable partagée entre les readers, et il faut aussi la synchroniser ! On définit un nouveau sémaphore, ce qui entraîne quelques modifications sur le code du reader : Semaphore mutex = 1 ; Faisons une trace de l’exécution. Arrivent R1, R2, W1, R3. while(true){ R1 : read_count = 1, write_block = 0, read <timer> acquire(mutex) ; R2 : read_count = 2, read <timer> read_count++ ; W1: write_block is 0 so we are blocked <timer> if(read_count == 1) acquire(write_block) ; R3 : read_count = 3, read <timer> release(mutex); R1 : finished, read_count = 2 // read from the file R2 : finished, read_count = 1 acquire(mutex); R3 : finished, read_count = 0 read_count--; W1 : write ! if(read_count == 0) release(write_block); On voit qu’un writer doit attendre tous les readers, ce qui release(mutex); n’est pas génial… On va utiliser des monitors plutôt. } 4) Solution logicielle : monitor Un monitor est implémenté comme un Abstract Data Type pour lequel sont définies des données privées et des méthodes publiques. Les processus font appels aux méthodes du monitor. Un monitor n’autorise qu’un seul processus à un moment donné, i.e. plusieurs processus ne peuvent pas exécuter une ou différentes méthodes du monitor à un moment donné ; on parle de Mutual Exclusion Principle. Exemple : monitor shared count { int count ; public entry increment(){ count++ ; } public entry decrement(){ count-- ; } } Les variables de condition sont un type de données qui apparaît seulement dans les monitors. On peut y accéder par trois opérations : - wait (équivalent de Acquire). On suspend le processus qui appelle, et on le met dans une file associée avec la variable de condition. On relâche le monitor. On attend un signal call (on est dans le wait state est un autre processus va nous faire revenir en ready grâce à son signal). - Signal (équivalent de Release). On reprend uniquement un processus de la file associée ; s’il n’y a aucun processus en attente, alors il n’y a pas d’effet (différence importante avec release). - Queue. Retourne vrai s’il y a au moins un processus en attente, et faux sinon. Exemple : soit A dans P1 et B dans P2 tel que A s’exécute avant B, en utilisant des variables de condition. Condition okgo ; P1 : A P2 : okgo.wait() Okgo.signal() B Cependant, il y a un problème ici. On part du principe que P2 s’exécute d’abord, il se met en attente, A est fait et va libérer P2. Cependant, si P1 exécute en premier, alors il va finir et P2 va se mettre en attente pour un signal qui ne viendra jamais… De même, analysons la solution suivante au problème readers/writers : monitor readerWriter_1 { int numberOfReaders = 0; int numberOfWriters = 0; boolean busy = FALSE; public: startRead() { while(numberOfWriters != 0) ; numberOfReaders++; }; finishRead() { numberOfReaders--; }; startWrite() { numberOfWriters++; while(busy || (numberOfReaders > 0)) ; busy = TRUE; }; finishWrite() { numberOfWriters--; busy = FALSE; }; }; Analyse sur la séquence R1 R2 W1 R3 R1 : numberOfReader = 1, read R2 : numberOfReader = 2, read W1 : numberOfWriters = 1, wait R3 : wait R1 : done, numberOfReader = 1 R2 : done, numberOfReader = 0 W1 : writes, numberOfWriters = 0 R3 : numberOfReader = 1, numberOfReader = 0 C’est mieux dans le sens où le writer n’a pas à attendre tous les readers… Cependant, cette solution est théoriquement fausse car un seul processus peut exécuter dans le monitor. Ainsi, quand W1 est dans le while loop et puisque cette boucle est dans le monitor alors on a un deadlock. monitor readerWriter_2 { int numberOfReaders = 0; boolean busy = FALSE; condition okToRead, okToWrite; public: startRead() { if(busy || (okToWrite.queue()) okToRead.wait(); numberOfReaders++; okToRead.signal(); }; finishRead() { numberOfReaders--; if(numberOfReaders == 0) okToWrite.signal(); }; startWrite() { if((numberOfReaders != 0) || busy) okToWrite.wait(); busy = TRUE; }; finishWrite() { busy = FALSE; if(okToRead.queue()) okToRead.signal() else okToWrite.signal() }; }; On utilise donc la solution améliorée ci-contre. Regardons son fonctionnement sur R1R2W1R3 : R1. if(busy||okToWrite.queue()) est false. NbReader = 1. OkToRead.signal() sans effet. READ R2. NbReader = 2. READ W1. NbReader ≠ 0, waiting queue of okToWrite, invoke scheduler. R3. okToWrite() est vrai, waiting queue de okToRead R1. FINISH READ. NbReader = 1. R2. FINISH READ. NbReader = 0. Signal the writer W1. W1. busy = true. WRITE. On entre dans finishWrite(), busy = false, signal the reader R3. R3. READ. On entre dans finishRead(). nbReader = -1. Que se passe-t-il lorsque R2 appelle okToWrite.signal() ? Il y a deux possibilités d’implémentations : - On fait un context switch, R2 va dans la waiting queue et W1 passe à l’exécution. Il y a un context-switch lorsque W1 termine, R2 exécute et termine immédiatement, re-context switch. La condition « nombre de readers à 0 » peut ne pas être vrai indéfiniment : un changement de contexte immédiatement nous permet de certifier que la condition qu’on veut signaler est toujours vraie. C’est la méthode « signal et wait », qui a besoin de plus de context switches. - R2 termine et passe en terminated state. On fait un context switch à W1. La condition n’est pas re-testée comme c’était un if, mais entre temps le nombre de readers peut être différent de 0. Solution est plus rapide mais plus risqué. Méthode signal & continue. Utiliser un while, pas if. VI. Memory Managment 1) Les différents moments où faire la liaison Le programme de l’utilisateur doit passer par plusieurs étapes avant l’exécution. L’objectif est de comprendre comment les adresses sont transformées durant ces étapes. Les adresses des variables pointent sur des adresses physiques : c’est l’address binding. Un programme dans le disque peut-être chargé plusieurs fois en mémoire (dû au swap par exemple) et ainsi une même donnée peut se retrouver à des endroits différents lors de chaque chargement, puisqu’on a un Random Access Memory. Le but de l’address binding est de s’assurer que les adresses dans le programme correspondent aux adresses physiques : le address binding peut se passer lors de chacune des étapes. Dans les systèmes d’exploitations modernes, on préfère repousser l’address binding jusqu’au moment de l’exécution, ce qui nécessite un support matériel (contrairement aux 2 étapes précédentes). Code Source Compile Time 1 Compilateur Relocatable Object Code Load Time 2 Object Modules Linker Static System Library Load Module Loader Execution Time 3 In Memory Executable Dynamic Linking (Runtime Libraries) Address Binding at Compile Time. Il faut connaître les emplacements mémoires où le programme va être chargé, sinon il est impossible d’écrire une adresse physique. Dès qu’on change d’emplacement, il faut recompiler le programme. // starting address Exemple : int gvax ; 1000 … int proc1(int arg){ 1008 entry proc_a gvar = 7 ; … put_record(gvar) ; 1220 load R1, 7 } 1224 store R1, 3004 // address de la variable // address de la routine 1228 call 2334 Physical Address = Relative Address … 2334 entry put_record C’est le type de liaison utilisé dans MS-DOS … pour les exécutables .com servant à faire les 3004 [gvar space] commandes. ↑ relative addresses Le chargement du programme se fera ici à partir de l’emplacement mémoire 1000 : il n’y a pas le choix. Address Binding at Load Time. L’emplacement mémoire d’où on charge est inconnu. On génère des relocatable code lors de la compilation. Dans l’adresse relative, on affirme que le programme commence à 0000. On aura donc store R1 2004, call 1334, etc. On parle ici d’un espace d’adresse logique (logical address space), qui démarre donc à 0. Lorsque le programme est chargé, on met à jour l’adresse relative (en utilisant une base address : on l’ajoute) et on obtient le physical address space (les adresses réelles). Address Binding at Execution Time. Les programmes peuvent bouger d’un endroit à un autre dans la mémoire, donc on fait la liaison au dernier moment : lors de l’exécution ; comme on l’a vu, il faut un support matériel. P1 (110K) Les programmes bougent en mémoire afin de garantir une utilisation optimale. Mettons qu’à un temps t1 le programme P2 termine. Il n’y a pas de bloc continue de plus de 30 Ko, ce qui peut être gênant : on choisit de faire bouger le nouvel espace libre à la fin, et on réduit la fragmentation. Il faut alors qu’on décale tous les autres programmes… Ainsi, bouger les programmes en mémoire intervient lorsqu’on veut réduire la fragmentation. P3 (75K) P2 (20K) P4 (80K) Free (32K) L’adresse logique est celle générée par le CPU, et l’adresse physique est celle reçue par la mémoire. Call 1334 CPU 1334 MMU Adresse logique Adresse physique Le Memory Mapping Unit ajoute le base register (version simplifiée du vieux MMU sur Intel 8086). Si l’adresse logique = l’adresse physique, alors la liaison s’est faite à la compilation. Sinon, la liaison est faite au load time ou à l’execution time. Dynamic Linking (performed at run time). La liaison est reportée jusqu’à l’exécution pour : - Sauver de la mémoire et de l’espace disque - Pouvoir mettre à jour les librairies utilisées sans avoir à recompiler tous les programmes qui les utilisent. Attention : le stub call doit être exécuté en #include <system library> load module supervisor mode pour être capable d’accéder … … compiled à l’espace mémoire d’autres programmes. Il call system routine 1 call stub function y a besoin d’un support de l’OS. … Des systèmes d’exploitations entry of the stub comme windows permettent if the called routine is already in the memory Un stub désigne d’avoir plusieurs versions des execute a call to this routine une fonction qui librairies en mémoire ; le else load the routine in the process address space fait l’intermédiaire numéro de version identifie. 2) Allocation mémoire continue Un processus se voit allouer un bloc de mémoire contiguë. Il y a deux méthodes pour le faire. Multiple Partition Method. Les blocs sont déjà calculés et à tailles fixes (on trouve de 4 Kb à 4 Mo…). Les blocs ont des tailles en puissance de 2 (puisque la mémoire est elle-même une puissance de 2).Un trou (hole) est un ensemble de partitions libres (i.e. pas encore utilisés par un processus). Si un processus fait une demande d’espace, on lui trouve des partitions dans un trou qui est ‘assez grand’. OS loaded in low memory 4K P5 4K P8 12K P2 4K Si P8 termine, on a un trou de 12k. Mettons que P10 arrive à ce moment là, on va le stocker dans ce trou s’il fait moins de 12k. Il demande 7k, donc on lui donne la quantité de blocs nécessaires, soit 8k. On voit que : Mémoire allouée ≥ Mémoire demandée Si quelqu’un veut alors 5k, on lui chercher 8k mais on ne les a pas. Alors qu’en pratique P10 nous laisse encore 1k de libre et qu’on a un bloc, ça pourrait loger… La mémoire dans un bloc non utilisée par un processus (comme le 1k de P10) est dite fragmentation interne : on ne peut pas l’allouer à d’autres processus, elle est perdue. Plus grande est la taille des partitions et plus il y a de fragmentation interne, mais plus il y a de partitions et plus l’address binding a un coût élevé. Variable Partition Method. Mémoire allouée = mémoire demandée. La stratégie d’allocation est : un processus demande un bloc mémoire de taille n, et on essaye de l’avoir à partir d’un trou. Soit n = 15 avec des trous n1 = 10k, n2 = 20k, n3 = 16k, n4 = 40k. On a plusieurs stratégies : minimiser la taille des trous restants (on prend n3), maximiser (on prend n4)… On les nomme comme suit : - First Fit Strategy : allouer le premier trou assez grand (soit n2). Rapide. - Best Fit Strategy : allouer le plus petit trou assez grand pour contenir le processus (soit n3). Avantage : on minimise la mémoire laissée (left over), on tente d’optimiser son utilisation. - Worst Fit Strategy : allouer le plus grand trou (soit n4), afin que le nouveau trou restant soit assez grand pour pouvoir contenir d’autres processus. Il n’y a pas de fragmentation interne mais une fragmentation externe : la mémoire n’est pas perdue parce qu’elle est demandée par un processus qui n’en fait rien, mais parce qu’il reste des miennes inutilisables. 3) Memory Paging On subdivise un processus en plusieurs blocs. La mémoire logique d’un processus est ainsi coupée en blocs de tailles fixes appelés pages. La mémoire physique est aussi divisée en blocs de tailles fixes appelés frames. La taille d’une page est la même que la taille d’une frame (ceci est une assertion cruciale). Process Page number 00 01 Frame Number 000 001 10 11 010 011 100 101 110 111 Memory Pour lancer un processus de n pages, on a besoin de trouver n frames en mémoire : chaque page est chargée dans une frame. Les frames n’ont pas besoin d’être continues. La fragmentation sera interne car il faut exprimer la taille du processus en fonctions d’un nombre de pages à taille constante. Pour garder la trace des frames allouées à un processus, on utilise une fonction de mappage implémentée dans une lookup table (LUT). Call logical address La traduction d’une adresse logique dans une page en adresse physique dans la mémoire se fait avec : Physical(page number . offset) = LUT(page number) . offset CPU Adresse logique 0000 a 000… xx.yy b 0001 offset 01 Adresse logique page nbr.offset 001… 0010 ijkl c d 0011 Page nbr Offset Adresse physique 010… mnop 0100 e xxx.yy 0101 f Frame nbr.offset g 0110 011.. LUT h 0111 1000 i 100.. 1001 j 00 101 k 1010 101… 01 110 Frame nbr Offset 1011 l 10 001 abcd 110… m 1100 11 010 n 1101 Adresse physique LUT e f g h o 1110 111… p 1111 Mémoire Frame Number Adresse Logique Translation faite par le MMU (pages de 4b) Considérons que nous avons un processeur 32 bits. La taille d’une page est mise à 4Kb = 212 bits. Quelle est la taille de l’offset d ? 12. Quelle est la taille du page number p ? 32 – 12 = 20 bits sont libres pour écrire p, soit 220 entrées en LUT. Problème : on veut avoir quelque chose de rapide dans le LUT, donc on utilise des registres. Ici, on a besoin de 1 Mo en registres, ce qui coûte extrêmement cher… Pour avoir une facture modérée sur le matériel, on va de 256 à 1024 registres. On a donc le choix entre 2 implémentations pour la LUT : - Soit des registres rapides dédiés, mis à jour à chaque changement de contexte (puisqu’ils représentent un processus). Problème : il en faut 220 et même 252 sur processeur 64 bits, $$$ ! - La LUT de chaque processus est gardée en mémoire dans l’espace mémoire du processus. On a besoin d’un pointeur (page table base register) pour connaître où la table se trouve. Ce pointeur vers l’emplacement mémoire du LUT est contenu dans le MMU. Pas coûteux du tout. OS P1 LUT for Process 1 P2 LUT for Process 2 Address 1 Address 2 Si le processus 1 est en exécution, PTBR contient address 1. On fait un context switch, le processus 2 se met en route : PTBR contiendra address 2, etc. L’adresse de la table est stockée dans le PCB. CPU → a = page number p . offset d → *(*(PTBR + p) + d) On fait un accès à la zone de la LUT et on se décale pour avoir la bonne page, soit PTBR + p. On regarde où cela pointe pour avoir la frame, et on rajoute l’offset soit *(PTBR +p) + d. Finalement, on va à l’adresse mémoire que cela pointe. Il y a donc deux indirections, ce qui ralentit la mémoire par deux… Un autre problème se pose : la mémoire est fragmentée, et le LUT est assez gros. S’il se retrouve fragmenté lui aussi, alors il faudra une autre table pour stocker sa propre fragmentation et c’est sans fin. Ce qui impose donc qu’il nous faut un LUT non fragmenté. On choisit un compromis : on stocke 64 à 256 page entries dans un ensemble de registres dédiés, nommé le TLB (translation look-aside buffer : ce n’est pas un lookup table), en mettant dedans les pages les plus fréquemment utilisées. Toutes les entrées sont stockées dans la mémoire. Si la page qu’on cherche n’est pas dans le TLB (TLB miss) alors on va la prendre dans la mémoire et on la met dans le buffer (à la place de la page la moins utilisée) ; si la page qu’on cherche est dans le TLB (TLB hit), on l’utilise et on incrémente son utilisation. Le TLB est implémentée de façon matérielle comme un tableau. TLB update 2 7 3 4 1 50 p = 10 d = 10 Mémoire CPU OS 3 5 7 4 0 1 2 3 frame page TLB miss Shift 2 1 5 3 4 30 50 Frame number Page number Frequence A Translation Lookaside Buffer (TLB) is a cache in a CPU that is used to improve the speed of virtual address translation. A TLB has a fixed number of entries containing parts of the page table which translate virtual addresses into physical addresses. It is typically a contentaddressable memory (CAM), in which the search key is the virtual address and the search result is a physical address. Exemple : pour un processus donné, on a 4 pages chacun de 4 bites. On a d = 2 (car 2 bits sont nécessaires pour accéder à 4 bites) et p = 2 (car 2 bits sont nécessaires pour accéder à 4 pages). Comme c’est un exemple, on se permet de ne pas avoir |p| = |f|, mais on rappelle qu’il le faut dans la réalité. 4) Nouvelle génération d’algorithmes de paging : hierarchical paging On a un livre de 1000 pages, et on décide de le diviser en 10 chapitres de 100 pages. Si on a un processeur de 32 bits et une taille des pages de 4 Kb, un single paging serait [p : 20 bits ; d : 12 bits]. Dans le hierarchical paging, chaque numéro de page est lui-même pagé (similaire à la création de chapitres). Ainsi, on divise p : [p1 : 10 bits page number ; p2 : 10 bits page offset ; d : 12 bits]. Cette solution est celle implémentée dans le Vax et le Pentium 2. a CPU p d b Appliquer un 00 2 000 2 f d 01 1 001 1 décalage de 01 10 4 010 4 p1 p2 11 7 011 7 0 address 1 100 0 1 address 2 mémoire 101 6 p1 110 5 111 3 Cependant, sur une machine 64 bits on se retrouverait avec un p1 de 42 bits ce qui est trop p1 p2 f grand car les registres sont trop chers. Il faut alors augmenter le niveau de hiérarchie, ce qui réduit d’autant la vitesse de la mémoire…. Ainsi, on va introduire les hashed page tables, et le inverted page tables, spécialement pour les architectures 64 bits. 5) Inverted Page Table Si chaque processus a sa Page Table, alors on a besoin de devoir changer toutes les entrées de la Page Table en mémoire lorsqu’on change de processus. Ca nécessite du temps, et c’est d’une utilité assez faible puisqu’un processus a peu de chances d’utiliser toute la mémoire. Ainsi, on décide d’avoir une seule table où les entrées sont les frames au lieu d’être les pages (d’où l’idée d’une table inversée). P1 PID Page Number PID, adresse a CPU On réduit les temps pour les changements de contextes et la mémoire nécessaire, mais la recherche peut être coûteuse : on fait intervenir un TLB pour éviter une recherche trop chère. … Frame 1 Pn Frame 2n-1 PID Page Number Offset Search table Frame Number Offset 6) Partage de pages Editor1 Editor2 Editor3 Data1 1 2 4 6 Editor1 Editor2 Editor3 Data1 1 2 4 7 Mémoire physique → 1 2 Editor1 Editor2 3 4 5 6 7 Editor3 Editor6 Editor7 On veut partager certaines pages. Par exemple, un petit éditeur de texte est partagé par deux processus, mais chacun a ses données propres. Comment peut-on garantir qu’il y a bien sécurité, en différenciant bien les données qui sont partagées du reste ? Comment donner une sémantique à ce partage ? On va introduire les segments. 7) Segmentation Il s’agit d’un schéma de gestion mémoire qui permet de donner un sens à un processus : on le considère comme une collection d’unités logiques appelés segments, tels que le programme principal, les routines, les objets, les variables globales… Les segments (de 1 routines 1 taille variable) sont Les segments System chargés d’un seul sont numérotés Library 3 2 bloc en mémoire main 3 à la compilation Sqrt 4 stack (i.e. aucun paging) Process A la compilation, l’appel à une routine est traduit en « call <segment number 2, offset d> » où la routine est à la dième entrée. Pour chaque segment, on utilise un base register et un limit register : le segment appelé est transformé en la base, et la partie interne est l’offset (dont on vérifie qu’il est inférieur à la limite sinon on dépasse du segment : segmentation fault !). CPU a Segment number Base Value On a une segment table propre à chaque processeur, et elle change donc s’il y a changement de contexte. Cette solution a été développée avant le memory paging, mais on peut trouver les deux combinées, par exemple dans le Pentium 2. Offset Limit Value < b