Bitrates para conversão de vídeos

Já falei sobre isso aqui. Diversas vezes, mas quero compartilhar com vocês as taxas de bitrate onde obtenho excelentes resultados.

Bitrate é a taxa de transferência de dados do vídeo para o dispositivo que vai “tocá-lo”. Tem alguma relação entre o sample rate e o tamanho final do arquivo e a qualidade do mesmo. Quando menor o bitrate, pior o arquivo fica (embora fique menor). Acontece que usar bitrates muito grandes não melhora, necessariamente, a qualidade e só faz seu arquivo ficar gigantesco…. O Youtube, por exemplo, recomenda as seguintes taxas:

Eu prefiro vídeos no formato 720p porque tenho um problema de visão… a qualidade de um vídeo 1080p ou 2160p não é assim tão perceptível para mim e, acredito, para a maioria das pessoas também. Mas, note, 5 Mb/s (Mega bits por segundo) e até mesmo 7.5 Mb/s me parecem um exagero. Se você usar metade dessa taxa conseguirá resultados muito bons. Por isso, converto meus vídeos com as seguintes opções do ffmpeg:

-maxrate 2000k -bufsize 2000k -b:v 2000k

Isso me dá um bitrate máximo de 2 Mb/s, em 720p, para o stream de vídeo. Também uso o codec h264 com a opção -c:v libx264.

Mas, e quanto ao áudio? De novo, usar uma taxa de 384 kb/s para um áudio stereo me parece exagero… Como regra geral, uso 64 kb/s para cada canal de áudio… Se for stereo, 128 kb/s. Se for Dolby 5.1, sendo 6 canais, 384 kb/s. Isso tem duas vantagens: O áudio fica pequeno, não há grandes perdas de qualidade e, para alguém com ouvidos sensíveis, um pedacinho da faixa de alta frequência é ceifada… Isso normalmente já ocorre, especialmente em áudios codificados no codec MP3 (embora eu use AC3 – que é um pouco melhor):

-c:a ac3 -b:a 128k -ac 2

Ou seja, meus vídeos em HD (mesmo em 1080p) são sempre convertidos com a seguinte linha de comando:

$ ffmpeg -i videoin.mkv -c:v libx264 -maxrate 2000k -bufsize 2000k -b:v 2000k -s hd720 \
                        -c:a ac3 -b:a 128k -ac 2 \
            videoout.mp4

No exemplo, o vídeo videoin.mkv será convertido com o bitrate de 2 Mb/s, no formato 1280×720 usando o codec h264 e o áudio, em 128 kb/s, stereo (fazendo downsampling se for Dolby) com bitrate de 128 kb/s – e o resultado será colocado num arquivo (container) mpeg4 – pode ser um matroska mesmo, se você quiser. A conversão respeitará o aspect ratio do vídeo original.

Uma alternativa ao codec de áudio AC3 é o AAC, que é preferido pelo Youtube e vídeos que você baixa via torrents… No caso do ffmpeg use a opção -strict experimental para usá-lo, se quiser, já que o ffmpeg não o implementa com precisão “em produção”, ou o codec libfdk_aac, se seu ffmpeg tiver sido compilado para suportá-lo… Mas, recomendo o AAC padrão, mesmo que experimental – se precisar que o áudio esteja codificado dessa forma, caso contrário, AC3.

Existe também o EAC3, mas ele não é suportado por muitos devices

Anúncios

strncpy, strncat (e snprintf) versus strlcpy e strlcat…

Well… ultimamente tenho mexido, profissionalmente, com alguns códigos de terceiros feitos para lidam com banco de dados via OCI (Oracle Call Interface). Algumas coisas me chamam a atenção:

  1. A não ser quando lidamos com o tipo externo SQLT_STR no mapeamento de binds e definições de campos de statements, a OCI não lida com strings do tipo ASCIIZ;
  2. O tamanho do buffer, incluindo o ‘\0’ final, no caso de SQLT_STR precisa ser informado para as funções da OCI.

Essas duas restrições torna obrigatório que as strings tenham um tamanho máximo bem definido. Se um campo NOME é definido como VARCHAR2(100), então o buffer, mapeado para SQLT_STR deve ter 101 bytes de tamanho para acomodar o ‘\0’. Mas, o que acontece se um ponteiro apontar para uma string com mais de 100 caracteres (excluindo o ‘\0’)?

Para evitar esse problema podemos lançar mão das funções strncpy() e strncat(). Só que existe uma pegadinha: strncpy() copia n bytes para o destino e só coloca o ‘\0’ final se houver espaço para isso. Algo como:

char d[10];
char *s = "Frederico Lamberti Pissarra";
...
strncat( d, s, sizeof d );

Fará com que o buffer d contenha a sequência “Frederico ” sem o ‘\0’ final!

A coisa piora para strncat(), se precisarmos fazer uma concatenação com tamanho fixo… Essa rotina copia n bytes, no máximo, além do tamanho original da string no buffer destino. O efeito é o mesmo acima, se fizermos:

char d[10] = "Fred";
...
strncat( d, "erico Lamberti Pissarra", 6 );

Note que o n de strcat() não significa o tamanho total do buffer, mas a quantidade máxima que será copiada para o buffer.

Aliás, funções como snprintf() sofrem do mesmo mal de strncpy… Se não tem espaço para o ‘\0’, ele simplesmente não é escrito no buffer.

FreeBSD

No FreeBSD (até onde sei) as funções strlcpy() e strlcat() foram adicionadas para considerar o tamanho máximo do buffer destino. O ‘l’ vem de length. Ou seja, no primeiro caso, strlcpy(), copiará n caracteres incluindo o ‘\0’ final. No segundo, strncat, também fará o mesmo, mas o tamanho a ser considerado é o do buffer final.

A primeira função é simples de implementar, se ela não for nativamente suportada pela libc (não estiver presente no header string.h):

#ifndef __FreeBSD__
char *strlcpy( char * restrict dest,
               char * restrict src,
               size_t n )
{
  if ( n > 1 )
  {
    --n;
    memcpy( dest, src, n );
    dest[n] = '\0';
  }
}
#endif

Repare que fiz uma pequena otimização, onde a cópia é feita apenas se n for maior que 1 byte.

Outro detalhe é que, possivelmente, usar memcpy() será mais rápido do que usar strncpy(). A primeira tende, em algumas arquiteturas, a usar instruções especializadas de cópia de blocos, enquanto a segunda, tende a copia byte por byte (Este é o motivo, em algumas análises, porque strlcpy tende a ser bem mais rápido que strncpy).

strlcat() precisa saber o tamanho da string original antes de fazermos a cópia:

#ifndef __FreeBSD__
char *strlcat( char * restrict dest, 
               char * restrict src, 
               size_t n )
{
  size_t size = strlen( dest );

  // Temos que considere o '\0' final!
  if ( size < n )
    strlcpy( dest + size, src, n - size );

  return dest;
}
#endif

O esquisito formato de Data/Hora do Oracle

Estou brincando com a OCI (Oracle Call Interface) para um projeto grande no meu “trampo”. Eis que topo com o tipo DATE que, parece, é simples. São apenas 7 bytes contendo, na sequência, o “século”, o “ano”, “mês”, “dia”, “hora”, “minutos” e “segundos”; e tudo obtido via o tipo “externo” SQLT_DAT… Bem… parece ser simples, mas a Oracle tinha que fazer suas “oraclices”.

Pra quê diabos eu quero obter a estrutura DATE ao invés de uma simples string do tipo “DD/MM/YYYY HH:MI:SS”? Ora bolas: Eu não gosto de mexer com strings, prefiro os dados inteiros! Isso ai gasta bem mais que 7 bytes, para começo de conversa, e precisa ser manipulado se eu quiser lidar com diferença entre datas/horas. É claro que eu podedia retornar uma string formatada como “YYYYMMDDHHMISS” e interpretá-la como um número inteiro, mas com 14 algarismos isso gastará 8 bytes (um dword ou uint64_t)…

A definição do campo não podedia ser mais simples, para um statement do tipo “SELECT SYSDATE FROM DUAL“:

char oradate[7];
int16_t oradate_ind;
OCIDefine *defp = NULL;
...
OCIDefineByPos(
  stmthp,
  &defp,
  errhp,
  1,
  oradate, sizeof oradate,
  SQLT_DAT,
  &oradate_ind,
  NULL, NULL,
  OCI_DEFAULT );

Acontece que, para que um ano caiba em um byte a Oracle preferiu dividir em dois (nada mais lógico), onde o primeiro é o século (ano, dividido por 100) e o segundo é o resto do ano, dividido por 100… Assim, 2018 seria dividido em 20 e 18. Porém, para suportar anos “negativos” esses dois valores são “polarizados” em 100. Ou seja, a Oracle soma 100 aos valores. 2018 é 120 e 118. A segunda polarização (ou excesso), para mim, não faz sentido, já que a primeira já garante o uso do século -100 até 155 (supostamente do ano -10000 até 15500. que seria somado ao ano. Assim, para decodificar o ano temos que fazer:

int year = (oradate[0] - 100)*100 + (oradate[1] % 100);

A divisão está ai porque assumo que o ano seja sempre positivo e aproveito a multiplicação por 100 feita antes na tentativa que o compilador otimize as operações inversas. O mês e o dia funcionam como devem…

Mas, o mais esquisito não é a data. O horário é fornecido com a adição de 1. Para a Oracle não existe 0:00:00, mas sim 1:01:01. Assim, o dia começa às 1:01:01 e termina às 24:60:60. Yep… existe 60 minutos e 60 segundos…

Como saber quanta memória está sendo usada pelo seu processo, no Linux?

O padrão POSIX fornece uma função para isso: getrusage(). Mas, infelizmente, alguns campos importantes ainda não são suportados pelo Linux. Até onde sei, a única maneira de obter esses dados é lendo as infos no procfs, como mostrado abaixo:

#define _GNU_SOURCE
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>

/*
  From 'man 5 proc':

  /proc/[pid]/statm
    Provides information about memory usage, measured in 
    pages.  The columns are:

      size       (1) total program size
                 (same as VmSize in /proc/[pid]/status)
      resident   (2) resident set size
                 (same as VmRSS in /proc/[pid]/status)
      share      (3) shared pages (i.e., backed by a file)
      text       (4) text (code)
      lib        (5) library (unused in Linux 2.6)
      data       (6) data + stack
      dt         (7) dirty pages (unused in Linux 2.6)

 */
struct memusage_s
{
  uint64_t size;
  uint64_t resident;
  uint64_t shared;
  uint64_t text;
  uint64_t data;
};

static int get_memusage ( pid_t, struct memusage_s * );

int main ( void )
{
  struct memusage_s mu;
  struct rusage ru;

  if ( get_memusage ( getpid(), &mu ) )
  {
    fputs ( "ERROR getting memory usage.\n", stderr );
    return EXIT_FAILURE;
  }

  printf ( "From procfs (used):\n"
           "\tProcess Size: %"PRIu64" KiB\n"
           "\tResident Set: %"PRIu64" KiB\n"
           "\tShared Pages: %"PRIu64"\n"
           "\tText Section Size: %"PRIu64" KiB\n"
           "\tData + Stack Size: %"PRIu64" KiB\n",
           mu.size, 
           mu.resident, 
           mu.shared, 
           mu.text, 
           mu.data );

  // RUSAGE_SELF porque quero info sobre ESTE processo!
  if ( getrusage ( RUSAGE_SELF, &ru ) )
  {
    fputs ( "ERROR getting resource usage.\n", stderr );
    return EXIT_FAILURE;
  }

  printf ( "\nFrom getrusage():\n"
           "\tMaximum Resident Set: %lu KiB\n"
           "\tIntegral shared text size: %lu KiB\n"
           "\tIntegral unshared data size: %lu KiB\n"
           "\tIntegral unshared stack size: %lu KiB\n"
           "\tPage reclaims: %lu\n"
           "\tPage faults: %lu\n"
           "\tSwaps: %lu\n",
           ru.ru_maxrss,
           ru.ru_ixrss,
           ru.ru_idrss,
           ru.ru_isrss,
           ru.ru_minflt,
           ru.ru_majflt,
           ru.ru_nswap );

  return EXIT_SUCCESS;
}

// NOTE: Unfortunately some fields from rusage structure
//       aren't implemented. So we have to get them from
//       procfs.
static int get_memusage ( pid_t pid, struct memusage_s *memp )
{
  FILE *f;
  char fname[24]; // 24 bytes are sufficient.

  snprintf ( fname, sizeof fname, "/proc/%d/statm", pid );

  if ( ! ( f = fopen ( fname, "r" ) ) )
    return -1;

  if ( fscanf ( f, "%"PRIu64" "
                   "%"PRIu64" "
                   "%"PRIu64" "
                   "%"PRIu64" "
                   "%"PRIu64,
                &memp->size,
                &memp->resident,
                &memp->shared,
                &memp->text,
                &memp->data ) != 5 )
  {
    fclose ( f );
    return -1;
  }

  fclose ( f );
  return 0;
}

Ao executar o programinha você obtém algo como isso:

$ ./memusage
From procfs (used):
	Process Size: 1088 KiB
	Resident Set: 159 KiB
	Shared Pages: 142
	Text Section Size: 1 KiB
	Data + Stack Size: 0 KiB

From getrusage():
	Maximum Resident Set: 3464 KiB
	Integral shared text size: 0 KiB
	Integral unshared data size: 0 KiB
	Integral unshared stack size: 0 KiB
	Page reclaims: 79
	Page faults: 0
	Swaps: 0

Não listei todos os campos da estrutura rsusage, só aqueles referentes ao uso de memória… Os campos em vermelho indicam os valores que deveríamos ler para obter o uso da memória pelo processo, mas o Linux não os implementa desde o kernel 2.6 (acho)…

Repare que o procfs nos diz que o processo usa 159 KiB do “conjunto residente” (RAM)… O consumo de 0 KiB dos dados + pilha é porque o uso não chegou nem a 1 KiB, mas a pilha está lá e o segmento de dados também… Note, também, que getrusage() nos dá o “conjunto residente” máximo usado até o momento. Não significa que está em uso.

Duas formas de implementar timeout para system calls que bloqueiam…

… e estou falando daquelas que usam file descriptors aqui, em ambiente Unix (Linux, FreeBSD, MacOS…).

A primeira e mais fácil é usando o sinal SIGALRM, como mostrei no artigo anterior, sobre uso de NTP:

  1. Escrevemos uma rotina de tratamento para SIGALRM;
  2. Chamamos alarm(segundos) antes da syscall que bloqueia;
  3. Chamos a syscall;
  4. Chamamos alarm(0) para desabilitar o disparo de SIGALRM.

Funciona que é uma beleza, mas tem um problema! A implementação da libc pode usar SIGALRM para algumas funções. É, talvez, o caso de sleep() — veja, por exemplo, no livro Advanced Programming in the UNIX Environment de Richard W. Stevens… Isso, é claro, atrapalha, já que sleep() escreverá sua própria rotina de tratamento de SIGALRM.

Outro método, mais seguro que funcione, é usar uma das syscalls de multiplexação (select, poll, epoll ou a biblioteca libevent). Para efeitos de simplicidade, usarei poll() aqui. A técnica é simples. Essas rotinas verificam se o file descriptor está pronto para escrita ou leitura, deixando a thread “dormente” enquanto não estão… Por exemplo, para verificar se o descritor contém dados para serem lidos, fazemos:

#define TIMEOUT_MS 3000

// Assume que sockfd seja nosso descritor...
struct pollfd pfd = { .fd = sockfd, .events = POLLIN };
sigset_t set, oldset;
int retcode, blocked;

sigfillset ( &set ); // todos os sinais...
sigprocmask ( SIG_BLOCK, &set, &oldset );
blocked = 1;

if ( ( retcode = poll ( &pfd, 1, TIMEOUT_MS ) > 0 )
{
  sigprocmask ( SIG_SETMASK, &oldset, NULL );
  blocked = 0;

  // chama read() para ler do descritor...
  ...
}

// Ainda temos os sinais bloquados, volta para o estado
// anterior!
if ( blocked )
{
  sigprocmask ( SIG_SETMASK, &oldset, NULL );
  blocked = 0;  // por garantia, caso usemos isso adiante.
}

// Ocorreu timeout ou erro em poll()?
switch ( retcode )
{
case -1: /* trata o erro aqui... */
         ...
         break;
case 0: /* trata o timeout aqui. */
        ...
}
...

Note que poll também é uma syscall e pode ser interrompida por um sinal. É conveniente bloquear os sinais enquanto ela estiver sendo executada. Isso é importante, especialmente se seu tratador de sinais não reinicia a syscall interrompida (default, quando se usa signal(), ajustável quando se usa sigaction() para registrar a rotina de tratamento de sinais usando o flag SA_RESTART)… Mas não está claro (para mim, pelo menos) nas man pages se, mesmo que poll seja reiniciada depois de interrompida, retornará ou não como interrompida (errno será EINTR) quando o argumento timeout não for negativo. Isso porque a documentação nos diz que poll desbloqueia em 3 condições:

  1. O file descriptor está “pronto”, de acordo com o evento na estruttura de pollfd;
  2. Um sinal a interrompe;
  3. O timeout expira.

Para não correr riscos, bloqueio todos os sinais para o processo (ou thread, se for o caso, mas, para isso temos pthread_sigmask() ou a função ppoll().

O método tradicional de multiplexing pode ser usado também (via select()) e é similar a poll, só tem aquele jeito esquisito de ajustar os eventos via macros FD_* e necessita da estrutura struct timespec para estabelecer o timeout (acho poll mais simples!).

O uso de epoll, que também é uma syscall, dizem, funciona bem melhor no Linux, mas é bem complicadinha… Dizem, por ai, também que libevent é a melhor maneira de lidar com eventos. De fato, se você está acostumado a lidar com o Apache HTTPD, lembrará dos “módulos de multi-processamento” (MPMs): prefork, worker e event. Hoje em dia, geralmente, prefere-se o MPM event no Apache… Dê uma pesquisada nesses dois últimos métodos (epoll e libevents). Se bem me lembro, o NGINX usa libevents por default.

Usando NTP…

Um dos métodos muito comuns em desenvolvimento de aplicações usando bancos de dados é o uso do relógio contido no host onde o banco está hospedado como se fosse “confiável”, como um método de sincronização de data/hora. No caso de sistemas que usam o Oracle é muito comum ver uma query assim:

SELECT SYSDATE FROM DUAL;

Algo semelhante é feito para o MS SQL Server e outros bancos de dados famosos:

SELECT GETDATE();  -- MS SQL Server
SELECT NOW();      -- MySQL e PostgreSQL

O problema é que todo momento em que seu código precise obter data e hora, terá que fazer uma consulta ao banco de dados. Lembre-se que uma consulta precisa ser decodificada pelo servidor antes de ser processada e há tratamento de conexão e acessos concorrentes. Sua data/hora obtida pode não ser precisa.

Felizmente existe uma solução, que já existe tem mais de duas décadas! NTP. O Network Time Protocol, além de fornecer a data/hora de fontes confiáveis, permite corrigir os atrasos na transmissão… A ideia do protocolo surgiu como ferramente de medição de performance (quanto tempo dura um roundtrip, ou seja, mandei um pacote e em quanto tempo ele chega?), bem como a obtenção de data e hora de fontes extremamente precisas e padronizadas.

A ideia não é nova. Durante as grandes guerras mundiais a necessidade do uso de relógios precisos é fator essencial para comunicações e até ações militares. Hoje em dia, em “tempo de paz”, a manutenção de horários precisos é essencial não apenas para o comércio, mas também para sistemas de posicionamento globais (GPS)… Não é incomum que as fontes de tempo sejam baseadas em “relógios atômicos”, por exemplo.

O protocolo, diferente de outros mais comuns como HTTP, não é do tipo stream ou baseado em caracter. É um padrão binário, onde o requisitante envia um pacote e recebe um pacote. Ainda, tudo trafega via UDP, ou seja, não exige a manutençao de uma conexão TCP — o que torna a transação bem rápida, em teoria… Basta enviar 48 bytes para o ntp server e ler 48 bytes que ele te enviar.

Isso pode parecer pior que enviar uma string de 25 bytes (no caso do Oracle) e obter uns 20 bytes de volta (se o retorno for do tipo “DD/MM/YYYY HH:MM:SS”), mas note, abaixo, que NTP leva em conta o atraso na rede e ainda temos a questão da confiabilidade da fonte…

Abaixo temos o código-fonte de um pequeno cliente NTP, escrito em C:

// ntp4.c
//
// Compilar com:
//  cc -O2 -o ntp4 ntp4.c
//

// _GNU_SOURCE necessário para usar a função basename()
// definida pela GNU, não a POSIX.
#define _GNU_SOURCE
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>

// Códigos ANSI CSI para "colorir" a saída...
#define RED    "\033[1;31m"
#define YELLOW "\033[1;33m"
#define DFL    "\033[0m"

// NTP usa "1º de janeiro de 1900, 0:00:00" como base de
// tempo. UNIX usa "1º de janeiro de 1970, 0:00:00".
// NTP_TIMESTEMP_DELTA é a diferença, em segundos.
// O macro NTP2UNIX_TIMESTAMP faz o que o nome diz.
#define NTP_TIMESTAMP_DELTA 2208988800U
#define NTP2UNIX_TIMESTAMP( x ) \
  ( time_t ) ( ( x ) - NTP_TIMESTAMP_DELTA )

// O protocolo NTP v3 usa essa estrutura.
// RFC-1305: https://tools.ietf.org/html/rfc1305
struct ntp_packet_s
{
  // +-+-+-+-+-+-+-+-+
  // |0|0|0|1|1|0|1|1|
  // +-+-+-+-+-+-+-+-+
  //  --- ----- -----
  //   |    |     |
  //   |    |     +------ Modo (3 para cliente)
  //   |    +------------ Versão (3)
  //   +----------------- Indicador de "leap second"
  //                      (do último minuto) (0)
  uint8_t li_vn_mode;

  uint8_t stratum;
  uint8_t poll;
  uint8_t precision;

  uint32_t rootDelay;
  uint32_t rootDispersion;

  uint32_t refId;
  uint32_t refTm_s;
  uint32_t refTm_f;
  uint32_t origTm_s;
  uint32_t origTm_f;

  uint32_t rxTm_s;
  uint32_t rxTm_f;
  // Os timestamps transmitidos pelo NTP estão nesses
  // dois valores.
  uint32_t txTm_s;
  uint32_t txTm_f;
};

// Usado como tempo de timeout para escrita/leitura.
// Em segundos.
#define TIMEOUT 3

// Nosso sinal usa essa variável para determinar
// para quem estamos atendendo o sinal SIGALRM.
static int sigop;         // 0 = write, 1 = read

// Protótipo local do tratador de SIGALRM.
static void timeout_handler ( int );

int main ( int argc, char *argv[] )
{
  // Restringi para IPv4.
  struct addrinfo ai_hint = { .ai_family = AF_INET };
  struct ntp_packet_s ntppkt = { .li_vn_mode = 033 };
  struct sigaction sa = {};

  struct addrinfo *resai;
  struct servent *se;
  struct sockaddr_in *sinp;
  time_t t;
  int fd;

  // Se o segundo argumento não foi informado, erro.
  if ( ! *++argv )
  {
    fprintf ( stderr, 
              YELLOW "Usage" DFL ": %s <\"addr\">\n", 
              basename( *(argv - 1) ) );
    return EXIT_FAILURE;
  }

  // Resolve o "nome"...
  if ( getaddrinfo ( *argv, NULL, &ai_hint, &resai ) )
  {
    perror ( "getaddrinfo" );
    return EXIT_FAILURE;
  }

  if ( ! resai )
  {
    fprintf ( stderr, 
              RED "ERROR" DFL ": Cannot resolve '%s'.\n", 
              *argv );
    return EXIT_SUCCESS;
  }

  // Cria o file descriptor.
  fd = socket ( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
  if ( fd == -1 )
  {
    perror ( "socket" );
    freeaddrinfo ( resai );
    return EXIT_FAILURE;
  }

  // Pegamos a porta do serviço NTP de /etc/services.
  sinp = ( ( struct sockaddr_in * )resai->ai_addr;
  if ( se = getservbyname ( "ntp", NULL ) )
    sinp->sin_port = se->s_port;
  else
    // Se não achou, assume porta 123.
    sinp->sin_port = htons ( 123 );

  // read()/write() exigem que o file descriptor esteja
  // "conectado". Poderíamos usar sendto() e recvfrom()
  // e evitar isso... Para UDP connect() não "conecta",
  // mas apenas ajusta o descritor.
  if ( connect ( fd, 
                 resai->ai_addr, 
                 resai->ai_addrlen ) == -1 )
  {
    perror ( "connect" );
    freeaddrinfo ( resai );
    goto fail;
  }

  // Não preciso mais da lista
  // devolvida por getaddrinfo().
  freeaddrinfo ( resai );

  // Ajusta o tratador de sinal SIGALRM.
  sigfillset ( & ( sa.sa_mask ) );
  sa.sa_handler = timeout_handler;
  sigaction ( SIGALRM, &sa, NULL );

  // Tenta enviar o pacote...
  sigop = 0;
  alarm ( TIMEOUT );
  if ( write ( fd, &ntppkt, sizeof ntppkt ) == -1 )
  {
    perror ( "write" );
    goto fail;
  }
  alarm ( 0 );

  // Tenta obter uma resposta.
  sigop = 1;
  alarm ( TIMEOUT );
  if ( read ( fd, &ntppkt, sizeof ntppkt ) == -1 )
  {
    perror ( "write" );
    goto fail;
  }
  alarm ( 0 );

  // Calcula e mostra a data/hora.
  t = NTP2UNIX_TIMESTAMP( ntohl ( ntppkt.txTm_s ) );
  printf ( "%s\n", ctime ( &t ) );

  // SUCESSO!
  close ( fd );
  return EXIT_SUCCESS;

fail:
  // Não precisamos chamar alarm(0) aqui!
  close ( fd );
  return EXIT_FAILURE;
}

// Tratador de SIGALRM
void timeout_handler ( int signum )
{
  static const char *const msg[] =
  { RED "TIMEOUT" DFL ": ", 
    "sending request\n", 
    "receiving respospnse\n" };

  // Uso write() aqui porque printf() não é "AS-Safe".
  write ( STDERR_FILENO, msg[0], strlen ( msg[0] ) );
  write ( STDERR_FILENO, 
          msg[1 + sigop], 
          strlen ( msg[1 + sigop] ) );
  exit ( EXIT_FAILURE );
}

Nesse programinha você pode usar qualquer ntp server que esteja disponível para a sua rede. Por exemplo:

$ cc -O2 -o ntp4 ntp4.c
$ ./ntp4 a.ntp.br
Fri Jun 15 14:27:55 2018

Aqui, a.ntp.br é um dos ntp servers de estrato 2, em conformidade com o “horário legal” brasileiro. Eis a estrutura, como descrita em ntp.br (aqui):

Segundo a documentação e orientações de ntp.br, todos os servers do estrato 1 também são acessíveis publicamente… Qual a vantagem de usar ntp.br? A data e hora são as chamadas “horário legal” brasileiro… Isso levanta a questão de que para obter a data/hora “corretas” teríamos que realizar uma consulta na Internet… Não necessariamente.

Instalando e configurando seu próprio NTP server no Linux:

Com seu próprio ntp server você pode apontar todos os seus hosts para sincronização de horário por ele. Deixe-o ir até o ntp.br por você…

Nada mais simples:

  • Instale o pacote ntp:
$ sudo apt-get install ntp
  • Modifique /etc/ntp.conf:
# Retire as linhas contendo 'pool' e 'server' e as substitua por:
server a.st1.ntp.br iburst
server b.st1.ntp.br iburst
server c.st1.ntp.br iburst
server d.st1.ntp.br iburst
server gps.ntp.br iburst
server a.ntp.br iburst
server b.ntp.br iburst
server c.ntp.br iburst
  • Reinicie o serviço ntp.service:
$ sudo systemctl restart ntp.service

Pronto… Basta apenas liberar a porta 123 e, se quiser, registrar um nome para seu ntp server. Existem algumas configurações extras que você pode fazer para tornar seu servidor mais seguro, mas, essencialmente, isso é tudo.

Mifu…

Sabem aqueles grupos onde o pessoal costuma fazer perguntas de “problemas” de cursos de graduação de computação? Do tipo “faça um programa que some 5 números…”? Well… Eu acho isso enfadonho! Mas, recentemente, topei com um que, onde “me fudi” com uma resposta irônica de minha parte (e peço desculpas, porque a solução é mesmo interessante para o aprendiz!)…

O problema era esse ai mesmo que citei acima:

“Faça um programa que leia 5 valores inteiros e apresente a soma desses valores usando apenas uma variável

A parte em negrito é a que torna o problema interessante… Normalmente o estudante usa uma variável para ler os valores e outra para acumulá-los:

/* test1.c */
#include <stdio.h>

void main ( void )
{
  int sum, n;

  scanf ( "%d", &sum );
  scanf ( "%d", &n ); sum += n;
  scanf ( "%d", &n ); sum += n;
  scanf ( "%d", &n ); sum += n;
  scanf ( "%d", &n ); sum += n;}

  printf( "%d\n", sum );
}

Claro, isso usa duas variáveis, o que invalida a solução. Poderíamos usar um array:

/* test2.c */
#include <stdio.h>

void main ( void )
{
  int n[2];

  scanf ( "%d", &n[0] );
  scanf ( "%d", &n[1] ); n[0] += n[1];
  scanf ( "%d", &n[1] ); n[0] += n[1];
  scanf ( "%d", &n[1] ); n[0] += n[1];
  scanf ( "%d", &n[1] ); n[0] += n[1];

  printf( "%d\n", n[0] );
}

O programa acima é, essencialmente, a mesma coisa que o anterior e o estudante pode pensar que está usando apenas uma “variável”. No entanto, um arry é uma “sequência” de variáveis (a tradução direta da palavra “array” significa “sequência”), ou seja, está usando duas variáveis. Isso invalida a parte “usar apenas uma variável” do problema. Eis a solução engenhosa:

/* test3.c */
#include <stdio.h>

void main ( void )
{
  long long n;  // n tem 64 bits de tamanho!

  scanf ( "%d", ( int * )&n );

  scanf ( "%d", ( int *)&n + 1 ); 
  *( int * )&n += *( int * )(&n + 1);

  scanf ( "%d", ( int *)&n + 1 ); 
  *( int * )&n += *( int * )(&n + 1);

  scanf ( "%d", ( int *)&n + 1 ); 
  *( int * )&n += *( int * )(&n + 1);

  scanf ( "%d", ( int *)&n + 1 ); 
  *( int * )&n += *( int * )(&n + 1);

  printf( "%d\n", *( int * )&n );
}

Ao fazer o casting ( int * ) usamos a aritmética de ponteiros envolvendo o tipo int, que tem 32 bits de tamanho. Assim, ( int * )&n + 1 é uma expressão que é resolvida para os 32 bits superiores de n, enquanto ( int * )&n aponta para os 32 bits inferiores. Ao derreferenciar esse ponteiro, acessamos o seu valor, na pilha.

Em essência, estamos mexendo com um array, mas usando apenas uma variável!!! Isso atende a ambas os requisitos do problema, mas causa um problema de performance… Quando declaramos uma variável local qualquer o compilador, se tiver sido chamado com opções de otimização, tenta usar os registradores da CPU ao invés da pilha. Ao usar o operador & (endereço-de) forçamos o compilador a manter a variável na pilha. Como os scanf precisam do endereço da variável lida, os três programas, acima, geral praticamente o mesmíssimo código… Assim, cada chamda a scanf e cada acumulação usará um ou mais acessos à memória que, mesmo estando no cache L1d, poderá adicionar 1 ciclo de clock adicional às instruções (resolução do endereço efetivo).

A rotina abaixo, mais tradicional, tem o potencial de ser bem mais rápida e gastar menos espaço no cache L1i:

/* test4.c 
   Compile com: 
     gcc -O2 -fno-stack-protector -w -o test4 test4.c 
*/
#include <stdio.h>

void main ( void )
{
  int sum, n, count;

  count = 5;
  sum = 0;
  while (count--)
  {
    scanf ( "%d", &n );
    sum += n;
  }

  printf( "%d\n", sum );
}

Posso mostrar que count e sum não são mantidos na memória, mas em registradores. Apenas n não é otimizado dessa forma por causa do operador &:

; test4.asm - equivale a test4.c.
bits 64
section .rodata
scanf_fmt:  db `%d`
printf_fmt: db `%d\n`

section .text
global main
main:
  push rbp
  push rbx

  xor  ebp,ebp  ; sum=0
  mov  ebx,5    ; count=5

  sub  rsp,24   ; reserva espaço na pilha, alinhando-a

.loop:
  lea  rsi,[rsp+12]  ; n está alocado na pilha em rsp+12.
  xor  eax,eax       ; scanf() precisa disso (porquê?)
                     ; EAX, aqui, NÃO é stdin!
  mov  edi,scanf_fmt
  call __isoc99_scanf

  add  ebp,[rsp+12]  ; sum += n;
  sub  ebx,1         ; --count;
  jne  .loop         ; if (count != 0) goto loop.

  mov  edx,ebp
  mov  esi,printf_fmt
  mov  edi,1         ; stdout
  xor  eax,eax       ; printf() precisa disso (porquê?)
  call __printf_chk

  add  rsp,24   ; dealoca espaço previamente alocado na pilha.

  pop  rbx
  pop  rbp
  ret

Se não fosse pela necessidade do scanf, n também seria mantido em um registrador (R12D, por exemplo). Mas, note que temos apenas UM acesso à memória por iteração do loop (em add ebp,[rsp+12], mas, é claro, scanf também acessa memória!). No caso de test3.c temos 9 indireções, além das causadas pelo scanf, no caso de test4.c, 5.

Ahhh… lea rsi,[rsp+12] não é uma indireção, mas somente o cálculo do endereço efetivo…

Embora o exercício seja interessante, não passa de curiosidade acadêmica…