Resource limits: ulimit e cgroups

1. Introdução aos Resource Limits em Sistemas Unix

Em sistemas Unix e Linux, o gerenciamento de recursos é fundamental para garantir estabilidade, segurança e previsibilidade de execução. Resource limits são mecanismos que restringem o consumo de recursos do sistema por processos, prevenindo que um único programa consuma toda a memória, CPU ou descritores de arquivo, causando negação de serviço ou falhas catastróficas.

Dois mecanismos principais existem para esse fim: ulimit (user limits) e cgroups (control groups). O ulimit opera no nível de processo individual, herdado por processos filhos, e é configurado via shell (ulimit -n) ou chamadas de sistema C. Já os cgroups operam em grupos hierárquicos de processos, permitindo limites agregados e dinâmicos, sendo a base de tecnologias como Docker e LXC.

Os recursos controláveis incluem: memória virtual e residente, tempo de CPU, número de arquivos abertos, tamanho de pilha, número de processos, core dumps e prioridade de agendamento.

2. Trabalhando com ulimit via API C: getrlimit e setrlimit

A API C para manipular limites de recursos utiliza a estrutura struct rlimit, definida em <sys/resource.h>:

struct rlimit {
    rlim_t rlim_cur;  /* Soft limit (limite atual) */
    rlim_t rlim_max;  /* Hard limit (limite máximo) */
};

O soft limit pode ser aumentado até o hard limit por qualquer processo; o hard limit só pode ser reduzido (a menos que se tenha privilégios de root ou CAP_SYS_RESOURCE). As funções principais são:

#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlp);
int setrlimit(int resource, const struct rlimit *rlp);

Exemplo prático: alterar o limite de arquivos abertos em um processo filho:

#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    struct rlimit rl;

    // Obtém o limite atual de arquivos abertos
    if (getrlimit(RLIMIT_NOFILE, &rl) == -1) {
        perror("getrlimit");
        exit(EXIT_FAILURE);
    }

    printf("Limite atual de arquivos: soft=%lu, hard=%lu\n",
           (unsigned long)rl.rlim_cur, (unsigned long)rl.rlim_max);

    // Aumenta o soft limit para 4096 (se hard limit permitir)
    rl.rlim_cur = 4096;
    if (setrlimit(RLIMIT_NOFILE, &rl) == -1) {
        perror("setrlimit");
        exit(EXIT_FAILURE);
    }

    pid_t pid = fork();
    if (pid == 0) {
        // Processo filho herda os limites
        getrlimit(RLIMIT_NOFILE, &rl);
        printf("Filho: limite de arquivos = %lu\n",
               (unsigned long)rl.rlim_cur);
        exit(0);
    }
    return 0;
}

3. Limites de Recursos Comuns e Seus Impactos

RLIMIT_NOFILE: Controla o número máximo de descritores de arquivo abertos. Um servidor web que não fecha conexões pode esgotar esse limite, impedindo novas conexões. Exemplo de verificação:

struct rlimit rl;
getrlimit(RLIMIT_NOFILE, &rl);
printf("Max open files: %lu\n", (unsigned long)rl.rlim_cur);

RLIMIT_AS e RLIMIT_DATA: Limitam o tamanho da memória virtual e do segmento de dados, respectivamente. Essenciais para prevenir estouro de memória (OOM) em processos que alocam dinamicamente sem controle. Quando o limite é excedido, malloc() retorna NULL.

struct rlimit rl;
rl.rlim_cur = 100 * 1024 * 1024; // 100 MB
rl.rlim_max = 200 * 1024 * 1024; // 200 MB
setrlimit(RLIMIT_AS, &rl);

RLIMIT_CPU: Limita o tempo de CPU em segundos. Ao atingir o soft limit, o processo recebe SIGXCPU; ao atingir o hard limit, recebe SIGKILL. Útil para loops infinitos acidentais.

RLIMIT_NPROC: Restringe o número de processos que um usuário pode criar, prevenindo fork bombs.

4. Limites Específicos: Stack, Core Dump e Prioridade

RLIMIT_STACK: Controla o tamanho máximo da pilha (stack). Útil para evitar stack overflow em programas com recursão profunda. O valor padrão é tipicamente 8 MB no Linux. Quando excedido, o processo recebe SIGSEGV.

struct rlimit rl;
rl.rlim_cur = 2 * 1024 * 1024; // 2 MB de pilha
rl.rlim_max = 4 * 1024 * 1024;
setrlimit(RLIMIT_STACK, &rl);

RLIMIT_CORE: Controla o tamanho máximo de core dumps. Definir como 0 desabilita core dumps, importante em ambientes de produção para evitar vazamento de dados sensíveis em disco. Definir como RLIM_INFINITY permite core dumps completos para debug.

RLIMIT_NICE e RLIMIT_RTPRIO: Controlam a prioridade de agendamento. RLIMIT_NICE define o valor máximo que nice() pode aumentar (prioridade mais baixa). RLIMIT_RTPRIO limita a prioridade máxima de tempo real.

5. Introdução ao cgroups (Control Groups) em C

cgroups é um mecanismo do kernel Linux que organiza processos em grupos hierárquicos e aplica limites de recursos a esses grupos. Diferente do ulimit, que é por processo, cgroups opera em coleções de processos, permitindo limites agregados.

A interface é baseada no sistema de arquivos virtual montado em /sys/fs/cgroup/. Cada subsistema (cpu, memory, pids, etc.) possui seu próprio diretório com arquivos de controle. Em C, manipulamos esses arquivos com funções padrão de I/O:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void write_cgroup_file(const char *path, const char *value) {
    int fd = open(path, O_WRONLY);
    if (fd == -1) {
        perror("open cgroup file");
        exit(EXIT_FAILURE);
    }
    if (write(fd, value, strlen(value)) == -1) {
        perror("write cgroup file");
        close(fd);
        exit(EXIT_FAILURE);
    }
    close(fd);
}

6. Gerenciando Memória e CPU com cgroups via C

Exemplo completo: criar um cgroup filho, limitar memória e CPU, e anexar o processo atual:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void write_cgroup(const char *path, const char *value) {
    FILE *f = fopen(path, "w");
    if (!f) { perror(path); exit(1); }
    fprintf(f, "%s", value);
    fclose(f);
}

int main() {
    // Criar diretório do cgroup filho
    mkdir("/sys/fs/cgroup/memory/meu_grupo", 0755);
    mkdir("/sys/fs/cgroup/cpu/meu_grupo", 0755);

    // Limitar memória a 50 MB
    write_cgroup("/sys/fs/cgroup/memory/meu_grupo/memory.max", "52428800");

    // Limitar CPU a 50% de um core (50000 = 50ms em 100ms)
    write_cgroup("/sys/fs/cgroup/cpu/meu_grupo/cpu.max", "50000 100000");

    // Anexar o processo atual ao cgroup
    char pid_str[32];
    snprintf(pid_str, sizeof(pid_str), "%d", getpid());
    write_cgroup("/sys/fs/cgroup/memory/meu_grupo/cgroup.procs", pid_str);
    write_cgroup("/sys/fs/cgroup/cpu/meu_grupo/cgroup.procs", pid_str);

    printf("Processo %d anexado ao cgroup meu_grupo\n", getpid());

    // Simular alocação de memória (deve falhar após 50 MB)
    void *p = malloc(60 * 1024 * 1024);
    if (!p) printf("malloc falhou: limite de memória atingido!\n");
    else free(p);

    return 0;
}

7. Diferenças Práticas entre ulimit e cgroups

Característica ulimit cgroups
Escopo Por processo Por grupo de processos
Herança Filhos herdam do pai Filhos permanecem no grupo
Dinamicidade Apenas reduz hard limit Pode ser alterado a qualquer momento
Hierarquia Simples (pai-filho) Hierarquia aninhada arbitrária
Subsistemas Limitados a ~15 recursos Dezenas de subsistemas (blkio, hugetlb, etc.)
Permissão Root ou usuário (soft limit) Root obrigatório

Casos de uso: ulimit é ideal para scripts shell e programas simples que precisam de limites básicos. cgroups são essenciais para contêineres (Docker usa cgroups v2), ambientes multi-tenant e sistemas que exigem isolamento granular entre serviços.

8. Boas Práticas e Erros Comuns ao Gerenciar Limites

Verifique erros sempre: getrlimit e setrlimit retornam -1 em caso de falha. Use errno para diagnóstico:

#include <errno.h>
if (setrlimit(RLIMIT_NOFILE, &rl) == -1) {
    if (errno == EPERM) {
        printf("Permissão negada: hard limit não pode ser aumentado\n");
    } else if (errno == EINVAL) {
        printf("Recurso inválido ou rlim_cur > rlim_max\n");
    }
}

Permissões: Para aumentar hard limits, o processo deve ter CAP_SYS_RESOURCE ou ser executado como root. Em cgroups, escrever em arquivos de controle geralmente requer root.

Testando limites: Para detectar violações, monitore sinais como SIGXCPU (tempo de CPU), SIGSEGV (stack overflow) ou verifique o retorno de malloc() (NULL quando RLIMIT_AS é excedido).

Erro comum: Esquecer que fork() herda os limites do pai, mas exec() mantém os limites. Ajuste antes de criar processos filhos.

Referências