Linux —— 文件操作
创始人
2024-04-14 19:58:59

目录

1.内核提供的文件系统调用

1.1open和close

1.2write和read

2.文件描述

2.1文件描述符

 2.2文件描述符分配规则

3.重定向

3.1最“挫”的重定向

3.2使用系统调用

3.3重定向原理

3.4让我们的"shell"支持重定向操作

4.一切皆文件

1.内核提供的文件系统调用

1.1open和close

通过[man]指令浏览其描述,这里截取片段。 

第一个open是文件存在的情况下打开文件,第一个参数为文件名,若不指定文件路径,则默认为父进程的工作路径。第二个参数为标记位,int类型的每个比特位的0 和 1代表了不同的标记。Linux提供了多种标记。

第二个open是文件不存在的情况下打开文件,第三个参数为文件创建的初始权限。

close即关闭文件。

 下面给出代码实例以供参考:

umask(0);       //将umask置0
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);     //相当于C语言的fopen的"w",只写、自动创建、自动覆盖
//文件的权限受umask的影响
close(fd);

下面列举一些常用的标记:

  • O_WRONLY        ->只写
  • O_CREAT           ->创建
  • O_TRUNC          ->覆盖
  • O_RDONLY        ->只读
  • O_APPEND        ->追加

需要多个标记组合在一起时,使用 '|'(按位或运算符) 连接即可。

1.2标记位

int类型有4个字节,32个比特位,每一个比特位的0 和 1代表了不同的标记。例如有:......0001与......0010就是两个不同的标记。我们使用代码实例来演示:

#include 
#include 
#include 
#include 
#include 
#include #define ONE (1<<0)
#define TOW (1<<1)      
#define THREE (1<<2)        
#define FOUR (1<<3)void select(int flags)
{if(flags & ONE) printf("one\n");if(flags & TOW) printf("tow\n");if(flags & THREE) printf("three\n");if(flags & FOUR) printf("four\n");
}int main()
{select(ONE);        select(ONE | TOW);select(ONE | TOW | THREE);select(ONE | TOW | THREE | FOUR);return 0;
}

 也就是说,Linux提供的标记实质上是一个宏,每一个宏代表了不同的信号。

1.3write和read

write是一个系统调用,其声明为([man]指令查看):

需要注意的是,write()是从文件的起始位置开始写的,如果在open()中没有O_TRUNC或者O_APPEND标记,那么为将文件以前的内容的一部分覆盖掉。也就是说,要想像C语言一样自动清空文件的内的数据必须加上O_TRUNC标记 。

下面给出write的实际使用案例以供参考:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{umask(0);       //将umask置0int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);     //相当于C语言的fopen的"w",只写、自动创建、自动覆盖if(fd < 0) return 1;char* msg = "hello Linux\n";write(fd,msg,strlen(msg));      //像fd描述的文件写入msg指向的字符串close(fd);return 0;
}

编译运行,查看生成的"log.txt"文件:

read也是一个系统调用,其声明为([man]指令查看):

这里以上面write生成的文件给出read的实际使用案例以供参考:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{int fd = open("log.txt",O_RDONLY);char buffer[64];read(fd,buffer,sizeof(buffer)-1);       //留一个位置补'\0'buffer[strlen(buffer)]=0;       //文件的字符串不以'\0'结尾printf("%s",buffer);close(fd);return 0;
}

2.文件描述

2.1文件描述符

进程可以打开多个文件,所以操作系统会有大量的文件。操作系统为了管理被打开的文件,使用了"先描述,再组织"的方法将被打开的文件描述为一个struct file的内核结构体,其包含了文件的大部分属性,多个内核结构体之前以特定的数据结构组织起来。

这些struct file结构体并不是文件描述符,而是操作系统为了管理被打开的文件而创建的。事实上,文件操作研究的是进程和被打开文件的关系,也就是说文件是被进程打开的。那么进程和struct file结构体中间还有一层结构体,名为struct files_struct(文件描述符表),在task_struct中有一个struct files_struct* files指针指向文件描述符表。文件描述表有一个专门用来存储struct file结构体的地址的指针数组(struct file* fd_array[]),这个数组的下标即为文件描述符

在进程(C语言程序)被加载到内存时,会默认生成打开三个文件(这些文件是C语言使用的,系统是是使用文件描述符的),即:

  • stdin    ->标准输入
  • stdout    ->标准输出
  • stderr    ->标准错误

这三个文件的struct file结构体的地址依次按顺序存储在struct file* fd_array数组对应的下标0、1、2位置。所以我们用户的进程第一次创建的文件的描述符为3。

下面给出进程与文件的假象模型图:

 2.2文件描述符分配规则

 在struct file* fd_array[]数组中,按下标从小到大的顺序,寻找最小、且没有被占用下标作为文件描述符。

下面给出一段代码供加深理解:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{close(0);close(2);       //关闭掉标准输入和标准错误文件,即清空数组的占用int fd = open("log.txt",O_RDONLY);printf("%d\n",fd);close(fd);return 0;
}

3.重定向

3.1最“挫”的重定向

观察下面这段代码以及现象:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{close(1);       //将fd=1的数组位置清空int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);     //文件打开时就占用fd=1的位置printf("hello world\n");fflush(stdout);close(fd);return 0;
}

以上是最原始的重定向操作。其原因在于:printf()函数是默认向fd=1对应的文件(标准输出)输出的,但是进行close(1)操作后,fd=1位置的内容就清空了,随后log.txt文件的struct file结构体地址占用了fd=1的位置,所以printf就向log.txt文件输出了。 

3.2使用系统调用

dup类接口是系统提供给我们的接口,其中dup2最为常用。我们通过[man]指令查询:

下面给出实际使用案例以供参考:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);dup2(fd,1);     //重定向printf("hello world");fflush(stdout);close(fd);return 0;
}

3.3重定向原理

上层看到的文件描述符是不会变的,例如printf规定了向标准输出输出,即向fd=1对应的文件输出,那么printf找的是文件描述符而不是对应的文件,所以fd的内容无论怎么变,上层找的还是fd。

子进程重定向不会影响父进程,因为进程之间相互独立。即子在进程在创建出来的时候,就拷贝了一份文件描述符表。但是文件不属于进程,是不会拷贝的。

3.4让我们的"shell"支持重定向操作

在上次模拟实现命令行解释器的基础上,再进行升级,以支持重定向操作。

#include 
#include 
#include 
#include 
#include       
#include 
#include 
#include #include 
#include #define NUM 1024
#define NON_REDIR 0     //无重定向
#define INPUT_REDIR 1       //输入重定向 '<'
#define OUT_REDIR 2        //输出重定向 '>'
#define APPEND_REDIR 3      //追加重定向 '>>'char command[NUM];      //c99数组
char* myargv[64];       //存储指令参数
char* file_name=NULL;
int redir_type=0;void command_check(char* command)
{assert(command);//首、尾指针char* begin = command;char* end = command+strlen(command);while(begin < end){if(*begin == '>'){*begin=0;begin++;if(*begin == '>'){redir_type=APPEND_REDIR;begin++;}else redir_type=OUT_REDIR;while(*begin == ' ') begin++;file_name=begin;}else if(*begin == '<'){*begin=0;       //置0redir_type=INPUT_REDIR;begin++;while(*begin == ' ') begin++;file_name = begin;}else ++begin;}}
int main()
{while(1){redir_type=NON_REDIR;file_name=NULL;char buffer[1024]={0};getcwd(buffer,sizeof(buffer)-1);        //获取shell的工作路径buffer[strlen(buffer)]=0;printf("[用户名@主机名 %s]",buffer);fflush(stdout);     //刷新缓冲区char* s = fgets(command,sizeof(command),stdin);     //输入指令command[strlen(command)-1]=0;       //清除 \n command_check(command);     //检查指令是否有重定向操作myargv[0] = strtok(command," ");int i = 1;while(myargv[i++] = strtok(NULL," "));      //切割空格if(myargv[0] != NULL &&  strcmp(myargv[0],"cd") == 0){if(myargv[1] != NULL) chdir(myargv[1]);      //cd命令移动shell的工作路径continue;}pid_t id = fork();if(id == 0){switch(redir_type)      //使用switch语句{case NON_REDIR:     //不作处理break;case INPUT_REDIR:       //输入重定向{int fd = open(file_name,O_RDONLY);if(fd < 0){perror("open:");exit(1);}dup2(fd,0);}break;case OUT_REDIR:case APPEND_REDIR:{umask(0);int flag = O_WRONLY | O_CREAT;if(redir_type == APPEND_REDIR) flag |= O_APPEND;else flag |= O_TRUNC;int fd = open(file_name,flag,0666);if(fd < 0){perror("open:");exit(1);}dup2(fd,1);}break;default:break;}execvp(myargv[0],myargv);        //进程替换exit(1);}waitpid(id,NULL,0);}return 0;
}

文件描述符表是属于进程的并且是一个内核数据结构,进程替换是不会影响它的,进程替换只是替换数据段和代码段,是不影响内核数据结构的。

对于文件的关闭,在进程退出时自动关闭。其本质还是在于struct file里面的计数器,这个计数器记录有多少个进程正在引用这个struct file,当进程退出时,计数器就会减1,当计数器为0才文件才销毁。

4.一切皆文件

不止是磁盘上的可执行文件被打开才是文件,硬件也是文件。键盘、鼠标、显示器、内存、硬盘等等都是文件。

操作系统能够使用驱动来管理硬件,那么在驱动上,就一定有硬件与操作系统的IO交互方法。那么在内核中,都有唯一的一份struct file内核结构体对应硬件,以便操作系统管理。所以在Linux的视角来看,一切皆文件。

那么在上层想要与硬件互动时,也是通过struct file结构体实现的,其原因在于此结构体有硬件与操作系统IO交互方法的函数指针,通过这些函数指针去调用不同的交互方式。

发现了吗?即使磁盘上的各种可执行文件或者是硬件非常杂乱,但是在操作系统下总是能有序的抽象化成一个struct file结构体来进行管理。也就是说,我们通过统一的struct file结构体(其中描述了文件的共有属性)来操作不同的文件。用官方的话说(参考Linux内核设计与实现原理):我们可以直接使用open()、read()和write()这样的系统调用而无需考虑具体文件系统和实际物理介质。这样的行为就构成了内核的子系统——虚拟文件系统(VFS)。

相关内容

热门资讯

猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...