APUE学习笔记1——第三章——文件I/O

APUE学习笔记1——第三章——文件I/O


学号:16340043
中山大学
本博客为《UNIX环境高级编程》的学习笔记,希望能对大家有所帮助


1.前面的废话

学APUE(《UNIX环境高级编程》)本是为了准备学校微软俱乐部的二面,一看大方向是POSIX标准相关,我立马就买了这本书。恩,这很POSIX。然后小方向出来啦,要我们搞*nix shell…然后嘞,我就愉快地跑偏了~虽然书中确实有涉及到shell的内容,但当然没有专门写shell的书深吧…不过,既然学下去了,就顺其自然吧。

身为一个萌新,在CSDN上第一次发博客(还是第一次用markdown,乱搞..),真是压力好大…然而这是学校的作业,还是得要做。有错误的地方欢迎dalao们指出(初学肯定会有错的啦)。

至于为什么从第三章开始嘛…这个…第一第二章…我觉得我真的比较看不进去哦,就更别提记笔记了,直接从干货章开始~OK,那就开始正文吧~


2.博客正文

3.1 引言

本章将说明:open , read , write, lseek , close , dup , fcntl , sync 和 ioctl函数

3.2 文件描述符

文件描述符是一个非负整数,用来标识一个文件(这玩意儿大概就是handle?)
通常,使用open或creat返回的文件描述符标识该文件,将其参数传送给read或write以供使用

  • 特殊的文件描述符有:
符号常量 作用
STDIN_FILENO 0 标准输入
STDOUT_FILENO 1 标准输出
STDERR_FILENO 2 标准错误

这些常量都定义在<unistd.h>中1

3.3 函数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*/);
  • path:打开或创建文件的名字
  • oflag:用来说明打开的方式
oflag参数 打开方式
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
O_EXEC 只执行打开
O_SEARCH 只搜索打开(应用于目录)

以上5个常量中必须且只能指定一个,然后还很多可选的常量之后遇到了再补充…太多了

  • …:表明余下的参数的数量及其类型是可变的,对于这两个函数而言,仅当创建新文件时才使用最后两>个参数
  • fd(文件描述符):在path是相对路径名的情况下有效(是绝对路径名时,openat相当于open)fd参数具有特殊值AT_FDCWD指定在当前工作目录中获取
  • 返回值:若成功,返回文件描述符;若失败,返回-1

openat可以在相对路径名中打开文件,并且能避免TOCTTOU错误

若创建的文件(或路径)名超过了规定的长度,可能有两种行为:

  • 1)将文件名截断为最大的长度
  • 2)返回出错状态,将errno设置为ENAMETOOLONG
    常量_POSIX_NO_TRUNC用来决定默认用什么方式处理文件名过长,可用fpathconf或pathconf来查询目录具体支持何种行为

3.4 函数creat

creat用来创建一个新文件

#include <fcntl.h>
int creat(const char *path, mode_t mode)
  • path同上,mode相当于oflag
  • 返回值:若成功,返回只写打开的文件描述符;若出错,返回-1

其实这个creat等效于:

open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);

由于open提供了选项O_CREAT和O_TRUNC,所以不需要再用单独的creat函数(不用创建了后再用open打开)

oflag参数 功能
O_CREAT 文件不存在时创建它
O_TRUNC 若此文件存在,而且为只写或读-写成功打开,则将其长度截断为0

3.5 函数close

close用来关闭一个打开文件

#include <unistd.h>
int close (int fd);
  • fd:同上
  • 返回值:若成功,返回0;若出错,返回-1

由于一个进程终止时,内核自动关闭它打开的文件。所以一般不用显式地用close关闭文件

3.6 函数lseek

每个打开的文件都有一个”当前文件偏移量”,度量从文件开始处计算的字节数
通常,读、写操作都是从当前文件偏移量开始,并使偏移量增加所读写的字节数

按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项2,否则该偏移量被设置为0

lseek只为一个打开文件修改文件偏移量,不进行任何I/O操作:

#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可正可负
  • 返回值:若成功,返回新的文件偏移量;若出错,返回-1

故可用下列方式确定打开文件的当前偏移量:

off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);

如果文件描述符指向的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE

由于偏移量可能为负值,所以测试时不要测试它是否小于0,而是测试它是否等于-1

文件偏移量可以大于文件当前长度,这种情况下,下次写文件时在文件中构成一个空洞,没有写过的字节都被读成0(空洞不要求在磁盘上占据存储区)

3.7 函数read

read从打开文件中读取数据

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
  • read函数会把参数fd所指的文件传送nbyte个字节到buf指针所指的内存中
  • 返回值:读到的字节数,若已到文件尾,返回0;若出错,返回-1

3.8 函数 write

write向打开文件写数据

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
  • write函数把buf中nbyte个字节写入文件描述符fd所指的文档
  • 返回值:若成功,返回已写的字节数;若出错,返回-1

由于read和write都在内核执行,所以称这些函数为不带缓冲的I/O函数

3.9 I/O的效率

关于如何选取BUFFSIZE的值,随文件系统磁盘快长度而定3

3.10 文件共享

UNIX系统支持在不同的进程中共享打开文件,首先要介绍内核用于I/O流的数据结构:

  • 1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,与每个文件描述符相关联的是:
    a. 文件描述符标志
    b. 指向一个文件表项的指针
  • 2)内核为所有打开文件维持一张文件表,每个文件表包含:
    a. 文件状态标志(读、写、同步、非阻塞等)
    b. 当前文件偏移量
    c. 指向该文件v节点表项的指针
  • 3)每个打开文件都有一个v节点结构,v节点包含:
    a. 文件类型
    b. 对此文件进行各种操作函数的指针
    c. 大多v节点还包含该文件的i节点(索引节点),i节点包含文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等信息

若出现了两个进程各自打开了同一文件,它们拥有不同的文件表项(偏移量未必一样),拥有同一v节点表项

3.11 原子操作

当两个进程同时操作一个文件,可能会因另一进程改变文件长度而发生定位错误问题,使用多个函数可能会发生覆写等问题

于是,便需要原子操作这个工具

原子操作是指多步组成的一个操作,要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集

pread和pwrite原子性地定位并读写文件:

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);

该函数相当于调用lseek后调用read,但是

  • 1) 调用pread时,无法终止其定位和读操作
  • 2) 不更新当前文件偏移量

以下函数

ssize_t pwrite(int fd, void *buf, size_t nbytes, off_t offset);

相当于调用lseek后调用write,特性同上

3.12 函数dup和dup2

dup和dup2用来复制一个现有的文件描述符:

#include <unistd.h>
int dup(int fd);
int dup2(int fd1, int fd2);
  • 返回值:若成功,返回新的文件描述符;若失败,返回-1。返回的新文件描述符一定是可用的中的最小值
  • fd2:指定的新的文件描述符的值,若fd2已经打开,则将其关闭。若fd1等于fd2,则不关闭文件,返回fd2的值

这两个函数返回的新文件描述符与参数fd共享同一个文件表项(也就是说连偏移量都是一样的)

复制一个描述符的另一种方法是使用fcntl函数(3.14):

调用

 dup(fd); 

等效于

 fcntl (fd, F_DUPFD, 0);

调用

dup(fd); 

等效于 4

  close(fd2);   
  fcntl (fd, F_DUPFD, fd2);          

3.13 函数sync、fsync和fdatasync

我们向文件写入数据时,内核先将数据复制到缓存区中,然后排入队列,晚些再写入磁盘。

为了保证磁盘上的实际文件系统与缓存区中的内容一致,UNIX系统提供了sync、fsync和fdatasync这三个函数:

#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
  • 返回值:若成功,返回0;若失败,返回-1
void sync(void);
  • sync只将修改过的块缓存区排入写队列,不等待写磁盘操作结束
  • fsync只对fd指定的一个文件起作用,等待写磁盘操作结束才返回5
  • fdatasync类似fsync,但只影响文件数据部分,会同步更新文件的属性

3.14 函数fcntl

fcntl用来改变已打开的文件的属性

#include <fcntl.h>
int fcntl(int fd, int cmd, .../* int arg*/);
  • fcntl有5种功能,依赖于cmd:
cmd 功能
F_DUPFD或F_DUPFD_CLOEXEC 复制一个已有的描述符
F_GETFD或F_SETFD 获取/设置文件描述符标志
F_GETFL或F_SETFL 获取/设置文件状态标志
F_GETOWN或F_SETOWN 获取/设置异步I/O所有权
F_GETLK、F_SETLK或F_SETLKW 获取/设置记录锁
  • 返回值:与命令有关;若出错,返回-1

在修改文件描述符标志或文件状态标志时需谨慎,要先获取标志值,然后修改他,再设置新的标志值。

不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位

使用fcntl可以在只知道打开文件的描述符的情况下,修改描述符的属性(学管道时会用)

3.15 函数ioctl

ioctl函数是I/O操作的杂物箱,完成别的函数做不到的事,终端是使用ioctl最多的地方

#include <unistd.h>      /* System V */
#include <sys/ioctl.h>   /* BSD and Linux */
int ioctl(int fd, int request, ...);
  • 返回值:与功能有关;若出错,返回-1
  • 省略号表示其余参数,通常是一个变量或结构的指针

终端I/O的ioctl命令都需要头文件<termios.h>

每个设备驱动程序都可以定义它自己专有的一组ioctl命令6

3.16 /dev/fd

较新的系统都提供名为/dev/fd的目录,其目录项是名为0\1\2等的文件。打开文件/dev/fd/n等效于复制描述符n

fd = open("/dev/fd/0", mode);

等效于(因为大多数系统都忽略指定的mode,mode按描述符0原先指定的来)

fd = dup(0);

3.17 小结

本章说明了UNIX系统提供的基本I/O函数,然后…差不多就这样吧~~


3.习题

课后习题,我虽然只做了要写代码的部分,但是也是蛮多的…

习题3.2

  • 编写一个与dup2功能相同的函数,要求不调用fcntl函数,并且要有正确的出错处理
/*好不和谐啊第一个程序题,毫无想法,让我先回顾一下学过的函数,有哪些能提供确切的fd的呢?
open、openat、creat、close、lseek、read、write、pread、pwrite、dup、sync、fsync、fdatasync、ioctl
好像就只有fcntl可以了,然而被禁用了...那我只能搞些效率低的办法了,不确定该想法对不对*/

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

int mydup2(int fd, int fd2){
    int record[fd2];                   //用来记载哪些描述符编号被用来复制
    int count;

    if(fd == fd2)                      //fd与fd2相等,则直接返回,不用处理
        return fd2;

    close(fd2);                        //无论如何,关闭fd2,无法设置错误判断(我没想到)
                                       //因为若fd2不为打开文件和无法关闭它时返回都-1

    memset(record, 0, sizeof(int) * fd2);
    while((count = dup(fd)) < fd2)     //从小到大复制描述符编号...(是不是很傻
        record[count] = 1;
    for(int i = 0; i < fd2; i++)
        if(record[count] == 1)         //再把不该复制的关掉
            if(close(record[count]) == -1){
            printf("another fd point to file fail to close\n");
            }

    if(count == fd2)                   //小于fd2的count的下个数是fd2则代表复制成功
        return fd2;
    else{                              //关闭不了原先fd2所占的文件或中途有别的进程占用了fd2
        printf("dup error\n");
        return -1;
    }
}                                      //我大抵就只能写出这样的东西了,非原子操作(哭


int main(){                            //测试函数
    int fd;
    int fd2;

    if((fd = open("test", O_WRONLY)) == -1){
        printf("open error\n");
        return -1;
    }                                  //打开新文件来测试
    printf("oldfd is %d\n",fd);        //打印该文件的fd

    printf("enter the new fd you want(an integer):");
    if(scanf("%d",&fd2) != 1){         //获取想要复制到的新fd
        printf("you entered sth wrong\n");
        return -1;
    }
    if((fd = mydup2(fd, fd2)) == -1){  //复制
        printf("dup error\n");
        return -1;
    }
    printf("newfd is %d\n",fd);        //打印新fd
    return 0;
}

我只能说我已经尽力了……还有些情况可能没测试到……难……

习题3.6

  • 如果使用追加标志打开一个文件以便读、写,能否仍用lseek在任一位置开始读?能否用lseek更新文件中任一部分的数据?请编写一段程序验证。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(){
    int fd;
    char buf1[] = "not change";        //用来读
    char buf2[] = "heiheihei";         //用来写

    if((fd = open("test", O_RDWR | O_APPEND)) == -1){
        printf("open error\n");
        return -1;
    }                                  //打开新文件来测试

    if(lseek(fd, 10, SEEK_SET) == -1){
        printf("lseek error\n");
        return -1;
    }                                  //测试能否用lseek

    if(read(fd, buf1, 10) == -1){
        printf("read error\n");
    }                                  //测试能否读
    printf("%s\n",buf1);

    if(write(fd, buf2, 9) == -1){
        printf("write error\n");
    }                                  //测试能否写
    return 0;
}

最终结果:可用lseek在任一位置读,但写的时候还是会跳到最后面去写。

这题…应该是…这么做的吧

求解答的问题

  • ./a.out > outfile 2>&1
  • ./a.out 2>&1 > outfile

两者有什么不同?看了答案还是不懂0.0


4.后面的废话

搞完了这篇博客,累觉不爱……就算是在原有笔记上加工,也很耗时间,也差不多就这样啦。其实这笔记本是昨天写的,今天还有一篇,我看能不能赶工赶出来(新的课后习题还没做,比较悬)/逃



  1. 为了提高代码可读性,应尽量使用符号常量来表示
  2. O_APPEND : 每次写时都追加到文件尾端
  3. 这节书上列出了很多执行时间的表,有兴趣可以买书看看
  4. 其实不完全等效,前者是原子操作,且两者拥有不同的errno
  5. 一般用于数据库这样需要确保修改过的块立即写到磁盘上的应用程序
  6. 该函数会在18\19节中再有讲解
发布了5 篇原创文章 · 获赞 4 · 访问量 2192
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览