文件IO 第二天 (文件IO)

一、Linux文件I/O概述

1、POSIX规范

    POSIX(Portable Operating System Interface,可移植操作系统接口规范)标准最初由IEEE(Institute of Electrical and Electronics Engineers,电气和电子工程师协会,是目前最大的全球性非营利性专业技术学会)制定,目的是提高UNIX环境下程序的可移植性。通俗来讲,为一个兼容POSIX标准的操作系统编写的应用程序,可以在任何其他兼容POSIX标准的操作系统上编译执行而无需修改代码。常见的Linux与UNIX系统都支持POSIX标准。

2、虚拟文件系统VFS

    Linux系统的一个成功的关键因素是它具有与其他操作系统共存的能力。Linux的文件系统由两层结构搭建:上面的虚拟文件系统VFS(Virtual File System),和下面的各种不同的具体文件系统(例如Ext、FAT32、NFS等)。

VFS将各种具体的文件系统的公共部分抽取出来形成一个抽象层,位于用户的程序与具体需要使用的系统中间,并提供系统调用接口。这样我们只需针对VFS提供的系统调用进行文件操作而无需具体考虑底层细节。VFS屏蔽了用户对底层细节的描述使得编程简化。

可以使用指令:

cat /proc/filesystems

查看当前操作系统支持哪些具体文件系统。

3、文件与文件描述符

Linux操作系统是基于文件概念搭建起来的操作系统(“万物皆文件”),基于这一点,所有的I/O设备都可以直接当做文件来处理。因此操作普通文件的操作函数与操作设备文件的操作函数是相同的,这样大大简化了系统对不同设备、不同文件的处理,提高了效率。

那么对于内核而言,内核是如何区分不同的文件呢?内核使用文件描述符来索引打开的文件。文件描述符是一个非负整数,每当打开一个存在的文件或创建一个新文件的时候,内核会向进程返回一个文件描述符,当对文件进行相应操作的时候,使用文件描述符作为参数传递给相应的函数。

通常一个进程启动时,都会打开三个流:标准输入、标准输出、标准错误输出,这三个流的文件描述符分别是0、1、2,对应的宏定义是STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。可以查看头文件unistd.h查看相关定义。

流的名称        文件描述符    宏定义

标准输入           0            STDIN_FILENO

标准输出           1            STDOUT_FILENO

标准错误输出    2            STDERR_FILENO

基于文件描述符的I/O操作虽然不能直接移植到诸如Windows系统等之外的操作系统上,但对于某些底层的I/O操作(例如驱动程序、网络连接等)是唯一的操作途径。

4、标准I/O与文件I/O的区别:

1.文件I/O又称为低级磁盘I/O,遵循POSIX标准。任何兼容POSIX标准的操作系统都支持文件I/O。标准I/O又称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境有标准C库,标准I/O就可以使用。

在Linux系统中使用GLIBC标准,它是标准C库的超集,既支持ANSI C中定义的函数又支持POSIX中定义的函数。因此Linux下既可以使用标准I/O,也可以使用文件I/O。

2.通过文件I/O读写文件时,每次操作都会执行相关系统调用。这样的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销。标准I/O在文件I/O的基础上封装了缓冲机制,每次先操作缓冲区,必要时再访问文件,从而减少了系统调用的次数。

3.文件I/O使用文件描述符打开操作一个文件,可以访问不同类型的文件(例如普通文件、设备文件和管道文件等)。而标准I/O使用FILE指针来表示一个打开的文件,通常只能访问普通文件。

二、文件I/O编程

1、打开文件

函数open()

    需要头文件:#include<sys/stat.h>

                         #include<fcntl.h>

    函数原型:int open(const char *pathname,int flags,int perms);

    函数参数:pathname:打开文件名(可以包含具体路径名)

                              flags:打开文件的方式,具体见下

                            perms:新建文件的权限,可以使用宏定义或者八进制文件权限码,具体见下

    函数返回值:成功:文件描述符

                        失败:-1

参数2flags具体可用参数(若使用多个flags参数可以使用|组合):

    O_RDONLY:以只读方式打开文件

    O_WRONLY:以只写方式打开文件

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

    O_CREAT:如果文件不存在,就创建这个文件,并使用参数3为其设置权限

    O_EXCL:如果使用O_CREAT创建文件时文件已存在则返回错误信息。使用这个参数可以测试文件是否已存在

    O_NOCTTY:若打开的是一个终端文件,则该终端不会成为当前进程的控制终端

    O_TRUNC:若文件存在,则删除文件中全部原有数据并设置文件大小为0

    O_APPEND:以添加形式打开文件,在对文件进行写数据操作时数据添加到文件末尾

    注意:O_RDONLY与O_WRONLY与O_RDWR三个参数互斥,不可同时使用

若在参数2的位置有多个参数进行组合,注意使用按位或(|)运算符。

/** 可查看/usr/include/i386-linux-gnu/bits/fcntl.h文件看到具体的宏定义 **/

参数3perms表示新建文件的权限,可以使用宏定义或八进制文件权限码。其中宏定义的格式是:S_I(R/W/X)(USR/GRP/OTH),其中R/W/X代表可读/可写/可执行,USR/GRP/OTH代表文件所有者/文件组/其他用户。例如:

S_IRUSR|S_IWUSR表示设置文件所有者具有可读可写权限,即0600。(一般情况下该参数都直接使用八进制文件权限码因为使用宏定义的形式太复杂)。

2、关闭文件

函数close()

    需要头文件:#include<unistd.h>

    函数原型:int close(int fd);

    函数参数:fd:文件描述符

    函数返回值:成功:0

                失败:-1

示例:使用open()与close()打开文件和关闭文件

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<sys/stat.h>

#include<fcntl.h>

int main()

{

    int fd;

    if((fd=open("hello.txt",O_RDWR|O_CREAT|O_TRUNC,0666))<0)

    {

        perror("fail to open file");

        exit(0);

    }

    close(fd);

    return 0;

}

练习:说明以下在标准I/O中打开文件的模式所对应的在文件I/O中的模式(即flags的参数组合),其中文件名使用命令行传参的形式

例:w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)

r

r+

w

w+

a

a+

答案:

r ----->  open(argv[1],O_RDONLY)

r+ ----> open(argv[1],O_RDWR)

w -----> open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666)

w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)

a -----> open(argv[1],O_WRONLY|O_CREAT|O_APPEND,0666)

a+ ----> open(argv[1],O_RDWR|O_CREAT|O_APPEND,0666)

3、文件读写

函数read()

    需要头文件:#include<unistd.h>

    函数原型:int read(int fd,void *buf,size_t count);

    函数参数:fd:文件描述符

                   buf:取出的数据存放的缓冲区(内存地址)

                count:指定读取的字节数

    函数返回值:成功:读到的字节数 0(表示文件已结尾)

                        失败:-1

函数write()

    需要头文件:#include<unistd.h>

    函数原型:ssize_t write(int fd,void *buf,size_t count);

    函数参数:fd:文件描述符

                   buf:写入的数据存放的缓冲区

               count:指定写入的字节数

    函数返回值:成功:已写的字节数

                        失败:-1

示例:使用read()和write()函数,先向文件中写入一些数据,之后读取出来

#include<stdio.h>

#include<stdlib.h>

#include<strings.h>

#include<unistd.h>

#include<sys/stat.h>

#include<fcntl.h>

#define MAX 128

int main(int argc,char *argv[])

{

    int fdread,fdwrite;

    char readbuffer[MAX]={0},writebuffer[MAX];    //相当于自己设置了两块缓冲区

    if(argc<2)

    {

        perror("arguments are too few");

        exit(0);

    }

    //先打开文件写入数据

    if((fdwrite=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)

    {

        perror("fail to open file");

        exit(0);

    }

    printf("请输入写入的内容:");

    scanf("%[^\n]",writebuffer);

    write(fdwrite,writebuffer,MAX);

    close(fdwrite);

    //再打开文件读取刚写入的内容

    int n=0,sum=0;

    if((fdread=open(argv[1],O_RDONLY))<0)

    {

        perror("fail to open file");

        exit(0);

    }

    while((n=read(fdread,readbuffer,MAX))>0)

    {

        sum += n;

        printf("%s",readbuffer);

        bzero(readbuffer,MAX);

    }

    printf("共读取到%d个字节\n",sum);

    close(fdread);

    return 0;

}

执行示例程序后查看文件可以发现文件除了写入的字符外,还有部分乱码字符。这是因为writebuffer[]数组没有初始化,存储了部分随机数,未被数据覆盖掉的部分也同时被写入了文件中。

思考:如何解决这个问题?

练习:使用文件I/O的read()/write()函数实现文件的复制

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<sys/stat.h>

#include<fcntl.h>

#define MAX 128

int main(int argc,char *argv[])

{

    int fdread,fdwrite;

    char buffer[MAX]={0};

    int n=0,sum=0;

    if(argc<3)

    {

        printf("arguments are too few, Usage:%s <src_file> <dst_file>\n",argv[0]);

        exit(0);

    }

    if((fdread=open(argv[1],O_RDONLY))<0)

    {

        perror("fail to open file");

        exit(0);

    }

    if((fdwrite=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)

    {

        perror("fail to open file");

        exit(0);

    }

    while((n=read(fdread,buffer,MAX))>0)

    {

        sum += n;

        write(fdwrite,buffer,n);

    }

    printf("复制文件成功,共操作%d字节\n",sum);

    close(fdread);

    close(fdwrite);

    return 0;

}

4、文件定位

函数lseek()

    需要头文件:#include<unistd.h>

                         #include<sys/types.h>

    函数原型:off_t lseek(int fd,off_t offset,int whence);

    函数参数:fd:文件描述符

               offset:相对于基准点whence的偏移量正数表示向前移动负数表示向后移动0表示不移动

            whence:基准点(取值同标准I/O内fseek()函数第三个参数)

    函数返回值:成功:当前读写位置

                        失败:-1

其中第三个参数whence的取值如下:

        SEEK_SET:代表文件起始位置,数字表示为0

        SEEK_CUR:代表文件当前的读写位置,数字表示为1

        SEEK_END:代表文件结束位置,数字表示为2

lseek()仅将文件的偏移量记录在内核内而不进行任何I/O操作。

注意:lseek()函数仅能操作常规文件,一些特殊的文件(例如socket文件、管道文件等)无法使用lseek()函数。

示例:读取文件的最后10个字节的数据

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<sys/stat.h>

#include<fcntl.h>

#define MAX 10

int main(int argc,char *argv[])

{

    int fd;

    char buffer[MAX]={0};

    int n=0,sum=0;

    if(argc<2)

    {

        printf("arguments are too few\n",argv[0]);

        exit(0);

    }

    if((fd=open(argv[1],O_RDONLY))<0)

    {

        perror("cannot open file");

        exit(0);3

    }

    lseek(fd,-10,SEEK_END);

    if(read(fd,buffer,MAX)>0)

        printf("读到的数据:%s\n",buffer);

    else

        printf("读取出错!\n");

    close(fd);

    return 0;

}

思考:若将基准点设置为SEEK_END但是偏移量是正数(即从文件末尾再向后偏移),会产生什么情况?

/*******************文件空洞******************/

若将lseek()函数的基准点设置为SEEK_END但是偏移量是正数(即从文件末尾再向后偏移),则会产生“文件空洞”的情况。#但是不能从文件头向前偏移#

文件的偏移量是从文件开始位置开始计算的,若文件的偏移量大于了文件的实际数据长度,则会延长该文件,形成空洞。

示例:创建一个有空洞的文件。故意在文件结尾偏移好多个字节,然后再写入数据

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<unistd.h>

#include<sys/stat.h>

#include<fcntl.h>

int main(int argc,char *argv[])

{

    int fd;

    int n;

    char buf1[]="LiLaoShiZhenShuai!";

    char buf2[]="ABCDEFG";

    if(argc<2)

    {

        printf("arguments are too few\n");

        exit(0);

    }

    if((fd=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666))<0)

    {

        perror("cannot open file");

        exit(0);

    }

    write(fd,buf1,strlen(buf1));//首先写入某些数据

    n=lseek(fd,0,SEEK_END);//返回文件末尾位置求出文件大小

    printf("原先的文件大小是%d\n",n);

    n=lseek(fd,987654321,SEEK_END);//在文件末尾向后偏移很多字节

    printf("此时偏移量是%d\n",n);

    write(fd,buf2,strlen(buf2));//写入buf内数据

    n=lseek(fd,0,SEEK_END);

    printf("空洞后文件大小是%d\n",n);

    close(fd);

    return 0;

}

程序执行后,使用vim查看该文件,会发现在两段数据之间有一段乱码数据,并且使用ls -l指令查看,文件的大小也变大了

    在UNIX/Linux文件操作中,文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被设为0,用read读取空洞部分读出的数据是0

    空洞文件作用很大,例如迅雷下载文件,在未下载完成时就已经占据了全部文件大小的空间,这时候就是空洞文件。下载时如果没有空洞文件,多线程下载时文件就都只能从一个地方写入,这就不是多线程了。如果有了空洞文件,可以从不同的地址写入,就完成了多线程的优势任务。

/*******************文件空洞end***************/

5、文件锁(选学)

    通过之前的open()/close()/read()/write()/lseek()函数已经可以实现文件的打开、关闭、读写等基本操作,但是这些基本操作是不够的。对于文件的操作而言,“锁定”操作是对文件(尤其是对共享文件)的一种高级的文件操作。当某进程在更新文件内数据时,期望某种机制能防止多个进程同时更新文件从而导致数据丢失,或者防止文件内容在未更新完毕时被读取并引发后续问题,这种机制就是“文件锁”。

    对于共享文件而言,不同的进程对同一个文件进行同时读写操作将极有可能出现读写错误、数据乱码等情况。在Linux系统中,通常采用“文件锁”的方式,当某个进程独占资源的时候,该资源被锁定,其他进程无法访问,这样就解决了共享资源的竞争问题

    文件锁包括建议性锁(又名“协同锁”)和强制性锁两种建议性锁要求每个相关进程访问文件的时候检查是否已经有锁存在并尊重当前的锁(也可以不尊重)。一般情况下不建议使用建议性锁,因为无法保证每个进程都能自动检测是否有锁,Linux内核与系统总体上都坚持不使用建议性锁。而强制性锁是由内核指定的锁,当一个文件被加强制性锁的过程中,直至该所被释放之前,内核将阻止其他任何进程对该文件进行读或写操作,每次读或写操作都得检测锁是否存在。当然,采用强制性锁对内核的性能影响较大,每次内核在操作文件的时候都需要检查是否有强制性锁。

    在Linux内核提供的系统调用中,实现文件上锁的函数有lockf()和fcntl(),其中lockf()用于对文件加建议性锁,这里不再讲解。fcntl()函数既可以加建议性锁,也可以加强制性锁。同时,fcntl()还能对文件某部分上记录锁。所谓记录锁,其实就是字节范围锁,它能锁定文件内某个特定区域,当然也可锁定整个文件

    记录锁又分为读锁和写锁两种。其中读锁又称为共享锁,它用来防止进程读取的文件记录被更改。记录内可设置多个读锁,但当有一个读锁存在的时候就不能在该记录区域设置写锁。写锁又称为排斥锁在任何时刻只能有一个程序对文件的记录加写锁,它用来保证文件记录被某一进程更新数据的时候不被其他进程干扰,确保文件数据的正确性,同时也避免其他进程“弄脏”数据。文件记录一旦被设置写锁,就不能再设置任何锁直至该写锁解锁

本节只简单讲述fcntl()对文件施加读锁和写锁并查看两种锁的效果,有关函数fcntl()的更详细用法请查阅fcntl()手册(man fcntl)。

函数fcntl()

    需要头文件:#include<unistd.h>

                        #include<sys/types.h>

                        #include<fcntl.h>

    函数原型:int fcntl(int fd,int cmd,struct flock *lock_set);

    函数参数:fd:文件描述符

              cmd:检测锁或设置锁

              lock_set:结构体类型指针,结构体struct flock需要事先设置,与第二个参数连用

    函数返回值:成功:0

                失败:-1

第二个参数cmd表示该操作对文件的命令,若该命令是对文件检测锁或施加锁,则需要第三个参数:

    F_GETLK:检测文件锁状态,检测结果存放在第三个参数的结构体的l_type内

    F_SETLK:对文件进行锁操作,锁操作类型存放在第三个参数的结构体的l_type内

    F_SETLKW:同F_SETLK,不过使用该参数时若不能对文件进行锁操作则会阻塞直至可以进行锁操作为止(W即wait,等待)

    (更多参数请参阅fcntl()函数的使用手册)

第三个参数是对文件施加锁操作的相关参数设置的结构体

注意:必须定义struct flock类型结构体并初始化结构体内的数据,然后使用地址传递的方式传递参数,不允许直接定义struct flock* 类型指针直接传参

关于struct flock的成员如下:

   

struct flock

    {

        short l_type;    //文件锁类型:F_RDLCK, F_WRLCK, F_UNLCK

        short l_whence;    //起始位置:SEEK_SET, SEEK_CUR, SEEK_END

        off_t l_start;

        off_t l_len;

        pid_t l_pid;    //将文件锁定的进程PID(仅在F_GETLK时有效)

    }

结构体成员说明:

    l_type:有三个参数

            F_RDLCK:读锁(共享锁)

            F_WRLCK:写锁(排斥锁)

            F_UNLCK:无锁/解锁

    l_whence:相对于偏移量的起点,参数等同于fseek()与lseek()中的whence参数

            SEEK_SET:位置为文件开头位置

            SEEK_CUR:位置为文件当前读写位置

            SEEK_END:位置为文件结尾位置

    l_start:加锁区域在文件中的相对位移量,与l_whence的值共同决定加锁区域的起始位置

    l_len:加锁区域的长度,若为0则表示直至文件结尾EOF

    l_pid:具有阻塞当前进程的锁,其持有的进程号会存放在l_pid中,仅由F_GETLK返回

思考:如何设置该结构体内的成员使得加锁的范围为整个文件?

答案:设置l_whence为SEEK_SET,l_start为0,l_len为0即可。

示例:使用fcntl()函数对文件进行锁操作。首先初始化结构体flock中的值,然后调用两次fcntl()函数。第一次参数设定为F_GETLK判断是否可以执行flock内所描述的锁操作;第二次参数设定为F_SETLK或F_SETLKW对该文件进行锁操作。

注意:需要至少两个终端运行该程序才能看到效果

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<fcntl.h>

#include<sys/types.h>

int lock_set(int fd,int type)

{

    struct flock lock;

    lock.l_whence = SEEK_SET;

    lock.l_start = 0;

    lock.l_len = 0;//三个参数设置锁的范围是全文件

    lock.l_type = type;//type的参数由主调函数传参而来

    lock.l_pid = -1;

    

    //第一次操作,判断该文件是否可以上锁

    fcntl(fd,F_GETLK,&lock);

    if(lock.l_type!=F_UNLCK)//如果l_type得到的返回值不是F_UNLCK则证明不能加锁,需判断原因

    {

        if(lock.l_type==F_RDLCK)

        {

            printf("This is a ReadLock set by %d\n",lock.l_pid);

        }

        else if(lock.l_type==F_WRLCK)

        {

            printf("This is a WriteLock set by %d\n",lock.l_pid);

        }

    }

    

    //第二次操作,对文件进行相应锁操作

    lock.l_type = type;

    if((fcntl(fd,F_SETLKW,&lock))<0)

    {

        printf("Lock Failed:type = %d\n",lock.l_type);

        return -1;

    }

    switch(lock.l_type)

    {

        case F_RDLCK:

            printf("ReadLock set by %d\n",getpid());break;

        case F_WRLCK:

            printf("WriteLock set by %d\n",getpid());break;

        case F_UNLCK:

            printf("ReleaseLock by %d\n",getpid());

            return 1;

            break;

    }

    return 0;

}

int main(int argc, const char *argv[])

{

    int fd;

    if((fd=open("hello.txt",O_RDWR))<0)

    {

        perror("fail to open hello.txt");

        exit(0);

    }

    printf("This pid_no is %d\n",getpid());

    //给文件上锁

    lock_set(fd,F_WRLCK);

    printf("Press ENTER to continue...\n");

    getchar();

    //给文件解锁

    lock_set(fd,F_UNLCK);

    close(fd);

    return 0;

}

这里标注比较随意,可以看我的印象笔记:文件IO 第二天 (文件IO)

猜你喜欢

转载自blog.csdn.net/nan_lei/article/details/81488349