O que realmente abre um arquivo?

266

Em todas as linguagens de programação (que eu uso pelo menos), você deve abrir um arquivo antes de poder ler ou gravar nele.

Mas o que essa operação aberta realmente faz?

As páginas de manual para funções típicas não dizem nada além de 'abrir um arquivo para leitura / gravação':

http://www.cplusplus.com/reference/cstdio/fopen/

https://docs.python.org/3/library/functions.html#open

Obviamente, através do uso da função, você pode dizer que ela envolve a criação de algum tipo de objeto que facilita o acesso a um arquivo.

Outra maneira de colocar isso seria, se eu implementasse uma openfunção, o que seria necessário fazer no Linux?

jramm
fonte
13
Editando esta pergunta para focar no CLinux; já que o que Linux e Windows fazem é diferente. Caso contrário, é um pouco amplo demais. Além disso, qualquer linguagem de nível superior acabará chamando uma API C para o sistema ou compilando até C para executar, portanto, sair no nível de "C" é colocá-lo no mínimo denominador comum.
George Stocker
1
Sem mencionar que nem todas as linguagens de programação possuem esse recurso, ou é um recurso altamente dependente do ambiente. É certo que é raro hoje em dia, é claro, mas até hoje o manuseio de arquivos é uma parte completamente opcional do ANSI Forth, e nem estava presente em algumas implementações no passado.

Respostas:

184

Em quase todos os idiomas de alto nível, a função que abre um arquivo é um invólucro em torno da chamada de sistema do kernel correspondente. Também pode fazer outras coisas sofisticadas, mas nos sistemas operacionais contemporâneos, a abertura de um arquivo sempre deve passar pelo kernel.

É por isso que os argumentos da fopenbiblioteca funcionam, ou os do Python opense parecem muito com os argumentos da open(2)chamada do sistema.

Além de abrir o arquivo, essas funções geralmente configuram um buffer que será conseqüentemente usado nas operações de leitura / gravação. O objetivo desse buffer é garantir que sempre que você desejar ler N bytes, a chamada de biblioteca correspondente retorne N bytes, independentemente de as chamadas para as chamadas subjacentes do sistema retornarem menos.

Na verdade, não estou interessado em implementar minha própria função; apenas para entender o que diabos está acontecendo ... 'além da linguagem', se você quiser.

Nos sistemas operacionais do tipo Unix, uma chamada bem-sucedida openretorna um "descritor de arquivo" que é meramente um número inteiro no contexto do processo do usuário. Consequentemente, esse descritor é passado para qualquer chamada que interaja com o arquivo aberto e, após a chamada close, o descritor se torna inválido.

É importante observar que a chamada openatua como um ponto de validação no qual várias verificações são feitas. Se não todas as condições forem atendidas, a chamada falhar, retornando -1ao invés do descritor, e do tipo de erro é indicado no errno. As verificações essenciais são:

  • Se o arquivo existe;
  • Se o processo de chamada é privilegiado para abrir este arquivo no modo especificado. Isso é determinado combinando as permissões do arquivo, o ID do proprietário e o ID do grupo com os respectivos IDs do processo de chamada.

No contexto do kernel, deve haver algum tipo de mapeamento entre os descritores de arquivos do processo e os arquivos abertos fisicamente. A estrutura de dados interna que é mapeada para o descritor pode conter ainda outro buffer que lida com dispositivos baseados em bloco ou um ponteiro interno que aponta para a posição atual de leitura / gravação.

Blagovest Buyukliev
fonte
2
Vale ressaltar que em sistemas operacionais do tipo Unix, os descritores de arquivos de estrutura no kernel são mapeados para, é chamado de "descrição de arquivo aberto". Portanto, os FDs de processo são mapeados para OFDs do kernel. Isso é importante para entender a documentação. Por exemplo, veja man dup2e verifique a sutileza entre um descritor de arquivo aberto (que é um FD que está aberto) e uma descrição de arquivo aberto (um OFD).
Rodrigo #
1
Sim, as permissões são verificadas em horário aberto. Você pode ler a fonte da implementação "aberta" do kernel: lxr.free-electrons.com/source/fs/open.c, embora delegue a maior parte do trabalho ao driver específico do sistema de arquivos.
Pjc50 03/11/2015
1
(nos sistemas ext2, isso envolverá a leitura das entradas do diretório para identificar em qual inode os metadados estão sendo carregados e o carregamento no cache do inode. Observe que pode haver sistemas pseudofiles como "/ proc" e "/ sys" que podem fazer coisas arbitrárias quando você abre um arquivo)
pjc50
1
Observe que as verificações no arquivo aberto - se o arquivo existe e se você tem permissão - são, na prática, insuficientes. O arquivo pode desaparecer ou suas permissões podem mudar sob seus pés. Alguns sistemas de arquivos tentam impedir isso, mas, desde que o seu sistema operacional suporte o armazenamento em rede, é impossível impedir (um sistema operacional pode entrar em pânico se o sistema de arquivos local se comportar mal e for razoável: um que o faça quando um compartilhamento de rede não for um sistema operacional viável). Essas verificações também são feitas no arquivo aberto, mas devem (efetivamente) ser feitas em todos os outros acessos a arquivos.
Yakk - Adam Nevraumont
2
Não esquecer a avaliação e / ou criação de bloqueios. Eles podem ser compartilhados ou exclusivos e podem afetar o arquivo inteiro ou apenas uma parte dele.
Thinkeye
83

Eu sugiro que você dê uma olhada neste guia através de uma versão simplificada da open()chamada do sistema . Ele usa o seguinte trecho de código, que é representativo do que acontece nos bastidores quando você abre um arquivo.

0  int sys_open(const char *filename, int flags, int mode) {
1      char *tmp = getname(filename);
2      int fd = get_unused_fd();
3      struct file *f = filp_open(tmp, flags, mode);
4      fd_install(fd, f);
5      putname(tmp);
6      return fd;
7  }

Resumidamente, eis o que esse código faz, linha por linha:

  1. Aloque um bloco de memória controlada pelo kernel e copie o nome do arquivo para ele na memória controlada pelo usuário.
  2. Escolha um descritor de arquivo não utilizado, que você pode considerar um índice inteiro em uma lista crescente de arquivos abertos no momento. Cada processo possui sua própria lista, embora seja mantida pelo kernel; seu código não pode acessá-lo diretamente. Uma entrada na lista contém todas as informações que o sistema de arquivos subjacente usará para extrair bytes do disco, como número de inode, permissões de processo, sinalizadores abertos e assim por diante.
  3. A filp_openfunção tem a implementação

    struct file *filp_open(const char *filename, int flags, int mode) {
            struct nameidata nd;
            open_namei(filename, flags, mode, &nd);
            return dentry_open(nd.dentry, nd.mnt, flags);
    }
    

    que faz duas coisas:

    1. Use o sistema de arquivos para procurar o inode (ou mais geralmente, qualquer tipo de identificador interno que o sistema de arquivos use) correspondente ao nome do arquivo ou caminho que foi passado.
    2. Crie um struct filecom as informações essenciais sobre o inode e retorne-o. Essa estrutura se torna a entrada nessa lista de arquivos abertos que mencionei anteriormente.
  4. Armazene ("instale") a estrutura retornada na lista de arquivos abertos do processo.

  5. Libere o bloco alocado de memória controlada pelo kernel.
  6. Retornar o descritor de arquivo, que pode então ser passado para funções de operação de arquivo como read(), write(), e close(). Cada um deles entregará o controle ao kernel, que pode usar o descritor de arquivo para procurar o ponteiro de arquivo correspondente na lista do processo e usar as informações nesse ponteiro de arquivo para realmente executar a leitura, gravação ou fechamento.

Se você estiver se sentindo ambicioso, poderá comparar este exemplo simplificado com a implementação da open()chamada do sistema no kernel do Linux, uma função chamada do_sys_open(). Você não deve ter problemas para encontrar as semelhanças.


Obviamente, essa é apenas a "camada superior" do que acontece quando você chama open()- ou, mais precisamente, é a parte do código do kernel de mais alto nível que é invocada no processo de abertura de um arquivo. Uma linguagem de programação de alto nível pode adicionar camadas adicionais. Há muita coisa acontecendo em níveis mais baixos. (Agradecemos a Ruslan e pjc50 pela explicação.) Aproximadamente, de cima para baixo:

  • open_namei()e dentry_open()invoque o código do sistema de arquivos, que também faz parte do kernel, para acessar os metadados e o conteúdo dos arquivos e diretórios. O sistema de arquivos lê bytes brutos do disco e interpreta esses padrões de bytes como uma árvore de arquivos e diretórios.
  • O sistema de arquivos usa a camada de dispositivo de bloco , novamente parte do kernel, para obter esses bytes brutos da unidade. (Curiosidade: o Linux permite que você acesse dados brutos da camada de dispositivo de bloco usando /dev/sdae similares.)
  • A camada de dispositivo de bloco chama um driver de dispositivo de armazenamento, que também é código do kernel, para converter de uma instrução de nível médio como "setor de leitura X" para instruções individuais de entrada / saída no código da máquina. Existem vários tipos de drivers de dispositivos de armazenamento, incluindo IDE , (S) ATA , SCSI , Firewire etc., correspondentes aos diferentes padrões de comunicação que uma unidade pode usar. (Observe que o nome está uma bagunça.)
  • As instruções de E / S usam os recursos internos do chip do processador e do controlador da placa-mãe para enviar e receber sinais elétricos no fio que vai para a unidade física. Isso é hardware, não software.
  • Na outra extremidade do fio, o firmware do disco (código de controle incorporado) interpreta os sinais elétricos para girar os pratos e mover as cabeças (HDD), ou ler uma célula ROM flash (SSD) ou o que for necessário para acessar os dados. esse tipo de dispositivo de armazenamento.

Isso também pode estar um pouco incorreto devido ao armazenamento em cache . :-P Mas, falando sério, há muitos detalhes que deixei de fora - uma pessoa (não eu) poderia escrever vários livros descrevendo como todo esse processo funciona. Mas isso deve lhe dar uma idéia.

David Z
fonte
67

Qualquer sistema de arquivos ou sistema operacional sobre o qual você queira falar está bem para mim. Agradável!


Em um ZX Spectrum, a inicialização de um LOADcomando colocará o sistema em um loop restrito, lendo a linha Audio In.

O início dos dados é indicado por um tom constante e, em seguida, segue uma sequência de pulsos longos / curtos, onde um pulso curto é para um binário 0e um pulso mais longo para um binário 1( https://en.wikipedia.org/ wiki / ZX_Spectrum_software ). O loop de carga apertada reúne bits até preencher um byte (8 bits), armazena isso na memória, aumenta o ponteiro da memória e volta para procurar mais bits.

Normalmente, a primeira coisa que um carregador lê é um cabeçalho de formato curto e fixo , indicando pelo menos o número de bytes a serem esperados e possivelmente informações adicionais, como nome do arquivo, tipo de arquivo e endereço de carregamento. Depois de ler esse cabeçalho curto, o programa pode decidir se continua carregando a maior parte dos dados ou sai da rotina de carregamento e exibe uma mensagem apropriada para o usuário.

Um estado de fim de arquivo pode ser reconhecido ao receber o número de bytes esperado (um número fixo de bytes, conectado no software ou um número variável, como indicado em um cabeçalho). Foi gerado um erro se o loop de carregamento não receber um pulso na faixa de frequência esperada por um determinado período de tempo.


Um pouco de fundo sobre esta resposta

O procedimento descrito carrega dados de uma fita de áudio comum - daí a necessidade de digitalizar a entrada de áudio (conectada com um plugue padrão para gravadores). Um LOADcomando é tecnicamente o mesmo que openum arquivo - mas está fisicamente ligada a realmente carregar o arquivo. Isso ocorre porque o gravador não é controlado pelo computador e você não pode (com êxito) abrir um arquivo, mas não carregá-lo.

O "loop apertado" é mencionado porque (1) a CPU, uma Z80-A (se a memória servir), era realmente lenta: 3,5 MHz e (2) o Spectrum não tinha relógio interno! Isso significa que ele precisou manter a contagem precisa dos estados T (tempos de instrução) para todos. solteiro. instrução. dentro desse loop, apenas para manter o tempo exato do sinal sonoro.
Felizmente, essa baixa velocidade da CPU tinha a vantagem distinta de poder calcular o número de ciclos em um pedaço de papel e, portanto, o tempo real que eles levariam.

usr2564301
fonte
10
@ BillWoodger: bem, sim. Mas é uma pergunta justa (quero dizer a sua). Votei em fechar como "muito amplo", e minha resposta tem como objetivo ilustrar quão extremamente ampla é a questão.
precisa saber é o seguinte
8
Eu acho que você está ampliando a resposta um pouco demais. O ZX Spectrum tinha um comando OPEN, e isso era totalmente diferente de LOAD. E mais difícil de entender.
Rodrigo
3
Também não concordo em encerrar a pergunta, mas realmente gosto da sua resposta.
Enzo Ferber
23
Embora eu tenha editado minha pergunta para restringir ao sistema operacional Linux / Windows na tentativa de mantê-la aberta, esta resposta é totalmente válida e útil. Como afirmado na minha pergunta, não estou procurando implementar algo ou fazer com que outras pessoas façam meu trabalho, estou procurando aprender. Para aprender, você deve fazer as 'grandes' perguntas. Se encerrarmos constantemente as perguntas sobre SO por serem "muito amplas", corre o risco de se tornar um local apenas para que as pessoas escrevam seu código para você sem fornecer nenhuma explicação sobre o que, onde ou por que. Prefiro mantê-lo como um lugar que posso aprender.
jramm
14
Essa resposta parece provar que sua interpretação da pergunta é muito ampla, e não que a própria pergunta é muito ampla.
GTC
17

Depende do sistema operacional o que exatamente acontece quando você abre um arquivo. Abaixo, descrevo o que acontece no Linux, pois dá uma idéia do que acontece quando você abre um arquivo e pode verificar o código-fonte se estiver interessado em mais detalhes. Não estou cobrindo permissões, pois isso tornaria a resposta muito longa.

No Linux, todo arquivo é reconhecido por uma estrutura chamada inode. Cada estrutura possui um número único e cada arquivo obtém apenas um número de inode. Essa estrutura armazena metadados para um arquivo, por exemplo, tamanho do arquivo, permissões de arquivo, carimbos de data e hora e blocos de ponteiro para disco, no entanto, não o nome do arquivo real. Cada arquivo (e diretório) contém uma entrada de nome de arquivo e o número do inode para pesquisa. Quando você abre um arquivo, assumindo que você possui as permissões relevantes, um descritor de arquivo é criado usando o número de inode exclusivo associado ao nome do arquivo. Como muitos processos / aplicativos podem apontar para o mesmo arquivo, o inode possui um campo de link que mantém a contagem total de links para o arquivo. Se um arquivo estiver presente em um diretório, sua contagem de links será uma, se houver um link físico, sua contagem será dois e se um arquivo for aberto por um processo, a contagem de links será incrementada em 1.

Alex
fonte
6
O que isso tem a ver com a questão real?
precisa saber é o seguinte
1
Ele descreve o que acontece em um nível baixo quando você abre um arquivo no Linux. Concordo que a pergunta é bastante ampla, portanto essa pode não ter sido a resposta que jramm estava procurando.
Alex
1
Então, novamente, sem verificação de permissões?
Bill Woodger
11

Escrituração, principalmente. Isso inclui várias verificações como "O arquivo existe?" e "Tenho permissões para abrir este arquivo para gravação?".

Mas isso é tudo sobre o kernel - a menos que você esteja implementando seu próprio sistema operacional de brinquedos, não há muito o que se aprofundar (se você se divertir, é uma ótima experiência de aprendizado). Obviamente, você ainda deve aprender todos os códigos de erro possíveis que pode receber ao abrir um arquivo, para poder manipulá-los adequadamente - mas essas geralmente são pequenas abstrações agradáveis.

A parte mais importante no nível do código é que ele fornece um identificador para o arquivo aberto, usado para todas as outras operações que você faz com um arquivo. Você não poderia usar o nome do arquivo em vez deste identificador arbitrário? Bem, com certeza - mas usar uma alça oferece algumas vantagens:

  • O sistema pode acompanhar todos os arquivos que estão abertos no momento e impedir que sejam excluídos (por exemplo).
  • Os sistemas operacionais modernos são construídos com alças - existem inúmeras coisas úteis que você pode fazer com alças, e todos os diferentes tipos de alças se comportam quase de forma idêntica. Por exemplo, quando uma operação de E / S assíncrona é concluída em um identificador de arquivo do Windows, o identificador é sinalizado - isso permite que você bloqueie o identificador até que seja sinalizado ou conclua a operação totalmente de forma assíncrona. Esperar em um identificador de arquivo é exatamente o mesmo que esperar em um identificador de encadeamento (sinalizado, por exemplo, quando o encadeamento termina), um identificador de processo (novamente, sinalizado quando o processo termina) ou um soquete (quando alguma operação assíncrona é concluída). Tão importante quanto isso, os identificadores pertencem aos seus respectivos processos; portanto, quando um processo é encerrado inesperadamente (ou o aplicativo é mal gravado), o sistema operacional sabe o que identifica.
  • A maioria das operações é posicional - você readda última posição no seu arquivo. Usando um identificador para identificar uma "abertura" específica de um arquivo, você pode ter vários identificadores simultâneos no mesmo arquivo, cada um lendo seus próprios locais. De certa forma, o identificador atua como uma janela móvel para o arquivo (e uma maneira de emitir solicitações de E / S assíncronas, que são muito úteis).
  • Os identificadores são muito menores que os nomes de arquivos. Um identificador geralmente é do tamanho de um ponteiro, normalmente de 4 ou 8 bytes. Por outro lado, os nomes de arquivos podem ter centenas de bytes.
  • Os identificadores permitem que o sistema operacional mova o arquivo, mesmo que os aplicativos o tenham aberto - o identificador ainda é válido e ainda aponta para o mesmo arquivo, mesmo que o nome do arquivo tenha sido alterado.

Também há outros truques que você pode fazer (por exemplo, compartilhar identificadores entre processos para ter um canal de comunicação sem usar um arquivo físico; em sistemas unix, os arquivos também são usados ​​para dispositivos e vários outros canais virtuais, portanto, isso não é estritamente necessário ), mas eles não estão realmente ligados à openoperação em si, então não vou me aprofundar nisso.

Luaan
fonte
7

No cerne da questão, ao abrir para leitura, nada sofisticado realmente precisa acontecer. Tudo o que precisa fazer é verificar se o arquivo existe e se o aplicativo possui privilégios suficientes para lê-lo e criar um identificador no qual você pode emitir comandos de leitura para o arquivo.

É nesses comandos que a leitura real será despachada.

O sistema operacional geralmente obtém vantagem na leitura iniciando uma operação de leitura para preencher o buffer associado ao identificador. Então, quando você realmente faz a leitura, pode retornar o conteúdo do buffer imediatamente, em vez de precisar aguardar na E / S do disco.

Para abrir um novo arquivo para gravação, o sistema operacional precisará adicionar uma entrada no diretório para o novo arquivo (atualmente vazio). E, novamente, é criada uma alça na qual você pode emitir os comandos de gravação.

catraca arrepiante
fonte
5

Basicamente, uma chamada para abrir precisa localizar o arquivo e, em seguida, registrar o que for necessário para que operações posteriores de E / S possam encontrá-lo novamente. Isso é bastante vago, mas será verdade em todos os sistemas operacionais em que consigo pensar imediatamente. As especificidades variam de plataforma para plataforma. Muitas respostas já aqui falam sobre os sistemas operacionais de desktop modernos. Eu fiz uma pequena programação no CP / M, por isso vou oferecer meu conhecimento sobre como ele funciona no CP / M (o MS-DOS provavelmente funciona da mesma maneira, mas por razões de segurança, normalmente não é feito hoje em dia. )

No CP / M, você tem uma coisa chamada FCB (como você mencionou C, você pode chamá-la de struct; é realmente uma área contígua de 35 bytes na RAM contendo vários campos). O FCB possui campos para gravar o nome do arquivo e um número inteiro (4 bits) identificando a unidade de disco. Então, quando você chama o Open File do kernel, passa um ponteiro para essa estrutura colocando-o em um dos registros da CPU. Algum tempo depois, o sistema operacional retorna com a estrutura ligeiramente alterada. Qualquer que seja a E / S que você faça nesse arquivo, você passa um ponteiro para essa estrutura para a chamada do sistema.

O que o CP / M faz com este FCB? Ele reserva determinados campos para seu próprio uso e os utiliza para acompanhar o arquivo, portanto é melhor nunca tocá-los de dentro do seu programa. A operação Abrir arquivo procura na tabela no início do disco por um arquivo com o mesmo nome do conteúdo do FCB (o caractere curinga '?' Corresponde a qualquer caractere). Se ele encontrar um arquivo, ele copia algumas informações no FCB, incluindo o (s) local (is) físico (s) do arquivo no disco, para que as chamadas de E / S subsequentes chamem o BIOS, que pode passar esses locais para o driver de disco. Nesse nível, as especificidades variam.

OmarL
fonte
-7

Em termos simples, quando você abre um arquivo, na verdade está solicitando ao sistema operacional que carregue o arquivo desejado (copie o conteúdo do arquivo) do armazenamento secundário para o ram para processamento. E a razão por trás disso (carregar um arquivo) é porque você não pode processar o arquivo diretamente do disco rígido, devido à sua velocidade extremamente lenta em comparação com o Ram.

O comando open irá gerar uma chamada do sistema que, por sua vez, copia o conteúdo do arquivo do armazenamento secundário (disco rígido) para o armazenamento primário (RAM).

E fechamos um arquivo porque o conteúdo modificado deve ser refletido no arquivo original que está no disco rígido. :)

Espero que ajude.


fonte