因为APUE的强大之处,所以引用APUE大部分内容
可用的文件I/O函数,打开文件,读文件,写文件等,UNIX系统中大多数文件I/O只用5个函数:open,read,write,lseek,close
注:read,write是不带缓冲的文件I/O,不带缓冲指:每次read,write都调用内核中的一个函数调用
在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。
文件描述符(进程级别):
1)、在每个进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视察为一个矢量(数组),每个描述符占用一项。与每个文件描述符相关联的是:
(a)、文件描述符标志。
(b)、指向一个文件表项的指针。(不同的文件描述符可以指向同一个文件表项,例如用dup函数复制文件描述符,得到一个新的文件描述符,但是它们指向同一个文件表项,因此它们也指向同一个V节点表。)
文件表项
2)、内核为所有打开的文件维持一张文件表(同一个文件可以被打开多次,所以同一个文件可以在文件表中占有多项)。每个文件表项包含:
(a)、文件状态标志(读、写、添写、同步和非阻塞等)
(b)、当前文件偏移量(不同的文件表项的V节点指向可以指向同个V节点表,也就是指向同一个真实的物理文件。因为一个文件表项里包含当前文件偏移量,所以不同的文件表项看到同一文件的偏移量可以不同,这种安排可以使每个进程都有它自己的对该文件的当前偏移量。)
(c)、指向该文件的V节点表项的指针。
V节点表项
(3)、每个打开文件(或设备)都有一个V节点结构。V节点包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,V节点还包含了该文件的I节点。
原子操作
考虑一个进程,它要将数据添加到一个文件尾端。早期的UNIX系统版本并不支持open的O_APPEND选项,所以程序被编写成下列形式:
if (lseek(fd, 0L, 2) < 0) /* position to EOF */ err_sys("lseek error"); if (write(fd, buf, 100) != 100) /* and write */ err_sys("write error");
对单个进程而言,这段程序能正常工作,但若对多个进程同时使用这种方法将数据添加到同一文件,则会产生问题。(例如,若此程序由多个进程同时执行,各自将消息添加到一个日志文件中,就会产生这种情况。)
假定有两个独立的进程A和B都对同一个文件进行操作,给个进程都已打开了该文件,但未使用O_APPEND标志。此时,各数据结构之间的关系如图2所示。每个进程都有自己的文件表项,但是共享一个v节点表项。假定进程A调用了lseek,它将进程A的该文件当前偏移量设置为1500字节(当前文件尾端处)。然后内核调度进程使进程B运行。进程B执行sleek,也将其对该文件的当前偏移量设置为1500字节(当前文件尾端处)。然后B调用write函数,它将B的该文件当前文件偏移量增值1600.引文该文件的长度已经增加了,所以内核对v节点中的当前文件长度更新为1600.然后,内核又进行进程切换使进程A恢复运行。当A调用write时,就从其当前文件偏移量(1500字节)处将数据写到文件中去。这样就代换了进程B刚写到该文件中的数据。
问题出在逻辑操作“定位到文件尾端处,然后写”上,它使用了两个分开的函数调用。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。任何一个需要多个函数调用的操作都不可能是原子操作,因为在两个函数调用之间,内核有可能会临时挂起该进程。
UNIX系统提供了一种方法是这种操作成为原子操作,该方法是在打开文件时设置O_APPEND标志。正如前面所述,这就是内核每次对这种文件进行写之前,都将进程的当前偏移量设置到文件的尾端处,于是在每次写之前就不在需要调用sleek了。
pread和pwrite函数
Single UNIX Specification包括了XSI扩展,该扩展允许原子性地定位搜索(seek)和执行I(/O。pread和pwrite就是这种扩展。
#include <unistd.h> ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset); 返回值:读到的字节数,若已到文件结尾则返回0,若出现错误返回-1 ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset); 返回值:若成功则返回已写的字节数,若出错则返回-1一般而言,原子操作(atomic operation)指的是由多步组成的操作,如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
<pre name="code" class="cpp">#include <fcntl.h> int open(const char *path,int oflag,.../*mode_t mode*/); int openat(int fd,const *path,int oflag,.../*mode_t mode*/); int create(const char*path,mode_t mode);
#include <unistd.h> int close(int fd); off_t lseek(int fd,off_t offset,int whence);
#inude <unistd.h> ssize_t read(int fd,void *buf,size_t nbytes); ssize_t write(int fd,const void *buf,size_t nbytes);
#include <unistd.h> int dup(int filedes); int dup2(int filedes, int filedes2); 两函数的返回值:若成功则返回新的文件描述符,若出错则返回-1
由dup返回的新文件符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。如若filedes等于filedes2,则dup2返回filedes2,而不关闭它。
这些函数返回的新文件描述符与参数参数filesdes共享同一个文件表项。如图所示。
我们假定进程执行了:
当此函数开始执行时,假定下一个可用的文件描述是3(这是非常有可能的,因为0、1、2是由shell打开的)。因为两个描述符指向同一文件表项,所以它们共享同一个文件状态标志(读、写、添加等)以及同一文件当前偏移量。
每个文件描述符都有它自己的一套文件描述符标志。新描述的执行时关闭(close-on-exec)标志总是由dup函数清除。
复制一个描述符的另一种方法是使用fcntl函数,实际上,调用
等效于
而调用
等效于
在后一种情况下,dup2并不完全等同于close()加上fcntl.它们之间的区别是:
#include <unistd.h> void sync(void); 将所有修改过的块缓冲区排入写队列,然后返回,不等待写磁盘的操作结束 int fsync(int fd); 只对文件描述符fd管用,等待写磁盘操作结束才返回,可用于数据库的应用程序 int fdatasync(int fd); 类似于fsync,但他只影响文件的数据部分,相比fsync,fsync还会更新文件的属性
#include <fcntl.h> int fcntl(int fd,int cmd,.../* int arg*/);
#include "apue.h" #include <fcntl.h> 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)) < 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 defined(O_SYNC) if (val & O_SYNC) printf(", synchronous writes"); #endif #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) if (val & O_FSYNC) printf(", synchronous writes"); #endif putchar('\n'); exit(0); }调用结果
root@ubuntu:/test/linux/20151228# ./a.out 0 < /dev/tty read only root@ubuntu:/test/linux/20151228# ./a.out 1 > /dev/tty write only