Princípio e implementação do Linux Direct I / O

E / S em cache

De modo geral, ao chamar a chamada de sistema open () para abrir um arquivo, se o sinalizador O_DIRECT não for especificado, a E / S armazenada em cache será usada para ler e gravar o arquivo. Vejamos primeiro a definição da chamada de sistema open ():

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

O seguinte explica a função de cada parâmetro:

  • nome do caminho: especifique o caminho do arquivo a ser aberto.
  • sinalizadores: especifique os sinalizadores para abrir o arquivo.
  • modo: Opcional, especifique a permissão para abrir o arquivo.

Os valores opcionais do parâmetro flags são os seguintes:
Insira a descrição da imagem aqui

O parâmetro flags é usado para especificar os sinalizadores para abrir o arquivo.Por exemplo, se O_RDONLY for especificado, o arquivo só pode ser lido e escrito em modo somente leitura. Esses sinalizadores podem ser definidos por operação de bit ou (|) para definir vários sinalizadores, como:

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

Mas os três sinalizadores O_RDONLY, O_WRONLY e O_RDWR são mutuamente exclusivos, o que significa que esses três sinalizadores não podem ser definidos ao mesmo tempo, apenas um deles pode ser definido.

Quando o arquivo é aberto sem especificar o sinalizador O_DIRECT, o método de E / S em cache é usado para abri-lo por padrão. Podemos usar a figura a seguir para entender onde está a E / S do cache no sistema de arquivos:
Insira a descrição da imagem aqui

A caixa vermelha na figura acima é onde a E / S do cache está localizada, entre o sistema de arquivos virtual e o sistema de arquivos real.

Ou seja, quando o sistema de arquivos virtual lê um arquivo, ele primeiro pesquisa o cache para ver se o conteúdo do arquivo a ser lido existe no cache e, se existir, lê-o diretamente do cache. O mesmo é verdadeiro ao gravar arquivos. Primeiro, grave no cache e, em seguida, sincronize com o dispositivo de bloco (como um disco) pelo sistema operacional.
Precisa de materiais de aprendizagem do arquiteto do servidor Linux C / C ++ além do grupo 812855908 (dados incluindo C / C ++, Linux, tecnologia golang, Nginx, ZeroMQ, MySQL, Redis, fastdfs, MongoDB, ZK, mídia de streaming, CDN, CDN, K8S, Docker, TCP / IP, corrotina, DPDK, ffmpeg, etc.), compartilhamento gratuito

Insira a descrição da imagem aqui

Prós e contras de E / S em cache

A introdução de I / O de cache visa reduzir as operações de I / O em dispositivos de bloco, mas como as operações de leitura e gravação são primeiro armazenadas em cache e depois copiadas do cache para o espaço do usuário, há mais uma operação de cópia de memória. Como mostrado abaixo:

Insira a descrição da imagem aqui

Portanto, a vantagem de E / S em cache é reduzir as operações de E / S em dispositivos de bloco, mas a desvantagem é que requer mais uma cópia de memória. Além disso, alguns aplicativos precisam gerenciar o cache de E / S por conta própria (como sistemas de banco de dados), portanto, precisam usar E / S direta.

I / O direto

E / S direta refere-se à operação de E / S executada pelo usuário para interagir diretamente com o dispositivo de bloco sem armazenamento em cache.

A vantagem do I / O direto é: como os blocos de dados de I / O não são armazenados em cache, ele pode interagir diretamente com os dados do usuário, reduzindo uma cópia de memória.
A desvantagem do I / O direto é que cada operação de I / O interage diretamente com o dispositivo de bloco, aumentando as operações de leitura e gravação no dispositivo de bloco.
Mas, como o aplicativo pode armazenar em cache o bloco de dados sozinho, ele é mais flexível e adequado para alguns aplicativos que são mais sensíveis a operações de E / S, como sistemas de banco de dados.

Implementação de E / S direta

Ao chamar a chamada de sistema open (), especifique o sinalizador O_DIRECT no parâmetro sinalizadores para usar E / S direta. Começamos com o sistema de arquivos virtual para rastrear o processamento de E / S direta do Linux.

Quando a chamada de sistema open () é chamada, ela aciona a chamada de sistema sys_open (). Vamos dar uma olhada na implementação da função 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 o processo de abertura de um arquivo é mais complicado, mas não importa muito para nossa análise de E / S direta.

Nossa principal preocupação é que a função sys_open () finalmente chamará dentry_open () para salvar o parâmetro flags no campo f_flags do objeto de arquivo. A cadeia de chamada: 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;
    ...
}

Em outras palavras, a função sys_open () abrirá o arquivo e salvará o parâmetro flags no campo f_flgas do objeto de arquivo. A seguir, vamos analisar como lidar com E / S direta ao ler arquivos. A operação de leitura de arquivo usa a chamada de sistema read (), e read () eventualmente chamará a função sys_read () do kernel. O código é o seguinte:

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;
}

Uma vez que a função sys_read () pertence à categoria de sistema de arquivo virtual, ela eventualmente chamará a função file-> f_op-> read () do sistema de arquivo real. O sistema de arquivo ext2 corresponde à função generic_file_read (). Vamos analisar a função 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 pode ser visto no código acima, se o sinalizador O_DIRECT for especificado ao chamar open (), a função generic_file_read () chamará a função generic_file_direct_IO () para processar a operação de E / S. Como a implementação da função generic_file_direct_IO () é tortuosa, segue-se a análise principal das 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);
        ...
    }
    ...
}

O processamento principal da função generic_file_direct_IO () tem duas partes:

Chame a função map_user_kiobuf () para aplicar para páginas de memória física para o espaço de memória virtual do usuário.
Chame a interface direct_IO () do sistema de arquivos real para processar E / S direta.
A função map_user_kiobuf () pertence à parte de gerenciamento de memória, então não vou explicar aqui.

A função generic_file_direct_IO () eventualmente chamará a interface direct_IO () do sistema de arquivos real. Para o sistema de arquivos ext2, a interface direct_IO () corresponde à função ext2_direct_IO (), e a função ext2_direct_IO () simplesmente encapsula a função generic_direct_IO () função, então vamos analisar a implementação da função 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;
}

A lógica da função generic_direct_IO () também é relativamente simples.Primeiro chame get_block () para obter a lista de números do bloco de dados a serem lidos, e então chame a função brw_kiovec () para realizar operações de I / O. Portanto, a função brw_kiovec () é o ponto de disparo final das operações de E / S. Continuamos a analisar:

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;
}

A função brw_kiovec () completa principalmente 3 tarefas:

  • Defina o endereço de memória (memória solicitada pelo usuário) para salvar os dados após a operação de E / S.
  • Defina a função de retorno de chamada final após a operação de E / S ser concluída como: end_buffer_io_kiobuf ().
  • Envie operações de E / S para a camada de bloco geral.

Pode-se ver que os dados após a operação de I / O serão salvos diretamente na memória do espaço do usuário sem passar pelo cache do kernel como um trânsito, de modo a atingir o propósito de I / O direto.

Acho que você gosta

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