¿Puede write(2) devolver 0 bytes escritos*, y qué hacer si lo hace?
linux file-io (6)
TL; resumen de DR
A menos que haga todo lo posible para invocar un comportamiento no especificado, no obtendrá un resultado cero de write()
menos que, tal vez, intente escribir cero bytes (lo que el código en la pregunta evita hacer).
POSIX dice:
La especificación POSIX para write()
cubre el problema, creo.
La función write () intentará escribir bytes de nbyte desde el búfer apuntado por buf al archivo asociado con el descriptor de archivo abierto, fildes .
Antes de realizar cualquier acción descrita a continuación, y si nbyte es cero y el archivo es un archivo normal, la función write () puede detectar y devolver los errores como se describe a continuación. En ausencia de errores, o si no se realiza la detección de errores, la función write () devolverá cero y no tendrá otros resultados. Si nbyte es cero y el archivo no es un archivo normal, los resultados no se especifican.
Esto indica que si solicita una escritura de cero bytes, puede obtener un valor de retorno de cero, pero hay un conjunto de advertencias: debe ser un archivo normal y puede obtener un error si se detectan errores como EBADF
, y no se especifica qué sucede si el descriptor de archivo no se refiere a un archivo normal.
Si un write () solicita que se escriban más bytes de los que hay espacio para (por ejemplo, [XSI] [Option Start] el límite de tamaño de archivo del proceso o [Option End] el final físico de un medio), solo tantos Se escribirán los bytes que haya espacio para. Por ejemplo, supongamos que hay espacio para 20 bytes más en un archivo antes de alcanzar un límite. Una escritura de 512 bytes devolverá 20. La siguiente escritura de un número de bytes distinto de cero daría un retorno de error (excepto como se indica a continuación).
[XSI] ⌦ Si la solicitud causaría que el tamaño del archivo exceda el límite de tamaño del archivo suave para el proceso y no se puede escribir ningún byte, la solicitud fallará y la implementación generará la señal SIGXFSZ para el hilo. ⌫
Si write () es interrumpido por una señal antes de escribir cualquier dato, devolverá -1 con errno establecido en [EINTR].
Si write () es interrumpido por una señal después de que escribe con éxito algunos datos, devolverá el número de bytes escritos.
Si el valor de nbyte es mayor que {SSIZE_MAX}, el resultado está definido por la implementación.
Estas reglas realmente no dan permiso para devolver 0 (aunque un pedante puede decir que un valor de nbyte
que es demasiado grande podría definirse para devolver 0).
Cuando intente escribir en un descriptor de archivo (que no sea una tubería o FIFO) que admita escrituras sin bloqueo y no pueda aceptar los datos inmediatamente:
Si el indicador O_NONBLOCK está desactivado, write () bloqueará el subproceso de llamada hasta que se puedan aceptar los datos.
Si se establece el indicador O_NONBLOCK, write () no bloqueará el hilo. Si se pueden escribir algunos datos sin bloquear el hilo, write () escribirá lo que pueda y devolverá el número de bytes escritos. De lo contrario, devolverá -1 y pondrá errno en [EAGAIN].
... detalles para tipos de archivos oscuros, algunos de ellos con comportamiento no especificado ...
Valor de retorno
Una vez completada con éxito, estas funciones devolverán el número de bytes realmente escritos en el archivo asociado con los fildes . Este número nunca será mayor que el byte . De lo contrario, se devolverá -1 y errno se establecerá para indicar el error.
Por lo tanto, dado que su código evita intentar escribir cero bytes, siempre que len
no sea mayor que {SSIZE_MAX}, y mientras no escriba en tipos de archivos ocultos (como un objeto de memoria compartida o un objeto de memoria con tipo), No debería ver cero devuelto por write()
.
La justificación de POSIX dice:
Más adelante en la página POSIX de write()
, en la sección Racional, está la información:
Cuando este volumen de POSIX.1-2008 requiere que se devuelva
-1
yerrno
esté establecido en [EAGAIN], la mayoría de las implementaciones históricas devuelven cero (con el indicadorO_NDELAY
establecido, que es el antecesor histórico deO_NONBLOCK
, pero no está en este volumen de POSIX.1-2008). Las indicaciones de error en este volumen de POSIX.1-2008 se eligieron para que una aplicación pueda distinguir estos casos del final del archivo. Si bienwrite()
no puede recibir una indicación de fin de archivo,read()
puede, y las dos funciones tienen valores de retorno similares. Además, algunos sistemas existentes (por ejemplo, Octava edición) permiten una escritura de cero bytes para que el lector obtenga una indicación de fin de archivo; para esos sistemas, un valor de retorno de cero desdewrite()
indica una escritura exitosa de una indicación de fin de archivo.
Por lo tanto, aunque POSIX (en gran parte si no totalmente) excluye la posibilidad de un retorno cero de write()
, hubo técnica anterior en sistemas relacionados que tenían write()
devolvió cero.
Me gustaría implementar un bucle de write(2)
adecuado que tome un búfer y siga llamando a write
hasta que se write
todo el búfer.
Supongo que el enfoque básico es algo como:
/** 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;
}
... pero esto plantea la cuestión de si write()
puede devolver válidamente 0 bytes escritos y qué hacer en este caso. Si la situación persiste, el código anterior solo hará girar la llamada de write
que parece una mala idea. Mientras se devuelva algo distinto de cero, se está avanzando.
La página del manual para write
es un poco ambigua. Dice, por ejemplo:
En caso de éxito, se devuelve el número de bytes escritos (cero indica que no se escribió nada).
Lo que parece indicar que es posible en algunos escenarios. Solo uno de estos escenarios se menciona explícitamente:
Si el recuento es cero y fd se refiere a un archivo normal, entonces write () puede devolver un estado de falla si se detecta uno de los siguientes errores. Si no se detectan errores o no se realiza la detección de errores, se devolverá 0 sin causar ningún otro efecto. Si el recuento es cero y fd se refiere a un archivo que no es un archivo normal, los resultados no se especifican.
Ese caso se evita arriba porque nunca llamo write
con len == 0
. Hay muchos otros casos en los que no se puede escribir nada, pero en general todos tienen códigos de error específicos asociados con ellos.
El archivo en sí se open
desde una ruta / nombre en la línea de comandos. Por lo general, será un archivo normal, pero los usuarios pueden, por supuesto, pasar cosas como tuberías, hacer redirecciones de entrada, pasar dispositivos especiales como /dev/stdout
y así sucesivamente. Al menos tengo el control de la llamada open
y el indicador O_NONBLOCK
no se pasa a abrir. No puedo verificar razonablemente el comportamiento de todos los sistemas de archivos, todos los dispositivos especiales (e incluso si pudiera, se agregarán más), así que quiero saber cómo manejar esto de una manera razonable y general .
* ... para un tamaño de búfer distinto de cero.
Creo que el único enfoque factible (aparte de ignorar el problema por completo, que parece ser lo que se debe hacer de acuerdo con la documentación) es permitir "girar en su lugar".
Puede implementar un recuento de reintentos, pero si este extremadamente improbable "retorno 0 con longitud distinta de cero" se debe a alguna situación transitoria, tal vez una cola de LapLink llena; Recuerdo a ese conductor haciendo cosas raras: el bucle probablemente será tan rápido que cualquier cuenta de reintentos razonable se vería abrumada de todos modos; y no es aconsejable un recuento de reintentos excesivamente grande en caso de que tenga otros dispositivos que, en cambio, tomen un tiempo no despreciable para devolver 0.
Así que intentaría algo como esto. Es posible que desee utilizar gettimeofday () en su lugar, para una mayor precisión.
(Estamos introduciendo una penalización de rendimiento insignificante para un evento que parece tener una probabilidad insignificante de que suceda alguna vez).
/** 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;
}
Depende de a qué se refiere el descriptor de archivo. Cuando llama a write
en un descriptor de archivo, el núcleo finalmente termina llamando a la rutina de escritura en el vector de operaciones de archivo asociado, que corresponde al sistema de archivos subyacente o dispositivo al que se refiere el descriptor de archivo.
La mayoría de los sistemas de archivos normales nunca devolverán 0, pero los dispositivos pueden hacer casi cualquier cosa. Debe consultar la documentación del dispositivo en cuestión para ver qué podría hacer. Es legal que un controlador de dispositivo devuelva 0 bytes escritos (el kernel no lo marcará como un error o algo así), y si lo hace, la llamada al sistema de escritura devolverá 0.
Personalmente uso varios enfoques para este problema.
A continuación se muestran tres ejemplos, que todos esperan trabajar en un descriptor de bloqueo. (Es decir, consideran que EAGAIN
/ EWOULDBLOCK
un error).
Al guardar los datos importantes del usuario, sin un límite de tiempo (y, por lo tanto, la escritura no debe interrumpirse por la entrega de señales), prefiero usar
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
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;
}
Esto se anulará si se produce un error (que no sea EINTR
), o si write()
devuelve cero o un valor negativo distinto de -1
.
Debido a que no hay una razón sensata para que lo anterior devuelva el recuento parcial de escritura, en su lugar devuelve 0
si es correcto y el código de error no cero errno de lo contrario
Cuando se escriben datos importantes, pero la escritura debe interrumpirse si se entrega una señal, la interfaz es un poco diferente:
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);
}
En este caso, la cantidad de datos escritos siempre se devuelve. Esta versión también establece errno
en todos los casos, normalmente no se establece errno
excepto en casos de error.
Aunque esto significa que si se produce un error a mitad de camino, y la función devolverá la cantidad de datos que se escribieron con éxito (con llamadas de write()
anteriores), la razón para configurar siempre errno
es facilitar la detección de errores, esencialmente para separar el estado ( errno
) desde el conteo de escritura.
De vez en cuando, necesito una función que escribe un mensaje de depuración en un error estándar de un controlador de señales. (Estándar <stdio.h>
E / S no es segura para señales asíncronas, por lo que, en cualquier caso, se necesita una función especial). Quiero que esa función se cancele incluso en la entrega de señales. No es gran cosa si la escritura falla. siempre que no lo haga con el resto del programa, pero no cambie errno
. Esto imprime cadenas exclusivamente, ya que es el caso de uso previsto. Tenga en cuenta que strlen()
no es seguro para señales asíncronas, por lo que se usa un bucle explícito.
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;
}
Esta versión devuelve 0 si el mensaje se escribió correctamente en la salida estándar y, de lo contrario, un código de error distinto de cero. Como se mencionó, siempre se mantiene errno
sin cambios, para evitar efectos secundarios inesperados en el programa principal si se usa en un controlador de señales.
Utilizo principios muy simples al tratar con errores inesperados o valores de retorno de syscalls. El principio fundamental es nunca descartar ni manipular silenciosamente los datos del usuario . Si los datos se pierden o dañan, el programa siempre debe notificar al usuario. Todo lo inesperado debe ser considerado un error.
Solo algunas de las escrituras en un programa involucran datos del usuario. Mucho es informativo, como información de uso o un informe de progreso. Para aquellos, prefiero ignorar la condición inesperada o omitir la escritura. Depende de la naturaleza de los datos escritos.
En resumen, no me importa lo que dicen los estándares sobre los valores de retorno: los manejo todos. La respuesta a cada (tipo de) resultado depende de los datos que se escriben, específicamente, la importancia de esos datos para el usuario. Debido a esto, uso varias implementaciones diferentes incluso en un solo programa.
Posix lo define para tuberías, FIFO y FD que admiten operaciones de no bloqueo, en el caso de que nbyte
(el tercer parámetro) sea positivo y la llamada no se haya interrumpido:
si O_NONBLOCK es claro ... devolverá
nbyte
.
En otras palabras, no solo no puede devolver 0 a menos que nbyte
sea cero, tampoco puede devolver una longitud corta, en los casos mencionados.
Yo diría que toda la cuestión es innecesaria. Simplemente estás siendo demasiado cuidadoso. Usted espera que el archivo sea un archivo normal, no un socket, no un dispositivo, no un fifo, etc. Yo diría que cualquier devolución de un archivo de write
a un archivo regular que no sea igual a len
es un error irrecuperable. No trates de arreglarlo. Probablemente llenaste el sistema de archivos, o tu disco está roto, o algo así. (todo esto supone que no ha configurado sus señales para interrumpir las llamadas del sistema)
Para los archivos normales, no conozco ningún kernel que no haya hecho todos los reintentos necesarios para que se escriban sus datos y, si esto falla, lo más probable es que el error sea lo suficientemente grave como para que la aplicación lo solucione. Si el usuario decide pasar un archivo no regular como argumento, ¿y qué? Es su problema. Su pie y su pistola, que disparen.
Al tratar de solucionar esto en su código, es mucho más probable que empeore las cosas creando un CPU de bucle sin fin o llenando el diario del sistema de archivos o simplemente colgando.
No maneje 0 u otras escrituras cortas, solo imprima un error en cualquier devolución que no sea len
y salga. Una vez que obtenga un informe de error adecuado de un usuario que realmente tenga una razón legítima para que las escrituras falle, corríjalo. Lo más probable es que esto nunca suceda, porque esto es lo que casi todos hacen.
Sí, a veces es divertido leer POSIX y encontrar los casos de borde y escribir código para lidiar con ellos. Pero los desarrolladores de sistemas operativos no son enviados a prisión por violar POSIX, por lo que incluso si su código inteligente coincide perfectamente con lo que dice la norma, eso no garantiza que las cosas siempre funcionarán. A veces es mejor simplemente hacer las cosas y confiar en estar en buena compañía cuando se rompen. Si las escrituras de archivos normales comienzan a devolverse, estará en una compañía tan buena que lo más probable es que se solucione mucho antes de que sus usuarios lo noten.
NB Hace casi 20 años trabajé en una implementación de un sistema de archivos y tratamos de ser abogados de normas sobre el comportamiento de una de las operaciones (no de write
, pero se aplica el mismo principio). Nuestro "es legal devolver los datos en este orden" fue silenciado por el diluvio de informes de errores de aplicaciones rotas que esperaban las cosas de una manera determinada y al final fue simplemente más rápido para solucionarlo en lugar de luchar contra la misma pelea. todos y cada uno informe de errores. Para cualquiera que se pregunte, muchas cosas en aquel entonces (y probablemente todavía hoy) esperaban que readdir
regresara .
y ..
como las dos primeras entradas en un directorio que (al menos en ese entonces) no era obligatorio por ninguna norma.