Unix文件IO相关函数

Unix中大多数文件的操作只需要用到五个函数open、read、write、lseek、和close。本章将详细讲解这些函数的用法以及参数详解。

文件描述符

对于内核而言,所有打开的文件都是通过文件描述符进行引用。文件描述符是一个非负整数。当打开或者创建一个文件的时候,内核向进程返回一个文件描述符。当对这个文件进行读写的时候,将这个参数传递给read或write。LINUX用于IO的数据结构一章中讲了STDIN_FILENO、STDOUT_FILENO以及STDERR_FILENO所对应的文件描述符。

为了保证系统资源的合理使用和安全性,Unix系统对于系统和用户能打开的文件描述符的格式都做了一定的限制。通过一下命令我们可以进行查看:

1
2
sysctl -a | grep fs.file-max //查看系统级限制的文件描述符的个数
ulimit -n //查看用户(进程)级别限制的文件描述符的个数,

文件描述符与文件指针的关系

文件描述符:内核会为每一个运行中的进程在进程控制块(pcb)中维护一个打开文件的记录表,每个表项都有一个指针指向打开的文件,文件描述符就是记录表的索引。

文件指针:C语言使用文件指针而不是文件描述符作为文件IO的句柄,文件指针是指向进程的用户空间中的一个FILE结构的数据结构,FILE结构中包括一个IO缓冲区和一个文件描述符,而文件描述符是
文件描述符表的一个索引,从某种意义上可以将文件指针理解为文件句柄的句柄。

1
2
3
4
5
6
7
8
9
10
typedef struct {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
} FILE;
  • 文件指针相比于文件描述符是高级的接口.
  • 文件指针使用fread()和fwrite()函数进行操作,文件描述符使用write()和read()函数进行操作
  • 文件指针具有缓冲区,是较高级别的IO,读写时具有缓冲,具有错误指示和EOF检测;文件描述符没有
  • 文件指针具有移植性,文件描述符不能移植到除Unix之外的系统。
  • fopen在stdio.h中,open在fcntl.h中
  • fopen是标准C中定义的,而open是posix中定义的。
  • fwrite/fread处理的速度快于read/write,但是在内存方面read/write性能较好。

函数open()和openat()

函数原型

1
2
int open(const char* path, int oflag, .../*mode_t mode*/);
int openat(int fd, const char* path, int oflag, .../*mode_t mode*/)

返回值说明

若文件打开失败返回-1,打开失败原因可以通过errno或者strerror(errno)查看;

若成功将返回最小的未用的文件描述符的值。

参数说明

  • path为要打开的文件的文件路径
  • oflag为文件打开模式.
  • …为可变参数,可以视情况添加

文件打开模式

文件打开模式标识当前进程对打开文件的操作权限。通常用一个或者多个权限的或来表示。权限列表如下:

flag 含义
O_RDONLY 只读权限
O_WRONLY 只写权限
O_RDWR 读写权限
O_EXEC 可执行权限
O_SEARCH 搜索权限(针对目录)
O_APPEND 每次写都追加到文件的末端
O_CLOEXEC 把close_on_exec设置为文件描述符标识
O_CREATE 若文件不存在,则创建它。使用此选项的时候,需要使用第三个参数指定该新文件的访问权限位
O_DIRECTORY 如果path不是目录则出错
O_EXCL 若同时执行了O_CREATE,若文件存在则出错,可以用此选项测试文件是否存在
O_NOCTTY 如果PATH引用的是终端设备,则不将该终端设备作为该进程的控制终端
O_NOFOLLOW 若PATH引用的是符号链接,则出错
O_NONBLOCK 如果path引用的是FIFO,一个块特殊文件或者一个字符特殊文件,则将文本打开操作和后续的IO设置为非阻塞模式
O_SYNC 使每次write等待物理IO完成,包括该write属性引起的文件属性更新需要的IO
O_TRUNC 如果文件存在,且打开模式为只写或者读写,则将文件内容截断为0
O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single Unix Specification
O_DSYNC 每次write要等待物理IO操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新
O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作完成

文件访问权限mode_t

mode的值表示了对文件的访问权限。这个访问权限与使用shell命令chmod去修改文件的权限的含义相同,文件的权限包括三大类,分别是当前文件对于文件所有者(u),文件所有者所在的组(g)以及其他
用户(o)而言,对这三种角色又分别具有读写可执行的权限。所以这个参数只有在创建文件的时候才用到,用来指定当前创建的文件所对应的权限。

mode 含义
S_IRUSR 用户读
S_IWUSR 用户写
S_IXUSR 用户可执行
S_IRGRP 组读
S_IWGRP 组写
S_IXGRP 组可执行
S_IROTH 其他读
S_IWOTH 其他写
S_IXOTH 其他可执行

需要注意的是,目录的可执行权限以及读权限与文件的相应权限完全不同。目录的可执行权限表示搜索位,即可搜索权限,若目录不具有可执行权限则不能cd进入文件;
目录的读权限是可以查看目录中文件内容的权限,若文件夹不具有读权限,则ls不能显示目录内的内容。

open与openat的区别

open和openat的区别主要在fd上

  • path参数指定的是绝对路径名,在这种情况下,open与openat相同,fd忽略.
  • path参数是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数通过打开相对路径名所在的文件目录获取。即此时fd为打开相对路径所获取的文件描述符。
  • path参数为相对路径,fd参数为AT_FDCWD,此时相对路径为当前目录,作用于open相同。

函数create()

函数原型

1
int create(const char *path, mode_t mode);

返回值说明

若文件创建失败返回-1;
若创建成功返回当前创建文件的文件描述符。

参数说明

参数与open中对应的参数含义相同

函数功能说明

create(path, mode)函数功能为创建新文件,与open(path, O_CREATE|O_TRUNC|O_WRONLY)功能相同。

函数close()

函数原型

1
int close(int fd);

返回值说明

文件关闭成功返回0,关闭失败返回-1.

函数功能介绍

该函数的作用是关闭指定文件描述符的文件,关闭文件时还会释放该进程加在该文件上的所有的记录锁。当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都是利用这一功能而不是close
函数关闭打开的文件。但是对于长期运行的函数,最好还是使用close关闭打开的文件。

lseek()函数

每个打开的文件在文件表项中存在着对应的当前文件的偏移量(current file offset),通常为非负整数。文件的读写操作都是从当前偏移量开始,并在操作后将偏移量增加相应的字节数。当打开文件
模式为O_APPEND时,该文件的offset是文件末尾,除此之外,其他情况都被初始化为0.

函数原型

1
int lseek(int fd, off_t offset, int whence);

返回值说明

成功则返回新的文件的偏移量;
失败则返回-1.

函数功能

使用lseek()函数显式的为一个打开的文件设置偏移量。lseek仅将文件的偏移量记录在内核中,并不引起IO开销。

参数说明

参数offset的解释与whence相关。

  • 若whence为SEEK_SET,则将该文件的偏移量设置为距离当前文件开始处offset字节。
  • 若whence为SEEK_CUR,则将该文件的偏移量设置为距离当前偏移量加offset个字节,此时offset可正可负。
  • 若whence为SEEK_END,则将该文件的偏移量设置为当前文件长度加offser个字节,此时offset可正可负。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
int main() {
int fd = open("./data", O_WRONLY | O_CREAT | O_TRUNC);
lseek(fd, 20, SEEK_END);
write(fd, "haha", 4);
//可以用这种方法查看打开文件的当前偏移量。
//这种方法可以用来确定当前文件是否可以设置偏移量
//若文件是管道、FIFO或者网络套接字,则lseek返回-1,并将errno设置为ESPIPE
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
}

注意一般情况下文件的偏移量不能为负值,但是一些特殊的文件允许偏移量为负值,如在FreeBSD上运行的设备/dev/kmem支持负的偏移量。
因为偏移量可能为负值,所以在比较偏移量的时候不能直接判断其是否小于0,而是要判断其是否等于-1.

文件的偏移量允许大于文件的长度,这时会在文件中出现一些空洞,但是是被允许的。文件中的空洞并不在文件中占磁盘块(block)。

read()函数

函数原型

1
2
3
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);

返回值说明

若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数。
若读取失败,返回-1。

参数说明

fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。

注意read函数默认读入多行,遇到换行不会停止读入,直到读到文件末尾,下一次读取返回值为0.

write()函数

函数原型

1
2
3
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t ntyes);

返回值说明

若写入成功则返回写入的字节数;失败返回-1.

参数说明

buf为写入内容的缓冲区,ntyes为期待写入的字节数,通常为sizeof(buf)。一般情况下返回值与ntypes相等,否则写入失败。

当指定O_APPEND选项,内容将从文件末尾写入,否则从文件开始写入。一般情况下将缓冲区的长度设置为磁盘块的大小可以最大程度的提升程序读写的性能。