万字解读文件IO基础与深入

目录

一、文件I/O基础

1.man命令

2.文件基本操作(4个函数)

3.文件描述符

4.open函数

5.write函数

6.read读文件函数

7.close关闭文件函数

8.lseek函数

9.文件操作练习

二、文件I/O深入

1.linux系统如何管理文件

1.1静态文件与inode

1.2文件打开时的状态

2.返回错误处理

2.1errno

2.2strerror

2.3perror函数-重要

2.4exit函数

3.空洞文件

4.O_APPEND和O_TRUNC标志

5.多次打开同一个文件

6.多次打开同一个文件-进行读操作与O_APPEND标志

7.复制文件描述符

7.1dup函数

7.2dup2函数

8.文件共享

9.原子操作与竞争冒险

9.1竞争冒险

9.2原子操作

10.fcntl和ioctl

10.1fcntl函数

​编辑

10.2ioctl函数

11.截断文件


一、文件I/O基础

1.man命令

当我们需要查询某个系统调用的功能介绍、使用方法时,直接通过man命令便可以搞定

man 命令后面跟着两个参数,数字 2 表示系统调用,例如:

man 2 open #查看open函数的帮助信息

man 命令除了可以查看系统调用的帮助信息外,还可以查看 Linux 命令(对应数字 1)

以及标准 C 库函数(对应数字 3)所对应的帮助信息;最后一个参数 open 表示需要查看的系统调用函数名。

2.文件基本操作(4个函数)

一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作,主要涉及到 4 个函数:open()、read()、write()以及 close()

示例代码:

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

int main(void)
{
    char buff[1024];
    int  fd1,fd2;
    int ret;

    /*打开源文件src_file(只读方式)*/
    fd1 = open("./src_file",O_RDONLY);
    if(-1 == fd1)
            return fd1;
     /*打开源文件src_file(只写方式)*/
     fd2 = open("./dest_file",O_WRONLY);
     if(-1 == fd2){
        ret = fd2;
        goto out1;
     }

     ret = read(fd1,buff,sizeof(buff));
     if(-1 == ret)
            goto out2;

    ret = write(fd2,buff,sizeof(buff));
    if(-1 == ret)
            goto out2;

    ret = 0;
out2:
    close(fd2);

out1:
    close(fd1);
    return ret;
}
3.文件描述符
  1. int类型;

  2. open执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符

  3. 对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引

  4. 每次给打开的文件分配文件描述符都是从最小的没有被使用的文件描述符(0~1023)开始,当之前打开的文件被关闭之后,那么它对应的文件描述符会被释放,释放之后也就成为了一个没有被使用的文件描述符了。

  5. 调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始

  6. 0、1、2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2)

4.open函数

在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件。

open函数用于打开文件,函数原型如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

函数参数和返回值含义如下:

1)pathname**:**字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信息,譬如:"./src_file"(当前目录下的 src_file 文件)、"/home/dengtao/hello.c"等;如果 pathname 是一个符号链接,会对其进行解引用。

2)flags**:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,这些标志使用宏定义进行描述,都是常量,open 函数提供了非常多的标志,我们传入 flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合。

(1)open函数flags参数值标志介绍:

下面三个标志:

传入的flags 参数中必须要包含其中一种标志,而且只能包含一种

O_RDONLY:只读打开文件

O_WRONLY:只写打开文件

O_RDWR:以可读可写方式打开文件

O_CREAT:与O_EXCL结合使用,用于专门创建文件,在 flags 参数同时使用到了 O_CREAT 和O_EXCL 标志的情况下,如果 pathname 参数指向的文件已经存在,则 open 函数返回错误。

3)mode**:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。权限对于文件来说是一个很重要的属性,那么在 Linux系统中,我们可以通过 touch 命令新建一个文件,此时文件会有一个默认的权限,如果需要修改文件权限,可通过 chmod 命令对文件权限进行修改,譬如在 Linux 系统下我们可以使用"ls -l"命令来查看到文件所对应的权限。

(2)open函数文件权限宏:

S_IRUSR 允许文件所属者读文件

S_IWUSR 允许文件所属者写文件

S_IXUSR 允许文件所属者执行文件

S_IRWXU 允许文件所属者读、写、执行文件

S_IRGRP 允许同组用户读文件

S_IWGRP 允许同组用户写文件

S_IXGRP 允许同组用户执行文件

S_IRWXG 允许同组用户读、写、执行文件

S_IROTH 允许其他用户读文件

S_IWOTH 允许其他用户写文件

S_IXOTH 允许其他用户执行文件

S_IRWXO 允许其他用户读、写、执行文件

S_ISUID set-user-ID(特殊权限)

S_ISGID set-group-ID(特殊权限)

S_ISVTX sticky(特殊权限)

4)返回值

成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1。

open函数使用示例:

/*oepn函数使用*/

//打开一个已经存在的文件-只读方式
int fd = open("./app.c",O_RDONLY);
if(-1 == fd)
        return fd;

//打开一个已经存在的文件-可读可写方式
int fd = open("./app.c",O_RDWR);
if(-1 == fd)
        return fd;

//打开一个绝对路径下的指定文件,使用可读可写,
//如果该文件是一个符号链接文件,则不对其进行解引用
//直接返回错误
int fd = open("/home/xxl/hello",O_RDWR|O_NOFOLLOW);
if(-1 == fd)
        return fd;

//打开指定文件,文件不存在就创建,创建时:
//设置:文件所属拥有读写执行权限
//同组用户和其他用户只有读权限
//使用可读可写方式打开
int fd = open("/home/xxl/test",O_RDWR|O_CREAT,S_IRWXU|S_IRGRP|S_IROTH);
if(-1 == fd)
        return fd;
5.write函数

调用 write 函数可向打开的文件写入数据,其函数原型如下所示

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

函数参数和返回值含义如下:

fd**:**文件描述符,我们需要将进行写操作的文件所对应的文件描述符传递给 write 函数。

buf**:**指定写入数据对应的缓冲区。

count**:**指定写入的字节数。

返回值:如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

读写操作默认情况下当前位置偏移量一般是 0,也就是指向了文件起始位置

6.read读文件函数

调用 read 函数可从打开的文件中读取数据,其函数原型如下所示

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

首先使用 read 函数需要先包含 unistd.h 头文件。

函数参数和返回值含义如下:

fd**:文件描述符。与 write 函数的 fd 参数意义相同。

buf**:指定用于存储读取数据的缓冲区。

count**:指定需要读取的字节数。

返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节数,也有可能会为 0

7.close关闭文件函数

可调用 close 函数关闭一个已经打开的文件,其函数原型如下所示

#include <unistd.h>
int close(int fd);

首先使用 close 函数需要先包含 unistd.h 头文件,当我们对文件进行 IO 操作完成之后,后续不再对文件

进行操作时,需要将文件关闭。

函数参数和返回值含义如下:

fd**:**文件描述符,需要关闭的文件所对应的文件描述符。

返回值:如果成功返回 0,如果失败则返回-1。除了使用 close 函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件,也就是说在我们的程序中打开了文件,如果程序终止退出时没有关闭打开的文件,那么内核会自动将程序中打开的文件关闭。很多程序都利用了这一功能而不显式地用 close 关闭打开的文件。

8.lseek函数

读写偏移量:对于每个打开的文件,系统都会记录它的读写位置偏移量,我们也把这个读写位置偏移量称为读写偏移量,记录了文件当前的读写位置,当调用 read()或 write()函数对文件进行读写操作时,就会从当前读写位置偏移量开始进行数据读写。

lseek 函数的原型,如下所示

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

函数参数和返回值含义如下:

fd**:文件描述符。

offset**:偏移量,以字节为单位。

whence**:用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):

宏定义:

SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);

SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移

SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移。

返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生错误将返回-1。

使用示例:

//使用示例
//1)将读写位置移动到文件开头处
off_t off = lseek(fd, 0,SEEK_SET);
if(-1 == off)
        return -1;

//2) 将读写位置移动到文件末尾
off_t off = lseek(fd,0,SEEK_END);
if(-1 == off)
        return -1;
//3) 将读写位置移动到偏移文件开头100个字节处
off_t off = lseek(fd,100,SEEK_SET);
if(-1 == off)
        return -1;
//4) 获取当前位置读写偏移量
off_t off = lseek(fd,0,SEEK_CUR);
if(-1 == off)
        return -1;
9.文件操作练习

(1)打开一个已经存在的文件(例如 src_file),使用只读方式;然后打开一个新建文件(例如 dest_file),

使用只写方式,新建文件的权限设置如下:

文件所属者拥有读、写、执行权限;

同组用户与其他用户只有读权限。

从 src_file 文件偏移头部 500 个字节位置开始读取 1Kbyte 字节数据,然后将读取出来的数据写入到

dest_file 文件中,从文件开头处开始写入,1Kbyte 字节大小,操作完成之后使用 close 显式关闭所有文件,然后退出程序。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <unistd.h>
#include <stdio.h>
int main(void)
{
    char buff[1024];
    int fd1,fd2,fd3,fd4;
    int ret;
    //(1)
    /*只读方式打开一个已经存在的文件*/
    fd1 = open("./src_file.txt",O_RDONLY);
    if(-1 == fd1) {
        //增加调试信息
        perror("open error:");
        exit(-1);
    }
//一定要有全局意识,因为这是对文档的连续操作
//以只写方式打开文件,O_CREAT|O_EXCL配合使用,用于专门创建文件
//S_IRGRP|S_IROTH|S_IRWXU文件所属者拥有读,写,执行权限,同组用户以及其他用户拥有读权限
    fd2 = open("./dest_file",O_WRONLY|O_CREAT|O_EXCL,S_IRGRP|S_IROTH|S_IRWXU);
    if(-1 == fd2) {
        //增加调试信息
       perror("open error:");
       close(fd1);
       exit(-1);
    }
    
    //移动位置,到偏移头部500个字节位置
    ret = lseek(fd1,500,SEEK_SET);
    if(-1 == ret) {
        perror("lseek  head 500 error:");
        goto out2;
    }
    //读取1Kb字节数据
    ret = read(fd1,buff,sizeof(buff));
    if(-1 == ret) {
        perror("read error:");
        //printf("error:read src_file failed\n");
        goto out2;
    }else if (ret == 0) {
        fprintf(stderr,"read error:src_file has no data to read\n");
        goto out2;
    }

    //将dest_file文件读写位置移动到文件头
    ret = lseek(fd2,0,SEEK_SET);
    if(-1 == ret){
        perror("lseek file head error");
        goto out2;
    }

    /*将buffer中的数据写入dets_file文件大小为1KBytes*/
    ret = write(fd2,buff,sizeof(buff));
    if(-1 == ret) {
        perror("write error");
        goto out2;
    }
    printf("OK: test successfully!\n");
    close(fd2);
    close(fd1);
    exit(0);

    out2:
    close(fd2);
    close(fd1);
    exit(-1);

(2)通过 open 函数判断文件是否存在(例如 test_file),并将判断结果显示出来。

const char *filename = "test_file";
  fd3 = open(filename,O_RDONLY);
  if(fd3 == -1) {
        printf("文件 '%s'不存在\n",filename);
  } else {
        printf("文件 '%s'存在\n",filename);
  }

(3)新建一个文件(例如 new_file),新建文件的权限设置为:

文件所属者拥有读、写、执行权限;同组用户与其他用户只有读权限。

使用只写方式打开文件,将文件前 1Kbyte 字节数据填充为 0x00,将下 1Kbyte 字节数据填充为 0xFF,

操作完成之后显式关闭文件,退出程序。

fd3 = open("./new_file",O_WRONLY|O_CREAT|O_EXCL,S_IRWXU|S_IRGRP|S_IROTH);
if(fd3 == -1) {
    perror("open error");
    exit(-1);
}
ret = lseek(fd3,0,SEEK_SET);
//memset 是 C 标准库中的一个函数,用于将一块内存区域的内容设置为指定的值。
//它通常用于初始化数组或缓冲区。
//       #include <string.h>
//函数原型
//void *memset(void *s, int c, size_t n);
//从左到右,参数代表:要填充的内存区域的指针;要设置的值;要设置的字节数,即内存区域的大小
memset(buff,0x00,sizeof(buff));
ret = write(fd3,buff,sizeof(buff));
if(ret != sizeof(buff)) {
    perror("write 0x00 error");
    close(fd3);
    exit(-1);
}
​
ret = lseek(fd3,1024,SEEK_SET);
if(ret == -1) {
    perror("lseek error");
    close(fd3);
    exit(-1);
}
memset(buff,0xFF,sizeof(buff));
ret = write(fd3,buff,sizeof(buff));
if(ret != sizeof(buff)) {
    perror("write 0xFF error:");
    close(fd3);
    exit(-1);
}
close(fd3);
printf("文件操作完成");
exit(0);

(4)打开一个已经存在的文件(例如 test_file),通过 lseek 函数计算该文件的大小,并打印出来。

  fd4 = open("./new_file",O_RDONLY);
    if(fd4 == -1) {
        perror("打开文件失败");
        goto error4;
    }
    off_t size = lseek(fd4,0,SEEK_END);
    if(size == -1){
        perror("lseek 失败");
        close(fd4);
        return 1;
    }
    printf("文件大小为:%ld 字节\n",size);
    close(fd4);
    error4:
            return 1;

二、文件I/O深入

1.linux系统如何管理文件
1.1静态文件与inode

文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U 盘等外部存储设备,文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件

文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。

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

结构图如下图:

inode table 表本身也需要占用磁盘的存储空间。每一个文件都有唯一的一个 inode,每一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode。在 Linux 系统下,我们可以通过"ls -i"命令查看文件的 inode 编号

通过以上介绍可知,打开一个文件,系统内部会将这个过程分为三步:

\1) 系统找到这个文件名所对应的 inode 编号;

\2) 通过 inode 编号从 inode table 中找到对应的 inode 结构体;

\3) 根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。

1.2文件打开时的状态

1)文件打开&&读写的过程

调用 open 函数去打开文件

内核申请一段内存(一段缓冲区)并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。

打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件

对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。

2)这样做的原因

因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比磁盘读写快得多。

3)进程控制块

在 Linux 系统中,内核会为每个进程(可以简单地理解为一个运行的程序就是一个进程,运行了多个程序那就是存在多个进程)设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写

PCB)

PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:

inode 数据结构体中的元素会记录该文件的数据存储的 block(块),也就是说可以通过 inode 找到文件数据存在在磁盘设备中的那个位置,从而把文件数据读取出来。

2.返回错误处理
2.1errno

errno :本质上是一个 int 类型的变量,用于存储错误编号

使用示例:

#include <stdio.h>
#include <errno.h> 
int main(void)
 {
    printf("%d\n",errno);
    return 0;
 }
2.2strerror

strerror:该库函数可以将对应的errno转化成我们查看的字符串信息

函数原型:

char *strerror(int errnum)

使用示例:

#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("./test_file",O_RDONLY);
    if(fd == -1) {
        printf("Error: %s\n",strerror(errno));
        return -1;
    }
​
    close(fd);
    return 0;
}
2.3perror函数-重要

该函数常用来打印错误信息

函数原型:

#include <stdio.h>
void perror(const char *s);

函数参数和返回值含义如下:

s**:在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。

返回值:void 无返回值。

使用示例:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
​
int main(void) {
    int fd;
    fd = open("./test_file",O_RDONLY);
    if(fd == -1) {
        perror("open error");
        return -1;
    }
    close(fd);
    return 0;
}
2.4exit函数

在 Linux 系统下,进程(程序)退出可以分为正常退出和异常退出,注意这里说的异常并不是执行函数出现了错误这种情况,异常往往更多的是一种不可预料的系统异常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等

exit()函数_exit()函数都是用来终止进程的,exit()是一个标准 C 库函数,而 __ exit()和 __ Exit() 是系统调用。执行 exit()会执行一些清理工作,最后调用__ exit()函数。

函数原型如下:

#include<stdlib.h>
void exit(int status);

一般来说:exit(0)表示正常退出;exit(-1)表示异常退出

3.空洞文件

譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调

用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,然后使用 write()函数从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件

空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小

实际中空洞文件的两个应用场景:

在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势;

在创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,但其实系统安装完成之后,开始也不过只用了 3、4G 的磁盘空间,如果一开始就把 100G 分配出去,资源是很大的浪费。

代码测试:

使用 open 函数新建了一个文件 hole_file,在 Linux 系统中,新建文件大小是 0,也就是没有任何数据写入,此时使用lseek函数将读写偏移量移动到4K字节处,再使用write函数写入数据0xFF,每次写入 1K,一共写入 4 次,也就是写入了 4K 数据,也就意味着该文件前 4K 是文件空洞部分,而后 4K数据才是真正写入的数据。

#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("./holefile",O_WRONLY|O_CREAT|O_EXCL,
                            S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    if(fd == -1) {
        perror("open erro:");
        exit(-1);
    }
    /* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
    ret = lseek(fd,4096,SEEK_SET);
    if(ret == -1) {
        perror("lseek error");
        goto err;
    }
    memset(buffer,0xFF,sizeof(buffer));
​
    for(i = 0;i<4;i++) {
        ret = write(fd,buffer,sizeof(buffer));
        if(ret == -1) {
            perror("write error:");
            goto err;
        }else{
            printf("write successfully!");
        }
    }
    ret = 0;
    err:
        close(fd);
        exit(ret);
}

使用 ls 命令查看到空洞文件的大小是 8K,使用 ls 命令查看到的大小是文件的逻辑大小,自然是包括了空洞部分大小和真实数据部分大小;当使用 du 命令查看空洞文件时,其大小显示为 4K,du 命令查看到的大小是文件实际占用存储块的大小。

Q1:若使用 read 函数读取文件空洞部分,读取出来的将会是什么?

在读取一个包含空洞(sparse file)的文件时,如果使用 read 函数读取文件的空洞部分,读取出来的内容将是 零字节 (0x00)。这是因为文件系统在处理稀疏文件时,不会为空洞部分分配实际的磁盘存储空间,而是将这些区域逻辑上视为填充零字节

4.O_APPEND和O_TRUNC标志

1)O_APPEND标志:

使用该标志,调用open函数打开文件的时候会将文件原本的内容全部丢弃 文件大小变为0

示例代码:

#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
​
int main(void) {
    int fd;
    fd = open("./src_file.txt",O_RDONLY|O_TRUNC);
    if(fd == -1) {
        perror("open error");
        close(fd);
        exit(-1);
    }
    close(fd);
    exit(0);
}

shell指令:“ls -l查看文件状态”

2)O_TRUNC标志:

加入该标志每次都是从文件末尾开始写数据的

测试代码:

测试代码中会去打开当前目录下的 test_file 文件,使用可读可写方式,并且使用了O_APPEND 标志,前面笔者给大家提到过,open 打开一个文件,默认的读写位置偏移量会处于文件头,但测试代码中使用了O_APPEND 标志,如果 O_APPEND 确实能生效的话,也就意味着调用 write 函数会从文件末尾开始写;代码中写入了 4 个字节数据,都是 0x55,之后,使用 lseek 函数将位置偏移量移动到距离文件末尾 4 个字节处,读取 4 个字节(也就是读取文件最后 4 个字节数据),之后将其打印出来,如果上面笔者的描述正确的话,打印出来的数据就是我们写入的数据,如果 O_APPEND 不能生效,则打印出来数据就不会是 0x55

int main(void) {
    char buffer[16];
    int fd;
    int ret;
​
    fd = open("./src_file.txt",O_RDWR|O_APPEND);
    if(fd == -1) {
        perror("open error");
        exit(-1);
    }
​
    memset(buffer,0x55,sizeof(buffer));
​
    //细节1:使用O_APPEND标志,即使是通过lseek函数也是无法修改写文件时对应的位置偏移量
    //注意不包括读哦,
    //细节2:使用该标志时,不会影响读的偏移量,默认仍然是文件头
    //测试代码
    ret = lseek(fd,0,SEEK_SET);
    
    ret = write(fd,buffer,sizeof(buffer));
    if(ret == -1) {
        perror("write error:");
        goto err;
    }
​
    memset(buffer,0x00,sizeof(buffer));
​
    ret = lseek(fd,-4,SEEK_END);
    if(ret == -1) {
        perror("lseek error");
        goto err;
    }
​
    ret = read(fd,buffer,sizeof(buffer));
    if(ret == -1) {
        perror("read error:");
        goto err;
    }
    printf("0x%x 0x%x 0x%x 0x%x\n",buffer[0],buffer[1],buffer[2],buffer[3]);
    ret = 0;
​
    err:
    close(fd);
    exit(-1);
}
5.多次打开同一个文件

同一个文件可以被多次打开,譬如在一个进程中多次打开同一个文件、在多个不同的进程中打开同一个文件

1)一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd**,同理在关闭文件的时候也需要调用** close 依次关闭各个文件描述符

测试代码:

int main(void) {
    int fd1,fd2,fd3;
    int ret;
​
    /*第一次打开文件*/
    fd1 = open("./src_file.txt",O_RDWR);
    if(fd1 == -1) {
        perror("open error1:");
        exit(-1);
    }
​
       /*第二次打开文件*/
    fd2 = open("./src_file.txt",O_RDWR);
    if(fd2 == -1) {
        perror("open error2:");
        ret = -1;
        goto err1;
    }
​
       /*第三次打开文件*/
    fd3 = open("./src_file.txt",O_RDWR);
    if(fd3 == -1) {
        perror("open error3:");
        ret = -1;
        goto err2;
    }
​
    printf("%d %d %d\n",fd1,fd2,fd3);
​
    close(fd3);
    ret = 0;
​
    err2: 
    close(fd2);
    err1:
    close(fd1);
    exit(-1);
}

调用 open 函数打开文件使用的是什么权限,则返回的文件描述符就拥有什么权限,文件 IO 操作完成之后,在结束进程之前需要使用 close 关闭各个文件描述符。

2)多次打开一个文件的时候,内存中也只有一个动态文件

测试代码:

fd1写入4个字节数据,从文件头开始写,fd2读取四个字节数据,如果有多个的话,读取的应该是空(因为fd2对应的文件大小是0)否则就是写入的字节数据

详细:当前目录下不存在 test_file 文件,上述代码中,第一次调用 open 函数新建并打开 test_file 文件,第二次调用 open 函数再次打开它,新建文件时,文件大小为 0;首先通过文件描述符 fd1 写入 4 个字节数据(0x11/0x22/0x33/0x44),从文件头开始写;然后再通过文件描述符 fd2 读取 4 个字节数据,也是从文件头开始读取。假如,内存中只有一份动态文件,那么读取得到的数据应该就是 0x11、0x22、0x33、0x44,如果存在多份动态文件,那么通过 fd2 读取的是与它对应的动态文件中的数据,那就不是 0x11、0x22、0x33、0x44,而是读取出 0 个字节数据,因为它的文件大小是 0。

/*2.多次打开一个文件的时候,内存中也只有一个动态文件
实验:fd1写入4个字节数据,从文件头开始写
fd2读取四个字节数据,如果有多个的话,读取的应该是空
否则就是写入的字节数据*/
int main(void) {
    int fd1,fd2;
    int ret;
    char buffer[4];
    
    /*创建新文件test_file并且打开*/
    fd1 = open("./test_file",O_RDWR|O_CREAT|O_EXCL,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    if(fd1 == -1) {
        perror("open error:");
        exit(-1);
    }
    
    /*再次打开test_file文件*/
    fd2 = open("./test_file",O_RDWR);
    if(fd2 == -1) {
        perror("open error2:");
        ret = -1;
        goto err1;
    }
​
    /*向fd1文件描述符写入4个字节数据*/
    buffer[0] = 0x11;
    buffer[1] = 0x22;
    buffer[2] = 0x33;
    buffer[3] = 0x44;
​
    ret = write(fd1,buffer,sizeof(buffer));
    if(ret == -1) {
        perror("write error");
        goto err2;
    }
    /*3.一个进程内多次open打开同一个文件,不同文件描述符之间所对应的读写
    位置偏移量是相互独立的
    实验:将lseek注释掉之后,如果不是独立的,此时应该是从四个字节后开始
    但是实际上还是从文件头开始的*/
    /*移动到文件开头处*/
    // ret = lseek(fd2,0,SEEK_SET);
    // if(ret == -1){
    //     perror("lseek error:");
    //     goto err2;
    // }
​
   // memset(buffer,0x00,sizeof(buffer));
    /*读取数据*/
    ret = read(fd2,buffer,4);
    if(ret == -1) {
        perror("read error:");
        goto err2;
    }
    printf("0x%x 0x%x 0x%x 0x%x\n",buffer[0],buffer[1],buffer[2],buffer[3]);
    close(fd2);
    close(fd1);
    ret = 0;
    exit(ret);
    
    err2:
    close(fd2);
    err1:
    close(fd1);
    exit(ret);
}

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

多个不同的进程中调用 open()打开磁盘中的同一个文件,同样在内存中也只是维护了一份动态文件,多个进程间共享,它们有各自独立的文件读写位置偏移量

6.多次打开同一个文件-进行读操作与O_APPEND标志

重复打开同一个文件,进行写操作,譬如一个进程中两次调用 open 函数打开同一个文件,分别得到两个文件描述符 fd1 和 fd2,使用这两个文件描述符对文件进行写入操作,那么它们是分别写(各从各的位置偏移量开始写),如果在文件打开的时候加入O_APPEND就是接续写了(一个写完,另外一个接着后面写)。

测试代码:

重复两次打开 test_file 文件,分别得到两个文件描述符 fd1、fd2;首先通过 fd1 写入 4 个字节数据(0x11、0x22、0x33、0x44)到文件中,接着再通过 fd2 写入 4 个字节数(0xaa、0xbb、0xcc、0xdd)到文件中,循环写入 4 此;最后再将写入的数据读取出来,将其打印到终端。如果它们是分别写,那么读取出来的数据就应该是 aabbccdd……,因为通过 fd1 写入的数据被 fd2 写入的数据给覆盖了;如果它们是接续写,那么读取出来的数据应该是 11223344aabbccdd

最后实验结果很显然是aabbccdd

open的时候加入O_APPEND,那么就是11223344aabbccdd

/*多次打开同一个文件的时候
两个文件描述符所对应的读写位置偏移量是相互独立的
所以是分别写*/
​
#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) {
    unsigned char buffer1[4] ,buffer2[4];
    int fd1,fd2;
    int ret;
    int i;
//不想被覆盖调很简单,只需要在打开文件的时候加上O_APPEND即可,便可以变成接续写
    fd1 = open("./test_file2",O_RDWR|O_CREAT|O_EXCL|O_APPEND,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    if(fd1 == -1) {
        perror("open error:");
        exit(-1);
    }
​
    fd2 = open("./test_file2",O_RDWR|O_APPEND);
    if(fd2 == -1) {
        perror("open error2:");
        ret = -1;
        goto err1;
    }
    buffer1[0] = 0x11;
    buffer1[1] = 0x22;
    buffer1[2] = 0x33;
    buffer1[3] = 0x44;
​
    buffer2[0] = 0xaa;
    buffer2[1] = 0xbb;
    buffer2[2] = 0xcc;
    buffer2[3] = 0xdd;
​
/*循环写入数据*/
/*这个就是分别写,第二个把第一个给覆盖掉了*/
    for(i = 0;i<4;i++) {
        ret = write(fd1,buffer1,sizeof(buffer1));
        if(ret == -1) {
            perror("write error1:");
            goto err2;
        }
​
        ret = write(fd2,buffer2,sizeof(buffer2));
        if(ret == -1) {
            perror("write error2:");
            goto err2;
        }
    }
    /*将读写位置偏移到文件开头*/
    ret = lseek(fd1,0,SEEK_SET);
    for(i = 0;i<2;i++) {
        ret = read(fd1,buffer1,sizeof(buffer1));
        if(ret == -1) {
            perror("read error1:");
            goto err2;
        }
        printf("%x%x%x%x",buffer1[0],buffer1[1],buffer1[2],buffer1[3]);
    }
    printf("\n");
    ret = 0;
​
​
err1:
close(fd1);
return ret;
err2:
close(fd2);
}
7.复制文件描述符

在 Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限,譬如使用旧的文件描述符对文件有读写权限,那么新的文件描述符同样也具有读写权限;在 Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制

复制得到的文件描述符与旧的文件描述符都指向了同一个文件表,假设 fd1 为原文件描述符,fd2 为复制得到的文件描述符,如下图所示:

可知道“复制”的含义实则是复制文件表。同样,在使用完毕之后也需要使用 close 来关闭文件描述符。

7.1dup函数

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

#include <unistd.h>
int dup(int oldfd);

函数参数和返回值含义如下:

oldfd**:需要被复制的文件描述符。

返回值:成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;如果复制失败将返回-1,并且会设置 errno 值。

复制得到的文件描述符与原文件描述符都指向同一个文件表,所以它们的文件读写偏移量是一样的,因此可以用dup实现接续写

测试代码:

#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) {
    unsigned char buffer1[4] ,buffer2[4];
    int fd1,fd2;
    int ret;
    int i;
    fd1 = open("./test_file3",O_RDWR|O_CREAT|O_EXCL,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    if(fd1 == -1) {
        perror("open error:");
        exit(-1);
    }
​
    fd2 = dup(fd1);
    if(fd2 == -1) {
        perror("open error2:");
        ret = -1;
        goto err1;
    }
​
    printf("fd1:%d\nfd2:%d\n",fd1,fd2);
​
    buffer1[0] = 0x11;
    buffer1[1] = 0x22;
    buffer1[2] = 0x33;
    buffer1[3] = 0x44;
​
    buffer2[0] = 0xaa;
    buffer2[1] = 0xbb;
    buffer2[2] = 0xcc;
    buffer2[3] = 0xdd;
​
/*循环写入数据*/
/*这个就是分别写,第二个把第一个给覆盖掉了*/
    for(i = 0;i<4;i++) {
        ret = write(fd1,buffer1,sizeof(buffer1));
        if(ret == -1) {
            perror("write error1:");
            goto err2;
        }
​
        ret = write(fd2,buffer2,sizeof(buffer2));
        if(ret == -1) {
            perror("write error2:");
            goto err2;
        }
    }
    /*将读写位置偏移到文件开头*/
    ret = lseek(fd1,0,SEEK_SET);
    if(ret == -1) {
        perror("lseek error");
        goto err2;
    }
​
    for(i = 0;i<2;i++) {
        ret = read(fd1,buffer1,sizeof(buffer1));
        if(ret == -1) {
            perror("read error1:");
            goto err2;
        }
        printf("%x%x%x%x",buffer1[0],buffer1[1],buffer1[2],buffer1[3]);
    }
    printf("\n");
    ret = 0;
    exit(ret);
​
​
err1:
close(fd1);
exit(ret);
err2:
close(fd2);
exit(ret);
}

输出结果:

7.2dup2函数

dup2 系统调用修复了(dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件描述符)这个缺陷,可以手动指定文件描述符,而不需要遵循文件描述符分配原则

#include <unistd.h>
int dup2(int oldfd, int newfd);

函数参数和返回值含义如下:

oldfd**:需要被复制的文件描述符。

newfd**:指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。

返回值:成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返回-1,并且会设置 errno 值。

测试代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
/*dup2可以手动制定文件描述符,不需要遵循文件描述符分配规则
函数原型:
int dup2(int oldfd,int newfd)
oldfd指的是要复制的文件描述符
newfd指的是新分配的文件描述符*/
int main(void) {
    int fd1,fd2;
    int ret;
​
    fd1 = open("./test_file4",O_RDWR|O_CREAT|O_EXCL,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    if(fd1 == -1) {
        perror("open error");
        exit(-1);
    }
​
    fd2 = dup2(fd1,1000);
    if(fd2 == -1) {
        perror("open error2:");
        ret = -1;
        goto err1;
    }
​
    printf("fd1:%d\nfd2:%d\n",fd1,fd2);
    close(fd2);
    ret = 0;
    exit(ret);
    err1:
        close(fd1);
        exit(ret);
}

输出结果:

8.文件共享

文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被多个独立的读写体(理解为对应于同一个文件的多个不同的文件描述符,譬如多次打开同一个文件所得到的多个不同的 fd)同时进行 IO 操作。

进行 IO 操作指的是一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件,例如同一个文件对应两个不同的文件描述符 fd1 和 fd2,当使用 fd1 对文件进行写操作之后,并没有关闭 fd1,而此时使用 fd2 对文件再进行写操作,这其实就是一种文件共享。

文件共享的意义有很多,多用于多进程或多线程编程环境中,譬如我们可以通过文件共享的方式来实现多个线程同时操作同一个大文件,以减少文件读写时间、提升效率。

文件共享的核心是:如何制造出多个不同的文件描述符来指向同一个文件。

常见的三种文件共享的实现方式

(1) 同一个进程中多次调用 open 函数打开同一个文件,各数据结构之间的关系如下图所示:

这种情况非常简单,多次调用 open 函数打开同一个文件会得到多个不同的文件描述符,并且多个文件描述符对应多个不同的文件表,所有的文件表都索引到了同一个 inode 节点,也就是磁盘上的同一个文件。

(2)*不同进程中分别使用 open 函数打开同一个文件,其数据结构关系图如下所示:

进程 1 和进程 2 分别是运行在 Linux 系统上两个独立的进程(理解为两个独立的程序),在他们各自的程序中分别调用 open 函数打开同一个文件,进程 1 对应的文件描述符为 fd1,进程 2 对应的文件描述符为fd2,fd1 指向了进程 1 的文件表 1,fd2 指向了进程 2 的文件表 2;各自的文件表都索引到了同一个 inode 节点,从而实现共享文件。

(3)同一个进程中通过dup(dup2)函数对文件描述符进行复制,其数据结构关系如下图所示:

9.原子操作与竞争冒险
9.1竞争冒险

竞争冒险(Race Condition) 是指多个线程或进程在并发运行时,因对共享资源的访问和操作顺序不确定而导致程序行为异常或不可预期的问题。

例如:

假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志,此时,各数据结构之间的关系如图 3.8.2 所示。每个进程都有它自己的进程控制块 PCB,有自己的文件表(意味着有自己独立的读写位置偏移量),但是共享同一个 inode 节点(也就是对应同一个文件)。假定此时进程 A 处于运行状态,B 未处于等待运行状态,进程 A 调用了lseek 函数,它将进程 A 的该文件当前位置偏移量设置为 1500 字节处(假设这里是文件末尾)刚好此时进程 A 的时间片耗尽,然后内核切换到了进程 B,进程 B 执行 lseek 函数,也将其对该文件的当前位置偏移量设置为 1500 个字节处(文件末尾)。然后进程 B 调用 write 函数,写入了 100 个字节数据,那么此时在进程 B 中,该文件的当前位置偏移量已经移动到了 1600 字节处。B 进程时间片耗尽,内核又切换到了进程 A,使进程 A 恢复运行,当进程 A 调用 write 函数时,是从进程 A 的该文件当前位置偏移量(1500 字节处)开始写入,此时文件 1500 字节处已经不再是文件末尾了,如果还从 1500字节处写入就会覆盖进程 B 刚才写入到该文件中的数据。

如图:

9.2原子操作

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

常见的原子操作如下:

1)O_APPEND 实现原子操作

当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作。加入 O_APPEND 标志后,不管怎么写入数据都会是从文件末尾写,这样就不会导致出现“进程 A 写入的数据覆盖了进程 B 写入的数据”这种情况了

2)pread()**和** pwrite()

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

函数原型如下:

#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);

函数参数和返回值含义如下:

fd**、buf、count** 参数与 read 或 write 函数意义相同。

offset**:表示当前需要进行读或写的位置偏移量。

返回值:返回值与 read、write 函数返回值意义一样。

虽然 pread(或 pwrite)函数相当于 lseek 与 pread(或 pwrite)函数的集合,但还是有下列区别:

1.调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);

2.不更新文件表中的当前位置偏移量

测试代码:

在当前目录下存在一个文件 test_file,上述代码中会打开 test_file 文件,然后直接使用 pread 函数读取100 个字节数据,从偏移文件头部 1024 字节处,读取完成之后再使用 lseek 函数获取到文件当前位置偏移量,并将其打印出来。假如 pread 函数会改变文件表中记录的当前位置偏移量,则打印出来的数据应该是1024 + 100 = 1124;如果不会改变文件表中记录的当前位置偏移量,则打印出来的数据应该是 0

int main(void) {
    unsigned char buffer[100];
    int fd;
    int ret;
​
    fd = open("./test_file",O_RDWR);
    if(fd == -1) {
        perror("open error");
        exit(-1);
    }
    /* 使用 pread 函数读取数据(从偏移文件头 1024 字节处开始读取) */
    ret = pread(fd,buffer,sizeof(buffer),1024);
    if(ret == -1) {
        perror("pread error");
        close(fd);
        exit(ret);
    }
​
    /*获取当前位置偏移量*/
    ret = lseek(fd,0,SEEK_CUR);
    if(ret == -1) {
        perror("lseek error");
        close(fd);
        exit(ret);
    }
    printf("Current Offset:%d\n",ret);
    ret = 0;
    exit(ret);
}

从上图中可知,打印出来的数据为 0,正如前面所介绍那样,pread 函数确实不会改变文件表中记录的当前位置偏移量;同理,pwrite 函数也是如此。

3)创建一个文件

当 open 函数中同时指定了 O_EXCL 和O_CREAT 标志,如果要打开的文件已经存在,则 open 返回错误;如果指定的文件不存在,则创建这个文件,这里就提供了一种机制,保证进程是打开文件的创建者,将“判断文件是否存在、创建文件”这两个步骤合成为一个原子操作

10.fcntl和ioctl
10.1fcntl函数

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

函数原型如下:

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(获取文件状态标志)将返回文件状态标志等。

fcntl 使用示例

(1)**复制文件描述符**

前面给大家介绍了 dup 和 dup2,用于复制文件描述符,除此之外,我们还可以通过 fcntl 函数复制文件描述符, 可用的 cmd 包 括 F_DUPFD 和 F_DUPFD_CLOEXEC , 这 里 就 只 介 绍 F_DUPFD ,F_DUPFD_CLOEXEC 暂时先不讲。当 cmd=F_DUPFD 时,它的作用会根据 fd 复制出一个新的文件描述符,此时需要传入第三个参数,第三个参数用于指出新复制出的文件描述符是一个大于或等于该参数的可用文件描述符(没有使用的文件描述符);如果第三个参数等于一个已经存在的文件描述符,则取一个大于该参数的可用文件描述符。

测试代码:

int main(void) {
    int fd1,fd2;
    int ret;
​
    fd1 = open("./test_file",O_RDWR);
    if(fd1 == -1) {
        perror("open error");
        exit(-1);
    }
​
    fd2 = fcntl(fd1,F_DUPFD,0);
    if(fd2 == -1) {
        perror("fcntl error");
        ret = -1;
        close(fd1);
        exit(ret);
    }
​
    printf("fd1:%d\nfd2:%d\n",fd1,fd2);
​
    ret = 0;
    close(fd2);
    exit(ret);
}

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

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

这些标志指的就是我们在调用 open 函数时传入的 flags 标志,可以指定一个或多个(通过位或 | 运算符组合),但是文件权限标志(O_RDONLY、O_WRONLY、O_RDWR)以及文件创建标志(O_CREAT、O_EXCL、O_NOCTTY、O_TRUNC)不能被设置、会被忽略;在 Linux 系统中,只有 O_APPEND、O_ASYNC、O_DIRECT、O_NOATIME 以及 O_NONBLOCK 这些标志可以被修改

测试代码:

设置O_APPEND标志

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

int main(void) {
    int fd;
    int ret;
    int flag;
    
/* 打开文件 test_file */
    fd = open("./test_file",O_RDWR);
    if(fd == -1) {
        perror("open error:");
        exit(-1);
    }
/*获取文件标志*/
    flag = fcntl(fd,F_GETFL);
    if(flag == -1) {
        perror("fcntl F_GETFL error:");
        ret = -1;
        close(fd);
        exit(ret);
    }
    printf("flags:0x%x\n",flag);
/* 设置文件状态标志,添加 O_APPEND 标志 */
    ret = fcntl(fd,F_SETFL,flag|O_APPEND);
    if(ret == -1) {
        perror("fcntl F_SETFL error:");
        ret = -1;
        close(fd);
        exit(ret);
    }
    close(fd);
    exit(0);
}
10.2ioctl函数

octl()可以认为是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设,此函数将会在进阶篇中使用到,譬如可以通过 ioctl 获取 LCD 相关信息等

函数原型:

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...)

函数参数和返回值含义如下:

fd**:文件描述符。

request**:此参数与具体要操作的对象有关,没有统一值,表示向文件描述符请求相应的操作;后面用到的时候再给大家介绍。

...**:此函数是一个可变参函数,第三个参数需要根据 request 参数来决定,配合 request 来使用。

返回值:成功返回 0,失败返回-1。

11.截断文件

系统调用 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 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字节"\0"。

使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必须要具有可写权限,也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。调用这两个函数并不会导致文件读写位置偏移量发生改变,所以截断之后一般需要重新设置文件当前的读写位置偏移量,以免由于之前所指向的位置已经不存在而发生错误(譬如文件长度变短了,文件当前所指向的读写位置已不存在)。调用成功返回 0,失败将返回-1,并设置 errno 以指示错误原因。

使用示例:

分别使用 ftruncate()和 truncate()将当前目录下的文件 file1 截断为长度 0、将文件 file2 截断为长度 1024 个字节。

#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;
​
    fd = open("./test_file",O_RDWR);
    if(fd < 0) {
        perror("open error");
        exit(-1);
    }
/* 使用 ftruncate 将 file1 文件截断为长度 0 字节 */
    if(ftruncate(fd,0)<0) {
        perror("ftruncate error:");
        close(fd);
        exit(-1);
    }
/* 使用 truncate 将 file2 文件截断为长度 01024字节 */
    if(truncate("./test_file2",1024)<0) {
        perror("truncate error");
        close(fd);
        exit(-1);
    }
​
    close(fd);
    exit(0);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值