底层文件访问
每个进程都会有一些自己的文件描述符,这些文件描述符表示当前进程正在操作的文件。在一个进程打开一个文件的时候,实际上就是在自己的文件表中创建该文件的文件描述符,创建文件描述符之后,可以通过该描述符对文件进行操作。
当一个程序开始运行的时候,一般都会有三个默认存在的文件描述符。他们分别是:
- 0:标准输入
- 1:标准输出
- 2:标准错误
write
系统调用
系统调用write
的作用是把缓冲区buf
的前nbytes
个字节写入与文件描述符fildes
关联的文件,并返回实际写入的字节数。如果函数返回0
,表示未写入任何数据;如果返回-1
,就表示在write
调用中出现了错误,错误代码保存在全局变量errno
里。
errno
在头文件errno.h
中定义,该头文件还定义了很多表示不同错误类型的宏,如果有需要可以在头文件中查找错误代码对应的错误类型。
write
系统调用的定义在头文件<unistd.h>
里,其形式如下:
size_t write(int fildes, const void * buf, size_t nbytes);
下面是一个例子:
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
int main(void){
if((write(1, "Here is some data\n", 18)) != 18){ //可以将这里的进程描述符1改成3 看会发生什么
write(2, "Write error has occurred on file descriptor 1\n", 46);
printf("An error occurred, errno = %d\n", errno);
}
return 0;
}
read
系统调用
read
的作用是从与文件描述符fildes
相关联的文件里读入nbytes
个字节的数据,并把它们放到缓冲区buf
中。函数返回读到的字节数,如果返回0
则还没读到数据就到文件末尾了,-1
表示出现了错误。
read()
在头文件unistd.h
中定义,其形式为:
size_t read(int fildes, void * buf, size_t nbytes);
下面是一个使用的例子:
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
int main(void){
char buffer[128];
int nread;
nread = read(0, buffer, 128);
if(nread == -1){
write(2, "A read error has occurred/n", 26);
printf("The error code is %d\n", errno);
}
if((write(1, buffer, nread)) != nread){
write(2, "A write error has occrred\n", 27);
printf("The error code is %d\n", errno);
}
return 0;
}
open
系统调用
open()
如果执行成功,就会返回一个可以被read
,write
和其他系统调用使用的文件描述符。这个文件描述符是唯一的,不与其他进程的文件描述符共享。如果两个进程同时打开同一个文件,这两个进程都会创建属于自己的文件描述符。如果两个进程同时写文件,则会按照先后顺序对文件进行修改。他们会接着自己上次离开的位置继续往下写, 所以会彼此覆盖。
open()
在fcntl.h
中定义,其形式为:
int open(const char * path, int oflags);
int open(const char * path, int oflags, mode_t mode);
path
表示文件的名字,oflags
参数用于指定打开文件所采取的动作,用只读,只写还是读写的方式打开。后面的mode
用于制定访问权限。
其中oflags
参数的值必须是下面三种中的一种:
O_RDONLY
:以只读方式打开。O_WRONLY
:以只写方式打开。O_RDWR
:以读写方式打开。
oflags
被指定成上述值之后,还可以在值的后面加上|
,并添加额外的值,额外的值可以是:
O_APPEND
:在文件末尾写入数据。O_TRUNC
:把文件长度设置为0,丢弃已有内容。O_CREAT
:如果需要,就按参数mode
中给定的访问权限创建文件。O_EXCL
:与O_CREAT
一起使用,确保调用者创建出文件。open
调用是一个原子操作,也就是说,它只执行一个函数调用。使用这个可选模式可以防止两个程序同时创建同一个文件。如果文件已经存在,open
调用将失败。
open
调用成功时返回一个新的文件描述符,失败时返回-1
并将错误代码放在全局变量errno
中。
当你使用O_CREAT
作为参数oflags
的值时,必须为mode
参数指定值,mode
参数可能的取值为:
S_IRUSR
:所有者读权限。S_IWUSR
:所有者写权限。S_IXUSR
:所有者执行权限。S_IRGRP
:组内读权限。S_IWGRP
:组内写权限。S_IXGRP
:组内执行权限。S_IROTH
:其他用户读权限。S_IWOTH
:其他用户写权限。S_IXOTH
:其他用户执行权限。
这些参数在头文件sys/stat.h
中定义,并且可以通过|
传入多个,以设置不同等级用户的权限,例如:
open("myfile", O_CREAT, S_IRUSR | S_IXOTH);
该函数创建了一个名为myfile
的文件,并设置其访问权限为所有者只读,其他用户可执行。
umask
环境变量
该环境变量在shell中用umask
命令指定,制定了该变量之后,它将规定当前环境下运行的程序可创建的文件的权限。如果umask
禁止其他用户访问新创建的文件,就算程序在使用open
调用文件设置了S_IROTH
,程序在执行的时候,该选项也会被移除,导致新创建的文件不可被其他用户读。
umask
是由三个八进制数字组成,从左到右依次表示用户
,组
和其他用户
的权限,其指定的方式跟Linux
中chmod
命令指定权限的格式相反,chmod
的数字表示赋予权限,而umask
中的数字表示禁止权限。
close
系统调用
该系统调用解除文件描述符与文件之间的关联,使当前的文件描述符重新变得可用。它在unistd.h
中声明。定义为:
int close(int fildes);
close
在成功时返回0,出错时返回1.
一个例子
这个例子实现了复制文件的过程,使用了上述介绍的系统调用。
程序将输入文件file.in
逐个字符地复制到输出文件file.out
中。
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void){
char c;
int in, out;
//以只读的形式打开文件file.in
in = open("file.in", O_RDONLY);
//以读写的形式打开文件file.out,并且设置只有文件所有者可以读写
out = open("file.out", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
while(read(in, &c, 1) == 1){
write(out, &c, 1);
}
return 0;
}
其他与文件管理相关的系统调用
除了基本的文件打开,关闭,读写操作之外,系统还提供了另外一些对文件描述符进行操作的系统调用,通过这些系统调用,可以更灵活地处理文件。
lseek
系统调用
该系统调用用来设置读写文件的位置,他会设置文件描述符中的读写指针的位置。通过这个调用,可以读写文件中任意一个位置的数据。
它在头文件unistd.h
中定义,并且参数所用到的宏在sys/types.h
中定义:
#include <unistd.h>
off_t lseek(int fildes, off_t offset, int whence);
#include <sys/types.h>
#define SEEK_SET
#define SEEK_CUR
#define SEEK_END
offset
是一个表示位置的数字,该数字的含义由whence
定义,whence
可能的取值如下:
SEEK_SET
:offset
是一个绝对位置。SEEK_CUR
:offset
是相对于当前位置的一个相对位置。SEEK_END
:offset
是相对于文件末尾的一个相对位置。
lseek
返回从文件头到文件设置位置的字节偏移值,失败时返回-1
。
fstat
,stat
和lstat
系统调用
这三个系统调用都会返回文件的信息,并且都会将文件的信息装进一个stat
结构体中,而这三个系统调用的区别如下:
fstat
:通过文件描述符查找文件。stat
:通过文件名查找文件,并且当接受一个链接为参数时,返回链接指向的文件的信息。lstat
:通过文件名查找文件,并且当接受一个链接为参数时,返回链接本身的信息。
这三个系统调用的声明如下:
#include <unsitd.h>
#include <sys/stat.h>
int fstat(int fildes, struct stat * buf);
int stat(const char * path, struct stat * buf);
int lstat(const char * path, struct stat * buf);
结构体stat
的成员可以上百度搜索。
标准I/O库
当使用上面的底层系统调用操作文件的时候,可能会导致很多问题。例如不同系统提供的系统调用不同,使用底层系统调用操作文件会导致程序的可移植性很差。而且使用系统调用的时候,每一次调用都要访问一次磁盘,这样会造成额外的系统消耗,如果我们可以先把数据先放在缓冲区中,等攒到一定数量的数据之后在写入磁盘,这样就能节省系统资源。标准I/O库就为我们解决了这种问题。
标准I/O库的函数都定义在头文件stdio.h
里,就是我们平常使用的fopen
,fread
等函数。