[单刷APUE系列]第三章——文件I/O

山河永寂

目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]

文件描述符

在学习C语言的时候,应该也学习了使用<stdio.h>提供的通用文件操作,在C语言中,已经封装好了File结构体帮助操作文件,打开<stdio.h>

typedef struct __sFILE {
        unsigned char *_p;      /* current position in (some) buffer */
        int     _r;             /* read space left for getc() */
        int     _w;             /* write space left for putc() */
        short   _flags;         /* flags, below; this FILE is free if 0 */
        short   _file;          /* fileno, if Unix descriptor, else -1 */
        struct  __sbuf _bf;     /* the buffer (at least 1 byte, if !NULL) */
        int     _lbfsize;       /* 0 or -_bf._size, for inline putc */

        /* operations */
        void    *_cookie;       /* cookie passed to io functions */
        int     (*_close)(void *);
        int     (*_read) (void *, char *, int);
        fpos_t  (*_seek) (void *, fpos_t, int);
        int     (*_write)(void *, const char *, int);

        /* separate buffer for long sequences of ungetc() */
        struct  __sbuf _ub;     /* ungetc buffer */
        struct __sFILEX *_extra; /* additions to FILE to not break ABI */
        int     _ur;            /* saved _r when _r is counting ungetc data */

        /* tricks to meet minimum requirements even when malloc() fails */
        unsigned char _ubuf[3]; /* guarantee an ungetc() buffer */
        unsigned char _nbuf[1]; /* guarantee a getc() buffer */
        unsigned char _ubuf[3]; /* guarantee an ungetc() buffer */
        unsigned char _nbuf[1]; /* guarantee a getc() buffer */

        /* separate buffer for fgetln() when line crosses buffer boundary */
        struct  __sbuf _lb;     /* buffer for fgetln() */

        /* Unix stdio files get aligned to block boundaries on fseek() */
        int     _blksize;       /* stat.st_blksize (may be != _bf._size) */
        fpos_t  _offset;        /* current lseek offset (see WARNING) */
} FILE;

可容易看到FILE结构体内有一个成员short _file;这就是Unix使用的文件描述符(file descriptor),而且结构体内除了必须的一些缓冲区、文件打开标志等东西,还包括了以函数指针的方式提供的“成员函数”。如果一些朋友曾经使用过TC2.0并且查看过<stdio.h>头文件,可能会惊讶在Unix环境下多出的非常多的内容。
像这种提供给开发者的操作文件的函数,都统称为带缓冲的I/O函数,而Unix系统本身提供的就是不带缓冲的I/O函数。
对于每个运行中的进程,都维护了一个文件描述符表,文件描述符是一个非负整数,当打开一个文件的时候,内核会向进程返回一个文件描述符。按照Unix惯例,0、1、2的数字分别被标准输入、标准输出、标准错误相关联,我们也可以将其进行替换。在POSIX规范中,已经提供了STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO来替代0、1、2数字,这样更加便于开发者理解。
按照Unix规定,每个Unix系统都应当提供OPEN_MAX限制作为进程最大打开文件限制,也就是说文件描述符的范围在0~OPEN_MAX-1的范围内变动,但是用过Linux的朋友知道,可以使用ulimit命令修改最大文件打开数,甚至可以修改为无限,也就是说,无法通过OPEN_MAX的定义来获得最大文件打开数。这也是上一章中提到的无法在运行时获得的参数。

打开文件函数族

int open(const char *path, int oflag, ...);
int openat(int fd, const char *path, int oflag, ...);

oflag可以指定为以下常量

O_RDONLY        open for reading only
O_WRONLY        open for writing only
O_RDWR          open for reading and writing
O_NONBLOCK      do not block on open or for data to become available
O_APPEND        append on each write
O_CREAT         create file if it does not exist
O_TRUNC         truncate size to 0
O_EXCL          error if O_CREAT and the file exists
O_SHLOCK        atomically obtain a shared lock
O_EXLOCK        atomically obtain an exclusive lock
O_NOFOLLOW      do not follow symlinks
O_SYMLINK       allow open of symlinks
O_EVTONLY       descriptor requested for event notifications only
O_CLOEXEC       mark as close-on-exec

两个函数就是一个绝对路径和相对路径的区别,oflag可以进行组合,使用|或运算符构成新的参数。
具体的详情可以看原著解释,里面已经非常详细。
在原著中写了五个常量

O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR   读、写打开
O_EXEC   只执行打开
O_SEARCH 只搜索打开

并且指明这五个常量必须指定一个而且只能指定一个,但是根据笔者实际查看头文件,发现O_EXECO_SEARCH常量并没有在头文件中出现,相反,头文件中只找到了

/*
 * File status flags: these are used by open(2), fcntl(2).
 * They are also used (indirectly) in the kernel file structure f_flags,
 * which is a superset of the open/fcntl flags.  Open flags and f_flags
 * are inter-convertible using OFLAGS(fflags) and FFLAGS(oflags).
 * Open/fcntl flags begin with O_; kernel-internal flags begin with F.
 */
/* open-only flags */
#define O_RDONLY        0x0000          /* open for reading only */
#define O_WRONLY        0x0001          /* open for writing only */
#define O_RDWR          0x0002          /* open for reading and writing */
#define O_ACCMODE       0x0003          /* mask for above modes */

虽然后面还定义了一些POSIX实际上需要的函数,但是却使用#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)条件编译将其分类在了OS X自有源代码下。

创建文件函数

int creat(const char *path, mode_t mode);

The creat() function is the same as:
    open(path, O_CREAT | O_TRUNC | O_WRONLY, mode);

其实把opencreat函数对比,可以发现,creat功能以及完全被open函数替代了,实际上这是一个历史遗留产物,而且由于creat函数有着诸多的限制,实际开发中极少使用到。从creat函数说明页极少的说明也可以看出官方也并不推荐使用creat。

关闭文件

int close(int fildes);

原著里认为当进程终止时内核会自动关闭所有打开文件,所以不需要显式关闭,实际上在Unix手册中是推荐使用close关闭

When a process exits, all associated file descriptors are freed, but since there is a limit on active descriptors per processes, the
close() function call is useful when a large quantity of file descriptors are being handled.

When a process forks (see fork(2)), all descriptors for the new child process reference the same objects as they did in the parent
before the fork.  If a new process is then to be run using execve(2), the process would normally inherit these descriptors.  Most of
the descriptors can be rearranged with dup2(2) or deleted with close() before the execve is attempted, but if some of these descriptors
will still be needed if the execve fails, it is necessary to arrange for them to be closed if the execve succeeds.  For this reason,
the call ``fcntl(d, F_SETFD, 1)'' is provided, which arranges that a descriptor will be closed after a successful execve; the call
``fcntl(d, F_SETFD, 0)'' restores the default, which is to not close the descriptor.

手册页还讲到了关于fork进程导致的文件描述符继承的情况。

文件偏移

在学习C语言FILE文件操作的时候通常也会讲到文件偏移量,文件偏移量实际上是一个非负整数,但是可以理解为一个指针,指向当前文件从开头开始的字节数,正常非O_APPEND方式打开,偏移量会被重置为0。

off_t lseek(int fildes, off_t offset, int whence);

whence参数只有三种值,0、1、2,不过都已经如同标准文件描述符一样使用常量代替了,也就是SEEK_SETSEEK_CURSEEK_END,非常的简洁易懂。
在原著中提到了lseek可以形成文件空洞,实际上这个和内核实现无关,而是和文件系统相关,也就是说,允许空洞存在,如何存储空洞,都是归给文件系统的。目前大部分文件系统都是使用null填充。

#include "include/apue.h"
#include <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main(int argc, char *argv[])
{
    int fd;
    
    if ((fd = creat("file.hole", FILE_MODE)) < 0)
        err_sys("creat error");

    if (write(fd, buf1, 10) != 10)
        err_sys("buf1 write error");
    
    if (lseek(fd, 16384, SEEK_SET) == -1)
        err_sys("lseek error");
    
    if (write(fd, buf2, 10) != 10)
        err_sys("buf2 write error");
        
    close(fd);
    exit(0);
}

然后编译运行后生成了file.hole文件,使用od命令查看

> od -c file.hole
0000000    a   b   c   d   e   f   g   h   i   j  \0  \0  \0  \0  \0  \0
0000020   \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0040000    A   B   C   D   E   F   G   H   I   J
0040012

很好换算,16384转换为8进制就是40000。顺便说一句,od命令在查询二进制文件的时候非常好用。

读取写入函数

ssize_t read(int fildes, void *buf, size_t nbyte);
ssize_t pread(int d, void *buf, size_t nbyte, off_t offset);
ssize_t readv(int d, const struct iovec *iov, int iovcnt);

ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);

read/write函数族包含三个函数,但是实际上用到的就是read/write函数,其他函数一以后介绍。

文件共享

在学习操作系统原理的时候,大家应该学习过锁的使用。文件是一种资源,当一个进程打开文件的同时另一个进程也持有了此文件的使用权,那么很容易造成文件被覆盖和误读。庆幸的是,操作系统已经为我们准备好了快捷安全的方式共享文件。
首先先讲解一下Unix内核对文件的数据结构

  1. 每个进程都自行维护了一个链表,里面记录了文件描述符(file descriptor)和文件指针的映射

  2. 内核为所有打开的文件维护了一个文件表,注意,是所有打开文件,也就是说,一个文件被多个进程打开,就会出现多个文件表项,这是非常正常的。每个文件表项包含了文件状态标志(读、写等等)文件偏移量文件系统逻辑指针

  3. 最后就是文件系统自己的逻辑指针

原子操作

可能对于使用过数据库的朋友来说,原子操作已经听过了。由于操作系统是基于多任务操作的,内核有可能在执行任何代码后挂起线程然后切换到另外一个线程或者说是另外一个进程的线程,所以说无法保证后一个代码执行时候前一行代码执行结果是有效的,因为很有可能被其他线程改变了。原子操作就是这样的一个方案,就如同数据库中的事务,在提交事务之前,所有的资源都是被锁定。或者说能保证相关的代码执行不中断。
正如open文件后lseek到文件末尾和直接用O_APPEND参数打开文件,两者之间的区别就是原子性和非原子性的区别。
在前面关于读写函数的时候介绍的preadpwirte就是一个原子操作,将lseekreadwirte函数合并。但是请注意,由于这两个函数并非更新了文件偏移量而是自行加上了offset,所以内核中的文件偏移量是不会改变的。
前文open函数在同时使用O_CREATO_EXCL参数的时候也是一种原子操作,能在创建文件的时候就判断文件是否已经存在。

复制文件描述符

int dup(int fildes);
int dup2(int fildes, int fildes2);

dup执行后将会返回最小的可返回的文件描述符,dup2则是自定义文件描述符值,在手册中有

In dup2(), the value of the new descriptor fildes2 is specified.  If fildes and fildes2 are equal, then dup2() just returns fildes2; no other changes are made to the existing descriptor.  Otherwise, if descriptor fildes2 is already in use, it is first deallocated as if a close(2) call had been done first.

也就是如果fildes2正在使用则关闭后再分配;如果fildes等于fildes2则只返回fildes2,且不关闭。

数据同步到磁盘

为了确保磁盘读写能高速有效,Unix系统在内核中设置了高速缓冲区,大多数情况下,我们都使用带有缓冲的I/O函数,在某些情况下,我们需要立刻将缓冲区内数据写入到磁盘,Unix系统提供了syncfsyncfdatasync三个函数,但是在FreeBSD系Unix实现不包含fdatasync函数,包括Mac OS X系统,具体详细介绍可以查看原著和Unix
系统手册。

void sync(void);
int fsync(int fd);

文件控制函数

int fcntl(int fildes, int cmd, ...);

原书中列出了11中参数,实际上,现代Unix实现除了这11种参数意外还设置了其他的参数,这里不再探讨。

#include "include/apue.h"
#include <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main(int argc, char *argv[])
{
    int val;
    
    if (argc != 2)
        err_quit("usage: a.out <descriptor#>");
    
    if ((val = fcntl(atoi(argv[1]), F_GETFL)) < 0)
        err_sys("fcntl error for fd %d", atoi(argv[1]));
    
    switch (val & O_ACCMODE) {
        case O_RDONLY:
            printf("read only");
            break;
        case O_WRONLY:
            printf("write only");
            break;
        case O_RDWR:
            printf("read write");
            break;
        
        default:
            err_dump("unknown access mode");
    }
    
    if (val & O_APPEND)
        printf(", append");
    if (val & O_NONBLOCK)
        printf(", nonblocking");
    if (val & O_SYNC)
        printf(", synchronous writes");
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
    if (val & O_FSYNC)
        printf(", synchronous writes");
#endif

    putchar('\n');
    exit(0);
}

代码非常简洁易懂,可能有些朋友对位运算的技巧不是很了解,所以看不懂一些代码,例如,O_ACCMODE实际上是一个掩码值,它不代表实际意义,而是为了能够快速运算取得每一位的具体数值,一般来说,二进制每一位都代表一个具体含义,当这一位是1的时候,表示这个开关打开,当为0的时候开关关闭,而O_APPEND实际上是(1000)b,O_NONBLOCK则是(100)b,都是各占一位的,所以可以用AND运算取得。
原著后面的两个封装的set_flclr_fl函数实际上也是跟位运算相关

val |= flags;
val &= ~flags;

一个是或运算,一个是按位取反后进行和运算。都是非常实用的小技巧。

设备控制函数

int ioctl(int fildes, unsigned long request, ...);

在Unix手册中,ioctl函数被用于一些底层设备参数的设置和获取,ioctl函数可以控制一些特殊字符设备文件。但是实际上I/O操作不能杂类都是归给这个函数,正如说明文件中说的,终端可能是使用这个函数最多的地方,但是随着标准推进,更多的终端操作函数被提出来用于替代ioctl,实际上很少用到这个函数。

文件描述符设备

在大多数的Unix实现中,都提供了/dev/fd文件夹,里面有若干个文件,打开这些文件,等同于复制文件描述符,实际上由于Linux系统和Unix系统很多不同的实现,在操作这个设备文件的时候需要非常小心,在实际开发中我们有更好的方式来复制文件描述符,正如原著所说,/dev/fd文件夹更多的被使用在shell脚本中。

阅读 4.3k

静雅斋
码代码|Minecraft|Node.jser&PHPer|iOS移动端开发者|Web前端码农
2.4k 声望
157 粉丝
0 条评论
2.4k 声望
157 粉丝
文章目录
宣传栏