三、Linux系统之文件管理
1、硬盘中的静态文件和inode(i节点)
(2)一块硬盘可以分为两大区域:硬盘内容管理表项和真正存储内容的区域。操作系统访问硬盘时先去读取硬盘内容管理表,从中找到我们要访问的那个文件的扇区级别的信息,再用这个信息去查询真正存储内容的区域,最后得到我们要的文件。
(3)操作系统最初拿到的信息是文件名,最终得到的是文件内容。
第一步就是查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种信息,每一个文件有一个信息列表,inode实质是一个结构体,这个结构体有很多元素,每个元素记录了这个文件的一些信息,其中包括文件名、文件在硬盘上对应的扇区号、块号等内容·····)
强调:硬盘管理的时候是以文件为单位的,每个文件一个inode,每个inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。 inode其实就是硬盘用来记录文件信息的一种数据结构
(4)联系实际:格式化硬盘(U盘)时发现有快速格式化和底层格式化两种。快速格式化非常快,普通格式化格式化速度慢。这两个的差异?其实快速格式化就是只删除了U盘中的硬盘内容管理表(其实就是inode),真正存储的内容没有动。这种格式化的内容是有可能被找回的。
2、内存中被打开的文件和vnode(v节点)
(1)一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程。每个进程都有一个数据结构用来记录这个进程的所有信息,叫做进程信息表。表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息,文件管理表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode。
(2)一个vnode中就记录了一个被打开的文件的各种信息,我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。
(3)open在打开文件的时候在文件管理表中增加了一个表象,表象里的内容就是一个结构体vnode,反馈一个数字作为文件描述符,供我们使用。
3、文件与流的概念
(1)流(stream)对应自然界的水流。文件操作中,文件类似是一个大包裹,里面装了一堆字符,但是文件被读出/写入时都只能一个字符一个字符的进行。那么一个文件中N多的个字符被挨个一次读出/写入时,这些字符就构成了一个字符流。
(2)流这个概念是动态的,不是静态的。
(3)编程中提到流这个概念,一般都是IO相关的。所以经常叫IO流。文件操作时就构成了一个IO流。
4、lseek函数详解
(1)文件指针:当我们要对一个文件进行读写时,一定需要先打开这个文件,所以我们读写的所有文件都是动态文件,而动态文件在内存中的形态就是文件流的形式。
(2)文件流很长,里面有很多个字节。在动态文件中,我们会通过文件指针来表征这个正在操作的位置。所谓文件指针,就是我们文件管理表这个结构体里面的一个指针,也是vnode中的一个元素。这个指针表示当前我们正在操作文件流的哪个位置,文件指针不能被直接访问,linux系统用lseek函数来访问这个文件指针。
(3)当我们打开一个空文件时,默认情况下文件指针指向文件流的开始。所以这时候去write时写入就是从文件开头开始的。write和read函数本身自带移动文件指针的功能,所以当我write了n个字节后,文件指针会自动向后移动n位。如果需要人为的更改文件指针,只能通过lseek函数。
(4)read和write函数都是从当前文件指针处开始操作的,所以当我们用lseek显式的将文件指针移动后,那么再去read/write时就是从移动过后的位置开始的。
off_t lseek(int fd, off_t offset, int whence);
SEEK_SET :The offset is set to offset bytes.
SEEK_CUR : The offset is set to its current location plus offset bytes.
SEEK_END :The offset is set to the size of the file plus offset bytes.
(5)用lseek计算文件长度
#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 fd = -1;
int ret = -1;
char writebuf[] = "woainiy";
if (argc != 2)
{
printf("usage: %s filename\n", argv[0]);
_exit(-1);
}
fd = open(argv[1], O_RDWR);
if (-1 == fd)
{
perror("文件打开错误");
return -1;
}
ret = lseek(fd, 0, SEEK_END);// 文件末尾偏移0字节
printf("文件长度是:%d字节\n", ret);
return 0;
}
//输出:文件长度是:8字节
(6)用lseek构建空洞文件
空洞文件表示这个文件中有一段是空的。普通文件中间是不能有空的,因为我们write时文件指针是依次从前到后去移动的,不可能绕过前面直接到后面。
我们打开一个文件后,用lseek往后跳过一段,再write写入一段,就会构成一个空洞文件。
空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件,如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每个线程负责其中一段的写入。
#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 fd = -1;
char buf[100] = {0};
char writebuf[20] = "abcd";
int ret = -1;
fd = open("123.txt", O_RDWR | O_CREAT);
if (fd < 0)
{
perror("文件打开错误");
_exit(-1);
}
else
{
printf("文件打开成功,fd = %d.\n", fd);
}
ret = lseek(fd, 10, SEEK_SET);// 文件开始偏移10字节
printf("lseek, ret = %d.\n", ret);
ret = write(fd, writebuf, strlen(writebuf));
if (ret < 0)
{
perror("write失败");
_exit(-1);
}
else
{
printf("write成功,写入了%d个字符\n", ret);
}
close(fd);
return 0;
}
//输出:
//文件打开成功, fd=3
//Iseek, ret=10
//write成功, 写入了4个字符
5、多次打开同一文件与O_APPEND
(1)重复打开同一文件读取,会分别读。使用open两次打开同一个文件时,fd1和fd2对应的文件指针是2个独立的指针。文件指针是包含在动态文件的文件管理表中。
先建立一个文件a.txt,写入abcdefghijklmn
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(void)
{
int fd1 = -1;
int fd2 = -1;
int ret = -1;
char buf[100] = {0};
fd1 = open("a.txt", O_RDWR);
fd2 = open("a.txt", O_RDWR);
if ((fd1 < 0) || (fd2 < 0))
{
printf("error\n");
return -1;
}
while (1)
{
memset(buf, 0, sizeof(buf));
ret = read(fd1, buf, 2);
if (ret < 0)
{
printf("error\n");
return -1;
}
else
{
printf("%s\n", buf);
}
sleep(1); //睡眠一段时间
memset(buf, 0, sizeof(buf));
ret = read(fd2, buf, 2);
if (ret < 0)
{
printf("error\n");
return -1;
}
else
{
printf("%s\n", buf);
}
}
close(fd1);
close(fd2);
return 0;
}
//输出:
//ab
//ab
//cd
//cd
//ef
//ef
//gh
(2)重复打开同一文件写入
默认结果是分别写,fd2的内容会覆盖掉fd1
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(void)
{
int fd1 = -1;
int fd2 = -1;
int ret = -1;
fd1 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
fd2 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
if ((fd1 < 0) || (fd2 < 0))
{
printf("error\n");
return -1;
}
while (1)
{
ret = write(fd1, "ab", 2);
if (ret < 0)
{
printf("error\n");
return -1;
}
sleep(1);
ret = write(fd2, "cd", 2);
if (ret < 0)
{
printf("error\n");
return -1;
}
}
close(fd1);
close(fd2);
return 0;
}
//输出:
//cdcdcdab
若想实现接续读,在open时加O_APPEND标志即可
fd1 = open(“a.txt”, O_RDWR | O_CREAT | O_TRUNC | O_APPEND, 0666);
fd2 = open(“a.txt”, O_RDWR | O_CREAT | O_TRUNC | O_APPEND, 0666);
结果为:
abcdabcdabcdbcdab
(3)O_APPEND为什么能够将分别写改为接续写?
核心的东西是文件指针,分别写的内部原理就是2个fd拥有不同的文件指针,并且彼此只考虑自己的位移。但是O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动。(即使加了O_APPEND,fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个动了会通知另一个跟着动)
(4)O_APPEND对文件指针的影响,对文件的读写是原子的。
原子操作的含义是:整个操作一旦开始是不会被打断的,必须直到操作结束其他代码才能得以调度运行,这就叫原子操作。每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行。
6、文件共享的3种实现方式
(1)文件共享的核心是实现多个文件描述符指向同一个文件。
(2)常见的有3种文件共享的情况:
第一种是同一个进程中多次使用open打开同一个文件
第二种是在不同进程中去分别使用open打开同一个文件(这时候因为两个fd在不同的进程中,所以两个fd的数字可以相同也可以不同)
第三种情况是后面要学的,linux系统提供了dup和dup2两个API来让进程复制文件描述符。
(3)我们分析文件共享时的核心关注点在于:分别写/读还是接续写/读
7、dup和dup2函数介绍
int dup(int oldfd);
int dup2(int oldfd, int newfd);
(1)dup系统调用对fd进行复制,会返回一个新的文件描述符(譬如原来的fd是3,返回的就是4)
(2)dup系统调用特点:不能指定复制后得到的fd的数字是多少,而是由操作系统内部自动分配的,分配原则为现有最小的fd。
(3)dup返回的fd和原来的oldfd都指向oldfd打开的那个动态文件,操作这两个fd实际操作的都是oldfd打开的那个文件。实际上构成了文件共享。
(4)dup和close配合进行文件的重定向
fd1 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC | O_APPEND, 0666);
if ((fd1 > 0))
{
close(1);
fd2 = dup(fd1);
printf("error\n");
return -1;
}
让第三行代码出错,错误信息会重定向到a.txt中。
的实现原理,其实就是利用open+close+dup,open打开一个文件2.txt,然后close关闭stdout,然后dup将1和2.txt文件关联起来即可。
(5)使用dup2复制的新的符号描述符,在对同一文件交叉写入的时候可以实现接续写。
8、fcntl函数的简单用法
int fcntl(int fd, int cmd, … /* arg */ );
不同的cmd使fcntl具有不同的功能,比如F_DUPFD这个cmd的作用就是复制复制文件描述符,作用和dup/dup2相似。和dup2的区别在于,F_DUPFD不能任意分配fd,分配的文件描述符必须大于等于 arg。
9、标准IO库
(1)标准IO是C库函数,而文件IO是linux系统的API。
C语言库函数是由API封装而来的,库函数内部也是通过调用API来完成操作的,但是库函数因为多了一层封装,所以比API要更加好用一些。
API在不同的操作系统之间是不能通用的,但是C库函数在不同操作系统中几乎是一样的。所以C库函数具有可移植性而API不具有可移植性。
从性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高。
(2)常用的标准IO库函数有:fopen、fclose、fwrite、fread、ffulsh、fseek
(3)一个简单的标准IO读写文件实例
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
FILE *fp;
int len;
int buf[100] = {0};
// 打开并创建一个文件
//fp = fopen("1.txt", "w+");
fp = fopen("1.txt", "r+");
if (fp == NULL)
{
perror("fopen");
exit(-1);
}
#if 0
// 写文件
len = fwrite("12345", sizeof(char), 2, fp);
if (0 == len)
{
perror("fwrite");
exit(-1);
}
printf("write the len is %d\n", len);
#endif
#if 1
// 读文件
len = fread(buf, sizeof(char), 8, fp);
if (0 == len)
{
perror("fread");
exit(-1);
}
printf("read the len is %d\n", len);
#endif
// 关闭文件
fclose(fp);
return 0;
}