1. 文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数,当打开一个现有文件或者创建一个新文件时,进程会向内核返回一个文件描述符。在POSIX应用程序中,0代表标准输入,1代表标准输出,2代表标准出错输出。分别代表常量,STDIN_FILENO
STDOUT_FILENO、STDERR_FILENO。 这些常量定义在头文件<unistd.h>
2. 文件I/O相关函数
2.1 open
#include<fcntl.h>
int open(const char *pathname,int flag,/*int mode*/);
- 第三个参数只有创建文件时才会使用
- 第二个参数可以说明这个函数的多个选项:
1.以下三个选项必须且只可指定一个:
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RWWR 读、写打开
- 下列常量则是可选择的:
O_APPEND 每次写时都追加到文件的尾端
O_CREAT 若文件不存在,则创建它(注意使用这个选项时,需要第三个参数指定新文件的访问权限)
O_EXCL 可以用来测试一个文件是否存在(如果同时指定了O_CREAT,且文件已经存在,则会出错)
O_TRUNC 如果文件存在且被只读或只写成功打开,则将其长度截断为0
O_NOCTTY 如果文件是终端设备,则不分配该设备作为此进程的控制终端
O_NOBLOCK 如果文件是FIFO,或者其他特殊文件,则后续操作设置为非阻塞模式
2.2 creat
#include<fcntl.h>
int creat(const char* pathname,mode_t mode);
成功返回为只写打开的文件描述符,错误返回-1
此函数等效于
open(pathanme,O_WRONLY | O_CREAT | O_TRUNC,mode)
2.3 lseek
每个打开的文件有一个当前文件偏移量,用以度量从文件开始处计算的字节数,通常,读写操作从当前文件偏移量开始,并使偏移量增加读写的字节数。而系统的默认情况是:打开一个文件时,除非指定O_APPEND选项,否则偏移量被设置为0。
lseek函数的作用就是显示地为打开文件设置一个偏移量,原型如下:
#include<unistd.h>
off_t lseek(int filedes,off_t offet,int whence);
成功返回新文件偏移量,失败返回-1
来看whence的选项:
- SEEK_SET:将该文件偏移量设置为距文件开始的offet个字节
- SEEK_CUR:将该文件偏移量设置为当前值加offet,offet可正可负。
- SEEK_END:将该文件偏移量设置为文件长度加offet,offet可正可负。
注意:文件偏移量可以大于文件的当前长度,这种情况下,对该文件的下一次写将加成该文件,所以在文件中间形成一个空洞,这些存在却没被写的字节,将会被读城0,但文件空洞并不占用磁盘存储区。
2.4 read
#include<unistd.h>
ssize_t read (int fd,void*buf,size_t bytes);
读成功返回读到的字节数,若已到文件的末尾返回0,若出错返回-1
以下情况可使实际读到的字节数少于要求读的:
- 读普通文件时,在读到要求字节数前已经到达文件末尾。
- 从终端设备读时,通常一次只允许读一行。
- 从网络读时,网络中的缓冲机构可能造成返回值小于要求读的数。
- 从管道或者FIFO读时,若管道包含的字节数少于所需数量,那么read则返回实际可用的字节。
- 当某一信号造成终端,且已经读了部分数据。
2.5 write
函数原型参数和read一样,不再赘述。其返回值通常和参数bytes一样,否则表示出错。常见的出错原因:磁盘已经写满或者超过了一个给定进程的文件长度限制。
3. 文件共享
3.1 打开文件的数据结构
内核使用了三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
- 文件描述符表
与每个文件描述符表相关联的是:文件描述符标志,指向一个文件表项的指针。 - 文件表
包含了文件状态标志、当前文件偏移量,指向该文件v节点的表项的指针。 - v节点表
包含了文件类型和对此文件进行各种操作的函数指针。
注意文件描述符标志和文件状态标志的区别:前者只用于一个进程的一个描述符,后者是适用于指向该文件的任何进程中的所有描述符。
一个进程打开两个不同文件,一个打开标准输入,一个打开输出,则三张表的关系如下图:
那么当两个不同进程同时打开一个文件,内核如何做呢?来看下图:
我们可以看到,每个进程都有自己的文件表项,这样就保证了每个进程都有它自己的对该文件的当前偏移量。
3.2 dup和dup2函数
作用:复制一个存在的文件描述符
原型:
#include<unistd.h>
int dup(int filedes);
int dup2(int filedes,int filedes 2);
成功返回新的文件描述符,失败返回-1
区别:dup返回的新文件描述符一定是当前可用的最小数值。用dup2则可以用filedes2指定新描述符的数值,如果filedes2已经打开,则先将其关闭,如果filedes等于filedes2,则dup2返回filedes2,而不关闭它。
而在内核态,数据结构是这样描述复制文件描述符的过程:
可以看到,两个描述符指向同一个文件表,所以它们共享同一文件状态标志,以及同一当前文件的偏移量。
注意fctnl函数也可以实现复制描述符:
dup(filedes)等于调用fcntl(filedes,F_DUPFD,filedes2)。
而dup2(filedes,filedes2)等于先调用clsoe(filedes2)在调用fcntl(filedes,F_DUPFD,filedes2)。
4. fcntl和ioctl函数
4.1 fcntl函数
作用:改变已打开文件的性质。
原型:
#include<fcntl.h>
int fcntl(int filedes,int cmd,.../* int args */)
成功则依赖于cmd,错误返回-1
fcntl函数有5种功能:
- 复制一个现有的文件描述符(cmd = F_DUPFD)
- 获得/设置文件描述符标记(cmd = F_GETFD/F_SETFD)
- 获得/设置文件状态标志(cmd = F_GETFL/F_SETFL)
- 获得/设置异步IO所有权(cmd = F_GETOWN/F_SETOWN)
- 获得/设置记录锁(cmd = F_GETLK/F_SETLK/F_SETLKW)
4.2 ioctl函数
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。
int ioctl(int fd, ind cmd, …);
5./dev/fd
比较新的系统中都提供/dev/fd这个目录,其目录项是名为0,1,2的文件,打开文件/dev/fd/n等效于复制描述符n(假设n是打开的)。/dev/fd主要由shell调用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入/输出。