今天继续学习文件与io,主要是学习文件共享及文件、复制文件描述符,有点抽象,主要是概念上的理解,但是很重要,下面一一来分解:
文件共享:
回顾一下,在linux系统调用中,是通过文件描述符来访问文件的,文件描述符是一个非负的整数,这是站在用户的观点来看的,实际上在linux内核上是有一定的数据结构来表示文件描述符的,下面就从三方面来看图分析一下内核中是怎么来表示打开的文件的:
一个进程打开两个文件内核数据结构:
![](https://i-blog.csdnimg.cn/blog_migrate/71d265ae0c934a9c0ae7c5f2853c6e0b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/100f978e63cef079d5aada2b86c53f87.png)
(
说明:关于这点,可以看一下我之前的博客:
http://www.cnblogs.com/webor2006/p/3487718.html)
所以,由于文件描述符0、1、2被占用了,则我们用户打开的文件描述符只能从第3开始了:
![](https://i-blog.csdnimg.cn/blog_migrate/66bbdef685bb0fae5020580b51e3aa85.png)
如图上所示,对于内核,当打开一个文件时,会有
一张表格来记录文件的状态
,回顾一下,当我们在读取文件内容的时候,会自动的从当前文件的偏移位置去读取下一个数据,在文件的随机读写中已经介绍过(http://www.cnblogs.com/webor2006/p/3493218.html),原因就在于这个偏移量就保存在文件表当中的:
![](https://i-blog.csdnimg.cn/blog_migrate/a889e0b9f17828b4c6be90e384270bdd.png)
而每当我们打开一个文件时,内核就会为文件分配一个文件表,里面有不同的项,其中当前文件偏移量就是一个文件表项
回忆一下,我们打开一个文件,可以以读、写、追加、同步、非阻塞(这个之后会学到)等方式打开,用来描述它的就是另一个文件表项:文件状态标志
![](https://i-blog.csdnimg.cn/blog_migrate/d7ef4be1193ca3813d571d5f0a57a88c.png)
另外图中还有一个文件引用计数,它是用来描述一个文件被多少个文件描述符指向了
(这个在下面的复制文件描述符中就可以体会到了):
![](https://i-blog.csdnimg.cn/blog_migrate/5dc1fe8e4de54deeb0eddbdcd95f12de.png)
另外,还有一个文件表项,它是
v节点指针,它指向了
v节点表,如图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/8f8f60aca0413122c242919c730ae638.png)
其中v节点表中,存放了两项很重要的信息,一个是
v节点信息,一个是
i节点信息:
v节点信息:我们上节学习的stat函数获得文件信息返回的状态信息就全保存在v节点信息里(
http://www.cnblogs.com/webor2006/p/3496281.html):
![](https://i-blog.csdnimg.cn/blog_migrate/ffe54f57d899544c812b6f33198075e0.png)
i节点信息:当我们打开一个文件时,会将文件系统当中的i结点数据拷贝到v节点表中的i节点信息所存放的位置,比如说:
![](https://i-blog.csdnimg.cn/blog_migrate/f1de468a6f411908796f491aff729a75.png)
一个进程两次打开同一个文件内核数据结构:
![](https://i-blog.csdnimg.cn/blog_migrate/fd6691767233df98c8760bf3b00bd75f.png)
下面以具体代码来进行说明:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int fd1;
int fd2;
char buf1[1024] = {0};
char buf2[1024] = {0};
fd1 = open("test.txt", O_RDONLY);//以只读的方式打开文件
if (fd1 == -1)
ERR_EXIT("open error");
read(fd1, buf1, 5);
printf("buf1=%s\n", buf1);
fd2 = open("test.txt", O_RDWR);//以读写的方式打开文件
if (fd2 == -1)
ERR_EXIT("open error");
read(fd2, buf2, 5);
printf("buf2=%s\n", buf2);
close(fd1);
close(fd2);
return 0;
}
先新建一个test.txt,里面输点测试内容:
![](https://i-blog.csdnimg.cn/blog_migrate/1f5fed9aaea8872fdb4ded2748108a9d.png)
编译运行:
![](https://i-blog.csdnimg.cn/blog_migrate/9524cf09d96ca5adbdaf18dbd66d0b29.png)
也就是各个描述符有各自的偏移量,当buf1输出ABCDE时,如果第二个描述符共享偏移量的话,应该buf2输出FGhel,但是buf2输出的也是ABCDE,也就说明了各个文件描述符有不同的文件表项。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int fd1;
int fd2;
char buf1[1024] = {0};
char buf2[1024] = {0};
fd1 = open("test.txt", O_RDONLY);
if (fd1 == -1)
ERR_EXIT("open error");
read(fd1, buf1, 5);
printf("buf1=%s\n", buf1);
fd2 = open("test.txt", O_RDWR);
if (fd2 == -1)
ERR_EXIT("open error");
read(fd2, buf2, 5);
printf("buf2=%s\n", buf2);
write(fd2, "AAAAA", 5);
memset(buf1, 0, sizeof(buf1));
read(fd1, buf1, 5);
printf("buf1=%s\n", buf1);
close(fd1);
close(fd2);
return 0;
}
编译运行:
![](https://i-blog.csdnimg.cn/blog_migrate/8249efeaad084d8fe7e5c9169e7f23d0.png)
这结果为什么是它呢?下面来分析下:
write(fd2, "AAAAA", 5);
这时我们先来查看下test.txt的内容:
![](https://i-blog.csdnimg.cn/blog_migrate/f5e6579065a1bd417831953f2d32e876.png)
由于它会改变v节点表中的i节点信息所指向磁盘中的数据,而两个文件描述符的v节点表是共享的,而fd1此时的偏移量为5:
![](https://i-blog.csdnimg.cn/blog_migrate/b7323d7762465acea8b251806340fc4c.png)
总结:每打开一个文件描述符,就有一个对应的文件表项描述,而如果打开的是同一个文件,v节点表是共享的
两个进程打开同一个文件内核数据结构:
![](https://i-blog.csdnimg.cn/blog_migrate/fb6be497b92bdfbb974801a95a605bb2.png)
说明:不同的进程可以打开同一个文件,但是每个进程的文件描述符对应一个独立的文件表项,而最终共享v节点表。
总结:文件描述符跟文件不是一一对应的,文件描述符可以有多个,但是文件可以只有一个。
理解了打开的文件在内核中的结构,进而我们就可以理解复制文件描述符是怎么一回事了,如下:
复制文件描述符:
先用图来进行说明:
![](https://i-blog.csdnimg.cn/blog_migrate/1767a3506129a80d576f345bc5e349f6.png)
其中复制文件描述符,可以执行dup命令,注意,这时它会从0开始找出有空闲的文件描述符,如图,0、1、2是已经默认被系统给占用了,这时,执行dup之后,就会找到空闲的fd 4文件描述符,将它也指向同一个文件表,如图:
![](https://i-blog.csdnimg.cn/blog_migrate/01e8ee0d2c3c537ce6179f94f03c863a.png)
下面,以一段程序来说明一下
输出重定向的原理,先复习一下什么是输出重定向:
![](https://i-blog.csdnimg.cn/blog_migrate/915f3c5b0e8510d08baeff9d4d777931.png)
下面,以具体程序来说明它,利用的就是复制文件描述符的知识:
![](https://i-blog.csdnimg.cn/blog_migrate/eeaac5e49e738a9c8eaec669e15c09e2.png)
先本地建一个空的test2.txt文件:
![](https://i-blog.csdnimg.cn/blog_migrate/2232eaf5be9c499c2affe0f3fa2beb54.png)
分析一下这个程序:
close(1)的作用,就是为了让输出到屏幕的文件描述符成为空闲的,然后dup时,会从0开始找空闲文件描述符,发现1是空闲的,则这时它的内存模型就变成这样了:
![](https://i-blog.csdnimg.cn/blog_migrate/fcd1b7a9619a3717fd5578c17694557e.png)
所以,清楚了它之后,对于ls > aa这样的输出重定向的功能,就比较容易实现了。
另外,对于复制文件描述符有三种方法:
![](https://i-blog.csdnimg.cn/blog_migrate/350216ab8f8e69df92b67255fa835531.png)
对于dup2,理解它,我们可以将上面复制文件描述符的程序用dup2代替dup,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/62f70e49d7ccf80f2a7fe47ff2a7d8ed.png)
编译运行:
![](https://i-blog.csdnimg.cn/blog_migrate/f8243880d9c51ba9317956cf5cf8a39c.png)
另外第三种复制文件描述符的方法,是通过fcntl函数,它稍复杂一些,这个会在下节详细进行分析,先看一下man帮助:
![](https://i-blog.csdnimg.cn/blog_migrate/1090dcdcc7d9b319191d104336d832f3.png)
其中第三个参数,可以决定复制文件描述符时,从第几个描述符开始搜索空闲,利用dup实现复制文件描述符时都是从0开始搜索的。
好了,关于fcntl的使用,下节再见!