本章主要说明系统调用相关的文件I/O函数
基本函数有open、read、write、lseek和close
不带缓冲的I/O,即每个read和write都直接调用内核中的一个系统调用,和后面第五章的标准I/O有所差别,标准I/O是通过标准函数调用系统调用的。
原子操作:由于多进程之间的资源共享问题,特别是在操作文件的时候,我们必须保证同一时刻只有一个进程在操作文件,才能保证文件数据不被搞乱,而原子操作是一种保证操作不被打断直到执行完所有操作。
open函数打开一个文件,返回的是一个和此文件相关联的、当前未被使用的最小文件描述符,文件描述符为非负整数,而0、1、2三个文件描述符早已被系统分别指定为标准输入(STDIN_FILENO),标准输出(STDOUT_FILENO)和标准出错(STDERR_FILENO),因为在linux系统中,一切皆以文件的形式展示给用户,便于操作和管理。文件描述符个数有限,因此在使用完成以后要用close函数关闭。
open函数可以用来创建新文件和打开旧文件,O_CREAT标志表明文件不存在则创建新文件,而第三个参数就是说明以什么权限来创建我们的新文件,一般有读写、执行等权限。当然,如果我们再加上O_EXCL标志,则在创建新文件时,如果文件存在,则会出错,此标志可用于判断文件是否存在。还有很多标志目前尚未使用,待后面章节涉及到,再说明。
还有一个函数openat,与open操作相似,差别在于参数文件路径是绝对路径还是相对路径,其中提到的TOCTTOU错误,是一种竞态条件。即不能保证文件操作是原子的。
文件名和路径名截断,一般取名还是取正常一点的。
create函数用于创建新文件,缺点在于只能以只写的方式打开创建的文件,而要使用其他的权限,又要借用open重新打开新文件关联的文件描述符,因此一般用open代替create函数。
close函数关闭文件并释放加在该文件上的所有记录锁,也就是会释放文件相关的资源。但是进程终止的时候,内核也会自动关闭它所打开的文件。
lseek函数用于操作文件的偏移量。对文件的读写操作均会增加文件的当前偏移量,也会在当前文件偏移量处开始读写。而write对打开的文件进行写操作时,往往会覆盖掉以前的内容,因为文件打开以后偏移在开头,目前不想覆盖的话只能以追加的方式打开文件,即给open添加O_APPEND标志
lseek的返回值为新的文件偏移量,错误返回-1,而在有些设备文件允许lseek的返回可以为负,因此判断lseek是否出错,只能用返回值是否是-1。
关于第三个参数whence,SEEK_SET表示将文件偏移量设置为距离文件开始处offset个字节,这里的文件偏移offset必须为正数。SEEK_CUR表示将该文件偏移量设置为当前偏移量加offset个字节,此时的offset可正可负,此值可用于求文件当前偏移量,也可确定此文件是否支持文件偏移。SEEK_END表示将该文件偏移量设置为文件长度加offset,同样offset可正可负,此值可用于求出文件的长度,将offset设为0即可。
因为文件偏移量只针对拥有实际内容的文本有用,而linux下一切皆文件,比如管道,socket等等,这些我们不能使用lseek操作它,因此可以事先判断。文件偏移量可以大于文件长度,没有内容的位置称为空洞,读为0,只占用磁盘快。
read函数从打开的文件中读取数据,成功返回读到的字节数,到达文件尾返回0,出错返回-1,而关于read的返回值,有多种情况。
- 成功返回读到的字节数,但是有可能比所要求的少,比如已经到达文件尾端,或者所读文件内容少于所需的量
- 错误,errno为EAGAIN和EWOULDBLOCK:fd为非阻塞且没有可读数据
- EINTR:read被信号中断,此时如果数据没有读完,那么我们必须要继续读完,后面的readn函数能做到。
write函数向打开的文件写入数据,和read的问题差不多,也可用writen来说明。就是要注意文件的偏移问题和写时覆盖的问题。
关于I/O效率
首先说说缓冲机制吧,例如我们要搬一车货,可以一件一件地搬,也可以先搬到一个小车上,然后一次拉几件过去,这个小车就是我们的缓冲区,系统要想达到运行快速的效果,肯定想一次性地搬更多的数据呀,但是货多了,我们又能不能搬得动呢?会不会影响效率呢,因此一个合适的缓冲区大小至关重要。系统I/O是不带缓冲的,即每次都是直接进行系统调用,而每次调用系统调用的时间开销有点大,标准I/O带有缓冲机制,可以减少系统调用的次数。缓冲区大小4096开始就是最好的了。
具体的可参考http://www.cnblogs.com/bakari/p/5532810.html
文件共享
进程中文件描述符表记录着文件的状态,这里简要记录三张表
- 进程表项,包含fd和文件指针
- 文件表项,包含文件状态标志、当前偏移量和V节点指针
- V节点表项,包含文件的具体信息,包括文件大小,长度等等。
共享文件的进程可拥有不同的文件表项,因为打开文件的当前偏移量可能不同,而V节点指针指向的V节点表项却是一样的,在write文件的时候,原子操作至关重要。
对于pread和pwrite两个函数,区别在于这两个函数相当于先lseek在read和write,并且不会造成文件偏移。
函数dup和dup2
两个函数返回新的描述符,出错返回-1,其中dup返回最小可用fd,dup2可以指定描述符值,如果已经打开,则先将其关闭,如果两个参数一样,则不关闭。这里复制的新描述符和旧的描述符共享一个文件表项,就包括文件偏移了。用fcntl也可实现文件描述符复制。dup和dup2这两个函数都是原子操作。
/*********************************************************************************
* Copyright: (C) 2018 gaoketongxin
* All rights reserved.
*
* Filename: duptest.c
* Description: This file test dup && dup2
*
* Version: 1.0.0(07/19/2018)
* Author: tangyanjun <519656780@qq.com>
* ChangeLog: 1, Release initial version on "07/19/2018 02:53:57 PM"
*
********************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
/********************************************************************************
* Description:
* Input Args:
* Output Args:
* Return Value:
********************************************************************************/
int main (int argc, char **argv)
{
int wfd = -1;
int fd1 = open("bb.txt", O_RDWR);
int fd2 = open("aa.txt", O_RDWR);
int newfd1 = dup2(fd2, fd1);
if (wfd = write(fd1, "bbbbb", 5) == -1)
{
perror("write");
exit(-1);
}
return 0;
} /* ----- End of main() ----- */
这个程序得到的结果是数据写入了fd2中,即aa.txt文件中,开始有一个疑问,既然dup2已经关闭了fd1,为什么我还能操作fd1,然后依旧将数据成功写入fd2,这是因为文件描述符被关闭以后,重定向为新产生的描述符,所以操作它不会出错,但是操作的结果已经转移到新的文件描述符所指向的文件,他们所指向的是同一个文件表项,因此操作fd1就相当于操作fd2,反过来也是一样。
这里提到dup2可以调用close和fcntl函数实现,但是这不能保证原子操作,因为在close和fcntl之间不能保证没有信号干扰。
延迟写的问题降低了文件内容的同步速率,即先将数据复制到缓冲区,等待一会再写入磁盘,sync函数将修改过的块缓冲区排入写队列就返回,可以实现周期性的冲洗内核的块缓冲区。fsync函数等待写磁盘操作结束返回,确保修改过的块能立即写入磁盘,fdatasync除了数据以外,还会同步更新文件的属性
函数fcntl
5种功能:
- 复制已有的描述符
- 获取/设置文件描述符标志
- 获取/设置文件状态标志
- 获取/设置异步I/O所有权
- 获取/设置记录锁
具体的参数标志用的时候再查把…………………..
这里要注意的问题就是在修改文件描述符标志和文件状态标志时要先获取原来的值,再修改,最后再设置,保证以前的标志不被清除。O_SYNC标志为同步写,是write等待数据写入磁盘完成。因为write并不马上写,只是将数据排入队列,使用此标志表示即写即到,用于非常重要的内容,但是会增加运行时间。
函数ioctl
这个函数在操作设备文件的时候最有用,可以完成read、write、lseek等操作不了的内容。本章对于ioctl讲解甚少,在后面会详细提到。
/dev/fd
此目录下是一些描述符,打开这些描述符相当于是复制描述符,不过最好不要操作这里。
总结:APUE第三章主要讲解文件I/O,无缓冲的,这些函数的基本使用和返回状态要熟练,很多标志也要熟悉。还要注意原子操作的重要性。
补充:
- 所有磁盘I/O都要使用内核的块缓存区,因此不带缓存的I/O,指的是在用户态的时候不带缓冲,每次都要进行系统调用。
opan相同的文件,文件表不一样,其余一样,dup复制的文件描述符,都一样。
下面是一个复制文件的程序
/*********************************************************************************
* Copyright: (C) 2018 gaoketongxin
* All rights reserved.
*
* Filename: ofile.c
* Description: This file makes a test for copy a file
*
* Version: 1.0.0(07/18/2018)
* Author: tangyanjun <519656780@qq.com>
* ChangeLog: 1, Release initial version on "07/18/2018 08:38:30 PM"
*
********************************************************************************/
#include <stdio.h>
/* open() */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h> //perror()
/* exit() */
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024 //the buf size
/********************************************************************************
* Description:
* Input Args:
* Output Args:
* Return Value:
********************************************************************************/
int main (int argc, char **argv)
{
int old_fd = -1;
int new_fd = -1;
char buf[SIZE] = {};
int read_fd = -1;
new_fd = open("bb.txt", O_RDWR | O_CREAT, 0666);
printf("%d\n", new_fd);
if (new_fd == -1)
{
perror("open new");
exit(-1);
}
old_fd = open("aa.txt", O_RDWR);
if (old_fd == -1)
{
perror("open old");
exit(-2);
}
while (1)
{
read_fd = read(old_fd, buf, 5);
if (read_fd == -1)
{
perror("read");
exit(-3);
}
if (read_fd == 0)
{
break;
}
write(new_fd, buf, 5);
}
return 0;
} /* ----- End of main() ----- */