先来提一下文件标识符的概念,对内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数,打开或创建一个文件的时候,内核向进程返回它的描述符。我们要进行读写操作的时候,把这个描述符传给read和write即可对文件内容进行操作。
按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,1和标准输出关联,2和标准错误关联。在unistd.h中有相关的STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO宏定义。文件描述符的变化范围是0~OPEN_MAX-1。
open函数:
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);
两函数的返回值都是打开的文件描述符,如果打开失败,返回-1。
path:文件名称
oflag:选项
- - - 必选:
- O_RDONLY:只读打开;
- O_WRONLY:只写打开;
- O_RDWR:读写打开;
- O_EXEC:执行打开;
- O_SEARCH:搜索打开。
- - - 可选:
- O_APPEND:追加写入,每次写入的时候偏移量都会固定在最尾端,lseek和write组成了原子操作,每次写入的时候都调用一次(所以lseek不管用了),而不是之前想的打开后lseek就不变了,以后只write(请看附1代码);
- O_CREAT:创建文件时设定第三个参数mode,指定访问权限;
- O_DIRECTORY:保证path是个目录,如果不是目录就会报错;
- O_EXCL:将测试文件是否存在和创建文件组成原子操作,如果creat时文件已存在,出错(这样的设计就不会出现两个进程同时创建覆盖的后果了);
- O_NONBLOCK:如果path引用的是FIFO、块特殊文件或字符特殊文件,将I/O操作设置为非阻塞状态;
- O_SYNC:使每次write等待雾里I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O;
- O_TRUNC:用写或读写方式打开时将文件长度截断为0(等同于创建覆盖文件)。
// 所有的参数都在fcntl.h中进行了宏定义
fd参数把open和openat函数区分开,共有三种可能:
1. path参数指定的是绝对路径,在这种情况下fd被忽略,openat就相当于open;
2. path参数指定的是相对路径,fd指出了相对路径名在文件系统中开始的位置(fd参数通过打开相对路径名所在的目录获取的O_SEARCH | O_DIRECTORY);
3. path参数指定的是相对路径,fd为特殊值AT_FDCWD,此时就在工作目录获取,与open一样。
【使用openat函数可以让我们使用相对路径去在不同的目录进行工作,解决了多线程工作在不同目录的问题】
creat函数:
int creat(const char *path, mode_t mode);
// open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
// 现在open添加了O_CREAT和O_TRUNC这些选项,也不再需要单独的creat了
如果创建成功,返回文件描述符,出错返回-1。
creat的不足是使用只写的方式打开文件,如果要读和写同时进行,就只能进行切换,十分麻烦。现在可以使用open(path, O_RDWR | O_CREAT | O_TRUNC, mode)来实现。
close函数:
int close(int fd);
当一个进程终止的时候,内核自动关闭它所有打开的文件,很多程序利用这一点而不去主动close。
lseek函数:
off_t lseek(int fd, off_t offset, int whence);
如果成功,返回文件新的偏移量,否则返回-1。通常open一个文件都会将偏移量设置为0,除非指定了O_APPEND选项。如果对FIFO或者套接字管道等设置偏移量,会返回-1并将errno设置为ESPIPE。
whence参数:SEEK_SET, SEEK_CUR, SEEK_END
文件的偏移量是可以大于文件长度的,这样对文件的下一次写将会加长该文件,并在中间构成一个用0填充的空洞(虽然在存储的时候跟文件系统压缩方式有关,但它就是有那么大)。
read函数:
ssize_t read(int fd, void *buf, size_t nbytes);
返回值是读取到的字节数,如果已经到达文件的尾端,返回0。需要使用返回值来确定长度而不是nbytes,因为有很多情况是读不满的。
write函数:
ssize_t write(int fd, void *buf, size_t nbytes);
返回值是成功写入的字节数,如果出错返回-1。与read不同,write返回值通常是与nbytes相等的,如果不相等则表示出了一些问题,如磁盘写满了,或者单进程文件写太长了受限。
dup函数:
int dup(int fd);
int dup2(int fd1,int fd2);
两个函数均为复制一个现存的文件的描述。若成功返回值为新的文件描述符,若出错为-1。由dup返回的新文件描述符一定是当前可用文件描述中的最小数值。用dup2则可以用fd2参数指定新的描述符数值。如果fd2已经打开,则先关闭。若fd1=fd2,则dup2返回fd2,而不关闭它。通常使用这两个系统调用来重定向一个打开的文件描述符(构成选择通道,筛选器等等)。
dup可以看作是close和fcntl的原子集合操作。
我们的系统还提供了/dev/fd目录,这个目录里有数字0、1、2等文件,在程序中打开文件就相当于复制对应的描述符,注意此时的mode必须为之前打开对应fd的mode的子集,不可越限。使用这种方法creat的时候可能会导致底层文件被截断,因为linux实现使用指向实际文件的符号链接。
fd = open("/dev/fd/0", O_RDONLY);
sync函数:
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
由于大多数磁盘I/O都采用了缓冲区写入的办法,I/O操作都是内核对缓冲区的写入操作,只有在内核需要重用缓冲区来存放其他磁盘块数据时,才会把所有延迟写数据块写入磁盘。这时就会出现一些一致性的问题,所以UNIX提供了sync、fsync和fdatasync三个函数将缓冲区文件内容写入磁盘。通常系统守护进程update就是周期性的调用sync函数来保证定期flush缓冲区。
还有fnctl和ioctl函数可以执行多种操作,但由于太过复杂,并且功能大多使用前面的函数就可以完成,暂时不说了。详情查看APUE-3.14,APUE-3.15。
附1代码:append标识符实验(不知为何左键不能用了虚拟机,就直接截图)
在这里我是创建了一个文件,让程序写了两次,可以看到每次write的时候seek才会更新。
代码结果: