Monitorando Sistemas De Arquivos Com inotify Em C

Introdução

Desde a versão 2.6.13 do Linux, o kernel inclui internamente um notificador de eventos chamado inotify. Com ele, podemos monitorar várias mudanças no sistema de arquivo através de eventos, como por exemplo, arquivos deletados, acessados e criados. Esse sistema de notificação pode ser acessível por bibliotecas padrões do C, permitindo assim qualquer aplicação do usuário usufruir dessa funcionalidade.

Hoje veremos como criar programas simples que utilizam o inotify.

O sistema inotify

A monitoração de sistema de arquivos é muito importante e pode ser explorada principalmente por aplicações de segurança. O sistema antigo, conhecido como dnotify, não conseguiu acompanhar as exigências de novas aplicações e também não fornecia uma interface nada amigável ao programador, e um exemplo disso era que a notificação de eventos era feita através de sinais. Devido a esses problemas, foi-se necessário a criação de um outro sistema de monitoração, mas que deveria ser mais rápido, amigável e acima de tudo seguro. O kernel, desde sua versão 2.6.13, incluiu em seu código o sistema inotify que trabalha com arquivos descritores para a notificação de eventos, um modo bem melhor que os sinais do dnotify.

Para uma melhor de discussão sobre a vantagem do inotify em cima do dnotify, veja o link [3].

Conferindo suporte ao inotify

Para usar o inotify, você deve ter um kernel Linux superior ou igual a 2.6.13:

$ uname -a
Linux daemonio.fedora 3.5.2-1.fc17.i686 #1 SMP Wed Aug 15 16:52:27 UTC 2012 i686 i686 i386 GNU/Linux

Confira também se existe o arquivo /usr/include/linux/inotify.h:

$ ls -la /usr/include/linux/inotify.h
-rw-r--r--. 1 root root 2914 May  7 14:52 /usr/include/linux/inotify.h

Como funciona o inotify

Esse sistema é interno ao kernel e por isso as chamadas a sua API são feitas através de system calls. Antes de tudo, devemos obter um arquivo descritor de monitoração através da função inotify_init(). Desse modo, assim que um evento ocorrer, as informações sobre ele deverão ser lidas nesse descritor.

Após a obtenção do descritor, devemos definir qual arquivo será monitorado. Se esse arquivo for um diretório, então todos os arquivos nele serão capazes de engatilhar eventos. Setamos qual arquivo será monitorado através da função inotify_add_watch(). Essa função recebe o arquivo descritor, o caminho do arquivo e também os eventos que a aplicação irá monitorar. O par arquivo+eventos é chamado de watch.

A partir de agora, se todo e qualquer evento indicado acontecer no arquivo monitorado, algumas informações estarão prontas para serem lidas no descritor. Essas informações nos diz, por exemplo, qual arquivo engatilhou o evento.

Basicamente é isso que acontece. A seguir veremos a API mais detalhadamente e logo adiante, um código para exemplificar seu uso.

A API inotify

A comunicação com a API inotify é feita através de system calls e arquivos descritores. Isso torna a programação bem flexível, pois já que estamos trabalhando com arquivos descritores, poderemos utilizar as chamadas de sistema para I/O, como read(), close() e select().

Antes de tudo, devemos obter uma instância inotify usando a função:

int fd ;
fd = inotify_init() ;
if(fd<0) {
perror("inotify_init") ; /* errno é setado */
return -1 ;
}

A função retorna um arquivo descritor que será usado nas outras funções da API.

Em seguida devemos adicionar um watch. Um watch nada mais é que o arquivo monitorado mais os eventos que serão tratados. Por exemplo, para monitorar o diretório /tmp para os eventos de acesso, modificação e deleção, fazemos:

int wd ;
wd = inotify_add_watch(fd, "/tmp", IN_ACCESS | IN_MODIFY | IN_DELETE) ;

if(wd < 0) {
perror("inotify_add_watch") ;
return -1 ;
}

A função inotify_add_watch() é a responsável por associar um watch ao descritor fd. Os eventos são passados em forma de OR, porque o armazenamento e mapeamento de eventos é feito através de mapas de bits. O retorno da função é o identificador do watch. A partir desse momento, se um desses três eventos ocorrer, o arquivo descritor fd estará pronto para a leitura das informações indicando qual arquivo engatilhou o evento e também qual evento ocorreu.

E por fim, para se remover um watch, usamos a função inotify_rm_watch():

inotify_rm_watch(fd, wd)

Uma chamada à função close() com parâmetro fd, fecha a instância inotify e toda a memória e watches, associados a ela:

close(fd) ;

Resumindo, um programa típico de monitoração fará o seguinte:

1) Usar inotify_init() para se obter o arquivo descritor
2) Adicionar um ou mais watches com inotify_add_watch()
3) Esperar por eventos
4) Processar eventos recebidos e esperar por mais eventos
5) Fechar arquivo descritor, liberar memória e sair

Lista de Eventos

Vários tipos de eventos são fornecidos pelo inotify. A seguir, alguns deles:

+---------------+-----------------------------------+
|    Evento     |            Descrição              |
+---------------+-----------------------------------+
|  IN_ACCESS    |         Arquivo foi lido.         |
+---------------+-----------------------------------+
|  IN_MODIFY    |       Arquivo foi alterado.       |
+---------------+-----------------------------------+
|  IN_ATTRIB    | Metadados modificados (ex: inode) |
+---------------+-----------------------------------+
|   IN_OPEN     |          Arquivo aberto.          |
+---------------+-----------------------------------+
|   IN_CREATE   |          Arquivo criado.          |
+---------------+-----------------------------------+
|  IN_DELETE    |         Arquivo deletado.         |
+---------------+-----------------------------------+
|IN_ALL_EVENTS  |         Todos os eventos.         |
+---------------+-----------------------------------+

Lembrando que, quando se monitora um diretório, os eventos são disparados para qualquer arquivo dentro dele.

Veja em [1] uma tabela maior de eventos.

Leitura de Eventos

Após a inicialização do inotify e da adição de watches, sua aplicação estará pronta para receber eventos. Os eventos são enfileirados de forma assíncrona, mas são lidos de forma síncrona por read(), isto é, a chamada a read() bloqueia a aplicação até que dados suficientes estejam no descritor de leitura.

Eventos são entregues para a aplicação na forma de uma struct:

struct inotify_event 
{
  int wd; 		/* The watch descriptor */
  uint32_t mask; 	/* Watch mask */
  uint32_t cookie;	/* A cookie to tie two events together */
  uint32_t len;		/* The length of the filename found in the name field */
  char name __flexarr;	/* The name of the file, padding to the end with NULs */	
}

wd é o identificador do watch. O inteiro mask é uma máscara de bits que guarda o evento ocorrido mais algumas informações. O cookie é usado quando há movimentação de arquivos entre diretórios monitorados pela aplicação. O campo len contém o tamanho total do campo name mais a quantidade de bytes nulos usados para padding. O campo name é o nome do arquivo que disparou o evento. Esse campo é alocado dinamicamente e seu tamanho total é indicado por len e não por strlen(), por causa dos bytes de padding. Se len é zero, o arquivo não tem um nome associado.

Veja que o nome do arquivo é relativo ao watch wd, isto é, se estamos monitorando o diretório /tmp e o arquivo /tmp/nomearquivo engatilhar um evento, então o campo name terá somente o nome do arquivo, nesse caso nomearquivo.

No caso da leitura com read(), como a estrutura de eventos tem tamanho dinâmico, não podemos passar um tamanho fixo para essa função, pois se esse valor for menor que o esperado, perderemos alguns dados. A solução, então, é passar um valor muito alto para read() a fim de ler o máximo possível de eventos, e em seguida, tratá-los separadamente.

#define TAMANHO_EVENTO sizeof(struct inotify_input)
/* Tamanho para 1024 eventos */
#define TAMANHO_BUF 1024*TAMANHO_EVENTO
...
char buf[TAMANHO_BUF] ;
int len ;
struct input_event *evento ;

len = read(fd, buf, TAMANHO_BUF) ;
if(len < 0) { /* erro */ }
if(len == 0) { /* tamanho do buffer muito pequeno. */

/* Aqui tratamos os eventos lidos. */

Para se tratar os eventos lidos, devemos obtê-los do buffer usado na leitura. Se nenhum dado foi perdido, os bytes do buffer estarão alinhados de forma que toda vez que avançamos TAMANHO_EVENTO + evento->len bytes, teremos um novo evento. [3]

Sabemos que, por padrão, uma chamada a read() é assíncrona, o que faz a aplicação travar até que dados cheguem ao buffer de leitura. Para evitar esse comportamento, podemos ler esses dados quando temos certeza quando que eles estão disponíveis, e um modo de fazer isso é com a ajuda de select().

Para ilustrar, veremos a seguir um exemplo completo da utilização do inotify.

o código: dmonitor.c

/*
 * [dmonitor.c]
 * Monitora um arquivo/diretório e intercepta
 * os eventos de Acesso, Criação e Deleção.
 *
 * [Autor]
 * Marcos Paulo Ferreira (daemonio)
 * undefinido gmail com
 * https://daemoniolabs.wordpress.com
 *
 * [Compilação]
 * $ gcc -o dmonitor dmonitor.c
 *
 * [Uso]
 * $ ./dmonitor /tmp
 *
 * Crie e delete arquivos no diretório /tmp e observe
 * a saída do programa logo em seguida.
 *
 * Versão 1.0 by daemonio @ Tue Sep 25 18:18:55 BRT 2012
 *
 */
#include <stdio.h>
#include <sys/inotify.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

/* Define tamanho máximo do buffer que receberá
 * os eventos. */
#define TAMANHO_EVENTO (sizeof(struct inotify_event))
#define TAMANHO_BUF 1024*TAMANHO_EVENTO

int main(int argc, char **argv) {
    int fd, wd ;
    char buf[TAMANHO_BUF] ;
    int i, t, l ;
    fd_set rfds ; /* para select */
    struct inotify_event *evento ;

    /* Testa parâmetros. */
    if(argc == 1) {
        printf("[uso] %s <arquivo/diretorio>\n", argv[0]) ;
        return -1 ;
    }

    /* Obtém o descritor. */
    if((fd = inotify_init())<0) {
        perror("inotify_init") ;
        return -1 ;
    }

    /* Adiciona watch com três eventos: acesso, criação e deleção. */
    wd = inotify_add_watch(fd, argv[1], IN_ACCESS | IN_CREATE | IN_DELETE) ;
    if(wd < 0) {
        perror("inotify_add_watch") ;
        return -1 ;
    }

    while(1) {
        /* Procedimentos de select() */
        FD_ZERO(&rfds) ;
        FD_SET(fd, &rfds) ; /* Aqui podemos inserir mais descritores. */

        /* Verifica se há dados no descritor. */
        t = select(FD_SETSIZE, &rfds, NULL, NULL, NULL) ;
        if(t<0) {
            perror("select") ;
            return -1 ;
        }

        /* Sem dados. */
        if(t == 0) continue ;

        /* Aqui temos dados. */

        /* Lê o máximo de eventos. */
        l = read(fd, buf, TAMANHO_BUF) ;

        /* Percorre cada evento lido. */
        i=0 ;
        while(i<l) {
            /* Obtém dados na forma da struct. */
            evento = (struct inotify_event *)&buf[i] ;

            /* Se o campo len não é nulo, então temos
             * um nome no campo name. */
            if(evento->len) {
                printf("[+] Arquivo `%s': ", evento->name) ;
            } else {
                printf("[+] Arquivo desconhecido: ") ;
            }

            /* Obtém o evento. */
            if(evento->mask & IN_CREATE) {
                printf("Criado.\n") ;
            } else if(evento->mask & IN_DELETE) {
                printf("Deletado.\n") ;
            } else {
                printf("Acessado.\n") ;
            }

            /* Avança para o próximo evento. */
            i += TAMANHO_EVENTO + evento->len ;
        }
    }

    return 0;
}
/* EOF */

Compile e use:

$ gcc -o dmonitor dmonitor.c
$ ./dmonitor /tmp
[+] Arquivo `arquivovazio': Criado.
[+] Arquivo `arquivovazio': Deletado.

Em outro terminal, digite:
$ cd /tmp
$ touch arquivovazio
$ rm arquivovazio

Códigos semelhantes a este podem ser vistos em [3] e [4].

Aplicações

Vários aplicativos utilizam a biblioteca notify, como o procurador Beagle e o sistema de monitoramento Gamin [3]. Porém, essa biblioteca pode ser utilizada para outros fins. Alguns exemplos são:

  • Segurança: monitorar arquivos é uma tarefa essencial para mantermos a integridade do sistema. Com o inotify, podemos verificar se arquivos importantes estão sendo acessados e alterados por aplicações desconhecidas.
  • Estatístico: com o inotify podemos gerar, por exemplo, relatórios de arquivos mais acessados pelos usuários. Com isso, o acesso a esses arquivos poderia ser agilizado para refletir num maior desempenho nas aplicações do usuário.
  • Hacking: um aplicativo que monitora sistemas de arquivos é de muito interesse para hackers e crackers em geral. Um exemplo que vi foi uma backdoor que era habilitada quando determinado arquivo era criado. Como esse comportamento é pouco previsível e pouco rastreável para muitos administradores, um atacante terá acesso total no sistema pela backdoor simplesmente criando/deletando arquivos.

Referências

[1] Monitor Linux file system events with inotify by Ian Shields (Acessado em: Setembro/2012)
http://www.ibm.com/developerworks/linux/library/l-inotify/

[2] Monitor file system activity with inotify by Martin Streicher (Acessado em: Setembro/2012)
http://www.ibm.com/developerworks/linux/library/l-ubuntu-inotify/index.html

[3] Kernel Korner – Intro to inotify by Robert Love (Acessado em: Setembro/2012)
http://www.linuxjournal.com/article/8478?page=0,1

[4] Inotify Example: Introduction to Inotify with a C Program Example by BALAKRISHNAN MARIYAPPAN (Acessado em: Setembro/2012)
http://www.thegeekstuff.com/2010/04/inotify-c-program-example/

6 pensamentos sobre “Monitorando Sistemas De Arquivos Com inotify Em C

  1. Pingback: Monitorando Com O inotify-tools | Daemonio Labs

  2. Prezado,
    Parabens pela dica.

    Estou usando seu arquivo para estilizar (colocar logo e pontos cardeais) numa imagem q é recebida de uma camera aproximadamente todo minuto, antes dela ir para grande rede.

    Depois do evento Create gostaria de rodar um script shell, ou mesmo dentro do seu script em C, usando o nome do arquivo recem gravado. Porem ocorre o erro abaixo:
    $ [+] Arquivo `IMG_2040.JPG’: Criado.
    sh: 1: Syntax error: Unterminated quoted string

    Coloquei esta chamada assim:

    if(evento->mask & IN_CREATE) {
    printf(“Criado.\n”);
    system(“convert -font helvetica -fill white -pointsize 12 -draw \”text 235,10 Norte\” `%s’ `%s’ &”);
    system(“php -q ./ctc-organizador.php &”);
    }

    O segundo comando roda bem, mas o convert, nao to conseguindo fazer estilizar a imagem.
    Agradeco qq dica.
    Lima

  3. Olá Lima,

    já que você está utilizando Shell Script então melhor usar o pacote inotfy-tools. Veja aqui como instalá-lo

    Monitorando Com O inotify-tools

    Abaixo, um pequeno script que deve resolver seu problema. Coloque seus comandos após o echo dentro do while. Lembrando que esse comando será executado para cada arquivo. O nome do arquivo está na variável $ARQUIVO. Em DIRETORIO coloque o nome do diretorio onde as imagens são criadas.

    —-
    #!/bin/bash
    # lima.sh
    # Sat Jul 6 06:11:33 BRT 2013

    DIRETORIO=’/tmp/imagens’ # mude aqui

    # inotifywait irá rodar indefinidamente.
    # é preciso um ctrl+c para encerrar o script

    inotifywait -e create –format ‘%f’ -m $DIRETORIO | while read ARQUIVO
    do
    # Esse comando será executado para cada arquivo criado.
    # O nome do arquivo está na variável $ARQUIVO
    echo “[+] Processando arquivo: $ARQUIVO”
    done
    —–

    Verifique se é isso mesmo que deseja.

    Abraços

  4. Ola,
    Obrigado pela ajuda.
    Eu ja tinha visto inotfy-tools. Mas optei por nao usa-lo pq eu teria q instalar mais um pacote, caso fosse distribuir o script.
    Nao to conseguindo rodar. da sempre erro:
    Setting up watches.
    Couldn’t watch –format: No such file or directory
    e sai do script.

Deixe um comentário