APUE第三章 文件I/O

来源:互联网 发布:飞友网络 编辑:程序博客网 时间:2024/06/01 10:31

函数open和openat

调用open或openat函数可以打开或创建一个文件。

#include<fcntl.h>int open(const char *path, int flag, .../mode_t);int openat(int fd, const char *path, int flag, .../mode_t);//int fd = open("/Users/hupac/Public/k.c", 0666);//int fd = open(fd,"/Users/hupac/Public/k.c",0666);//两者成功则返回文件描述符,失败则返回-1

path是要打开或创建文件的名字,flag参数可以用来说明此函数的多个选项,mode_t是文件访问权限。
重要的flag参数:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读写打开
- O_APPEND:每次写时都追加到文件尾
- O_CREAT:若此文件不存在则创建它,使用此选项必须同时说明mode参数。
- O_EXEC:只执行打开

函数creat

调用函数creat可创建一个文件

#include<fcntl.h>int creat(const char* path, mode_t mode);//int fd = creat("/Users/hupac/Public/k.c", 0666);//int fd = open("/Users/hupac/Public/k.c", O_WRONLY | O_CREAT | O_TRUNC, 0666);//以上两行代码等效

creat的一个不足之处是它以只写方式打开所创建的文件,所以不能读文件的内容。若要对文件同时进行读写操作,则要先调用函数creat,然后再close(fd),再对函数以读的方式调用open。

函数close

#include<unistd.h>int close(int fd);//若成功返回0,失败则返回-1

函数lseek

调用lseek显式地为一个打开文件设置偏移量。

#include<unistd.h>off_t lseek(int fd, off_t offset, int whence);//off_t是long long型变量off_t curr;curr = lseek(fd, 0, SEEK_SET);//获取文件当前偏移量curr = lseek(fd, 0, SEEK_END);//置偏移量为文件尾//成功则返回新的偏移量,失败则返回-1

注:文件偏移量可以大于文件长度,这意味着之后每次写文件操作都会增加文件的长度。

文件的洞

文件中的空洞并不要求在磁盘上占有存储区,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块。但是中间的空洞部分并不需要磁盘块来装载。void Filehole(){char buf1[] = "abcdefghij";char buf2[] = "ABCDEFGHIJ";int fd;if ((fd = creat("/Users/hupac/Public/file.hole", 0666)) <0) {    err_sys("creat error");}if (write(fd, buf1, 10) != 10) {    err_sys("buf1 write error");//现在偏移量=10}if (lseek(fd, 16384, SEEK_SET) == -1)    err_sys("lseek error");if (write(fd, buf2, 10) != 10)    err_sys("buf2 write error");//现在偏移量=16394

}
本例中10-16384位置为空洞,而16384-16394有数据,因此前者不占磁盘块而后者占有磁盘块。
结论:当两个文件的长度相同时,有空洞的文件占用的磁盘块少于无空洞的文件。因为中间空洞部分并不占用磁盘块。

函数read

#include<unistd.h>ssize_t read(int fd, void *buf, size_t nbytes);//ssize_t是类似long型的数据类型long k = read(fd, buf, BUFFSIZE);//返回值为读到的文件字节数,若已到文件尾则返回0,出错返回-1

注:读普通文件时,若在到达文件尾端之前有30个字节,而read函数要求读100个字节,则read函数返回值为30。下一次再调用read时,它将返回0。–>因为read函数会改变当前文件偏移量。

函数write

#include<unistd.h>ssize_t write(int fd, const void *buf, size_t nbytes);long k = write(fd, buf, BUFFSIZE);//返回值同read

read和write实现文件复制

void Dup(char *from, char *to){//实现复制功能int fd = open(from, 0666);int fd1 = creat(to, 0666);long n;int BUFFSIZE = 1;//BUFFSIZE有关I/O性能char buf[BUFFSIZE];while((n = read(fd, buf, BUFFSIZE)) > 0)//buf缓冲区的大小会影响到程序执行效率      if(write(fd1, buf, n) != n)        printf("write error");if (n < 0)      printf("read error");

}
Linux上不同缓冲长度进行读操作的时间结果显示,缓冲区越大,复制操作的执行速度越快。

文件共享

Unix系统支持在不同进程间共享打开文件,内核使用三种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程产生的影响。
- 内核为每个进程分配一张文件描述符表,当进程打开一个文件时,内核默认为其分配0、1、2三个文件描述符。
- 内核为所有打开文件维护一张文件表,文件表的每个表项包含
(1)文件状态标识
(2)当前文件偏移量(lseek函数使用)
(3)指向该文件v节点表项的指针
- 索引节点表

如果两个独立进程各自打开了一个文件,我们假设进程A在文件描述符3上打开该文件,进程B在文件描述符4上打开该文件,则进程A、B都在内核文件表中获得各自的一个文件表项(其中含有文件的当前偏移量和指向索引节点的指针),而该文件表项指向同一个索引节点,如图所示。

根据这三个数据结构,现在对前面所述的操作进一步说明。

  • 在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果当前文件偏移量超过了文件大小,则之后的写入操作(不改变偏移量的情况下)都会使得文件加长。

     void WriteOppend(){        int fd = open(path, 0666);        //打开x.c,此时文件内容为abc123共6个字符        lseek(fd, 10, SEEK_SET);//从文件首部偏移        char buf[] = "mp";        write(fd, buf, 2);}
  • 如果用O_APPEND标志打开一个文件,则相应标志也被设置文件表项的文件状态标志中。这种追加写标志操作文件时,文件的当前偏移量会首先被设置为索引节点表项中的文件长度。以下阐述用lseek函数和write函数来实现O_APPEND方式。

    不通过O_OPPEND实现文件追加内容:

        void Oppend(char *pathname, char *buf, int size){        int fd = open(pathname, 0666);    //    通过read获取文件尾    //    char buf[BUFFSIZE];    //    ssize_t k = read(fd, buf, BUFFSIZE);    //    lseek(fd, k-1, SEEK_SET);//偏移到文件尾,因为有一个\0,    //    所以要减1    //    char *buf1 = "123";    //    write(fd, buf1, 3);    //    通过lseek获取文件尾        lseek(fd, 0, SEEK_END);        write(fd, buf, size);        }
  • lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。

原子操作

追加到一个文件

利用函数lseek追加文件内容和设置O_APPEND标志追加文件内容,两者有本质上的差别。例如进程A调用lseek,将文件偏移量设置为1500(为当前文件末尾)。然后内核切换至进程B运行,B调用函数write写入文件并将文件偏移量增加至1600。此时内核继续调度进程A,当A调用write写入时,就从其lseek获得的偏移量1500继续写入,这样会覆盖掉进程B写入文件的内容。我也遇到过类似情况,如果文件长度为5,进程A若用lseek定位到第5个字符完成写入,然后进程B用lseek定位到第三个字符完成写入,此时进程A的写入内容将会被覆盖。
注1:write和read函数都会使文件偏移量改变。
注2:原子操作一般是由多个命令组成的操作,该操作要么执行完所有命令,要么不执行。

函数pread和pwrite

#include<unistd.h>ssize_t pread(int fd, void *buf, size_t nbytes, off_ offset);//返回读到的字节数,若已到文件尾返回0,出错返回-1;ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);//成功返回已写的字节数,失败则返回-1

调用pread相当于先lseek定位后在调用read函数,pwrite类似,但是:

  • 调用pread无法中断其定位和读操作。

  • pread读完后不更新当前文件偏移量。(read和write都更新offset)

函数dup和dup2

#include<unistd.h>int dup(int fd);//返回当前可用文件描述符中的最小数值int dup2(int fd, int fd2);//成功返回新的文件描述符,出错则返回-1

dup执行过后一个进程的文件描述符表中两个表项(两个文件描述符)共同指向内核文件表中的一个表项。即共享索引节点和文件偏移量。
dup2是一个原子操作,包括close和fcntl两个函数调用。

函数synvc、fsync和fdatasync

#include<unistd.h>int fsync(int fd);int fdatasync(int fd);void sync(void);//成功返回0,出错返回-1

函数fcntl

#include<fcntl.h>int fcntl(int fd, int cmd, .../* int arg */);//成功则依赖于cmd,出错返回-1//fcntl可以改变打开文件的属性

fcntl函数根据cmd的不同来执行不同的文件操作

  • cmd=F_DUPFD | F_DUPFD_CLOEXEC:复制一个已有的文件描述符 //类似dup(fd)的操作,返回最小可用且大于等于第三个整型参数的第一个文件描述符值。
    如int fd1 = fcntl(fd, F_DUPFD, 6);若6可用,则返回6

  • cmd=F_GETFD | F_SETFD:获取|设置文件描述符标志

  • cmd=F_GETFL | F_SETFL:获取|设置文件状态标志

  • cmd=F_GETOWN | F_SETOWN:获取|设置文件异步I/O所有权

  • cmd=F_GETLK | F_SETLK | F_SETLKW:获取|设置记录锁

函数ioctl

ioctl可用于本章其他I/O函数无法使用的情况,例如磁带操作。利用read、write和lseek难于在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,而对磁带进行上述操作的简便方法就是利用ioctl

/dev/fd

打开文件/dev/fd/n等效于复制文件描述符n(假设文件描述符n已打开),类似dup(fd)。在Mac OS中/dev/fd中默认有0、1、2、3、4、5这几个文件描述符。

习题

  1. 当读/写磁盘文件时,本章中描述的函数确实是不带缓冲机制吗?
    请说明原因。
    答:个人理解,这里的read和write函数确实是不带内核缓冲区的。因为数据缓冲高速缓冲是内核体系结构中的,然而这里在利用read和write对磁盘文件进行读写时,read和write函数中都有一个参数const void *buf,这个缓冲区属于编译时分配的用户缓冲区,与缓冲机制还是有不同的。
  2. 编写一个与3.12节中dup2功能相同的函数,要求不调用fcntl函数,并且要有正确的出错处理。

    int Dup2(int fd, int fd2) //感觉应该是错的        {            int fd1 = fd;            fd2 = dup(fd1);            close(fd);            return fd2;        }
原创粉丝点击