• UNIX环境高级编程 第3章 文件I/O


    前面两章说明了UNIX系统体系和标准及其实现,本章具体讨论UNIX系统I/O实现,包括打开文件、读文件、写文件等。

    UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek、close。它们是不带缓冲的I/O。

    只要涉及多个进程间共享资源,原子操作的概念就变得很重要,本章通过open( )函数来讨论此概念。

    文件描述符

    文件描述符是一个非负整数,它是内核对打开文件的一个抽象。每当打开或者创建一个文件时,内核会向进程返回一个文件描述符,随后可以利用该描述符来进行文件的读或写。一个进程默认的文件描述符范围是有限的,可以通过调用sysconf( _SC_OPEN_MAX )函数来查看限制,也可以通过shell命令ulimit -n来查看。例如,在我的Ubuntu Server上,其限制为65536,如下图所示:

    而在我的Mac OS X上,则默认最大为256:

    函数open和openat

    函数open和openat用于打开或创建一个文件。其头文件及函数原型如下:

    #include <fcntl.h>
    
    int open(const char *path, int oflag, ... /* mode_t mode */ );
    int openat(int fd, const char *path, int oflag, ... /* mode_t mode */ );

     这两个函数成功时,返回非负的文件描述符,出错时返回-1。由open和openat函数返回的文件描述符一定是最小的未用的描述符数值。

    函数create

    函数create用于创建文件。其头文件及函数原型如下:

    #include <fcntl.h>
    
    int creat (const char* file, mode_t  mode);

    此函数存在致命缺点,即创建和写不是原子操作,因此已经成为一个鸡肋接口。

    函数close

    函数close用于关闭一个已经打开的文件。其头文件及函数原型如下:

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

    关闭一个文件时会释放该进程加在其上的文件记录锁。当一个进程终止时,内核为自动关闭该进程打开的所有文件。

    函数lseek

    每个文件都有一个与其相关联的“当前文件偏移量”,它通常是一个非负整数,用来度量从文件开始处计算的字节数。读写操作通常从当前文件偏移量处开始,并使偏移量增加读写的字节数。打开一个文件时默认文件偏移量为0,若指定了O_APPEND选项,则偏移量设置为末尾字节。我们可以使用lseek来手动设置文件偏移量。其头文件及函数原型如下:

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

    其中的whence指的是偏移量设置方式,其值有如下三种:

    • SEEK_SET:将文件偏移量从开始处开始偏移,offset只能正值
    • SEEK_CUR:将文件偏移量从当前处开始偏移,offset可正可负
    • SEEK_END:将文件偏移量从文件尾开始偏移,offset可正可负

    如果lseek执行成功,则返回新的文件偏移量。lseek也可以用来测试目标文件是否支持设置偏移量。

    对于SEEK_CUR和SEEK_END,当文件偏移量设置为负数并且lseek成功执行,则返回的文件偏移量是实际偏移量,而不是设置的offset值,例如:

    #include <unistd.h>
    #include <fcntl.h>
    #include <iostream>
    
    using std::cout;
    using std::endl;
    
    int main()
    {
        auto fd = open("/file",O_RDONLY);
        cout << fd << endl;
        cout << lseek(fd,-2,SEEK_END);
        close(fd);
        return 0;
    }

    假定/file是一个文本文件,其内容为“abcde”,则当上面代码中leesk执行成功后,lseek返回值为4,而不是-2,因为我们指定从文件末尾处(SEEK_END)开始进行偏移,偏移量向前(-2),则实际偏移量移动到“d”,被移动经过的第二个是“e”,而第一个是Linux系统上文本末尾的结束标记字符“$”。

    文件偏移量的设置可以大于文件的长度,在这种情况下,下一次对文件的读写会加长文件,并在文件中间构成一个空洞,空洞部分被读取为0,空洞部分并不占用硬盘空间。  

    函数read

    函数read用于从打开的文件读取数据。其头文件及函数原型如下:

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

    ssize_t在Linux系统上是一个long int类型。fd是待读取的源文件,buf是待写入的目标缓冲,而nbytes则是想要读取的最大字节数。read函数成功之后返回读取的实际字节数。

    • 返回的字节数和想要读取的最大字节数可能不一致,原因有如下几个:
    • 即将到达文件尾部,而剩余的字节数小于要读取的字节数;
    • 从终端设备读取时,是以换行为准,指定的字节数大于一行的总字节数时;
    • 从网络读时,缓冲导致小于想要读取的字节数;
    • 从面向记录的设备读时,一次最多返回一个记录;
    • 信号中断导致只读取部分的返回。

    函数write

    函数write用于向打开的文件写入数据。其头文件及函数原型如下:

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

    write函数返回值通常等于n,也即指定写入的数量,否则返回-1表示出错。

    对于read和write函数,一定要注意其操作的是内存中的字节数,比如要用read和write去读写int类型变量,则一次性要读写32位,也即4字节。因此其是二进制还是文本模式取决于对字节的解释。

    I/O的效率

    由于read和write是不带缓冲的,因此每一次的调用都会进行一次内核调用,这会对I/O的效率造成很大的影响。

    原子操作

    原子操作指的是一个活一系列操作是密不可分的,要么完成全部,要么一个都没完成,是不可能只执行了其中的一部分的。

    函数dup和dup2

    函数dup和dup2用来复制一个现有的文件描述符。其头文件及函数原型如下:

    #include <unistd.h>
    
    int dup (int fd);
    int dup2(int fd1, int fd2);

    这两个在成功执行时返回新的描述符,当失败时,它们返回-1。对于dup2( )来说,如果fd2已经被占用,其会先关闭旧的fd2,然后返回与fd2相等的描述符值,当fd1和fd2相等时,其什么也不做,仅仅返回fd2。

    函数sync、fsync、fdatasync

    UNIX系统通常会实现一个磁盘缓冲的功能,当程序向硬盘写入内容时,并不会每次都去写硬盘,而是将待写入的东西缓存buffer中,在稍后将多次缓存的数据一次性写入硬盘,这种方式称为延迟写。通常内核会在缓冲区满了或者需要重用缓冲区时进行刷新写入。UNIX提供了三个这样的函数。其头文件及函数原型如下:

    #include <unistd.h>
    
    void sync(void);
    int fsync(int fd);
    int fdatasync(int fd); 

    其中,fdatasync( )函数在FreeBSD及其衍生版(比如MacOS)中不受支持。

    sync( )函数是对整个缓冲区作用生效,并且不等待实际磁盘操作的结束就返回;fsync( )函数是只对指定的文件描述符作用生效,它等待磁盘操作结束才返回。fdatasync( )函数和fsync( )函数类似,区别是它只刷新文件的数据部分,不刷新文件的属性部分。

    函数fcntl

    函数fcntl( )可以用来设置文件描述符的属性。其头文件及函数原型如下:

    #include <fcntl.h>
    
    int fcntl (int fd, int cmd, ...);

    fcntl( )函数成功时返回对应的值,失败时返回-1。它具有以下5种功能:

    • 1.复制一个已有的描述符;
    • 2.获取或设置文件描述符标志;
    • 3.获取或设置文件状态标志;
    • 4.获取或设置异步I/O所有权;
    • 5.获取或设置记录锁。

    利用fcntl( )函数修改文件描述符标志或者文件状态标志时,必须先获取当前的标志状态,然后再追加更新,最后将新的状态标志设置写入回去,如果直接设置会导致旧的标志被复位。

    函数ioctl

    ioctl( )函数是一个功能比较混杂的函数。通常用于终端I/O,其头文件及函数原型如下:

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

    习题

    3.1 当读/写磁盘文件时,本章中描述的函数确实是不带缓冲机制的吗?请说明原因。

    最终的硬盘I/O是带缓冲的,因为内核会提供一个缓冲区用来存储向硬件设备中写入的数据。对于普通概念上的缓冲,通常是指非内核提供的用户级缓冲。

    3.2 编写一个与3.12节中dup2功能相同的函数,要求不调用fcntl函数,并且要有正确的出错处理。

    int dup2_self(int fd, int fd2)
    {
        if (fd < 0 || fd2 < 0 || fd2 > OPEN_MAX)    //判断文件描述符的合法性
        {
            return -1;
        }
        if (fd == fd2)
        {
            return fd2;
        }
    
        close(fd2);     //如果已打开fd2,则关闭。未打开也不会有影响。
    
        if (fd2 == 0)
        {
            return dup(fd);     //dup()总是返回最小的,如果是0,则close()关闭后,一定返回0
        }
    
        int *fdp = new int[(sizeof(int) * fd2)]{};      //全部默认初始化为0,执行到此处时fd2一定大于0
        int tempfd = -1, i = 0;
        while ((tempfd = dup(fd)) != fd2 && tempfd != -1)
        {
            fdp[i] = tempfd;
            ++i;
        }
        while (i+1)
        {
            close(fdp[i--]);
        }
        delete[] fdp;
        return tempfd;
    }

    3.3 假设一个进程执行下面3个函数调用:

    fd1 = open(pathname, oflags);
    fd2 = dup(fd1);
    fd3 = open(pathname, oflags);

    画出类似于图3-9的结果图。对fcntl作用于fd1来说,F_SETFD命令会影响哪一个文件描述符?F_SETFL呢?

    3.4 在许多程序中都包含下面一段代码?

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if (fd > 2)
        close(fd);

    为了说明if语句的必要性,假设fd是1, 画出每次调用dup2时3个描述符项及相应的文件表项的变化情况。然后再画出fd为3的情况。

    3.5 在Bourne shell、Bourne-again shell和Korn shell中,digit1>&digit2表示要将描述符digit1重定向至描述符digit2的同一文件。请说明下面两条命令的区别。

    ./a.out > outfile 2>&1

    ./a.out 2>&1 > outfile

    (提示:shell从左到右处理命令行。)

    3.6 如果使用添加标志打开一个文件以便读、写,能否使用lseek在任一位置开始读?能否用lseek更新文件中任一部分的数据?请编写一段程序以验证之。

  • 相关阅读:
    windows本地文件搜索神器 Everything 为什么速度这么快?
    Electron构建跨平台应用
    「前端进阶」高性能渲染十万条数据(虚拟列表)
    Chrome开发者工具之JavaScript内存分析
    网页性能管理详解
    TCP-IP详解:滑动窗口(Sliding Window)
    滑动窗口
    流量控制(滑动窗口)和 拥塞控制(拥塞控制的工作过程)
    详解 Git 大文件存储(Git LFS)
    TCP流量控制
  • 原文地址:https://www.cnblogs.com/pluse/p/6794910.html
Copyright © 2020-2023  润新知