O endereço do texto original é atualizado e o efeito de leitura é melhor!
Espaço do usuário e espaço do kernel
A versão de distribuição de qualquer sistema Linux, seu kernel do sistema é o Linux. Todos os nossos aplicativos precisam interagir com o hardware por meio do kernel do Linux.
Para evitar que os aplicativos do usuário causem conflitos ou até pânico no kernel, os aplicativos do usuário são separados do kernel:
- O espaço de endereçamento de memória é dividido em duas partes: espaço do kernel e espaço do usuário
Para um sistema operacional de 32 bits, o endereço de endereçamento é 0 ~ 2322 ^ {32}232
- Somente instruções restritas (Ring3) podem ser executadas no espaço do usuário, e os recursos do sistema não podem ser chamados diretamente, devendo ser acessados através da interface fornecida pelo kernel
- O espaço do kernel pode executar comandos privilegiados (Ring0) e chamar todos os recursos do sistema
Quando um processo é executado no espaço do usuário, ele é chamado de modo de usuário e, quando é executado no espaço do kernel, é chamado de modo do kernel.
Para melhorar a eficiência de IO, o sistema Linux adicionará buffers no espaço do usuário e no espaço do kernel:
- Ao gravar dados, os dados do buffer do usuário devem ser copiados para o buffer do kernel e, em seguida, gravados no dispositivo
- Ler dados é ler dados do dispositivo para o buffer do kernel e, em seguida, copiá-los para o buffer do usuário
5 modelos IO
- Bloqueando IO (bloqueando IO)
- Non-blocking IO (Nonblocking IO)
- Multiplexação IO (Multiplexação IO)
- E/S acionada por sinal (E/S acionada por sinal)
- E/S assíncrona (E/S assíncrona)
#blocking IO
Como o nome indica, bloquear IO significa que ele deve bloquear e aguardar durante os dois estágios de espera por dados e cópia de dados para o espaço do usuário .
- Threads de usuário emitem solicitações de E/S
- O kernel verificará se os dados estão prontos, caso contrário, aguardará para sempre e o thread do usuário estará em um estado bloqueado e o thread do usuário estará em um estado bloqueado
- Quando os dados estiverem prontos, o kernel copiará os dados para o thread do usuário e retornará o resultado para o thread do usuário, e o thread do usuário desbloqueará o estado
Pode-se observar que no modelo IO de bloqueio, o processo do usuário é bloqueado em dois estágios.
# IO sem bloqueio
A operação recvfrom de IO sem bloqueio retorna o resultado imediatamente, em vez de bloquear o processo do usuário.
- Aguardando o estágio de dados, se os dados não estiverem prontos, retorne EWOULDBLOCK imediatamente. Nesse processo, o processo do usuário não bloqueia, mas o processo do usuário sempre iniciará solicitações e estará ocupado com o treinamento de rotação até que o processamento do kernel comece a interromper o treinamento de rotação.
- Depois que os dados estiverem prontos, os dados são copiados do kernel para o espaço do usuário. O processo do usuário é bloqueado durante esta fase.
Pode-se observar que no modelo IO sem bloqueio, o processo do usuário é não bloqueador na primeira fase e bloqueado na segunda fase. Embora seja sem bloqueio, o desempenho não foi aprimorado e o mecanismo de espera ocupada fará com que a CPU fique ociosa e o uso da CPU aumentará acentuadamente.
# multiplexação IO
Seja IO bloqueante ou IO não bloqueante, o aplicativo do usuário precisa chamar recvfrom para obter dados no primeiro estágio. A diferença está no método de processamento quando não há dados:
- Se não houver dados quando recvfrom for chamado, o bloqueio de IO fará com que o processo seja bloqueado e o IO sem bloqueio fará com que a CPU fique ociosa, o que não pode desempenhar o papel da CPU.
- Se houver dados quando recvfrom for chamado, o processo do usuário pode entrar diretamente no segundo estágio para ler e processar os dados
Por exemplo, quando o servidor processa solicitações de Socket do cliente, no caso de um único thread, ele só pode processar um Socket por vez. Se o soquete que está sendo processado não estiver pronto (os dados não podem ser lidos ou gravados), o thread será bloqueado e todos os outros soquetes do cliente devem esperar e o desempenho é naturalmente ruim.
Descritor de Arquivo (File Descriptor): conhecido como FD, é um inteiro não assinado incrementado a partir de 0, usado para associar um arquivo no Linux. Tudo no Linux é um arquivo, como arquivos comuns, vídeos, dispositivos de hardware, etc., claro, incluindo soquetes de rede (Socket)
Multiplexação de IO: usa um único thread para monitorar vários FDs ao mesmo tempo e é notificado quando um determinado FD é legível ou gravável, para evitar espera inválida e fazer uso total dos recursos da CPU.
Existem três maneiras de implementar a tecnologia de multiplexação IO:
- selecionar
- enquete
- epoll
diferença:
- select e poll apenas notificarão o processo do usuário que o FD está pronto, mas não tem certeza de qual é o FD, e o processo do usuário precisa percorrer o FD um por um para confirmar
- O epoll notificará o processo do usuário de que o FD está pronto e, ao mesmo tempo, gravará o FD pronto no espaço do usuário e localizará diretamente o FD pronto
# SELECIONE
select é a implementação mais antiga de multiplexação de E/S no Linux:
// 定义类型别名 __fd_mask,本质是 long int
typedef long int __fd_mask;
/* fd_set 记录要监听的fd集合,及其对应状态 */
typedef struct {
// fds_bits是long类型数组,长度为 1024/32 = 32
// 共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
// ...
} fd_set;
// select函数,用于监听多个fd的集合
int select(
int nfds,// 要监视的fd_set的最大fd + 1
fd_set *readfds,// 要监听读事件的fd集合
fd_set *writefds,// 要监听写事件的fd集合
fd_set *exceptfds, // 要监听异常事件的fd集合
// 超时时间,nulT-永不超时;0-不阻塞等待;大于0-固定等待时间
struct timeval *timeout
);
O processo específico é o seguinte:
- Crie fd_set rfds no espaço do usuário
- Se você deseja monitorar fd = 1, 2, 5
- Execute select(5 + 1, rfds, null, null, 3) no espaço do usuário
- Copie o array fd_set rfds criado no espaço do usuário para o espaço do kernel
- Atravessando o array fd_set rfds copiado no espaço do kernel
- Se não estiver pronto, defina o fd nesse local como 0.
Problemas com o modo de seleção:
- É necessário copiar todo o fd_set do espaço do usuário para o espaço do kernel e copiá-lo novamente para o espaço do usuário após selecionar
- Select não pode saber qual fd está pronto, ele precisa percorrer fd_set
- O número de fds monitorados pelo fd_set não pode exceder 1024,
# ENQUETE
O modo de pesquisa fez uma melhoria simples no modo de seleção, mas a melhoria de desempenho não é óbvia. Alguns códigos de chave são os seguintes:
// pollfd 中的事件类型
#define POLLIN //可读事件
#define POLLOUT //可写事件
#define POLLERR //错误事件
#define POLLNVAL //fd未打开
// pollfd结构
struct pollfd{
int fd; // 要监听的 fd
*short int events; // 要监听的事件类型:读、写、异常
short int revents; // 实际发生的事件类型
}
// poll函数
int poll(
struct pollfd xfds, // pollfd数组,可以自定义大小
nfds_t nfds, // 数组元素个数
int timeout // 超时时间
);
Processo de E/S:
- Crie uma matriz pollfd, adicione as informações fd em questão a ela e personalize o tamanho da matriz
- Chame a função poll, copie o array pollfd para o espaço do kernel, transfira para a lista encadeada para armazenamento, sem limite superior
- O kernel percorre fd para determinar se está pronto
- Depois que os dados estiverem prontos ou expirarem, copie o array pollfd para o espaço do usuário e retorne o número fd pronto n
- O processo do usuário julga se n é maior que 0
- Se for maior que 0, percorra o array pollfd e encontre o fd pronto
Compare com SELECT:
- O valor fixo de fd_set no modo select é 1024, enquanto pollfd usa uma lista encadeada no kernel, que é teoricamente infinita
- Quanto mais FDs você ouvir, mais tempo levará cada travessia e, em vez disso, o desempenho diminuirá.
# EPOLL
O modo epoll é uma melhoria dos modos select e poll, fornecendo três funções:
struct eventpoll{
//...
struct rb_root rbr; // 一颗红黑树,记录要监听的fd
struct list_head rdlist; // 一个链表,记录就绪的 FD
//...
}
// 1.会在内核创建eventpolL结构体,返回对应的句柄epfd
int epoll create(int size);
// 2.将一个FD添加到epol的红黑树中,并设置ep_poli_calLback
// calTback触发时,就把对应的FD加入到rdlist这个就绪列表中
int epoll _ctl(
int epfd, // epoll实例的句柄
int op, // 要执行的操作,包括:ADD、MOD、DEL
int fd, // 要监听的 FD
struct epoll_event *event // 要监听的事件类型: 读、写、异常等
);
// 3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量
int epoll wait(
int epfd, // eventpoll 实例的句柄
struct epoll_event *events, // 空event 数组,用于接收就绪的 FD
int maxevents, // events 数组的最大长度
int timeout // 超时时间,-1永不超时;0不阻塞;大于0为阻塞时间
);
#mecanismo de notificação de eventos
Quando o FD tem dados para ler, podemos chamar epoll_wait para ser notificado, mas existem dois modos de notificação de tempo:
- Nível acionado: LT para abreviar. Quando o FD tiver dados para ler, ele repetirá a notificação várias vezes até que o processamento dos dados seja concluído. É o modo padrão do epoll.
- Edge Triggered: ET para abreviar. Quando o FD tiver dados para ler, será notificado apenas uma vez, independentemente de os dados terem sido processados ou não
por exemplo
- Suponha que o FD correspondente a um Socket cliente tenha sido registrado na instância epoll
- Client Socket enviou 2kb de dados
- O servidor chama epoll_wait e é notificado de que o FD está pronto
- O servidor lê 1kb de dados do FD
- Volte para a etapa 3 (chame epoll_wait novamente para formar um loop)
para concluir
- O modo ET evita o fenômeno de grupo chocante que pode ocorrer no modo LT
- O modo ET é melhor combinado com IO sem bloqueio para ler dados FD, que são mais complicados que LT
# processo de serviço WEB
Fluxograma básico do serviço web baseado no modo epoll:
#Resumo_ _
Existem três problemas com o modo de seleção:
- O número máximo de FDs que podem ser monitorados não excede 1024
- Toda vez que você selecionar, você precisa copiar todos os FDs a serem monitorados para o espaço do kernel
- Sempre que for necessário percorrer todos os DFs para determinar o estado pronto
Problemas com o modo de enquete:
- poll usa a lista vinculada para resolver o problema de monitorar o limite superior do FD no select, mas ainda precisa percorrer todos os FDs. Se houver mais monitores, o desempenho cairá
Como resolver esses problemas no modo epoll:
- Com base na árvore rubro-negra na instância epoll para salvar os FDs a serem monitorados, não há limite superior em teoria, e a eficiência de adicionar, excluir, modificar e verificar é muito alta, e o desempenho não diminuirá significativamente à medida que o número de FDs monitorados aumenta
- Cada FD só precisa executar epoll_ctl uma vez para adicionar à árvore rubro-negra, e não há necessidade de passar nenhum parâmetro para cada epoll_wait no futuro, e não há necessidade de copiar repetidamente FD para o espaço do kernel
- O kernel copiará diretamente o FD pronto para o local especificado no espaço do usuário, e o processo do usuário pode saber quem é o FD pronto sem passar por todos os FDs
# IO acionada por sinal
IO orientada por sinal é estabelecer uma associação de sinal SIGIO com o kernel e definir um retorno de chamada. Quando o kernel tiver um FD pronto, ele enviará um sinal SIGIO para notificar o usuário. Durante esse período, o aplicativo do usuário pode executar outros serviços sem bloqueio e espera.
Quando há um grande número de operações IO, há muitos sinais e a função de processamento SIGIO não pode lidar com isso a tempo, o que pode causar o estouro da fila de sinais.
Além disso, o desempenho da interação de sinal frequente entre o espaço do kernel e o espaço do usuário também é baixo.
#E/S assíncrona
Todo o processo de IO assíncrono é sem bloqueio. Depois que o processo do usuário chama a API assíncrona, ele pode fazer outras coisas. O kernel espera que os dados estejam prontos e os copia para o espaço do usuário antes de enviar o sinal para notificar o processo do usuário.
No modelo de IO assíncrono, o processo do usuário não bloqueia em ambas as fases.
Embora o modelo de IO assíncrono seja muito simples, sob alto acesso simultâneo, um grande número de solicitações será processado no kernel, o que pode facilmente levar a uma falha do kernel.
# síncrono e assíncrono
Se a operação IO é síncrona ou assíncrona depende do processo de cópia de dados entre o espaço do kernel e o espaço do usuário (operação IO de leitura e gravação de dados), ou seja, se a segunda fase é síncrona ou assíncrona: