Page suivante Page précédente Table des matières

6. Traitement MultiTâche Linux

6.1 Vue d'ensemble

Cette section va analyser les structures de données -- le mécanisme utilisé pour contrôler l'environnement de traitement multitâche sous Linux.

États de Tâche

Une Tâche Linux peut avoir (can be) un des états suivants (selon [ include/linux.h ]):

  1. TASK_RUNNING, signifie qu'elle est dans "la liste prête" ("Ready List")
  2. TASK_INTERRUPTIBLE, Tâche attendant un signal ou une ressource (sommeil)
  3. TASK_UNINTERRUPTIBLE, Tâche attendant une ressource (sommeil), elle est dans la même "file d'attente d'attente"
  4. TASK_ZOMBIE, enfant de Tâche sans père
  5. TASK_STOPPED, Tâche à corriger (task being debugged)

Interaction Graphique

       ______________     CPU Disponible    ______________
      |              |  ---------------->  |              |
      | TASK_RUNNING |                     | Real Running |  
      |______________|  <----------------  |______________|
                           CPU Occupée
            |   /|\       
  Attend     |    | Ressource  
 Ressource   |    | Disponible            
           \|/   |      
    ______________________                     
   |                      |
   | TASK_INTERRUPTIBLE / |
   | TASK-UNINTERRUPTIBLE |
   |______________________|
 
                     Flot Multitâche Principal

6.2 Timeslice (glissement de temps)

Programmation PIT 8253

Toutes les 10ms (selon la valeur de HZ) un IRQ0 se produit, qui nous aide en environnement multitâche. Ce signal vient de PIC 8259 (en arch 386+) qui est relié à PIT 8253 avec une horloge de 1,19318 mégahertz.

    _____         ______        ______        
   | CPU |<------| 8259 |------| 8253 |
   |_____| IRQ0  |______|      |___/|\|
                                    |_____ CLK 1.193.180 MHz
          
// From include/asm/param.h
#ifndef HZ 
#define HZ 100 
#endif
 
// From include/asm/timex.h
#define CLOCK_TICK_RATE 1193180 /* Underlying HZ */
 
// From include/linux/timex.h
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */
 
// From arch/i386/kernel/i8259.c
outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */ 
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
 

Ainsi nous programmons 8253 (PIT, Programmable Interval Timer ou Compteur à intervalle programmable) avec LATCH (VERROU) = (1193180/hz) = 11931,8 quand HZ=100 (défaut). LATCH indique le facteur divisant la fréquence.

LATCH = 11931,8 donne à 8253 (en sortie) une fréquence de 1193180/11931,8 = 100 Hz, ainsi la période = 10ms

Ainsi Timeslice = 1/hz.

Avec chaque Timeslice nous interrompons temporairement l'exécution du processus courant (sans commutation de Tâche), et nous faisons du ménage, après quoi nous retournerons de nouveau à notre processus précédent.

Linux Timer (Compteur) IRQ ICA

Linux Timer IRQ
IRQ 0 [Timer]
 |  
\|/
|IRQ0x00_interrupt        //   wrapper IRQ handler
   |SAVE_ALL              ---   
      |do_IRQ                |   wrapper routines
         |handle_IRQ_event  ---
            |handler() -> timer_interrupt  // registered IRQ 0 handler
               |do_timer_interrupt
                  |do_timer  
                     |jiffies++;
                     |update_process_times  
                     |if (--counter <= 0) { // if time slice ended then
                        |counter = 0;        //   reset counter           
                        |need_resched = 1;   //   prepare to reschedule
                     |}
         |do_softirq
         |while (need_resched) { // if necessary
            |schedule             //   reschedule
            |handle_softirq
         |}
   |RESTORE_ALL
 

Des fonctions peuvent être trouvées sous:

Notes:

  1. La fonction "IRQ0x00_interrupt" (comme d'autres IRQ0xXY_interrupt) est directement dirigée par IDT (Interrupt Descriptor Table ou Tableau de descripteur d'interruption, semblable au Real Mode Interrupt Vector Table, voir chap 11), ainsi CHAQUE interruption arrivant au processeur est contrôlée par la routine "IRQ0x#NR_interrupt", où #NR est le numéro d'interruption. Nous nous référons à elle en tant que "gestionnaire d'irq d'emballage" ("wrapper irq handler").
  2. des routines d'emballage sont exécutées, comme "do_IRQ","handle_IRQ_event" [arch/i386/kernel/irq.c].
  3. Après ceci, la main est passée à la routine IRQ officielle (pointée par "handler()"), précédemment enregistré avec "request_irq" [arch/i386/kernel/irq.c], dans ce cas "timer_interrupt" [arch/i386/kernel/time.c].
  4. la routine "timer_interrupt" [arch/i386/kernel/time.c] est exécutée, et quand elle se termine,
  5. le contrôle revient à des routines assembleur [arch/i386/kernel/entry.S].

Description:

Pour gérer le Multitâche, Linux (comme chaque autre Unix) utilise un ''compteur'' variable pour suivre combien de CPU (processeur) a été utilisée par la Tâche. Ainsi, à chaque IRQ 0, le compteur est décrémenté (point 4) et, quand il atteint 0, nous devons commuter la Tâche de gérer le temps partagé (point 4 la variable "need_resched" est mise à 1, puis, au point 5 les routines assembleur contrôlent "need_resched" et appellent, si besoin, "le programme" [kernel/sched.c]).

6.3 Programmateur

Le programmateur est le bout de code qui choisit quelle Tâche doit être exécutée à un moment donné (chooses what Task has to be executed at a given time).

A chaque fois que vous devez changer la Tâche courante, choisissez un candidat. Ci-dessous il y a la fonction ''programme [kernel/sched.c]''.

|schedule
   |do_softirq // manages post-IRQ work
   |for each task
      |calculate counter
   |prepare_to__switch // does anything
   |switch_mm // change Memory context (change CR3 value)
   |switch_to (assembler)
      |SAVE ESP
      |RESTORE future_ESP
      |SAVE EIP
      |push future_EIP *** push parameter as we did a call 
         |jmp __switch_to (it does some TSS work) 
         |__switch_to()
          ..
         |ret *** ret from call using future_EIP in place of call address
      new_task

6.4 Moitié inférieure, files d'attente de Tâche et Tasklets

Vue d'ensemble

En Unix classique, quand un IRQ se produit (par un périphérique), Unix fait la "commutation de Tâche" pour interroger la Tâche qui a demandé le périphérique.

Pour améliorer les performances, Linux peut remettre le travail non urgent à plus tard, pour mieux gérer la grande vitesse des évènements.

Ce dispositif est géré depuis le noyau 1.x par "la moitié inférieure" (BH pour Bottom Half). Le gestionnaire d'irq "marque" une moitié inférieure, pour être exécuté plus tard, le temps de programmé (scheduling time).

Dans les derniers noyaus il y a une "queue de tâche" qui est plus dynamique que BH et il y a aussi une petite tâche ("tasklet") pour gérer les environnements multiprocesseur.

Le schéma BH est:

  1. Déclaration
  2. Marque
  3. Exécution

Déclaration

#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q)
#define LIST_HEAD(name) \
   struct list_head name = LIST_HEAD_INIT(name) 
struct list_head { 
   struct list_head *next, *prev; 
};
#define LIST_HEAD_INIT(name) { &(name), &(name) } 
 
      ''DECLARE_TASK_QUEUE'' [include/linux/tqueue.h, include/linux/list.h] 

La macro "DECLARE_TASK_QUEUE(q)" est utilisée pour déclarer une structure appelée file d'attente de tâche qui gère "q" (managing task queue).

Marque

Voici le schéma Ica pour la fonction "mark_bh" [include/linux/interrupt.h]:

|mark_bh(NUMBER)
   |tasklet_hi_schedule(bh_task_vec + NUMBER)
      |insert into tasklet_hi_vec
         |__cpu_raise_softirq(HI_SOFTIRQ) 
            |soft_active |= (1 << HI_SOFTIRQ)
 
                   ''mark_bh''[include/linux/interrupt.h]

Par exemple, quand un gestionnaire d'IRQ veut "remettre" du travail, il ferait "mark_bh(NOMBRE)", où NOMBRE est un BH déclaré (voir la section précédente).

Exécution

Nous pouvons voir ceci appelé par la fonction "do_IRQ" [arch/i386/kernel/irq.c]:

|do_softirq
   |h->action(h)-> softirq_vec[TASKLET_SOFTIRQ]->action -> tasklet_action
      |tasklet_vec[0].list->func
         

"h->action(h);" est la fonction qui a été précédemment alignée.

6.5 Routines de très bas niveau

set_intr_gate

set_trap_gate

set_task_gate (non utilisé).

(*interrupt)[NR_IRQS](void) = { IRQ0x00_interrupt, IRQ0x01_interrupt, ..}

NR_IRQS = 224 [kernel 2.4.2]

6.6 Commutation de Tâche

Quand la commutation de Tâche se passe-t-elle?

Maintenant nous allons voir comment le noyau de Linux permute d'une Tâche à l'autre.

La permutation de Tâche est nécessaire dans beaucoup de cas, comme le suivant:

Commutation de Tâche

                           TASK SWITCHING TRICK
#define switch_to(prev,next,last) do {                                  \
        asm volatile("pushl %%esi\n\t"                                  \
                     "pushl %%edi\n\t"                                  \
                     "pushl %%ebp\n\t"                                  \
                     "movl %%esp,%0\n\t"        /* save ESP */          \
                     "movl %3,%%esp\n\t"        /* restore ESP */       \
                     "movl $1f,%1\n\t"          /* save EIP */          \
                     "pushl %4\n\t"             /* restore EIP */       \
                     "jmp __switch_to\n"                                \
                     "1:\t"                                             \
                     "popl %%ebp\n\t"                                   \
                     "popl %%edi\n\t"                                   \
                     "popl %%esi\n\t"                                   \
                     :"=m" (prev->thread.esp),"=m" (prev->thread.eip),  \
                      "=b" (last)                                       \
                     :"m" (next->thread.esp),"m" (next->thread.eip),    \
                      "a" (prev), "d" (next),                           \
                      "b" (prev));                                      \
} while (0)

L'astuce c'est:

  1. ''pushl %4'' qui met future_EIP dans la pile
  2. ''jmp __switch_to'' qui exécute la fonction''__switch_to'', mais au contraire de ''call'' nous reviendrons à la valeur suivante (to valued pushed in point 1) du poiint 1 (donc une nouvelle tâche!)
      U S E R   M O D E                 K E R N E L     M O D E

 |          |     |          |       |          |     |          |
 |          |     |          | Timer |          |     |          |
 |          |     |  Normal  |  IRQ  |          |     |          |
 |          |     |   Exec   |------>|Timer_Int.|     |          |
 |          |     |     |    |       | ..       |     |          |
 |          |     |    \|/   |       |schedule()|     | Task1 Ret|
 |          |     |          |       |_switch_to|<--  |  Address |
 |__________|     |__________|       |          |  |  |          |
                                     |          |  |S |          | 
Task1 Data/Stack   Task1 Code        |          |  |w |          |
                                     |          | T|i |          |
                                     |          | a|t |          |
 |          |     |          |       |          | s|c |          |
 |          |     |          | Timer |          | k|h |          |
 |          |     |  Normal  |  IRQ  |          |  |i |          | 
 |          |     |   Exec   |------>|Timer_Int.|  |n |          |
 |          |     |     |    |       | ..       |  |g |          |
 |          |     |    \|/   |       |schedule()|  |  | Task2 Ret|
 |          |     |          |       |_switch_to|<--  |  Address |
 |__________|     |__________|       |__________|     |__________|
 
Task2 Data/Stack   Task2 Code        Kernel Code  Kernel Data/Stack

6.7 Fourche

Vue d'ensemble

La fourche est utilisée pour créer une autre Tâche. Nous commençons par une Tâche Parent, et nous copions plusieurs structures de données vers l'Enfant de la Tâche.

 
                               |         |
                               | ..      |
         Task Parent           |         |
         |         |           |         |
         |  fork   |---------->|  CREATE |   
         |         |          /|   NEW   |
         |_________|         / |   TASK  |
                            /  |         |
             ---           /   |         |
             ---          /    | ..      |
                         /     |         |
         Task Child     / 
         |         |   /
         |  fork   |<-/
         |         |
         |_________|
              
                       Fork SysCall

Ce qui n'est pas copié

La nouvelle Tâche juste créée (''Enfant de Tâche '') est presque égale au parent (''Parent de Tâche''), il y a seulement quelques différences:

  1. évidemment le PID
  2. l'enfant ''fork()'' renverra 0, alors que le parent ''fork() ''renverra le PID de la Tâche Enfant, pour les distinguer en Mode Utilisateur
  3. Toutes les pages de données de l'enfant sont marquées ''LECTURE + EXÉCUTION'', aucune "ÉCRITURE'' (tandis que le parent a droit d'ÉCRITURE sur ses propres pages) ainsi, quand une demande d'écriture se produit, une exception de ''Faute de page'' est générée qui créera une nouvelle page indépendante: ce mécanisme s'appelle ''Copy on Write'' (copie sur écriture) (voir Chap.10).

Fourche ICA

|sys_fork 
   |do_fork
      |alloc_task_struct 
         |__get_free_pages
       |p->state = TASK_UNINTERRUPTIBLE
       |copy_flags
       |p->pid = get_pid    
       |copy_files
       |copy_fs
       |copy_sighand
       |copy_mm // should manage CopyOnWrite (I part)
          |allocate_mm
          |mm_init
             |pgd_alloc -> get_pgd_fast
                |get_pgd_slow
          |dup_mmap
             |copy_page_range
                |ptep_set_wrprotect
                   |clear_bit // set page to read-only              
          |copy_segments // For LDT
       |copy_thread
          |childregs->eax = 0  
          |p->thread.esp = childregs // child fork returns 0
          |p->thread.eip = ret_from_fork // child starts from fork exit
       |retval = p->pid // parent fork returns child pid
       |SET_LINKS // insertion of task into the list pointers
       |nr_threads++ // Global variable
       |wake_up_process(p) // Now we can wake up just created child
       |return retval
              
               fork ICA
 

Copy on Write (Copie sur Ecriture)

Pour implémenter la Copie sur Ecriture pour Linux:

  1. Marquez toutes les pages copiées en lecture-seule, entraînant une Faute de Page quand une Tâche essaye d'y écrire.
  2. Le gestionnaire de Faute de page cré une nouvelle page.
 
 | Page 
 | Fault 
 | Exception
 |
 |
 -----------> |do_page_fault
                 |handle_mm_fault
                    |handle_pte_fault 
                       |do_wp_page        
                          |alloc_page      // Alloue une nouvelle page
                          |break_cow
                             |copy_cow_page // Copie l'ancienne page vers une nouvelle
                             |establish_pte // reconfigure les pointeurs de la Table de Page
                                |set_pte
                            
              Page Fault ICA
 

Page suivante Page précédente Table des matières