unix-like(后面以linux为例)系统中的文件操作只需要五个函数就足够了,open、close、read、write以及lseek。这些操作被称为不带缓存的io,这里有必要说一下带缓存和不带缓存的操作的区别。不带缓存的io是相对于带缓存的io的来说的,带缓存的io有哪些呢。在后面的fwrite、fread函数等标准io操作都是带缓存的io。说了这么多还是没有说清楚带缓存和不带缓存的区别,下面就以write函数和fwrite函数来说明带缓存和不带缓存的区别。虽然write函数叫做不带缓存的io,但是当write函数向文件中写入数据时,会先将数据写入到内核的一个缓冲区中,注意这个缓冲区是由内核提供的,当内核的缓冲区填满以后,内核将缓冲区中的数据排队到输出队列的末端,然后由内核相应的功能将输出队列的数据输出到磁盘,所以write函数还是有缓冲的,我们也将write函数称作系统调用。下面介绍了fwrite函数之后就可以理解为什么write函数叫不带缓存的函数。fwrite函数是对write函数封装的结果。在封装时,在用户的层面上提供了一些缓冲区,当向文件中写入数据时,只有当用户层面的缓冲区填满时,才会将这些缓冲区中的数据写入到内核的缓冲中。这样就可以理解带缓存的io和不带缓存的io,这都是相对于用户层面而言的。
在linux系统中,为每一个打开的文件分配一个文件描述符。所有的程序都打开了三个文件,标准输入,标准输出和标准出错。这三个文件的描述符分别为0,1,2,或者分别用宏定义STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO来表示。当打开一个其它非标准输入输出文件时,则返回一个系统中尚未使用的编号最小的文件描述符,当然这个描述符必定是大于或者等于3的。
open函数用来打开一个文件,它的函数原型是:
int open(const char* pathname,int oflag,...);
第一个参数是用来指定所要打开的文件的路径和名字,第二个参数用来指定文件的打开方式,第三个参数...表示后面可以有任何类型的任意多个参数,不过第三个参数只有在用open函数创建新文件时才用来指定新建文件的权限。第二个参数可以有的选择有:
O_RDONLY:以只读方式打开文件
O_WRONLY:以只写方式打开文件
O_RDWR:以可读可写的方式打开文件
实际上面的三种选择的值分别为0,1和2。在初学linux编程时比较容易犯的一个错误是,文件以可读可写方式打开时,写成如下的形式:
int fd = open("filename" , O_RDONLY | O_WRONLY);
O_RDONLY | O_WRONLY表示将两个宏按位进行或运算,所以这两个的运算就是 0 | 1 ,结果是1,也就是文件是按照只写的方式打开的,并不是按照可读可写的方式打开的。另外初学者还有一个疑惑,就是当文件以O_WRONLY只写方式打开时,能不能读呢?我当时就这么想,既然都可以向文件里面写数据了,为什么不能从文件中读呢?后来,我发现问题其实很简单,只是我将“读”理解错了,这里的读是机器读而不是人在读。显然当文件以只写的方式打开时,是不能将文件的内容从文件读到计算机中的。上面三种对文件的打开方式是互斥的,即三者只能存在其一。还有一些和上面三个参数进行搭配的选择,包括:O_APPEND、O_CREAT 、O_TRUNC 、O_EXCL 、O_NOCTTY 、O_NONBLOCK等,后面的这几个选项可以和前面的三个选择以按位或的方式并存。open函数如果调用成功,则返回打开文件的描述符,如果出错则返回-1,linux系统返回的文件描述符是系统尚未使用的最小的文件描述符。
creat函数的作用是创建一个新的文件,它的函数原型是:
int creat(const char* pathname,mode_t mode);
第一个参数用来指定要创建的文件的路径和名字,第二个参数用来指定创建文件的相关权限信息。creat创建的文件只能进行写操作,它相当于:
open("filename",WRONLY | O_TRUNC | O_CREAT,mode);
由于creat函数创建的文件只能向其中写入数据,所以如果需要读时,需要用以下的方式创建文件:
open("filename",O_RDWR | O_CREAT | O_TRUNC,mode);
creat函数如果调用成功,则返回打开文件的描述符,如果出错则返回-1。
close函数用来关闭打开的文件,它的函数原型是:
int close(int filedes);
它的参数是一个文件描述符,如果调用成功,则返回0,否则返回-1。
lseek函数用来设置文件指针的位置,它的函数原型是:
off_t lseek(int filedes,off_t offset,int whence);
第一个参数为打开的文件描述符,第二个参数是相对于第三个参数的偏移量,第三个参数是一个标记。第三个参数有三种选择,分别是:
SEEK_SET:表示从文件起始位置开始
SEEK_CUR:表示从当前文件指针位置开始
SEEK_END: 表示从文件末尾开始
对于SEEK_SET来说,第二个参数偏移量只能为非负值,对于SEEK_CUR来说,第二个参数偏移量可正可负,对于SEEK_END来说,偏移量也是可正可负。如果SEEK_END,偏移量为正数时,会将文件指针,从当前文件末尾向后移动,中间掠过的位移填充为空,这种文件称为空洞文件。lseek如果成功返回,则返回当前文件指针的位移,如果出错则返回-1。在有些特殊的文件中,文件指针的位移允许为负值,所以,如果判断lseek是否执行成功,最好用返回值是否为-1来进行判断。
read函数用来从一个已打开的文件中读取文件的内容,它的函数原型是:
ssize_t read(int filedes,void* buff,size_t nbytes);
第一个参数是一个已打开的文件的描述符,第二个参数是一个缓冲区的指针,第三个参数是期望一次读出多少数据。这个函数的作用是每次将文件的内容读取到缓冲区buff中,如果执行成功,则返回所读取的实际字节数,如果失败,则返回-1,如果到达文件末尾,则返回0。
write函数用来向文件中写入数据,它的函数原型是:
ssize_t write(int filedes,const void* buff,size_t nbytes);
第一个参数是一个文件描述符,第二个参数是一个缓冲区的首地址,第三个参数是将要写到文件中的字节数。这个函数解释为向filedes中写入缓冲区buff的前nbytes个数据。如果执行成功,则返回实际写入到文件中的字节数,如果失败则返回-1。