Sorprendido por el comportamiento de cp con enlaces duros

20

Entiendo muy bien la noción de enlaces duros, y he leído las páginas del manual sobre herramientas básicas como cp--- e incluso las especificaciones POSIX recientes --- varias veces. Aún así me sorprendió observar el siguiente comportamiento:

$ echo john > john
$ cp -l john paul
$ echo george > george

En este punto johny paultendrá el mismo inodo (y contenido), y georgediferirá en ambos aspectos. Ahora hacemos:

$ cp george paul

En este punto esperaba georgey paultener diferentes números de inodo pero el mismo contenido --- esta expectativa se cumplió --- pero también esperaba paultener ahora un número de inodo diferente de john, y johnaún tener el contenido john. Aquí es donde me sorprendió. Resulta que copiar un archivo a la ruta de destino paultambién tiene el resultado de instalar ese mismo archivo (mismo inodo) en todas las demás rutas de destino que comparten paulel inodo. Estaba pensando que cpcrea un nuevo archivo y lo mueve al lugar anteriormente ocupado por el archivo anterior paul. En cambio, lo que parece hacer es abrir el archivo existente paul, truncarlo y escribirgeorgeEl contenido de ese archivo existente. Por lo tanto, cualquier "otro" archivo con el mismo inodo actualiza "su" contenido al mismo tiempo.

Ok, este es un comportamiento sistemático y ahora que sé que puedo esperarlo, puedo descubrir cómo solucionarlo o aprovecharlo, según corresponda. Lo que me desconcierta es dónde se suponía que debía ver este comportamiento documentado. Me sorprendería si no está documentado en algún lugar de los documentos que ya he visto. Pero aparentemente me lo perdí, y ahora no puedo encontrar una fuente que discuta este comportamiento.

dubiousjim
fuente

Respuestas:

4

Primero, ¿por qué se hace de esta manera? Una razón es histórica: así fue como se hizo en Unix First Edition .

Los archivos se toman en pares; el primero se abre para leer, el segundo modo creado 17. Luego, el primero se copia en el segundo.

"Creado" se refiere a la creatllamada al sistema (la que le falta una e ), que trunca el archivo existente por el nombre dado si hay uno.

Y aquí está el código fuente de cpUnix Second Edition (no puedo encontrar el código fuente de First Edition). Puede ver las llamadas al openarchivo fuente y creatal segundo archivo; y, como una mejora a la Primera Edición, si el segundo archivo es un directorio existente, cpcrea un archivo en ese directorio.

Pero, usted puede preguntar, ¿por qué se hizo de esa manera en ese momento? La respuesta a "por qué Unix lo hizo originalmente de esa manera" es casi siempre simplicidad. cpabre su fuente de lectura y crea su destino, y la llamada al sistema para crear un archivo sobrescribe un archivo existente abriéndolo para escribir, porque eso permite a la persona que llama imponer el contenido de un archivo por el nombre dado si el archivo ya existía o no.

Ahora, en cuanto a dónde está documentado: en la página de manual de FreeBSD .

Para cada archivo de destino que ya existe, su contenido se sobrescribe si los permisos lo permiten. Su modo, ID de usuario e ID de grupo no cambian a menos que se especifique la opción -p.

Esa redacción estaba presente al menos desde 1990 (cuando BSD era 4.3BSD). Hay una redacción similar en Solaris 10 :

Si target_file existe, cp sobrescribe su contenido, pero el modo (y ACL si corresponde), el propietario y el grupo asociado con él no cambian.

Su caso incluso se explica en el manual de HP-UX 10 :

Si new_file es un enlace a un archivo existente con otros enlaces, sobrescribe el archivo existente y retiene todos los enlaces.

POSIX lo pone en standardese. Citando desde Single UNIX v2 :

Si existe dest_file, se siguen los siguientes pasos: (...) Se obtendrá un descriptor de archivo para dest_file realizando acciones equivalentes a la función XSH de la especificación open () llamada utilizando dest_file como argumento de ruta, y el OR de O_WRONLY y O_TRUNC que incluye bit a bit como el argumento del retraso.

Las páginas de manual y la especificación que cité especifican además que si -fse pasa la opción y falla el intento de abrir / crear el archivo de destino (generalmente debido a que no tiene permiso para escribir el archivo), cpintenta eliminar el destino y crear un archivo nuevamente . Esto rompería el vínculo duro en su escenario.

Es posible que desee informar un error de documentación contra el manual de GNU coreutils , ya que no documenta este comportamiento. Incluso la descripción de --preserve=links, que en su escenario llevaría a la pauleliminación del enlace y la creación de un nuevo archivo, no deja en claro lo que sucede sin él --preserve=links. La descripción del -ftipo implica lo que sucede sin él, pero no lo explica en detalle ("Cuando se copia sin esta opción y no se puede abrir para escribir un archivo de destino existente, la copia falla. Sin embargo, con --force, ...").

Gilles 'SO- deja de ser malvado'
fuente
¿por qué dice "porque eso le permite a la persona que llama tomar posesión de un nombre de archivo si el archivo ya existe o no"? Cp no toma posesión de un archivo preexistente.
jrw32982 es compatible con Monica el
@ jrw32982 Me refería a la propiedad en el sentido de decidir qué entra en el archivo, no a la propiedad en el sentido de los metadatos del archivo. He reescrito esa oración.
Gilles 'SO- deja de ser malvado'
20

cpdocumentos que sobrescribe el archivo de destino si el archivo de destino ya está presente. Tiene razón en que no especifica en detalle qué significa "sobrescribir", pero definitivamente dice "sobrescribir", no "reemplazar". Si quiere ser pedante, puede argumentar que "sobrescribir" es exactamente lo que cphace, y el comportamiento que esperaba se llamaría correctamente "reemplazar".

También tenga en cuenta que si cp"reemplazara" archivos de destino preexistentes, eso podría considerarse razonablemente sorprendente o incorrecto, probablemente más que "sobrescribir". Por ejemplo:

  • Si cpprimero eliminó el archivo antiguo y luego creó uno nuevo, habría un intervalo de tiempo durante el cual el archivo estaría ausente, lo que sería sorprendente.
  • Si cp primero creó un archivo temporal y luego lo movió en su lugar, entonces probablemente debería documentarlo, debido al hecho de que ocasionalmente se notarían tales archivos temporales con nombres extraños ... pero no lo hace.
  • Si cp no puede crear un nuevo archivo en el mismo directorio que el anterior debido a los permisos, sería desafortunado (especialmente si ya hubiera eliminado el anterior).
  • Si el archivo no era propiedad del usuario en ejecución cpy el usuario en ejecución cpno eraroot , sería imposible hacer coincidir el propietario y los permisos del nuevo archivo con los del nuevo archivo.
  • Si el archivo tiene atributos especiales elegantes que cpno conoce, entonces estos se perderían en la copia. Hoy en día, las implementaciones de cpdeberían comprender de manera confiable cosas como los atributos extendidos, pero no siempre fue así. Y hay otras cosas, como los tenedores de recursos de MacOS o, para sistemas de archivos remotos, básicamente cualquier cosa.

En conclusión: ahora sabes lo que cprealmente hace. ¡Nunca más te sorprenderá! Honestamente, creo que lo mismo me podría haber pasado a mí también, hace muchos años.

Celada
fuente
Debe verificar la referencia POSIX, pero de hecho las manpáginas de cpBSD (al menos, OSX) y las versiones de Gnu cpno son tan explícitas sobre la "sobrescritura". Esa palabra solo se usa en los comentarios sobre opciones -iy -n. La página de manual de Gnu es especialmente poco informativa, comenzando Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.La página de In the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
manual de
La página de información de Gnu coreutils comienza:‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
dubiousjim
2
Veo que el estándar POSIX 2008 especifica el comportamiento observado; Agregaré una respuesta.
dubiousjim
16

Veo que el estándar POSIX 2013 sí especifica el comportamiento observado . Dice:

  1. Si source_file es de tipo archivo regular, se seguirán los siguientes pasos:

    a. ... si existe dest_file , se seguirán los siguientes pasos:

    yo. Si la -iopción está vigente, la cputilidad escribirá un aviso al error estándar y leerá una línea desde la entrada estándar. Si la respuesta no es afirmativa, cpno hará nada más con source_file y continuará con los archivos restantes.

    ii. Se obtendrá un descriptor de archivo para dest_file realizando acciones equivalentes a la open()función definida en el volumen de Interfaces del sistema de POSIX.1-2008 llamado utilizando dest_file como argumento de ruta, y el bit-inclusive ORde O_WRONLYy O_TRUNCcomo oflag argumento.

    iii) Si el intento de obtener un descriptor de archivo falla y la -fopción está en vigencia, cpdeberá intentar eliminar el archivo realizando acciones equivalentes a la unlink()función definida en el volumen de Interfaces del sistema de POSIX.1-2008 llamado utilizando dest_file como argumento de ruta. Si este intento tiene éxito, cpcontinuará con el paso 3b.

    ...

    re. El contenido del archivo fuente debe escribirse en el descriptor de archivo. Cualquier error de escritura hará cpque se escriba un mensaje de diagnóstico al error estándar y continúe con el paso 3e.

    mi. El descriptor de archivo estará cerrado.

dubiousjim
fuente
1
Interesante. Al igual que usted, supuse cpque daría resultados similares mvy rompería los enlaces duros de los que formaba parte el destino. Pero ahora que lo pienso, eso significaría que tendría que ser específicamente unlink(2)el objetivo ( cp -f), o crear un temporal con un nombre diferente y luego rename(2). La implementación sencilla es simplemente abrir el archivo para sobrescribir, que es lo que requiere POSIX. Es equivalente acat src > dest
Peter Cordes
2

Si puede decir, "copiar un archivo a la ruta de destino paul también copia el mismo archivo (mismo inodo) a todas las otras rutas de destino que comparten paulel inodo", lamento decir que no comprende la noción de enlaces duros muy bien. Si le doy una manzana a Sir McCartney, le he dado una manzana a Paul, y le he dado una manzana al compañero de composición de John Lennon. Pero no he dado tres manzanas; Le he dado una manzana a una persona que tiene múltiples nombres / títulos / descriptores.

Del mismo modo, cuando se copia georgea paul, usted no está también copia a john. Por el contrario, está copiando los georgedatos en el archivo cuyo inodo señala la paulentrada del directorio.

Paso a paso:   cuando lo haces

echo john > john

ha creado un nuevo archivo (suponiendo que no haya un archivo nombrado johnen ese directorio). O, para hablar más estrictamente, esto supone que no había una entrada de directorio con el nombre johnen ese directorio (porque, estrictamente hablando, no hay archivos en los directorios; solo entradas de directorio, que apuntan a inodes). Después de que lo hagas

cp -l john paul

o

ln john paul

no ha creado un nuevo archivo; más bien, le ha dado un nombre nuevo a su archivo existente. Ahora tiene un archivo con dos nombres: johny paul. Y cuando dices

cp george paul

Estás sobrescribiendo ese archivo . El hecho de que tenga dos nombres es irrelevante; podría tener 42 nombres, posiblemente en lugares a los que ni siquiera puede acceder, y este comando no estaría copiando los george\ndatos a todos esos nombres (rutas); solo está copiando los datos en un archivo que tiene varios nombres.

Scott
fuente
1
Gracias. Correcto, era consciente del carácter necesario de comillas de miedo de lo que estaba escribiendo mientras lo escribía: johny comencé paulcomo dos nombres de ruta para el mismo archivo. Pero era la forma más fácil en que podía pensar para expresarme. No creo que la mera noción de un enlace duro, entendido correctamente, dicte cualquiera de los dos comportamientos para cp(sin -l).
dubiousjim
Pero gracias por la insistencia; He tratado de aclarar la redacción.
dubiousjim