¿Qué hace realmente abrir un archivo?

266

En todos los lenguajes de programación (que utilizo al menos), debe abrir un archivo antes de poder leerlo o escribirlo.

Pero, ¿qué hace realmente esta operación abierta?

Las páginas de manual para funciones típicas en realidad no le dicen nada más que 'abre un archivo para leer / escribir':

http://www.cplusplus.com/reference/cstdio/fopen/

https://docs.python.org/3/library/functions.html#open

Obviamente, a través del uso de la función, puede decir que implica la creación de algún tipo de objeto que facilita el acceso a un archivo.

Otra forma de decir esto sería, si tuviera que implementar una openfunción, ¿qué necesitaría hacer en Linux?

jramm
fuente
13
Edición de esta pregunta para centrarse en CLinux; ya que lo que hacen Linux y Windows es diferente. De lo contrario, es un poco demasiado amplio. Además, cualquier lenguaje de nivel superior terminará llamando a una API de C para el sistema o compilando a C para ejecutar, por lo que salir al nivel de "C" es colocarlo en el mínimo común denominador.
George Stocker
1
Sin mencionar que no todos los lenguajes de programación tienen esta instalación, o es una instalación que depende mucho del entorno. Es cierto que es raro en estos días, por supuesto, pero hasta el día de hoy el manejo de archivos es una parte completamente opcional de ANSI Forth, y ni siquiera estuvo presente en algunas implementaciones en el pasado.

Respuestas:

184

En casi todos los lenguajes de alto nivel, la función que abre un archivo es un contenedor alrededor de la correspondiente llamada al sistema del núcleo. También puede hacer otras cosas sofisticadas, pero en los sistemas operativos contemporáneos, abrir un archivo siempre debe pasar por el núcleo.

Esta es la razón por la cual los argumentos de la fopenfunción de biblioteca, o Python, opense parecen mucho a los argumentos de la open(2)llamada al sistema.

Además de abrir el archivo, estas funciones generalmente configuran un búfer que se utilizará en consecuencia con las operaciones de lectura / escritura. El propósito de este búfer es garantizar que cada vez que desee leer N bytes, la llamada a la biblioteca correspondiente devuelva N bytes, independientemente de si las llamadas a las llamadas del sistema subyacente devuelven menos.

No estoy realmente interesado en implementar mi propia función; solo para entender qué demonios está pasando ... 'más allá del idioma' si quieres

En los sistemas operativos tipo Unix, una llamada exitosa a opendevuelve un "descriptor de archivo" que es simplemente un número entero en el contexto del proceso del usuario. En consecuencia, este descriptor se pasa a cualquier llamada que interactúa con el archivo abierto, y después de invocarlo close, el descriptor deja de ser válido.

Es importante tener en cuenta que la llamada a openactuar actúa como un punto de validación en el que se realizan varias verificaciones. Si no se cumplen todas las condiciones, la llamada falla al regresar en -1lugar del descriptor, y el tipo de error se indica en errno. Los controles esenciales son:

  • Si el archivo existe;
  • Si el proceso de llamada tiene privilegios para abrir este archivo en el modo especificado. Esto se determina haciendo coincidir los permisos del archivo, la identificación del propietario y la identificación del grupo con las identificaciones respectivas del proceso de llamada.

En el contexto del núcleo, tiene que haber algún tipo de mapeo entre los descriptores de archivo del proceso y los archivos abiertos físicamente. La estructura de datos interna que se asigna al descriptor puede contener otro búfer que se ocupa de dispositivos basados ​​en bloques, o un puntero interno que apunta a la posición actual de lectura / escritura.

Blagovest Buyukliev
fuente
2
Vale la pena señalar que en los sistemas operativos tipo Unix, los descriptores de archivo de estructura en el núcleo se asignan a, se denomina "descripción de archivo abierto". Por lo tanto, los FD de proceso se asignan a OFD de kernel. Esto es importante para entender la documentación. Por ejemplo, vea man dup2y verifique la sutileza entre un descriptor de archivo abierto (que es un FD que está abierto) y una descripción de archivo abierto (un OFD).
rodrigo
1
Sí, los permisos se verifican en tiempo abierto. Puede ir y leer la fuente de la implementación "abierta" del núcleo: lxr.free-electrons.com/source/fs/open.c, aunque delega la mayor parte del trabajo al controlador específico del sistema de archivos.
pjc50
1
(en los sistemas ext2 esto implicará leer las entradas del directorio para identificar en qué inodo tiene los metadatos y luego cargar ese inodo en la caché del inodo. Tenga en cuenta que puede haber sistemas de pseudofiles como "/ proc" y "/ sys" que pueden hacer cosas arbitrarias cuando abres un archivo)
pjc50
1
Tenga en cuenta que las comprobaciones en el archivo abierto (que el archivo existe, que tiene permiso) no son, en la práctica, suficientes. El archivo puede desaparecer, o sus permisos pueden cambiar, bajo sus pies. Algunos sistemas de archivos intentan evitar esto, pero mientras su sistema operativo sea compatible con el almacenamiento en red, es imposible evitarlo (un sistema operativo puede "entrar en pánico" si el sistema de archivos local se comporta mal y es razonable: uno que lo hace cuando un recurso compartido de red no lo es). Un sistema operativo viable). Esas comprobaciones también se realizan al abrir el archivo, pero también deben realizarse (efectivamente) en todos los demás accesos a archivos.
Yakk - Adam Nevraumont el
2
Sin olvidar la evaluación y / o creación de cerraduras. Estos pueden ser compartidos o exclusivos y pueden afectar todo el archivo, o solo una parte del mismo.
Thinkeye el
83

Le sugiero que eche un vistazo a esta guía a través de una versión simplificada de la open()llamada al sistema . Utiliza el siguiente fragmento de código, que es representativo de lo que sucede detrás de escena cuando abre un archivo.

0  int sys_open(const char *filename, int flags, int mode) {
1      char *tmp = getname(filename);
2      int fd = get_unused_fd();
3      struct file *f = filp_open(tmp, flags, mode);
4      fd_install(fd, f);
5      putname(tmp);
6      return fd;
7  }

Brevemente, esto es lo que hace ese código, línea por línea:

  1. Asigne un bloque de memoria controlada por el kernel y copie el nombre del archivo desde la memoria controlada por el usuario.
  2. Elija un descriptor de archivo no utilizado, que puede considerar como un índice entero en una lista ampliable de archivos abiertos actualmente. Cada proceso tiene su propia lista, aunque el núcleo la mantiene; su código no puede acceder a él directamente. Una entrada en la lista contiene cualquier información que el sistema de archivos subyacente utilizará para extraer bytes del disco, como el número de inodo, los permisos de proceso, los indicadores de apertura, etc.
  3. La filp_openfunción tiene la implementación

    struct file *filp_open(const char *filename, int flags, int mode) {
            struct nameidata nd;
            open_namei(filename, flags, mode, &nd);
            return dentry_open(nd.dentry, nd.mnt, flags);
    }

    que hace dos cosas:

    1. Use el sistema de archivos para buscar el inodo (o más generalmente, cualquier tipo de identificador interno que use el sistema de archivos) correspondiente al nombre de archivo o ruta que se pasó.
    2. Cree un struct filecon la información esencial sobre el inodo y devuélvalo. Esta estructura se convierte en la entrada en esa lista de archivos abiertos que mencioné anteriormente.
  4. Almacene ("instale") la estructura devuelta en la lista de archivos abiertos del proceso.

  5. Libere el bloque asignado de memoria controlada por el núcleo.
  6. Devolver el descriptor de archivo, que luego se puede pasar a las funciones de operación de archivos como read(), write(), y close(). Cada uno de estos entregará el control al kernel, que puede usar el descriptor de archivo para buscar el puntero de archivo correspondiente en la lista del proceso, y usar la información en ese puntero de archivo para realizar realmente la lectura, escritura o cierre.

Si se siente ambicioso, puede comparar este ejemplo simplificado con la implementación de la open()llamada del sistema en el kernel de Linux, una función llamada do_sys_open(). No deberías tener problemas para encontrar las similitudes.


Por supuesto, esta es solo la "capa superior" de lo que sucede cuando llama open(), o más precisamente, es el código de kernel de más alto nivel que se invoca en el proceso de abrir un archivo. Un lenguaje de programación de alto nivel podría agregar capas adicionales además de esto. Hay muchas cosas que suceden en los niveles inferiores. (Gracias a Ruslan y pjc50 por su explicación). Aproximadamente, de arriba a abajo:

  • open_namei()e dentry_open()invoque el código del sistema de archivos, que también es parte del núcleo, para acceder a metadatos y contenido de archivos y directorios. El sistema de archivos lee bytes sin procesar del disco e interpreta esos patrones de bytes como un árbol de archivos y directorios.
  • El sistema de archivos utiliza la capa de dispositivo de bloque , nuevamente parte del núcleo, para obtener esos bytes sin procesar de la unidad. (Dato curioso: Linux le permite acceder a datos sin procesar desde la capa del dispositivo de bloque usando /dev/sday similares).
  • La capa de dispositivo de bloque invoca un controlador de dispositivo de almacenamiento, que también es código de núcleo, para traducir de una instrucción de nivel medio como "leer sector X" a instrucciones de entrada / salida individuales en código de máquina. Existen varios tipos de controladores de dispositivos de almacenamiento, incluidos IDE , (S) ATA , SCSI , Firewire , etc., correspondientes a los diferentes estándares de comunicación que una unidad podría usar. (Tenga en cuenta que el nombramiento es un desastre).
  • Las instrucciones de E / S utilizan las capacidades integradas del chip del procesador y el controlador de la placa base para enviar y recibir señales eléctricas en el cable que va a la unidad física. Esto es hardware, no software.
  • En el otro extremo del cable, el firmware del disco (código de control incorporado) interpreta las señales eléctricas para hacer girar los platos y mover los cabezales (HDD), o leer una celda de ROM flash (SSD), o lo que sea necesario para acceder a los datos en ese tipo de dispositivo de almacenamiento.

Esto también puede ser algo incorrecto debido al almacenamiento en caché . :-P En serio, hay muchos detalles que he omitido: una persona (no yo) podría escribir varios libros que describan cómo funciona todo este proceso. Pero eso debería darte una idea.

David Z
fuente
67

Cualquier sistema de archivos o sistema operativo del que quieras hablar está bien para mí. ¡Agradable!


En un ZX Spectrum, la inicialización de un LOADcomando pondrá el sistema en un ciclo cerrado, leyendo la línea de entrada de audio.

El inicio de los datos se indica mediante un tono constante, y luego sigue una secuencia de pulsos largos / cortos, donde un pulso corto es para un binario 0y uno más largo para un binario 1( https://en.wikipedia.org/ wiki / ZX_Spectrum_software ). El bucle de carga ajustada reúne bits hasta que llena un byte (8 bits), lo almacena en la memoria, aumenta el puntero de la memoria y luego vuelve a recorrer para buscar más bits.

Por lo general, lo primero que leería un cargador es un encabezado de formato corto y fijo , que indique al menos el número de bytes que se esperan, y posiblemente información adicional como el nombre del archivo, el tipo de archivo y la dirección de carga. Después de leer este breve encabezado, el programa podría decidir si continúa cargando la mayor parte de los datos o si sale de la rutina de carga y muestra un mensaje apropiado para el usuario.

Se podría reconocer un estado de fin de archivo al recibir tantos bytes como se esperaba (ya sea un número fijo de bytes, cableado en el software o un número variable como se indica en un encabezado). Se produjo un error si el bucle de carga no recibió un pulso en el rango de frecuencia esperado durante un cierto período de tiempo.


Un poco de historia sobre esta respuesta

El procedimiento descrito carga datos de una cinta de audio normal, de ahí la necesidad de escanear la entrada de audio (se conecta con un enchufe estándar a las grabadoras de cinta). Un LOADcomando es técnicamente lo mismo que openun archivo, pero está físicamente vinculado a la carga real del archivo. Esto se debe a que la computadora no controla la grabadora y no puede (con éxito) abrir un archivo pero no cargarlo.

El "lazo cerrado" se menciona porque (1) la CPU, un Z80-A (si la memoria sirve), era realmente lenta: 3.5 MHz, y (2) ¡el Spectrum no tenía reloj interno! Eso significa que tenía que mantener con precisión el recuento de los estados T (tiempos de instrucción) para cada uno. soltero. instrucción. dentro de ese bucle, solo para mantener la sincronización precisa del pitido.
Afortunadamente, esa baja velocidad de la CPU tenía la clara ventaja de que podía calcular el número de ciclos en una hoja de papel y, por lo tanto, el tiempo real que tomarían.

usr2564301
fuente
10
@BillWoodger: bueno, sí. Pero es una pregunta justa (me refiero a la tuya). He votado para cerrar como "demasiado amplio", y mi respuesta tiene la intención de ilustrar cuán extremadamente amplia es realmente la pregunta.
usr2564301
8
Creo que estás ampliando demasiado la respuesta. ZX Spectrum tenía un comando ABIERTO, y eso era totalmente diferente de CARGAR. Y más difícil de entender.
rodrigo
3
Tampoco estoy de acuerdo en cerrar la pregunta, pero realmente me gusta su respuesta.
Enzo Ferber
23
Aunque edité mi pregunta para restringirla al sistema operativo Linux / Windows en un intento de mantenerla abierta, esta respuesta es completamente válida y útil. Como dije en mi pregunta, no estoy buscando implementar algo o hacer que otras personas hagan mi trabajo, estoy buscando aprender. Para aprender debes hacer las preguntas 'grandes'. Si constantemente cerramos preguntas sobre SO por ser 'demasiado amplio', corre el riesgo de convertirse en un lugar para que las personas escriban su código sin dar ninguna explicación de qué, dónde o por qué. Prefiero mantenerlo como un lugar donde pueda venir a aprender.
jramm
14
Esta respuesta parece probar que su interpretación de la pregunta es demasiado amplia, en lugar de que la pregunta en sí es demasiado amplia.
jwg
17

Depende del sistema operativo lo que sucede exactamente cuando abre un archivo. A continuación, describo lo que sucede en Linux, ya que le da una idea de lo que sucede cuando abre un archivo y puede verificar el código fuente si está interesado en obtener más detalles. No estoy cubriendo los permisos, ya que haría que esta respuesta fuera demasiado larga.

En Linux cada archivo es reconocido por una estructura llamada inodo. Cada estructura tiene un número único y cada archivo solo obtiene un número de inodo. Esta estructura almacena metadatos para un archivo, por ejemplo, tamaño de archivo, permisos de archivo, marcas de tiempo y puntero a bloques de disco, sin embargo, no el nombre del archivo en sí. Cada archivo (y directorio) contiene una entrada de nombre de archivo y el número de inodo para la búsqueda. Cuando abre un archivo, suponiendo que tiene los permisos relevantes, se crea un descriptor de archivo utilizando el número de inodo único asociado con el nombre del archivo. Como muchos procesos / aplicaciones pueden apuntar al mismo archivo, inode tiene un campo de enlace que mantiene el recuento total de enlaces al archivo. Si un archivo está presente en un directorio, su recuento de enlaces es uno, si tiene un enlace fijo, su recuento de enlaces será dos y si un archivo se abre mediante un proceso, el recuento de enlaces se incrementará en 1.

Alex
fuente
66
¿Qué tiene esto que ver con la pregunta real?
Bill Woodger el
1
Describe lo que sucede en un nivel bajo cuando abre un archivo en Linux. Estoy de acuerdo en que la pregunta es bastante amplia, por lo que esta podría no haber sido la respuesta que Jramm estaba buscando.
Alex
1
Entonces, de nuevo, ¿no está buscando permisos?
Bill Woodger
11

Teneduría de libros, en su mayoría. Esto incluye varias verificaciones como "¿Existe el archivo?" y "¿Tengo los permisos para abrir este archivo para escribir?".

Pero eso es todo del núcleo: a menos que esté implementando su propio sistema operativo de juguete, no hay mucho en lo que profundizar (si es así, diviértase, es una gran experiencia de aprendizaje). Por supuesto, aún debe aprender todos los códigos de error posibles que puede recibir al abrir un archivo, para que pueda manejarlos correctamente, pero generalmente son pequeñas abstracciones agradables.

La parte más importante en el nivel de código es que le da un control del archivo abierto, que utiliza para todas las demás operaciones que realiza con un archivo. ¿No podría usar el nombre de archivo en lugar de este identificador arbitrario? Bueno, claro, pero usar un mango te da algunas ventajas:

  • El sistema puede realizar un seguimiento de todos los archivos que están abiertos actualmente y evitar que se eliminen (por ejemplo).
  • Los sistemas operativos modernos están construidos alrededor de los identificadores: hay toneladas de cosas útiles que puede hacer con los identificadores, y todos los diferentes tipos de identificadores se comportan de manera casi idéntica. Por ejemplo, cuando una operación de E / S asíncrona se completa en un identificador de archivo de Windows, el identificador se señaliza; esto le permite bloquear el identificador hasta que se señale, o completar la operación de forma completamente asincrónica. Esperar en un identificador de archivo es exactamente lo mismo que esperar en un identificador de subproceso (indicado, por ejemplo, cuando finaliza el subproceso), un identificador de proceso (nuevamente, indicado cuando finaliza el proceso) o un socket (cuando se completa alguna operación asincrónica). Igualmente importante, los identificadores son propiedad de sus respectivos procesos, por lo que cuando un proceso finaliza inesperadamente (o la aplicación está mal escrita), el sistema operativo sabe qué identificadores puede liberar.
  • La mayoría de las operaciones son posicionales: usted readdesde la última posición en su archivo. Al usar un identificador para identificar una "apertura" particular de un archivo, puede tener múltiples identificadores simultáneos para el mismo archivo, cada uno de los cuales se lee desde sus propios lugares. En cierto modo, el identificador actúa como una ventana móvil en el archivo (y una forma de emitir solicitudes de E / S asíncronas, que son muy útiles).
  • Los identificadores son mucho más pequeños que los nombres de archivo. Un identificador suele ser del tamaño de un puntero, normalmente de 4 u 8 bytes. Por otro lado, los nombres de archivo pueden tener cientos de bytes.
  • Los controladores permiten que el sistema operativo mueva el archivo, aunque las aplicaciones lo tengan abierto; el controlador sigue siendo válido y todavía apunta al mismo archivo, aunque el nombre del archivo haya cambiado.

También hay otros trucos que puede hacer (por ejemplo, compartir manejadores entre procesos para tener un canal de comunicación sin usar un archivo físico; en sistemas unix, los archivos también se usan para dispositivos y otros canales virtuales, por lo que esto no es estrictamente necesario) ), pero no están realmente vinculados a la openoperación en sí, por lo que no voy a profundizar en eso.

Luaan
fuente
7

En el fondo, cuando se abre para leer, no es necesario que ocurra nada lujoso . Todo lo que necesita hacer es verificar que el archivo existe y que la aplicación tiene suficientes privilegios para leerlo y crear un controlador en el que pueda emitir comandos de lectura para el archivo.

Es en esos comandos que se enviará la lectura real.

El sistema operativo a menudo obtendrá una ventaja inicial en la lectura al iniciar una operación de lectura para llenar el búfer asociado con el identificador. Luego, cuando realmente hace la lectura, puede devolver el contenido del búfer inmediatamente en lugar de tener que esperar en el disco IO.

Para abrir un nuevo archivo para escribir, el sistema operativo deberá agregar una entrada en el directorio para el nuevo archivo (actualmente vacío). Y nuevamente se crea un identificador en el que puede emitir los comandos de escritura.

monstruo de trinquete
fuente
5

Básicamente, una llamada para abrir necesita encontrar el archivo y luego registrar lo que sea necesario para que las operaciones de E / S posteriores puedan encontrarlo nuevamente. Eso es bastante vago, pero será cierto en todos los sistemas operativos en los que puedo pensar de inmediato. Los detalles varían de una plataforma a otra. Muchas respuestas ya aquí hablan sobre los sistemas operativos de escritorio modernos. He realizado un poco de programación en CP / M, por lo que ofreceré mis conocimientos sobre cómo funciona en CP / M (MS-DOS probablemente funciona de la misma manera, pero por razones de seguridad, normalmente no se hace así hoy) )

En CP / M tiene una cosa llamada FCB (como mencionó C, podría llamarlo una estructura; realmente es un área contigua de 35 bytes en RAM que contiene varios campos). El FCB tiene campos para escribir el nombre de archivo y un entero (4 bits) que identifica la unidad de disco. Luego, cuando llama al archivo abierto del núcleo, pasa un puntero a esta estructura colocándola en uno de los registros de la CPU. Algún tiempo después, el sistema operativo regresa con la estructura ligeramente modificada. Independientemente de la E / S que haga a este archivo, pasará un puntero a esta estructura a la llamada del sistema.

¿Qué hace CP / M con este FCB? Reserva ciertos campos para su propio uso, y los utiliza para realizar un seguimiento del archivo, por lo que es mejor que nunca los toque desde el interior de su programa. La operación Abrir archivo busca en la tabla al inicio del disco un archivo con el mismo nombre que el contenido de la FCB (el carácter comodín '?' Coincide con cualquier carácter). Si encuentra un archivo, copia cierta información en el FCB, incluidas las ubicaciones físicas del archivo en el disco, de modo que las llamadas de E / S posteriores finalmente llamen al BIOS, que puede pasar estas ubicaciones al controlador de disco. En este nivel, los detalles varían.

OmarL
fuente
-7

En términos simples, cuando abre un archivo, en realidad está solicitando al sistema operativo que cargue el archivo deseado (copie el contenido del archivo) del almacenamiento secundario a la memoria RAM para su procesamiento. Y la razón detrás de esto (cargar un archivo) es porque no puede procesar el archivo directamente desde el disco duro debido a su velocidad extremadamente lenta en comparación con Ram.

El comando abrir generará una llamada al sistema que a su vez copia el contenido del archivo desde el almacenamiento secundario (disco duro) al almacenamiento primario (Ram).

Y 'Cerrar' un archivo porque el contenido modificado del archivo debe reflejarse en el archivo original que se encuentra en el disco duro. :)

Espero que ayude.


fuente