J'ai réfléchi un peu aux variables d'environnement et j'ai quelques questions/observations.
putenv(char *string);
Cet appel semble fatalement vicié. Parce qu'il ne copie pas la chaîne transmise, vous ne pouvez pas l'appeler avec un local et il n'y a aucune garantie qu'une chaîne allouée au tas ne sera pas écrasée ou supprimée accidentellement. De plus (bien que je ne l'ai pas testé), étant donné qu'une utilisation des variables d'environnement consiste à transmettre des valeurs à l'environnement de l'enfant, cela semble inutile si l'enfant appelle l'une des fonctions exec*()
. Suis-je dans l'erreur?
La page de manuel Linux indique que la glibc 2.0-2.1.1 a abandonné le comportement ci-dessus et a commencé à copier la chaîne mais cela a conduit à une fuite de mémoire qui a été corrigée dans la glibc 2.1.2. Ce n'est pas clair pour moi quelle était cette fuite de mémoire ou comment elle a été corrigée.
setenv()
copie la chaîne mais je ne sais pas exactement comment cela fonctionne. L'espace pour l'environnement est alloué lorsque le processus se charge mais il est fixe. Y a-t-il une convention (arbitraire?) À l'œuvre ici? Par exemple, allouer plus d'emplacements dans le tableau de pointeurs de chaîne env qu'actuellement utilisé et déplacer le pointeur de fin nul vers le bas si nécessaire? La mémoire de la nouvelle chaîne (copiée) est-elle allouée dans l'espace d'adressage de l'environnement lui-même et si elle est trop grande pour s'adapter, vous obtenez simplement ENOMEM?
Compte tenu des problèmes ci-dessus, y a-t-il une raison de préférer putenv()
à setenv()
?
- [L'appel]
putenv(char *string);
[...] semble fatalement défectueux.
Oui, il est fatalement défectueux. Il a été conservé dans POSIX (1988) parce que c'était l'art antérieur. Le mécanisme Correction: La norme POSIX 1990 dit au §B.4.6.1 "Fonctions supplémentaires putenv () et clearenv () ont été pris en compte mais rejetés ". La Single Unix Specification (SUS) version 2 de 1997 répertorie setenv()
est arrivé plus tard.putenv()
mais pas setenv()
ou unsetenv()
. La prochaine révision (2004) a également défini à la fois setenv()
et unsetenv()
.
Parce qu'il ne copie pas la chaîne transmise, vous ne pouvez pas l'appeler avec un local et il n'y a aucune garantie qu'une chaîne allouée au tas ne sera pas écrasée ou supprimée accidentellement.
Vous avez raison de dire qu'une variable locale est presque toujours un mauvais choix à passer à putenv()
- les exceptions sont obscures au point de ne presque pas exister. Si la chaîne est allouée sur le tas (avec malloc()
et al), vous devez vous assurer que votre code ne le modifie pas. Si c'est le cas, cela modifie l'environnement en même temps.
De plus (bien que je ne l'ai pas testé), étant donné qu'une utilisation des variables d'environnement est de passer des valeurs à l'environnement de l'enfant, cela semble inutile si l'enfant appelle l'une des fonctions
exec*()
. Suis-je dans l'erreur?
Les fonctions exec*()
font une copie de l'environnement et la transmettent au processus exécuté. Il n'y a aucun problème là-bas.
La page de manuel Linux indique que la glibc 2.0-2.1.1 a abandonné le comportement ci-dessus et a commencé à copier la chaîne, mais cela a entraîné une fuite de mémoire qui a été corrigée dans la glibc 2.1.2. Ce n'est pas clair pour moi quelle était cette fuite de mémoire ou comment elle a été corrigée.
La fuite de mémoire se produit car une fois que vous avez appelé putenv()
avec une chaîne, vous ne pouvez plus utiliser cette chaîne à quelque fin que ce soit car vous ne pouvez pas dire si elle est toujours utilisée, bien que vous puissiez modifier la valeur en écrasant il (avec des résultats indéterminés si vous changez le nom en celui d'une variable d'environnement trouvée à une autre position dans l'environnement). Donc, si vous avez alloué de l'espace, la classique putenv()
la perd si vous modifiez à nouveau la variable. Lorsque putenv()
a commencé à copier des données, les variables allouées n'ont plus été référencées car putenv()
ne conservait plus de référence à l'argument, mais l'utilisateur s'attendait à ce que l'environnement le fasse référence, donc la mémoire était fuite. Je ne sais pas quel était le correctif - je m'attendrais à 3/4 à ce qu'il revienne à l'ancien comportement.
setenv()
copie la chaîne mais je ne sais pas exactement comment cela fonctionne. L'espace pour l'environnement est alloué lorsque le processus se charge mais il est fixe.
L'espace d'environnement d'origine est fixe; lorsque vous commencez à le modifier, les règles changent. Même avec putenv()
, l'environnement d'origine est modifié et pourrait croître à la suite de l'ajout de nouvelles variables, ou à la suite de la modification de variables existantes pour avoir des valeurs plus longues.
Y a-t-il une convention (arbitraire?) À l'œuvre ici? Par exemple, allouer plus d'emplacements dans le tableau de pointeurs de chaîne env qu'actuellement utilisé et déplacer le pointeur de terminaison nul vers le bas si nécessaire?
C'est ce que le mécanisme setenv()
est susceptible de faire. La variable (globale) environ
pointe vers le début du tableau de pointeurs vers les variables d'environnement. S'il pointe vers un bloc de mémoire à la fois et un bloc différent à un autre moment, l'environnement est commuté, comme ça.
La mémoire de la nouvelle chaîne (copiée) est-elle allouée dans l'espace d'adressage de l'environnement lui-même et si elle est trop grande pour s'adapter, vous obtenez simplement ENOMEM?
Eh bien, oui, vous pourriez obtenir ENOMEM, mais vous devriez essayer assez fort. Et si vous agrandissez l'environnement trop grand, vous ne pourrez peut-être pas exécuter correctement d'autres programmes - soit l'environnement sera tronqué, soit l'opération d'exécution échouera.
Compte tenu des problèmes ci-dessus, y a-t-il une raison de préférer putenv () à setenv ()?
setenv()
dans le nouveau code.setenv()
, mais n'en faites pas une priorité absolue.putenv()
dans le nouveau code.Il n'y a pas d'espace spécial "l'environnement" - setenv alloue simplement dynamiquement l'espace pour les chaînes (avec malloc
par exemple) comme vous le feriez normalement. Étant donné que l'environnement ne contient aucune indication sur l'origine de chaque chaîne, il est impossible pour setenv
ou unsetenv
de libérer de l'espace qui aurait pu être alloué dynamiquement par les appels précédents à setenv. .
"Parce qu'il ne copie pas la chaîne passée, vous ne pouvez pas l'appeler avec un local et il n'y a aucune garantie qu'une chaîne allouée en tas ne sera pas écrasée ou supprimée accidentellement." Le but de putenv est de s'assurer que si vous avez une chaîne allouée au tas, il est possible de la supprimer volontairement. C'est ce que le texte de justification veut dire par "la seule fonction disponible à ajouter à l'environnement sans autoriser les fuites de mémoire". Et oui, vous pouvez l'appeler avec un local, supprimez simplement la chaîne de l'environnement (putenv("FOO=")
ou unsetenv) avant de revenir de la fonction.
Le fait est que l'utilisation de putenv rend le processus de suppression d'une chaîne de l'environnement entièrement déterministe. Alors que setenv sur certaines implémentations existantes modifiera une chaîne existante dans l'environnement si la nouvelle valeur est plus courte (pour éviter toujours fuite de mémoire), et comme il a fait une copie lorsque vous avez appelé setenv, vous n'êtes pas dans contrôle de la chaîne allouée dynamiquement à l'origine afin que vous ne puissiez pas la libérer lorsqu'elle est supprimée.
Pendant ce temps, setenv lui-même (ou unsetenv) ne peut pas libérer la chaîne précédente, car - même en ignorant putenv - la chaîne peut provenir de l'environnement d'origine au lieu d'être allouée par une précédente invocation de setenv.
(Toute cette réponse suppose un putenv correctement implémenté, c'est-à-dire pas celui de la glibc 2.0-2.1.1 que vous avez mentionné.)
Lisez la section JUSTIFICATION de la page de manuel setenv
de la version 6 de The Open Group Base Specifications.
putenv
et setenv
sont tous deux censés être conformes à POSIX. Si vous avez du code contenant putenv
et que le code fonctionne bien, laissez-le tranquille. Si vous développez un nouveau code, vous voudrez peut-être envisager setenv
.
Regardez le code source de la glibc si vous voulez voir un exemple d'implémentation de setenv
( stdlib/setenv.c
) ou putenv
( stdlib/putenv.c
).
Je recommande fortement de ne pas utiliser l'une de ces fonctions. Soit peut être utilisé en toute sécurité et sans fuites, tant que vous êtes prudent et qu'une seule partie de votre code est responsable de la modification de l'environnement, mais il est difficile de se comporter correctement et dangereux si un code peut être en utilisant des threads et peut lire l'environnement (par exemple pour le fuseau horaire, les paramètres régionaux, la configuration DNS, etc.).
Les deux seuls objectifs auxquels je peux penser pour modifier l'environnement sont de changer le fuseau horaire lors de l'exécution ou de passer un environnement modifié aux processus enfants. Pour le premier, vous devrez probablement utiliser l'une de ces fonctions (setenv
/putenv
), ou vous pourriez marcher environ
manuellement pour le changer (ce pourrait soyez plus sûr si vous craignez que d'autres threads puissent essayer de lire l'environnement en même temps). Pour cette dernière utilisation (processus enfants), utilisez l’une des fonctions de la famille exec
- qui vous permet de spécifier votre propre tableau d’environnement, ou simplement assommez environ
(le global) ou utilisez setenv
/putenv
dans le processus enfant après fork
mais avant exec
, auquel cas vous n'avez pas à vous soucier des fuites de mémoire ou de la sécurité des threads car il y a aucun autre thread et vous êtes sur le point de détruire votre espace d'adressage et de le remplacer par une nouvelle image de processus.
De plus (bien que je ne l'ai pas testé), étant donné qu'une utilisation des variables d'environnement est de passer des valeurs à l'environnement de l'enfant, cela semble inutile si l'enfant appelle l'une des fonctions exec (). Suis-je dans l'erreur?
Ce n'est pas ainsi que l'environnement est transmis à l'enfant. Toutes les différentes versions de exec()
(que vous trouvez dans la section 3 du manuel parce que ce sont des fonctions de bibliothèque) appellent finalement l'appel système execve()
(que vous trouverez dans la section 2 de la Manuel). Les arguments sont les suivants:
int execve(const char *filename, char *const argv[], char *const envp[]);
Le vecteur des variables d'environnement est passé explicitement (et peut être en partie construit à partir des résultats de vos appels putenv()
et setenv()
). Le noyau les copie dans l'espace d'adressage du nouveau processus. Historiquement, il y avait une limite à la taille de votre environnement dérivée de l'espace disponible pour cette copie (similaire à la limite d'argument) mais je ne connais pas les restrictions sur un noyau Linux moderne.