
Tengo un archivo de 10Mb lleno de bytes nulos. Un programa accede a él y cambia ceros a cadenas específicas hasta el final del archivo.
Intenté usarlo tail -F | grep wanted_text | grep -v "unwanted_text"
, pero no monitorea los cambios. Sólo funciona con archivos de texto habituales, pero no con archivos llenos de ceros.
Todos los bytes nulos se reemplazan por líneas separadas por un carácter de nueva línea hasta el final del archivo. Una vez que se completa el archivo, se le cambia el nombre y en su lugar se crea uno nuevo.
Entonces, ¿cómo podría monitorear los cambios de un archivo lleno de bytes nulos con capacidad para filtrar la salida?
Respuesta1
Este es el script para Reader, que debería estar cerca de lo que necesita para falsificar un comando de cola para un archivo lleno de NUL. Comprueba si hay cambios en el archivo (comparando toda la salida de ls -l, que incluye una marca de tiempo de hasta nanosegundos) e informa cualquier adición en un lote. No informa líneas que ya están en el archivo cuando se inicia, solo adiciones mientras se está ejecutando.
Funciona a dos velocidades para evitar cheques desperdiciados. Si detecta alguna adición, vuelve a intentarlo después de 1,0 segundos. Si un ciclo no ve adiciones, lo intenta nuevamente después de 5 segundos (este 5 es un argumento para el proceso).
#! /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"
Respuesta2
Problemas con todo el concepto.
¿El escritor solo reemplaza los bytes NUL con otras cadenas, o puede escribir cadenas nuevas sobre cadenas antiguas, posiblemente con superposiciones incompletas? ¿Las cadenas siempre tendrán al menos un separador NUL entre ellas?
¿Puede escribir sobre cadenas con NUL nuevos para borrar partes del archivo también?
¿El archivo original tiene realmente 10 MB de NUL o es inicialmente un archivo disperso?
Dado que sólo podemos encontrar cadenas leyendo el archivo completo, ¿con qué frecuencia estás preparado para hacer esto?
¿Hay alguna forma de bloquear el archivo mientras se escribe para evitar condiciones de carrera?
¿Puede cambiar el tamaño del archivo durante toda la operación?
awk (al menos, GNU/awk) puede manejar caracteres NUL y líneas largas. Podría mantener una lista de rangos que eran NUL (inicialmente solo [0,10485760]) y verificar si hay nueva fragmentación en esas regiones. Sin embargo, eso no detectaría sobrescrituras. Pero podría informar todas las adiciones sin ningún proceso adicional.
GNU/awk tiene una función patsplit() incorporada, que corta una cadena de acuerdo con un separador RE, creando una matriz de campos y una matriz de separadores. Entonces, RE /[\000]+/ debe colocar todas las cadenas en una matriz, y todas las repeticiones NUL en la otra matriz, y puede longitud() todas ellas de forma acumulativa para encontrar el desplazamiento total en el archivo para cada cadena. Parece un excelente candidato para la investigación.
Por cierto, el comando cat muestra caracteres NUL. Puedes verlos en un archivo usando el comando od. La razón por la que no aparecen en una terminal es que el controlador de la terminal los ignora.
Como sugiere Romeo, mantener un cksum del archivo anterior le indicaría si había cambiado, pero no dónde. Por lo tanto, podría ser una optimización útil, dependiendo de la frecuencia de las actualizaciones.
Respuesta3
He hecho lo suficiente para verificar que mi concepto de usar GNU/awk con patsplit() es factible. Configurar un Writer falso tomó alrededor del 70% del tiempo de desarrollo. Encontré un conjunto de opciones de dd que me permiten configurar un archivo de 10 MB y luego escribir cadenas en lugares aleatorios periódicamente.
Tengo un lector que arrastra todo a la memoria como una cadena larga y separa los valores nulos en una matriz y las cadenas en otra. Se necesitan 0,044 segundos para leer los 10 MB, 0,989 para dividir la cadena en matrices y 0,138 segundos para informar el inicio, la longitud y el contenido de las 20 cadenas que coloqué. Aproximadamente 1,2 segundos para hacer una instantánea del archivo.
Todos los tiempos se realizaron en mi computadora portátil barata de 8 años. Creo que, como de todos modos tiene que analizar los 10 MB completos, tener muchas más cadenas no afectará tanto el rendimiento. El siguiente paso es confirmarlo.
Creo que mantener una tabla hash nueva y antigua de las cadenas y encontrar los cambios será simple y eficiente.
¿Se sabe algo más sobre la adición de cadenas a los datos aquí? Si siempre fuera contiguo a los datos anteriores, sería fácil emular la cola mirando justo después de la cadena anterior. Si fuera poco frecuente, podríamos verificar la marca de tiempo antes de leer el archivo. Si estuviera escribiendo un índice en la primera parte del archivo, entonces podríamos verificar esto primero. De todos modos, todo el concepto de este archivo hace que sea difícil ver qué utilidad tiene para el resto del sistema: es simplemente una forma hostil de usar el almacenamiento.
¿Sigue siendo interesante esta pregunta? No veo ninguna respuesta del OP a mis preguntas anteriores, pero me parece que las cadenas se superponen, etc., solo se mostrarán como actualizaciones y cambios de longitud.
Respuesta4
Este es el guión de Writer. Utiliza el comando dd para crear el archivo inicial de una sola vez (si no existe) y luego usa dd para introducir líneas aleatorias de un archivo de script en el archivo. Solía hacer esto en posiciones aleatorias, pero ahora coloca cada una después de la anterior. Agrega líneas a intervalos aleatorios promediados en torno a un argumento determinado (2 segundos en esta versión). Sale después de un límite de tiempo específico o si el archivo está lleno.
#! /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