Isolamento de Núcleos de Processamento
Conceito de CPU Isolado
Para tarefas que exigem intensidade computacional elevada, onde a carga do processador é crítica, recomenda-se configurar a afinidade de CPU (CPU Affinity). Essa prática visa aumentar a eficiência da execução, mitigando trocas de contexto desnecessárias e maximizando a taxa de acertos na cache do processador. Por padrão, o escalonador do kernel Linux distribui tarefas entre todos os núcleos disponíveis. Contudo, caso seja necessário que um processo ou thread específico tenha exclusividade sobre um núcleo, é possível isolar esse CPU para impedir que outras tarefas o utilizem.
Características do Isolamento
A utilização de CPUs isolados melhora significativamente a previsibilidade e o tempo de resposta das tarefas executadas nesses núcleos. Entretanto, ao reservar recursos para isolamento, reduz-se a quantidade de processamento disponível para o restante do sistema, exigindo um planejamento cuidadoso da infraestrutura de hardware.
Configurando o Isolamento
O kernel Linux oferece o parâmetro de inicialização isolcpus para excluir núcleos específicos do algoritmo de balanceamento de carga SMP. Após isolar os núcleos, processos devem ser manualmente vinculados a eles através de afinidade.
Sintaxe do parâmetro:
isolcpus=numero_cpu [,numero_cpu,...]
Para aplicar essa configuração:
- Edite o arquivo de configruação do GRUB, geralmente localizado em
/etc/default/grub. - Adicione a lista de núcleos isolados na variável
GRUB_CMDLINE_LINUX. Os núcleos devem ser separados por vírgulas, sem intervalos contíguos automáticos. ``` GRUB_CMDLINE_LINUX="isolcpus=4,5,6 crashkernel=auto rd.lvm.lv=vg0/root rhgb quiet" - Regenerate o arquivo de boot e reinicie o sistema para aplicar as mudanças. ```
update-grub
grub-mkconfig -o /boot/grub/grub.cfg
Com o parâmetro ativo, o escalonador padrão ignora os núcleos especificados. O usuário deve utilizar utilitários como taskset ou cset para alocar processos manualmente a essas CPUs.
Fundamentos de Afinidade de CPU
Arquitetura de Processadores
A tecnologia Hyper-Threading permite que um núcleo físico seja tratado como múltiplos núcleos lógicos pelo sistema operacional, possibilitando paralelismo a nível de thread e reduzindo tempos de ociosidade.
- CPU Física: O chip instalado na placa-mãe.
- CPU Lógica: Cada núcleo dentro de uma CPU física. Com Hyper-Threading (HT), o número de núcleos lógicos pode ser o dobro dos físicos.
Comandos para inspeção de hardware:
# Contar CPUs físicas
cat /proc/cpuinfo | grep "physical id" | sort -u | wc -l
# Verificar núcleos por CPU física
cat /proc/cpuinfo | grep "cpu cores" | uniq
# Contar processadores lógicos totais
cat /proc/cpuinfo | grep "processor" | wc -l
# Identificar modelo do processador
cat /proc/cpuinfo | grep "name" | cut -f2 -d: | uniq
# Verificar em qual CPU lógica um processo está rodando
ps -eo pid,args,psr
Definição de Afinidade
A afinidade de CPU restringe a execução de um processo ou thread a um subconjunto específico de núcleos. Em arquiteturas SMP (Symmetric Multi-Processing), o escalonador respeita essa máscara, evitando migrações frequentes entre processadores. Isso previne a invalidação da cache (cache miss), que ocorre quando um processo muda de núcleo e precisa recarregar dados na cache local, degradando a performance.
Embora o kernel possua uma afinidade "suave" natural, tentando manter processos no mesmo CPU, a definição manual é crucial para cargas de trabalho de alta performance.
Comandos de verificação:
# Ver afinidade de todos os processos
ps -eo pid,cmd,psr
# Ver afinidade das threads de um PID específico
ps -To 'pid,lwp,psr,cmd' -p [PID]
Vantagens da Vinculação
- Aumento da taxa de acerto na cache, reduzindo latência de memória.
- Em arquiteturas NUMA, o ganho é substancial devido à localidade de memória associada a cada socket. Em SMP, o benefício existe mas pode ser menor devido ao compartilhamento de recursos.
- Redução de overhead de contexto quando há dedicación de núcleo por processo.
Ferramentas de Usuário
Utilizando taskset
O pacote util-linux fornece o comando taskset.
# Instalar ferramenta
yum install util-linux
# Verificar afinidade de um PID (retorna máscara hex ou lista com -c)
taskset -p [PID]
taskset -c -p [PID]
# Definir afinidade para um processo existente
taskset -p [máscara] [PID]
taskset -c [lista_de_núcleos] -p [PID]
# Iniciar um novo processo vinculado a núcleos específicos
taskset -c 4,5,6 python script.py
taskset -c 4-6 python script.py
Em containers Docker, é possível restringir o uso de CPU durante a criação do container com a flag --cpuset-cpus.
Utilizando cset
O cset permite gerenciar conjuntos de CPUs (cpusets).
# Criar um conjunto de CPUs
cset set --cpu 4,5,6 --set=meu_conjunto
# Mover processos para o conjunto criado
cset proc --move --pid=123,456 --toset=meu_conjunto
Implementação via Código: Vinculação de Processos
API do Sistema
As chamadas de sistema sched_setaffinity e sched_getaffinity permitem controle programático.
#define _GNU_SOURCE
#include <sched.h>
int sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
pid: ID do processo (0 refere-se ao processo atual).cpusetsize: Tamanho da máscara (geralmentesizeof(cpu_set_t)).mask: Estrutura definindo os núcleos permitidos.
Exemplo Prático em C
O código abaixo demonstra como definir a afinidade para o contexto de execução atual dentro de múltiplas threads.
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include <pthread.h>
#include <sched.h>
#include <string.h>
#define MAX_WORKERS 10
int total_cores = 0;
int core_alvo = 3;
void* worker_routine(void* arg) {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(core_alvo, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
perror("Falha ao definir afinidade");
}
cpu_set_t atual;
CPU_ZERO(&atual);
if (sched_getaffinity(0, sizeof(atual), &atual) == -1) {
perror("Falha ao obter afinidade");
}
for (int i = 0; i < total_cores; i++) {
if (CPU_ISSET(i, &atual)) {
printf("Worker %d executando no core %d\n", *((int*)arg), i);
}
}
return NULL;
}
int main(int argc, char* argv[]) {
pthread_t threads[MAX_WORKERS];
int ids[MAX_WORKERS];
total_cores = sysconf(_SC_NPROCESSORS_CONF);
printf("Sistema detectou %i processadores.\n", total_cores);
for (int i = 0; i < MAX_WORKERS; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, worker_routine, &ids[i]);
}
for (int i = 0; i < MAX_WORKERS; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
Compilação e execução:
gcc -o app_afi app_afi.c -pthread
./app_afi
Implementação via Código: Vinculação de Threads
API pthread
Para controle granular por thread, utilizam-se as funções específicas da biblioteca pthread.
#define _GNU_SOURCE
#include <pthread.h>
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize, cpu_set_t *cpuset);
Exemplo Prático em C
Neste exemplo, a afinidade é definida explicitamente para cada handle de thread criado.
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include <pthread.h>
#include <sched.h>
#include <string.h>
#define QTD_THREADS 10
int num_cores = 0;
int core_destino = 3;
void* thread_task(void* arg) {
cpu_set_t mask_ret;
CPU_ZERO(&mask_ret);
pthread_t self = pthread_self();
if (pthread_getaffinity_np(self, sizeof(mask_ret), &mask_ret) != 0) {
fprintf(stderr, "Erro ao ler afinidade da thread\n");
}
for (int i = 0; i < num_cores; i++) {
if (CPU_ISSET(i, &mask_ret)) {
printf("Thread %d alocada no processador %d\n", *((int*)arg), i);
}
}
return NULL;
}
int main(int argc, char* argv[]) {
pthread_t handles[QTD_THREADS];
int thread_ids[QTD_THREADS];
cpu_set_t cpumask;
num_cores = sysconf(_SC_NPROCESSORS_CONF);
printf("Total de processadores: %i\n", num_cores);
CPU_ZERO(&cpumask);
CPU_SET(core_destino, &cpumask);
for (int i = 0; i < QTD_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&handles[i], NULL, thread_task, &thread_ids[i]);
if (pthread_setaffinity_np(handles[i], sizeof(cpumask), &cpumask) != 0) {
fprintf(stderr, "Erro ao vincular thread %d\n", i);
}
}
for (int i = 0; i < QTD_THREADS; i++) {
pthread_join(handles[i], NULL);
}
return 0;
}
Compilação e execução:
gcc -o app_thread app_thread.c -pthread
./app_thread