En todos los lenguajes de programación (que yo uso 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, mediante el uso de la función se puede decir que implica la creación de algún tipo de objeto que facilita el acceso a un archivo.
Otra forma de expresar esto sería, si tuviera que implementar una función open , ¿qué tendría que hacer en Linux?
En casi todos los lenguajes de alto nivel, la función que abre un archivo es un contenedor alrededor de la llamada al sistema del núcleo correspondiente. También puede hacer otras cosas sofisticadas, pero en los sistemas operativos contemporáneos, abrir un archivo siempre debe pasar por el kernel.
Esta es la razón por la que los argumentos de la función de biblioteca fopen , o open de Python, se parecen mucho a los argumentos de la llamada al sistema open(2) .
Además de abrir el archivo, estas funciones suelen configurar 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 de biblioteca correspondiente devolverá N bytes, independientemente de si las llamadas al sistema subyacente devuelven menos.
No estoy realmente interesado en implementar mi propia función; solo en entender qué diablos está pasando... 'más allá del lenguaje' si se quiere.
En los sistemas operativos similares a Unix, una llamada exitosa para open devuelve 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úe con el archivo abierto y, después de llamar a close , el descriptor deja de ser válido.
Es importante señalar que la llamada a open actúa como un punto de validación en el que se realizan varias comprobaciones. Si no se cumplen todas las condiciones, la llamada falla al devolver -1 en lugar del descriptor, y el tipo de error se indica en errno . Los controles esenciales son:
En el contexto del kernel, 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 trata con dispositivos basados en bloques o un puntero interno que apunta a la posición actual de lectura/escritura.
Depende del sistema operativo qué 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 consultar 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 inode . Cada estructura tiene un número único y cada archivo solo recibe 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 tenga los permisos pertinentes, 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 proceso abre un archivo, el recuento de enlaces se incrementará en 1.
En el centro de esto, cuando se abre para leer, no es necesario que suceda nada sofisticado. Todo lo que necesita hacer es comprobar que el archivo existe y que la aplicación tiene suficientes privilegios para leerlo y crear un identificador 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 archivo nuevo para escribir, el sistema operativo deberá agregar una entrada en el directorio para el archivo nuevo (actualmente vacío). Y nuevamente se crea un identificador en el que puede emitir los comandos de escritura.
Cualquier sistema de archivos o sistema operativo del que quieras hablar está bien para mí. ¡Agradable!
En un ZX Spectrum, inicializar un comando LOAD pondrá el sistema en un bucle 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 0 binario y uno más largo para un 1 binario ( https://en.wikipedia. org/wiki/ZX_Spectrum_software ). El ciclo de carga estrecha reúne bits hasta que llena un byte (8 bits), lo almacena en la memoria, aumenta el puntero de la memoria y luego regresa para buscar más bits.
Por lo general, lo primero que leería un cargador es un encabezado corto de formato fijo, que indica al menos la cantidad 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 continuar cargando la mayor parte de los datos o salir de la rutina de carga y mostrar un mensaje apropiado para el usuario.
Un estado de fin de archivo se puede reconocer al recibir tantos bytes como se espera (ya sea un número fijo de bytes, cableado en el software o un número variable como el que se indica en un encabezado). Se lanzaba un error si el bucle de carga no recibía 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 regular, de ahí la necesidad de escanear la entrada de audio (se conecta con un enchufe estándar a las grabadoras). Un comando LOAD es técnicamente lo mismo que open un archivo, pero está físicamente vinculado a cargar el archivo. Esto se debe a que la grabadora no está controlada por la computadora y no puede (con éxito) abrir un archivo pero no cargarlo.
El "bucle cerrado" se menciona porque (1) la CPU, una 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 llevar la cuenta con precisión de los estados T (tiempos de instrucción) para cada uno. único. instrucción. dentro de ese ciclo, 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 la cantidad de ciclos en una hoja de papel y, por lo tanto, el tiempo real que tomarían.
Le sugiero que eche un vistazo a esta guía a través de una versión simplificada de la llamada al sistema open() . 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:
La función filp_open 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:
struct file con 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.Almacene ("instale") la estructura devuelta en la lista de archivos abiertos del proceso.
read() , write() y close() . Cada uno de estos transferirá el control al núcleo, 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 la lectura, escritura o cierre. Si se siente ambicioso, puede comparar este ejemplo simplificado con la implementación de la llamada al sistema open() en el kernel de Linux, una función llamada do_sys_open() . No deberías tener ningún problema para encontrar las similitudes.
Por supuesto, esta es solo la "capa superior" de lo que sucede cuando llama a open() , o más precisamente, es la pieza de código del 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 explicar). Aproximadamente, de arriba a abajo:
open_namei() y dentry_open() invocan el código del sistema de archivos, que también forma 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./dev/sda y similares).Esto también puede ser algo incorrecto debido al almacenamiento en caché . :-P Hablando 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.
Contabilidad, principalmente. Esto incluye varias comprobaciones como "¿Existe el archivo?" y "¿Tengo los permisos para abrir este archivo para escribir?".
Pero eso es todo sobre el 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 posibles códigos de error 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 brinda un identificador para el archivo abierto, que usa 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:
read desde la última posición en su archivo. Al usar un controlador para identificar una "apertura" particular de un archivo, puede tener varios controladores simultáneos para el mismo archivo, cada uno leyendo 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). También hay algunos otros trucos que puede hacer (por ejemplo, compartir identificadores entre procesos para tener un canal de comunicación sin usar un archivo físico; en los sistemas Unix, los archivos también se usan para dispositivos y varios otros canales virtuales, por lo que esto no es estrictamente necesario ), pero en realidad no están vinculados a la operación open en sí, por lo que no voy a profundizar en eso.
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 que se me ocurran inmediatamente. Los detalles varían de una plataforma a otra. Muchas respuestas que ya están aquí hablan de los sistemas operativos de escritorio modernos. He hecho un poco de programación en CP/M, así que voy a ofrecer mi conocimiento 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í en la actualidad ).
En CP/M tiene una cosa llamada FCB (como mencionó C, podría llamarlo estructura; en realidad es un área contigua de 35 bytes en RAM que contiene varios campos). El FCB tiene campos para escribir el nombre del archivo y un número 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ándolo en uno de los registros de la CPU. Tiempo después, el sistema operativo regresa con la estructura ligeramente modificada. Independientemente de la E/S que realice en este archivo, pasa 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 principio del disco un archivo con el mismo nombre que el que está en el 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.
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) desde el almacenamiento secundario a la RAM para su procesamiento. Y la razón detrás de esto (Cargar un archivo) es que no puede procesar el archivo directamente desde el disco duro debido a su velocidad extremadamente lenta en comparación con la RAM.
El comando abrir generará una llamada al sistema que, a su vez, copiará el contenido del archivo desde el almacenamiento secundario (disco duro) al almacenamiento primario (ram).
Y 'Cerramos' un archivo porque el contenido modificado del archivo debe reflejarse en el archivo original que está en el disco duro. :)
Espero que ayude.