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 :
- Signaux synchrones (erreurs d'exécution comme SIGSEGV)
- Signaux privés du thread courant
- 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 :
- Allocation de l'espcae pile utilisateur
- Sauvegarde du contexte utilisateur actuel
- Configuration des registres pour l'appel du gestionnaire
- Gestion des informations supplémentaires (siginfo)
- 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()
}
}