Linux应用编程和网络编程(2)------- linux中操作与管理文件


一,linux系统如何管理文件

1、硬盘中的静态文件和inode(i节点)
(1)文件平时都在存放在硬盘中的,硬盘中存储的文件以一种固定的形式存放的,我们叫静态文件
(2)一块硬盘中可以分为两大区域:一个是硬盘内容管理表项,另一个是真正存储内容的区域。操作系统访问硬盘时是先去读取硬盘内容管理表,从中找到我们要访问的那个文件的扇区级别的信息,然后再用这个信息去查询真正存储内容的区域,最后得到我们要的文件。
(3)操作系统最初拿到的信息是文件名,最终得到的是文件内容。第一步就是去查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种信息,每一个文件有一个信息列表(我们叫inode,i节点,其实质是一个结构体,这个结构体有很多元素,每个元素记录了这个文件的一些信息,其中就包括文件名、文件在硬盘上对应的扇区号、块号等信息·····)

强调:硬盘管理的时候是以文件为单位的,每个文件一个inode,每个inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。

(4)联系平时实践,大家格式化硬盘(U盘)时发现有:快速格式化和底层格式化。快速格式化非常快,格式化一个32GB的U盘只要1秒钟,普通格式化格式化速度慢。这两个的差异?其实快速格式化就是只删除了U盘中的硬盘内容管理表(其实就是inode),真正存储的内容没有动。这种格式化的内容是有可能被找回的。

2、内存中被打开的文件和vnode(v节点)
(1)一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程。每个进程都有一个数据结构用来记录这个进程的所有信息(叫进程信息表),表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息。文件管理表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode。
(2)一个vnode中就记录了一个被打开的文件的各种信息,而且我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。

3、文件与流的概念
(1)流(stream)对应自然界的水流。文件操作中,文件类似是一个大包裹,里面装了一堆字符,但是文件被读出/写入时都只能一个字符一个字符的进行,而不能一股脑儿的读写,那么一个文件中N多的个字符被挨个一次读出/写入时,这些字符就构成了一个字符流。
(2)流这个概念是动态的,不是静态的。
(3)编程中提到流这个概念,一般都是IO相关的。所以经常叫IO流。文件操作时就构成了一个IO流。


二,lseek函数

1、lseek函数介绍

在这里插入图片描述

(1)文件指针:当我们要对一个文件进行读写时,一定需要先打开这个文件,所以我们读写的所有文件都是动态文件。动态文件在内存中的形态就是文件流的形式。
(2)文件流很长,里面有很多个字节。那我们当前正在操作的是哪个位置?GUI模式下的软件用光标来标识这个当前正在操作的位置,这是给人看的。
(3)在动态文件中,我们会通过文件指针来表征这个正在操作的位置。所谓文件指针,就是我们文件管理表这个结构体里面的一个指针。所以文件指针其实是vnode中的一个元素。这个指针表示当前我们正在操作文件流的哪个位置。这个指针不能被直接访问,linux系统用lseek函数来访问这个文件指针

(4)当我们打开一个空文件时,默认情况下文件指针指向文件流的开始。所以这时候去write时写入就是从文件开头开始的。write和read函数本身自带移动文件指针的功能,所以当我write了n个字节后,文件指针会自动向后移动n位。如果需要人为的随意更改文件指针,那就只能通过lseek函数了。
(5)read和write函数都是从当前文件指针处开始操作的,所以当我们用lseek显式的将文件指针移动后,那么再去read/write时就是从移动过后的位置开始的。
(6)回顾前面一节中我们从空文件,先write写了12字节,然后read时是空的(但是此时我们打开文件后发现12字节确实写进来了)。

2、用lseek计算文件长度
(1)linux中并没有一个函数可以直接返回一个文件的长度。但是我们做项目时经常会需要知道一个文件的长度,怎么办?自己利用lseek来写一个函数得到文件长度即可。

     // 打开一个文件时此时文件指针指向文件开头
	// 我们用lseek将文件指针移动到末尾,然后返回值就是文件指针距离文件开头的偏移量,也就是文件的长度了
	ret = lseek(fd, 0, SEEK_END);
	return ret;

3、用lseek构建空洞文件
(1)空洞文件就是这个文件中有一段是空的。
(2)普通文件中间是不能有空的,因为我们write时文件指针是依次从前到后去移动的,不可能绕过前面直接到后面。
(3)我们打开一个文件后,用lseek往后跳过一段,再write写入一段,就会构成一个空洞文件跳过的那段不占用内存。
(4)空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件,如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每个线程负责其中一段的写入。

ret = lseek(fd, 10, SEEK_SET);

三,多次打开同一文件与O_APPEND

1、重复打开同一文件读取
(1)一个进程中两次打开同一个文件,然后分别读取,看结果会怎么样?

#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};       //用来存放读取文件内容的buf
	char writebuf[20] = "l love linux";   
	int ret = -1;
	
	// 第一步:打开同一个文件,返回两个文件描述符
    fd1 = open("a.txt", O_RDWR | O_TRUNC | O_CREAT , 0666);
	fd2 = open("a.txt", O_RDWR | O_TRUNC | O_CREAT , 0666);
	if ((-1 == fd1) || (fd2 == -1))		
	{
		perror("文件打开错误");
		_exit(-1);
	}
	else
	{
		printf("文件打开成功,fd1 = %d. fd2 = %d.\n", fd1, fd2);
	}
	
#if 0  //先关掉写文件
	while (1)
	{
		// 写文件
		ret = write(fd1, "ab", 2);
		if (ret < 0)
		{
			perror("write失败");
			_exit(-1);
		}
		else
		{
			printf("write成功,写入了%d个字符\n", ret);
		}
		ret = write(fd2, "cd", 2);
		if (ret < 0)
		{
			perror("write失败");
			_exit(-1);
		}
		else
		{
			printf("write成功,写入了%d个字符\n", ret);
		}
		sleep(1);
	}	
#endif

#if 1        //先打开读文件
	while(1)
	{
		// 读文件fd1
		memset(buf, 0, sizeof(buf));      //先清空用来存放读取内容的buf
		ret = read(fd1, buf, 2);         
		if (ret < 0)
		{
			printf("read失败\n");
			_exit(-1);
		}
		else
		{	
			printf("fd1:[%s].\n", buf);
		}
		
		sleep(1);
		
		// 读文件fd2
		memset(buf, 0, sizeof(buf));
		ret = read(fd2, buf, 2);
		if (ret < 0)
		{
			printf("read失败\n");
			_exit(-1);
		}
		else
		{	
			printf("fd2:[%s].\n", buf);
		}
		
	}
	
#endif	
	// 第三步:关闭文件
	close(fd1);
	close(fd2);
	
	_exit(0);
}

(2)结果无非2种情况:一种是fd1和fd2分别读,第二种是接续读。经过实验验证,证明了结果是fd1和fd2分别读

在这里插入图片描述

(3)分别读说明:我们使用open两次打开同一个文件时,fd1和fd2所对应的文件指针是不同的2个独立的指针。文件指针是包含在动态文件的文件管理表中的,所以可以看出linux系统的进程中不同fd对应的是不同的独立的文件管理表

2、重复打开同一文件写入
(1)一个进程中2个打开同一个文件,得到fd1和fd2.然后看是分别写还是接续写?

在这里插入图片描述

(2)正常情况下我们有时候需要分别写,有时候又需要接续写,所以这两种本身是没有好坏之分的。关键看用户需求
(3)默认情况下应该是:分别写(实验验证过的)

3、加O_APPEND解决覆盖问题
(1)有时候我们希望接续写而不是分别写?办法就是在open时加O_APPEND标志即可

在这里插入图片描述

4、O_APPEND的实现原理和其原子操作性说明
(1)O_APPEND为什么能够将分别写改为接续写?关键的核心的东西是文件指针。分别写的内部原理就是2个fd拥有不同的文件指针,并且彼此只考虑自己的位移。但是O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动。(也就是说即使加了O_APPEND,fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个动了会通知另一个跟着动)
(2)O_APPEND对文件指针的影响,对文件的读写是原子的
(3)原子操作的含义是:**整个操作一旦开始是不会被打断的,必须直到操作结束其他代码才能得以调度运行,这就叫原子操作。**每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行。

四,文件共享的实现方式

1、什么是文件共享
(1)文件共享就是同一个文件(同一个文件指的是同一个inode(静态文件),同一个pathname)被多个独立的读写体(几乎可以理解为多个文件描述符)去同时(一个打开尚未关闭的同时另一个去操作)操作。
(2)文件共享的意义有很多:譬如我们可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。

2、文件共享的3种实现方式
(1)文件共享的核心就是怎么弄出来多个文件描述符指向同一个文件
(2)常见的有3种文件共享的情况:第一种是同一个进程中多次使用open打开同一个文件第二种在不同进程中去分别使用open打开同一个文件(这时候因为两个fd在不同的进程中,所以两个fd的数字可以相同也可以不同),第三种情况是后面要学的,linux系统提供了dup和dup2两个API来让进程复制文件描述符。
(3)我们分析文件共享时的核心关注点在于:分别写/读还是接续写/读

在这里插入图片描述

3、再论文件描述符

  • (1)文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项进程通过文件描述符作为index去索引查表得到文件表指针再间接访问得到这个文件对应的文件表
  • (2)文件描述符这个数字是open系统调用内部由操作系统自动分配的,操作系统分配这个fd时也不是随意分配,也是遵照一定的规律的,我们现在就要研究这个规律。
  • (3)操作系统规定,fd从0开始依次增加。fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,所以当时一个进程最多允许打开20个文件。linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是index,文件表指针是value。
  • (4)当我们去open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回。也就是说如果之前fd已经占满了0-9,那么我们下次open得到的一定是10.(但是如果上一个fd得到的是9,下一个不一定是10,这是因为可能前面更小的一个fd已经被close释放掉了
  • (5)fd中0、1、2已经默认被系统占用了,因此用户进程得到的最小的fd就是3了。
  • (6)linux内核占用了0、1、2这三个fd是有用的,当我们运行一个程序得到一个进程时,内部就默认已经打开了3个文件,这三个文件对应的fd就是0、1、2。这三个文件分别叫stdin、stdout、stderr。也就是标准输入标准输出标准错误
  • (7)标准输入一般对应的是键盘(可以理解为:0这个fd对应的是键盘的设备文件),标准输出一般是LCD显示器(可以理解为:1对应LCD的设备文件
  • (8)printf函数其实就是默认输出到标准输出stdout上了。stdio中还有一个函数叫fpirntf,这个函数就可以指定输出到哪个文件描述符中。

五,文件描述符的复制

1、dup和dup2函数介绍

在这里插入图片描述

2、使用dup进行文件描述符复制
(1)dup系统调用对fd进行复制,会返回一个新的文件描述符(譬如原来的fd是3,返回的就是4)
(2)dup系统调用有一个特点,就是自己不能指定复制后得到的fd的数字是多少,而是由操作系统内部自动分配的,分配的原则遵守fd分配的原则。
(3)dup返回的fd和原来的oldfd都指向oldfd打开的那个动态文件,操作这两个fd实际操作的都是oldfd打开的那个文件。实际上构成了文件共享。

3、练习
(1)之前讲过0、1、2这三个fd被标准输入、输出、错误通道占用。而且我们可以关闭这三个。
(2)我们可以close(1)关闭标准输出,关闭后我们printf输出到标准输出的内容就看不到了。
(3)然后我们可以使用dup重新分配得到1这个fd,这时候就把oldfd打开的这个文件和我们1这个标准输出通道给绑定起来了。这就叫标准输出的重定位。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FILENAME	"1.txt"

int main(void)
{
	int fd1 = -1, fd2 = -1;
	
	fd1 = open(FILENAME, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd1 < 0)
	{
		perror("open");
		return -1;
	}
	printf("fd1 = %d.\n", fd1);
	
	close(1);		// 1就是标准输出stdout
	
	// 复制文件描述符
	fd2 = dup(fd1);		// fd2一定等于1,因为前面刚刚关闭了1,这句话就把
	if (fd2 < 0)        // 1.txt文件和标准输出就绑定起来了,所以以后输出
	{                   // 到标准输出的信息就可以到1.txt中查看了。
		perror("dup");
		return -1;
	}                   
	                   				
	printf("fd2 = %d.\n", fd2);
	printf("this is for test\n");
	
	close(fd1);
	return -1;
}

在这里插入图片描述

4、使用dup2进行文件描述符复制

(1)dup并不能指定分配的新的文件描述符的数字,dup2系统调用修复了这个缺陷,所以平时项目中实际使用时根据具体情况来决定用dup还是dup2.
(2)dup2和dup的作用是一样的,都是复制一个新的文件描述符。但是dup2允许用户指定新的文件描述符的数字

5、dup2共享文件交叉写入测试
(1)dup2复制的文件描述符,和原来的文件描述符虽然数字不一样,但是这连个指向同一个打开的文件.
(2)交叉写入的时候,结果是接续写(实验证明的)。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FILENAME	"1.txt"

int main(void)
{
	int fd1 = -1, fd2 = -1;
	
	fd1 = open(FILENAME, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd1 < 0)
	{
		perror("open");
		return -1;
	}
	printf("fd1 = %d.\n", fd1);
	
	
	fd2 = dup2(fd1, 16);
	printf("fd2 = %d.\n", fd2);

	while (1)
	{
		write(fd1, "aa", 2);
		sleep(1);
		write(fd2, "bb", 2);
	}

	close(fd1);
	return -1;
}

在这里插入图片描述
6、命令行中重定位命令 >
(1)linux中的shell命令执行后,打印结果都是默认进入stdout的(本质上是因为这些命令譬如ls、pwd等都是调用printf进行打印的),所以我们可以在linux的终端shell中直接看到命令执行的结果。
(2)能否想办法把ls、pwd等命令的输出给重定位到一个文件中(譬如2.txt)去,实际上linux终端支持一个重定位的符号>很简单可以做到这点。

在这里插入图片描述
(3)这个>的实现原理,其实就是利用open+close(1)+dupopen打开一个文件2.txt,然后close关闭stdout,然后dup将1和2.txt文件关联起来即可。

六,fcntl函数介绍

1、fcntl的原型和作用
(1)fcntl函数是一个多功能文件管理的工具箱,接收2个参数+1个变参。第一个参数是fd表示要操作哪个文件,第二个参数是cmd表示要进行哪个命令操作。变参是用来传递参数的,要配合cmd来使用。
(2)cmd的样子类似于F_XXX,不同的cmd具有不同的功能。学习时没必要去把所有的cmd的含义都弄清楚(也记不住),只需要弄明白一个作为案例,搞清楚它怎么看怎么用就行了,其他的是类似的。其他的当我们在使用中碰到了一个fcntl的不认识的cmd时再去查man手册即可。

2、fcntl的常用cmd
(1)F_DUPFD这个cmd的作用是复制文件描述符(作用类似于dup和dup2),这个命令的功能是从可用的fd数字列表中找一个比arg大或者和arg一样大的数字作为oldfd的一个复制的fd,和dup2有点像但是不同。dup2返回的就是我们指定的那个newfd否则就会出错,但是F_DUPFD命令返回的是>=arg的最小的那一个数字。

在这里插入图片描述

3、使用fcntl模拟dup2

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FILENAME	"1.txt"

int main(void)
{
	int fd1 = -1, fd2 = -1;
	
	fd1 = open(FILENAME, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd1 < 0)
	{
		perror("open");
		return -1;
	}
	printf("fd1 = %d.\n", fd1);

	fd2 = fcntl(fd1, F_DUPFD, 6);
	printf("fd2 = %d.\n", fd2);

	while (1)
	{
		write(fd1, "aa", 2);
		sleep(1);
		write(fd2, "bb", 2);
	}

	close(fd1);
	return -1;
}

在这里插入图片描述

七,标准IO库介绍

1、标准IO和文件IO有什么区别
(1)看起来使用时都是函数,但是:标准IO是C库函数,而文件IO是linux系统的API
(2)C语言库函数是由API封装而来的。库函数内部也是通过调用API来完成操作的,但是库函数因为多了一层封装,所以比API要更加好用一些。
(3)库函数比API还有一个优势就是:API在不同的操作系统之间是不能通用的,但是C库函数在不同操作系统中几乎是一样的。所以C库函数具有可移植性而API不具有可移植性
(4)性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高。

2、常用标准IO函数介绍
(1)常见的标准IO库函数有:fopen、fclose、fwrite、fread、ffulsh、fseek
(2)使用man 手册查询标准IO库函数时用命令man 3 c库函数,如man 3 fopen。
3、一个简单的标准IO读写文件实例

(1)读文件代码

#include <stdio.h>		// standard input output
#include <stdlib.h>
#include <string.h>


#define FILENAME	"1.txt"

int main(void)
{
	FILE *fp = NULL;
	size_t len = -1;
	char buf[100] = {0};        //用来储存读取文件的内容
	
	fp = fopen(FILENAME, "r+");
	if (NULL == fp)
	{
		perror("fopen");
		exit(-1);
	}
	printf("fopen success. fp = %p.\n", fp);
	
	// 在这里去读文件
	memset(buf, 0, sizeof(buf));
	len = fread(buf, 1, 10, fp);
	printf("len = %d.\n", len);
	printf("buf is: [%s].\n", buf);
	
	fclose(fp);
	return 0;
}

在这里插入图片描述
(2)写文件代码

#include <stdio.h>		// standard input output
#include <stdlib.h>
#include <string.h>


#define FILENAME	"1.txt"

int main(void)
{
	FILE *fp = NULL;
	size_t len = -1;
	int array[10] = {1, 2, 3, 4, 5};
fp = fopen(FILENAME, "w+");
	if (NULL == fp)
	{
		perror("fopen");
		exit(-1);
	}
	printf("fopen success. fp = %p.\n", fp);
	
	// 在这里去写文件
	len = fwrite(array, 4, 10, fp);
	printf("len = %d.\n", len);
		
	fclose(fp);
	return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值