一、打开、创建文件、关闭文件
- 文件描述符:一个非负整数,范围是
0~OPEN_MAX-1
。内核用它来标识进程正在访问的文件。当进程创建时,默认为它打开了3个文件描述符,它们都链接向终端:- 0: 标准输入
- 1: 标准输出
- 2: 标准错误输出
通常我们应该使用
STDIN_FILENO
,STDOUT_FILENO
和STDERR_FILENO
来替代这三个幻数,从而提高可读性。这三个常量位于<unistd.h>
中。
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`:要打开或者创建文件的名字
- `oflag`:用于指定函数的操作行为,定义在`<fcntl.h>`中。
- `mode`:文件访问权限。文件访问权限常量在 `<sys/stat.h>` 中定义,有下列九个:
- `S_IRUSR`:用户读
- `S_IWUSR`:用户写
- `S_IXUSR`:用户执行
- `S_IRGRP`:组读
- `S_IWGRP`:组写
- `S_IXGRP`:组执行
- `S_IROTH`:其他读
- `S_IWOTH`:其他写
- `S_IXOTH`:其他执行
- 对于`openat`函数,被打开的文件名由`fd`和`path`共同决定:
**- 如果`path`指定的是绝对路径,此时`fd`被忽略。`openat`等价于`open`**
**- 如果`path`指定的是相对路径名,则`fd`是一个目录打开的文件描述符。被打开的文件的绝对路径由该`fd`描述符对应的目录加上`path`组合而成**
**- 如果`path`是一个相对路径名,而`fd`是常量`AT_FDCWD`,则`path`相对于当前工作目录。被打开文件在当前工作目录中查找。**
- 返回值:
- 成功:返回文件描述符。
- 失败:返回 -1
由 `open/openat` 返回的文件描述符一定是最小的未使用的描述符数字。
3. creat
函数:创建一个新文件
```
#include<fcntl.h>
int creat(const char*path,mode_t mode);
```
该函数等价于`open(path,O_WRONLY|O_CREAT|O_TRUNC,mode)`。注意:
- 它以只写方式打开,因此若要读取该文件,则必须先关闭,然后重新以读方式打开。
- 若文件已存在则将文件截断为0。
-
4. close
函数:关闭文件
```
#include<unistd.h>
int close(int fd);
```
- 参数:
- `fd`:待关闭文件的文件描述符
- 返回值:
- 成功:返回 0
- 失败:返回 -1
注意:
- 进程关闭一个文件会释放它加在该文件上的所有记录锁。
- 当一个进程终止时,内核会自动关闭它所有的打开的文件。
二、定位、读、写文件
lseek
函数:设置打开文件的偏移量#include<unistd.h> off_t lseek(int fd, off_t offset,int whence);
- 参数:
fd
:打开的文件的文件描述符whence
:必须是SEEK_SET
、SEEK_CUR
、SEEK_END
三个常量之一offset
:- 如果
whence
是SEEK_SET
,则将该文件的偏移量设置为距离文件开始处offset
个字节 - 如果
whence
是SEEK_CUR
,则将该文件的偏移量设置为当前值加上offset
个字节,offset
可正,可负 - 如果
whence
是SEEK_END
,则将该文件的偏移量设置为文件长度加上offset
个字节,offset
可正,可负
- 如果
- 返回值:
- 成功: 返回新的文件偏移量
- 失败:返回 -1
每个打开的文件都有一个与其关联的“当前文件偏移量”。它通常是个非负整数,用于度量从文件开始处计算的字节数。通常读、写操作都从当前文件偏移量处开始,并且使偏移量增加所读写的字节数。注意:
- 打开一个文件时,除非指定
O_APPEND
选项,否则系统默认将该偏移量设为0 - 如果文件描述符指定的是一个管道、FIFO、或者网络套接字,则无法设定当前文件偏移量,则
lseek
将返回 -1 ,并且将errno
设置为ESPIPE
。 - 对于普通文件,其当前文件偏移量必须是非负值。但是某些设备运行负的偏移量出现。因此比较
lseek
的结果时,不能根据它小于0 就认为出错。要根据是否等于 -1 来判断是否出错。 lseek
并不会引起任何 I/O 操作,lseek
仅仅将当前文件的偏移量记录在内核中。- 当前文件偏移量可以大于文件的当前长度。此时对该文件的下一次写操作将家常该文件,并且在文件中构成一个空洞。空洞中的内容位于文件中但是没有被写过,其字节被读取时都被读为0
- 参数:
/*
test lseek function for std file io
*/
#include "apue.h"
int main(void)
{
if (lseek(STDIN_FILENO,0,SEEK_CUR) == -1) /*there is must be is equal operation*/
printf("cannot seek\n");
else
printf("seek ok\n");
exit(0);
}
read
函数:读取文件内容#include<unistd.h> ssize_t read(int fd,void *buf,size_t nbytes);
- 参数:
fd
:打开的文件的文件描述符buf
:存放读取内容的缓冲区的地址(由程序员手动分配)nbytes
:期望读到的字节数
- 返回值:
- 成功:返回读到的字节数,若已到文件尾则返回 0
- 失败:返回 -1
读操作从文件的当前偏移量开始,在成功返回之前,文件的当前偏移量会增加实际读到的字节数。有多种情况可能导致实际读到的字节数少于期望读到的字节数:
- 读普通文件时,在读到期望字节数之前到达了文件尾端
- 当从终端设备读时,通常一次最多读取一行(终端默认是行缓冲的)
- 当从网络读时,网络中的缓存机制可能造成返回值小于期望读到的字节数
- 当从管道或者
FIFO
读时,若管道包含的字节少于所需的数量,则read
只返回实际可用的字节数 - 当从某些面向记录的设备(如磁带)中读取时,一次最多返回一条记录
- 当一个信号造成中断,而已读了部分数据时。
- 参数:
write
函数:想文件写数据#include<unistd.h> ssize_t write(int fd,const void *buf,size_t nbytes);
- 参数:
fd
:打开的文件的文件描述符buf
:存放待写的数据内容的缓冲区的地址(由程序员手动分配)nbytes
:期望写入文件的字节数
- 返回值:
- 成功:返回已写的字节数
- 失败:返回 -1
write
的返回值通常都是与nbytes
相同。否则表示出错。write
出错的一个常见原因是磁盘写满,或者超过了一个给定进行的文件长度限制对于普通文件,写操作从文件的当前偏移量处开始。如果打开文件时指定了
O_APPEND
选项,则每次写操作之前,都会将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。- 参数:
- 测试
lseek,read,write
:#include <stdio.h> #include<unistd.h> #include<fcntl.h> #include<string.h> #include<errno.h> void test_read(int fd,void* read_buffer,ssize_t len) { int read_num; read_num=read(fd,read_buffer,len); if(read_num<0) { printf("\tread error!\n"); fprintf(stderr,"\tBeause:%s\n",strerror(errno)); }else { printf("\tread num:%d(expexted read :%d)\n",read_num,len); } } void test_write(int fd,void*write_buffer,ssize_t len) { int write_num; write_num=write(fd,write_buffer,len); if(write_num<0) { printf("\twrite error!\n"); fprintf(stderr,"\tBeause:%s\n",strerror(errno)); }else { printf("\twrite num:%d(expexted write :%d)\n",write_num,len); } } void test_lseek(int fd,off_t offset,int loc) { int seek_ok; seek_ok=lseek(fd,offset,loc); if(-1==seek_ok) { printf("\tlseek error!\n"); fprintf(stderr,"\tBeause:%s\n",strerror(errno)); }else { printf("\tlseek ok.New offset is:%d\n",seek_ok); } } int main(int argc, char *argv[]) { int fd; char read_buffer[20]; char write_buffer[10]; strcpy(write_buffer,"123456789"); fd=openat(AT_FDCWD,"test",O_RDWR|O_TRUNC); printf("File is empty:\n"); test_read(fd,read_buffer,20); test_write(fd,write_buffer,10); test_read(fd,read_buffer,20); printf("Lseek to begin:\n"); test_lseek(fd,0,SEEK_SET); test_read(fd,read_buffer,20); printf("Lseek to end+10:\n"); test_lseek(fd,10,SEEK_END); test_read(fd,read_buffer,20); test_write(fd,write_buffer,10); test_read(fd,read_buffer,20); printf("Lseek to begin:\n"); test_lseek(fd,0,SEEK_SET); test_read(fd,read_buffer,20); test_read(fd,read_buffer,20); return 0; }
测试序列为:
- 开始文件为空,所以读取20个字节的
read
只读取0 - 写入文件10个字节
- 读取文件。此时读和写共享一个当前文件偏移。而且当前文件偏移被
write
置于文件结尾。此时读取0个字节 - 执行
lseek
将当前文件偏移量重置到文件开头,返回0(新的文件偏移量) - 读取文件,只能读取10个字节(因为文件此时就10个字节)
- 执行
lseek
将文件偏移量放到文件末尾之后的10个字节,返回20(新的文件偏移量) - 读取文件。此时当前文件偏移被置于文件结尾。此时读取0个字节
- 写入文件10个字节
- 读取文件。此时当前文件偏移被
write
置于文件结尾。此时读取0个字节 - 执行
lseek
将当前文件偏移量重置到文件开头,返回0(新的文件偏移量) - 读取文件,读取20个字节(因为文件结尾的偏移是30个字节)
- 读取文件,只能读取10个字节(因为文件结尾的偏移是30个字节)
- 开始文件为空,所以读取20个字节的
三、 原子操作、同步、复制、修改文件描述符
- 内核使用三种数据结构描述打开文件。它们之间的关系决定了一个进程与另一个进程在打开的文件之间的相互影响。
- 内核为每个进程分配一个进程表项(所有进程表项构成进程表),进程表中都有一个打开的文件描述符表。每个文件描述符占用一项,其内容为:
- 文件描述符标志
- 指向一个文件表项的指针
- 内核为每个打开的文件分配一个文件表项(所有的文件表项构成文件表)。每个文件表项的内容包括:
- 文件状态标志(读、写、添写、同步和阻塞等)
- 当前文件偏移量
- 指向该文件 v 结点表项的指针
- 每个打开的文件或者设备都有一个 v 结点结构。 v 结点结构的内容包括:
- 文件类型和对此文件进行各种操作函数的指针。
- 对于大多数文件, v 结点还包含了该文件的 i 结点。
这些信息都是在打开文件时从磁盘读入内存的。如 i 结点包含了文件的所有者、文件长度、指向文件实际数据在磁盘上所在位置的指针等等。 v 结点结构和 i 结点结构实际上代表了文件的实体。
现在假设进程 A 打开文件
file1
,返回文件描述符 3;进程 B 也打开文件file2
,返回文件描述符 4:- 内核在文件表上新增两个表项:
- 这两个文件表项指向同一个 v 结点表项
- 进程 A 、B 各自的文件描述符表项分别指向这两个文件表项;
- 对文件的操作结果:
- 进程 A 每次
write
之后,进程 A 对应的文件表项的当前文件偏移量即增加所写入的字节数。- 若这导致当前文件偏移量超过当前文件长度,则修改 i 节点的当前文件长度,设为当前文件偏移量
- 如果进程 B 用
O_APPEND
标志打开一个文件,在相应标志也设置到进程 B 对于的文件表项的文件状态标志中。- 每次进程 B 对具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先被置为 i 结点中的文件长度。
- 若进程 B 用
lseek
定位到文件当前的尾端,则进程 B 对应的文件表项的当前文件偏移量设置为 i 结点中的当前长度 lseek
函数只是修改文件表项中的当前文件偏移量,不进行任何 I/O 操作
- 进程 A 每次
可能一个进程中有多个文件描述符指向同一个文件表项。
- 内核为每个进程分配一个进程表项(所有进程表项构成进程表),进程表中都有一个打开的文件描述符表。每个文件描述符占用一项,其内容为:
- 原子操作:
- 追加一个文件时,不能通过
lseek
到末尾然后write
。要用O_APPEND
选项打开文件,然后直接write
。- 通过
lseek
到末尾然后write
时,如果多个进程同时执行这两个操作,则会引起竞争条件 - 通过
O_APPEND
选项打开文件,然后直接write
时,内核每一次在写操作之前,都会将进程的当前偏移量设置到文件的末尾,于是就不需要执行lseek
定位操作
- 通过
pread/pwrite
可以执行原子性的定位读/定位写O_CREAT|O_EXCL
选项打开文件时,可以原子性的检查文件是否存在和创建文件这两个操作。
- 追加一个文件时,不能通过
pread/pwrite
:原子定位读和原子定位写#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);
- 参数:
fd
:打开的文件描述符buf
:读出数据存放的缓冲区/ 写到文件的数据的缓冲区nbytes
:预期读出/写入文件的字节数offset
:从文件指定偏移量开始执行read/write
- 返回:
- 成功:读到的字节数/已写的字节数
- 失败: -1
调用
pread
相当于先调用lseek
再调用read
.但是调用pread
时,无法中断其定位和读操作,并且不更新当前文件偏移量;调用pwrite
相当于先调用lseek
再调用write
.但是调用pwrite
时,无法中断其定位和写操作,并且不更新当前文件偏移量- 参数:
dup/dup2
:复制一个现有的文件描述符:#include<unistd.h> int dup(int fd); int dup2(int fd,int fd2);
- 参数:
fd
:被复制的文件描述符(已被打开)fd2
:指定的新的文件描述符(待生成)-返回值:
- 成功: 返回新的文件描述符
- 失败: 返回 -1
对于
dup
函数,返回的新的文件描述符一定是当前可用的文件描述符中最小的数字。对于dup2
函数:- 如果
fd2
已经是被打开的文件描述符且不等于fd
,则先将其关闭,然后再打开(注意关闭再打开是一个原子操作) - 如果
fd2
等于fd
,则直接返回fd2
(也等于fd
),而不作任何操作
任何情况下,这个返回的新的文明描述符与参数
fd
共享同一个文件表项(因此文件状态标志以及文件偏移量都会共享)。 任何情况下,这个返回的新的文明描述符的close-on-exec
标志总是被清除- 参数:
- UNIX操作系统在内核中设有缓冲区,大多数磁盘 I/O 都通过缓冲区进行。当我们想文件写入数据时,内核通常都首先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式称为延迟写。
- 当内核需要重用缓冲区来存方其他数据时,它会把所有延迟写的数据库写入磁盘
- 你也可以调用下列函数来显式的将延迟写的数据库写入磁盘
#include<unistd.h> int fsync(int fd); int fdatasync(int fd); void sync(void);
- 参数(前两个函数):
fd
:指定的打开的文件描述符
- 返回值(前两个函数):
- 成功:返回 0
- 失败: 返回 -1
区别:
sync
:将所有修改过的块缓冲区排入写队列,然后返回,它并不等待时机写磁盘结束fsync
:只对由fd
指定的单个文件起作用,等待写磁盘操作结束才返回fdatasync
:只对由fd
指定的单个文件起作用,等待写磁盘操作结束才返回,但是它只影响文件的数据部分(fsync
会同步更新文件的属性)
update
守护进程会周期性的调用sync
函数。命令sync
也会调用sync
函数 fcntl
函数:改变已经打开的文件的属性#include<fcntl.h> int fcntl(int fd,int cmd,.../* int arg */);
- 参数:
fd
:已打开文件的描述符cmd
:有下列若干种:F_DUPEF
常量:复制文件描述符fd
。新文件描述符作为函数值返回。它是尚未打开的个描述符中大于或等于arg
中的最小值。新文件描述符与fd
共享同一个文件表项,但是新描述符有自己的一套文件描述符标志,其中FD_CLOEXEC
文件描述符标志被清除F_DUPFD_CLOEXEC
常量:复制文件描述符。新文件描述符作为函数值返回。它是尚未打开的个描述符中大于或等于arg
中的最小值。新文件描述符与fd
共享同一个文件表项,但是新描述符有自己的一套文件描述符标志,其中FD_CLOEXEC
文件描述符标志被设置F_GETFD
常量:对应于fd
的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC
F_SETFD
常量:设置fd
的文件描述符标志为arg
F_GETFL
常量:返回fd
的文件状态标志。文件状态标志必须首先用屏蔽字O_ACCMODE
取得访问方式位,然后与
O_RDONLY
、O_WRONLY
、O_RDWR
、O_EXEC
、O_SEARCH
比较(这5个值互斥,且并不是各占1位)。剩下的还有:
O_APPEND
、O_NONBLOCK
、O_SYNC
、
O_DSYNC
、O_RSYNC
、F_ASYNC
、O_ASYNC
F_SETFL
常量:设置fd
的文件状态标志为arg
。可以更改的标志是:O_APPEND
、O_NONBLOCK
、O_SYNC
、O_DSYNC
、O_RSYNC
、F_ASYNC
、O_ASYNC
F_GETOWN
常量:获取当前接收SIGIO
和SIGURG
信号的进程ID
或者进程组ID
F_SETOWN
常量:设置当前接收SIGIO
和SIGURG
信号的进程ID
或者进程组ID
为arg
。若arg
是个正值,则设定进程ID
;若arg
是个负值,则设定进程组ID
F_GETLK
、F_SETLK
、F_SETLKW
:获取/设置文件记录锁
arg
:依赖于具体的命令
- 返回值:
- 成功: 依赖于具体的命令
- 失败: 返回 -1
#include <stdio.h> #include<fcntl.h> #include<unistd.h> #include<string.h> #include<errno.h> void print_error(int fd,const char* action,int result) { if(result==-1) { printf("\t %s on fd(%d) error:beause %s!\n",action,fd,strerror(errno)); } } void test_get_fd(int fd) { printf("\tget_fd on fd(%d):",fd); int result; result=fcntl(fd,F_GETFD); print_error(fd,"F_GETFD",result); if(result!=-1) printf("return:%d !\n",result); } void test_set_fd(int fd, int flag) { printf("\tset_fd on fd(%d) of flag(%d):",fd,flag); int result; result=fcntl(fd,F_SETFD,flag); print_error(fd,"F_SETFD",result); if(result!=-1) printf("set_fd ok !\n"); } int test_dup_fd(int fd,int min_fd) { printf("\tdup_fd on fd(%d),set min_fd(%d),:",fd,min_fd); int result; result=fcntl(fd,F_DUPFD,min_fd); print_error(fd,"F_DUPFD",result); if(result!=-1) printf("return:%d !\n",result); return result; } int test_dup_exec_fd(int fd,int min_fd) { printf("\tdup_exec_fd on fd(%d),set min_fd(%d),:",fd,min_fd); int result; result=fcntl(fd,F_DUPFD_CLOEXEC,min_fd); print_error(fd,"F_DUPFD_CLOEXEC",result); if(result!=-1) printf("return:%d !\n",result); return result; } void test_get_fl(int fd) { printf("\tget_fl on fd(%d):",fd); int result; result=fcntl(fd,F_GETFL); print_error(fd,"F_GETFL",result); if(result!=-1) { printf("F_GETFL on fd(%d) has ",fd); if(result&O_APPEND) printf("\tO_APPEND;"); if(result&O_NONBLOCK) printf("\tO_NONBLOCK;"); if(result&O_SYNC) printf("\tO_SYNC;"); if(result&O_DSYNC) printf("\tO_DSYNC;"); if(result&O_RSYNC) printf("\tO_RSYNC;"); if(result&O_FSYNC) printf("\tO_FSYNC;"); if(result&O_ASYNC) printf("\thas O_ASYNC;"); if((result&O_ACCMODE)==O_RDONLY)printf("\tO_RDONLY;"); if((result&O_ACCMODE)==O_WRONLY)printf("\thas O_WRONLY;"); if((result&O_ACCMODE)==O_RDWR)printf("\tO_RDWR;"); printf("\n"); } } void test_set_fl(int fd,int flag) { printf("\tset_fl on fd(%d) of flag(%d):",fd,flag); int result; result=fcntl(fd,F_SETFL,flag); print_error(fd,"F_SETFL",result); if(result!=-1) printf("set_fl ok !\n"); } void test_get_own(int fd) { printf("\tget_own on fd(%d):",fd); int result; result=fcntl(fd,F_GETOWN); print_error(fd,"F_GETOWN",result); if(result!=-1) printf("return:%d !\n",result); } void test_set_own(int fd,int pid) { printf("\tset_own on fd(%d) of pid(%d):",fd,pid); int result; result=fcntl(fd,F_SETOWN,pid); print_error(fd,"F_SETOWN",result); if(result!=-1) printf("set_own ok !\n"); } int main(int argc, char *argv[]) { int fd; fd=openat(AT_FDCWD,"test.txt",O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR); printf("Test dup:\n"); test_get_fd(test_dup_fd(fd,10)); test_get_fd(test_dup_fd(fd,0)); test_get_fd(test_dup_exec_fd(fd,10)); test_get_fd(test_dup_exec_fd(fd,0)); printf("Test set_get_fd:\n"); test_get_fd(fd); test_set_fd(fd,~FD_CLOEXEC); test_get_fd(fd); test_set_fd(fd,FD_CLOEXEC); test_get_fd(fd); printf("Test set_get_fl:\n"); test_get_fl(fd); test_set_fl(fd,O_RDWR); test_get_fl(fd); test_set_fl(fd,O_RDONLY|O_NONBLOCK); test_get_fl(fd); printf("Test set_get own:\n"); test_get_own(fd); test_set_fl(fd,1); test_get_own(fd); return 0; }
注意:
- Linux 下,不支持文件状态标志:
F_EXEC与
,F_SEARCH
(result&O_ACCMODE)==O_RDONLY
表达式中,&
优先级较低F_SETFL
命令:当文件读打开时,你无法将文件状态标志修改为O_WRONLY
、O_WRWR
这两种中任何一个。你只能修改:O_APPEND
、O_NONBLOCK
、O_SYNC
、O_DSYNC
、O_RSYNC
、F_ASYNC
、O_ASYNC
等标志
- 参数:
/dev/fd
目录:该目录下是名为0、1、2
等的文件。打开文件/dev/fd/n
等效于复制描述符(假定描述符n
是打开的)fd=open("/dev/fd/0",mod)
:fd
和文件描述符0
共享同一个文件表项。- 大多数系统忽略
mod
参数 - 在 Linux 操作系统上,
/dev/fd/0
是个例外,它是个底层物理文件的符号链接。因此在它上面调用creat
会导致底层文件被截断