saber - ¿Qué hace realmente abrir un archivo?
programas para abrir archivos de todo tipo (8)
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 hecho un poco de programación en CP / M, por lo que 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í 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 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á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 a través de la tabla al comienzo del disco un archivo con el mismo nombre que el contenido del 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, para que las llamadas de E / S posteriores llamen al BIOS, que puede pasar estas ubicaciones al controlador de disco. En este nivel, los detalles varían.
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 función
open
, ¿qué necesitaría hacer en Linux?
Contabilidad, 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 manera 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
read
desde 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 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). - 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 aún 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 operación de
open
en sí, por lo que no voy a profundizar en eso.
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 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 inode . 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 duro, su recuento de enlaces será dos y si un archivo se abre mediante un proceso, el recuento de enlaces se incrementará en 1.
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 función de biblioteca
fopen
, o la
open
de Python, se parecen mucho a los argumentos de la llamada al sistema
open(2)
.
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 al 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 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úa con el archivo abierto, y después de llamar
close
de él, el descriptor deja de ser válido.
Es importante tener en cuenta que la llamada a
open
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 devolver
-1
lugar 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 físicamente abiertos. 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.
En el fondo, cuando se abre para leer, nada lujoso realmente tiene que suceder. 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.
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 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.
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:
- Asigne un bloque de memoria controlada por el kernel y copie el nombre del archivo desde la memoria controlada por el usuario.
- 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 usará para extraer bytes del disco, como el número de inodo, los permisos de proceso, los indicadores de apertura, etc.
-
La función
filp_open
tiene la implementaciónstruct 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:
- 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ó.
-
Cree un
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.
- Libere el bloque asignado de memoria controlada por el núcleo.
-
Devuelva el descriptor de archivo, que luego se puede pasar a funciones de operación de archivo como
read()
,write()
yclose()
. 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 llamada al sistema
open()
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 se llama a
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()
ydentry_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 filesystem 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/sda
y 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.
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 comando
LOAD
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
0
binario y uno más largo para un
1
binario (
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 comando
LOAD
es técnicamente lo mismo que
open
un 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.