首页 > 技术文章 > 第三章:文件I/O

Lioker 2019-04-02 19:58 原文

 


本章开始讨论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()函数类似,区别是它只刷新文件的数据部分,不刷新文件的属性部分。

 

 

下一章  第四章:文件和目录

 

推荐阅读