Otimização de Performance no Linux: Isolamento e Afinidade de CPU

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:

  1. Edite o arquivo de configruação do GRUB, geralmente localizado em /etc/default/grub.
  2. 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"
  3. 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 (geralmente sizeof(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

Tags: linux-kernel cpu-affinity isolcpus taskset pthreads

Publicado em 10-29 07:59