Traitement des signaux Linux : Mécanismes et implémentation

Moment de traitement des signaux

Dans le noyau Linux, le traitement des signaux intervient au moment précis où une tâche retourne de l'espace noyau vers l'espace utilisateur. Peu importe la raison de l'entrée en mode noyau (appel système, exception synchrone, interruption), le noyau vérifie systématiquement la présence de signaux en attente avant le retour utilisateur.

// Toute entrée depuis EL0 vers EL1 passe par exit_to_user_mode
entry_handler
    el0t_64_sync_handler / el0t_64_irq_handler / el0t_64_fiq_handler / el0t_64_error_handler
        ...
        exit_to_user_mode(regs)   // Vérification des signaux en attente
    .if \el == 0
    b	ret_to_user

La fonction exit_to_user_mode effectue plusieurs vérifications pré-retour utilisateur, dont le traitement des signaux :

exit_to_user_mode(struct pt_regs *regs) {
    prepare_exit_to_user_mode(regs) {
        local_daif_mask()
        flags = read_thread_flags()
        
        if (unlikely(flags & _TIF_WORK_MASK))        
            do_notify_resume(regs, flags) {
                do {
                    local_daif_restore(DAIF_PROCCTX)
                    
                    if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
                        do_signal(regs)  // Traitement des signaux
                    
                    local_daif_mask()
                    thread_flags = read_thread_flags()
                } while (thread_flags & _TIF_WORK_MASK)
            }
    }
    __exit_to_user_mode()
}

Processus de traitement des signaux

L'implémentation du traitement des signaux dans le noyau se décompose en plusieurs étapes clés :

Récupération des signaux

La fonction get_signal suit un ordre de priorité strict pour récupérer les signaux en attente :

  1. Signaux synchrones (erreurs d'exécution comme SIGSEGV)
  2. Signaux privés du thread courant
  3. Signaux partagés entre threads du processus

Pour chaque signal récupéré, plusieurs cas sont traités :

  • Signal nécessitant un core dump : appel à do_coredump
  • Signal STOP : suspension du groupe de threads
  • Signal avec action par défaut : terminaison du processus
  • Signal avec gestionnaire personnalisé : préparation pour exécution

Préparasion de l'exécution

La fonction handle_signal configure l'environnement nécessaire à l'exécution du gestionnaire de signal :

  1. Allocation de l'espcae pile utilisateur
  2. Sauvegarde du contexte utilisateur actuel
  3. Configuration des registres pour l'appel du gestionnaire
  4. Gestion des informations supplémentaires (siginfo)
  5. Mise à jour du masque de signaux bloqués

Retour après traitement

Le mécanisme de retour utilise une fonction de restauration insérée dans le contexte utilisateur :

.global __restore_rt
__restore_rt:
	mov x8,#139  // SYS_rt_sigreturn
	svc 0

Cette fonction invoque l'appel système rt_sigreturn qui restaure le contexte utilisateur d'origine :

SYSCALL_DEFINE0(rt_sigreturn) {
    struct pt_regs *regs = current_pt_regs()
    struct rt_sigframe __user *frame = (struct rt_sigframe __user *)regs->sp
    
    // Restauration du masque de signaux
    __copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set))
    set_current_blocked(&set)
    
    // Restauration du contexte processeur
    for (i = 0; i < 31; i++)
        __get_user_error(regs->regs[i], &frame->uc.uc_mcontext.regs[i], err)
    __get_user_error(regs->sp, &frame->uc.uc_mcontext.sp, err)
    __get_user_error(regs->pc, &frame->uc.uc_mcontext.pc, err)
    
    return regs->regs[0]  // Retour à l'emplacement d'origine
}

Gestion des interruptions d'appels systèmes

Certains appels systèmes peuvent être interrompus par des signaux. Pour ces cas, le noyau supporte le redémarrage automatique via des codes spécifiques :

do_signal(struct pt_regs *regs) {
    if (in_syscall(regs)) {
        continue_addr = regs->pc
        restart_addr = continue_addr - 4  // Retour à l'instruction SVC
        retval = regs->regs[0]
        
        switch (retval) {
            case -ERESTARTNOHAND:
            case -ERESTARTSYS:
            case -ERESTARTNOINTR:
            case -ERESTART_RESTARTBLOCK:
                // Redémarrage de l'appel système
                regs->regs[0] = regs->orig_x0
                regs->pc = restart_addr
                break
        }
    }
}

Traitement multiple de signaux

Quand plusieurs signaux sont en attente, le noyau les traite séquentiellement :

do_notify_resume(struct pt_regs *regs, unsigned long thread_flags) {
    do {
        local_daif_restore(DAIF_PROCCTX)
        
        if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
            do_signal(regs)
        
        local_daif_mask()
        thread_flags = read_thread_flags()
    } while (thread_flags & _TIF_WORK_MASK)
}

Modification du flot d'exécution

Les gestionnaires de signaux peuvent modifier le comportement normal du programme en ajustant le contexte sauvegardé :

static void cancel_handler(int sig, siginfo_t *si, void *ctx) {
    pthread_t self = __pthread_self()
    ucontext_t *uc = ctx
    uintptr_t pc = uc->uc_mcontext.MC_PC
    
    // Modification de l'adresse de retour
    if (pc >= (uintptr_t)__cp_begin && pc < (uintptr_t)__cp_end) {
        uc->uc_mcontext.MC_PC = (uintptr_t)__cp_cancel
        return
    }
}

Implémentation complète du traitement

Voici une version simplifiée de l'implémentation principale :

do_signal(struct pt_regs *regs) {
    struct ksignal ksig
    bool syscall = in_syscall(regs)
    
    if (syscall) {
        // Gestion du redémarrage d'appel système
        handle_syscall_restart(regs)
    }
    
    if (get_signal(&ksig)) {
        // Préparation de l'exécution du gestionnaire
        handle_signal(&ksig, regs)
    } else {
        restore_saved_sigmask()
    }
}

Étiquettes: linux-kernel signals ARM64 interrupt-handling system-calls

Publié le 3 août à 00h57