Programación del sistema Linux (1): E/S de archivos

Referencias

1. Conocimientos básicos de UNIX

1.1 Arquitectura UNIX (como se muestra en la siguiente figura)

  • En sentido estricto, un sistema operativo puede definirse como un software que controla los recursos de hardware de la computadora y proporciona un entorno de ejecución de programas. Este software generalmente se denomina núcleo porque es relativamente pequeño y está ubicado en el núcleo del entorno.
    • La interfaz del kernel se llama llamada al sistema (área sombreada en la siguiente figura)
    • La biblioteca de funciones públicas se basa en la interfaz de llamada del sistema. Las aplicaciones pueden usar la biblioteca de funciones públicas o la llamada del sistema.
    • Un shell es una aplicación especial que proporciona una interfaz para ejecutar otras aplicaciones.

Insertar descripción de la imagen aquí

1.2 Archivos y directorios

1.2.1 Sistema de archivos

  • El sistema de archivos UNIX es una estructura jerárquica de directorios y archivos. El punto de partida de todo es un directorio llamado raíz . El nombre de este directorio es el carácter "/".
  • Un directorio es un archivo que contiene entradas de directorio. Lógicamente, se puede considerar que cada entrada de directorio contiene un nombre de archivo e información que describe los atributos del archivo .
    • Los atributos del archivo se refieren al tipo de archivo (ya sea un archivo normal o un directorio, etc.), el tamaño del archivo, el propietario del archivo, los permisos del archivo (si otros usuarios pueden acceder al archivo) y la hora de la última modificación del archivo , etc. .
    • Las funciones stat y fstat devuelven una estructura de información que contiene todos los atributos del archivo.

1.2.2 Nombre del archivo

  • Cada nombre en el directorio se llama nombre de archivo (flename)
    • Sólo dos caracteres, la barra diagonal (/) y el carácter nulo, no pueden aparecer en los nombres de archivos.
    • Las barras diagonales se utilizan para separar los nombres de archivos que forman un nombre de ruta y los caracteres nulos se utilizan para terminar un nombre de ruta.
  • Para portabilidad, POSIX.1 recomienda limitar los nombres de archivos a los siguientes conjuntos de caracteres: letras (a~z, A~Z), números (0~9), puntos (.), guiones (-) y guión bajo (_).
  • Cuando crea un nuevo directorio, se crean automáticamente dos nombres de archivo: (llamado punto) y... (llamado punto)
    • El punto apunta al directorio actual y el punto apunta al directorio principal.
    • En el nivel superior del directorio raíz, punto punto es lo mismo que punto

1.2.3 Nombre de ruta

  • Una secuencia de uno o más nombres de archivos separados por barras (también puede comenzar con una barra) se convierte en un nombre de ruta (pathmamme)
    • Un nombre de ruta que comienza con una barra diagonal es un nombre de ruta absoluto; de lo contrario, se llama nombre de ruta relativa . Un nombre de ruta relativa apunta a un archivo relativo al directorio actual.
    • El nombre de la raíz del sistema de archivos (/) es un nombre de ruta absoluta especial que no contiene el nombre del archivo.

1.2.4 Directorio de trabajo

  • Cada proceso tiene un directorio de trabajo, a veces llamado directorio de trabajo actual. Todos los nombres de rutas relativas se interpretan a partir del directorio de trabajo. Un proceso puede cambiar su directorio de trabajo usando la función chdir.
  • El nombre de ruta relativa doc/memo/joe se refiere al archivo (o directorio) joe en el directorio memo en el directorio doc en el directorio de trabajo actual.
    • Por el nombre de la ruta se puede ver que tanto doc como memo deben ser directorios, pero no se puede distinguir si joe es un archivo o un directorio.
  • El nombre de ruta /urs/lib/lint es un nombre de ruta absoluto , que se refiere al archivo (o directorio) lint en el directorio lib en el directorio usr en el directorio raíz.

1.3 Entrada y salida

1.3.1 Descriptor de archivo

  • Un descriptor de archivo suele ser un número entero pequeño y no negativo que el núcleo utiliza para identificar el archivo al que accede un proceso específico . Cuando el kernel abre un archivo existente o crea un archivo nuevo, devuelve un descriptor de archivo.

1.3.2 Entrada estándar, salida estándar y error estándar

  • Cada vez que se ejecuta un nuevo programa, todos los shells abren tres descriptores de archivos: entrada estándar, salida estándar y error estándar.

1.3.3 E/S sin búfer

  • Las funciones abrir, leer, escribir, buscar y cerrar proporcionan E/S sin búfer. Estas funciones utilizan descriptores de archivos.

1.3.4 E/S estándar

  • Las funciones de E/S estándar proporcionan una interfaz con búfer para aquellas funciones de E/S sin búfer. La función de E/S estándar más familiar es printf.

1.4 Procedimientos y procesos

1.4.1 Procedimiento

  • Un programa es un archivo ejecutable almacenado en un directorio del disco . El kernel usa la función exec para leer el programa en la memoria y ejecutarlo.

1.4.2 Procesos e ID de procesos

  • La instancia de ejecución de un programa se llama proceso y algunos sistemas operativos utilizan tareas para representar el programa que se está ejecutando.
  • Los sistemas UNIX garantizan que cada proceso tenga un identificador numérico único, llamado ID de proceso. El ID del proceso es siempre un número entero no negativo.

1.4.3 Control de procesos

  • Hay 3 funciones principales para el control de procesos: fork, exec y waitpid (hay 7 variaciones de la función exec, pero a menudo se las denomina colectivamente función exec)

1.4.4 Hilos e ID de hilos

  • Por lo general, un proceso tiene un solo hilo de control: un conjunto de instrucciones de máquina ejecutadas en un momento determinado . Algunos problemas son mucho más fáciles de resolver si hay múltiples hilos de control trabajando en diferentes partes. Además, múltiples subprocesos de control también pueden aprovechar al máximo las capacidades paralelas de los sistemas multiprocesador.
  • Todos los subprocesos dentro de un proceso comparten el mismo espacio de direcciones, descriptores de archivos, pila y atributos relacionados con el proceso . Como tienen acceso a la misma área de almacenamiento, los subprocesos necesitan sincronizar su acceso a los datos compartidos para evitar inconsistencias.
  • Al igual que los procesos, los subprocesos también se identifican por ID . Sin embargo, un hilo sólo funciona dentro del proceso al que pertenece. Un ID de hilo en un proceso no tiene significado en otro proceso. Cuando trabaja en un hilo específico en un proceso, puede hacer referencia a él utilizando el ID del hilo.

1.5 Manejo de errores

  • Cuando ocurre un error en una función del sistema UNIX, generalmente se devuelve un valor negativo y la variable entera errno generalmente se establece en un valor con información específica. Algunas funciones utilizan otra convención en lugar de devolver un valor negativo en caso de error. Por ejemplo, la mayoría de las funciones que devuelven un puntero a un objeto devolverán un puntero nulo en caso de error.
  • POSIX.1 e ISO C definen errno como un símbolo que se expande a un valor l entero modificable
    • Puede ser un número entero que contenga el número de error o una función que devuelva un puntero al número de error.
  • En un entorno que admite subprocesos, varios subprocesos comparten el espacio de direcciones del proceso y cada subproceso tiene su propio error local para evitar que un subproceso interfiera con otro subproceso.
  • Se deben tener en cuenta dos reglas para errno
    • Primero: si no se produce ningún error, la rutina no borra su valor. Por lo tanto, el valor de una función se verifica solo si su valor de retorno indica un error.
    • Segundo: ninguna función establecerá el valor de errno en 0 y todas las constantes definidas en <errno.h> no son 0

1.6 Identificación del usuario

1.6.1 Identificación de usuario

  • La ID de usuario en la entrada del archivo de contraseñas es un valor numérico que identifica a cada usuario diferente en el sistema. El administrador del sistema determina la ID de usuario de un usuario al mismo tiempo que determina el nombre de inicio de sesión de un usuario. Los usuarios no pueden cambiar su ID de usuario; normalmente cada usuario tiene una ID de usuario única.
  • El usuario con ID de usuario 0 es el usuario root (root) o superusuario (superusuario) . En el archivo de contraseñas, suele haber un elemento de inicio de sesión cuyo nombre de inicio de sesión es root, y los privilegios de este usuario se denominan privilegios de superusuario. Ciertas funciones del sistema operativo sólo están disponibles para superusuarios, que tienen libre control sobre el sistema.

1.6.2 ID de grupo

  • La entrada del archivo de contraseñas también incluye el ID del grupo del usuario, que es un valor numérico. El administrador del sistema también asigna los ID de grupo al especificar un nombre de inicio de sesión de usuario. Generalmente, hay varios inicios de sesión con el mismo ID de grupo en el archivo de contraseña . Los grupos se utilizan para agrupar usuarios en proyectos o departamentos. Este mecanismo permite compartir recursos entre miembros de un mismo grupo.
  • El archivo de grupo asigna el nombre del grupo a un ID de grupo numérico. El archivo de grupo suele ser /etc/group
  • Para cada archivo en el disco, el sistema de archivos almacena el ID de usuario y el ID de grupo del propietario del archivo. Solo se necesitan 4 bytes para almacenar estos dos valores (suponiendo que cada uno se almacene como un valor entero de doble byte) . Durante la verificación de permisos, comparar cadenas lleva más tiempo que comparar números enteros.
  • Pero para los usuarios, es más conveniente usar nombres que números, por lo que el archivo de contraseña contiene la relación de mapeo entre los nombres de inicio de sesión y los ID de usuario, y el archivo de grupo contiene la relación de mapeo entre los nombres de grupo y el grupo D.

1.7 Señales

  • Una señal se utiliza para notificar a un proceso que algo ha sucedido . Por ejemplo, si un proceso realiza una operación de división y su divisor es 0, se envía al proceso una señal denominada SIGEPE (Excepción de punto flotante). Los procesos tienen las siguientes tres formas de manejar señales:

    • (1) Ignora la señal . Algunas señales indican excepciones de hardware, como dividir por 0 o acceder a unidades de almacenamiento fuera del espacio de direcciones del proceso. Debido a que las consecuencias de estas excepciones son inciertas, no se recomienda este método de procesamiento.
    • (2) Procese según el método predeterminado del sistema . Para un divisor de 0, el método predeterminado del sistema es finalizar el proceso.
    • (3) Proporcione una función que se llama cuando ocurre una señal, que se llama captar la señal . Al proporcionar una función autoescrita, puede saber cuándo se genera una señal y manejarla de la manera deseada.
  • Las señales pueden ocurrir en muchas situaciones. Hay dos formas de generar señales en el teclado del terminal.

    • La tecla de interrupción (normalmente la tecla Eliminar o Crl+C) y la tecla de salida (normalmente Ctrl+\) , que se utilizan para interrumpir el proceso que se está ejecutando actualmente.
    • Llame a la función matar . Llamar a esta función desde un proceso envía una señal a otro proceso. Por supuesto, existen algunas limitaciones para esto: al enviar una señal a un proceso, debes ser el propietario de ese proceso o el superusuario.

1.8 Valor del tiempo

  • Los sistemas UNIX han utilizado dos valores de tiempo diferentes.
    • (1) Hora del calendario . Este valor es el número acumulado de segundos que han transcurrido desde una hora específica a las 00:00:00 del 1 de enero de 1970, hora universal coordinada (UTC) (los primeros manuales se referían a UTC como hora media de Greenwich). Estos valores de tiempo se pueden utilizar para registrar la hora de la última modificación del archivo, etc.
      • El tipo de datos básico del sistema time_t se utiliza para guardar este valor de tiempo.
    • (2) Tiempo de proceso . También conocido como tiempo de CPU, mide los recursos de CPU utilizados por un proceso. El tiempo del proceso se mide en tics de reloj. Cada segundo solía considerarse como 50, 60 o 100 tics de reloj.
      • El tipo de datos básico del sistema clock_t almacena este valor de tiempo.
  • Al medir el tiempo de ejecución de un proceso, el sistema UNIX mantiene 3 valores de tiempo de proceso para un proceso
    • Hora del reloj
      • El tiempo del reloj también llamado tiempo del reloj de pared, es el tiempo total que se ejecuta un proceso, su valor está relacionado con la cantidad de procesos que se ejecutan simultáneamente en el sistema.
    • Tiempo de CPU del usuario
      • El tiempo de CPU del usuario es la cantidad de tiempo dedicado a ejecutar las instrucciones del usuario.
    • Tiempo de CPU del sistema
      • El tiempo de CPU del sistema es el tiempo que tarda el proceso en ejecutar el programa del kernel.
      • La suma del tiempo de CPU del usuario y el tiempo de CPU del sistema a menudo se denomina tiempo de CPU.

1.9 Llamadas al sistema y funciones de biblioteca

  • ¿Qué es una llamada al sistema?

    • La interfaz de programación de aplicaciones (API) implementada por el sistema operativo y proporcionada a aplicaciones externas es un puente para la interacción de datos entre las aplicaciones y el sistema.
    • Todos los sistemas operativos proporcionan puntos de entrada para diversos servicios a través de los cuales los programas solicitan servicios del kernel. Varias versiones de implementaciones de UNIX proporcionan un número limitado y bien definido de puntos de entrada directamente al núcleo, estos puntos de entrada se denominan llamadas al sistema.
  • Las funciones de biblioteca genéricas pueden invocar una o más llamadas al sistema del kernel, pero no son el punto de entrada al kernel.

    • Por ejemplo, la función printf llama a la llamada al sistema de escritura para generar una cadena
    • Pero las funciones strcpy (copiar una cadena) y atoi (convertir ASCII a entero) no utilizan ninguna llamada al sistema del kernel.
  • Las llamadas al sistema y las funciones de biblioteca toman la forma de funciones C, las cuales brindan servicios a la aplicación.

    • Las funciones de la biblioteca se pueden reemplazar, pero las llamadas al sistema generalmente no se pueden reemplazar
    • Las llamadas al sistema suelen proporcionar una interfaz mínima, mientras que las funciones de la biblioteca suelen proporcionar funciones más complejas.
  • Función de biblioteca estándar de C y función del sistema/relación de llamada: un caso de cómo imprimir "hola" en la pantalla

    • La llamada al sistema es equivalente a una encapsulación superficial de la función del sistema (función en la página de manual)

Insertar descripción de la imagen aquí

2. Estándares e implementación de UNIX

2.1 estandarización UNIX

2.1.1 IOS C

  • El estándar ISO C ahora es mantenido y desarrollado por el Grupo de Trabajo de Estándares Internacionales de ISO/TEC para el Lenguaje de Programación C. El grupo de trabajo se llama ISO/IEC JTC1/SC22/WG14, o WG14 para abreviar. La intención del estándar ISO C es proporcionar portabilidad de los programas C a una gran cantidad de sistemas operativos diferentes, no solo a los sistemas UNIX.
  • Archivos de encabezado definidos por el estándar ISO C

Insertar descripción de la imagen aquí

2.1.2 IEEE POSIX.1

  • POSIX.1 es una familia de estándares desarrollados originalmente por el IEEE (Instituto de Ingenieros Eléctricos y Electrónicos). POSIX.1 se refiere a la interfaz del sistema operativo portátil . Originalmente se refería solo al estándar IEEE 1003.1-1988 (Interfaz del sistema operativo), pero luego se amplió para incluir muchos estándares y borradores de estándares marcados como 1003, como shells y utilidades (1003.2, este tutorial usa 1003.1).
    • Dado que el estándar 1003.1 especifica una interfaz en lugar de una implementación, no distingue entre llamadas al sistema y funciones de biblioteca. Todas las rutinas del estándar se denominan funciones.
  • Archivos de encabezado requeridos definidos por el estándar POSIX.1

Insertar descripción de la imagen aquí

2.2 Implementación del sistema UNIX

2.2.1 4.4 BSD

  • BSD (Berkeley Sofware Distibution) fue desarrollado y distribuido por el Grupo de Investigación de Sistemas Computacionales de la Universidad de California, Berkeley. 4.2BSD salió en 1983, 4.3BSD fue lanzado en 1986 y 4.4BSD fue lanzado en 1994.

2.2.2 LibreBSD

  • FreeBSD está basado en el sistema operativo 4.4BSD-Lite. Después de que el Grupo de Investigación de Sistemas Informáticos de la Universidad de California, Berkeley, decidiera poner fin a su trabajo de investigación y desarrollo en la versión BSD del sistema operativo UNIX, y el proyecto 386BSD fuera ignorado durante mucho tiempo, se formó el proyecto FreeBSD para para continuar adhiriéndose a la serie BSD.

2.2.3 Linux

  • Linux fue desarrollado por Linus Torvalds en 1991 como reemplazo de MNIX
  • Linux es un sistema operativo que proporciona un rico entorno de programación similar a UNIX. Linux es de uso gratuito bajo la guía de la licencia pública GNU.

2.2.4 MacOS X

  • Mac OS X utiliza una tecnología completamente diferente a sus versiones anteriores. Su sistema operativo principal se llama "Darwin" y se basa en una combinación del kernel Mach, el sistema operativo FreeBSD y controladores con marcos orientados a objetos y otras extensiones del kernel.

2.2.5 Solaris

  • Solaris es una versión de UNIX desarrollada por Sun Microsystems (ahora Oracle)

2.3 Tipos de datos básicos del sistema

  • Ciertos tipos de datos relacionados con la implementación se definen en el archivo de encabezado <sys/types.h>, que se denominan tipos de datos básicos del sistema.

  • Algunos tipos de datos del sistema básicos de uso común

Insertar descripción de la imagen aquí

3. E/S de texto

3.1 Introducción

  • Funciones de E/S de archivos disponibles: abrir (abrir) archivo, leer (leer) archivo, escribir (escribir) archivo, etc.
  • La mayoría de las E/S de archivos en sistemas UNIX requieren solo 5 funciones: abrir, leer, escribir, buscar y cerrar.

Las funciones descritas en este capítulo a menudo se denominan E/S sin búfer (a diferencia de las funciones de E/S estándar).

  • Sin búfer significa que cada lectura y escritura llama a una llamada al sistema en el kernel.
  • Estas funciones de E/S sin búfer no son parte de ISO C, pero sí de POSIX1

3.2 Descriptor de archivo

  • Para el kernel, todos los archivos abiertos están referenciados por descriptores de archivos.

    • El descriptor del archivo es un número entero no negativo.
    • Al abrir un archivo existente o crear un archivo nuevo, el kernel devuelve un descriptor de archivo al proceso
    • Al leer o escribir un archivo, utilice el descriptor de archivo devuelto por open o creat para identificar el archivo y páselo como parámetro para leer o escribir.
  • Por convención, los shells del sistema UNIX

    • El descriptor de archivo 0 está asociado con la entrada estándar del proceso.
    • El descriptor de archivo 1 está asociado con la salida estándar del proceso.
    • El descriptor de archivo 2 está asociado con el error estándar del proceso.
  • En aplicaciones compatibles con POSIX.1, los números mágicos 0, 1 y 2, aunque están estandarizados, deben reemplazarse con las constantes simbólicas STDIN_FILENO, STDOUT_FILENO y STDERR_FILENO para mejorar la legibilidad. Estas constantes se definen en el archivo de encabezado <unistd.h>

Un descriptor de archivo es un puntero a un
bloque de control de proceso de PCB de estructura de archivos: esencialmente una estructura, sus miembros son tablas de descriptores de archivos.

Insertar descripción de la imagen aquí

3.3 Funciones open y openat (abrir o crear un archivo)

3.3.1 Función de apertura y análisis de parámetros de apertura

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>  // 定义 flags 参数

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // 仅当创建新文件时才使用第三个参数,表明文件权限

int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
  • nombre de ruta: el nombre de ruta del archivo que se abrirá o creará
  • flags: Se utiliza para describir múltiples opciones de esta función. Utilice una o más de las siguientes constantes para realizar una operación "O" para formar el parámetro flags.
    • O_RDONLY (abierto solo para lectura), O_WRONLY (abierto solo para escritura), O_RDWR (abierto para lectura y escritura), O_EXEC (abierto solo para ejecución), O_SEARCH (abierto solo para búsqueda, usado para directorios)
    • O_APPEND (agregar al final del archivo cada vez que escribe)
    • O_CREAT (si este archivo no existe, créelo y utilícelo junto con el tercer parámetro mode )
    • O_EXCL (si también se especifica O_CREAT y el archivo ya existe, se produce un error)
    • O_NONBLOCK (Establezca el modo sin bloqueo para esta operación de apertura de archivos y operaciones de E/S posteriores )
    • O_TRUNC (si este archivo existe y se abre correctamente para solo escritura o lectura-escritura, trunca su longitud a 0 )
  • valor de retorno de la función
    • Si tiene éxito, devuelve el descriptor del archivo.
    • Si se produce un error, se devuelve -1
  • El parámetro dirfd distingue las funciones open y openat, hay tres posibilidades.
    • El parámetro de ruta especifica un nombre de ruta absoluto. En este caso, el parámetro dirfd se ignora y la función openat es equivalente a la función open.
    • El parámetro de ruta especifica el nombre de la ruta relativa y el parámetro dirfd indica la dirección inicial del nombre de la ruta relativa en el sistema de archivos. El parámetro dirfd se obtiene abriendo el directorio donde se encuentra el nombre de la ruta relativa.
    • El parámetro de ruta especifica un nombre de ruta relativo y el parámetro dirfd tiene el valor especial AT_FDCWD. En este caso, el nombre de la ruta se toma en el directorio de trabajo actual y la función openat es similar en funcionamiento a la función open.
  • La función openat es una de las nuevas funciones en la última versión de POSIX.1, con la esperanza de resolver dos problemas.
    • Primero, permita que los subprocesos utilicen nombres de rutas relativas para abrir archivos en el directorio, en lugar de abrir solo el directorio de trabajo actual.
      • Todos los subprocesos del mismo proceso comparten el mismo directorio de trabajo actual, por lo que es difícil tener varios subprocesos diferentes del mismo proceso trabajando en diferentes directorios al mismo tiempo.
    • En segundo lugar, se pueden evitar los errores de tiempo de verificación a tiempo de uso (TOCTTOU).
      • La idea básica del error TOCTTOU es: si hay dos llamadas a funciones basadas en archivos, donde la segunda llamada depende del resultado de la primera llamada, entonces el programa es vulnerable . Debido a que las dos llamadas no son operaciones atómicas, es posible que el archivo haya cambiado entre las dos llamadas a funciones, lo que hará que el resultado de la primera llamada ya no sea válido, lo que hará que el resultado final del programa sea incorrecto.

3.3.2 Truncamiento de nombre de archivo y ruta

  • En POSIX.1, la constante _POSIX_NO_TRUNC determina si se truncan los nombres de archivos o los nombres de rutas que son demasiado largos, o si se devuelve un error . Dependiendo del tipo de sistema de archivos, este valor puede variar. Puede utilizar fpathconf o pathconf para consultar qué tipo de comportamiento admite un directorio: ¿truncar nombres de archivos demasiado largos o devolver un error?
  • Si _POSIX_NO_TRUNC es válido, cuando el nombre de la ruta completa excede PATH_MAX, o cualquier nombre de archivo en el nombre de la ruta excede NAME_MAX, se devuelve un error y errno se establece en ENAMETOOLONG

3.4 Función cerrar (cerrar un archivo abierto)

#include <unistd.h>

int close(int fd);
  • valor de retorno de la función

    • Si tiene éxito, devuelve 0
    • Si se produce un error, se devuelve -1
  • Cerrar un archivo también libera todos los bloqueos de registros mantenidos por el proceso en el archivo.

  • Cuando finaliza un proceso, el kernel cierra automáticamente todos los archivos abiertos. Muchos programas aprovechan esta característica sin cerrar explícitamente el archivo con close.

3.5 Función crear (crear un nuevo archivo)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int creat(const char *pathname, mode_t mode);
  • valor de retorno de la función

    • Si tiene éxito, devuelve el descriptor del archivo abierto sólo para escritura.
    • Si se produce un error, se devuelve -1
  • Esta función es equivalente a

    open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
    

Una desventaja de creat es que abre el archivo creado sólo para escribir . Antes de que se proporcionara la nueva versión de open, si deseaba crear un archivo temporal, escribir en el archivo y luego leerlo, debía llamar a creat, cerrar y luego abrir. Ahora puedes llamar a la implementación abierta de la forma anterior.

3.3-3.5 Caso

Caso 1

// open.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    fd = open("./AUTHORS.txt", O_RDONLY);
    printf("fd = %d\n", fd);
    
    close(fd);	
    
    return 0;
}
$ gcc open.c -o open

$ ./open
# 输出如下,表示文件存在并正确打开
fd = 3

Caso 2

// open2.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT, 0644); // rw-r--r--
    printf("fd = %d\n", fd);

    close(fd);

    return 0;
}
$ gcc open2.c -o open2

$ ./open2
fd = 3

$ ll 
# 创建了一个新文件 AUTHORS.cp,且文件权限对应于 0644
-rw-r--r-- 1 yue yue    0 9月  10 22:19 AUTHORS.cp

Caso 3

// open3.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    // 如果文件存在,以只读方式打开并且截断为 0
    // 如果文件不存在,则把这个文件创建出来并指定权限为 0644
    fd = open("./AUTHORS.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // rw-r--r--
    printf("fd = %d\n", fd);

    close(fd);

    return 0;
}
$ gcc open3.c -o open3

$ ./open3
# 输出如下,表示文件存在并正确打开
fd = 3

$ ll 
# 首先在 AUTHORS.cp 文件中输入内容,然后经过 O_TRUNC 截断后为 0
-rw-r--r-- 1 yue yue    0 9月  10 22:19 AUTHORS.cp

Caso 4

  • Al crear un archivo, especifique el modo de permiso de acceso al archivo, y los permisos también se ven afectados por umask. La conclusión es
    • Permisos de archivo = modo y ~umask
$ umask
0002 # 表明默认创建文件权限为 ~umask = 775(第一个 0 表示八进制)
// open4.c
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    
    
    int fd;
    fd = open("./AUTHORS.cp2", O_RDONLY | O_CREAT | O_TRUNC, 0777); // rwxrwxrwx
    printf("fd = %d\n", fd);

    close(fd);

    return 0;
}
$ gcc open4.c -o open4

$ ./open4
fd = 3

$ ll 
# 创建了一个新文件 AUTHORS.cp2,且文件权限为 mode & ~umask = 775(rwxrwxr-x)
-rwxrwxr-x 1 yue yue    0 9月  10 22:38 AUTHORS.cp2*

Caso 5

  • Errores comunes en la función abierta.
    • El archivo abierto no existe
    // open5.c
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd;
    
        fd = open("./AUTHORS.cp4", O_RDONLY);
        printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
    
        close(fd);
    
        return 0;
    }
    
    $ gcc open5.c -o open5
    
    $ ./open5
    fd = -1, errno = 2 : No such file or directory
    
    • Abra un archivo de solo lectura en modo de escritura (no existe el permiso correspondiente para abrir el archivo)
    // open6.c
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd;
    
        fd = open("./AUTHORS.cp3", O_WRONLY); // AUTHORS.cp3 文件权限为只读
        printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
    
        close(fd);
    
        return 0;
    }
    
    $ gcc open6.c -o open6
    
    $ ./open6
    fd = -1, errno = 13 : Permission denied
    
    • Abrir directorio solo para escritura
    $ mkdir mydir # 首先创建一个目录
    
    // open7.c
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd;
    
        fd = open("mydir", O_WRONLY);
        printf("fd = %d, errno = %d : %s\n", fd, errno, strerror(errno));
    
        close(fd);
    
        return 0;
    }
    
    $ gcc open7.c -o open7
    
    $ ./open7
    fd = -1, errno = 21 : Is a directory
    

3.6 Función lseek (establece explícitamente el desplazamiento para un archivo abierto)

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
  • Cada archivo abierto tiene asociado un "desplazamiento de archivo actual", que suele ser un número no negativo que mide el número de bytes contados desde el principio del archivo.

  • La l en lseek representa el tipo entero largo

  • valor de retorno de la función

    • Si tiene éxito, devuelva el nuevo desplazamiento del archivo
    • Si se produce un error, se devuelve -1
  • De forma predeterminada, cuando se abre un archivo, este desplazamiento se establece en 0 a menos que se especifique la opción O_APPEND.

  • La interpretación del desplazamiento del parámetro está relacionada con el valor del parámetro de donde

    • Si de dónde es SEEK_SET, el desplazamiento del archivo se establece en bytes de desplazamiento desde el principio del archivo.
      • SEEK_SET(0) compensación absoluta
    • Si de dónde es SEEK_CUR, el desplazamiento del archivo se establece en su valor actual más el desplazamiento. El desplazamiento puede ser positivo o negativo.
      • SEEK_CUR(1) Desplazamiento relativo a la posición actual
    • Si de dónde es SEEK_END, el desplazamiento del archivo se establece en la longitud del archivo más el desplazamiento. El desplazamiento puede ser positivo o negativo.
      • SEEK_END (2) desplazamiento relativo al final del archivo
  • lseek solo registra el desplazamiento del archivo actual en el kernel, no provoca ninguna operación de E/S. Este desplazamiento se utiliza luego para la siguiente operación de lectura o escritura.

  • El desplazamiento del archivo puede ser mayor que la longitud actual del archivo , en cuyo caso la siguiente escritura en el archivo lo alargará y creará un agujero en el archivo, lo cual está permitido. Los bytes que están en el archivo pero que no se han escrito se leen como 0

Caso 1

  • La lectura y escritura de archivos utilizan la misma posición de desplazamiento
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    
    int main(void) {
          
          
        int fd, n;
        char msg[] = "It's a test for lseek\n";
        char ch;
    
        fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);
        if (fd < 0) {
          
          
            perror("open lseek.txt error");
            exit(1);
        }
    
        // 使用 fd 对打开的文件进行写操作,读写位置位于文件结尾处
        write(fd, msg, strlen(msg));
        // 若注释下行代码,由于文件写完之后未关闭,读、写指针在文件末尾,所以不调节指针,直接读取不到内容
        lseek(fd, 0, SEEK_SET); // 修改文件读写指针位置,位于文件开头
    
        while ((n = read(fd, &ch, 1))) {
          
          
            if (n < 0) {
          
          
                perror("read error");
                exit(1);
            } 
            write(STDOUT_FILENO, &ch, n);  // 将文件内容按字节读出,写出到屏幕
        }
    
        close(fd);
    
        return 0;
    }
    

Caso 2

  • Utilice lseek para obtener el tamaño del archivo
    // lseek_size.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd = open(argv[1], O_RDWR);
        if (fd == -1) {
          
          
            perror("open error");
            exit(1);
        }
    
        int length = lseek(fd, 0, SEEK_END);
        printf("file size: %d\n", length);
    
        close(fd);
    
        return 0;
    }
    
    $ gcc lseek_size.c -o lseek_size
    $ ./lseek_size fcntl.c  # fcntl.c 文件大小为 678
    678
    

Caso 3

  • Ampliar el tamaño del archivo usando lseek
    • Para que el tamaño del archivo se expanda realmente, se deben provocar operaciones de E/S
    // 修改案例 2 中下行代码(扩展 111 大小)
    // 这样并不能真正扩展,使用 cat 命令查看文件大小未变化
    int length = lseek(fd, 111, SEEK_END);
    
    // 在 printf 函数下行写如下代码(引起 IO 操作)
    write(fd, "\0", 1); // 结果便是在扩展的文件尾部追加文件空洞
    
  • Los archivos se pueden ampliar directamente usando la función truncar
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    
    int main(int argc, char*argv[]) {
          
          
        int ret = truncate("dict.cp", 250);
        printf("ret = %d\n", ret);
    
        return 0;
    }
    

El tamaño del archivo leído por lseek siempre es relativo al encabezado del archivo. El uso de lseek para leer el tamaño del archivo en realidad utiliza la diferencia de desplazamiento entre las posiciones inicial y final de los punteros de lectura y escritura . Para un archivo recién abierto, las posiciones iniciales de los punteros de lectura y escritura están al principio del archivo. Si usa esto para expandir el tamaño del archivo, debe causar IO, por lo que se debe escribir al menos un carácter.

3.7 Función de lectura (leer datos de un archivo abierto)

#include <unistd.h>

// ssize_t 表示带符号整型;void* 表示通用指针
// 参数1:文件描述符;参数2:存数据的缓冲区;参数3:缓冲区大小
ssize_t read(int fd, void *buf, size_t count);
  • valor de retorno de la función
    • Si la lectura es exitosa, se devuelve el número de bytes leídos. Si se llega al final del archivo, se devuelve 0.
    • Si se produce un error, se devuelve -1
    • Si se devuelve -1 y errno = EAGIN o EWOULDBLOCK, significa que no es que la lectura haya fallado, sino que esa lectura está leyendo un archivo de dispositivo/archivo de red sin bloqueo, y el archivo no tiene datos.
  • Hay varias situaciones en las que el número real de bytes leídos es menor que el número solicitado de bytes leídos.
    • 1. Al leer un archivo normal, se llega al final del archivo antes de leer la cantidad requerida de bytes.
      • Por ejemplo, si faltan 30 bytes antes de llegar al final del archivo y se requieren 100 bytes para leer, leer devuelve 30. La próxima vez que se llame a read, devolverá 0 (fin del archivo)
    • 2. Cuando se lee desde un dispositivo terminal, normalmente se lee una línea como máximo a la vez.
    • 3. Al leer de la red, el mecanismo de almacenamiento en búfer de la red puede hacer que el valor de retorno sea menor que la cantidad de bytes necesarios para leer.
    • 4. Al leer desde una tubería o FIFO, si la tubería contiene menos del número requerido de bytes, la lectura solo devolverá el número real de bytes disponibles.
    • 5. Cuando se lee desde algunos dispositivos orientados a registros (como cintas), se devuelve como máximo un registro a la vez.
    • 6. Cuando una señal provoca una interrupción y se ha leído parte de los datos

3.8 Función de escritura (escribir datos para abrir un archivo)

#include <unistd.h>

// 参数1:文件描述符;参数2:待写出数据的缓冲区;参数3:数据大小
ssize_t write(int fd, const void *buf, size_t count);
  • valor de retorno de la función

    • Si la escritura se realiza correctamente, se devuelve el número de bytes escritos ( el valor de retorno suele ser el mismo que el valor de recuento del parámetro; de lo contrario, se produce un error ).
    • Si se produce un error, se devuelve -1
  • Una razón común para los errores de escritura es que el disco está lleno o que se ha excedido el límite de longitud de archivo para un proceso determinado.

  • Para archivos normales, la escritura comienza en el desplazamiento actual del archivo. Si se especifica la opción O_APPEND cuando se abre el archivo, el desplazamiento del archivo se establece en el final actual del archivo antes de cada operación de escritura. Después de una escritura exitosa, el desplazamiento del archivo se incrementa según el número de bytes realmente escritos.

Bloqueo y no bloqueo

  • Bloquear : cuando un proceso llama a una función del sistema de bloqueo, el proceso se coloca en un estado de suspensión. En este momento, el kernel programa la ejecución de otros procesos hasta que ocurra el evento que el proceso está esperando (como recibir un paquete de datos en la red). ). , o cuando el tiempo de suspensión especificado al llamar a suspensión finaliza), es posible que continúe ejecutándose. Lo opuesto al estado de suspensión es el estado de ejecución. En el kernel de Linux, los procesos en estado de ejecución se dividen en dos situaciones:

    • Estar programado para su ejecución . La CPU está en el contexto del proceso. El contador de programa almacena la dirección de instrucción del proceso. El registro general almacena los resultados intermedios de la operación del proceso. Está ejecutando las instrucciones del proceso y está leyendo y escribiendo el espacio de direcciones de el proceso.
    • Estado listo . El proceso no necesita esperar a que ocurra ningún evento y puede ejecutarse en cualquier momento. Sin embargo, la CPU actualmente está ejecutando otro proceso, por lo que el proceso está esperando en una cola lista para ser programado por el kernel.
  • Leer un archivo normal no se bloqueará . No importa cuántos bytes se lean, la lectura definitivamente regresará en un tiempo limitado. La lectura desde el dispositivo terminal o la red no es necesariamente el caso . Si la entrada de datos desde el terminal no tiene un carácter de nueva línea, llamar a leer para leer el dispositivo terminal se bloqueará. Si no se recibe ningún paquete de datos en la red, llamar a leer leer desde la red se bloqueará. En cuanto a si se bloqueará. También es incierto cuánto tiempo tomará. Si no llegan datos, permanecerán bloqueados allí. De manera similar, escribir en un archivo normal no bloqueará , pero escribir en un dispositivo terminal o en una red no lo bloqueará.

    • /dev/tty – archivo de terminal

Terminal de lectura de bloques

// block_readtty.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    
    
    char buf[10];
    int n;
    
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0){
    
    
        perror("read STDIN_FILENO");
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    
    return 0;
}
$ gcc block_readtty.c -o block
$ ./block  # 此时程序在阻塞等待输入,下面输入 hello 后回车即结束
hello
hello

terminal de lectura sin bloqueo

// nonblock_readtty.c
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"

int main(void) {
    
    
    char buf[10];
    int fd, n, i;
    
    // 设置 /dev/tty 非阻塞状态(默认为阻塞状态)
    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); 
    if(fd < 0) {
    
    
        perror("open /dev/tty");
        exit(1);
    }
    printf("open /dev/tty ok... %d\n", fd);

    for (i = 0; i < 5; i++) {
    
    
        n = read(fd, buf, 10);
        if (n > 0) {
    
      // 说明读到了东西
            break;
        }
        if (errno != EAGAIN) {
    
      
            perror("read /dev/tty");
            exit(1);
        } else {
    
    
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            sleep(2);
        }
    }

    if (i == 5) {
    
    
        write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    } else {
    
    
        write(STDOUT_FILENO, buf, n);
    }

    close(fd);

    return 0;
}
$ gcc block_readtty.c -o block
$ ./block  # 此时程序在阻塞等待输入,下面输入 hello 后回车即结束
hello
hello

3.9 Eficiencia de E/S

  • Utilice la función de lectura/escritura para implementar la copia de archivos
// 将一个文件的内容复制到另一个文件中:通过打开两个文件,循环读取第一个文件的内容并写入到第二个文件中
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    
    
    char buf[1];  // 定义一个大小为 1 的字符数组,用于存储读取或写入的数据
    int n = 0;

    // 打开第一个参数所表示的文件,以只读方式打开
    int fd1 = open(argv[1], O_RDONLY);
    if (fd1 == -1) {
    
    
        perror("open argv1 error");
        exit(1);
    }

    // 打开第二个参数所表示的文件,以可读写方式打开,如果文件不存在则创建,如果文件存在则将其清空
    int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);
    if (fd2 == -1) {
    
    
        perror("open argv2 error");
        exit(1);
    }

    // 循环读取第一个文件的内容,每次最多读取 1024 字节
    // 将返回的实际读取字节数赋值给变量 n
    while ((n = read(fd1, buf, 1024)) != 0) {
    
    
        if (n < 0) {
    
    
            perror("read error");
            break;
        }
        // 将存储在 buf 数组中的数据写入文件描述符为 fd2 的文件
        write(fd2, buf, n);
    }

    close(fd1);
    close(fd2);

    return 0;
}
  • Utilice la función fputc/fgetc para implementar la copia de archivos
// 使用了 C 标准库中的文件操作函数 fopen()、fgetc() 和 fputc() 来实现文件的读取和写入
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    
    
    FILE *fp, *fp_out;
    int n = 0;
    
    fp = fopen("hello.c", "r");
    if (fp == NULL) {
    
    
        perror("fopen error");
        exit(1);
    }

    fp_out = fopen("hello.cp", "w");
    if (fp_out == NULL) {
    
    
        perror("fopen error");
        exit(1);
    }

    // 判断是否读取到文件结束符 EOF
    while ((n = fgetc(fp)) != EOF) {
    
    
        fputc(n, fp_out);  // 将读取的字符写入输出文件
    }

    fclose(fp);
    fclose(fp_out);

    return 0;
}
  • lectura/escritura: cada vez que escribe un byte, cambiará constantemente entre el modo kernel y el modo usuario, por lo que lleva mucho tiempo.
  • fgetc/fputc: hay un búfer 4096, por lo que no se escribe byte a byte y hay menos cambios entre el kernel y el usuario ( lectura previa en el mecanismo de salida almacenado en el búfer )

Las funciones del sistema no son necesariamente más rápidas que las funciones de la biblioteca. Cuando se puedan utilizar funciones de la biblioteca, utilice funciones de la biblioteca. Las funciones de
E/S estándar vienen con búferes de usuario. Las llamadas al sistema no tienen búferes a nivel de usuario. Los búferes del sistema están disponibles.

  • Resultados de tiempo para operaciones de lectura en Linux con diferentes longitudes de búfer
    • La mayoría de los sistemas de archivos utilizan algún tipo de tecnología de almacenamiento en búfer de lectura anticipada para mejorar el rendimiento . Cuando se detecta una lectura secuencial, el sistema intenta leer más datos de los que requiere la aplicación, asumiendo que la aplicación leerá los datos rápidamente. El efecto de la lectura anticipada se puede ver en la siguiente figura: el tiempo de reloj con una longitud de búfer tan pequeña como 32 bytes es casi el mismo que con una longitud de búfer mayor.

Insertar descripción de la imagen aquí

3.10 Compartir archivos

  • Los sistemas UNIX admiten compartir archivos abiertos entre diferentes procesos

  • El kernel utiliza tres estructuras de datos para representar archivos abiertos y la relación entre ellas determina el impacto que un proceso puede tener sobre otro proceso en términos de intercambio de archivos.

    • (1) Cada proceso tiene una entrada de registro en la tabla de procesos, que contiene una tabla de descriptores de archivos abiertos, que puede considerarse como un vector, y cada descriptor ocupa una entrada. Asociados con cada descriptor de archivo están:
      • bandera del descriptor de archivo
      • puntero a una entrada de la tabla de archivos
    • (2) El kernel mantiene una tabla de archivos para todos los archivos abiertos. Cada entrada de archivo contiene
      • Indicadores de estado de archivos (lectura, escritura, adición, sincronización, sin bloqueo, etc.)
      • Desplazamiento del archivo actual
      • Puntero a la entrada de la tabla de nodos v del archivo
    • (3) Cada archivo (o dispositivo) abierto tiene una estructura de nodo v. El nodo v contiene punteros al tipo de archivo y funciones para realizar diversas operaciones en el archivo. Para la mayoría de los archivos, el nodo v también contiene el nodo i del archivo (nodo índice). Esta información se lee del disco a la memoria cuando se abre el archivo, por lo que toda la información relevante sobre el archivo está siempre disponible.
  • Abrir estructura de datos del kernel de archivos

Insertar descripción de la imagen aquí

La diferencia de alcance entre los indicadores de descriptor de archivo y los indicadores de estado de archivo : el primero se aplica solo a un descriptor de un proceso, mientras que el segundo se aplica a todos los descriptores de cualquier proceso que apunte a la entrada de la tabla de archivos determinada.

3.11 Operaciones atómicas

En términos generales, una operación atómica se refiere a una operación que consta de varios pasos . Si la operación se realiza de forma atómica, se realizan todos los pasos o no se realiza ninguno, es imposible realizar solo un subconjunto de todos los pasos.

3.11.1 Agregar a un archivo

  • Considere un proceso que agrega datos al final de un archivo.

    • Para un solo proceso, este programa puede funcionar normalmente, pero si varios procesos usan este método para agregar datos al mismo archivo al mismo tiempo, ocurrirán problemas.
    if(lseek(fd, OL, 2) < 0)
        err_sys("lseek error");
    if(write(fd, buf, 100) != 100)
        err_sys("write error");
    
  • Supongamos que hay dos procesos independientes, A y B, ambos adjuntos al mismo archivo. Cada proceso ha abierto el archivo pero no ha utilizado el indicador O_APPEND.

    • En este punto, cada proceso tiene su propia entrada de archivo, pero comparte una entrada de nodo v.
    • Supongamos que el proceso A llama a lseek, que establece el desplazamiento actual del archivo en el proceso A en 1500 bytes (al final del archivo actual)
    • Luego, el kernel cambia de proceso y el proceso B ejecuta lseek y también establece su desplazamiento actual del archivo en 1500 bytes (al final del archivo actual)
    • Luego, B llama a escribir, lo que aumenta el desplazamiento del archivo actual de B a 1600. Debido a que la longitud del archivo ha aumentado, el kernel actualiza la longitud actual del archivo en v-node a 1600
    • Luego, el kernel realiza un cambio de proceso para reanudar la operación del proceso A. Cuando A usa escritura, comienza a escribir datos en el archivo desde su desplazamiento de archivo actual (1500), sobrescribiendo así los datos que acaba de escribir en el archivo el proceso B.

    El problema radica en la operación lógica "primero localizar el final del archivo, luego escribir", que utiliza dos llamadas a funciones separadas

    • Solución alternativa: convierta estas dos operaciones en operaciones atómicas para otros procesos . Cualquier operación que requiera más de una llamada a función no es atómica porque el núcleo puede suspender temporalmente el proceso entre llamadas a función.
    • Los sistemas UNIX proporcionan un método atómico para dicha operación, que consiste en establecer el indicador O_APPEND al abrir el archivo. Hacer esto hace que el kernel establezca el desplazamiento actual del proceso al final del archivo antes de cada operación de escritura, por lo que no es necesario llamar a lseek antes de cada operación de escritura.

3.11.2 Funciones pred y pwrite

#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
  • valor de retorno de la función pread

    • Si tiene éxito, se devolverá el número de bytes leídos. Si se ha leído el final del archivo, se devolverá 0.
    • Si se produce un error, se devuelve -1
  • valor de retorno de la función pwrite

    • Si tiene éxito, devuelve el número de bytes escritos.
    • Si se produce un error, se devuelve -1
  • Llamar a pred es equivalente a llamar a lseek y luego llamar a read Sin embargo, pred tiene las siguientes diferencias importantes con respecto a esta llamada secuencial.

    • Al llamar a pread, sus operaciones de posicionamiento y lectura no se pueden interrumpir.
    • No actualizar el desplazamiento del archivo actual

3.12 Funciones dup y dup2 (copiar un descriptor de archivo existente)

#include <unistd.h>

// dup 主要起一个保存副本的作用
int dup(int oldfd);
// dup2 = dupto 将 oldfd 复制给 newfd,返回 newfd
int dup2(int oldfd, int newfd);

// cmd: F_DUPFD
// 可变参数 3:
    // 被占用的,返回最小可用的
    // 未被占用的,返回 = 该值的文件描述符
int fcntl(int fd, int cmd, ...)
  • valor de retorno de la función

    • Si tiene éxito, devuelva el nuevo descriptor de archivo
    • Si se produce un error, se devuelve -1
  • El nuevo descriptor de archivo devuelto por dup debe ser el número más pequeño de descriptores de archivo disponibles actualmente.

  • Para dup2, puede utilizar el parámetro newfd para especificar el valor del nuevo descriptor.

    • Si newfd ya está abierto, ciérrelo primero
    • Si oldfd = newfd, dup2 devuelve newfd sin cerrarlo
    • De lo contrario, el indicador del descriptor de archivo FD_CLOEXEC de newfd se borra, de modo que newfd esté abierto cuando el proceso llame a exec.
  • Otra forma de copiar un descriptor es utilizar la función fcntl. Las siguientes llamadas a funciones son equivalentes

    dup(oldfd);
    fcntl(oldfd, F_DUPFD, 0);
    
    // 以下情况并不完全等价
    // (1) dup2 是一个原子操作,而 close 和 fcnt1 包括两个函数调用
        // 有可能在 close 和 fcntl 之间调用了信号捕获函数,它可能修改文件描述符
    // (2) dup2 和 fcntl 有一些不同的 errno
    dup2(oldfd, newfd);
    
    close(newfd);
    fcntl(oldfd, F_DUPFD, newfd);
    

caso doble

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

// argc 表示参数个数,argv[] 是参数列表
int main(int argc, char *argv[]) {
    
    
    // 只读方式打开 argv[1] 指定的文件
    int oldfd = open(argv[1], O_RDONLY);       // 012  --- 3

    // 创建一个新的文件描述符 newfd,并与 oldfd 指向同一文件,最后返回新的文件描述符
    int newfd = dup(oldfd);    // 4

    printf("newfd = %d\n", newfd);

	return 0;
}

caso dup2

  • Copie un descriptor de archivo existente fd1 a otro descriptor de archivo fd2 y luego use fd2 para modificar el archivo al que apunta fd1.
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <pthread.h>
    
    int main(int argc, char *argv[]) {
          
          
        int fd1 = open(argv[1], O_RDWR);       // 012  --- 3
        int fd2 = open(argv[2], O_RDWR);       // 0123 --- 4
    
        // fd2 指向 fd1
        int fdret = dup2(fd1, fd2);         // 返回 新文件描述符 fd2
        printf("fdret = %d\n", fdret);
    
        // 打开一个文件,读写指针默认在文件头:如果写入的文件是非空的,写入的内容默认从文件头部开始写,会覆盖原有内容
        int ret = write(fd2, "1234567", 7); // 写入 fd1 指向的文件
        printf("ret = %d\n", ret);
    
        // 将输出到 STDOUT 的内容重定向到文件里
        dup2(fd1, STDOUT_FILENO);           // 将屏幕输入,重定向给 fd1 所指向的文件
    
        printf("---------886\n");
    
    	return 0;
    }
    

caso fctl

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    
    
    int fd1 = open(argv[1], O_RDWR);

    printf("fd1 = %d\n", fd1);

    // 参数 3:传入一个文件描述符 k,如果 k 没被占用,则直接用 k 复制 fd1 的内容。如果 k 被占用,则返回描述符表中最小可用描述符
    // 0 被占用,fcntl 使用文件描述符表中可用的最小文件描述符返回
    int newfd = fcntl(fd1, F_DUPFD, 0);
    printf("newfd = %d\n", newfd);

    // 7 未被占用,返回 = 该值的文件描述符
    int newfd2 = fcntl(fd1, F_DUPFD, 7);
    printf("newfd2 = %d\n", newfd2);

    int ret = write(newfd2, "YYYYYYY", 7);
    printf("ret = %d\n", ret);

    return 0;
}
$ gcc ls-R.c -o fcntl2
$ ./fcntl2 mycat.c
fd1 = 3
newfd = 4
newfd2 = 7
ret = 7

3.13 Funciones sync, fsync y fdatasync

  • Las implementaciones tradicionales del sistema UNIX tienen un caché de búfer o un caché de página en el kernel , y la mayor parte de la E/S del disco se realiza a través del búfer.
    • Al escribir datos en un archivo, el kernel generalmente copia primero los datos en un búfer, luego los pone en cola y luego los escribe en el disco. Este método se denomina escritura retrasada.
    • Normalmente, el kernel escribe todos los bloques de datos de escritura diferida en el disco cuando necesita reutilizar el búfer para otros datos de bloques del disco.
    • Para garantizar la coherencia del sistema de archivos real en el disco y el contenido del búfer, el sistema UNIX proporciona tres funciones: sincronización, fsync y fdatasync.
#include <unistd.h>

int fsync(int fd);
int fdatasync(int fd);

void sync(void);
  • valor de retorno de la función
    • Si tiene éxito, devuelve 0
    • Si se produce un error, se devuelve -1
  • sync simplemente pone en cola todos los buffers de bloques modificados en la cola de escritura y luego regresa. No espera a que finalice la operación de escritura real en el disco.
    • Un demonio del sistema llamado actualización llama periódicamente (generalmente cada 30 segundos) a la función de sincronización, lo que garantiza que los buffers de bloques del kernel se vacíen regularmente.
  • La función fsync solo funciona en un archivo especificado por el descriptor de archivo fd y espera a que se complete la operación de escritura en el disco antes de regresar.
    • fsync se puede utilizar en aplicaciones como bases de datos que necesitan garantizar que los bloques modificados se escriban en el disco inmediatamente
  • La función fdatasync es similar a fsync, pero solo afecta la porción de datos del archivo.
    • Además de los datos, fsync también actualiza los atributos del archivo de forma sincrónica.

3.14 Función fcntl (cambiar los atributos de un archivo abierto)

#include <unistd.h>
#include <fcntl.h>

// 参数 3 可以是整数或指向一个结构的指针
int fcntl(int fd, int cmd, ... /* int arg */ );
  • valor de retorno de la función
    • Si tiene éxito, depende de cmd.
      • Copie un descriptor existente: F_DUPFD o F_DUPFD_CLOEXEC, devolviendo un nuevo descriptor de archivo
      • Obtenga/establezca el indicador del descriptor de archivo: F_GETFD o F_SETFD, devuelva el indicador correspondiente
      • Obtener/establecer indicador de estado de archivo: F_GETFL o F_SETFL, devolver el indicador correspondiente
      • Obtener/establecer propiedad de E/S asíncrona: F_GETOWN o F_SETOWN, devolver una ID de proceso positiva o una ID de grupo de proceso negativa
      • Obtener/establecer bloqueo de registro: F_GETLK, F_SETLK o F_SETLKW
    • Si se produce un error, se devuelve -1

Caso

// 终端文件默认是阻塞读的,这里用 fcntl 将其更改为非阻塞读
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MSG_TRY "try again\n"

int main(void) {
    
    
    char buf[10];
    int flags, n;

    flags = fcntl(STDIN_FILENO, F_GETFL);
    if (flags == -1) {
    
    
        perror("fcntl error");
        exit(1);
    }
    flags |= O_NONBLOCK; // 与或操作,打开 flags
    int ret = fcntl(STDIN_FILENO, F_SETFL, flags);
    if (ret == -1) {
    
    
        perror("fcntl error");
        exit(1);
    }

tryagain:
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
    
    
        if (errno != EAGAIN) {
    
    
            perror("read /dev/tty");
            exit(1);
        }
        sleep(3);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        goto tryagain;
    }
    write(STDOUT_FILENO, buf, n);

    return 0;
}

3.15 Función ioctl

#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);
  • valor de retorno de la función
    • Si se produce un error, se devuelve -1
    • Si tiene éxito, devuelve otros valores
  • Administrar canales de E/S de dispositivos y controlar las características del dispositivo (utilizado principalmente en controladores de dispositivos)
  • Generalmente se usa para obtener las características físicas de un archivo (esta característica tiene diferentes valores para diferentes tipos de archivos)

3.16 Parámetros entrantes y salientes

#include <string.h>

char* strcpy(char* dest, const char* src);
char* strcpy(char* dest, const char* src, size_t n);
  • Parámetros entrantes: src

    • puntero como parámetro de función
    • Generalmente modificado con la palabra clave const
    • El puntero apunta al área válida y la operación de lectura se realiza dentro de la función.
  • Parámetros salientes: destino

    • puntero como parámetro de función
    • Antes de la llamada a la función, el espacio señalado por el puntero puede no tener sentido, pero debe ser válido.
    • Escribe operaciones dentro de la función.
    • Una vez completada la llamada a la función, sirve como valor de retorno de la función.
#include <string.h>

char* strtok(char* str, const char* delim);
char* strtok_r(char* str, const char* delim, char** saveptr);
  • Pasar parámetros de entrada y salida: saveptr
    • puntero como parámetro de función
    • Antes de la llamada a la función, el espacio señalado por el puntero tiene un significado real.
    • Dentro de la función, lee primero y luego escribe .
    • Una vez completada la llamada a la función, sirve como valor de retorno de la función.

Supongo que te gusta

Origin blog.csdn.net/qq_42994487/article/details/132842199
Recomendado
Clasificación