引言
本文主要介绍文件IO操作相关的系统调用,一个通用的IO模型包含打开文件、读写文件、关闭文件三个基本操作,这些操作主要涉及到四个函数:open()、read()、write()和close(),本文将结合具体实例讲解着四个函数的使用方法
文件描述符
我们对文件进行操作的第一步是打开文件,在学习之初我在想为什么不能直接对文件进行读写。后面学习到所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件,而文件描述符是通过调用open()函数生成的,所以所有的文件操作都是从打开文件开始。
使用open()函数成功打开一个文件后会返回一个文件描述符,后续的IO操作都将使用次描述符对相应的文件进行读、写以及关闭。文件描述符是一个非负的整型数据。
代码示例:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(){
int fd;
fd = open("1.txt",O_RDONLY);
//fd = open("2.txt",O_RDONLY);
if(fd < 0){
printf("open file failed!\n");
printf("fd1=%d\n",fd);
return fd;
}
else{
printf("open file success...\n");
printf("fd1=%d\n",fd);
}
return 0;
}
与代码文件同级目录下创建“1.txt"文件,且同级目录下没有"2.txt"文件,
- 当程序打开“1.txt"文件时运行结果如下所示:
- 当程序打开“2.txt"文件时运行结果如下所示:
通过代码实例可以验证,成功打开一个文件时会返回一个非负的整型数据;反之,返回一个负数。建议在使用open()函数时,通过返回值判断是否正确打开文件。
一个进程可打开的最大文件数
一个进程可以打开多个文件,但是在Linux系统中,一个进程(进程的知识后续专题会进行详细讲解)可以打开的文件数是有限制的,并非是可以打开无限个文件,如果超过进程可以打开的最大文件数限制,内核将会发送警告信号给对应的进程,然后结束进程。在Linux系统下,可以通过不过如下命令查看进程可以打开的最大文件数
ulimit -n
运行结果如下所示:
该最大值默认情况下是1024,也就意味着在默认情况下单个进程可以打开的最大文件数为1024个,那么得到的文件描述符的范围为0~1023. 每次打开文件分配的文件描述符都是从最小的没有被使用过的文件爱你描述符(0~1023)开始。当之前的文件关闭后,该文件对应的文件描述符就会被释放,释放后的文件描述符可以继续使用。下面开始正式讲解文件IO中open()、read()、write()以及close()四个函数的使用方法。
open打开文件
函数原型
在 Linux 系统下,可以通过 man 命令(也叫 man 手册)来查看某一个 Linux 系统调用的帮助信息,man 命令可以将该系统调用的详细信息显示出来,譬如函数功能介绍、函数原型、参数、返回值以及使用该函数所需包含的头文件等信息;,man 命令用法如下所示:
man 2 open #查看 open 函数的帮助信息
运行结果如下:
技巧:man 命令后面跟着两个参数,数字 2 表示系统调用,man 命令除了可以查看系统调用的帮助信息外,还可以查看 Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息;最后一个参数 open 表示需要查看的系统调用函数名。后续几个函数的原型也通过此方法查找,不再赘述。
函数原型解析
函数原型分析
从上图可知,open 函数有两种原型?由于C语言不支持重载,所以此处解释为可变参函数。由此可知,在应用程序中调用 open 函数即可传入 2 个参数(pathname、flags)、也可传入 3 个参数(pathname、flags、mode),但是第三个参数 mode 需要在第二个参数 flags 满足条件时才会有效,稍后将对此进行说明。
参数以及返回值详解
pathname
pathname:字符串类型,用于标识需要打开或创建的文件(open函数可以打开已存在的文件,也 可以创建并打开新文件),用于包含路径信息,可以是绝对路径也可以是相对路径;如果 pathname是一个符号链接,会对其进行解引用。
flags
flags:调用open函数时需要提供的标志,包含文件访问模式标志以及其他文件相关标志,这些标志使用宏定义描述,都是常量。可以使用单独使用某一个标志也可以使用( | )将多个标志进行组合;常用的一些标志的详细信息如下所示(后续还会对其他的标志进行讲解):
mode
mode:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)
mode是一个u32无符号整型数据,权限的表示方法如下:
每3个bit为分一个组;每一组代表的含义如下:
每一组的三个bit位中从高位到低位分别表示 读、写、可执行三个权限,对应位置为1表示拥有相应的权限,反之没有相应的权限
Linux中已经定义了相应的宏,不同的宏定义代表不同的权限,这些宏定义可以单独使用也可以使用‘ | “ 位运算符结合使用,对应的权限如下表所示:
返回值
成功时返回对应文件的文件描述符,文件描述符是一个非负整数(关于文件描述符可以参考之前章节),失败时返回-1。
应用实例
程序所在的目录内容如下所示:
实例1:打开已经存在的文件
1、使用只读的模式打开 1.txt文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(){
int fd;
fd = open("1.txt",O_RDONLY);
if(fd < 0){
printf("open file failed!\n");
printf("fd=%d\n",fd);
return fd;
}
else{
printf("open file success...\n");
printf("fd=%d\n",fd);
}
return 0;
}
输出结果如下所示:
2、使用可读可写的方式打开文件1.txt
fd = open("1.txt",O_RDWR);
注意:本人将程序改为以读写的方式打开文件,第一次编译运行后显示返回-1,打开文件失败;出现这种状况可能是程序问题,也有可能时相应文件夹的权限问题,查看代码没有错误,通过修改所属文件夹的权限后再进行编译,程序正常运行。修改权限的命令如下所示:
sudo chmod 777 -R filePath
- chmod 权限修改命令
- "777"代表要更改的权限(可参考mode节选择合适的权限值)
- -R 代表操作对象为文件夹
- filePath代表具体的文件夹路径
3、使用可读可写的方式打开文件,如果文件是一个符号连接则不对其进行解引用,直接返回错误
- 创建符号链接文件命令
ln -s 源文件 目标链接文件
在目录创建link.txt符号链接文件,指向1.txt位置
ln -s 1.txt link1.txt
- 删除符号链接文件命令rm link.txt
rm link.txt
- 打开link.txt文件,显示打开失败,返回-1
实例2:创建文件
创建一个文件,如果文件存在则返回错;如果文件不存在则创建文件,并设置如下权限
- 文件所属者:读写可执行
- 同组用户:只读
- 其他用户:只读fd = open("hello.txt",O_EXCL | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH);
代码1:
fd = open("hello.txt",O_EXCL | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH);
运行后文件夹下生成hello.txt文件:
代码2:
fd = open("1.txt",O_EXCL | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH);
运行结果返回错误
write写文件
函数原型
write函数可以向打开的文件中写入数据,其函数原型如下所示(可使用 “man 2 write”查看)
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数以及返回值详解
fd
fd: 成功调用open函数返回的文件描述符,使用open函数打开文件时至少要用可写的方式打开
buf
buf:指定写入数据对应的缓冲区
count
count:指定写入的字节数
返回值
- 如果成功写入文件,返回写入的字节数
- 返回0表示未写入任何字节
- 如果返回值小于count(输入的字节数),这不是错误,可能是磁盘已满
- 如果写入错误返回-1
写入的位置
- 默认从文件的当前位置偏移量开始,在成功写入后,偏移量会自动移动相应的字节数
- 也可以使用lseek函数手动设置偏移量,这个函数后续章节会进行详细说明
read读文件
函数原型
read从打开的文件中读取数据,其函数原型如下所示(可以通过 “man 2 read” 查看)
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数以及返回值详解
fd
fd:文件描述符
buf
buf:用于存储读取数据的缓冲区
count
count:指定需要读取的字节数
返回值
- 读取成功则返回读取到的字节数
- 当文件偏移量到了文件尾,则都不到数据,返回0
- 当文件的偏移量到文件尾的字节数小于参数count,则返回值小于count
close关闭文件
函数原型
使用close关闭一个打开了的文件,其函数原型如下所示(可以使用" man 2 close " 查看)
#include <unistd.h>
int close(int fd);
参数以及返回值详解
fd
fd:文件描述符
返回值
返回值:成功返回0,失败返回-1
tips:如果打开的文件未使用close关闭,当进程终止时内核会自动关闭程序中打开的文件,养成关闭打开的文件的习惯,减少资源浪费。
lseek设置文件偏移量
函数原型
读写偏移量用来指示read()或write()函数操作时文件的起始位置,每次调用read()或write()都会使得偏移量发生改变,指向已读或者已写数据后的下一个字节。lseek函数对文件的偏移量进行设置,其函数原型如下所示(可以使用“man 2 lseek” 进行查看):
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数以及返回值详解
fd
fd:文件描述符
offset
offset:偏移量,以字节为单位
whence
whence用于定义参数offset偏移量对应的参考值,参考值的宏定义如下表所示