linux中文件IO
一. linux常用文件IO接口
1.1. 文件描述符
1.1.1. 文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index去索引查表得到文件表指针,再间接访问得到这个文件对应的文件表。
1.1.2. 文件描述符这个数字是open系统调用内部由操作系统自动分配的,操作系统分配这个fd时也不是随意分配,也是遵照一定的规律的:fd从0开始依次增加。fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,所以当时一个进程最多允许打开20个文件。linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是index,文件表指针是value。当我们去open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回。
1.1.3. fd中0、1、2已经默认被系统占用了,因此用户进程得到的最小的fd就是3了。这三个文件对应的fd就是0、1、2。这三个文件分别叫stdin、stdout、stderr。也就是标准输入、标准输出、标准错误。
1.2. open
1.2.1. 在linux系统中要操作一个文件,一般是先open打开一个文件,得到一个文件描述符,然后对文件进行读写操作(或其他操作),最后close关闭文件即可
1.2.2. 文件平时是存在块设备中的文件系统中的,我们把这种文件叫静态文件。当我们去open打开一个文件时,linux内核做的操作包括:内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件)。
1.2.3. 打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。这样做主要由于:块设备本身有读写限制(回忆NnadFlash、SD等块设备的读写特征),本身对块设备进行操作非常不灵活。而内存可以按字节为单位来操作,而且可以随机操作(内存就叫RAM,random),很灵活。所以内核设计文件操作时就这么设计了。
1.3. read
ssize_t read(int fd, void *buf, size_t count);
a. fd表示要读取哪个文件,fd一般由前面的open返回得到
b. buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容
c. count是我们要读取的字节数
d. 返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取的字节数。
1.4. write
ssize_t write(int fd, const void *buf, size_t count);
1.4.1. 写入用write系统调用,write的原型和理解方法和read相似
1.5. lseek
off_t lseek(int fd, off_t offset, int whence);
1.5.1. 文件指针:当我们要对一个文件进行读写时,一定需要先打开这个文件,所以我们读写的所有文件都是动态文件。动态文件在内存中的形态就是文件流的形式。
1.5.2. 在动态文件中,我们会通过文件指针来表征这个正在操作的位置。所谓文件指针,就是我们文件管理表这个结构体里面的一个指针。所以文件指针其实是vnode中的一个元素。这个指针表示当前我们正在操作文件流的哪个位置。这个指针不能被直接访问,linux系统用lseek函数来访问这个文件指针。
1.5.3. 当我们打开一个空文件时,默认情况下文件指针指向文件流的开始。所以这时候去write时写入就是从文件开头开始的。write和read函数本身自带移动文件指针的功能,所以当我write了n个字节后,文件指针会自动向后移动n位。如果需要人为的随意更改文件指针,那就只能通过lseek函数了
1.5.4. 用lseek计算文件长度(length = lseek(fd,0,SEEK_END))
1.6. close
int close(int fd);
1.6.1. 关闭打开的文件
PS:实时查man手册
(1)当我们写应用程序时,很多API原型都不可能记得,所以要实时查询,用man手册
(2)man 1 xx查linux shell命令,man 2 xxx查API, man 3 xxx查库函数
二. open函数的flag详解
2.1. 读写权限
a. O_RDONLY就表示以只读方式打开,
b. O_WRONLY表示以只写方式打开,
c. O_RDWR表示以可读可写方式打开
2.2. 打开存在并有内容的文件时
2.2.1. 当我们打开一个已经存在并且内部有内容的文件时会怎么样?
可能结果1:新内容会替代原来的内容(原来的内容就不见了,丢了)
可能结果2:新内容添加在前面,原来的内容继续在后面
可能结果3:新内容附加在后面,原来的内容还在前面
可能结果4:不读不写的时候,原来的文件中的内容保持不变
2.2.2. O_TRUNC属性去打开文件时,如果这个文件中本来是有内容的,则原来的内容会被丢弃。这就对应上面的结果1
2.2.3. O_APPEND属性去打开文件时,如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面,对应结果3
2.2.4. 默认不使用O_APPEND和O_TRUNC属性时就是结果4
2.3. 打开不存在的文件时
2.3.1. 当我们open打开一个文件时如果这个文件名不存在则会打开文件错误。
2.3.2. O_CREAT
a. O_CREAT就表示我们当前打开的文件并不存在,我们是要去创建并且打开它
b. open中加入O_CREAT后,不管原来这个文件存在与否都能打开成功,如果原来这个文件不存在则创建一个空的新文件,如果原来这个文件存在则会重新创建这个文件,原来的内容会被消除掉(有点类似于先删除原来的文件再创建一个新的)
c. open函数在使用O_CREAT标志去创建文件时,可以使用第三个参数mode来指定要创建的文件的权限。mode使用4个数字来指定权限的,其中后面三个很重要,对应我们要创建的这个文件的权限标志。譬如一般创建一个可读可写不可执行的文件就用0666
2.3.3. O_EXCL
a. O_EXCL标志和O_CREAT标志来结合使用。当这连个标志一起的时候,则没有文件时创建文件,有这个文件时会报错提醒我们。
2.4. O_NONBLOCK
2.4.1. 我们打开一个文件默认就是阻塞式的,如果你希望以非阻塞的方式打开文件,则flag中要加O_NONBLOCK标志。
2.4.2. 只用于设备文件,而不用于普通文件
2.5. O_SYNC
2.5.1. write阻塞等待底层完成写入才返回到应用层。
2.5.2. 无O_SYNC时write只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候会将buf中的内容一次性的同步到硬盘中。这种设计是为了提升硬件操作的性能和销量,提升硬件寿命;但是有时候我们希望硬件不好等待,直接将我们的内容写入硬盘中,这时候就可以用O_SYNC标志。
三. 文件读写的一些细节
3.1. errno
3.1.1. errno就是error number,意思就是错误号码。linux系统中对各种常见错误做了个编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。
3.1.2. errno是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误。
3.1.3. errno本身实质是一个int类型的数字,每个数字编号对应一种错误。当我们只看errno时只能得到一个错误编号数字(譬如-37),不适应于人看。
3.2. perror
3.2.1. linux系统提供了一个函数perror(意思print error),perror函数内部会读取errno并且将这个不好认的数字直接给转成对应的错误信息字符串,然后print打印出来。
3.3. 文件IO效率和标准IO
3.3.1. 文件IO就指的是我们当前在讲的open、close、write、read等API函数构成的一套用来读写文件的体系,这套体系可以很好的完成文件读写,但是效率并不是最高的。
3.3.2. 应用层C语言库函数提供了一些用来做文件读写的函数列表,叫标准IO。标准IO由一系列的C库函数构成(fopen、fclose、fwrite、fread),这些标准IO函数其实是由文件IO封装而来的(fopen内部其实调用的还是open,fwrite内部还是通过write来完成文件写入的)。标准IO加了封装之后主要是为了在应用层添加一个缓冲机制,这样我们通过fwrite写入的内容不是直接进入内核中的buf,而是先进入应用层标准IO库自己维护的buf中,然后标准IO库自己根据操作系统单次write的最佳count来选择好的时机来完成write到内核中的buf(内核中的buf再根据硬盘的特性来选择好的实际去最终写入硬盘中)。
3.3.3. 库函数比API还有一个优势就是:API在不同的操作系统之间是不能通用的,但是C库函数在不同操作系统中几乎是一样的。所以C库函数具有可移植性而API不具有可移植性。
3.3.4. 性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高。
四. 退出进程方式
4.1. 在main(main函数由其父进程调用,故返回后进程就over)用return,一般原则是程序正常终止return 0,如果程序异常终止则return -1。
4.2. 正式终止进程(程序)应该使用exit或者_exit或者_Exit之一。
五. 文件共享的实现方式
5.1. 什么是文件共享
5.1.1. 文件共享就是同一个文件(同一个文件指的是同一个inode,同一个pathname)被多个独立的读写体(几乎可以理解为多个文件描述符)去同时(一个打开尚未关闭的同时另一个去操作)操作。
5.1.2. 文件共享的意义有很多:譬如我们可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。
5.2. 实现共享内存三种方式
5.2.1. 同一个进程中多次使用open打开同一个文件
5.2.1.1. 我们使用open两次打开同一个文件时,fd1和fd2所对应的文件指针是不同的2个独立的指针。文件指针是包含在动态文件的文件管理表中的,所以可以看出linux系统的进程中不同fd对应的是不同的独立的文件管理表。
5.2.1.2. 两个fd读文件时互相不干扰,都可以读出各种文件管理表指针内容
5.2.1.3. 两个fd写入时独立协议。故存在后写入的覆盖现象
5.2.1.4. open时加O_APPEND标志即可解决覆盖问题。
5.2.1.4.1. O_APPEND的实现原理和其原子操作性说明
a. O_APPEND为什么能够将分别写改为接续写?O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动。(也就是说即使加了O_APPEND,fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个动了会通知另一个跟着动)
b. O_APPEND对文件指针的影响,对文件的读写是原子的。
c. 原子操作的含义是:整个操作一旦开始是不会被打断的,必须直到操作结束其他代码才能得以调度运行,这就叫原子操作。每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行。
5.2.2. 在不同进程中去分别使用open打开同一个文件(这时候因为两个fd在不同的进程中,所以两个fd的数字可以相同也可以不同)
5.2.2.1. 由于也是独立指针故和5.2.1.相似。
5.2.3. linux系统提供了dup和dup2两个API来让进程复制文件描述符
六. 实例程序
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(int argc, char *argv[]) { int fd1 = -1, fd2 = -1; // fd 就是file descriptor,文件描述符 char buf[100] = {0}; char writebuf[20] = "l love linux"; int ret = -1; // 第一步:打开文件 fd1 = open("a.txt", O_RDWR); fd2 = open("a.txt", O_RDWR); //fd = open("a.txt", O_RDONLY); if ((-1 == fd1) || (fd2 == -1)) // 有时候也写成: (fd < 0) { //printf("\n"); perror("文件打开错误"); // return -1; _exit(-1); } else { printf("文件打开成功,fd1 = %d. fd2 = %d.\n", fd1, fd2); } #if 0 // 第二步:读写文件 // 写文件 ret = write(fd, writebuf, strlen(writebuf)); if (ret < 0) { //printf("write失败.\n"); perror("write失败"); _exit(-1); } else { printf("write成功,写入了%d个字符\n", ret); } #endif #if 1 while(1) { // 读文件 memset(buf, 0, sizeof(buf)); ret = read(fd1, buf, 2); if (ret < 0) { printf("read失败\n"); _exit(-1); } else { //printf("实际读取了%d字节.\n", ret); printf("fd1:[%s].\n", buf); } sleep(1); // 读文件 memset(buf, 0, sizeof(buf)); ret = read(fd2, buf, 2); if (ret < 0) { printf("read失败\n"); _exit(-1); } else { //printf("实际读取了%d字节.\n", ret); printf("fd2:[%s].\n", buf); } } #endif // 第三步:关闭文件 close(fd1); close(fd2); _exit(0); }