Programação C

Leia Syscall Linux

Leia Syscall Linux
Então você precisa ler dados binários? Você pode querer ler de um FIFO ou socket? Você vê, você pode usar a função de biblioteca padrão C, mas ao fazer isso, você não se beneficiará dos recursos especiais fornecidos pelo Linux Kernel e POSIX. Por exemplo, você pode querer usar o tempo limite para ler em um determinado momento, sem recorrer à sondagem. Além disso, você pode precisar ler algo sem se importar se é um arquivo especial ou soquete ou qualquer outra coisa. Sua única tarefa é ler alguns conteúdos binários e colocá-los em seu aplicativo. É aí que o syscall de leitura brilha.

Leia um arquivo normal com um syscall Linux

A melhor maneira de começar a trabalhar com esta função é lendo um arquivo normal. Esta é a maneira mais simples de usar a escala de syscall, e por uma razão: ela não tem tantas restrições quanto outros tipos de fluxo ou cano. Se você pensar nisso, isso é lógico, quando você lê a saída de outro aplicativo, você precisa ter alguma saída pronta antes de lê-la e, portanto, você precisará esperar que este aplicativo grave esta saída.

Em primeiro lugar, uma diferença fundamental com a biblioteca padrão: não há nenhum buffer. Cada vez que você chamar a função de leitura, você chamará o kernel do Linux, e isso vai levar algum tempo - é quase instantâneo se você ligar uma vez, mas pode atrasá-lo se ligar milhares de vezes em um segundo. Em comparação, a biblioteca padrão irá armazenar a entrada para você. Então, sempre que você chamar read, você deve ler mais do que alguns bytes, mas sim um grande buffer como alguns kilobytes - exceto se você realmente precisar de poucos bytes, por exemplo, se você verificar se um arquivo existe e não está vazio.

No entanto, isso tem um benefício: cada vez que você chamar read, tem certeza de que obterá os dados atualizados, se algum outro aplicativo modificar o arquivo atualmente. Isto é especialmente útil para arquivos especiais como aqueles em / proc ou / sys.

É hora de mostrar um exemplo real. Este programa C verifica se o arquivo é PNG ou não. Para fazer isso, ele lê o arquivo especificado no caminho fornecido no argumento da linha de comando e verifica se os primeiros 8 bytes correspondem a um cabeçalho PNG.

Aqui está o código:

#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
 
typedef enum
IS_PNG,
MUITO CURTO,
INVALID_HEADER
pngStatus_t;
 
unsigned int isSyscallSuccessful (const ssize_t readStatus)
retornar readStatus> = 0;
 

 
/ *
* checkPngHeader está verificando se a matriz pngFileHeader corresponde a um PNG
* cabeçalho do arquivo.
*
* Atualmente, ele verifica apenas os primeiros 8 bytes da matriz. Se a matriz for menor
* do que 8 bytes, TOO_SHORT é retornado.
*
* pngFileHeaderLength deve manter o comprimento da matriz tye. Qualquer valor inválido
* pode levar a um comportamento indefinido, como travamento do aplicativo.
*
* Retorna IS_PNG se corresponder a um cabeçalho de arquivo PNG. Se houver pelo menos
* 8 bytes na matriz, mas não é um cabeçalho PNG, INVALID_HEADER é retornado.
*
* /
pngStatus_t checkPngHeader (const unsigned char * const pngFileHeader,
size_t pngFileHeaderLength) const unsigned charpectedPngHeader [8] =
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A;
int i = 0;
 
if (pngFileHeaderLength < sizeof(expectedPngHeader))
return TOO_SHORT;
 

 
para (i = 0; i < sizeof(expectedPngHeader); i++)
if (pngFileHeader [i] != expectPngHeader [i])
retornar INVALID_HEADER;
 


 
/ * Se chegar aqui, todos os primeiros 8 bytes estão em conformidade com um cabeçalho PNG. * /
return IS_PNG;

 
int main (int argumentLength, char * argumentList [])
char * pngFileName = NULL;
char não assinado pngFileHeader [8] = 0;
 
ssize_t readStatus = 0;
/ * Linux usa um número para identificar um arquivo aberto. * /
int pngFile = 0;
pngStatus_t pngCheckResult;
 
if (argumentLength != 2)
fputs ("Você deve chamar este programa usando isPng seu nome de arquivo.\ n ", stderr);
return EXIT_FAILURE;
 

 
pngFileName = argumentList [1];
pngFile = abrir (pngFileName, O_RDONLY);
 
if (pngFile == -1)
perror ("Falha ao abrir o arquivo fornecido");
return EXIT_FAILURE;
 

 
/ * Leia alguns bytes para identificar se o arquivo é PNG. * /
readStatus = ler (pngFile, pngFileHeader, sizeof (pngFileHeader));
 
if (isSyscallSuccessful (readStatus))
/ * Verifique se o arquivo é um PNG, uma vez que obteve os dados. * /
pngCheckResult = checkPngHeader (pngFileHeader, readStatus);
 
if (pngCheckResult == TOO_SHORT)
printf ("O arquivo% s não é um arquivo PNG: é muito curto.\ n ", pngFileName);
 
else if (pngCheckResult == IS_PNG)
printf ("O arquivo% s é um arquivo PNG!\ n ", pngFileName);
 
senão
printf ("O arquivo% s não está no formato PNG.\ n ", pngFileName);
 

 
senão
perror ("Falha ao ler o arquivo");
return EXIT_FAILURE;
 

 
/ * Fechar o arquivo… * /
if (close (pngFile) == -1)
perror ("Falha ao fechar o arquivo fornecido");
return EXIT_FAILURE;
 

 
pngFile = 0;
 
return EXIT_SUCCESS;
 

Veja, é um exemplo completo, funcional e compilável. Não hesite em compilá-lo e testá-lo, ele realmente funciona. Você deve chamar o programa de um terminal como este:

./ isPng seu nome de arquivo

Agora, vamos nos concentrar na própria chamada de leitura:

pngFile = abrir (pngFileName, O_RDONLY);
if (pngFile == -1)
perror ("Falha ao abrir o arquivo fornecido");
return EXIT_FAILURE;

/ * Leia alguns bytes para identificar se o arquivo é PNG. * /
readStatus = ler (pngFile, pngFileHeader, sizeof (pngFileHeader));

A assinatura de leitura é a seguinte (extraída das páginas de manual do Linux):

ssize_t read (int fd, void * buf, size_t count);

Primeiro, o argumento fd representa o descritor de arquivo. Eu expliquei um pouco esse conceito no meu artigo sobre fork.  Um descritor de arquivo é um int que representa um arquivo aberto, socket, pipe, FIFO, dispositivo, bem, é uma série de coisas onde os dados podem ser lidos ou gravados, geralmente de forma semelhante a um fluxo. Vou me aprofundar mais nisso em um artigo futuro.

A função de abertura é uma das maneiras de dizer ao Linux: Eu quero fazer coisas com o arquivo nesse caminho, por favor, encontre-o onde está e me dê acesso a ele. Ele lhe devolverá este int chamado descritor de arquivo e agora, se você quiser fazer algo com este arquivo, use aquele número. Não se esqueça de chamar close quando terminar de usar o arquivo, como no exemplo.

Então você precisa fornecer este número especial para ler. Então há o argumento buf. Você deve fornecer aqui um ponteiro para o array onde read irá armazenar seus dados. Finalmente, a contagem é quantos bytes ele irá ler no máximo.

O valor de retorno é do tipo ssize_t. Tipo estranho não é? Significa “tamanho_t assinado”, basicamente é um inteiro longo. Ele retorna o número de bytes que lê com sucesso, ou -1 se houver um problema. Você pode encontrar a causa exata do problema na variável global errno criada pelo Linux, definida em . Mas para imprimir uma mensagem de erro, usar perror é melhor, pois imprime errno em seu nome.

Em arquivos normais - e neste caso - ler retornará menos do que contar apenas se você tiver alcançado o final do arquivo. A matriz buf que você fornece devo ser grande o suficiente para caber pelo menos bytes de contagem, ou seu programa pode travar ou criar um bug de segurança.

Agora, ler não é útil apenas para arquivos normais e se você quiser sentir seus superpoderes - Sim, eu sei que não está em nenhum quadrinho da Marvel, mas tem poderes verdadeiros - você vai querer usá-lo com outros fluxos, como tubos ou tomadas. Vamos dar uma olhada nisso:

Arquivos especiais do Linux e leitura de chamada de sistema

O fato de ler funciona com uma variedade de arquivos como pipes, sockets, FIFOs ou dispositivos especiais como um disco ou porta serial é o que o torna realmente mais poderoso. Com algumas adaptações, você pode fazer coisas realmente interessantes. Em primeiro lugar, isso significa que você pode literalmente escrever funções trabalhando em um arquivo e usá-lo com um tubo. É interessante passar dados sem nunca atingir o disco, garantindo melhor desempenho.

No entanto, isso também aciona regras especiais. Vamos pegar o exemplo de uma leitura de uma linha do terminal em comparação com um arquivo normal. Quando você chama a leitura em um arquivo normal, ele só precisa de alguns milissegundos para o Linux obter a quantidade de dados solicitada.

Mas quando se trata de terminal, é outra história: digamos que você peça um nome de usuário. O usuário está digitando no terminal seu nome de usuário e pressiona Enter. Agora você segue meu conselho acima e chama read com um grande buffer, como 256 bytes.

Se a leitura funcionasse como com os arquivos, esperaria que o usuário digitasse 256 caracteres antes de retornar! Seu usuário esperaria para sempre e, infelizmente, mataria seu aplicativo. Certamente não é o que você quer, e você teria um grande problema.

Ok, você poderia ler um byte de cada vez, mas esta solução alternativa é terrivelmente ineficiente, como eu disse a você acima. Deve funcionar melhor do que isso.

Mas os desenvolvedores Linux pensaram ler de forma diferente para evitar este problema:

  • Quando você lê arquivos normais, ele tenta o máximo possível ler a contagem de bytes e obterá bytes do disco ativamente se necessário.
  • Para todos os outros tipos de arquivo, ele retornará assim que há alguns dados disponíveis e no máximo bytes de contagem:
    1. Para terminais, é geralmente quando o usuário pressiona a tecla Enter.
    2. Para soquetes TCP, é assim que seu computador recebe algo, não importa a quantidade de bytes que recebe.
    3. Para FIFO ou canais, geralmente é a mesma quantidade que o outro aplicativo escreveu, mas o kernel do Linux pode entregar menos por vez, se for mais conveniente.

Assim, você pode chamar com segurança com seu buffer de 2 KiB sem ficar trancado para sempre. Observe que também pode ser interrompido se o aplicativo receber um sinal. Como a leitura de todas essas fontes pode levar segundos ou até horas - até que o outro lado decida escrever, afinal - ser interrompido por sinais permite parar de ficar bloqueado por muito tempo.

Isso também tem uma desvantagem: quando você quiser ler exatamente 2 KiB com esses arquivos especiais, você precisará verificar o valor de retorno de read e chamar read várias vezes. ler raramente preencherá todo o seu buffer. Se o seu aplicativo usa sinais, você também precisará verificar se a leitura falhou com -1 porque foi interrompida por um sinal, usando errno.

Deixe-me mostrar como pode ser interessante usar essa propriedade especial de ler:

#define _POSIX_C_SOURCE 1 / * sigaction não está disponível sem este #define. * /
#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
/ *
* isSignal diz se a syscall de leitura foi interrompida por um sinal.
*
* Retorna TRUE se a syscall de leitura foi interrompida por um sinal.
*
* Variáveis ​​globais: lê errno definido em errno.h
* /
unsigned int isSignal (const ssize_t readStatus)
return (readStatus == -1 && errno == EINTR);

unsigned int isSyscallSuccessful (const ssize_t readStatus)
retornar readStatus> = 0;

/ *
* shouldRestartRead diz quando a syscall de leitura foi interrompida por um
* sinalizar evento ou não, e dado o motivo do "erro" ser temporário, podemos
* reinicie com segurança a chamada de leitura.
*
* Atualmente, ele só verifica se a leitura foi interrompida por um sinal, mas
* poderia ser melhorado para verificar se o número alvo de bytes foi lido e se é
* não é o caso, retorne TRUE para ler novamente.
*
* /
unsigned int shouldRestartRead (const ssize_t readStatus)
return isSignal (readStatus);

/ *
* Precisamos de um manipulador vazio, pois a syscall de leitura será interrompida apenas se o
* sinal é tratado.
* /
void emptyHandler (int ignorado)
Retorna;

int main ()
/ * Está em segundos. * /
const int alarmInterval = 5;
const struct sigaction emptySigaction = emptyHandler;
char lineBuf [256] = 0;
ssize_t readStatus = 0;
não assinado int waitTime = 0;
/ * Não modifique a sigação exceto se você souber exatamente o que está fazendo. * /
sigaction (SIGALRM, & emptySigaction, NULL);
alarme (alarmInterval);
fputs ("Seu texto: \ n", stderr);
Faz
/ * Não se esqueça do '\ 0' * /
readStatus = ler (STDIN_FILENO, lineBuf, sizeof (lineBuf) - 1);
if (isSignal (readStatus))
waitTime + = alarmInterval;
alarme (alarmInterval);
fprintf (stderr, "% u segundos de inatividade… \ n", waitTime);

while (shouldRestartRead (readStatus));
if (isSyscallSuccessful (readStatus))
/ * Encerra a string para evitar um bug ao fornecê-la a fprintf. * /
lineBuf [readStatus] = '\ 0';
fprintf (stderr, "Você digitou% lu caracteres. Aqui está sua string: \ n% s \ n ", strlen (lineBuf),
lineBuf);
senão
perror ("Falha na leitura de stdin");
return EXIT_FAILURE;

return EXIT_SUCCESS;

Mais uma vez, este é um aplicativo C completo que você pode compilar e realmente executar.

Ele faz o seguinte: lê uma linha da entrada padrão. No entanto, a cada 5 segundos, ele imprime uma linha informando ao usuário que nenhuma entrada foi fornecida ainda.

Exemplo se eu esperar 23 segundos antes de digitar “Pinguim”:

$ alarm_read
Seu texto:
5 segundos de inatividade ..
10 segundos de inatividade ..
15 segundos de inatividade ..
20 segundos de inatividade ..
Pinguim
Você digitou 8 caracteres. Aqui está sua string:
Pinguim

Isso é incrivelmente útil. Ele pode ser usado para atualizar frequentemente a IU para imprimir o progresso da leitura ou do processamento do seu aplicativo que você está fazendo. Ele também pode ser usado como um mecanismo de tempo limite. Você também pode ser interrompido por qualquer outro sinal que possa ser útil para o seu aplicativo. De qualquer forma, isso significa que seu aplicativo agora pode ser responsivo em vez de ficar preso para sempre.

Portanto, os benefícios superam a desvantagem descrita acima. Se você está se perguntando se deve suportar arquivos especiais em um aplicativo que normalmente funciona com arquivos normais - e assim chamando leitura em um loop - Eu diria para fazer, exceto se você estiver com pressa, minha experiência pessoal muitas vezes provou que substituir um arquivo por um pipe ou FIFO pode literalmente tornar um aplicativo muito mais útil com pequenos esforços. Existem até funções C predefinidas na Internet que implementam esse loop para você: são chamadas de funções readn.

Conclusão

Como você pode ver, fread e read podem ser semelhantes, não são. E com apenas algumas mudanças em como read funciona para o desenvolvedor C, read é muito mais interessante para projetar novas soluções para os problemas que você encontra durante o desenvolvimento de aplicativos.

Da próxima vez, contarei como funciona o syscall de gravação, já que ler é legal, mas poder fazer as duas coisas é muito melhor. Enquanto isso, experimente ler, conheça-o e desejo um feliz ano novo!

Ferramentas úteis para jogadores de Linux
Se você gosta de jogar no Linux, é provável que tenha usado aplicativos e utilitários como Wine, Lutris e OBS Studio para melhorar a experiência de jo...
Jogos HD Remasterizados para Linux que nunca tiveram uma versão Linux anterior
Muitos desenvolvedores e editores de jogos estão apresentando remasterização em HD de jogos antigos para estender a vida da franquia, por favor, os fã...
Como usar o AutoKey para automatizar jogos Linux
AutoKey é um utilitário de automação de desktop para Linux e X11, programado em Python 3, GTK e Qt. Usando sua funcionalidade de script e MACRO, você ...