APUE第三章 文件IO

前言:前面提到,UNIX的哲学是一切皆文件,文件的活动过程便是进程,整本APUE都是围绕文件和进程来阐述的,围绕文件必然是创建、增减、删除、关闭,其中增减便是IO的操作,IO是唯一的难点;进程同样如此,同样是创建、执行、退出的过程,进程的运行周期难免与文件交互,此时交互的过程就可以是前面提到的步骤,同时进程还可能与其他进程交互,这里面涉及的便是进程间通信;到此为止,本书的重点内容就算完了,看似多么简单的结构呀。


本章所讲述的IO有两个特点:1)不带缓冲,这个不带缓冲是打引号的,实际上是存在的,与后面提到的标准IO相比,我觉得区别就在于缓冲的位置与行为,书中指出每一个read和write都是一个系统调用,也就是内核操作,缓冲是在内核空间,而标准IO是在用户空间由用户来设置的,另外,所有缓冲的意义都是一个,那就是加速;2)原子操作,这一点在多线程或多进程环境下尤其重要;

一、文件描述符

1、所有的文件对于内核而言,都可以通过文件描述符来引用,记住,是所有;

2、文件描述符是一个非负整数,与进程相关联,不同的文件在不同的进程中文件描述符可能是不一样的,同样,同一个进程打开不同的文件的文件描述符是肯定不一样的;

3、惯例,标准输入与文件描述符0(STDIN_FILENO)相关联,标注输出与文件描述符1(STDOUT_FILENO)相关联,标注错误与文件描述符2(STDERR_FILENO)相关联,虽然是惯例,但要严格遵守;

二、IO函数

2.1 打开文件

1)open函数可以用于打开或者创建一个文件

函数原型:

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

函数说明:

path用来引用将要打开的文件或目录的文件名, oflag用于表示针对该文件的选项,涉及文件状态标志和文件描述符状态标志,mode涉及权限问题;

函数返回:

若创建或打开成功,返回文件描述符,出错返回-1;

下面重点介绍下oflag的候选项:

1)O_RDONLY :只读打开;

2)O_WRONLY : 只写打开;

3)O_RDWR : 读写打开;   

4)O_EXEC : 只执行打开;

5)O_SEARCH :只搜索打开;         //此五个选项用来表示打开文件的目的,能且只能指定其中一个;

6)O_APPEND : 每次写时追加到文件尾端; //注意文件偏移量与此选项的关系

7)O_CLOEXEC : 设置文件描述符标志,该选项与进程有关执行有关;

8)O_CREAT: 若文件不存在创建该文件,与mode配合设定访问权限;

9)O_DIRECTORY : 指定用于打开目录,如果path不是目录,则报错;

10)O_EXCL : 如果文件已经存在同时指定了O_CREAT报错,意义在于判断文件是否存在和不存在创建文件两者成为一个原子操作;

11)O_NOCTTY : 如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端;

12)O_NOFOLLOW : 如果path引用的是一个符号链接,则出错;

13)O_NONBLOCK :如果path应用的是一个FIFO、一个块特殊文件或一个字符特殊文件,可以用该选项来控制IO操作方式为非阻塞方式,这里面要提到一点,socket连接的非阻塞模式只能通过文件控制函数来设置;

14)O_SYNC : write操作等待物理IO完成,包括该操作引起的文件状态更新,这里面物理IO表示前面提到的内核缓冲数据真正写到磁盘;

15)O_TRUNC : 如果此文件存在,且作为只写或读写模式打开,则将其长度截断为0;

16)O_TTY_INIT : 如果打开一个还未打开的终端设备,设置非标准termios参数值;

17)O_DSYNC : 不等待文件属性更新。

为了更好的理解,下面有个例程:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

int main()
{
    int     fd;     //文件描述符
    char    path1[10] = "test1";
    char    path2[10] = "test2";
    if ((fd = open(path1, O_RDONLY, 0)) == -1) {
        printf("open %s error!\n", path1);
    }
    if ((fd = open(path1, O_CREAT | O_RDWR, S_IRUSR)) != -1) {  //创建文件,mode标志位设置为用户可读
        printf("create file %s, fd : %d\n", path1, fd);
    }
    if ((fd = open(path1, O_RDONLY | O_WRONLY, 0)) == -1) { //设定两个不能同时存在的标志会出错
        printf("can't open %s for read only and write only \n", path1);
    }
    if ((fd = open(path1, O_RDWR, 0)) != -1) {
        printf("open %s success, fd : %d\n", path1, fd);
    }
    if ((fd = open(path1, O_RDONLY, 0)) != -1) {    //虽然创建的时候设定了读写模式,但是访问权限只能是只读
        printf("open %s success, fd : %d\n", path1, fd);
    }
    if ((fd = open(path2, O_CREAT | O_EXCL)) == -1) {   //判断文件是否存在,原子操作
        printf("file %s existed\n", path2);
    }
    if ((fd = open(path2, O_RDONLY | O_TRUNC, 0)) != -1) {  //截断文件
        printf("open %s success, fd : %d\n", path2, fd);
    }
    exit(0);
}
运行结果:



结果里面还能看出来一件有意思的事情是文件描述符是从3开始增加的,这点与前面提到的标准输入输出错误占用了文件描述符0、1、2相符合,另外还要补充一点,程序里面没有关闭已经正确打开的文件,虽然进程退出时会进行清理操作,但是平时代码中还是养成自己关闭的习惯为好。

2.2 创建文件

create函数可以用于创建一个新文件,函数原型为:

#include <fcntl.h>
int create(const char *path, mode_t mode);
缺点在于,create函数创建的是只写模式打开的文件描述符,如果需要读文件,需要关闭文件再打开,所以可以直接使用open函数即可;

2.3 关闭文件

close函数可以用于关闭一个打开文件;

#include <unistd.h>
int close(int fd);
关闭一个文件会释放该进程加在该文件上的所有记录锁;当一个进程终止时,内核自动关闭它所有打开的文件,这也是前面例子提到没有主动关闭文件的原因;

2.4 文件偏移

每一个打开的文件都有一个与之对应的“当前文件偏移量”,这里可以提醒一点,同一个文件可以在进程中打开多次,同时对应多个偏移量;偏移量通常是一个非负整数,用来度量从文件开始处开始计算的字节数,通常,读写操作都是从当前文件偏移量处开始,并使偏移量增加所读写的字节数,默认情况下除非指定O_APPEND选项,否则该偏移量被设置为0;

lseek函数可以用于为一个打开文件设置偏移量:

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence)
重点需要了解参数offset和whence的配合使用:

1)若whence为SEEK_SET,则将该文件的偏移量设置为距离文件开始处offset字节;

2)若whence为SEEK_CUR,则将该文件的偏移量设置为当前值加上offset字节,offset可正可负;

3)若whence为SEEK_END, 则将该文件的偏移量设置为文件长度加offset, offset可正可负;

若lseek成功执行,返回新的文件偏移量,若出错,返回-1;注意,并不是什么文件都可以调用lseek的,如FIFO, socket等等;

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
//本例程是用来熟悉lseek的操作
//lseek不能用于管道、FIFO或者套接字
int main()
{
    if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
    {
        printf("cannot seek \n");
    } else {
        printf("seek ok \n");
    }
    exit(0);
}
执行:

lseek仅将当前的文件偏移量记录在内核中,它并不引起任何IO操作,这点很关键,后续的读写操作才是基于该偏移量;

文件偏移量可以大于当前的文件长度,在这种情况下,下一次写操作会加长文件长度,说得很明白,只有完成写才会这样,不写是不是对文件长度有改变的,另外写完之后文件中间是有空洞的存在,不占用磁盘,所以单看文件长度并不代表占用了那么多的磁盘空间;

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int     fd;
    char    buf[1024];
    char    buf1[10] = "add test\n";
    if ((fd = open("test1", O_RDWR | O_APPEND, 0)) == -1) {
        printf("open test1 failed\n");
        exit(1);
    }
    printf("open test1, fd : %d\n", fd);
    if (read(fd, buf, 11) != -1) {
        buf[11] = '\0';
        printf("read : %s\n", buf);
    }
    if (lseek(fd, 5, SEEK_END) != 0) {
        printf("lseek 5\n");
    }
    if (write(fd, buf1, 9) != -1) {
        printf("write : %s", buf1);
    }
}
这是一段有意思的代码,运行代码可以发现,当已O_APPEND模式打开文件时,实际上lseek的操作对write没有影响的,而当去掉O_APPEND选项时,write操作才会产生空洞;另外O_APPEND是针对写操作,读操作还是从文件头开始;

2.5 读取数据

read函数的使用在上面的例程中已经有了,其函数原型为:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes)
如果读取成功,则返回读取的字节数,如已到达文件的尾端,则返回0;

下面是一些实际读取的字节数比设定的nbytes要小的原因:

1)读取普通文件时,在读到要求的字节数之前已经到达了文件尾端;

2)从终端设备读时,一次最多读一行;

3)当从网络读取时,网络的缓冲机制可能造成返回值小于要求的字节数;

4)从管道或者FIFO读取时,若包含的字节少于所需要的数量;

5)从某些面向记录的设备读时,一次最多返回一条记录;

6)当信号造成读取中断,而已经读取部分数据时;

2.6 写入数据

write函数的使用也在上面的例程中有了,其函数原型为:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes)
其返回值与参数nbytes相同,否则表示出错;

三、文件共享

UNIX系统支持不同进程间共享打开的文件,这里首先需要了解下内核中用于IO的数据结构:

1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件的文件描述符表,可将其视为矢量,每个描述符占用一项,与每个描述符相关联的是文件描述符状态(close_on_exec)和一个文件表项的指针;

2)内核为所有打开的文件维持一张文件表,上面提到的指针指向该文件表中,每个文件表包含文件状态(读写,同步,阻塞等)、当前文件偏移量和指向该文件V节点表项的指针;

3)每个打开文件都有一个v节点结构,v节点包含了文件类型和此文件进行各种操作函数的指针;

三者之间的关系如图:


不同进程打开相同文件,两者之间的关联:


四、文件控制函数

4.1 文件描述符复制

#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
这两个函数用于复制文件描述符,dup返回的新文件描述符一定是当前可用的文件描述符的最小值,dup2则是在指定的fd2作为新的描述符的值,如果fd2已经打开则需要先关闭(原子操作),如果fd等于fd2,则不关闭;返回的新的文件描述符与参数fd共享同一个文件表项!!

4.2 改变文件的属性

fcntl函数可以改变打开文件的属性:

#include <fcntl.h>
int fcntl(int fd, int cmd, .../*int arg*/);
这个函数的功能很强大,可以包含如下功能:

1)复制一个已有的描述符(cmd = F_DUPED或者F_DUPED_CLOEXEC)-->这个类似于dup的功能;

2)获取\设置文件描述符标志(cmd = F_GETFD或者F_SETFD);

3)获取\设置文件状态标志(cmd = F_GETFL或者F_SETFL);

4)获取\设置异步IO所有权(cmd = F_GETOWN或者F_SETOWN);

5)获取\设置记录锁(cmd = F_GETLK, F_SETLK或者F_SETLKW).

例程:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int     val;
    if (argc != 2) {
        printf("usage: test <descriptor>");
        exit(1);
    }
    if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) {
        printf("fcntl error for fd %d", atoi(argv[1]));
        exit(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:
              printf("unknown access mode");
    }

    if (val & O_APPEND) printf(", append");
    if (val & O_NONBLOCK) printf(", nonblocking");
    if (val & O_SYNC) printf(", sync");
    printf("\n");
    exit(0);
}
使用fcntl函数需遵循一条,应该首先获取对应的标志,然后再设置想要增加的标志;


猜你喜欢

转载自blog.csdn.net/fusan2004/article/details/52357541