vendredi 28 février 2014

ASLR (Accélère? Vasistas?)

Le travail d'un compilateur est de générer un code exécutable pour une machine hôte donnée. Tout système moderne proposant un adressage virtuel, le compilateur est (presque) libre de choisir pour toute variable ou symbole devant avoir une adresse mémoire une adresse virtuelle. Saviez-vous que les loaders aussi se permettent de jouer avec l'espace ? Et oui, comme au bon vieux temps, votre code peut-être dans l'espace virtuel lui-même! Let's see...

Prenons, le cas du programme élémentaire suivant :

#include <stdio.h>
#include <stdlib.h>
int i;
int main() {
int j;
int *p = malloc(sizeof(int));
printf("%18p static\n",&i);
printf("%18p code\n",&main);
printf("%18p stack\n",&j);
printf("%18p heap\n",p);
 printf("%18p libs\n",&printf);
free(p);
return 0;
}

L'idée est d'essayer d'observer quelles parties de l'espace d'adressage sont réservées aux variables statiques, au code, à la pile et au tas.

Ok let's go.

<nivose-2-[~/tmp]> uname -a
SunOS nivose 5.10 Generic_144488-01 sun4u sparc SUNW,SPARC-Enterprise
<nivose-3-[~/tmp]> gcc --version
gcc (GCC) 3.4.3 (csl-sol210-3_4-branch+sol_rpath)
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

<nivose-4-[~/tmp]> gcc -Wall -o vir vir.c
<nivose-9-[~/tmp]> nm -tx vir
vir:
[Index]   Value      Size      Type  Bind  Other Shndx   Name
...
[76]    |0x00020a60|0x00000004|OBJT |GLOB |0    |22     |i
[68]    |0x00010700|0x000000a8|FUNC |GLOB |0    |9      |main
[58]    |0x00020938|0x00000000|FUNC |GLOB |0    |UNDEF  |printf
...
<nivose-12-[~/tmp]> ./vir
             20a60 static
             10700 code
          ffbffa4c stack
             20a78 heap
             20938 libs
<nivose-13-[~/tmp]> 

Parfait! Sur le SUN que j'ai sous la main tout semble aller pour le mieux. Dans l'ordre des adresses croissantes, mon espace d'adresse contient : le code, immédiatement suivi de la zone static, des fonctions de bibliothèques puis du tas, enfin la pile démarre d'une adresse en fin d'espace (on rappelle ici que les piles descendent en grandissant...). C'est une situation tout à fait conforme à ce qui est généralement décrit. Au passage, on remarque que très vraisemblablement l'espace d'adressage est 32 bits (bon c'est sûrement une combinaison subtile de vieilleries - machine, os, compilo, etc).

La curiosité nous tient, alors on regarde sur une autre machine :

<lucien-5-[~/tmp]> uname -a
Linux lucien 3.2.0-4-amd64 #1 SMP Debian 3.2.51-1 x86_64 GNU/Linux
<lucien-6-[~/tmp]> gcc --version
gcc (Debian 4.7.2-5) 4.7.2
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

<lucien-7-[~/tmp]> gcc -Wall -o vir vir.c
<lucien-8-[~/tmp]> nm vir
0000000000600a4c B i
00000000004005ac T main
                 U printf@@GLIBC_2.2.5
<lucien-9-[~/tmp]> ./vir
          0x600a4c static
          0x4005ac code
    0x7fff207bb8e4 stack
         0x10b8010 heap
          0x400470 libs
<lucien-11-[~/tmp]>

La vie est délicieuse, c'est légèrement différent mais conforme à l'esprit de la chose. Ici dans l'ordre on a les libs, le code, la zone statique, le tas et en fin d'espace la pile. Donc les trucs statiques d'abord, suivi par les machins dynamiques pour qu'il puissent s'étendre l'un d'un côté, l'autre de l'autre. Ok ici, l'adresse de la pile est vraiment grande, on doit être dans un espace d'adressage à 64 bits.

Allons voir encore ailleurs...

[Ciboulette:~/examples] yunes% uname -a
Darwin Ciboulette.local 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64
[Ciboulette:~/examples] yunes% gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.1.0
Thread model: posix
[Ciboulette:~/examples] yunes% gcc -Wall -o vir vir.c
[Ciboulette:~/examples] yunes% nm vir
0000000100000000 T __mh_execute_header
                 U _free
0000000100001030 S _i
0000000100000e70 T _main
                 U _malloc
                 U _printf
                 U dyld_stub_binder
[Ciboulette:~/examples] yunes% ./vir
       0x10a102030 static
       0x10a101e70 code
    0x7fff55afe958 stack
    0x7fd7fac03a40 heap
    0x7fff8c0218a8 libs
[Ciboulette:~/examples] yunes% 

Oh my god! J'ai bien quelque chose de cohérent puisque l'espace de largeur 64 bits contient visiblement et dans l'ordre : le code, la zone statique, puis loin ailleurs le tas, bien plus loin encore la pile et derrière la pile les libs. Ce qui perturbe est que les adresses virtuelles déterminées à la compilation, par exemple 100000103 pour la variable globale i a à l'exécution la valeur 10a102030!!! Bizarre ce truc (la même chose peut-être observée pour les autres adresses). Les informaticiens ont parfois des pensées étranges (le psychanalyste dirait magiques), me voilà saisit d'une idée (magique donc : « bon la machine n'a bien compris, je vais lui relancer le programme et maintenant elle va faire les choses bien, hein cocotte ? »). Alors soit :

[Ciboulette:~/examples] yunes% ./vir
       0x10d7e8030 static
       0x10d7e7e70 code
    0x7fff52418958 stack
    0x7fb8c0c03a40 heap
    0x7fff8c0218a8 libs
[Ciboulette:~/examples] yunes% 

Oh boy! Les valeurs ne sont pas les mêmes d'une exécution à l'autre! (Une pensée sombre traverse l'esprit perturbé de l'expérimentateur : il doit retourner refaire ses études depuis le départ). Troisième tentative (avant d'aller s'inscrire en première année) :

[Ciboulette:~/examples] yunes% ./vir
       0x106ee3030 static
       0x106ee2e70 code
    0x7fff58d1d958 stack
    0x7fc64b403a40 heap
    0x7fff8c0218a8 libs
[Ciboulette:~/examples] yunes% 

Bon, il y a tout de même un motif, les bits de poids faibles semblent bien correspondre à ce qu'indique la table des symboles de l'exécutable... Alors, c'est quoi ce truc ?

Essayons de déboguer :

[Ciboulette:~/examples] yunes% gdb vir
GNU gdb 6.3.50-20050815 (Apple version gdb-1824) (Wed Feb  6 22:51:23 UTC 2013)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done

(gdb) run
Starting program: /Users/yunes/examples/vir 
...
............ done
       0x100001030 static
       0x100000e70 code
    0x7fff5fbff8b8 stack
       0x100103aa0 heap
    0x7fff8c0218a8 libs

Program exited normally.
(gdb) 

Oh dear! Avec un débogueur j'ai les bonnes adresses! Qu'est ce que j'ai fait, qui m'en veut autant ?

La réponse est en fait simple. Le mécanisme est l'ASLR! Soit Address Space Layout Randomization. Koikidilui ? Le mécanisme est une protection (faible) destinée à perturber l'écriture de virus ou d'attaques. Les adresses étant virtuelle, la machine est donc libre, au chargement du code (i.e. lors de l'exec()) de reloger les variables comme bon lui semble! En pratique, le déplacement consiste simplement à perturber aléatoirement certains bits de l'adresse virtuelle de la table des symboles.

Cette fonctionnalité n'existe pas sur tous les systèmes, mais semble être désormais assez répandue. D'autre part, elle peut-être désactivée à divers niveaux. Nous renvoyons le lecteur à la documentation afférente à son système pour savoir : (1) si l'ASLR est présent, (2) quels sont les espaces qui peuvent être relogés ? (3) déterminer si la fonctionnalité est active, etc.

Pour mon MacOSX, je peux :

  • soit utiliser l'option --no-pie à la compilation qui a pour effet (entre autres) de désactiver l'ASLR
  • soit modifier le champ MH_PIE dans l'entête Mach-O de l'exécutable. Il existe un script Python sur Internet pour le faire : ici
  • peut-être une autre technique exotique... Genre un appel à une fonction pour désactiver la fonctionnalité avec un flag exotique lui aussi avant un exec() (_POSIX_SPAWN_DISABLE_ASLR par exemple)

DjiBee
PS: C'est à Jean-Marie Rifflet que je dois cette expérimentation. Le symptôme qu'il avait observé nous avait d'abord plongé tous les deux dans un drôle d'état (computer science?) ou abîme de perplexité (psychanalyse?)...

mardi 11 février 2014

Javascript et la création d'objets - Part I

C'est désormais connu depuis longtemps, Javascript est un langage à objets qui ne sont ni typés ni construits via des classes. Ce langage n'utilise pas le concept de classe comme modèle d'objets, mais celui d'objet comme modèle d'autres objets (on verra ce que cela signifie exactement plus tard). Ce qui est encore moins connu c'est le modèle d'objets que Javascript autorise à manipuler. Commençons donc par les choses simples : la création d'objets. Nous reviendrons sur l'héritage plus tard.

Javascript autorise différentes constructions permettant d'obtenir un objet de son choix, chacune des techniques ayant ses avantages et inconvénients. Avant toute chose précisons que les objets Javascript n'ont pas de structure statiquement déterminée; un objet est un agrégat dynamique. C'est particulièrement intéressant, car si quelque chose manque à un objet, il suffit de le rajouter opportunément; si quelque chose n'est pas ce qui était attendu on peut le changer; et si quelque chose est en trop on peut aussi le supprimer! Dans le cas de l'écriture d'un prototype c'est particulièrement agréable mais surtout très efficace. Ainsi donc, nous allons voir que la création d'un objet consiste d'abord à créer un objet vide puis à le décorer de ce qu'on l'on considère comme nécessaire.

Première façon de créer un objet.


Le plus simple est de partir d'un objet vide :

monObjet = {}

puis de lui ajouter les attributs dont on a besoin (ici un entier et une méthode) :

monObjet.valeur = 2
monObjet.getValeur = function () {
  return this.valeur
}
document.write(monObjet.getValeur())
document.write(monObjet.attributInconnu)

Attention : la syntaxe o.a, utilisée comme lvalue, permet d'ajouter, s'il n'existe pas, l'attribut a à l'objet désigné par o, puis de modifier sa valeur.

Note : dans le cas de l'utilisation en tant que rvalue, la valeur d'un attribut inexistant est undefined du type undefined (voir le second affichage).

Il est tout à fait possible d'obtenir un objet vide par appel à Object.create(null), mais on reviendra sur Object.create dans un autre article.

Note : en Javascript on ne devrait pas parler d'attribut mais de propriété (property). À une propriété peut-être associée une valeur primitive ou un objet. Ainsi une méthode Javascript n'est qu'une propriété dont la valeur est typée function (et oui, les fonctions sont des objets, on y reviendra aussi plus tard - voir les foncteurs C++).

Seconde technique.

Ici aussi, en partant d'un objet vide :

monObjet = {}
monObjet['valeur'] = 2
monObjet['getValeur'] = function () {
  return this.valeur
}
 

La construction est équivalente à la première. La syntaxe o['a'] est strictement équivalente à o.a. La (grande) différence est que, cette fois, la valeur de l'attribut est obtenue par utilisation d'une chaîne de caractères comme clé d'accès dans l'objet à la valeur associée; l'objet étant vu comme tableau associatif (l'API Cocoa d'Apple nomme cette construction le KVC, Key-Value Coding). Cette caractéristique est essentielle, car l'attribut auquel on accède n'est plus statiquement déterminé par un symbole du code mais dynamiquement déterminé par la valeur d'une variable. Comme par exemple dans :

att = "valeur"
...
monObjet[att]

on peut évidemment envisage créer une fonction permettant d'accéder à l'attribut d'un objet via une fonction paramétrée par le nom de cet attribut :

function getAttribut(o,n) {
  return o[n]
}

Troisième essai.


monObjet = {
  valeur: 2,
  getValeur: function () {
    return this.valeur
  }
}
document.write(monObjet.getValeur())

Ici l'objet est vu comme une structure créée de façon ad-hoc, on a donc affaire à une syntaxe légère pour l'obtention d'un singleton.

Quatrième idiome.


function Modele() {
  this.valeur = 2
  this.getValeur = function () {
    return this.valeur
  }
}
monObjet = new Modele()
document.write(monObjet.getValeur())

Ici on utilise une construction qui imite maladroitement la construction d'objet des langages orientés objets comme Java ou C++. L'instruction new est particulière, mais ce qu'il suffit de savoir ici est qu'elle procède en plusieurs étapes :

  1. elle créé un objet frais, vide et de type object
  2. elle met à jour le chaînage du prototype de l'objet nouvellement créé (ce point sera l'objet d'un article à venir)
  3. elle appelle la fonction de construction indiquée (avec paramètres effectifs éventuels); fonction dans laquelle l'objet nouvellement créé est manipulé via le symbole this
  4. elle renvoie l'objet fraîchement créé, à moins que la fonction ne décide de renvoyer une autre valeur (non primitive)
Encore une fois, l'objet créé est vide puis agrémenté de ce qui est nécessaire mais cette fois de l'intérieur par utilisation d'un constructeur et du mot-clé this. Le terme constructeur est assez adéquat puisqu'on construit effectivement l'objet brique par brique; alors qu'en Java ou C++ ce que l'on appelle constructeur est en réalité un initialiseur (une fonction d'initalisation).
Le point 4 est particulièrement intéressant car il permet d'obtenir un singleton via l'opérateur new :

function Singleton() {
  if (!Singleton.s) {
    Singleton.s = {
      valeur : 2,
      getValeur : function () {
        return this.valeur
      }
    }
  }
  return Singleton.s
}
o1 = new Singleton()
o2 = new Singleton()
document.write(o1.getValeur()+" "+o2.getValeur())
o1.valeur = 345
document.write(o1.getValeur()+" "+o2.getValeur())


Cette construction n'est pas sans rappeler la possibilité offerte par la surcharge de l'opérateur new en C++ ou encore les idiomes de construction d'objets en Objective-C (où il est autorisé de modifier self!). En Java, pour obtenir un singleton, il est nécessaire de cacher les constructeurs et de fournir une usine (factory); il est impossible en Java d'obtenir un singleton via l'opérateur new (sauf pour quelques classes internes très particulières - comme String).

Cinquième construction.


Là, on utilise simplement ce que nous venons de voir pour créer un objet par copie d'un objet original :

function clone(o) {
  var n = {}
  for (var p in o) {
    n[p] = o[p]
  }
  return n
}
o2 = clone(o1)

La fonction clone se contente de créer un objet vide, de parcourir l'ensemble des propriétés de l'objet passé en paramètre (les objets sont munis d'un itérateur sur l'ensemble des clés de leurs propriétés) et de créer pour chaque propriété originelle, une propriété de même nom et de même valeur dans l'objet frais. On notera, chose intéressante, que cette fonction peut-être appelée au choix brutalement clone(o) ou via l'opérateur new clone(o), ce qui était déjà le cas du Singleton précédent).

DjiBee

mercredi 5 février 2014

Quand on corrige du code, les poils se dressent...

Comme notre confrère et néanmoins ami LeFa, il nous arrive de mettre le nez dans le cambouis. En effet, en tant qu'enseignant d'informatique il m'arrive d'avoir à corriger du code; mais pour mon malheur je sais lire et je découvre parfois des trucs bizarres. Allez on rigole un bon coup...

Premier exemple

if (i==0) {
    blabla = toto;
}
if (i==1) {
    blabla = titi;
}

Le codeur : J'imagine que dans le cas où le nombre de tests serait plus grand que 100 (le prof il a le droit de demander ça au fait ? C'est dans les conventions de Genève ?), l'écriture d'une bonne macro ©Word résoudrait le problème de l'écriture du code (ça fera geek comme réponse, il va être impressionné, héhé) ? Ou alors une bonne règuexpe ? Faut que je regarde ce que ça veut dire ce truc, ça m'a l'air trop puissant, il a le droit de demander des trucs puissant le prof ? Faut vraiment que je lise ces foutues règlements intérieurs moi... Je commence à avoir faim, je finirais mon projet tout à l'heure.

Deuxième exemple

Object *o = new Object();
o = tartempion.getSomeObject();
// delete o;

Le codeur : Argh! C'est chiant ces pointeurs, je sais jamais quoi en faire. Bon on m'a dit qu'il valait mieux les initialiser pour éviter les problèmes alors... allons-y! Bon en ligne 3, je suis bien embêté, j'ai essayé de libérer la mémoire de la ligne 1 mais ça marche pas bien, ma foutue machine empêche mon super programme de bien fonctionner, c'est chiant (je crois que je vais passer à ©Windows, Linux c'est trop hi-tech)! Bon je commente le truc; au moins ça ne déconne plus; je règlerais ces menus détails plus tard. Mon pote me dit « peut-être y'a une fuite mémoire mais c'est pas sûr », j'ai regardé sous la table mais j'ai rien vu couler alors je pense qu'il a dit ça pour déconner (il est rigolo mon pote, toujours à déconner). Bon, allez, je vais me chercher un sandwich à la cafét'.

Troisième exemple


Le correcteur : Là, j'ai cherché s'il n'y avait pas une corde dans mon bureau, et puis je me suis souvenu que j'avais du coaxial 10Base2 avec connecteurs BNC, c'est solide ce truc, non ? Je devrais y passer assez vite... Faut que je prenne quoi comme longueur pour me casser les vertèbres à coup sûr et bien net ? 2m ? Et puis, paf! V'là t'y pas, debout sur le tabouret face à la fenêtre ouverte, que j'me dis que finalement, y'a bien des gugusses qui développent des DAB avec du Windows 3.2 dedans alors...
Subtile transition dans la tête du codeur : Du coup cette interface me paraît moderne, pas vous ? En tous cas c'est digne d'un futur ingénieur. Moi je serais fier de pouvoir montrer que je sais développer sur iOS. Le vert, il est pas un peut trop vert ? #00ee00 ? #00dd00 ? Faut que je commit mes tests au fait (tiptiptip, cp source_23.m source_24.m)... Au fait, faudrait aussi que je pense à attacher une petite fonctionnalité sympa sur le bouton gotoscreen2 (peut-être aller sur un autre écran ? vert ?); bon on verra ça plus tard pour l'instant faut que je potasse mes exams; Wah! J'ai faim, je vais aller à la cafét'.

Quatrième exemple

// c'est bon
for (int i=0; i<[[o list] count]; i++) {
    NSLog(@"%@",[(MyClass *)[[o list] objectAtIndex:i] name]);
    NSLog(@"%@",[(MyClass *)[[o list] objectAtIndex:i] age]);
    NSLog(@"%@",[(MyClass *)[[o list] objectAtIndex:i] mail]);
}

Le correcteur : Ahhh, ça c'est de la bonne trace! Bon ça fait des tâches mais avec un bon solvant ça partira.
Le codeur :
  1. Quoi, une énumeration ? kesako, lui zyva! Reste poli, moi je développe en Objective-C pas en énumeration ou j'sais pas quoi...
  2. J'ai écrit MyClass tout seul, c'est cool hein ? J'ai bien essayé le toString() mais ça marche pas! Ce Java me semble un peu pourri, enfin bon (je sais pas pourquoi le prof' il arrête pas de dire Objective-C tout le temps, il connaît pas Java ?)... En plus dans la Javadoc chez ©Oracle j'ai pas trouvé MyClass, ça doit être pour ça que le toString() marche pas. Quoi Objective-c ?! Hein ? En cours ? Il a dit quoi ? description ? Ben j'ai documenté, qu'est ce qui va pas ?
  3. Quoi NSLog le array direct ? Eh patate, comment tu veux que le Java pourri y trouve comment afficher ? T'es vraiment un naze toi, réfléchit!
Le correcteur : je me demande s'il ne faudrait pas introduire un cours de programmation dans le cursus d'informatique. J'ai faim, je vais commander du caviar chez ©FirstPetrossian24/24, il paraît qu'ils livrent en ©Honda, Goldwing 1800.

Cinquième exemple

NSURL *url = [NSURL fileURLWithPath:@"http://blablablabla/blibliblibli.jpg"];
...
NSData *imageData = [NSData dataWithContentOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:imageData];
// [self.imagePlaceHolder setImage:image]; MET UNE IMAGE BLANCHE ALORS QUE MON URL EST CORRECTE

Le correcteur : Quand on commente en majuscules c'est qu'on est pas content, mais pas du tout! L'URL est correcte alors forcément hein ? So what's wrong ? Une simple petite trace fait apparaître que, bien sûr, les datas ne sont pas chargées (imageData null)... Alors pourquoi ? Peut-être que l'objet URL n'est pas correct, l'URL oui mais l'objet ? Ben oui, pour l'obtenir la méthode fileURLWithPath a été utilisée! Et comme son nom l'indique (voilà la raison d'être du mode verbeux de nommage des méthodes) cette méthode crée une URL de fichier à partir d'un chemin... Si on remplace par URLWithString, c'est fou ça marche!
Sinon, quand on décommente la dernière ligne, qu'arrive t-il ? Ben il faut commenter à partir de MET... Pratique non ? J'ai un petit creux, je me demande si un bon Tournedos Rossini me ferait pas du bien...

Sixième code

Contexte : Bon il s'agit d'écrire un code permettant de charger des données depuis un serveur sur le réseau.
Le codeur : Bien entendu, quand je développe je place tout sur ma propre machine ce qui donne bien entendu une URL de la forme suivante :

          http://localhost/chemin/fichier.xml

Le correcteur : Je me demande encore pourquoi ça ne fonctionne pas quand je corrige.
Un coup de steadycam et on entre dans la tête du codeur : Tout m'a l'air correct, ou alors j'ai un doute, il faut que je mette l'adresse en dur si jamais son DNS ne connaît pas localhost ? Le prof a une machine chelou alors faut que je prenne des précautions. Que penser de 127.0.0.1 ? Ou 255.255.255.255 ? Je sais plus moi! Bon de toutes les façons, j'en ai besoin à plusieurs endroits alors je vais tout recopier par couper/coller, c'est plus facile (y'a bien mon voisin qui me dit que peut-être un fichier d'entête avec les constantes ça pourrait être pas mal - mais il parle une langue étrangère alors je fais semblant de comprendre, je hoche la tête et comme ça je le vexe pas). En plus, je me suis débrouillé pour qu'à l'exécution on obtienne un joli index 0 beyond bounds for empty array (y'a pas que mon voisin qui parle une langue étrangère, mon ordi aussi, pourtant j'ai bien sélectionné français comme environnement, enfin bon, la ©Pomme c'est plus ce que c'était). Mais je crois que je vais ruser encore, parce que ce vicieux de prof pourrait être tenté de comprendre mon code (y'a peu de chance tout de même, vu qu'il est vieux); je vais faire compliqué comme ça il saura que je suis super fort :

NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:[[something getAMutableArray] count]];
for (i=0; i<[[something getAMutableArray] count]; i++) {
    [array addObject:[[something getAMutableArray] objectAtIndex:i];
}

Le codeur : Héhé! Je l'ai bien eu le gars n'empêche. Ca fait tout de même 4 lignes de code bien longues pour obtenir un MutableArray à partir d'un MutableArray. J'aurais pu cloner à la limite mais bon, c'était trop simple. Peut-être même que j'aurais pu utiliser le MutableArray de départ, mais...heu...attends voir...non! impossible...trop trivial...non...euh...Je demande à l'alien à côté de moi ou pas ?non...pfff tous ces projets... Vaut mieux être prudent, copions... Qu'est ce que j'ai faim moi, allez hop allons chez Sans2Viche&SangDeProche pour changer.

DjiBee

samedi 1 février 2014

Tire-lire-lo combien j'ai de procesus sur mon serveur Linux?

Combien de processus peut-on créer?

  1. Un certain nombre? :-)
  2. Un nombre fixe déterminé par configuration lors de la génération du noyau?
  3. Un nombre fixe, fonction de la taille mémoire de la machine, déterminé par le noyau lors de son initialisation
  4. Réponse3, mais le super utilisateur peut aussi modifier dynamiquement cette valeur.
 La bonne réponse est la réponse N°4....Enfin, moyennant quelques précautions.

Dans ma machine virtuelle Ubuntu dotée de 1GB de RAM, la valeur de threads-max est : 15807.

# cat /proc/sys/kernel/threads-max 
15807
#

D'où sort cette valeur?

Pour simplifier, historiquement, le nombre de processus qu'un OS Unix pouvait créer était défini par une constante fixée lors de la génération du système. L'OS définissait une table de descripteurs de processus de la taille ainsi fixée. Cette constante qui imposait une reconstruction du noyau lors d'une modification est devenue un paramètre de configuration.

Le noyau Linux ne gère pas les processus (et/ou threads) dans une table mais dans une liste. Le noyau Linux calcule une valeur par défaut cette limite lors de son démarrage. Cette valeur est telle que l'occupation des piles systèmes des processus (et/ou threads) ne dépasse pas 1/8 de la mémoire physique disponible.

La taille d'une pile système est définie par la constante THREAD_SIZE. On peut en trouver une définition ici : http://lxr.free-electrons.com/source/arch/x86/include/asm/page_64_types.h#L5
On a donc une taille  de pile de 8K dans ce cas. La limite par défaut proposée par le noyau Linux est donc d'un processus pour 64KB de mémoire.

La valeur du champ MemTotal du fichier /proc/meminfo et le résultat de 15807*8KB*8 ne concordent pas exactement. Probablement parce que le calcul se base sur une taille plus petite tenant compte de réservations système.

En pratique

Mais cette valeur théorique peut être inatteignable si le nombre de pid que le système peut créer simultanément est inférieur à cette limite.

# cat /proc/sys/kernel/pid_max 
32768
#
Ici, les pid "sont recyclés" lorsqu'on atteint la valeur 32768. Autrement dit, on peut théoriquement avoir 32768 processus simultanés chacun ayant un pid différent. On peut donc normalement créer les 15807 processus maximum permis par le noyau Linux.

Sur des systèmes ayant beaucoup de mémoire physique (ex: 128 GB), le nombre maximum de processus que l'on peut créer sera évidemment bien plus grand (supérieur à 2 000 000). Et très souvent la valeur max de pid sera de l'ordre de 65 536. La limite sera alors imposée par las valeur max du pid.

Modifications de ces valeurs... avec prudence!

En fait, il est possible en tant qu'administrateur système de modifier dynamiquement ces deux valeurs, en écrivant la nouvelle valeur désirée dans ces fichiers...
# echo 1999999 > /proc/sys/kernel/pid_max 
# echo 100 > /proc/sys/kernel/threads-max #! Ne faites pas ça!
#
 
A priori le système ne fait aucune vérification. On peut sur un système où il y a déjà 157 processus (et/ou threads) réduire le nombre de processus maximum que l'on peut créer à 100. Pas malin, mais faisable. Le noyau ne tue aucun processus, mais refusera de créer de nouveau processus tant que le compte sera supérieur à 99.

# echo 100 >/proc/sys/kernel/threads-max
# ps
bash: fork: retry: No child processes
La seule chose à faire est de vous dépêcher de remettre une valeur correcte dans threads-max... echo étant un built-in du shell aucun processus n'est créé pour exécuter cette commande. Ouf.

Puis-je réellement créer tous ces processus?

 En fait, les pid servent à différentes choses:
  • identifier les processus,
  • identifier les threads,
  • identifier les groupes de processsus
  • identifier les sessions
Un pid, initialement attribué à un processus, ne pourra être réutilisé que si aucun groupe et aucune session n'utilise plus cet identifiant. Le noyau se charge de cette "comptabilité".

Donc le nombre de processus que vous pourrez créer sera souvent inférieur aux limites observées.
Comment déterminer le nombre de processus et de threads courants?
# ps -edf | wc -l # Nombre de proc
# echo /proc/[0-9]*/task/[0-9]* |wc -l # nombre de threads
 

Mais sur des serveurs?

On observe des nombres de processus très variables... Dans les grandes entreprises, les types d'applications se ressemblent (serveurs de base de données, serveur d'application web/Java)

Quand on a des applications de type Java le nombre de processus est souvent de quelques petites centaines (de l'ordre de 250  à 500).

Quand on a des applications de type base de données, on peut rencontrer un nombre de processus de 1000 à 1500.

Et on trouve des serveurs exécutant plus de 50 000 processus simultanément...