【Linux系统编程】文件原理

进程与文件

        当我们对文件进行操作时,文件必须要被加载到内存中,然后CUP从内存中拿到此文件进行操作,没有打开的文件放在磁盘中存储。 

        文件的打开其实也是设计到内部某个进程。无论是系统调用,还是专有库中的函数,都是启动进程来进行打开。进程会自动记录目前启动时的当前路径,平常所说的相对路径就是指相对于当前进程路径下的路径。当我们没有特意说明文件路径在此进程中对文件操作时,默认会在此进程的路径下进行。比如我们使用C语言新建文件使用绝对路径,默认就会在此进程的路径下进行,若是此进程的路径发生改变,新建的文件会在改变后的路径下进行,这就是相对路径的原理。

[zhu@VM-16-10-centos day3]$ ll ..
.........
[zhu@VM-16-10-centos day3]$ cat myfile.cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

int main()
{
    chdir("..");
    FILE* fp = fopen("test.txt", "w");
    if (!fp)
    {
        perror("fopen");
        exit(1);
    }
    char* str = "file system";
    int n = fwrite(str, strlen(str), 1, fp);
    cout << "write block: " << n << "  " << "pid = " << getpid() << endl; 
    fclose(fp);
    return 0;
}

 

[zhu@VM-16-10-centos day3]$ ./myfile
write block: 1  pid = 12291
[zhu@VM-16-10-centos day3]$ ll ..
...........
-rw-rw-r-- 1 zhu zhu   11 Mar 21 10:52 test.txt    //创建的文件

        总的来说是进程在打开文件。我们对文件的操作都是由系统下的某个进程进行的。

Linux下的文件系统

        在Linux系统下,我们可以把一切都看成文件(包括硬件)。Linux系统有一个重要特性,即“一切皆文件”的原则。无论是普通的磁盘文件、目录,还是网络套接字、硬件设备,在Linux中都被抽象为文件。

        我们平常使用高级语言进行底层文件的调用,本质上是封装了系统调用。因为用户不能直接调用系统硬件,本质上是操作系统进行调用的。我们通常使用语言进行调用实际就是操作系统提供了相应的接口供用户使用。比如以C语言为例,C语言的库函数接口fopen、fclose、fread、fwrite 在某种意义上来讲调用的是系统接口open、close、read、write(这些函数运用跟C中的文件操作相似,不懂的可用man指令查看文档,这里不再过多解释),只不过对系统调用进行了封装。系统调用接口和库函数的关系如下:

        下面我们来认识一下文件流操作。程序在启动时,默认会打开三个文件流:stdin、stdout、stderr。这三种流的类型都是文件指针FILE*。

        stdin:标准输入——默认是键盘设备。计算机系统从此文件流中获取数据信息,即从此文件中读取数据。

        stdout:标准输出——默认是显示器设备。将数据输出到此文件流中,即从此文件中输出数据。

        stderr:标准错误——默认是显示器设备。用于输出程序或命令的错误信息,与stdout原理相似。

        正因有了标准输入输出流操作(I/O设备操作),才能使得程序能够与用户和其他程序进行有效的交互。

        那么问题来了,系统下的所有都是文件,程序系统又是如何找到对应的文件?其实每个文件都有一个对应的文件描述符进行标志。文件描述符是一个非负整数,与文件名形成了一种索引关系,使得程序可以通过这个整数来访问和操作对应的文件。

        文件描述符的范围是0到N,其中0、1、2是特殊文件的文件描述符:0代表标准输入(stdin),1代表标准输出(stdout),2代表标准错误输出(stderr)。一般情况下,文件描述符从3开始数往后分配。因为内部的文件描述符其实就是存放管理文件结构体(struct file:包含三个方面,第一个是能够通过指针让我们找到文件的属性,第二个是对文件操作的一堆方法,第三个是是所提供的缓冲区。打开一个文件系统内部就会创建一个struct file结构体对文件进行管理)的指针数组 fd_array 的下标,此指针数组每个元素都是一个指向打开文件的结构体指针,而task_struct内部存在一个指针,指向存放此指针数组的结构体(struct files_struct)。

        总的来说文件描述符就是数组的下标,当使用一个文件时就必须找到此文件的文件描述符,通过文件描述符来找到对应的文件。这里的重点在于文件操作符,只要我们拿到文件操作符fd,就能够通过file_struct结构体内部的fd_array数组指针找到对应管理文件的结构体file,对其文件进行操作。如进程在调用open/fopen时,该方法把文件描述符fd返回,然后该进程拿到fd后,write函数又可通过文件描述符表fd_arry找到对应的文件对其进行操作。

        C标准库中的FILE(文件流:随机读取或写入文件,即与文件操作的底层)其实就是自己封装的一个结构体,里面封装了 stdin、stdout、stderr 的文件描述符0,1,2。之所以系统不直接封装而让语言单独封装是为了保证可移植性。若是系统直接封装,一旦换了平台系统可能就会出问题,导致不可移植。其实不仅仅是流操作,很多有关系统接口也一样,为了保证可移植性,都是在不同语言内部封装不同系统调用的接口和相关的文件接口。

文件描述符及重定向

        从上可看出,每个文件都有唯一的文件描述符。每个文件的文件描述符的分配规则是最小的没有被使用的数组下标会分配给最新打开的文件。我们先来看以下代码。   

[zhu@VM-16-10-centos day3]$ cat open.cpp

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //打开文件,成功后返回文件的文件描述符,失败返回-1
    cout << "fd(log.txt) = " << fd << endl;
    cout << "stdout: " << fileno(stdout) << endl;
    close(fd);
    return 0;
}


[zhu@VM-16-10-centos day3]$ ./open.exe  //发现在标准输出流中什么也没有输出
[zhu@VM-16-10-centos day3]$ ll
total 32
-rw-rw-r-- 1 zhu zhu    7 Mar 21 20:31 log.txt
............
[zhu@VM-16-10-centos day3]$ cat log.txt   //发现输出到文件log.txt中
fd(log.txt) = 1
stdout: 1

         注意,文件描述符会按照最小下标分配,以上程序中关掉了系统下标准输出流的文件描述符1。至于fileno(stdout)的调用,由于stdout是C语言的流,非系统专属,当程序启动时C的stdout就默认打开,因此close(1);只是关闭了与stdout关联的文件描述符,与C的stdout内部文件数据没有关系,fileno(stdout)仍会返回原始的文件描述符值(即1),但是 close是系统操作,它会关闭了底层系统文件描述符表索引值1所指向的 stdout 文件。当我们创建文件log.txt时系统会给此文件分配描述符1。C中的输出都是往文件描述符为1所对应的文件中输出的,即一般情况下都是往标准输出流stdout中输出。这里log.txt的文件描述符为1,进程拿到文件描述符后会自动往文件描述符表fd_array中寻找索引值为1对应的文件中输出,所以就会出现以上输出重定向。输入重定向同理,将文件描述符为0的进行重新指向。

        重定向运用的就是以上的原理,即语言层上不变,系统层上进行修改——系统将有关文件的文件描述符进行修改为指定的文件描述符,即指针数组元素的重新指向。以下是输入重定向的原理示例。

[zhu@VM-16-10-centos day3]$ cat log.txt
123456
[zhu@VM-16-10-centos day3]$ cat myfile.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;

int main()
{
    close(0);   //将标准输入重定向(键盘)去除
    open("log.txt", O_RDONLY);   //此时文件log.txt的文件描述符为0,即成为了输入流
    int a = 0;
    cin >> a;  //从输入流中读取数据
    cout << a << endl;  
    return 0;
}
[zhu@VM-16-10-centos day3]$ ./myfile
123456

        不难发现,以上类似的程序完成重定向功能比较麻烦——先close关闭再open分配。说白了,重定向功能就是分配到指定的文件描述符,而文件描述符对应指定文件的功能,这里我们可直接让指定文件的文件描述符指向对应功能的文件描述符所指向文件的功能即可。比如将文件描述符为3的指向文件描述符为1所对应的系统文件,即指针数组元素之间的浅拷贝fd_array[1]=fd_array[3]。

        系统提供了相应接口函数dup2(dup不常用)来完成这种浅拷贝。

头文件

#include <unistd.h>

接口形式

int dup2(int oldfd, int newfd); 

将所对应的oldfd拷贝到newfd,即fd_array[newfd]=fd_array[oldfd],此时文件描述符newfd所对应的文件结构将被覆盖。

dup2用法

[zhu@VM-16-10-centos day3]$ cat myfile.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    dup2(fd, 1);
    cout << "fd = " << fd << endl;
    return 0;
}

[zhu@VM-16-10-centos day3]$ ./myfile
[zhu@VM-16-10-centos day3]$ cat log.txt
fd = 3

  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值