目录
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.2write和read
write是一个系统调用,其声明为([man]指令查看):
需要注意的是,write()是从文件的起始位置开始写的,如果在open()中没有O_TRUNC或者O_APPEND标记,那么为将文件以前的内容的一部分覆盖掉。也就是说,要想像C语言一样自动清空文件的内的数据必须加上O_TRUNC标记 。
下面给出write的实际使用案例以供参考:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0); //将umask置0
int 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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
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[]),这个数组的下标即为文件描述符。
在操作系统启动时,会默认生成打开三个文件,即:
- stdin ->标准输入
- stdout ->标准输出
- strerr ->标准输入
这三个文件的struct file结构体的地址依次按顺序存储在struct file* fd_array数组对应的下标0、1、2位置。所以我们用户的进程第一次创建的文件的描述符为3。
下面给出进程与文件的假象模型图:
2.2文件描述符分配规则
在struct file* fd_array[]数组中,按下标从小到大的顺序,寻找最小、且没有被占用下标作为文件描述符。
下面给出一段代码供加深理解:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
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)操作后,log.txt文件的struct file结构体地址就占用了fd=1的位置,所以printf就向log.txt文件输出了。
3.2使用系统调用
dup类接口是系统提供给我们的接口,其中dup2最为常用。我们通过[man]指令查询:
下面给出实际使用案例以供参考:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <assert.h>
#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; //置0
redir_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)。