Linux:文件管理(一)

目录

一、文件基础认识

二、C语言操作文件的接口

[1.> 和 >>](#1.> 和 >>)

2.理解"当前路径"

三、相关系统调用

1.open

2.文件描述符

3.一切皆文件

4.再次理解重定向


一、文件基础认识

  • 文件 = 内容 + 属性。换句话说,如果在电脑上新建了一个空白文档,它虽然没有内容,但也是占据磁盘空间的。
  • 想要修改一个文件的内容,比如用WPS这样的软件操作文件内容,本质上都需要CPU完成相关的指令,而CPU又只与内存交互,所以,打开文件的含义其实就是把文件加载到内存中。
  • 在我们眼里,我们双击了一个文件就是打开了文件,但是在操作系统看来,并不是我们打开了文件,而是某一个正在运行的进程,文件是由进程打开的
  • 一个进程可以打开多个文件。
  • 操作系统管理多个被打开文件,必然也会像操作系统管理多个进程一样,利用面向对象和数据结构,因此,内核中必然定义了结构体来描述被打开的文件。
  • 从操作系统管理文件的角度看,文件被区分为被打开的文件(在内存中)和没有打开的文件(在磁盘中)。

二、C语言操作文件的接口

fopen 以**"w"**方法打开一个文件。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	FILE* pf = fopen("aaa.txt","w");
	if(pf == NULL)
	{
		perror("fopen:");
		return 1;
	}
	const char* str = "aaaaaaaaaaaaaaaaaaaaaa\n";
	fputs(str,pf);
	fclose(pf);
	return 0;
}
bash 复制代码
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ ./a.out 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt 
aaaaaaaaaaaaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ 

结果显示,文件aaa.txt中已经写入了一段字符串。修改源代码,将写入字符串的代码删除后,再执行编译运行一次。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	FILE* pf = fopen("aaa.txt","w");
	if(pf == NULL)
	{
		perror("fopen:");
		return 1;
	}
//	const char* str = "aaaaaaaaaaaaaaaaaaaaaa\n";
//	fputs(str,pf);
	fclose(pf);
	return 0;
}
bash 复制代码
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt 
aaaaaaaaaaaaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ gcc file.c 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ ./a.out 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ 

结果表明,aaa.txt 文件中的内容都消失了。原因在于fopen打开文件的方式"w" ,使用man 手册查看fopen打开文件方式的说明。

"w"方式打开文件时,会先清空文件中的所有内容。如果想保留文件中原来的内容做写入操作,就应该使用"a"的方式打开文件。


1.> 和 >>

bash 复制代码
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo aaaaaaaaaaaa > aaa.txt 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
aaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo bbbbbbbbb > aaa.txt 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
bbbbbbbbb
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ 

通过echo 做重定向操作向aaa.txt 文件中先后写入两次,最终效果并不是有两段字符串,说明重定向操作符**">"** 打开文件的方式本质上也是**"w"** 的方式。(需要一提的是,echo重定向到文件中,本质上也要修改文件的内容,所以一定会打开文件)。


bash 复制代码
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo aaaaaaaaaaaaaaaa >> aaa.txt 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt 
aaaaaaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo bbbbbbbbbbbbbbbb >> aaa.txt 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt 
aaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbb
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ 

而追加重定向操作符**" >> "** 先后向aaa.txt 文件写入两次后,最终效果是两段字符串都被保留了下来,说明**" >> "** 其实和**"a"**方式类似,是一种追加的形式。


2.理解"当前路径"

在使用C接口操作文件的时候,经常会听到说,"如果没有这个文件,则在当前路径下新建这个文件",如何理解这个**"当前路径"**?

最简单直接的理解,就是我们当前程序的路径

cpp 复制代码
//file.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
	FILE* pf = fopen("aaa.txt","w");
	if(pf == NULL)
	{
		perror("fopen:");
		return 1;
	}

	fclose(pf);
	return 0;
}

当前路径就是file.c 文件所在路径,编译运行前,该路径下没有aaa.txt 文件,编译运行后,该路径下存在名为aaa.txt的文件。

bash 复制代码
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ ll
总计 16
drwxrwxr-x  2 utocoo utocoo 4096 11月 22 12:22 ./
drwxrwxr-x 16 utocoo utocoo 4096 11月 22 12:19 ../
-rw-rw-r--  1 utocoo utocoo  233 11月 22 12:19 file.c
-rw-rw-r--  1 utocoo utocoo   64 11月 22 12:21 Makefile
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ make
gcc -o file file.c
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ ./file 
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ ll
总计 32
drwxrwxr-x  2 utocoo utocoo  4096 11月 22 12:23 ./
drwxrwxr-x 16 utocoo utocoo  4096 11月 22 12:19 ../
-rw-rw-r--  1 utocoo utocoo     0 11月 22 12:23 aaa.txt
-rwxrwxr-x  1 utocoo utocoo 16048 11月 22 12:23 file*
-rw-rw-r--  1 utocoo utocoo   233 11月 22 12:19 file.c
-rw-rw-r--  1 utocoo utocoo    64 11月 22 12:21 Makefile

在文件基础认识部分,已经提到过,文件是由进程打开的,那么新建一个文件也是由进程完成,进程是如何知道在哪条路径下新建一个文件呢。

在源代码中打印出进程的PID,运行后,再在**/proc**路径下找到对应进程的所在目录。

cpp 复制代码
while(1)
{
	printf("PID:%d\n",getpid());
	sleep(2);
}
bash 复制代码
PID:2930
PID:2930
PID:2930
PID:2930
PID:2930
PID:2930
PID:2930

当前路径在进程的属性中其实已经保存好了,是cwd这条信息。因此新建一个文件要被存放到哪里也是确定的。但是进程的工作路径是可以修改的,虽然进程的前身是一个可执行程序,可执行程序的路径是确定,但是当可执行程序被操作系统管理起来后变成进程,进程的工作路径是可以通过chdir指令修改的,那么修改路径后,再新建一个文件,这个文件的所在路径不再是修改前的路径了,而是修改后的路径。

这就表明,所谓的当前路径,其实是进程在运行的时候的工作路径,这个路径是由进程自己记录的,就是那条cwd信息。

三、相关系统调用

系统默认打开三个流,stdin,stdout,stderr,这三个流对应的外设分别为键盘、显示器显示器。而Linux管理外设,是以文件的方式,即必然存在系统调用system call。因此,C语言的fopen、fclose、fwrite等函数本质是调用了system call

下面就来认识Linux下文件相关的system call。

1.open

  • pathname就是路径,传参方法和C语言的fopen的参数差不多。
  • flags类型为int,传参的可选项如下所示

这些值都是C语言定义的宏,目的是为了实现,**只定义一个函数,却可以同时"传两个参数"。**比如

cpp 复制代码
#include <stdio.h>
#define ONE 1
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)

void Print(int flags)
{
	if(flags & ONE)printf("1\n");
	if(flags & TWO)printf("2\n");
	if(flags & THREE)printf("3\n");
	if(flags & FOUR)printf("4\n");
	if(flags & FIVE)printf("5\n");
}
int main()
{
	Print(ONE);
	printf("-----------------\n");
	Print(TWO);
	printf("-----------------\n");
	Print(ONE|TWO);
	printf("-----------------\n");
	Print(ONE|FOUR|FIVE);
	return 0;
}

如果使用两个形参的open接口,一般是操作已经存在了的文件,比如bbb.txt文件必须存在,否则会报错。

cpp 复制代码
int main()
{
	int fd = open("bbb.txt",O_WRONLY);
	if(fd == -1)
	{
		perror("open\n");
		return 1;
	}

	close(fd);
	return 0;
}

由于bbb.txt不存在,则fd=-1


用open接口实现fopen的"w"方式,文件如果不存在,则新建。而新建一个文件会有权限的初始化,一般普通用户新建一个文件的权限是0666(-rw-rw-rw-),而普通用户的权限掩码umask为0002,实际权限等于初始化权限减去权限掩码,即(-rw-rw-r--)

mode即初始化权限码,一般传0666,只有flags带O_CREAT时,mode传参才有效。

一般新建一个文件,在open的第二个参数上,应该传新建、可写、写入时清零,等同于fopen的"w"方式。

cpp 复制代码
int main()
{
	int fd = open("bbb.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
	if(fd == -1)
	{
		perror("open\n");
		return 1;
	}
	const char* msg = "this is open to w\n";
	write(fd,msg,strlen(msg));
	close(fd);
	return 0;
}

原来不存在的文件bbb.txt被创建了出来,并且o的权限少了w,符合预期。


2.文件描述符

再来理解open的返回值------文件描述符(int fd)------Linux用整型值描述被打开的文件。

这些整型值其实是数组下标,我们知道系统默认打开三个流,其实是三个文件,stdin、stdout、stderr,它们的下标对应为0、1、2,如果先后有序的打开1.txt、2.txt、3.txt,则它们的下标也是有序的为3、4、5。

这段话似乎让你很懵,不过我马上就要阐述具体的内容。

在此之前,要明确,操作文件只能由操作系统来做,因此有C语言的fopen封装open接口,有C语言定义的FILE指针的流封装文件描述符fd。

实际上,FILE类型是结构体类型,也是封装了文件描述符int fd。


对int fd的理解。

文件描述符的本质,就是数组下标。

  • OS管理进程,这一板块叫做进程管理,有PCB,Linux下被定义为task_struct。
  • OS管理文件,这一板块叫做文件管理,在之前介绍了,文件区分为内存中的文件和磁盘中的文件,被加载到内存中的文件,OS要对它们做管理,就必然做"面向对象"和"数据结构"的工作,"面向对象"就是定义结构体,"数据结构"就是把对象存储到链表或者其他数据结构里面。Linux下把这个结构体类型定义为file,结构体内容大致有属性、方法集、缓冲区、mode(权限码)、flag、pos以及指向下一个结点的next等。
  • 进程管理和文件管理是两个独立的板块,但是又有关联。进程可以打开多个文件,那么一个进程打开了哪些文件,该进程必然要做记录。于是Linux下,task_struct结构体中有一个结构体指针,指向的结构体类型为files_struct,而这个结构体中,有一个数组,数组的每个元素类型为结构体指针,指针指向的结构体类型为file,这个数组被称为文件描述符表

一个进程打开文件后,进程在这个数组中保存指向这个文件的指针,默认这个数组的前三个位置已经被stdin、stdout、stderr这三个文件占用了。

而数组下标,就是文件描述符,为什么close、write等这些接口都用int类型的文件描述符来操作文件,原因很简单,数组下标式访问,仅仅是O(1)复杂度

3.一切皆文件

硬件一层,由于各种原因,设备的操作方法各不相同,因此每台计算机都需要装载相应的驱动。而对于每台设备的操作函数,它们的函数类型相同,函数内容各不相同。

file结构体定义了方法集,本质就是函数指针

  • 每一台设备被视为一个结构体,方法集指向了该设备的操作方法。
  • 当系统调用read读取某个外设的内容,实际上就是函数回调的形式,用函数指针调用外设的读函数。

4.再次理解重定向

文件描述符的分配规则:一定会把最小的数组下标利用起来,如果存在没有被利用的较小下标,则会分配给最新打开的文件,比如打开b文件前,将已经打开的a文件关闭,则打开b文件后,a文件的较小fd会分配给b文件。

上面这段话,其实就是重定向的实现原理。


输出重定向:本该输出到屏幕的语句却输出到了bbb.txt。

cpp 复制代码
int main()
{
	close(1);
	int fd = open("bbb.txt",O_WRONLY);
	printf("这段话本该输出到屏幕\n");
	return 0;
}

原因就是在执行完close(1)语句后,当前进程的文件描述符表中数组下标为1的位置不再是指向屏幕文件的指针,而又打开了bbb.txt文件,则1号下标的指针指向了bbb.txt文件printf 底层封装的write 传参的fd值还是1,因此,这句字符串被写进了1位置指向的bbb.txt文件的缓冲区。

所以,重定向的本质,就是文件指针在文件描述表中的下标发生了变化


有一个专门用来拷贝文件描述符的系统调用------dup

想把打印到屏幕的内容重定向到bbb.txt,可以用dup2来实现。

大致意思是用oldfd的值覆盖到newfd

cpp 复制代码
int main()
{
	int fd = open("bbb.txt",O_WRONLY);
	dup2(fd,1);
	printf("----\n");
	return 0;
}
相关推荐
A小辣椒5 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒9 小时前
TShark:基础知识
linux
AlfredZhao11 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式