本章开始讨论UNIX系统的文件I/O函数,包括打开文件、读文件、写文件等。
UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek和close。它们每执行一次都会调用内核中的系统调用,也就是常说的不带缓冲的I/O函数。
只要涉及多个进程间共享资源(比如同时读写某个文件),原子操作的概念就变得很重要,我将通过open()函数来讨论此概念。
一、文件描述符
文件描述符是一个非负整数,每一个使用open()函数打开的文件都会分配一个文件描述符。
在第一章提到的标准输入、标准输出、标准错误分别对应文件描述符0、1、2。
每个操作系统中的文件描述符个数都是一定的,我们可以使用如下shell命令查看最大文件描述符数值:
$ ulimit -n
也可使用下面的代码进行查看:
1 #include <unistd.h> 2 3 sysconf(_SC_OPEN_MAX)
二、文件操作函数
UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek和close。我在下面给出它们的函数声明、例子和函数作用:
1 /* 文件打开函数 */ 2 #include <fcntl.h> 3 4 int open(const char *path, int oflag, ... /* mode_t mode */ ); 5 int openat(int fd, const char *path, int oflag, ... /* mode_t mode */ ); 6 7 /* 例子 */ 8 int fd = open("a.txt", O_RDWD | O_CREAT, 0666); 9 if (fd == -1) /* 失败 */ 10 return -1;
函数参数以及返回值:
path:需要打开文件的路径
oflag:打开文件的权限(比如是否可以读、是否可以写等)
mode:创建文件(O_CREAT)的权限,0666表示用户权限、组权限和其他人权限,我将在下面介绍
返回值:成功返回文件描述符;出错返回-1
关于oflag的定义一般有以下几类,不同类之间可以如上面例子,使用“|”组合。常用的我会使用加粗字体。
O_RDONLY、O_WRONLY、O_RDWR、O_EXEC:只读、只写、既可以读也可以写、只执行
O_APPEND:追加到文件的尾部
O_CREAT:如果没有名为path的文件,就会创建此文件
O_TRUNC:如果此文件存在,而且以只写或读写打开,将其长度截断为0,也就是删除文件内容
在我们使用shell命令ls -al时,会显示文件的权限,如:
drwxr-xr-x 2 lioker lioker 4096 7月 6 2017 Music
drwxr-xr-x其中的d表示目录,d后面的rwx表示用户权限为7(rwx分别用二进制表示,如果有权限为1,没有权限为0);rwx后面的r-x表示组中没有写权限,为5,其他人权限与组权限相同。
1 /* 文件读写函数 */ 2 #include <unistd.h> 3 4 ssize_t read(int fd, void *buf, size_t nbytes); 5 ssize_t write(int fd, const void* buf, size_t n); 6 7 /* 例子 */ 8 int res; /* 用来接收读写文件的返回值 */ 9 int buf[3] = {65532, 65533, 65534}; 10 int rcv; 11 12 res = write(fd, buf, 3 * 4); /* 3为个数,4为字节数 */ 13 printf("Write Bytes: %d\n", res); 14 15 res = read(fd, &rcv, 4); /* buf为World World */ 16 printf("Read Bytes: %d Content: %d\n", res, rcv);
函数参数以及返回值:
fd:文件打开函数返回的文件描述符
buf:读/写变量数据
nbytes:需要读的字节数
n:需要写的字节数
返回值:成功返回实际读写的字节数;出错返回-1
如果读者执行上面两行代码,会发现read函数返回的是写入的值。
这是由于文件有一个与其关联的“文件偏移量”,用于表示当前文件读/写到哪里。这个偏移量我们可以使用lseek函数控制。
还有一点需要读者注意:
对于read和write函数,一定要注意其操作的是内存中的字节数,比如要用read和write去读写int类型变量,则一次性要读写32位,也就是4字节。因此其是二进制还是文本模式取决于对字节的解释。具体请查看上面的例子。
1 /* 文件偏移量控制 */ 2 #include <unistd.h> 3 4 off_t lseek(int fd, off_t offset, int whence); 5 6 /* 例子 */ 7 lseek(fd, 0, SEEK_SET);
函数参数以及返回值:
fd:文件打开函数返回的文件描述符
offset:偏移量
whence:偏移量设置方式
返回值:成功返回新的文件偏移量;出错返回-1
关于whence有以下几类(下面的offset就是lseek()的第二个参数数值):
SEEK_SET:从文件开始处开始偏移offset
SEEK_CUR:从当前位置处开始偏移offset
SEEK_END:从文件结尾处开始偏移offset
1 /* 文件关闭函数 */ 2 #include <unistd.h> 3 4 int close(int fd); 5 6 /* 例子 */ 7 close(fd);
函数参数以及返回值:
fd:文件打开函数返回的文件描述符
返回值:成功返回0;出错返回-1
三、文件操作示例
1 #include <stdio.h> 2 #include <fcntl.h> 3 #include <unistd.h> 4 #include <string.h> 5 6 #define BUFSIZE 4096 7 8 int main() 9 { 10 int fd = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); 11 12 int res; /* 用来接收读写文件的返回值 */ 13 14 char snd = 'A'; 15 int i; 16 char buf[BUFSIZE] = {}; 17 18 #if 1 19 for (i = 0; i < BUFSIZE - 1; ++i) { 20 buf[i] = snd; 21 } 22 23 res = write(fd, buf, strlen(buf)); 24 printf("Write Bytes: %d\n", res); 25 #else 26 for (i = 0; i < BUFSIZE; ++i) { 27 write(fd, &snd, 1); 28 } 29 #endif 30 31 lseek(fd, 0, SEEK_SET); 32 33 res = read(fd, buf, BUFSIZE); 34 printf("Read Bytes: %d Content: %s\n", res, buf); 35 36 return 0; 37 }
四、I/O的效率
由于read()和write()是不带缓冲的,因此每一次的调用都会进行一次内核调用,这会对I/O的效率造成很大的影响。
比如把第三节程序的第18行#if 1改为0,然后在命令行编译后使用,可以发现效率有明显的降低:
time ./a.out
一般在使用时,我们会定义一个缓存区用于存储数据,等到数据满了之后,再调用read()和write()。
五、原子操作
原子操作指的是一系列的操作是密不可分的,要么完成全部,要么一个都没完成,是不可能只执行了其中的一部分的。
六、dup()和dup2()
dup()和dup2()用来复制一个现有的文件描述符。函数声明如下:
1 /* 复制文件描述符函数 */ 2 #include <unistd.h> 3 4 int dup(int fd); 5 int dup2(int fd, int fd2); 6 7 /* 例子 */ 8 int new = dup(fd); 9 printf("Dup New Fd: %d\n", new); 10 11 int new = dup2(fd, new); 12 printf("Dup New Fd: %d\n", new);
函数参数以及返回值:
fd:文件打开函数返回的文件描述符
fd2:需要fd变换为的文件描述符
返回值:成功返回新的文件描述符;出错返回-1。对于dup()来说,它会返回未使用的最小值;对于dup2()来说,如果fd2已经被占用,则会先关闭fd2,然后返回fd2;当fd1和fd2相等时,直接返回fd2。
需要注意的是,dup()和dup2()创建的新fd共享传入参数fd的文件标志和文件偏移量。
七、fcntl()
fcntl()可以用来获取或设置文件描述符的属性。函数声明如下:
1 /* 文件描述符属性控制函数 */ 2 #include <fcntl.h> 3 4 int fcntl(int fd, int cmd, ...); 5 6 /* 例子 */ 7 int acc = fcntl(fd, F_GETFL, 0); 8 9 int flg = acc & O_ACCMODE; 10 if (flg & O_RDONLY) 11 printf("O_RDONLY\n"); 12 else if (flg & O_WRONLY) 13 printf("O_WRONLY\n"); 14 else if (flg & O_RDWR) 15 printf("O_RDWR\n"); 16 else 17 printf("NONE\n");
函数参数以及返回值:
fd:文件打开函数返回的文件描述符
cmd:命令操作
返回值:成功返回对应的值;出错返回-1。
关于cmd有以下几类:
F_DUPFD:复制一个已有的描述符
F_GETFD、F_SETFD:获取或设置文件描述符标志
F_GETFL、F_SETFL:获取或设置文件状态标志
F_GETLK、F_SETLK、F_SETLKW:获取或设置记录锁,其中F_SETLKW表示若设置记录锁失败,则会等待直至成功
利用fcntl()函数修改文件描述符标志或者文件状态标志时,必须先获取当前的标志状态,然后再追加更新,最后将新的状态标志设置写入回去,如果直接设置会导致旧的标志被复位。
第六节讲解的dup()和dup2()也可以使用fcntl()完成。代码如下:
1 /* 相当于dup */ 2 int newfd; 3 newfd = fcntl(fd, F_DUPFD, 0); 4 printf("New Fd: %d\n", newfd); 5 6 /* 相当于dup2(),区别需要注意:dup()是原子操作 */ 7 close(newfd); 8 newfd = fcntl(fd, F_DUPFD, newfd); 9 printf("New Fd: %d\n", newfd);
八、ioctl
ioctl()函数是一个功能比较混杂的函数。通常用于终端I/O,函数声明如下:
1 /* 终端I/O控制函数 */ 2 #include <sys/ioctl.h> 3 4 int ioctl (int fd, unsigned long int request, ...);
九、其他函数
文件创建函数creat(),函数声明如下:
/* 文件创建函数 */ #include <fcntl.h> int creat(const char *pathname, mode_t mode);
此函数有一个致命的缺陷:不是原子操作。因此一般使用open()函数和O_CREAT选项代替。
同步函数sync()、fsync()和fdatasync(),函数声明如下:
1 /* 同步函数 */ 2 #include <unistd.h> 3 4 void sync(void); 5 int fsync(int fd); 6 int fdatasync(int fd);
UNIX操作系统中有磁盘缓冲功能,当程序向硬盘写入内容时,并不会每次都去写硬盘,而是将待写入的东西缓存buffer中,在稍后将多次缓存的数据一次性写入硬盘,这种方式称为延迟写。通常内核会在缓冲区满了或者需要重用缓冲区时进行刷新写入。因此提供了上面三个函数,其作用如下:
sync()对整个缓冲区作用生效,不等待实际磁盘操作的结束就返回;
fsync()对指定的文件描述符作用生效,它等待磁盘操作结束才返回;
fdatasync()函数和fsync()函数类似,区别是它只刷新文件的数据部分,不刷新文件的属性部分。
下一章 第四章:文件和目录