文件I/O
内核中存在一系列具备预定功能的函数,操作系统将这些函数功能抽象为一组被称为系统调用的接口,提供给应用程序使用,open()、read()、write()、lseek()、close()等都是系统调用中与I/O操作相关的接口。下面对系统调用级的I/O接口函数进行讲解。
1、 open()函数
open()函数的功能是打开或创建一个文件,该函数存在于系统函数库fcntl.h中,其函数声明如下:
int open(const char *pathname,int flags[,mode_t mode]);
open()函数的第一个参数通常为待打开文件的文件路径及名称;第二个参数为文件的访问模式,一般使用定义在函数库fcntl.h中的一组宏来表示,常用的宏及其含义如表1所示。
表1 文件访问模式相关宏定义
编号 | 宏 | 说明 |
---|---|---|
① | O_RDONLY | 以只读方式打开文件 |
② | O_WRONLY | 以只写方式打开文件 |
③ | O_RDWR | 以读写方式打开文件 |
④ | O_CREAT | 创建一个文件并打开,若文件已存在则会出错 |
⑤ | O_EXCL | 测试文件是否存在,若不存在,则创建文件 |
⑥ | O_NOCTTY | 若pathname为终端设备,则不会将该设备分配给对应进程作为控制终端 |
⑦ | O_TRUNC | 当以只写或读写方式成功打开文件时,将文件长度截断为0 |
⑧ | O_APPEND | 以追加的方式打开文件 |
其中编号1~3的宏必须使用,且一次只能使用1个;编号4~8的宏可有选择地使用管道符号“|”与前3个宏搭配使用。只有第二个参数flags=O_CREAT时,第三个参数才会被使用,该参数的作用是设置创建文件的权限,取值如表2所示。
表2 参数mode相关取值
mode | 说明 |
---|---|
S_IRWXU | 文件所有者对文件具有读、写与执行权限 |
S_IRUSR | 文件所有者对文件具有读权限 |
S_IWUSR | 文件所有者对文件具有写权限 |
S_IXUSR | 文件所有者对文件具有执行权限 |
S_IRWXG | 文件所属组对该文件有读、写与执行权限 |
S_IRGRP | 文件所属组对该文件有读权限 |
S_IWGRP | 文件所属组对该文件有写权限 |
S_IXGRP | 文件所属组对该文件有执行权限 |
S_IRWXO | 其他人对该文件有读、写与执行权限 |
S_IROTH | 其他人对该文件有读权限 |
S_IWOTH | 其他人对该文件有写权限 |
S_IXOTH | 其他人对该文件有执行权限 |
open()函数的返回值为一个整数,若函数调用成功,则会返回一个文件描述符,否则返回-1。
使用如下所示的open()函数可以创建一个文件:
open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode);
也可以使用系统调用中专门用于创建文件的函数——creat()函数,creat()的函数声明如下:
int creat(const char *pathname,mode_t mode);
该函数的第一个参数为路径名,第二个参数用于为文件设定权限,取值同表5-6。
creat()函数的返回值与open()函数相同,若文件创建成功,会返回一个文件描述符;否则返回-1。
2、 read()函数
read函数用于从已打开的设备或文件中读取数据,该函数存在于函数库unistd.h中,其函数声明如下:
ssize_t read(int fd, void *buf, size_t count);
read()函数基于文件描述符对文件进行操作,其中第一个参数为从open()函数或creat()函数获取的文件描述符;第二个参数为缓冲区;第三个参数为计划读取的字节数。调用read()函数后,该函数会从文件描述符fd对应的文件中读取count个字节的数据,存储到缓冲区buf中,并重新记录文件偏移量。
read()函数的返回值类型为ssize_t,表示有符号的size_t。read()函数的返回值可以是正数、0或者-1:若读取文件时出错,返回-1;若成功读取文件,则返回一个正数,该正数一般为本次请求读取的字节数,但在读取常规文件时,文件长度有限,若当前读写位置距文件末尾只有20个字节,但该函数请求读取30个字节,那么在第一次读取时,read()的返回值为20,第二次读取时,文件读写位置已在末尾,此时会返回0。
Linux系统中将一切都视为文件,因此read()函数也可以从设备或网络中读取数据。read()是一个阻塞函数,从常规文件中读取数据时,read()必定会在有限时间内返回,但从终端设备或网络端读取数据时,read()函数可能会阻塞。例如在程序中调用read()函数,该函数要求从终端读取数据,但终端写入的数据中没有回车,那么该数据就不会被传送给read()函数,read()函数就会一直阻塞;若要求read()函数从网络端读取数据,用于网络通信的socket文件中没有数据,read()函数同样会阻塞。
3、 write()函数
write()函数用于向已打开的设备或文件中写入数据,该函数存在于函数库unistd.h中,其函数声明如下:
ssize_t write(int fd, const void *buf, size_t count);
write()函数的第一个参数为文件描述符;第二个参数为需要输出的缓冲区;第三个参数为最大输出字节数。当write()函数调用成功时返回写入的字节数;否则返回-1,并设置errno。
同read()函数一样,write()在写常规文件时,会立刻返回请求写入的字节数count,但向终端或网络端写数据时,可能会进入阻塞状态。
4、 lseek()函数
每个打开的文件都有一个当前文件偏移量(current file offset),该数值是一个非负整数,表示当前文件的读写位置,Linux系统中可以通过系统调用lseek()对该数值进行修改,lseek()函数位于函数库unistd.h中,其函数声明如下:
off_t lseek(int fd, off_t offset, int whence);
lseek()函数中第一个参数fd为文件描述符;第二个参数offset用于对文件偏移量的设置,该参数值可正可负;第三个参数whence用于控制设置当前文件偏移量的方法,该参数有3个取值:
(1) 若whence为SEEK_SET,文件偏移量将被设置为offset;
(2) 若whence为SEEK_CUR,文件偏移量的值将会在当前文件偏移量的基础上加上offset;
(3) 若whence为SEEK_END,文件偏移量的值将会被设置为文件长度加上offset。
lseek()函数的返回值类型与参数offset相同,若偏移量设置成功,则会返回新的偏移量,否则返回-1。
5、 close()函数
打开的文件在操作结束后应该主动关闭,Linux系统调用中用于关闭文件的函数为close()函数,该函数的使用方法很简单,只要在函数中传入文件描述符,便可关闭文件。close()函数位于函数库unistd.h中,其声明如下:
int close(int fd);
若函数close()成功调用,则返回0,否则返回-1。
Linux系统中文件相关的5个基础I/O函数已讲解完毕,下面通过一个案例,来演示这5个函数的使用方法。
案例2:使用open()函数打开或创建一个文件,将文件清空,再使用write()函数在文件中写入数据,并使用read()函数将数据读取并打印。
案例实现如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <fcntl.h>
5 #include <string.h>
6 int main()
7 {
8 int fd=0;
9 char filename[20]="./itheima/a.txt";
10 //打开文件
11 fd=open(filename,O_RDWR|O_EXCL|O_TRUNC,S_IRWXG);
12 if(fd==-1){ //判断文件是否成功打开
13 perror("file open error.\n");
14 exit(-1);
15 }
16 //写数据
17 int i=0,len=0;
18 char buf[100]={0};
19 while(i<3)
20 {
21 scanf("%s",buf);
22 len=strlen(buf);
23 write(fd,buf,len);
24 i++;
25 }
26 close(fd); //关闭文件
27 printf("---------------------\n");
28 //读取文件
29 fd=open(filename,O_RDONLY); //再次打开文件
30 if(fd==-1){
31 perror("file open error.\n");
32 exit(-1);
33 }
34 off_t f_size=0;
35 f_size=lseek(fd,0,SEEK_END); //获取文件长度
36 lseek(fd,0,SEEK_SET); //设置文件读写位置
37 while(lseek(fd,0,SEEK_CUR)!=f_size) //读取文件
38 {
39 read(fd,buf,1024);
40 printf("%s\n",buf);
41 }
42 close(fd);
43 return 0;
44 }
使用gcc工具编译以上代码,执行二进制文件,代码运行后根据题述输入数据,运行结果如下所示:
1:itheima
2:C/C++
3:Linux
---------------------
1:itheima2:C/C++3:Linux
由运行结果可知,read()成功读取指定文件中的数据。
Linux系统调用中的I/O又被称为无缓存I/O,除此之外,在程序编写时我们还可以使用一种有缓存的I/O。有缓存的I/O又被称为标准I/O,是符合ANSI C标准的I/O处理,标准I/O有两个优点,一是执行系统调用read()和write()的次数较少;二是不依赖系统内核,可移植性强。
系统调用中的I/O虽然被称为无缓存I/O,但并不是说它的整个操作过程没有使用缓存。在用户通过read()或write()向内核发送请求时,内核会先将要读写的数据写入系统内存的缓存区中,待系统的缓存区存满时,再对数据统一进行一次操作。系统内存区缓存的存在,减少了内存与磁盘之间的读写次数。
标准I/O在用户层建立了一个流缓存区,当用户进程调用标准I/O请求执行读写操作时,要读写的数据会先被写入流缓存区,当流缓存区写满或读写完毕时,内核再通过函数调用,将其中的数据写入内存缓存区中,如此便减少了内核调用read()和write()的次数。系统I/O与标准I/O与内存缓存区的关系如图1所示。
图1 系统I/O、标准I/O与内存缓存关系示意图
结合图1,若进行写操作,对于无缓存的系统I/O,数据走过的路径为:数据——内存缓存区——磁盘;对于标准I/O,数据走过的路径为:数据——流缓存区——内存缓存区——磁盘。标准I/O中常用的接口为:fopen()、fwrite()、fread()、fseek()、fclose()、fputs()、pgets()等,这些函数与系统I/O函数的使用方法大致相同,读者可参考相关资料自行学习,此处不再赘述。