8.深入文件I/O

本文详细介绍了Linux系统中静态文件、inode结构、文件打开状态、错误处理、空洞文件、多文件描述符操作、原子操作以及fcntl函数的使用。重点讨论了文件在内存中的表示、文件描述符管理和文件截断等核心概念。
摘要由CSDN通过智能技术生成

Linux系统管理文件

1、静态文件

        文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件

        硬盘的最小存储单位叫做“扇区” (Sector),一个扇区的为512字节,也就是0.5KB,操作系统在读取硬盘内容的时候如果一个一个扇区的进行读取,会导致效率很低。因此对于操作系统来说读取磁盘通常都是以"块"(block)为单位,常见的块是4KB。

2、inode

        静态文件的数据都存储在磁盘设备中对应的不同的"块"中,open函数是通过文件对应的inode号找到数据的存储区域的。

        磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;另一个是 inode 区,用于存放 inode table(inode 表,inode table 中存放的是一个一个的 inode,不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode, inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、 文件类型、 文件数据存储的 block(块)位置等等信息,关系如下:

        并且通过命令ls -il,可以查看文件对应的inode

3、 文件打开的状态

        通过open函数打开文件,Linux内核会申请一段缓冲区,将要打开的静态文件的内容拷贝到该缓冲区,该缓冲区的文件数据也称动态文件

        操作系统采用该方法的原因是因为内存的读取速率比磁盘的读取速率要快得多,块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节) ,一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中, 所以导致对块设备的读写操作非常不灵活; 而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活。

        Linux内核为每一个进程分配一个进程控制块(PCB),里面有一个指针指向了文件描述符表(fd表),文件描述符表中存储本进程打开文件对应的文件描述符,通过文件描述符fd就可以查询对应文件的状态。它们的关系如下图所示:

 

4、 errno错误码

        在Linux系统中通过系统调用函数执行失败的时候,会执行return返回-1。然而仅仅通过-1并不能直到是什么原因导致的错误,因此当操作系统执行出错的时候,会将错误对应的编号赋值给errno变量。当出现一个新的错误的时候,会覆盖上一次的错误errno.

        然而并不是所有的系统调用都会改变errno,以open函数举例,通过man 2 open打开手册查找该函数返回值.如下图所示:

        错误码errno仅仅是一个数字,还需要通过strerrno函数或perror函数将errno转化成对应的字符信息。用法如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>


int main(void) 
{
    int fd;

    fd = open("./testfile", O_RDONLY);

    if(fd == -1) {
        /*1、strerror函数*/
        //printf("Error:%s\n",strerror(errno));   //打印错误

        /*2、perror函数*/
        perror("open error");
        return -1;
    }

    close(fd);
    return 0;
}

运行结果如下:

         由于在当前文件路径内,并不存在testfile文件,因此会打印文件不存在这个错误。

5、空洞文件

        使用lseek系统调用可以修改文件的读写偏移量,lseek函数还允许读写偏移量超出文件的长度,比如一个文件大小有4KB,也就是4096字节,使用lseek函数可以将文件偏移量修改到6000字节处,现在4096~6000字节中就出现了空洞,称为空洞文件

        空洞文件对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入。

        实验代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int main(void)
{
    int fd;
    int ret;
    char buffer[1024];
    int i;
    /* 打开文件 */
    fd = open("./hole_file", O_WRONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }
    /* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
    ret = lseek(fd, 4096, SEEK_SET);
    if (-1 == ret) {
        perror("lseek error");
        goto err;
    }
    /* 初始化 buffer 为 0xFF */
    memset(buffer, 0xFF, sizeof(buffer));
    /* 循环写入 4 次,每次写入 1K */
    for (i = 0; i < 4; i++) {
        ret = write(fd, buffer, sizeof(buffer));
        if (-1 == ret) {
            perror("write error");
            goto err;
    }
}
    ret = 0;
    err:
    /* 关闭文件 */
    close(fd);
    exit(ret);
}

        Linux中新创建的文件大小为0字节,该函数的作用是修改文件的读写偏移量到4096字节,然后在写入4096字节。通过一下命令可以查看文件的大小为8K和实际数据所占大小为4K:

        可以得出结论文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的。

        6、open函数多次打开同一个文件

        一个进程在多次使用open函数打开同一个文件的时候会得到不同的文件描述符。同时在使用close函数关闭的文件也得依次关闭各个文件描述符。

        #通过任何一个文件描述符对文件进行操作都是可以的,但是open函数打开文件对应的什么权限,返回的文件描述符就只有对应权限。

        以下函数验证open函数多次打开同一文件,并返回相应文件描述符:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    int fd1, fd2, fd3;
    int ret;
    /* 第一次打开文件 */
    fd1 = open("./test_file", O_RDWR);
    if (-1 == fd1) {
        perror("open error");
        exit(-1);
    }

    /* 第二次打开文件 */
    fd2 = open("./test_file", O_RDWR);
    if (-1 == fd2) {
        perror("open error");
        ret = -1;
    goto err1;
    }

    /* 第三次打开文件 */
    fd3 = open("./test_file", O_RDWR);
    if (-1 == fd3) {
        perror("open error");
        ret = -1;
        goto err2;
    }

    /* 打印出 3 个文件描述符 */
    printf("%d %d %d\n", fd1, fd2, fd3);

    close(fd3);
    ret = 0;

err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}

代码结果如下:

        3次打开同一文件,返回的文件描述符分别为3,4,5.


一个进程内多次 open 打开同一个文件,对各个文件描述符进行读取操作的是同一块动态内存。

以下面的代码为例子:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main(void)
{
    char buffer[4];
    int fd1, fd2;
    int ret;
    /* 创建新文件 test_file 并打开 */
    fd1 = open("./test_file", O_RDWR);
    if (-1 == fd1) {
        perror("open error");
        exit(-1);
    }
    /* 再次打开 test_file 文件 */
    fd2 = open("./test_file", O_RDWR);
    if (-1 == fd2) {
        perror("open error");
        ret = -1;
        goto err1;
    }

    //将buffer写入0xff
    buffer[0] = 0x01;
    buffer[1] = 0x02;
    buffer[2] = 0x03;
    buffer[3] = 0x04;

    ret = write(fd1, buffer, 4);
    if (-1 == ret) {
        perror("write error");
        goto err2;
    }

    /* 将读写位置偏移量移动到文件头 */
    ret = lseek(fd2, 0, SEEK_SET);
    if (-1 == ret) {
        perror("lseek error");
        goto err2;
    }

    /* 读取数据 */
    memset(buffer, 0x00, sizeof(buffer));
    ret = read(fd2, buffer, 4);
    if (-1 == ret) {
        perror("read error");
        goto err2;
    }

    printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],buffer[2], buffer[3]);
    ret = 0;

err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}

         改代码首先通过操作文件描述符fd1将0x01,0x02,0x03,0x04写入文件file_test,然后操作文件描述符fd2读取4个字节数据打印。可以证明操作同一文件的不同文件描述符是维护同一段动态内存。

一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。

        同一个文件被多次打开,会得到多个不同的文件描述符,也就意味着会有多个不同的文件表,而文件读写偏移量信息就记录在文件表数据结构中,所以从这里可以推测不同的文件描述符所对应的读写偏移量是相互独立的,并没有关联在一起,并且文件表中 i-node 指针指向的都是同一个 inode,如下图所示:

7、复制文件描述符 

        在 Linux 系统中, open 返回得到的文件描述符 fd 可以进行复制, 复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限。

        dup 函数用于复制文件描述符,此函数原型如下所示

#include <unistd.h>
int dup(int oldfd);
函数参数和返回值含义如下:
oldfd: 需要被复制的文件描述符。
返回值: 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;
如果复制失败将返回-1,并且会设置 errno 值。

        下面进行代码测试:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main(void)
{
    char buffer[4];
    int fd1, fd2;
    int ret;
    /* 创建新文件 test_file 并打开 */
    fd1 = open("./test_file", O_RDWR);
    if (-1 == fd1) {
        perror("open error");
        exit(-1);
    }

    fd2 = dup(fd1);

    //将buffer写入0xff
    buffer[0] = 0x01;
    buffer[1] = 0x02;
    buffer[2] = 0x03;
    buffer[3] = 0x04;

    ret = write(fd1, buffer, 4);
    if (-1 == ret) {
        perror("write error");
        goto err2;
    }

    /* 将读写位置偏移量移动到文件头 */
    ret = lseek(fd2, 0, SEEK_SET);
    if (-1 == ret) {
        perror("lseek error");
        goto err2;
    }

    /* 读取数据 */
    memset(buffer, 0x00, sizeof(buffer));
    ret = read(fd2, buffer, 4);
    if (-1 == ret) {
        perror("read error");
        goto err2;
    }

    printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],buffer[2], buffer[3]);
    ret = 0;

err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}

        和上一个代码一样,首先通过open函数得到文件描述符fd1,然后通过dup函数复制文件描述符fd1给fd2,随后进行内存的写和读取,代码结果如下:

8、原子操作 

        原子操作, 是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。

        (1) O_APPEND 实现原子操作

        进程 A 和进程 B 都对同一个文件进行追加写操作, 导致进程 A 写入的数据覆盖了进程 B 写入的数据,解决办法就是将“先定位到文件末尾,然后写”这两个步骤组成一个原子操作即可,就是 使用O_APPEND 标志。

        (2)pread()和 pwrite()

        pread()和 pwrite()都是系统调用,与 read()、 write()函数的作用一样,用于读取和写入数据。区别在于,pread()和 pwrite()可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数,用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read。函数原型如下:

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

首先调用这两个函数需要包含头文件<unistd.h>。
函数参数和返回值含义如下:
fd、 buf、 count 参数与 read 或 write 函数意义相同。
offset: 表示当前需要进行读或写的位置偏移量。
返回值: 返回值与 read、 write 函数返回值意义一样。
虽然 pread(或 pwrite)函数相当于 lseek 与 pread(或 pwrite)函数的集合,但还是有下列区别:
调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);
不更新文件表中的当前位置偏移量。

9、系统调用fcntl 

        fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。 fcntl()函数原型如下所示:

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

函数参数和返回值含义如下:
fd: 文件描述符。
cmd: 操作命令。 此参数表示我们将要对 fd 进行什么操作, cmd 参数支持很多操作命令,大家可以打
开 man 手册查看到这些操作命令的详细介绍,这些命令都是以 F_XXX 开头的,譬如 F_DUPFD、 F_GETFD、
F_SETFD 等, 不同的 cmd 具有不同的作用, cmd 操作命令大致可以分为以下 5 种功能:
复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC);
获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD);
获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL);
获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN);
获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK)

返回值: 执行失败情况下,返回-1,并且会设置 errno;执行成功的情况下,其返回值与 cmd(操作命
令)有关,譬如 cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、 cmd=F_GETFD(获取文
件描述符标志)将返回文件描述符标志、 cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。

        (1)使用fcntl复制文件描述符:

dup 和 dup2函数可以用于复制文件描述符,除此之外还可以通过fcntl函数复制文件描述符。当传入的参数cmd =  F_DUPFD,它的作用会根据 fd 复制出一个新的文件描述符,此时需要传入第三个参数,第三个参数用于指出新复制出的文件描述符是一个大于或等于该参数的可用文件描述符。具体代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int fd1, fd2;
    int ret;
    /* 打开文件 test_file */
    fd1 = open("./test_file", O_RDONLY);
    if (-1 == fd1) {
        perror("open error");
        exit(-1);
    }
    /* 使用 fcntl 函数复制一个文件描述符 */
    fd2 = fcntl(fd1, F_DUPFD, 0);
    if (-1 == fd2) {
        perror("fcntl error");
        ret = -1;
        goto err;
    }
    printf("fd1: %d\nfd2: %d\n", fd1, fd2);
    ret = 0;
    close(fd2);
    err:
    /* 关闭文件 */
    close(fd1);
    exit(ret);
}

结果如下:

因为在执行 fcntl 函数时,传入的第三个参数是 0,也就时指定复制得到的新文件描述符必须要大于或等于 0,但是因为 0~3 都已经被占用了,所以分配得到的 fd 就是 4。

        (2)获取/设置文件状态标志

        cmd=F_GETFL 可用于获取文件状态标志, cmd=F_SETFL 可用于设置文件状态标志。 cmd=F_GETFL 时不需要传入第三个参数,返回值成功表示获取到的文件状态标志;cmd=F_SETFL 时,需要传入第三个参数,此参数表示需要设置的文件状态标志。

10、截断文件

使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度, 其函数原型如下所示:

#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

这两个函数的区别在于: ftruncate()使用文件描述符 fd 来指定目标文件,而 truncate()则直接使用文件路
径 path 来指定目标文件,其功能一样。
如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展, 对扩展部分进行读取将得到空字节"\0"。
使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必
须要具有可写权限,也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。

使用示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(void)
{
    int fd;
    /* 打开 file1 文件 */
    if (0 > (fd = open("./file1", O_RDWR))) {
        perror("open error");
        exit(-1);
    }

    /* 使用 ftruncate 将 file1 文件截断为长度 0 字节 */
    if (0 > ftruncate(fd, 0)) {
        perror("ftruncate error");
        exit(-1);
    }

    /* 使用 truncate 将 file2 文件截断为长度 1024 字节 */
    if (0 > truncate("./file2", 1024)) {
        perror("truncate error");
        exit(-1);
    }

    /* 关闭 file1 退出程序 */
    close(fd);
    exit(0);
}

        该函数将file1截断为0字节,file2截断为1024字节。

运行代码前文件大小如下:

截断后文件如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值