Elmord's Magic Valley

Software, lingüística e rock'n'roll. Às vezes em Português, sometimes in English.

Laptop mode, ACPI, fsync, e a arte de interceptar syscalls

2012-09-27 13:46 -0300. Tags: comp, unix, mundane, prog, em-portugues

Recentemente eu comprei um Dell Inspiron Mini 10 usado. Essa máquina tem uma propriedade peculiar: ela não tem fan! O processador, um Intel Atom N270 1.6GHz, esquenta suficientemente pouco para que a máquina não precise de um. Conseqüentemente, a máquina é dead-silent – ou quase. Infelizmente esse modelo vem com um HD, ao invés de um SSD, e portanto a máquina faz algum barulho, ainda que isso só seja significativo em ambientes realmente silenciosos (e.g., à noite).

Felizmente, esse tal de Linux possui uma feature especial de primeira chamada laptop mode. Quando o laptop mode está ativo e ocorre uma leitura ou escrita física ao disco, o kernel aproveita para escrever em disco todos os blocos sujos (modificados) que estejam em buffers de memória. A idéia é que o disco fique desligado a maior parte do tempo, e nas poucas ocasiões em que ele é "acordado", realizam-se todas as operações de I/O pendentes, de modo que o disco não tenha que ser reativado tão cedo. Para que o laptop mode seja efetivo, é necessário ajustar o kernel para manter as coisas o maior tempo possível em buffers, através de certas configurações em /proc/sys/vm e outros locais. O laptop mode foi concebido para economizar bateria em laptops, mas também serve para deixá-lo mais silencioso.

Easy mode

A maneira mais fácil de usar o laptop mode é através das Laptop Mode Tools (pacote laptop-mode-tools no Debian e derivados), um conjunto de scripts que dão conta de ativar o laptop mode quando a máquina está fora da tomada, e desativá-lo quando a máquina é ligada na tomada e quando a bateria está fraca (evitando que se perca todas as mil coisas que estão em cache caso a bateria acabe). O laptop-mode-tools é um pacote bastante bloated completo, com mais de oito mil opções de configuração e módulos que controlam não somente o HD como diversas outras features de economia de energia, tais como brilho do display, redução da potência de certas placas wireless, USB auto-suspend, entre outras coisas. Basta um apt-get install laptop-mode-tools, e ser feliz para sempre.

Obviamente, tudo isso é insuportável.

True mode

Mais instrutivo é fazer o serviço na mão. O objetivo é ativar o disco apenas esporadicamente, tipicamente a cada 10 minutos. A idéia é que os buffers sejam gravados com uma freqüência suficiente para que não se perca muita coisa caso o sistema sofra uma queda (falta de energia ou whatever), mas se mantenha o disco desligado o maior tempo possível. São três os pontos que exigem configuração.

Arquivos em /proc/sys/vm

/proc/sys é uma interface para diversas variáveis que controlam o comportamento do kernel. /proc/sys/vm, em particular, contém as variáveis que controlam o gerenciamento de memória (Virtual Memory) do kernel. Essas variáveis podem ser manipuladas lendo e escrevendo nos arquivos, assim:

# cat /proc/sys/vm/laptop_mode 
0
# echo 2 >/proc/sys/vm/laptop_mode 
# cat /proc/sys/vm/laptop_mode 
2
As variáveis relevantes para o funcionamento do laptop mode são:
Arquivo Descrição Valor
padrão
Valor
laptop
laptop_mode Define quantos segundos o kernel espera quando ocorre um acesso ao disco para começar a despejar os buffers. 0 desativa o laptop mode. 0 2
dirty_writeback_centisecs Intervalo (em centésimos de segundo) entre ativações do daemon pdflush, que despeja os buffers sujos para o disco. 0 desativa o despejo periódico. 500 60000
dirty_expire_centisecs Tempo (em centésimos de segundo) depois do qual uma página suja é considerada velha o suficiente para ser elegível a ser despejada em disco na próxima ativação do pdflush. 3000 60000
dirty_ratio Percentual de páginas (da memória total) máximo que um processo pode sujar sem forçar um despejo em disco. 40 60
dirty_background_ratio Sem laptop mode: Percentual de páginas sujas (da memória total) que provoca a ativação do pdflush.

Com laptop mode: Percentual de páginas sujas máximo permitido depois de um despejo em disco causado por um processo que tenha excedido a dirty_ratio.

10 1

Os itens da coluna "Valor laptop" indicam valores típicos usados em laptop mode (roubados dos padrões do laptop-mode-tools e de alguma página que já não encontro mais).

Tempo de commit dos sistemas de arquivo

ext3 e outros sistemas de arquivo com journaling realizam um commit periódico (dos dados? dos metadados? do journal? de tudo? respostas são bem-vindas). No caso do ext3, o padrão é fazer um commit a cada 5 segundos. O intervalo de commit de um filesystem pode ser alterado com um comando do tipo:

mount filesystem -o remount,commit=600

onde filesystem pode ser um nome de dispositivo (e.g., /dev/sda1) ou um ponto de montagem (e.g., /).

Tempo de standby do disco e modo de economia de energia

Por fim, agora que evitamos que o disco seja acessado, podemos mandá-lo dormir quando não há acessos. Para isso, usamos o comando hdparm. O hdparm permite configurar dúzias de parâmetros do disco. Os que nos interessam no momento são as seguintes opções:

A sintaxe do hdparm é hdparm opções /dev/dispositivo.

Tudo de uma vez

Agora podemos fazer um pequeno script para ativar e desativar o laptop mode, no que diz respeito ao HD. Você pode colocar esse script em /usr/local/bin/set-laptop-mode, por exemplo. (O script segue a metodologia Facão-Driven Development™, e não se preocupa muito com flexibilidade de configuração. Afinal, basta alterar o fonte!)

#!/bin/bash

# Status atual do laptop mode. Evitaremos executar os comandos
# se o sistema já estiver no modo que queremos.
current_status="$(</proc/sys/vm/laptop_mode)"

# Mas se a primeira opção for -f, fazemos mesmo assim.
[[ $1 = -f ]] && { shift; current_status=-1; }

set_commit_time() {
    # set_commit_time DISK SECONDS
    # Ajusta o commit time de todos os filesystems do dispositivo DISK.
    for filesystem in $(mount | cut -d' ' | grep "^$DISK"); do
        mount "$filesystem" -o remount,commit="$SECONDS"
    done
}

case "$1" in
    on)
        [[ $current_status -gt 0 ]] && exit
        echo 60000 >/proc/sys/vm/dirty_expire_centisecs
        echo 60000 >/proc/sys/vm/dirty_writeback_centisecs
        echo 1 >/proc/sys/vm/dirty_background_ratio
        echo 60 >/proc/sys/vm/dirty_ratio
        echo 2 >/proc/sys/vm/laptop_mode
        set_commit_time sda 600
        hdparm -B 1 -S 4 /dev/sda
        ;;
    off)
        [[ $current_status -eq 0 ]] && exit
        echo 3000 >/proc/sys/vm/dirty_expire_centisecs
        echo 500 >/proc/sys/vm/dirty_writeback_centisecs
        echo 10 >/proc/sys/vm/dirty_background_ratio
        echo 40 >/proc/sys/vm/dirty_ratio
        echo 0 >/proc/sys/vm/laptop_mode
        set_commit_time sda 5
        hdparm -B 128 -S 0 /dev/sda
        ;;
    *)
        echo "Usage: ${0#*/} [-f] on|off" >&2
        exit 1
        ;;
esac

Agora basta dar permissão de executável para o camadada (chmod +x /usr/local/bin/set-laptop-mode), e ativar e desativar o laptop mode com os comandos set-laptop-mode on e set-laptop-mode off.

Mas eu quero automático

Bom, falta fazer o sistema chamar o script sozinho quando o laptop é ligado ou desligado da tomada. Para tal, podemos usar o acpid, um daemonzinho feliz que espera por eventos ACPI e executa comandos de acordo com os eventos que observa. ACPI trata-se do Advanced Configuration and Power Interface, um mecanismo para gerenciamento de energia e controle de certos dispositivos (notavelmente o botão Power da maior parte das máquinas e os botões multimídia de certos notebooks).

O acpid lê os arquivos de configuração contidos em /etc/acpi/events. Cada arquivo tem a forma:

event=regex
action=comando

O acpid identifica os eventos gerados pelo ACPI através de strings (tais como ac_adapter ACPI0003:00 00000080 00000000). Quando um evento é observado, o acpid procura por todos os arquivos cuja regex case com a string do evento, e executa os comandos associados. Na string do comando, o valor %e é substituído pela string do evento, e %% por um % literal. Como o comando é processado pelo shell, deve-se usar aspas em torno do %e caso não se queira que o shell splite a string de evento em palavras ou faça algum outro tipo de expansão.

Podemos então criar um arquivinho /etc/acpi/auto-laptop-mode:

event=ac_adapter.*
action=/usr/local/bin/auto-laptop-mode

Isso fará com que, toda vez que a máquina seja ligada ou desligada da tomada, o script /usr/local/bin/auto-laptop-mode seja chamado. Falta escrever o tal script. Ao invés de usarmos os números místicos da string de evento para determinar se a máquina foi ligada ou desligada, vamos ao invés disso usar o comando acpi para determinar o status da bateria (que é Discharging caso a máquina não esteja na tomada). Uma vantagem disso é que poderemos chamar o script por fora do ACPI (por exemplo, na inicialização da máquina, ou na cron).

O comando acpi imprime uma string do tipo:

Battery 0: Discharging, 97%, 01:56:46 remaining

Podemos extrair os dados que nos interessam com um sed:

# acpi | sed -r 's/^[^:]*: ([^,]*), ([0-9]+)%.*/\1 \2/'
Discharging 97

Nosso script ativará o laptop mode se a máquina não estiver na tomada e houver pelo menos 5% de carga, e o desativará caso contrário.

#!/bin/bash

# Coloca o status e o percentual de carga em $1 e $2.
set -- $(acpi | sed -r 's/^[^:]*: ([^,]*), ([0-9]+)%.*/\1 \2/')

if [[ $1 = Discharging && $2 -gt 5 ]]; then
    set-laptop-mode on
else
    set-laptop-mode off
fi

E está feito. Só temos um problema: quando a bateria ficar fraca, o evento ac_adapter não será gerado, e portanto o laptop mode não será desativado automaticamente. Há duas soluções para esse problema:

  1. Se a sua máquina gera eventos do tipo battery quando é (des)ligada na tomada e quando a bateria está fraca (o que não é o meu caso), você pode substituir a expressão regular do evento em /etc/acpi/events/auto-laptop-mode por battery.*.
  2. Se essa solução não lhe serve, você pode colocar uma tarefa na cron para executar o script uma vez por minuto. É por essa razão que o set-laptop-mode evita executar comandos caso seja executado múltiplas vezes: se colocado na cron, o script será chamado inúmeras vezes sem que tenha que fazer nada. Basta a seguinte linha no /etc/crontab:
    * * * * * root /usr/local/bin/auto-laptop-mode

    Você pode substituir o primeiro * por */5 para executar o script a cada 5 minutos, por exemplo.

Com isso, fizemos em cinqüenta linhas o que o laptop-mode-tools faz em umas mil, mais os módulos. Nada como o raciocínio categorial.

fsync

Tanto laptop-mode-tools quanto a nossa solução nos deixa um problema. Alguns programas fazem uso das chamadas de sistema fsync e fdatasync. Essas chamadas forçam o kernel a despejar os buffers de um arquivo para disco, e são usadas para garantir a consistência desses arquivos em caso de um crash do sistema. O kernel sempre obedece a essas chamadas, mesmo com o laptop mode ativo, fazendo com que o disco seja ativado. Se algum programa faz uso freqüente dessas chamadas, o laptop mode se torna inútil.

Três são os vilões mais comuns: o syslog, editores de texto como Vim e Emacs, e o Firefox.

O syslog pode ser configurado para não forçar o despejo dos logs adicionando-se um - antes dos nomes dos arquivos para os quais se deseja evitar o fsync nos arquivos de configuração (/etc/rsyslog.conf e /etc/rsyslog.d/*).

O Vim faz o fsync dos arquivos quando são salvos. Isso pode ser controlado pela opção fsync: :set fsync ativa o despejo automático (o padrão), e :set nofsync desativa. Além disso, o Vim faz o fsync dos swapfiles, arquivos temporários que são usados para recuperação de modificações caso haja um crash durante a edição antes de o arquivo ser salvo. Isso é controlado pela opção swapsync: se seu valor for fsync (o padrão), é usada a chamada a fsync na gravação do swapfile; se seu valor for sync, uma chamada a sync é feita; se for a string vazia, nada é feito.

Uma conseqüência de não fsyncar os arquivos é que se houver um crash seu trabalho pode ser perdido. Uma alternativa seria não sincronizar os arquivos quando salvos, mas sincronizar os arquivos de swap, e colocá-los em uma outra unidade (um pendrive ou um SD-card, por exemplo). Assim, se houver um crash os swapfiles são capazes de recuperar o arquivo, mas se evita escrever no HD. Note que normalmente, no momento em que o Vim salva um arquivo, ele considera que a informação atual do swapfile não é mais necessária, já que todas as modificações já foram para o disco, o que é falso caso desativemos o fsync. Para fazer com que o swapfile contenha toda a informação necessária para a recuperação do arquivo, pode-se usar o comando :preserve.

Uma pequena dose de Vimscript no ~/.vimrc dá conta de fazer tudo isso automaticamente:

function LaptopModeWrite()
    if readfile("/proc/sys/vm/laptop_mode")[0] == 0
        set fsync swapsync=fsync directory-=/media/seu-pendrive-ou-sdcard
    else
        set nofsync swapsync= directory^=/media/seu-pendrivre-ou-sdcard
        preserve
    fi
endfunction

call LaptopModeWrite()
autocmd BufWritePre * call LaptopModeWrite()

O caso do Firefox é mais complicado. O Firefox usa a biblioteca SQLite para guardar o histórico e favoritos, e a SQLite usa fsyncs para manter a consistência dos bancos de dados. Além disso, o Firefox fsynca um arquivinho chamado sessionrestore.js, que ele usa para recuperar as tabs abertas no caso de um crash do browser. Aparentemente não há uma opção para desativar o uso de fsync no Firefox. As únicas alternativas que nos restam são mover os arquivos em questão para outra unidade, ou alterar o fonte do Firefox e da SQLite para não chamar fsync.

Ou alterar a fsync para não fazer nada.

LD_PRELOAD

Que me conste, não há uma maneira "normal" de substituir uma syscall sem modificar o kernel. Entretanto, a maior parte dos programas não chama syscalls diretamente, mas sim por intermédio de funções da biblioteca padrão C (libc), que se encarregam de chamar as syscalls. Acontece que a libc, como a maior parte das bibliotecas, é uma biblioteca dinamicamente linkada. Isso significa que as chamadas para funções da biblioteca no programa não saltam para endereços fixos, mas sim para endereços contidos em uma tabela de indireção que é preenchida pelo linker quando o programa é iniciado. Isto é, o linker, quando carrega o programa, olha os nomes das funções que o programa procura na biblioteca, obtém seus endereços, e preenche a tabela, de modo que quando o programa chamar as funções, as chamadas apontarão para os lugares apropriados.

Acontece que o ld.so (o linker) permite sacanear esse processo. Se a variável de ambiente LD_PRELOAD existe, o linker a interpreta como uma lista (separada por espaços) de bibliotecas a serem carregadas antes de quaisquer outras. Se uma dessas bibliotecas redefinir um símbolo (por exemplo, a função fsync), a versão redefinida é a que vai parar na tabela de indireção do programa, e conseqüentemente chamadas à tal função caem na nossa função, e não na original. Isso quer dizer que podemos interceptar as chamadas a fsync e fdatasync que um programa qualquer faça, e repassá-las ou não para a fsync "legítima" segundo queiramos ou não.

A coisa mais simples que podemos fazer é simplesmente descartar todas as chamadas a fsync. Criemos um arquivo libnofsync.c:

#include <stdio.h>

int fsync(int fd) {
    fprintf(stderr, "Chamada a fsync interceptada.\n");
    return 0;
}

Agora compilemo-lo com gcc -Wall -shared -o libnofsync.so libnofsync.c. Está feito! Agora, se executarmos um export LD_PRELOAD=caminho-do-arquivo/libnofsync.so, todo programa que for executado a partir de agora nesse shell utilizará a fsync modificada. Experimente abrir o Firefox.

Podemos elaborar mais sobre o tema, entretanto. Idealmente, queremos interceptar fsyncs e fdatasyncs apenas quando o laptop mode estiver ativado, e apenas quando o arquivo a ser despejado se encontra no disco. Caso decidamos que o fsync é permitido, precisamos chamar a fsync legítima; para isso, utilizaremos a função dlsym, da biblioteca dl (dynamic linker), que permite procurar um símbolo por nome em uma biblioteca. Para mais detalhes, dê uma olhada na manpage da dlopen.

#include <stdio.h>
#include <dlfcn.h>  //define dlsym() e RTLD_NEXT

#include <sys/types.h>     // para uso da fstat()
#include <sys/stat.h>
#include <sys/unistd.h>

// Variáveis globais onde armazenaremos ponteiros para as versões legítimas
// das funções. 'static' faz com que as variáveis não sejam exportadas
// (visíveis a partir do programa ou de outras bibliotecas.)
static int (*sys_fsync)(int);
static int (*sys_fdatasync)(int);

// Função de inicialização da biblioteca. Qualquer função marcada por
// __attribute__((constructor)) é considerada pelo GCC/linker como uma função
// que deve ser chamada quando a biblioteca é carregada.

__attribute__((constructor)) void libnofsync_init() {
    // Procura por 'fsync' e 'fdatasync' nas bibliotecas que vêm depois da
    // atual na ordem de pesquisa de símbolos.
    sys_fsync = dlsym(RTLD_NEXT, "fsync");
    sys_fdatasync = dlsym(RTLD_NEXT, "fdatasync");
}

static int laptop_mode_enabled() {
    FILE *f = fopen("/proc/sys/vm/laptop_mode", "r");
    if (!f) return 1;
    int status = 0;
    fscanf(f, "%d", &status);
    fclose(f);
    return status;
}

static int fsync_allowed(int fd) {
    // Se não estamos em laptop mode, sempre permite.
    if (!laptop_mode_enabled())
        return 1;

    // Se o "major number" do dispositivo do arquivo for 0x08, trata-se do
    // dispositivo /dev/sda. Você pode descobrir o major number do seu disco
    // executando um 'stat -c %D /' e desprezando os dois últimos dígitos.
    struct stat info;
    if (fstat(fd, &info) < 0) return 1;
    if (major(info.st_dev) == 0x08) return 0;
    return 1;
}

static int handle_fsync(int fd, int (*sysfun)(int)) {
    if (fsync_allowed(fd))
        return sysfun(fd);
    else
        return 0;
}

int fsync(int fd) { return handle_fsync(fd, sys_fsync); }
int fdatasync(int fd) { return handle_fsync(fd, sys_fdatasync); }

E aí está. Para compilá-la, é necessário incluir a opção -ldl na linha de comando do GCC, para linkar a biblioteca com a biblioteca dl.

Caveat

Essa história de desligar e ligar o HD consome sua vida útil. Segundo alguém na Internet, HDs de laptops são feitos para durar cerca de 300 mil ciclos de liga-desliga. HDs de desktops duram ainda menos, por volta de 50 mil ciclos. Por isso, o ideal é evitar que o disco seja ligado e desligado com muita freqüência; por exemplo, enquanto se está em laptop mode, ficar lendo mil arquivos diferentes a intervalos espaçados (suficientes para que o HD tenha desligado) não é uma boa idéia, pois os arquivos não estarão em cache quando forem lidos. O laptop-mode-tools vem com uma ferramenta chamada lm-profiler, que é capaz de lhe dizer que programas estão produzindo acessos a disco. O que essa ferramenta faz é basicamente dar um echo 1 >/proc/sys/vm/block_dump, o que faz com que o kernel reporte todos os acessos a disco pelo log do kernel, acessível pelo comando dmesg, ou por cat /proc/kmsg, caso o daemon de logging não esteja ativo. (Aliás, é bom que ele não esteja, pois escrever nos logs pode gerar acessos extra ao disco.) Não tenho dados exatos de quanta bateria se economiza com o laptop mode, embora reze a lenda que é "bastante". Use com cautela.

Comentários / Comments (1)

Joooonas, 2012-09-28 11:28:25 -0300 #

Tu devia guardar tudo isso e escrever um livro técnico e ganhar milhões.



P.S.: O texto está muito bom, aliás!


Deixe um comentário / Leave a comment

Main menu

Posts recentes

Comentários recentes

Tags

em-portugues (213) comp (137) prog (68) in-english (50) life (47) pldesign (35) unix (34) lang (32) random (28) about (27) mind (25) lisp (23) mundane (22) fenius (20) ramble (17) web (17) img (13) rant (12) hel (12) privacy (10) scheme (10) freedom (8) copyright (7) bash (7) music (7) academia (7) lash (7) esperanto (7) home (6) mestrado (6) shell (6) conlang (5) emacs (5) misc (5) latex (4) editor (4) book (4) php (4) worldly (4) android (4) politics (4) etymology (4) wrong (3) security (3) tour-de-scheme (3) kbd (3) c (3) film (3) network (3) cook (2) poem (2) physics (2) wm (2) treta (2) philosophy (2) comic (2) lows (2) llvm (2) perl (1) en-esperanto (1) audio (1) old-chinese (1) kindle (1) german (1) pointless (1) translation (1)

Elsewhere

Quod vide


Copyright © 2010-2020 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.