Est-ce que write (2) peut renvoyer 0 octet écrit *, et que faire si c’est le cas?

Je voudrais implémenter une boucle write(2) correcte qui prend un tampon et continue à appeler write jusqu’à ce que tout le tampon soit écrit.

Je suppose que l’approche de base est quelque chose comme:

 /** write len bytes of buf to fd, returns 0 on success */ int write_fully(int fd, char *buf, size_t len) { while (len > 0) { ssize_t written = write(fd, buf, len); if (written < 0) { // some kind of error, probably should try again if its EINTR? return written; } buf += written; len -= written; } return 0; } 

… mais cela soulève la question de savoir si write() peut valablement renvoyer 0 octet écrit et que faire dans ce cas. Si la situation persiste, le code ci-dessus ne fera que write appel, ce qui semble être une mauvaise idée. Tant que quelque chose d’ autre que zéro est renvoyé, vous avancez.

La page de manuel relative à l’ write est un peu ambiguë. Il dit, par exemple:

En cas de succès, le nombre d’octets écrits est renvoyé (zéro indique que rien n’a été écrit).

Ce qui semble indiquer que cela est possible dans certains scénarios. Un seul de ces scénarios est explicitement appelé:

Si count est égal à zéro et que fd se réfère à un fichier normal, alors write () peut renvoyer un état d’échec si l’une des erreurs ci-dessous est détectée. Si aucune erreur n’est détectée ou si la détection d’erreur n’est pas effectuée, 0 sera renvoyé sans provoquer aucun autre effet. Si count est égal à zéro et que fd fait référence à un fichier autre qu’un fichier normal, les résultats ne sont pas spécifiés.

Ce cas est évité ci-dessus parce que je n’appelle jamais write avec len == 0 . Il y a beaucoup d’autres cas où rien ne peut être écrit, mais en général, ils comportent tous des codes d’erreur spécifiques.

Le fichier lui-même sera open depuis un chemin / nom donné sur la ligne de commande. Donc ce sera généralement un fichier normal, mais les utilisateurs peuvent bien sûr passer des choses comme des tuyaux, faire de la redirection d’entrée, passer des périphériques spéciaux comme /dev/stdout et ainsi de suite. Je contrôle au moins l’appel open et le drapeau O_NONBLOCK n’est pas passé à l’ouverture. Je ne peux pas raisonnablement vérifier le comportement de tous les systèmes de fichiers, de tous les périphériques spéciaux (et même si je le pouvais, d’autres seront ajoutés), je veux donc savoir comment gérer cela de manière générale et raisonnable .


* … pour une taille de tampon non nulle.

TL; résumé DR

À moins que vous ne fassiez tout en votre pouvoir pour invoquer un comportement non spécifié, vous n’obtiendrez aucun résultat de write() moins que vous n’essayiez, peut-être, d’écrire zéro octet (que le code évite dans la question).

POSIX dit:

La spécification POSIX pour write() couvre le problème, je crois.

La fonction write () doit essayer d’écrire des octets de nbyte à partir du tampon pointé par buf vers le fichier associé au descripteur de fichier ouvert, fildes .

Avant toute action décrite ci-dessous, et si nbyte est égal à zéro et que le fichier est un fichier normal, la fonction write () peut détecter et renvoyer des erreurs comme décrit ci-dessous. En l’absence d’erreurs, ou si la détection d’erreur n’est pas effectuée, la fonction write () renvoie zéro et n’a aucun autre résultat. Si nbyte est égal à zéro et que le fichier n’est pas un fichier normal, les résultats ne sont pas spécifiés.

Cela indique que si vous demandez une écriture de zéro octet, vous pouvez obtenir une valeur de retour de zéro, mais il existe un ensemble de mises en garde – ce doit être un fichier normal et une erreur peut EBADF si des erreurs comme EBADF sont détectées. il n’est pas spécifié ce qui se passe si le descripteur de fichier ne fait pas référence à un fichier normal.

Si un write () demande que plus d’octets soient écrits qu’il n’y a de place pour (par exemple, [XSI] [Option Start] la limite de taille de fichier du processus ou [Option End] la fin physique d’un support), seulement autant les octets, car il y a de la place pour, doivent être écrits. Par exemple, supposons qu’il y ait plus d’espace pour 20 octets dans un fichier avant d’atteindre une limite. Une écriture de 512 octets renverra 20. La prochaine écriture d’un nombre non nul d’octets donnerait un retour d’échec (sauf comme indiqué ci-dessous).

[XSI] the Si la demande entraîne un dépassement de la taille maximale du fichier pour le processus et qu’il n’y a pas de place pour l’écriture des octets, la requête doit échouer et l’implémentation doit générer le signal SIGXFSZ pour le thread. ⌫

Si write () est interrompu par un signal avant d’écrire des données, il doit renvoyer -1 avec errno avec la valeur [EINTR].

Si write () est interrompu par un signal après avoir écrit certaines données avec succès, il doit renvoyer le nombre d’octets écrits.

Si la valeur de nbyte est supérieure à {SSIZE_MAX}, le résultat est défini par l’implémentation.

Ces règles ne permettent pas vraiment de renvoyer 0 (bien qu’un pédant puisse dire qu’une valeur de nbyte trop grande peut être définie pour renvoyer 0).

Lors de la tentative d’écriture sur un descripteur de fichier (autre qu’un canal ou FIFO) prenant en charge les écritures non bloquantes et ne pouvant pas accepter les données immédiatement:

  • Si l’indicateur O_NONBLOCK est clair, write () doit bloquer le thread appelant jusqu’à ce que les données puissent être acceptées.

  • Si l’indicateur O_NONBLOCK est défini, write () ne doit pas bloquer le thread. Si certaines données peuvent être écrites sans bloquer le thread, write () doit écrire ce qu’il peut et retourner le nombre d’octets écrits. Sinon, il renverra -1 et définira errno à [EAGAIN].

… détails pour les types de fichiers obscurs – nombre d’entre eux avec un comportement non spécifié …

Valeur de retour

Une fois ces opérations terminées, ces fonctions renvoient le nombre d’octets réellement écrits dans le fichier associé aux fichiers . Ce nombre ne doit jamais être supérieur à un octet . Sinon, -1 doit être retourné et errno doit indiquer l’erreur.

Donc, puisque votre code évite d’essayer d’écrire des octets nuls, tant que len n’est pas plus grand que {SSIZE_MAX} et tant que vous n’écrivez pas pour masquer des types de fichiers (comme un object de mémoire partagé ou un object de mémoire typé) ne devrait pas voir zéro retourné par write() .


La logique de POSIX dit:

Plus loin dans la page POSIX pour write() , dans la section Rationale, il y a les informations:

Lorsque ce volume de POSIX.1-2008 requirejs le renvoi de -1 et que errno contient [EAGAIN], la plupart des implémentations historiques renvoient zéro (avec le O_NDELAY indicateurs O_NDELAY , qui est le prédécesseur historique de O_NONBLOCK , mais n’est pas lui-même dans ce volume). de POSIX.1-2008). Les indications d’erreur dans ce volume de POSIX.1-2008 ont été choisies pour qu’une application puisse distinguer ces cas de la fin du fichier. Alors que write() ne peut pas recevoir d’indication de fin de fichier, read() peut et les deux fonctions ont des valeurs de retour similaires. En outre, certains systèmes existants (par exemple, la huitième édition) autorisent une écriture de zéro octet pour signifier que le lecteur devrait recevoir une indication de fin de fichier; pour ces systèmes, une valeur de retour de zéro à partir de write() indique une écriture réussie d’une indication de fin de fichier.

Ainsi, bien que POSIX (en grande partie sinon entièrement) exclue la possibilité d’un retour nul à partir de write() , il existait des antériorités sur les systèmes associés qui avaient un retour zéro write() .

Cela dépend de la référence du descripteur de fichier. Lorsque vous appelez write sur un descripteur de fichier, le kernel finit par appeler la routine write dans le vecteur d’opérations de fichier associé, qui correspond au système de fichiers ou au périphérique sous-jacent auquel le descripteur de fichier fait référence.

La plupart des systèmes de fichiers normaux ne renverront jamais 0, mais les périphériques peuvent tout faire. Vous devez consulter la documentation du périphérique en question pour voir ce qu’il pourrait faire. Il est légal qu’un pilote de périphérique renvoie 0 octet écrit (le kernel ne le signalera pas comme une erreur ou autre), et si c’est le cas, l’appel système en écriture renverra 0.

Posix le définit pour les pipes, les FIFO et les FD prenant en charge les opérations non bloquantes, dans le cas où nbyte (le troisième paramètre) est positif et que l’appel n’a pas été interrompu:

si O_NONBLOCK est clair … il doit retourner nbyte .

En d’autres termes, non seulement il ne peut pas renvoyer 0 à moins que nbyte ne soit nul, il ne peut pas non plus renvoyer une courte longueur, dans les cas mentionnés.

Je pense que la seule approche possible (à part ignorer complètement le problème, ce qui semble être la chose à faire selon la documentation) est de permettre la “mise en place”.

Vous pouvez implémenter un nombre de tentatives, mais si cela est extrêmement improbable, “0 retourne avec une longueur différente de zéro” est dû à une situation transitoire – une queue LapLink complète peut-être; Je me souviens de ce pilote qui fait des choses étranges – la boucle sera probablement si rapide que le nombre de tentatives raisonnables serait dépassé de toute façon; et un nombre de tentatives excessivement élevé n’est pas recommandé si vous avez d’ autres périphériques qui prennent un temps non négligeable pour retourner 0.

Donc, je vais essayer quelque chose comme ça. Vous pouvez utiliser gettimeofday () pour plus de précision.

(Nous introduisons une pénalité de performance négligeable pour un événement qui semble avoir une chance négligeable de se produire).

 /** write len bytes of buf to fd, returns 0 on success */ int write_fully(int fd, char *buf, size_t len) { time_t timeout = 0; while (len > 0) { ssize_t written = write(fd, buf, len); if (written < 0) { // some kind of error, probably should try again if its EINTR? return written; } if (!written) { if (!timeout) { // First time around, set the timeout timeout = time(NULL) + 2; // Prepare to wait "between" 1 and 2 seconds // Add nanosleep() to reduce CPU load } else { if (time(NULL) >= timeout) { // Weird status lasted too long return -42; } } } else { timeout = 0; // reset timeout at every success, or the second zero-return after a recovery will immediately abort (which could be desirable, at that). } buf += written; len -= written; } return 0; } 

J’utilise personnellement plusieurs approches pour résoudre ce problème.

Vous trouverez ci-dessous trois exemples dont tous s’attendent à travailler sur un descripteur de blocage. (C’est-à-dire qu’ils considèrent EAGAIN / EWOULDBLOCK une erreur.)


Lors de la sauvegarde de données utilisateur importantes, sans limite de temps (et donc l’écriture ne doit pas être interrompue par la transmission du signal), je préfère utiliser

 #define _POSIX_C_SOURCE 200809L #include  #include  #include  int write_uninterruptible(const int descriptor, const void *const data, const size_t size) { const unsigned char *p = (const unsigned char *)data; const unsigned char *const q = (const unsigned char *)data + size; ssize_t n; if (descriptor == -1) return errno = EBADF; while (p < q) { n = write(descriptor, p, (size_t)(q - p)); if (n > 0) p += n; else if (n != -1) return errno = EIO; else if (errno != EINTR) return errno; } if (p != q) return errno = EIO; return 0; } 

Cela s’interrompt si une erreur (autre que EINTR ) se produit, ou si write() renvoie zéro ou une valeur négative autre que -1 .

Comme il n’y a pas de raison valable pour que ce qui précède retourne le nombre d’écritures partielles, il renvoie à la place 0 cas de succès et un code d’erreur autre que zéro d’erreur.


Lors de l’écriture de données importantes, mais si l’écriture doit être interrompue si un signal est délivré, l’interface est un peu différente:

 size_t write_interruptible(const int descriptor, const void *const data, const size_t size) { const unsigned char *p = (const unsigned char *)data; const unsigned char *const q = (const unsigned char *)data + size; ssize_t n; if (descriptor == -1) { errno = EBADF; return 0; } while (p < q) { n = write(descriptor, p, (size_t)(q - p)); if (n > 0) p += n; else if (n != -1) { errno = EIO; return (size_t)(p - (const unsigned char *)data); } else return (size_t)(p - (const unsigned char *)data); } errno = 0; return (size_t)(p - (const unsigned char *)data); } 

Dans ce cas, la quantité de données écrites est toujours renvoyée. Cette version définit également errno dans tous les cas – normalement, errno n’est pas défini sauf dans les cas d’erreur.

Bien que cela signifie que si une erreur survient à mi-chemin et que la fonction renvoie la quantité de données qui a été écrite avec succès (avec les appels précédents en write() ), la définition d’erreur est toujours facilitée, essentiellement pour séparer status ( errno ) du compte d’écriture.


De temps en temps, j’ai besoin d’une fonction qui écrit un message de débogage à l’erreur standard d’un gestionnaire de signaux. (Les E / S standard ne sont pas asynchrones, donc une fonction spéciale est nécessaire dans tous les cas.) Je veux que cette fonction soit interrompue même lors de la dissortingbution du signal – si l’écriture échoue, tant qu’il ne va pas avec le rest du programme -, mais gardez errno inchangé. Cela imprime des chaînes exclusivement, car c’est le cas d’utilisation prévu. Notez que strlen() n’est pas async-safe, donc une boucle explicite est utilisée à la place.

 int stderr_note(const char *message) { int retval = 0; if (message && *message) { int saved_errno; const char *ends = message; ssize_t n; saved_errno = errno; while (*ends) ends++; while (message < ends) { n = write(STDERR_FILENO, message, (size_t)(ends - message)); if (n > 0) message += n; else { if (n == -1) retval = errno; else retval = EIO; break; } } if (!retval && message != ends) retval = EIO; errno = saved_errno; } return retval; } 

Cette version renvoie 0 si le message a été écrit avec succès sur la sortie standard et un code d’erreur différent de zéro. Comme mentionné précédemment, les errno pas pour éviter les effets secondaires inattendus dans le programme principal si elles sont utilisées dans un gestionnaire de signaux.


J’utilise des principes très simples pour traiter les erreurs inattendues ou les valeurs renvoyées par syscalls. Le principe de base est de ne jamais supprimer ou modifier en silence les données utilisateur . Si des données sont perdues ou endommagées, le programme doit toujours en informer l’utilisateur. Tout ce qui est inattendu doit être considéré comme une erreur.

Seules certaines des écritures dans un programme impliquent des données utilisateur. Beaucoup sont informatifs, comme les informations d’utilisation ou un rapport d’avancement. Pour ceux-là, je préférerais soit ignorer la condition inattendue, soit ignorer cette écriture. Cela dépend de la nature des données écrites.

En résumé, je ne me soucie pas de ce que les normes disent à propos des valeurs de retour: je les traite toutes. La réponse à chaque type de résultat dépend des données écrites, en particulier de l’importance de ces données pour l’utilisateur. Pour cette raison, j’utilise plusieurs implémentations différentes, même dans un seul programme.

Je dirais que toute la question est inutile. Vous êtes simplement trop prudent. Vous vous attendez à ce que le fichier soit un fichier ordinaire, pas un socket, pas un périphérique, pas un fifo, etc. N’essayez pas de le réparer. Vous avez probablement rempli le système de fichiers, ou votre disque est cassé, ou quelque chose comme ça. (tout cela suppose que vous n’avez pas configuré vos signaux pour interrompre les appels système)

Pour les fichiers normaux, je ne connais aucun kernel qui ne fait pas déjà toutes les tentatives nécessaires pour obtenir vos données et si cela échoue, l’erreur est probablement assez grave pour que l’application ne puisse plus la réparer. Si l’utilisateur décide de transmettre un fichier non régulier en argument, alors quoi? C’est leur problème. Leur pied et leur arme, laissez-les tirer.

En essayant de résoudre ce problème dans votre code, vous risquez d’aggraver la situation en créant une boucle sans fin mangeant le processeur ou en remplissant le journal du système de fichiers ou simplement en le suspendant.

Ne manipulez pas 0 ou d’autres écritures courtes, imprimez simplement une erreur sur n’importe quel retour autre que len et exit. Une fois que vous obtenez un rapport de bogue approprié d’un utilisateur qui a une raison légitime pour que les écritures échouent, corrigez-le ensuite. Très probablement, cela n’arrivera jamais, car c’est ce que presque tout le monde fait.

Oui, il est parfois amusant de lire POSIX et de trouver les cas extrêmes et d’écrire du code pour les gérer. Mais les développeurs de systèmes d’exploitation ne sont pas envoyés en prison pour avoir enfreint POSIX, alors même si votre code intelligent correspond parfaitement à ce que dit la norme, cela ne garantit pas que les choses fonctionneront toujours. Parfois, il vaut mieux simplement faire avancer les choses et compter sur la bonne compagnie en cas de rupture. Si les écritures de fichiers régulières commencent à revenir à court, vous serez dans une si bonne société qu’elles seront probablement réparées bien avant que l’un de vos utilisateurs ne le remarque.

NB Il y a près de 20 ans, j’ai travaillé sur une implémentation de système de fichiers et nous avons essayé d’être des avocats de la normalisation sur le comportement de l’une des opérations (pas d’ write , mais le même principe s’applique). Notre “il est légal de retourner les données dans cet ordre” a été réduit au silence par le déluge de rapports de bogues sur les applications cassées qui attendaient des choses d’une certaine manière. chaque rapport de bogue. Pour quiconque se demande, beaucoup de choses à ce moment-là (et probablement encore aujourd’hui) s’attendaient à ce que readdir revienne . et .. comme les deux premières entrées dans un répertoire qui (au moins à l’époque) n’était mandaté par aucune norme.