• 操作系统:文件IO


    文件描述符

    文件描述符是一个非负整数,指向打开的文件。

    文件关闭后被文件使用的描述符会被释放,等着下一次open时,被重复利用。

    文件描述符池

    每个程序运行起来后,就是一个进程,系统会给每个进程分配 0 1 ˜ 023 0\~1023 01˜023的描述符范围,也就是说每个进程打开文件时,open所返回的文件描述符,是在 0 1 ˜ 023 0\~1023 01˜023范围中的某个数字。 0 1 ˜ 023 0\~1023 01˜023这个范围,其实就是文件描述符池。

    1023这个上限可不可以改?

    可以,但是没有必要,也不会介绍如何去改,因为一个进程基本不可能出现,同时打开1023个文件的情况,文件描述符的数量百分百够用。

    进程使用的文件以文件描述符表的形式存放在进程控制块(PCB)中

    在这里插入图片描述

    • i节点信息就是文件的权限属性等信息
    • 函数指针
      read、write等操作文件时,会根据底层具体情况的不同,调用不同的函数来实现读写,所以在v节点里面保存了这些不同函数的函数指针,方便调用。

    基本文件操作

    open

    #include 
    int open(const char *pathname,int flags[,mode_t mode]);//open and possibly create a file
    
    • 1
    • 2

    如果没有mode参数,只能打开存在的文件,否则会报错;如果有mode参数,就会按照mode指定的文件权限创建文件。

    返回:打开成功返回文件操作符,否则返回-1并设置errno

    参数:

    • flags

      以下三个选项必选其中之一,且是互斥的

      • O_RDONLY :Open for reading only.

      • O_WRONLY:Open for writing only.

      • O_RDWR:Open for reading and writing. The result is undefined if this flag is applied to a FIFO.

      以下是可选组合选项,通过|逻辑或组合

      • O_CREAT:若欲打开的文件不存在则自动建立该文件.

      • O_EXCL:如果O_EXCL与O_CREAT 被同时设置, 此指令会去检查文件是否存在,文件若不存在则建立该文件, 否则将导致打开文件错误. 此外, 若O_CREAT 与O_EXCL 同时设置, 并且欲打开的文件为符号连接, 则会打开文件失败.

      • O_NOCTTY:如果欲打开的文件为终端机设备时, 则不会将该终端设备当成进程控制终端机.

      • O_TRUNC:若文件存在并且以可写的方式打开时, 此旗标会令文件长度清为0, 而原来存于该文件的资料也会消失.

      • O_APPEND 当读写文件时会从文件尾开始移动, 也就是所写入的数据会以附加的方式加入到文件后面.

      • O_NONBLOCK:以非阻塞的方式打开文件, 也就是无论有无数据读取或等待, 都会立即返回进程之中.
        如果没有设置O_NONBLOCK:

        • 以只读模式open会阻塞直到有进程open该文件for write
        • 以只写模式open会阻塞知道有进程open该文件for read
        • 只有当设备可用时才会返回

        如果设置了O_NONBLOCK:

        • 当以只读或只写模式打开一个FIFO时,只读模式的open函数会立即返回,只写模式的open函数会返回错误如果当前无进程has the file open for reading
      • O_NDELAY 同O_NONBLOCK.

      • O_SYNC 以同步的方式打开文件.

      • O_NOFOLLOW:如果参数pathname 所指的文件为一符号连接, 则会令打开文件失败.

      • O_DIRECTORY:如果参数pathname 所指的文件并非为一目录, 则会令打开文件失败。

    • 可选参数mode,只有在创建新文件时才会生效

      • S_IRWXU,00700 权限, 代表该文件所有者具有可读、可写及可执行的权限.
      • S_IRUSR 或S_IREAD, 00400 权限, 代表该文件所有者具有可读取的权限.
      • S_IWUSR 或S_IWRITE, 00200 权限, 代表该文件所有者具有可写入的权限.
      • S_IXUSR 或S_IEXEC, 00100 权限, 代表该文件所有者具有可执行的权限.
      • S_IRWXG,00070 权限, 代表该文件用户组具有可读、可写及可执行的权限.
      • S_IRGRP,00040 权限, 代表该文件用户组具有可读的权限.
      • S_IWGRP,00020 权限, 代表该文件用户组具有可写入的权限.
      • S_IXGRP,00010 权限, 代表该文件用户组具有可执行的权限.
      • S_IRWXO,00007 权限, 代表其他用户具有可读、可写及可执行的权限.
      • S_IROTH,00004 权限, 代表其他用户具有可读的权限
      • S_IWOTH,00002 权限, 代表其他用户具有可写入的权限.
      • S_IXOTH,00001 权限, 代表其他用户具有可执行的权限.

    open返回文件描述符的规则

    open返回文件描述符池中当前最小的没有用到的那一个。
    进程一运行起来,0/1/2默认就被使用了,最小没被用的是3,所以返回3。
    如果又打开一个文件,最小没被用的就应该是4,所以open返回的应该是4。

    1. 程序开始运行时,有三个文件被自动打开了,打开时分别使用了这三个文件描述符。
    2. 依次打开的三个文件分别是/dev/stdin,/dev/stdout,/dev/stderr。
      • /dev/stdin:标准输入文件
        • 程序开始运行时,默认会调用open (“/dev/stdin”,O_RDONLY)将其打开,返回的文件描述符是0
        • 使用0这个文件描述符,可以读取从键盘输入的数据
          简单理解就是,/dev/stdin这个文件代表了键盘。
      • /dev/ stdout:标准输出文件
        • 程序开始运行时,默认open(“/dev/stdout”,O_WRONLY)将其打开,返回的文件描述符是1。先打开的是/dev/stdin,把最小的0用了,剩下最小没用的是1,因此返回的肯定是1。
        • 通过1这个文件描述符,可以将数据写(打印)到屏幕上显示
          简单理解就是,/dev/ stdout这个文件代表了显示器。
        • 人只看得懂字符,所以所有输出到屏幕显示的,都必须转成字符。
          所以我们输出时,输出的必须是文字编码,显示时会自动将文字编码翻译为字符图形。所以我们输出65时,65解读为A字符的AsCII编码,编码被翻译后的图形自然就是A
      • /dev/stderr:标准错误输出文件
        • 默认open(“/dev/stderr”,O_WRONLY)将其打开,返回的文件描述符是2
        • 通过2这个文件描述符,可以将报错信息写(打印)到屏幕上显示
        • write(2,buf,sizeof(buf))将buf中的数据写道屏幕上,
          数据中转的过程是:write应用缓存buf ->open /dev/stderr时开辟的内核缓存->显示器驱动程序的缓存->显示器
        • 1和2啥区别?
          使用这两个文件描述符,都能够把文字信息打印到屏幕。如果仅仅是站在打印显示的角度,其实用哪一个文件描述符都能办到。

    open的文件描述符与fopen的文件指针

    1. open: Linux 的系统函数(文件io函数)
      open成功后,返回的文件描述符,指向了打开的文件。

    2. fopen:c库的标准io函数

      #include 
      FILE* fopen (const char *path,const char *mpde);
      
      • 1
      • 2

      fopen成功后,返回的是FILE*的文件指针,指向了打开的文件。

    3. 对于Linux的c库来说,fopen这个c库函数,最终其实还是open函数来打开文件的
      fopen只是对open这个函数做了二次封装。

    close

    #include 
    int close(int fd);//close a  file descriptor
    
    • 1
    • 2
    1. 参数

      • fd:指向打开文件的文件描述符
    2. 返回值
      调用成功:返回0
      调用失败:返回-1,并给errno自动设置错误号

    write

    #include 
    ssize_t write(int fd,const void *buf,size_t count);//write to a file descriptor
    
    • 1
    • 2
    1. 功能:向fd所指向的文件写入数据。

    2. 参数

      • fd:指向打开文件的文件描述符
      • buf:保存数据的缓存空间的起始地址
      • count:从起始地址开始算起,把缓存中count个字符,写入fd指向的文件

      数据中转的过程:
      应用缓存(buf)—>open打开文件时开辟的内核缓存—>驱动程序的缓存—>块设备上的文件

    3. 返回值
      调用成功:返回所写的字符个数
      调用失败:返回-1,并给errno自动设置错误号

    直接写字符串常量时,字符串常量被保存(缓存)在了常量区,编译器在翻译如下这句话时,write (fd, “hello world” , strlen ( “hello world”) )会直接将"hello world"翻译为"hello world"所存放空间的起始地址(也就是h所在字节的地址),换句话说,直接使用使用字符串常量时,字符串常量代表的其实是一个起始地址。

    read

    #include 
    ssize_t read(int fd,const void *buf,size_t count);//read from a file descriptor
    
    • 1
    • 2
    1. 功能:从fd指向的文件中,将数据读到应用缓存buf中

    2. 参数

      • fd:指向打开的文件
      • buf:读取到数据后,用于存放数据的应用缓存的起始地址
      • count:缓存大小(字节数)

      数据中转的过程:应用缓存(buf)<—open打开文件时开辟的内核缓存<—驱动程序的缓存<—块设备上的文件

    3. 返回值
      1)成功:返回读取到的字符的个数
      2)失败:返回-1,并自动将错误号设置给errno。

    lseek

    #include 
    off_t lseek(int fd, off_t offset, int whence);
    
    • 1
    • 2
    1. 功能
      调整读写的位置,就像在纸上写字时,挪动笔尖所指位置是一样的。
      c库的标准io函数里面有一个fseek函数,也是用于调整读写位置的,fseek就是对lseek系统函数封装后实现的。

    2. 返回值

      • 调用成功:返回当前读写位置相对于文件开始位置的偏移量(字节)。
        可以使用lseek函数获取文件的大小,将文件读写的位置移动到最末尾,然后获取返回值,这个返回值就是文件头与文件尾之间的字节数,也就是文件大小。
      • 调用失败:返回-1,并给errno设置错误号。
    3. 参数:

      • fd:文件描述符
      • whence:粗定位
        • SEEK_SET :调到文件起始位置
        • SEEK_CUR:调到文件当前读写位置
        • SEEK_END:调到文件末尾
      • offset:精定位,微调位置(byte),负数向前,正数向后

    fcntl

    #include                                                            
    #include                                                                    
    int fcntl(int fd, int cmd, ... /* arg */ ); 
    
    • 1
    • 2
    • 3
    1. 功能
      fcntl函数其实是File Control的缩写,可以通过fcntl设置、或者修改已打开文件的某些性质。

    2. 返回值
      调用成功:返回值视具体参数而定
      调用失败:返回-1,并把错误号设置给errno。

    3. 参数

      • fd:指向打开文件的文件描述符

      • /* arg */是多余的参数如果没有用到就写0

      • cmd:控制命令,通过指定不同的宏来修改fd所指向文件的性质。

        • F_DUPFD
          复制描述符,可用来模拟dup和dup2,后面会有例子对此用法进行说明。
          返回值:返回复制后的新文件描述符

          #include 
          fd=open(FILE_NAME,O_RDWR);
          //模拟dup2(fd,1);
          close(1);
          fd1=fcntl(fd,F_DUPFD,1);
          
          • 1
          • 2
          • 3
          • 4
          • 5
        • F_GETFL、F_SETFL
          获取、设置文件状态标志,比如在open时没有指定O_APPEND,可以使用fcntl函数来补设。
          返回值:返回文件的状态标志

          什么时候需要fcntl来补设?
          当文件描述符不是你自己open得到,而是调用别人给的函数,别人的函数去open某个文件,然后再将文件描述符返回给你用,在这种情况下,我们是没办法去修改被人的函数,在他调用的open函数里补加文件状态标志。此时就可以使用fcntl来补设了,使用fcntl补设时,你只需要知道文件描述符即可。

        • FGETFD、FSETFD

        • F_GETOWN、F_SETOWN
          获取或设置文件描述符所属进程,如fcntl(mousefd,F_SETOWN,getpid());

        • F_GETLK或F_SETLK或F_SETLKW

    共享文件操作

    有两种情况:

    • 同一进程共享操作相同的文件
    • 多个进程之间,共享操作相同文件

    同一进程,多次open同一文件

    • 在进程内多次open打开同一文件时,文件描述符是不同的。在同一进程里面,一旦某个文件描述符被用了,在close释放之前,别人不可能使用,所以指向同一文件的描述符不可能相同。

    • 如果不用O_APPEND选项,open打开同一文件的多个文件描述符同时写数据时会相互覆盖:
      不同文件描述符对应不同的文件表,文件表内的文件偏移量也是独立的,一个文件描述符写操作不会影响另一个文件描述符对应的文件偏移量不变,它们写的位置是重叠的,就会相互覆盖。

    • 指定O_APPEND参数即可解决覆盖问题
      必须每个open都要指定,有一个不指定就会覆盖,就先过马路一样,都要准守交通规则才能安全,开车的和行人,只要有一个不准守都会出事。
      共享操作的文件描述符对应同一个V节点,V节点中的文件长度信息是大家共享的,当文件被写入数据后,文件长度就会被更新,都指定O_APPEND后,使用不同的文件描述符写数据时,都会先使用文件长度更新自己的文件位移量,保证每次都是在文件的最末尾写数据,就不会出现相互覆盖的情况。

    在这里插入图片描述

    多个进程,多次open同一文件

    同单进程文件共享相同,同样因为各自有独立的文件偏移量存在覆盖问题,解决方法同样为加O_APPEND标志。

    在这里插入图片描述

    dup

    #include 
    int dup(int oldfd);
    
    • 1
    • 2
    1. 功能
      复制某个已经打开的文件描述符,得到一个新的描述符,这个新的描述符,也指向被复制描述符所指向的文件。至于需要用到的新描述符,dup会使用描述符池(0~1023)中当前最小没用的那一个。
    2. 返回值
      • 成功:返回复制后的新文件描述符
      • 失败:返回-1,并且errno被设置。
    3. 参数
      oldfd:被复制的已经存在的文件描述符

    dup2

    #include 
    int dup2(int oldfd,int newfd);
    
    • 1
    • 2
    1. 功能

      功能同dup,只不过在dup2里面,我们可以自己指定新文件描述符。如果这个新文件描述符已经被打开了,dup2会把它给关闭后,再使用。
      dup2和dup的不同之处在于:
      dup:自己到文件描述符池中找新文件描述符dup2:我们可以自己指定新文件描述符

    2. 返回值

      • 成功:返回复制后的新文件描述符
      • 失败:返回-1,并且errno被设置。
    3. 参数
      oldfd:被复制的已经存在的文件描述符
      newfd:新的文件描述符

    利用dup、dup2实现重定位

    #include 
    int fd1=0,fd2=0;
    fd1=open(FILE_NAME,O_RDWR|O_TRUNC);
    
    close(1);
    fd2=dup(fd1);
    //或
    dup2(fd,1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    函数中的文件描述符值写死了,无法修改为新的描述符,但是你又希望该函数,把数据输出到其它文件中,此时就可以使用dup、dup2对该函数中的文件描述符,进行重定位,指向新的文件,函数就会将数据输出到这个新文件。

    linux命令>就是dup、dup2的典型应用

    高级文件IO

    非阻塞IO

    读键盘、鼠标是阻塞的;读普通文件时,如果读到了数据就成功返回,如果没有读到数据返回0,总之不会阻塞。

    可以将阻塞的读改为非阻塞的读,非阻塞读的意思就是说,如果有数据就成功读到,如果没有读到数据就出错返回,而不是阻塞。

    如何实现非阻塞读

    1. 打开文件时指定O_NONBLOCK状态标志

      fd=open("/dev/input/mouse0",O_RDONY|O_NONBLOCK);
      
      • 1
    2. 通过fcntl函数指定O_NONBLOCK来实现

      • 情况1:当文件已经被open打开了,但是open是并没有指定你要的文件状态标志,而你又想去修改open的参数,此时就是可以使用fcntl来补设。
      • 情况2:没办法在open指定,你手里只有一个文件描述符fd,此时就使用fcntl来补设
        比如无名管道,无名管道连名字都没有,没办法使用open函数,无名管道是使用pipe函数来返回文件描述符的,如果过你想非阻塞的读无名管道的话,是没有办法通过open来指定O_NONBLOCK的,此时就需要使用fcntl来重设或者补设。

    异步IO

    异步IO类似中断,读鼠标键盘 没有数据时进程干自己的事,有数据之后底层驱动给进程发一个SIGIO信号,通知进程数据准备好了,进程来处理数据。

    不过使用异步IO有两个前提,

    1. 底层驱动必须要有相应的发送SIGIO信号的代码,只有这样当底层数据准备好后,底层才会发送SIGIO信号给进程。
      我们之所以可以对鼠标设置异步IO,是因为人家在实现鼠标驱动时,有写发送SIGIO信号的代码,如果驱动程序是我们自己写的,发送SIGIO的代码就需要我们自己来写。
    2. 应用层必须进行相应的异步IO的设置,否者无法使用异步IO
      应用层进行异步IO设置时,使用的也是fcntl函数。

    使用异步IO时应用层的设置步骤:

    • 调用signal函数对SIGIO信号设置捕获函数
      在捕获函数里面实现读操作,比如读鼠标。

    • 使用fcntl函数,将接收SIGIO信号的进程设置为当前进程
      如果不设置的,底层驱动并不知道将SIGIO信号发送给哪一个进程

      fcntl(mousefd,F_SETOWN,getpid());
      
      • 1
    • 使用fcntl函数,对文件描述符增设O_ASYNC的状态标志,让fd支持异步IO

      mousefd = open("/dev/input/mouse1",O_RDONLY);
      flag = fcntl(mouse_fd,F_GETFL);
      flag |= O_ASYNC;//补设O_ASYNC
      fcntl(mouse_fd,F_SETFL,flag) ;
      
      • 1
      • 2
      • 3
      • 4
  • 相关阅读:
    k8s-service-3-clusterip
    阿里云99元服务器40G ESSD Entry云盘、2核2G3M带宽配置
    IB DP 语言怎么选?
    处理目标检测中的类别不均衡问题
    STM32学习笔记(七)--ADC详解
    Flutter yuv 转 rgb
    中手游上半年扭亏为盈,仙剑IP魅力不减?
    Mybatis-plus-generator 自定义模板生成自定义 DTO、VO等
    Go语言和Python语言哪个比较好?
    leetcode59螺旋矩阵II + 54螺旋矩阵
  • 原文地址:https://blog.csdn.net/wsl_longwudi/article/details/127445606