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:
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:
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
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:
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.