
Eu tenho um arquivo de 10 MB preenchido por bytes nulos. Um programa está acessando-o e altera zeros para strings específicas até o final do arquivo.
Já tentei usar o tail -F | grep wanted_text | grep -v "unwanted_text"
, mas ele não monitora alterações. Funciona apenas para arquivos de texto normais, mas não para arquivos preenchidos com zeros.
Todos os bytes nulos são substituídos por linhas separadas por um caractere de nova linha até o final do arquivo. Depois que o arquivo for preenchido, ele será renomeado e um novo será criado.
Então, como eu poderia monitorar alterações de um arquivo preenchido por bytes nulos com capacidade de filtrar a saída?
Responder1
Este é o script para o Reader, que deve estar próximo do que você precisa para falsificar um comando tail para um arquivo preenchido com NUL. Ele verifica alterações no arquivo (comparando toda a saída ls -l, que inclui um carimbo de data/hora até nanossegundos) e relata quaisquer adições em um lote. Ele não informa linhas que já estão no arquivo quando ele é inicializado, apenas adições enquanto ele está em execução.
Ele funciona em duas velocidades para evitar verificações desnecessárias. Se detectar alguma adição, ele tenta novamente após 1,0 segundo. Se um ciclo não vê adições, ele tenta novamente após 5 segundos (esse 5 é um argumento para o processo).
#! /bin/bash
#: Reader: tail -f a file which is pre-formatted with many trailing NUL characters.
#### Implement the User Requirement.
function Reader {
local RUN="${1:-60}" SLEEP="${2:-5}" FILE="${3:-/dev/null}"
local AWK='''
BEGIN { NUL = "\000"; }
function Tick (Local, cmd, ts) {
cmd = "date \047+%s\047";
cmd | getline ts; close (cmd); return (ts);
}
function TS (Local, cmd, ts) {
cmd = "date \047+%H:%M:%S.%N\047";
cmd | getline ts; close (cmd); return (ts);
}
function Wait (secs) {
system (sprintf ("sleep %s", secs));
}
function isChange (Local, cmd, tx) {
cmd = sprintf ("ls 2>&1 -l --full-time \047%s\047", Fn);
cmd | getline tx; close (cmd);
if (tsFile == tx) return (0);
tsFile = tx;
if (index (tx, "\047")) {
if (fSt != "B") { fSt = "B"; printf ("%s: No file: %s\n", TS( ), Fn); }
} else {
if (fSt != "G") { fSt = "G"; printf ("%s: Reading: %s\n", TS( ), Fn); }
}
return (1);
}
function atNul (buf, Local, j) {
j = index (buf, NUL);
return ((j > 0) ? j : 1 + length (buf));
}
function List (tx, Local, ts, X, j) {
sub ("\012$", "", tx); split (tx, X, "\012");
ts = TS( );
for (j = 1; j in X; ++j) {
printf ("%s %3d :%s:\n", ts, length (X[j]), X[j]);
}
}
function Monitor (Local, rs, tk, Buf, Now, End) {
printf ("%s: READER Begins\n", TS( ));
tk = Tick( ); Expired = tk + Run;
Now = -1;
while (Tick( ) <= Expired) {
if (! isChange( )) { Wait( Sleep); continue; }
rs = RS; RS = "\000";
Buf = ""; getline Buf < Fn; close (Fn);
RS = rs;
if (Now < 0) Now = atNul( Buf);
End = atNul( Buf);
List( substr (Buf, Now, End - Now));
Now = End;
Wait( 1.0);
}
printf ("%s: READER Exits\n", TS( ));
}
NR == 1 { Run = $0; next; }
NR == 2 { Sleep = $0; next; }
NR == 3 { Fn = $0; }
END { Monitor( Fn); }
'''
{
echo "${RUN}";
echo "${SLEEP}";
echo "${FILE}";
} | awk -f <( echo "${AWK}" )
}
#### Script Body Starts Here.
Reader 40 5 "./myNullFile"
Responder2
Problemas com todo o conceito.
O gravador apenas substitui bytes NUL por outras strings ou pode escrever novas strings sobre strings antigas, possivelmente com sobreposições incompletas? As strings sempre terão pelo menos um separador NUL entre elas?
Ele também pode escrever sobre strings com NULs novos para apagar partes do arquivo?
O arquivo original tem realmente 10 MB de NUL ou é inicialmente um arquivo esparso?
Dado que só podemos encontrar strings lendo o arquivo inteiro, com que frequência você está preparado para fazer isso?
Existe alguma maneira de bloquear o arquivo enquanto ele está sendo gravado, para evitar condições de corrida?
O tamanho do arquivo pode mudar durante toda a operação?
awk (pelo menos, GNU/awk) pode lidar com caracteres NUL e linhas longas. Ele poderia manter uma lista de intervalos NUL (inicialmente apenas [0,10485760]) e verificar se há nova fragmentação nessas regiões. Isso não detectaria sobregravações. Mas seria capaz de relatar todas as adições sem quaisquer processos extras.
GNU/awk possui uma função patsplit() integrada, que corta uma string de acordo com um separador RE, criando um array de campos e um array de separadores. Portanto, o RE /[\000]+/ deve colocar todas as strings em uma matriz, e todas as repetições NUL na outra matriz, e você pode length() todas elas cumulativamente para encontrar o deslocamento total no arquivo para cada string. Parece um excelente candidato para investigação.
A propósito, o comando cat exibe caracteres NUL. Você pode vê-los em um arquivo usando o comando od. A razão pela qual eles não aparecem em um terminal é que o driver do terminal os ignora.
Como sugere Romeo, manter um cksum do arquivo anterior informaria se ele havia mudado, mas não onde. Portanto, pode ser uma otimização útil, dependendo da frequência das atualizações.
Responder3
Já fiz o suficiente para verificar se meu conceito de usar GNU/awk com patsplit() é viável. Configurar um Writer falso levou cerca de 70% do tempo de desenvolvimento. Encontrei um conjunto de opções dd que me permite configurar um arquivo de 10 MB e, em seguida, escrever strings em locais aleatórios periodicamente.
Eu tenho um Reader que arrasta tudo para a memória como uma longa string e separa os nulos em um array e as strings em outro. Leva 0,044 segundos para ler os 10 MB, 0,989 para dividir a string nas matrizes e 0,138 segundos para relatar o início, comprimento e conteúdo das 20 strings que coloquei. Então, cerca de 1,2 segundos para fazer um instantâneo do arquivo.
Todos os tempos foram feitos no meu laptop barato de 8 anos. Eu acho que, como ele precisa analisar todos os 10 MB de qualquer maneira, ter muito mais strings não afetará tanto o desempenho. O próximo passo é confirmar isso.
Acredito que manter uma tabela hash antiga e nova das strings e encontrar as alterações será simples e eficiente.
Há mais informações sobre a adição de strings aos dados aqui? Se fosse sempre contíguo aos dados anteriores, seria fácil emular tail olhando logo após a string anterior. Se não fosse frequente, poderíamos verificar o carimbo de data/hora antes de ler o arquivo. Se estivesse escrevendo um índice na primeira parte do arquivo, poderíamos verificar isso primeiro. Todo o conceito deste arquivo torna difícil ver qual é a sua utilidade para o resto do sistema - é apenas uma forma hostil de usar o armazenamento.
Esta questão ainda é de interesse? Não vejo nenhuma resposta do OP às minhas perguntas anteriores, mas parece-me que as sobreposições de strings e assim por diante serão exibidas apenas como atualizações e alterações de comprimento.
Responder4
Este é o roteiro do Writer. Ele usa o comando dd para criar o arquivo inicial de uma só vez (se não existir) e, em seguida, usa dd para inserir linhas aleatórias de um arquivo de script no arquivo. Costumava fazer isso em posições aleatórias, mas agora coloca cada uma após a anterior. Ele adiciona linhas em intervalos aleatórios calculados em torno de um determinado argumento (2 segundos nesta versão). Ele sai após um limite de tempo específico ou se o arquivo estiver cheio.
#! /bin/bash
#.. Declare the shared file.
FILE="./myNullFile"
SIZE=$(( 10 * 1024 * 1024 ))
#.. Using script as source of the strings.
TEXT="./isNulls"
WORK="./myLine"
#### Simulate the file writer defined in the question.
function Writer {
local RUN="${1:-60}" SLEEP="${2:-5}"
local AWK='''
BEGIN { printf ("%s: WRITER Begins\n", TS( )); }
function Begin (Local) {
Pos = getNull( );
printf ("Initial NUL is at %d\n", Pos);
if (Pos < 0) exit;
}
function TS (Local, cmd, ts) {
cmd = "date \047+%H:%M:%S.%N\047";
cmd | getline ts; close (cmd); return (ts);
}
function Wait (secs) {
system (sprintf ("sleep %s", secs));
}
function getNull (Local, rs, Buf) {
rs = RS; RS = "^$";
getline Buf < Fn; close (Fn);
RS = rs;
return (-1 + index (Buf, "\000"));
}
function Str (Local, Row, Lth) {
Row = int (nTx * rand());
Lth = length (Tx[Row]);
if (Pos + Lth >= Sz) {
printf ("%s is full: Pos %d, Lth %d, Sz %d\n", Fn, Pos, Lth, Sz);
exit;
}
printf ("%s: Write Pos %10d Lth %3d Txt :%s:\n",
TS( ), Pos, 1 + Lth, Tx[Row]);
print Tx[Row] "\n" > Wk; close (Wk);
system (sprintf (Fmt, Pos, 1 + Lth, Wk, Fn, Wk));
Pos += 1 + Lth;
}
NR == 1 { Fmt = $0; srand (); next; }
NR == 2 { Fn = $0; next; }
NR == 3 { Sz = $0; next; }
NR == 4 { Wk = $0; Begin( ); next; }
NF { sub (/^[ \011]+/, ""); Tx[nTx++] = $0; next; }
{ Str( ); }
END { printf ("%s: WRITER Exits\n", TS( )); }
'''
local EXPIRED=$(( SECONDS + RUN ))
local AWK_WT='BEGIN { srand(); } { print 0.1 + 2 * $1 * rand(); }'
{
DD_OPT='status=none conv=notrunc bs=1 seek="%s" count="%s"'
DD_FNS='if="%s" of="%s" && rm -f "%s"'
echo "dd ${DD_OPT} ${DD_FNS}"
echo "${FILE}"; echo "${SIZE}"; echo "${WORK}"
awk NF "${TEXT}"
while (( SECONDS <= EXPIRED )); do
sleep "$( echo "${SLEEP}" | awk "${AWK_WT}" )"
echo ''
done
} | awk -f <( echo "${AWK}" )
}
#### Script Body Starts Here.
[[ -r "${FILE}" ]] || {
dd count=1 bs="${SIZE}" if="/dev/zero" of="${FILE}"
od -A d -t x1a "${FILE}"
}
Writer 32 2