Principio e implementación de E / S directa de Linux

E / S en caché

En términos generales, cuando se llama a la llamada al sistema open () para abrir un archivo, si no se especifica el indicador O_DIRECT, se utiliza la E / S en caché para leer y escribir el archivo. Veamos primero la definición de la llamada al sistema open ():

int open(const char *pathname, int flags, ... /*, mode_t mode */ );

A continuación se explica la función de cada parámetro:

  • ruta: especifique la ruta del archivo que se abrirá.
  • banderas: especifique las banderas para abrir el archivo.
  • modo: Opcional, especifique el permiso para abrir el archivo.

Los valores opcionales del parámetro flags son los siguientes:
Inserte la descripción de la imagen aquí

El parámetro banderas se utiliza para especificar las banderas para abrir el archivo. Por ejemplo, si se especifica O_RDONLY, el archivo solo se puede leer y escribir en modo de solo lectura. Estos indicadores se pueden configurar mediante operación bit o (|) para configurar varios indicadores, como:

open("/path/to/file", O_RDONLY|O_APPEND|O_DIRECT);

Pero los tres indicadores O_RDONLY, O_WRONLY y O_RDWR son mutuamente excluyentes, lo que significa que estos tres indicadores no se pueden configurar al mismo tiempo, solo se puede configurar uno de ellos.

Cuando el archivo se abre sin especificar el indicador O_DIRECT, se utiliza el método de E / S en caché para abrirlo de forma predeterminada. Podemos usar la siguiente figura para comprender dónde está la E / S de caché en el sistema de archivos:
Inserte la descripción de la imagen aquí

El cuadro rojo en la figura anterior es donde se encuentra la E / S de caché, entre el sistema de archivos virtual y el sistema de archivos real.

Es decir, cuando el sistema de archivos virtual lee un archivo, primero busca en la caché para ver si el contenido del archivo a leer existe en la caché y, si existe, lo lee directamente desde la caché. Lo mismo ocurre con la escritura de archivos: primero escriba en la caché y luego sincronice con el dispositivo de bloque (como un disco) mediante el sistema operativo.
Necesita materiales de aprendizaje para arquitectos de servidores Linux C / C ++ más el grupo 812855908 (datos que incluyen C / C ++, Linux, tecnología golang, Nginx, ZeroMQ, MySQL, Redis, fastdfs, MongoDB, ZK, medios de transmisión, CDN, P2P, K8S, Docker, TCP / IP, coroutine, DPDK, ffmpeg, etc.), uso compartido gratuito

Inserte la descripción de la imagen aquí

Pros y contras de la E / S en caché

La introducción de E / S de caché es para reducir las operaciones de E / S en dispositivos de bloque, pero debido a que las operaciones de lectura y escritura primero se almacenan en caché y luego se copian de la caché al espacio de usuario, hay una operación de copia de memoria más. Como se muestra abajo:

Inserte la descripción de la imagen aquí

Entonces, la ventaja de la E / S en caché es reducir las operaciones de E / S en dispositivos de bloque, pero la desventaja es que requiere una copia de memoria más. Además, algunas aplicaciones necesitan administrar la caché de E / S por sí mismas (como los sistemas de bases de datos), por lo que necesitan utilizar E / S directas.

E / S directa

E / S directa se refiere a la operación de E / S realizada por el usuario para interactuar directamente con el dispositivo de bloque sin almacenamiento en caché.

La ventaja de la E / S directa es: debido a que los bloques de datos de E / S no se almacenan en caché, puede interactuar directamente con los datos del usuario, reduciendo una copia de memoria.
La desventaja de la E / S directa es que cada operación de E / S interactúa directamente con el dispositivo de bloque, aumentando las operaciones de lectura y escritura en el dispositivo de bloque.
Pero debido a que la aplicación puede almacenar en caché el bloque de datos por sí misma, es más flexible y adecuada para algunas aplicaciones que son más sensibles a las operaciones de E / S, como los sistemas de bases de datos.

Implementación de E / S directa

Cuando llame a la llamada al sistema open (), especifique la bandera O_DIRECT en el parámetro flags para usar E / S directa. Comenzamos con el sistema de archivos virtual para rastrear el procesamiento de E / S directa de Linux.

Cuando se llama a la llamada al sistema open (), activará la llamada al sistema sys_open (). Echemos un vistazo a la implementación de la función sys_open ():

asmlinkage long sys_open(const char *filename, int flags, int mode)
{
    
    
    char *tmp;
    int fd, error;
    ...
    tmp = getname(filename); // 把文件名从用户空间拷贝到内核空间
    fd = PTR_ERR(tmp);
    if (!IS_ERR(tmp)) {
    
    
        fd = get_unused_fd(); // 申请一个还没有使用的文件描述符
        if (fd >= 0) {
    
    
            // 根据文件路径打开文件, 并获取文件对象
            struct file *f = filp_open(tmp, flags, mode);
            error = PTR_ERR(f);
            if (IS_ERR(f))
                goto out_error;
            fd_install(fd, f); // 把文件对象与文件描述符关联起来
        }
out:
        putname(tmp);
    }
    return fd;
    ...
}

Todo el proceso de abrir un archivo es más complicado, pero no importa mucho para nuestro análisis de E / S directa.

Nuestra principal preocupación es que la función sys_open () finalmente llame a dentry_open () para guardar el parámetro flags en el campo f_flags del objeto de archivo. La cadena de llamada: sys_open () -> filp_open () -> dentry_open ():

struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags)
{
    
    
    struct file *f;
    ...
    f = get_empty_filp();
    f->f_flags = flags;
    ...
}

En otras palabras, la función sys_open () abrirá el archivo y luego guardará el parámetro flags en el campo f_flgas del objeto de archivo. A continuación, analicemos cómo lidiar con las E / S directas al leer archivos. La operación de lectura del archivo usa la llamada al sistema read (), y read () eventualmente llamará a la función sys_read () del kernel. El código es el siguiente:

asmlinkage ssize_t sys_read(unsigned int fd, char *buf, size_t count)
{
    
    
    ssize_t ret;
    struct file *file;

    file = fget(fd);
    if (file) {
    
    
        ...
            if (!ret) {
    
    
                ssize_t (*read)(struct file *, char *, size_t, loff_t *);
                ret = -EINVAL;
                // ext2文件系统对应的是: generic_file_read() 函数
                if (file->f_op && (read = file->f_op->read) != NULL)
                    ret = read(file, buf, count, &file->f_pos);
            }
        ...
    }
    return ret;
}

Dado que la función sys_read () pertenece a la categoría de sistema de archivos virtual, eventualmente llamará a la función file-> f_op-> read () del sistema de archivos real. El sistema de archivos ext2 corresponde a la función generic_file_read (). Analicemos la función generic_file_read ():

ssize_t generic_file_read(struct file *filp, char * buf, size_t count, loff_t *ppos)
{
    
    
    ssize_t retval;
    ...
    if (filp->f_flags & O_DIRECT) // 如果标记了使用直接IO
        goto o_direct;
    ...
 o_direct:
    {
    
    
        loff_t pos = *ppos, size;
        struct address_space *mapping = filp->f_dentry->d_inode->i_mapping;
        struct inode *inode = mapping->host;
        ...
        size = inode->i_size;
        if (pos < size) {
    
    
            if (pos + count > size)
                count = size - pos;
            retval = generic_file_direct_IO(READ, filp, buf, count, pos);
            if (retval > 0)
                *ppos = pos + retval;
        }
        UPDATE_ATIME(filp->f_dentry->d_inode);
        goto out;
    }
}

Como puede verse en el código anterior, si se especifica el indicador O_DIRECT al llamar a open (), la función generic_file_read () llamará a la función generic_file_direct_IO () para procesar la operación de E / S. Dado que la implementación de la función generic_file_direct_IO () es tortuosa, el siguiente es el análisis principal de las partes importantes:

static ssize_t generic_file_direct_IO(int rw, struct file *filp, char *buf, size_t count, loff_t offset)
{
    
    
    ...
    while (count > 0) {
    
    
        iosize = count;
        if (iosize > chunk_size)
            iosize = chunk_size;

        // 为用户虚拟内存空间申请物理内存页
        retval = map_user_kiobuf(rw, iobuf, (unsigned long)buf, iosize);
        if (retval)
            break;

        // ext2 文件系统对应 ext2_direct_IO() 函数,
        // 而 ext2_direct_IO() 函数直接调用了 generic_direct_IO() 函数
        retval = mapping->a_ops->direct_IO(rw, inode, iobuf, (offset+progress) >> blocksize_bits, blocksize);
        ...
    }
    ...
}

El procesamiento principal de la función generic_file_direct_IO () tiene dos partes:

Llame a la función map_user_kiobuf () para solicitar páginas de memoria física para el espacio de memoria virtual del usuario.
Llame a la interfaz direct_IO () del sistema de archivos real para procesar E / S directa.
La función map_user_kiobuf () pertenece a la parte de administración de memoria, así que no la explicaré aquí.

La función generic_file_direct_IO () eventualmente llamará a la interfaz direct_IO () del sistema de archivos real. Para el sistema de archivos ext2, la interfaz direct_IO () corresponde a la función ext2_direct_IO (), y la función ext2_direct_IO () simplemente encapsula la función generic_direct_IO () función, entonces analicemos la implementación de la función generic_direct_IO ():

int generic_direct_IO(int rw, struct inode *inode, struct kiobuf *iobuf,
        unsigned long blocknr, int blocksize, get_block_t *get_block)
{
    
    
    int i, nr_blocks, retval;
    unsigned long *blocks = iobuf->blocks;

    nr_blocks = iobuf->length / blocksize;
    // 获取要读取的数据块号列表
    for (i = 0; i < nr_blocks; i++, blocknr++) {
    
    
        struct buffer_head bh;

        bh.b_state = 0;
        bh.b_dev = inode->i_dev;
        bh.b_size = blocksize;

        retval = get_block(inode, blocknr, &bh, rw == READ ? 0 : 1);
        ...
        blocks[i] = bh.b_blocknr;
    }

    // 开始进行I/O操作
    retval = brw_kiovec(rw, 1, &iobuf, inode->i_dev, iobuf->blocks, blocksize);

 out:
    return retval;
}

La lógica de la función generic_direct_IO () también es relativamente simple: primero llame a get_block () para obtener la lista de números de bloques de datos a leer, y luego llame a la función brw_kiovec () para realizar operaciones de E / S. Entonces, la función brw_kiovec () es el punto de activación final de las operaciones de E / S. Seguimos analizando:

int brw_kiovec(int rw, int nr, struct kiobuf *iovec[],
           kdev_t dev, unsigned long b[], int size)
{
    
    
    ...
    for (i = 0; i < nr; i++) {
    
    
        ...
        for (pageind = 0; pageind < iobuf->nr_pages; pageind++) {
    
    
            map  = iobuf->maplist[pageind];
            ...
            while (length > 0) {
    
    
                blocknr = b[bufind++];
                ...
                tmp = bhs[bhind++];

                tmp->b_size = size;
                set_bh_page(tmp, map, offset); // 设置保存I/O操作后的数据的内存地址 (用户空间的内存)
                tmp->b_this_page = tmp;

                init_buffer(tmp, end_buffer_io_kiobuf, iobuf); // 设置完成I/O后的收尾工作回调函数为: end_buffer_io_kiobuf()
                tmp->b_dev = dev;
                tmp->b_blocknr = blocknr;
                tmp->b_state = (1 << BH_Mapped) | (1 << BH_Lock) | (1 << BH_Req);
                ...
                submit_bh(rw, tmp); // 提交 I/O 操作 (通用块I/O层)

                if (bhind >= KIO_MAX_SECTORS) {
    
    
                    kiobuf_wait_for_io(iobuf);
                    err = wait_kio(rw, bhind, bhs, size);
                    ...
                }

            skip_block:
                length -= size;
                offset += size;

                if (offset >= PAGE_SIZE) {
    
    
                    offset = 0;
                    break;
                }
            } /* End of block loop */
        } /* End of page loop */
    } /* End of iovec loop */
    ...
    return err;
}

La función brw_kiovec () completa principalmente 3 tareas:

  • Configure la dirección de memoria (memoria solicitada por el usuario) para guardar los datos después de la operación de E / S.
  • Configure la función de devolución de llamada de finalización después de que se complete la operación de E / S como: end_buffer_io_kiobuf ().
  • Envíe las operaciones de E / S a la capa de bloques general.

Se puede ver que los datos después de la operación de E / S se guardarán directamente en la memoria en el espacio del usuario sin pasar por la caché del kernel como un tránsito, para lograr el propósito de E / S directa.

Supongo que te gusta

Origin blog.csdn.net/qq_40989769/article/details/110645449
Recomendado
Clasificación