sched_setaffinity(2) | System Calls Manual | sched_setaffinity(2) |
sched_setaffinity, sched_getaffinity - Définir et récupérer le masque d'affinité CPU d'un thread
Bibliothèque C standard (libc, -lc)
#define _GNU_SOURCE /* Consultez feature_test_macros(7) */ #include <sched.h>
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask); int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
Le masque d'affinité CPU d'un thread détermine l'ensemble des processeurs sur lesquels il est susceptible de s'exécuter. Sur un système multiprocesseur, définir le masque d'affinité CPU permet d'obtenir une meilleure performance. Par exemple, en dédiant un CPU à un thread particulier (c'est-à-dire définir le masque d'affinité de ce thread pour indiquer un seul CPU, et définir le masque d'affinité de tous les autres threads pour exclure ce CPU), il est possible d'assurer une vitesse d'exécution maximale pour ce thread. Restreindre un processus pour qu'il ne s'exécute que sur un seul CPU réduit le coût lié à l'invalidation du cache qui se produit lorsqu'un thread cesse de s'exécuter sur un CPU puis est relancé sur un autre CPU.
Un masque d'affinité CPU est représenté par la structure cpu_set_t, un ensemble de CPU (« CPU set »), pointé par mask. Des macros pour manipuler des ensembles de CPU sont décrites dans CPU_SET(3).
sched_setaffinity() définit le masque d'affinité CPU du thread dont l'identifiant est pid à la valeur donnée par mask. Si pid est 0, le thread appelant est utilisé. L'argument cpusetsize est la taille (en octets) de la structure pointée par mask. Normalement, cet argument doit être spécifié comme sizeof(cpu_set_t).
Si le thread indiqué par pid n'est pas actuellement en cours d'exécution sur l'un des CPU spécifiés dans mask, alors ce thread est migré vers l'un des CPU spécifiés dans mask.
La fonction sched_getaffinity() écrit dans la structure cpu_set_t pointée par mask le masque de préférences du thread pid. L'argument cpusetsize indique la taille (en octets) de mask. Si pid vaut zéro, le masque du thread en cours est renvoyé.
sched_setaffinity() et sched_getaffinity() renvoient 0 s'ils réussissent (mais voir « Différences entre la bibliothèque C et le noyau » ci-dessous, qui explique que le sched_getaffinity() sous-jacent diffère dans son code de retour). En cas d'échec, -1 est renvoyé et errno est positionné pour indiquer l'erreur.
Les appels système d'affinité ont été introduits dans Linux 2.5.8. Les fonctions enveloppes pour ces appels système ont été introduites dans la glibc 2.3. Au départ, les interfaces de la glibc avaient un paramètre cpusetsize de type unsigned int. Dans glibc 2.3.3, ce paramètre a été supprimé, mais il a été réintroduit dans glibc 2.3.4, avec pour type size_t.
Ces appels système sont spécifiques à Linux.
Après un appel à sched_setaffinity(), l'ensemble des CPU sur lesquels le thread s'exécutera est l'intersection de l'ensemble spécifié dans le paramètre mask et l'ensemble des CPU actuellement présents sur le système. Le système peut restreindre encore plus l'ensemble des CPU sur lesquels le thread peut tourner si le mécanisme « cpuset », décrit dans cpuset(7), est utilisé. Ces restrictions sur le véritable ensemble de CPU sur lesquels le thread peut tourner sont imposées sans avertissement par le noyau.
Il existe plusieurs manières de déterminer le nombre de processeurs disponibles sur le système, notamment l'inspection du contenu de /proc/cpuinfo, l'utilisation de sysconf(3) pour avoir les valeurs des paramètres _SC_NPROCESSORS_CONF et _SC_NPROCESSORS_ONLN, et l'inspection de la liste des répertoires de processeur dans /sys/devices/system/cpu/.
sched(7) décrit les politiques d'ordonnancement sous Linux.
Le masque d'affinité est un attribut de thread, qui peut être modifié indépendamment pour chacun des threads d'un groupe de threads. La valeur renvoyée par gettid(2) peut être passée dans l'argument pid. Spécifier un pid de 0 définira l'attribut pour le thread appelant, et une valeur égale à celle renvoyée par getpid(2) définira l'attribut pour le thread principal du groupe de threads. (Si vous utilisez l'API POSIX des threads, alors utilisez pthread_setaffinity_np(3) au lieu de sched_setaffinity().)
L'option d'amorçage isolcpus peut être utilisée pour isoler un ou plusieurs processeurs au moment du démarrage, afin qu'aucun processus n'utilise ces processeurs. Suite à l'utilisation de cette option, la seule manière d'affecter des processus à un processeur isolé est d'utiliser sched_setaffinity() ou le mécanisme cpuset(7). Pour plus d'informations, voir le fichier Documentation/admin-guide/kernel-parameters.txt des sources du noyau. Comme indiqué dans ce fichier, isolcpus est le mécanisme privilégié d'isolation des processeurs (plutôt que de définir à la main l'affinité processeur de tous les processus du système).
Un processus enfant créé par fork(2) hérite du masque d'affinité CPU de son parent. Le masque d'affinité est conservé au travers d'un execve(2).
Cette page de manuel décrit l'interface de la glibc pour les appels liés à l'affinité CPU. L'interface des appels système est légèrement différente, mask ayant le type unsigned long *, montrant le fait que l'implémentation des ensembles de CPU est en réalité un simple masque de bits.
En cas de succès, l'appel système sched_getaffinity() brut renvoie le nombre d'octets copiés dans le tampon mask ; il s'agira de la taille minimale de cpusetsize et de la taille (en octets) du type de données cpumask_t utilisée en interne par le noyau pour représenter le masque de bits du jeu de processeurs.
Les appels système sous-jacents (qui représentent les masques de processeur par des masques de bits de type unsigned long *) n'imposent aucune restriction quant à la taille du masque de processeur. Toutefois, le type de données cpu_set_t utilisé par la glibc a une taille fixe de 128 octets, si bien que le nombre maximal de processeurs qui peuvent être représentés est de 1023. Si le masque d'affinité de processeur du noyau est plus grand que 1024, les appels sous la forme :
sched_getaffinity(pid, sizeof(cpu_set_t), &mask);
échouent avec l'erreur EINVAL, celle produite par l'appel système sous-jacent si la taille mask indiquée dans cpusetsize est inférieure à celle du masque d'affinité utilisée par le noyau (selon la topologie des processeurs du système, le masque d'affinité du noyau peut être beaucoup plus grand que le nombre de processeurs actifs sur le système).
Si vous travaillez sur des systèmes ayant de grands masques d'affinité de processeur, vous pouvez allouer de manière dynamique l'argument mask (voir CPU_ALLOC(3)). Actuellement, la seule manière de faire cela est de sonder la taille de masque nécessaire en utilisant les appels sched_getaffinity() avec des tailles de masque croissantes (jusqu'à ce que l'appel n'échoue pas avec l'erreur EINVAL).
Gardez en tête qu'il se peut que CPU_ALLOC(3) alloue un peu plus de processeurs que vous ne l'avez demandé (car les ensembles de processeurs sont implémentés sous forme de masques de bits alloués par unités de sizeof(long)). Par conséquent, sched_getaffinity() peut positionner des bits au-delà de la taille d'allocation demandée car le noyau voit quelques bits supplémentaires. Donc, l'appelant doit revenir sur les bits de l'ensemble renvoyé en comptant ceux qui sont positionnés et s'arrêter lorsqu'il atteint la valeur renvoyée par CPU_COUNT(3) (et non pas revenir sur les bits de l'ensemble dont une allocation a été demandée).
Le programme ci-dessous crée un processus enfant. Puis le parent et l'enfant s'affectent mutuellement un processeur indiqué et exécutent des boucles identiques qui consomment du temps de processeur. Avant de se terminer, le parent attend que l'enfant s'achève. Le programme prend trois paramètres en ligne de commande : le numéro de processeur du parent, celui de l'enfant et le nombre de boucles que doivent effectuer les deux processus.
Comme le montre le modèle ci-dessous, la quantité de temps processeur et de temps réel consommé lors de l'exécution d'un programme dépendra des effets de mise en cache à l'intérieur du processeur et de l'utilisation ou non du même processeur par les processus.
On utilise d'abord lscpu(1) pour déterminer que ce système (x86) comporte deux cœurs, chacun ayant deux processeurs :
$ lscpu | egrep -i 'core.*:|socket' Thread(s) par cœur : 2 Cœur(s) par socket : 2 Socket(s) : 1
On chronomètre alors l'opération du programme exemple dans trois cas : les deux processus en fonction sur le même processeur, sur des processeurs différents du même cœur et sur des processeurs différents sur des cœurs différents.
$ time -p ./a.out 0 0 100000000 réel 14.75 utilisateur 3.02 sys 11.73 $ time -p ./a.out 0 1 100000000 réel 11.52 utilisateur 3.98 sys 19.06 $ time -p ./a.out 0 3 100000000 réel 7.89 utilisateur 3.29 sys 12.07
#define _GNU_SOURCE #include <err.h> #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> int main(int argc, char *argv[]) {
int parentCPU, childCPU;
cpu_set_t set;
unsigned int nloops;
if (argc != 4) {
fprintf(stderr, "Usage : %s CPU_parent CPU_enfant nb_boucles\n",
argv[0]);
exit(EXIT_FAILURE);
}
parentCPU = atoi(argv[1]);
childCPU = atoi(argv[2]);
nloops = atoi(argv[3]);
CPU_ZERO(&set);
switch (fork()) {
case -1: /* Erreur */
err(EXIT_FAILURE, "fork");
case 0: /* Enfant */
CPU_SET(childCPU, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) == -1)
err(EXIT_FAILURE, "sched_setaffinity");
for (unsigned int j = 0; j < nloops; j++)
getppid();
exit(EXIT_SUCCESS);
default: /* Parent */
CPU_SET(parentCPU, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) == -1)
err(EXIT_FAILURE, "sched_setaffinity");
for (unsigned int j = 0; j < nloops; j++)
getppid();
wait(NULL); /* Attendre que l'enfant se termine */
exit(EXIT_SUCCESS);
} }
lscpu(1), nproc(1), taskset(1), clone(2), getcpu(2), getpriority(2), gettid(2), nice(2), sched_get_priority_max(2), sched_get_priority_min(2), sched_getscheduler(2), sched_setscheduler(2), setpriority(2), CPU_SET(3), get_nprocs(3), pthread_setaffinity_np(3), sched_getcpu(3), capabilities(7), cpuset(7), sched(7), numactl(8)
La traduction française de cette page de manuel a été créée par Christophe Blaess <https://www.blaess.fr/christophe/>, Stéphan Rafin <stephan.rafin@laposte.net>, Thierry Vignaud <tvignaud@mandriva.com>, François Micaux, Alain Portal <aportal@univ-montp2.fr>, Jean-Philippe Guérard <fevrier@tigreraye.org>, Jean-Luc Coulon (f5ibh) <jean-luc.coulon@wanadoo.fr>, Julien Cristau <jcristau@debian.org>, Thomas Huriaux <thomas.huriaux@gmail.com>, Nicolas François <nicolas.francois@centraliens.net>, Florentin Duneau <fduneau@gmail.com>, Simon Paillard <simon.paillard@resel.enst-bretagne.fr>, Denis Barbier <barbier@debian.org>, David Prévot <david@tilapin.org>, Cédric Boutillier <cedric.boutillier@gmail.com>, Frédéric Hantrais <fhantrais@gmail.com> et Jean-Philippe MENGUAL <jpmengual@debian.org>
Cette traduction est une documentation libre ; veuillez vous reporter à la GNU General Public License version 3 concernant les conditions de copie et de distribution. Il n'y a aucune RESPONSABILITÉ LÉGALE.
Si vous découvrez un bogue dans la traduction de cette page de manuel, veuillez envoyer un message à debian-l10n-french@lists.debian.org.
5 février 2023 | Pages du manuel de Linux 6.03 |