Linux之文件IO
对于传统的操作系统来说,普通的 I/O 操作一般会被内核缓存,这种 I/O 被称作缓存I/O。
本文所介绍的文件访问机制不经过操作系统内核的缓存,数据直接在磁盘和应用程序地址空间进行传输,所以该文件访问的机制称作为直接 I/O。
Linux 中就提供了这样一种文件访问机制,对于那种将 I/O 缓存存放在用户地址空间的应用程序来说,直接 I/O 是一种非常高效的手段。
一、 Linux 中 IO 的概念介绍
所有的 I/O 操作都是通过读文件或者写文件来完成的。在这里,把所有的外围设备,包括键盘和显示器,都看成是文件系统中的文件。
Linux 中一切皆文件
什么是缓存 I/O?
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。缓存 I/O 有以下这些优点:
- 缓存 I/O 使用了操作系统内核缓冲区,在一定程度上分离应用程序空间和实际的物理设备。
- 缓存 I/O 可以减少读盘的次数,从而提高性能。
当应用程序尝试读取某块数据的时候,如果这块数据已经存放在了页缓存中,那么这块数据就可以立即返回给应用程序,而不需要经过实际的物理读盘操作。当然,如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。
对于写操作来说,应用程序也会将数据先写到页缓存中去,数据是否被立即写到磁盘上去取决于应用程序所采用的写操作机制:
3. 如果用户采用的是同步写机制( synchronous writes ), 那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止;
4. 如果用户采用的是延迟写机制( deferred writes ),那么应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中去就可以了。
在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。与异步写机制( asynchronous writes )不同的是,延迟写机制在数据完全写到磁盘上的时候不会通知应用程序,而异步写机制在数据完全写到磁盘上的时候是会返回给应用程序的。所以延迟写机制本身是存在数据丢失的风险的,而异步写机制则不会有这方面的担心。
因此,缓存 I/O 有以下这些缺点:在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样的话,数据在传输过程中需要在应用程序地址空间和页缓存之间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
对于某些特殊的应用程序来说,避开操作系统内核缓冲区而直接在应用程序地址空间和磁盘之间传输数据会比使用操作系统内核缓冲区获取更好的性能,下边这一小节中提到的自缓存应用程序就是其中的一种。
函数头文件简介
在所有的 linux 系统中,如果需要对文件的进行操作,只要包含如下 4 个头文件即可。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
上面四个头文件中包含了打开,关闭,创建,读文件,写文件的函数,还有标志位,以及在不同 32 位以及 64 位系统下数据长度的宏变量定义。
二、 打开文件函数open
1、open 函数例程
使用 open 函数的时候会返回一个文件句柄,文件句柄是文件的唯一识别符 ID。对文件的操作必须从读取句柄开始。
先来看一下函数 open 的两个原型。
int open(const char *path, int oflags);
//两个参数的 open 函数主要用于创建文件
int open(const char *path, int oflags,mode_t mode);
//open 函数可以建立一个到文件或者设备的访问路径。
//在打开或创建文件时可以制定文件的属性及用户的权限等各种参数。
- 第一个参数 path 表示:路径名或者文件名。路径名为绝对路径名,例如开发板中的 led驱动的设备几点/dev/leds。
- 第二个参数 oflags 表示:打开文件所采取的动作。
下面三个选项是必须选择其中之一的。
O_RDONLY 文件只读
O_WRONLY 文件只写
O_RDWR 文件可读可写
下面是可以任意选择的。
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode),后面会介绍什么是阻塞和非阻塞。
O_NDELAY 和 O_NONBLOCK 功能类似,调用 O_NDELAY 和使用的O_NONBLOCK 功能是一样的。 - 第三个参数 mode 表示:设置创建文件的权限。
S_IRUSR,
S_IWUSER,
S_IXUSR,
S_IRGRP,
S_IWGRP,
S_IXGRP,
S_IROTH,
S_IWOTH,
S_IXOTH.
其中 R:读,W:写,X:执行,USR:文件所属的用户,GRP:文件所属的组,OTH:其
他用户。第三个参数可以直接使用参数代替,参考 linux 权限。这个权限看似复杂,其实可以使用字母代替。前面用过的 chmod 0777 helloworld 命令,其中的含义是一样的,只不过 chmod 是文件创立之后再修改权限。
2、open 函数代码
编写简单的 open.c 文件测试 open 函数。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main(){
int fd;
char *leds = "/dev/leds";
char *test1 = "/bin/test1";
char *test2 = "/bin/test2";
if((fd = open(leds,O_RDWR|O_NOCTTY|O_NDELAY))<0){
printf("open %s failed!\n",leds);
}
printf("\n%s fd is %d\n",leds,fd);
if((fd = open(test1,O_RDWR,0777))<0){
printf("open %s failed!\n",test1);
}
printf("%s fd is %d\n",test1,fd);
if((fd = open(test2,O_RDWR|O_CREAT,0777))<0){
printf("open %s failed!\n",test2);
}
printf("%s fd is %d\n",test2,fd);
}
代码中打开了三个文件分别属于不同的情况。
/dev/leds 已经在开发板中存在,属于驱动的设备节点。
/bin/test1 和/bin/test2 都不存在
使用 open 函数调用上面三个文件,如果出错就会打印错误,然后打印句柄。
运行结果如下:
三、 创建函数 creat 和 open
1、creat 函数介绍
关于 creat 函数,首先这个单词并不是表示创建的意思,创建的英文单词是“create”,这是早期的一个小的拼写错误,却一直沿用下来。
在介绍 open 函数的时候,可以看到 open 函数有两种形式,一个是两个参数一个是三个参数,早期的时候 open 只有三个参数的形式,三个参数的形式会导致 open 函数无法打开一个未创建的文件,也就是无法建立文件,所以就有了这个 creat 函数。
现在 creat 函数可以完全用 open 替代,考虑到在阅读代码的时候可能会碰到,所以简单介绍一下。
creat 函数原型如下。
int creat(const char * pathname, mode_t mode);
creat 函数只有两个参数,参数的含义和 open 类似。
大家看到这个函数的时候知道它是创建文件的就成,在写代码的时候完全可以用 open 代替
2、creat 函数例程
//标准输入输出头文件
#include <stdio.h>
//文件操作函数头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
int fd;
//开发板中已经存在/dev/leds文件
char *leds = "/dev/leds";
//开发板中不存在的文件/bin/test1
char *test1 = "/bin/test1";
//开发板中不存在的文件/bin/test2
char *test2 = "/bin/test2";
//需要新建的文件/bin/test3
char *test3 = "/bin/test3";
//使用open函数打开文件
if((fd = open(leds, O_RDWR|O_NOCTTY|O_NDELAY))<0){
printf("open %s failed\n",leds);
}
printf("%s fd is %d\n",leds,fd);
//使用open函数打开不存在的文件,不添加O_CREAT标识符,会报错
if((fd = open(test1, O_RDWR))<0){
printf("open %s failed\n",test1);
}
//打开文件创建文件,添加标志位O_CREAT表示不存在这个文件则创建文件
if((fd = open(test2, O_RDWR|O_CREAT,0777))<0){
printf("open %s failed\n",test2);
}
printf("%s fd is %d\n",test2,fd);
fd = creat(test3,0777);
if(fd = -1){
printf("%s fd is %d\n",test3,fd);
}
else{
printf("create %s is succeed\n",test3);
}
}
运行结果如下:
如上图所示。
打开文件"/dev/leds"成功,这个文件已经存在
打开文件"/bin/test1"失败,因为没有添加参数 O_CREAT,这个文件不存在,新建的时候需要参数 O_CREAT。
打开文件"/bin/test2"成功,不存在这个文件,创建成功。
打开文件"/bin/test3"成功,不存在这个文件,使用 creat 新建成功。
三、 关闭函数 close与写函数 write
任何一个文件在操作完成之后都需要关闭,这个时候需要调用 close 函数。
1、close函数介绍
调用 close 函数之后,会取消 open 函数建立的映射关系,句柄将不再有效,占用的空间将被系统释放。
close 函数在头文件“#include <unistd.h>”中,close 函数的使用和参数都比较简单.
int close(int fd);
//参数 fd,使用 open 函数打开文件之后返回的句柄。
//返回值,一般很少使用 close 的返回值。
2、write函数介绍
write 函数在头文件“#include <unistd.h>”中。
函数原型为
ssize_t write(int fd,const void *buf,size_t count)
//参数 fd,使用 open 函数打开文件之后返回的句柄。
//参数*buf,需要写入的数据。
//参数 count,将参数*buf 中最多 count 个字节写入文件中。
//返回值为 ssize 类型,出错会返回-1,其它数值表示实际写入的字节数。
3、write函数例程
后面测试的时候可以在超级终端中,使用 vi 编辑器打开"/bin/testwrite"文件,可以看到这个文件中有字符 Hello Write Function!.
//标准输入输出头文件
#include <stdio.h>
//文件操作函数头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
main()
{
int fd;
char *testwrite = "/bin/testwrite";
ssize_t length_w;
char buffer_write[] = "Hello Write Function!";
if((fd = open(testwrite, O_RDWR|O_CREAT,0777))<0){
printf("open %s failed\n",testwrite);
}
//将buffer写入fd文件
length_w = write(fd,buffer_write,strlen(buffer_write));
if(length_w == -1)
{
perror("write");
}
else{
printf("Write Function OK!\n");
}
close(fd);
}
运行结果如下:
四、文件的读 read 函数
对文件进行写操作,read 函数使用的比较多。
1、read函数介绍
read 函数在头文件“#include <unistd.h>”中。
函数原型为
ssize_t read(int fd,void *buf,size_t len)
//参数 fd,使用 open 函数打开文件之后返回的句柄。
//参数*buf,读出的数据保存的位置。
//参数 len,每次最多读 len 个字节。
//返回值为 ssize 类型,出错会返回-1,其它数值表示实际写入的字节数,返回值大于 0 小于 len 的数值都是正常的。
2、read函数例程
最终测试的时候,除了会出现"/bin/testwrite"文件,还会打印 read 函数读取的数据。
//标准输入输出头文件
#include <stdio.h>
//文件操作函数头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
//定义读函数缓冲区为 1000
#define MAX_SIZE 1000
main(){
int fd;
ssize_t length_w,length_r = MAX_SIZE,ret;
char *testwrite = "/bin/testwrite";
char buffer_write[] = "Hello Write Function!";
char buffer_read[MAX_SIZE];
if((fd = open(testwrite,O_RDWR|O_CREAT,0777))<0){
printf("open %s failed!\n",testwrite);
}
length_w = write(fd,buffer_write,strlen(buffer_write));
if(length_w == -1){
perror("write");
}
else{
printf("Write Function OK!\n");
}
close(fd);
if((fd = open(testwrite,O_RDWR|O_CREAT,0777))<0){
printf("open %s failed!\n",testwrite);
}
if(ret = read(fd,buffer_read,length_r)){
perror("read");
}
printf("Files Content is %s \n",buffer_read);
close(fd);
}
运行结果: