对于传统的操作系统来说,普通的 I/O 操作一般会被内核缓存,这种 I/O 被称作缓存 I/O。本文所介绍的文件访问机制不经过操作系统内核的缓存,数据直接在磁盘和应用程序地址空间进行传输,所以该文件访问的机制称作为直接 I/O。Linux 中就提供了这样一种文件访问机制,对于那种将 I/O 缓存存放在用户地址空间的应用程序来说,直接 I/O 是一种非常高效的手段。& ?$ R/ L* _) f$ k
“Linux 中一切皆文件”这句话已经不知道说了多少遍了,后面也会提到很多次。那么在深入学习之前,肯定要掌握对 Linux 文件的各种操作,包括读、写、创建等基本知识。
本章配套视频为:
“视频 05_01 文件 IO 之 open 打开操作”2 e% F1 e6 B/ V4 M- P7 E- J
“视频 05_02 文件 IO 之 creat 创建操作”9 Q! Z7 J" N7 H6 T: W
“视频 05_03 文件 IO 之 write 写操作”* v+ r6 f# Z0 p) t' s/ b
“视频 05_04 文件 IO 之 read 读操作”2 Z! J" O7 s' ^; c _: X7 h
16.1 Linux 中 中 IO 的概念介绍
所有的 I/O 操作都是通过读文件或者写文件来完成的。在这里,把所有的外围设备,包括键盘和显示器,都看成是文件系统中的文件。$ Q% Z& W+ X& W- q0 \% B0 I
什么是缓存 I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache ) 中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。缓存 I/O 有2 s( P% W6 |1 J( j1 l, d
以下这些优点:) { e* o% j) w0 q, r
缓存 I/O 使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备。) L' Q; @# S/ t0 e. O
缓存 I/O 可以减少读盘的次数,从而提高性能。. A; t) G. H# F# i
当应用程序尝试读取某块数据的时候,如果这块数据已经存放在了页缓存中,那么这块数据就可以立即返回给应用程序,而不需要经过实际的物理读盘操作。当然,如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。对于写操作来说,应用程序也会将数据先写到" }5 y: `* F9 \1 f
页缓存中去,数据是否被立即写到磁盘上去取决于应用程序所采用的写操作机制:如果用户采用的是同步写机制( synchronous writes ), 那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止;如果用户采用的是延迟写机制( deferred writes ),那么应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中去就可以了。在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。与异步写机制( asynchronous writes )不同的是,延迟写机制在数据完全写到磁盘上的时候不会通知应用程序,而异步写机制在数据完全写到磁盘上的时候是会返回给应用程序的。所以延迟写机制本身是存在数据丢失的风险的,而异步写机制则不会有这方面的担心。* D% j" z+ B. y3 Q) r* y4 F# L! x
缓存 I/O 的缺点
在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样的 话,数据在传输过程中需要在应用程序地址空间和页缓存之间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是% P5 E( G. ], ?/ p6 n( Z
非常大的。
对于某些特殊的应用程序来说,避开操作系统内核缓冲区而直接在应用程序地址空间和磁盘之间传输数据会比使用操作系统内核缓冲区获取更好的性能,下边这一小节中提到的自缓存应用程序就是其中的一种。- [* ~* R9 s. V, ^# D. E
小贴士-关于文件的相关知识的一点说明
在 Linux 中,文件非常重要,所以在 linux 系统中提供了丰富的文件操作函数。, I3 \7 p: D9 c0 C
在系统编程中,只会介绍编程中用到的最终的 API 和必须掌握的知识,还有大量的和文件相关的知识,例如高级 IO,制作文件系统,虚拟文件系统,文件共享,网络文件系统等等。
如果真是要详细的介绍整个 linux 文件,可能好几千页的书都介绍不完,而且对于大家学习初期也没有太大的帮助,得不偿失。
手册中还会以其它形式介绍文件相关的知识,有的时候会介绍一步一步介绍如何操作,例如制作文件系统,NFS 网络启动;有时候会介绍如何使用,例如内核教程中介绍的虚拟文件系统等等。大家如果感兴趣,在学习教程之外可以通过学习相关知识,去了解更加具体的含义。- Q: ]1 {$ [+ S i" u
文件对于 Linux 实在是太重要了,不过大家学习了本章的知识和实验,了解其他和文件系统相关的知识,在后面 Linux 编程中遇到的问题基本可以自行解决了。
函数头文件
在所有的 Linux 系统中,如果需要对文件的进行操作,只要包含如下 4 个头文件即可。; v6 @% D/ c% S* N
#include <unistd.h>
#include <sys/types.h>. T6 i2 n ]# ^: G/ I$ A
#include <sys/stat.h>
#include <fcntl.h>
上面四个头文件中包含了打开,关闭,创建,读文件,写文件的函数,还有标志位,以及在不同 32 位以及 64 位系统下数据长度的宏变量定义。6 A! Q. Z1 C' @$ i" X9 a: s1 N, f
16.2 打开文件函数 open
使用 open 函数的时候会返回一个文件句柄,文件句柄是文件的唯一识别符 ID。对文件的操作必须从读取句柄开始。
先来看一下函数 open 的两个原型。
int open(const char *path, int oflags);
有两个参数的‘open’函数主要用于创建文件,在本章的 10.5.5 小节会和创建文件的函数 creat 的同时介绍具体用法,并给出例子。
int open(const char *path, int oflags,mode_t mode);
open 函数可以建立一个文件或者设备的访问路径。在打开或创建文件时可以指定文件的属性及用户的. L& J; q- B; Q1 d f1 i: Z
权限等参数。
第一个参数 path 表示:路径名或者文件名。路径名为绝对路径名,例如开发板中的 led 驱动的设备节点/dev/leds。8 }7 ?; p3 w% V9 ~" A. V
第二个参数 oflags 表示:打开文件所采取的动作。下 面 三 个 选 项 是 必 须 选 择 其 中 之 一的 。0 m! t; p: ]. k
O_RDONLY 文件只读
O_WRONLY 文件只写
O_RDWR 文件可读可写9 }8 |, K3 ~0 s- Q7 j
下面是可以任意选择的。! X) G1 E7 g& |
O_APPEND 每次写操作都写入文件的末尾* p x5 c) d e4 F ^, Z$ E y
O_CREAT 如果指定文件不存在,则创建这个文件6 R/ J" y; h3 i( X/ j
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值+ i4 h8 \4 U: [: l* A- g- H8 @
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容( C) ~* x& ?3 s& m
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O 设置为非阻塞模式0 |! H( [4 d2 V! N; N2 k ~
(nonblocking mode),后面会介绍什么是阻塞和非阻塞。5 p9 V% g: X+ C# `
O_NDELAY 和 O_NONBLOCK 功能类似,调用 O_NDELAY 和使用的 O_NONBLOCK 功能是一样的。第三个参数 mode 表示:设置创建文件的权限。0 {* L: T& Z& J; E
S_IRUSR,S_IWUSER,S_IXUSR,S_IRGRP,S_IWGRP,S_IXGRP,S_IROTH,S_IWOTH,S_IXOTH. 其中 R:读,W:写,X:执行,USR:文件所属的用户,GRP:文件所属的组,OTH: 其他用户。第三个参数可以直接使用参数代替,参考 10.4.5 小节‘Linux 权限’。
前面用过的‘chmod 0777 helloworld’命令,其中的含义是一样的,只不过 chmod 是在文件创建之后再修改权限。# Q8 U- t3 k! `* S, `
open 函数代码3 j$ v; e; x; ?) s* Q0 U
编写简单的 open.c 文件测试 open 函数。首先添加头文件,如下图所示。
首先添加头文件,如下图所示。

然后 main 函数如下。

上图中打开了三个文件分别属于不同的情况。
/dev/leds 已经在开发板中存在,属于驱动的设备节点,在 linux 驱动教程中会具体介绍
/bin/test1 和/bin/test2 都不存在3 d* I2 e& K& t9 j, d
使用 open 函数调用上面三个文件,如果出错就会打印错误,然后打印句柄。4 e- t/ u1 a# {: }5 I& P4 ]
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode/iofile”,使用命令“mkdir iofile”新建 iofile 文件夹,将源码 open.c 拷贝进去,进入新建的文件夹 iofileopen,如下图所示。

使用命令“arm-none-linux-gnueabi-gcc -o open open.c -static”编译 open 文件,如下图所示,使用命令“ls”可以看到生成了 open 可执行文件。

这里介绍 U 盘拷贝代码的方法,也可以编译进文件系统,具体方法参考 10.3.5 小节。将编译成的可执行文件 open,拷贝到 U 盘,启动开发板,插入 U 盘,加载 U 盘,运行程序如下。

如上图所示,可以看到打开/dev/leds 成功,这个是板载 LED 的内核驱动,调用的时候, 还调用了内核驱动中的函数,这个函数会打印“LEDS_CTL DEBUG
evice Opened Success!”和“LEDS_CTL DEBUG
eviceOpened Success!”
然后打印句柄 ID,/dev/leds fd is 3
调用“/bin/test1”会报错“open /bin/test1 failed”,这种打开文件的方式是 linux 中标准的用法,几乎所有对文件的 open 操作都会加上出错报警的语句。
创建“/bin/test2”会打印“/bin/test2 fd is 4”,表明创建“/bin/test2 ”成功了。
使用命令“ls /bin/test2”,查看一下对应目录‘/bin’下应该新建了“test2”,如下图所示。

另外的“dev/leds”本身就存在,如下图所示,这是驱动的设备节点文件,在后面的实验会介绍如何操作调用,在 linux 驱动实验中会介绍这个设备节点文件是如何生成的。

16.3 创建函数 creat 和 和 open
creat 函数介绍* ~; S3 |" s& X0 G. V. t
关于 creat 函数,首先这个单词并不是表示创建的意思,创建的英文单词是“create”, 这是早期的一个小的拼写错误,却一直沿用下来。5 {3 r- W/ U: `+ M1 @
在介绍 open 函数的时候,可以看到 open 函数有两种形式,一个是两个参数一个是三个参数,早期的时候 open 只有三个参数的形式,三个参数的形式会导致 open 函数无法打开一个未创建的文件,也就是无法建立文件,所以就有了这个 creat 函数。
现在 creat 函数可以完全用 open 替代,考虑到在阅读代码的时候可能会碰到,所以简单介绍一下。
creat 函数原型如下。4 s; l5 h# P7 Q! `2 M( o7 `7 x k
int creat(const char * pathname, mode_t mode);! c/ s3 M* ~6 B
creat 函数只有两个参数,参数的含义和 open 类似。大家看到这个函数的时候知道它是创建文件的就成,在写代码的时候完全可以用 open 代替。* z" ~8 ^: R% j j P
creat 函数例程
编写简单的 creat.c 文件测试 creat 函数。首先添加头文件,如下所示。

然后 main 函数如下所示。

第 22 行、27 行、31,open 可以打开已有的文件,也可以打开不存在的文件,即创建文件,创建文件的时候需要在参数中添加标志位 O_CREAT。在第 27 行代码中,没有添加标志位,运行的时候肯定会报错,这么写是希望大家能够记住这个参数。
第 36 行是使用 creat 函数创建文件“test3”,注意一下 creat 函数和 open 函数在创建文件的时候,参数的区别。) c) e% z$ s8 y8 `% o3 R0 u
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode/iofile”,将源码creat.c 拷贝进去,如下图所示。

使用命令“arm-none-linux-gnueabi-gcc -o creat creat.c -static”编译 creat.c 文件,如下图所示,使用命令“ls”可以看到生成了 creat 可执行文件。

这里介绍 U 盘拷贝代码的方法,也可以编译进文件系统,具体方法参考 10.3.5 小节
将编译成的可执行文件 creat,拷贝到 U 盘,启动开发板,插入 U 盘,加载 U 盘,运行程序如下。

如上图所示。# k" X5 j8 `; N q) T# l/ C) g
打开文件"/dev/leds"成功,这个文件已经存在
打开文件"/bin/test1"失败,因为没有添加参数 O_CREAT,这个文件不存在,新建的时候需要参数5 u( s! A2 _! @2 J* D; }& j
O_CREAT。
打开文件"/bin/test2"成功,不存在这个文件,创建成功。 X3 a! B Y8 L! ^9 ?: {* L
打开文件"/bin/test3"成功,不存在这个文件,使用 creat 新建成功。
如下图所示,使用命令“ls /bin/test* ”在“/bin”目录下可以看到新建的文件 test2 和 test3。

16.4 关闭函数 close
任何一个文件在操作完成之后都需要关闭,这个时候需要调用 close 函数。/ O8 q( |4 u- n6 |* \
close 函数介绍
调用 close 函数之后,会取消 open 函数建立的映射关系,句柄将不再有效,占用的空间将被系统释放。
close 函数在头文件“#include <unistd.h>”中,close 函数的使用和参数都比较简单.
int close(int fd);; W% D7 X0 O" V. f0 }0 y
参数 fd,使用 open 函数打开文件之后返回的句柄。返回值,一般很少使用 close 的返回值。
close 函数例程+ X9 s+ Z9 i6 U& F: ~
调用很简单,在下一个实验中会永到 close 函数。
16.5 写函数 write
对文件进行写操作,write 函数使用的比较多。
write 函数介绍
write 函数在头文件“#include <unistd.h>”中。# l; r, I3 @, u+ u0 o
函数原型为 ssize_t write(int fd,const void *buf,size_t count) 参数 fd,使用 open 函数打开文件之后返
回的句柄。% z8 \9 w4 M! x4 m: f) U
参数*buf,需要写入的数据。
参数 count,将参数*buf 中最多 count 个字节写入文件中。
返回值为 ssize 类型,出错会返回-1,其它数值表示实际写入的字节数。
write 函数例程' p3 g, f1 j4 @; |! {+ ?. c1 h
编写简单的 write.c 文件测试 write 函数。首先添加头文件,如下所示。
//标准输入输出头文件
#include <stdio.h>2 l. X3 `0 A) U" u9 [7 m+ p. \
//文件操作函数头文件
#include <sys/types.h>9 m& p7 l! Y1 C- j; ?
#include <sys/stat.h>' K" a7 d. I* t) w. R9 N7 R2 ^. O
#include <fcntl.h>
#include <unistd.h>% n- i1 z7 p/ ]' y
#include <string.h>" _ [& h L& S7 O4 u) J; L ]
然后 main 函数如下所示。

如上图代码所示。
在 16 行定义了 buffer_write 字符数组。" T9 ^2 P- V. N, L) [
在 18 行,进行写操作之前,必须得到文件的句柄,在这一行中使用 open 函数创建和打开文件“/bin/testwrite"。: z. f( @+ `" ^6 G( [, x
在 23 行中会调用 write 函数,将 buffer_write 字符数组中的内容写到新建的文件中。5 z) I6 ~9 [6 x
在 31 行调用 close 函数,将"/bin/testwrite"文件关闭。) Y/ A) T+ k3 Z1 D# J6 u! e
后面测试的时候可以在超级终端中,使用 vi 编辑器打开"/bin/testwrite"文件,可以看到这个文件中有字符 Hello Write Function!./ R3 S6 ]5 n4 B8 f
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode/iofile”,将源码write.c 拷贝进去,如下图所示。

使用命令“arm-none-linux-gnueabi-gcc -o write write.c -static”编译 write.c 文件,如下图所示,使用命令“ls”可以看到生成了 write 可执行文件。

这里介绍 U 盘拷贝代码的方法,也可以编译进文件系统。 u- ^( U' P1 W8 K8 D8 c% n
将编译成的可执行文件 write,拷贝到 U 盘,启动开发板,插入 U 盘,加载 U 盘,运行程序。如下图所示,打印出了 Write Function OK!。

在代码中定义的文件是"/bin/testwrite",使用 vi 编辑器打开文件,如下图所示,程序执行运行成功。

16.6 文件的读 read 函数' w/ G, f3 A' N% c: b0 ]/ `
对文件进行写操作,read 函数使用的比较多。
read 函数介绍; E" I; G5 K/ r `. l
read 函数在头文件“#include <unistd.h>”中。$ r, q6 l I9 p5 P
函数原型为 ssize_t read(int fd,void *buf,size_t len)
参数 fd,使用 open 函数打开文件之后返回的句柄。
参数*buf,读出的数据保存的位置。& h/ x2 ~& }- c% j" _; ]
参数 len,每次最多读 len 个字节。
返回值为 ssize 类型,出错会返回-1,其它数值表示实际写入的字节数,返回值大于 0 小于 len 的数
值都是正常的。8 E0 a) e# c9 u0 ?0 G; @$ Y0 J
read 函数例程7 ?. d6 R" r% ]& g! H
编写简单的 read.c 文件测试 read 函数。+ l' v: I: I# p6 R9 }
首先添加头文件和定义读函数缓冲区为 1000,如下图所示。
//标准输入输出头文件5 V7 P1 d; o2 h$ m, o1 l* b1 l
#include <stdio.h>2 ]' t. | S+ W3 @/ }' i
//文件操作函数头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>- }9 u9 }9 T; _( z
#include <unistd.h>. W' w: ?3 B1 I
#include <string.h>
然后 main 函数如下图所示。

如上代码所示。 在 1-9 行,头文件。1 K! L( m( d1 b1 l: x- g
在 20 行,使用 open 函数打开或者新建"/bin/testwrite"文件。% J! u# e/ n4 S& f) R, I: @
在 23 行,使用 write 函数将 buffer 中的内容写到"/bin/testwrite"文件中。在 35 行,使用 read 函数,将"/bin/testwrite"文件中的内容读出来。
在 38 行,使用打印函数 printf 打印 read 函数读出的数据。在 39 行,调用 close 函数关闭打开的文件,程序结束。1 ?1 X) [ {7 s8 J- H# [, H
最终测试的时候,除了会出现"/bin/testwrite"文件,还会打印 read 函数读取的数据。: ^# I e/ p! K3 ?: ^# Z# L6 v
编译运行测试
在 Ubuntu 系统下,如下图所示,进入前面实验创建的目录“/home/linuxsystemcode/iofile”,将源码read.c 拷贝进去,如下图所示。

使用命令“arm-none-linux-gnueabi-gcc -o read read.c -static”编译 read.c 文件,如下图所示,使用命令“ls”可以看到生成了 read 可执行文件。

这里介绍 U 盘拷贝代码的方法,也可以编译进文件系统。
将编译成的可执行文件 read,拷贝到 U 盘,启动开发板,插入 U 盘,加载 U 盘,运行程序。如下图所示,可以看到打印出了“Files Content is Hello Write Function”,使代码中预期的结果。


| 欢迎光临 EDA365电子论坛网 (https://bbs.eda365.com/) | Powered by Discuz! X3.2 |