常用的文件操作函数:open、read、write、lseek以及close。
read和write都是不带缓冲的I/O操作,直接调用内核中的一个系统调用。不带缓冲是指write和read函数不带缓冲,内核有缓冲区。
文章目录
1. 文件描述符
- 文件描述符是一个非负整数
- 当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符
- 按照惯例,文件描述符0(STDIN_FILENO)关联标准输入,1(STDOUT_FILENO)关联标准输出,2(STDERR_FILENO)关联标准错误
- 文件描述符的变化范围是0 ~ OPEN_MAX-1,文件描述符个数是有限制的
2. open、openat 打开文件
#include <fcntl.h>
int open (const char *__path, int __oflag, ...mode_t mode);
int openat (int __fd, const char *__path, int __oflag, ...mode_t mode);
参数 | 说明 | 取值范围 |
---|---|---|
path | 文件路径,或相对fd路径;path为绝对路径时fd被忽略 | 大多数系统支持的文件名最大长度为255 |
fd | path相对路径所在目录描述符,当path为相对路径时有效 | |
oflag | 五选一 | O_RDONLY :read only |
O_WRONLY :write onlt | ||
O_WRONLY :write onlt | ||
O_EXEC :exec 至执行打开 | ||
O_SEARCH :只搜索打开(用于目录) | ||
可选项 | O_WRONLY :write onlt | |
O_APPEND :每次写入时追加到文件末尾 | ||
O_CLOEXEC :把FD_CLOEXEC常量设置为文件描述符标志 | ||
O_CREATE :文件不存在则创建 | ||
O_DIRECTORY :如果path不是目录则出错 | ||
O_EXCL :如果同时指定了O_CREAT,而文件不存在则出错 | ||
O_NOCTTY :如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端 | ||
O_NOFOLLOW:如果path引用的是一个符号链接,则出错 | ||
O_NONBLOCK:如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则将此选项为文件的本次打开操作和后续操作设置非阻塞方式 | ||
O_SYNC :同步,使每次write等待物理I/O完成,等待文件属性更新 | ||
O_TRUNC :如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0 | ||
O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标注termios参数值 | ||
O_DSYNC :每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不等待文件属性更新 | ||
O_RSYNC :使每个以文件描述符为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成 | ||
返回值 | 返回最小的未使用描述符数值 |
// open.c
#include <fcntl.h>
#include <stdio.h>
void main()
{
int fd = open("/mnt/e/learning/unix_c/file_io/open.c", O_RDONLY);
if (fd < 0)
{
printf("open failed, return %d\r\n", fd);
}
else
{
printf("open success, fd = %d\r\n", fd);
}
}
3. creat 创建或打开可读文件
open可以指定O_CREAT选项,当文件不存在时创建文件;还有一种方式就是使用create。
#include <fcntl.h>
int creat (const char *__path, mode_t __mode);
// 等价于 open(path,O_WRONLY | O_CREATE | O_TRUNC,mode)
mode参数是在创建文件时添加文件权限,与chmod操作类似。
- 在文件存在是,以只读方式打开,并将文件截断为0(清空文件内容)
- 文件不存在时,创建新文件,并以只读方式打开,设置为mode文件访问权限
// creat.c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 100
/*
creat测试:
1. 带参数,使用creat创建文件,并写入数据,程序终止后使用cat打印文件内容
2. 不带参数,使用creat打开文件,程序终止后使用cat打印文件内容
*/
void main(int argc, char *argv)
{
char *path = "/mnt/e/learning/unix_c/file_io/creat_test.txt";
if (argc > 1)
{
char buf[BUFFER_SIZE];
int fd = creat(path, 0777); // 创建文件
sprintf(buf, "current file created by creat function,current fd = %d\r\n", fd);
write(fd, buf, strlen(buf)); // 写入文件
}
else
{
creat(path, 0777); // 打开已经存在的文件,清空已有数据
}
}
4. close 关闭文件描述符
可调用close函数关闭打开的文件。关闭文件时,还会释放该进程加在该文件上的所有记录锁。
当进程终止时,内核自动关闭它所有打开的文件,利用这一功能可以不显式调用close关闭文件。
#include <unistd.h>
int close(int fd);
测试:
// close.c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
void main()
{
printf("start close STDOUT_FILENO\n"); // 使用标准输出
close(STDOUT_FILENO); // 关闭标准输出
printf("STDOUT_FILENO closed\n"); // 使用标准输出,已经关闭了标准输出,将不会有字符串输出
int fd = open("/mnt/e/learning/unix_c/file_io/close_test.txt", O_RDWR | O_CREAT); // 打开新文件,fd = STDOUT_FILENO,相当于将标准输出关联到fd
printf("open file fd = %d\r\n", fd); // 标准输出,已经重定向到fd,相当于输出到fd
}
测试结果:
5. lseek 设置文件offset
- 每个打开文件都有一个与其关联的偏移量
- 通常write和read都是从当前偏移量位置开始
- 当打开(open、creat)文件没有设置O_APPEND时,默认打开的文件偏移量是0,读写都是从当前文件偏移量开始的
- lseek仅将当前的文件偏移记录在内核中,并不引起I/O操作
- 偏移量大于当前文件长度时引起空洞
#include <unistd.h>
off_t lseek (int __fd, __off_t __offset, int __whence) //off_t long
fd :设置偏移量的文件描述符
offset :偏移量
whence :相对位置
SEEK_SET 距文件开始位置
SEEK_CUR 距文件当前位置
SEEK_END 距文件末尾位置
成功则返回新的偏移量,-1 不能设置偏移量
6. read 读文件
从打开文件中读取数据;如果成功返回读到的字节数;如已到达文件的尾端,则返回0。
#include <unistd.h>
ssize_t read (int __fd, void *__buf, size_t __nbytes);
- 读普通文件,返回读取到的数据长度,0 到了文件尾端,-1 无权限
- 从终端设备读,通常最多一次只能读一行
- 从管道或FIFO读,返回实际可用的字节数
- 从面向记录的设备(如磁带)读,一次最多返回一个记录
7. write 写文件
向打开的文件写入数据。
#include <unistd.h>
ssize_t write (int __fd, const void *__buf, size_t __nbytes);
返回值通常与nbytes相同,否则出错,常见原因是磁盘已满,或者超过了一个给定进程的文件长度限制
8. I/O效率
复制一个文件的内容到另一个文件,如何才能速度最快?是一次性读取全部内容,再写入文件;还是读一个字节写一个字节;或是其他?
使用time获取程序执行时间:
/*
测试缓冲区长度对文件读写的影响
大多数文件系统为改善性能都采用某种预读技术,当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据。
每次读取不同长度的数据,测试全部读完文件的取时间长度
时间有三种:
时钟时间(总耗时)real time
用户CPU时间 user cpu time
系统CPU时间 sys cpu time
时间获取方式:
time ./a.out
测试方式:
time ./a.out [buffer_size]
调整buffer_size,看时间大小
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char *argv[])
{
int n;
int size = 4096;
if (argc > 1)
{
size = atoi(argv[1]);
}
char *buf = (char *)malloc(size);
char *path = "/mnt/e/BaiduNetdiskDownload/marry_photo_video.mp4"; // 指定一个大文件
close(STDIN_FILENO); // 关闭标准输出文件描述符 0
int fd = open(path, O_RDONLY); // 返回最小的未被使用的文件描述符 0 ,相当于将STDIN_FILENO重定向
printf("fd = %d\r\n", fd);
int fd3 = dup(STDOUT_FILENO); // 复制现有的文件描述符
close(STDOUT_FILENO);
int fd2 = open("/dev/null", O_WRONLY); // 重定向STDOUT_FILENO
printf("print fd2 = %d \r\n", fd2); // 将不会得到输出,因为STDOUT_FILENO已经重定向到/dev/null
// int fd3 = open("/dev/stdout", O_WRONLY);
char tbuf[100];
sprintf(tbuf, "fd2 = %d", fd2);
write(fd3, tbuf, strlen(tbuf)); // 输出到stdout
while ((n = read(STDIN_FILENO, buf, size)) > 0)
{
if (write(STDOUT_FILENO, buf, n) != n)
{
printf("write error\n");
}
}
if (n < 0)
printf("write error\n");
}
结果:
9. 文件共享(内核用于I/O的数据结构)
进程表项、文件表项、v节点表项
在不同进程中共享打开的文件
文件描述符表:
文件描述符标志、指向文件表项的指针
进程表项(进程):
进程内的记录项,包含一张打开文件描述符表(包含文件描述符标志、指向文件表项的指针)
文件表项(内核):
包含
文件状态标志(读、写、同步和非阻塞等)
文件偏移量(lseek修改文件偏移量,不是I/O操作)
指向该文件v节点表项的指针
V节点表项:
包含
文件类型及该类型的各种操作函数的指针
i节点(文件所有者,文件长度,指向文件实际数据块在磁盘上所在位置的指针)
每个进程维护自己的进程表项(文件描述符表)
多个进程打开同一个文件时,每个进程获得各自的文件表项,并指向同一个V节点表项
可能有多个文件描述符指向同一个文件表项(共享文件表项)
用O_APPEND打开一个文件,每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。
以O_APPEND打开的文件操作是原子操作
多个进程读取同一个文件时,可能产生预想不到的结果(非原子操作)
两个进程打开同一个文件的情况:
10. 原子操作
10.1 追加到一个文件
如果两个进程同时打开同一个文件,并同时将数据写入文件,如何保证数据不被覆盖?
在每次写入数据之前,将当前offset设置到文件尾端处,是否可以实现?代码如下:
lseek(fd,0,SEEK_END); // 设置文件offset值文件尾端
write(fd,buf,strlen(buf)); // 写入数据
好像是可以,但是如果在lseek与write之间有另一个进程写入了数据到文件尾端,write时将会覆盖其他线程写入的数据,并不能保证写入的原子性。lseek和write合成的操作并不是原子操作!
O_APPEND:在打开文件时设置O_APPEND标志,内核在每次写操作之前都将进程的当前偏移量设置到文件的尾端处。以O_APPEND打开的文件写入文件时是原子操作,每次都会将数据写入到文件尾端。
10.2 pread、pwrite
原子性定位并执行I/O。
#include <unistd.h>
ssize_t pread(int fd,void * buf,size_t nbytes,off_t offset);
ssize_t pwrite(int fd,const void * buf,size_t nbytes,off_t offset);
pread 等价于 lseek + read,但是pread是原子操作,不更新当前文件偏移量
pwrite相当于 lseek + write,但也是原子操作
11. dup、dup2 复制现有文件描述符
复制一个现有的文件描述符。
#include <unistd.h>
int dup (int __fd);
int dup2 (int __fd, int __fd2); // 指定新的文件描述符为fd2
示例:
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
void main()
{
int fd = dup(STDOUT_FILENO); // 复制标准输出文件描述符
char buf[100];
sprintf(buf, "this is output from fd = %d\r\n", fd);
write(fd, buf, strlen(buf)); // 输出字符串到标准输出
}
12. sync、fsync、fdatasync 从内核缓冲区写入队列或磁盘
#include <unistd.h>
int sync();
int fsync(int fd);
int fdatasync(int fd);
文件写入的顺序是:内核缓冲区 => 队列 => 磁盘,通常write之后数据保存在内核缓冲区,系统守护进程(update)周期性的调用(一般间隔30s)sync函数。
sync : 将内核缓冲区写入队列,对全部内核缓冲区起作用;
fsync:将内核缓冲区数据写入磁盘操作结束后才返回,同步更新文件的属性,只对指定的fd文件起作用;
fdatasync:将内核缓冲区数据写入磁盘操作后才返回,但是只影响数据部分(比fsync少了文件属性更新),只对指定的fd文件起作用;
13. fcntl 文件属性操作
fcntl函数可以改变已经打开文件的属性。
#include <fcntl.h>
int fcntl(int fd,int cmd,... /* arg */);
fcntl有以下5中功能:
- (1) 复制一个已有的文件描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)
- (2) 获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD)
- (3) 获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)
- (4) 获取/设置一部I/O所有权(cmd=F_GETTOWN或F_SETTOWN
- (5) 获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)
14. ioctl I/O操作
ioctl函数是I/O操作的杂物箱,所有I/O都能用ioctl实现。终端I/O是使用ioctl最多的地方。
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
int ioctl(int fd,int requset, ...);
每个设备驱动程序可以定义它自己专用的一组ioctl命令,系统则为不同种类的设备提供通用ioctl命令。
15. /dev/fd
较新的系统都提供名为/dev/fd/的目录,其目录项是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符n。
某些系统提供路径名/dev/stdin、/dev/stdout和/dev/stderr,这些等效于/dev/fd/0、/dev/fd/1、/dev/fd/2。
/dev/fd文件主要由shell使用。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
通过/dev/fd/1写到标准输出
*/
void main()
{
int fd = open("/dev/fd/1", O_RDWR);
char buf[100];
sprintf(buf,"this is output from /dev/fd/1, fd = %d\r\n",fd);
write(fd,buf,strlen(buf));
}