目录
一、回顾C文件接口
1.1 C语言如何写文件?
1.2 C语言如何读文件?
1.3 C语言打开文件的方式
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针。
二、系统文件IO
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:
2.1 接口介绍
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
mode_t理解:直接 man 手册查看,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
2.2 open的返回值
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数。
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
通过以上这张图,系统调用和库函数之间的关系就一目了然了,
可以认为,所有的f*系列的函数都是对系统调用接口进行了封装,方便二次开发,因为系统调用接口用起来的难度很大,所以库函数做了封装使用起来就变得更简单一些。
2.3 文件描述符(重中之重)
2.3.1 Linux进程默认打开的输入输出流(012)
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器。
所以输入输出还可以采用如下方式:
文件描述符的含义: 我们现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了struct file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。所以每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件,进行访问了。
2.3.2 文件描述符的分配规则
在files_struct结构体中维护的struct file* fd_array数组当中,找到当前没有被使用的最小的一个下标,作为新打开文件的文件描述符。
2.4 重定向(重中之重)
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那么重定向的本质是什么???
2.4.1 使用 dup2 系统调用
使用场景:
printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向是如何完成呢?追加重定向的原理和输出重定向的原理是一样的,仅仅是open打开文件时的方式不同,追加重定向是不清空文件,输出重定向是先清空文件再输出。
追加重定向:
追加重定向和输出重定向的代码在其它的地方没有一点区别。
输入重定向:
2.5 FILE
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write(系统调用) 只输出了一次。为什么呢?肯定和fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf 和 fwrite 等库函数会自带缓冲区(之前的进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。所以我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后,但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,即子进程也会刷新缓冲区,所以printf和fwite会产生两份数据。write 没有变化,说明系统调用接口没有所谓的缓冲区。
综上: printf fwrite 等库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们这的讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C库函数,所以该缓冲区由C标准库提供。
在/usr/include/libio.h中有FILE结构体的定义:
三、Linux下的ext2文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。例如学校管理学院一样。
1、超级块(Super Block):存放文件系统本身的结构信息,是描述整个分区的所有基本信息的。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了,但是一般在多个Block Group中都保存着Super Block,所以某个Super Block信息被破环,整个文件系统也不会立即被破坏了。
2、GDT,Group Descriptor Table:块组描述符,描述块组属性信息,例如该块组中有多少个inode被使用了,剩余多少个,有多少个data block被使用了,剩余多少个,inode编号是从多少开始的,该分组一共多大等关于该块组的所有基本信息。
3、块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
4、inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
5、inode table:存放文件属性的结构体数组,数组中的每一个元素是一个inode结构体,结构体存文件的信息, 如 文件大小,所有者,最近修改时间等。
6、Data blocks :存放着以块为单位的内存块,用于保存文件内容,一个块等于8个扇区。
四、软硬链接
4.1 硬链接
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。
test.txt和hard_link的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 657073的硬连接数为2。
我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则在对应的磁盘释放。
硬链接不是一个独立的文件,因为它没有独立的inode,建立硬链接只是在该目录下增加了一个文件名与inode的映射关系而已。所以硬链接数起始就是指有多少个文件名指向同一个inode的意思。
4.2 软链接
软链接是一个独立的文件,因为它有独立的inode,软链接文件中的内容是指向的文件的路径,在这里可以认为soft_link中保存的内容是file.txt的路径信息。
五、shell命令行解释器进化版(添加重定向)
5.1 makefile
5.2 myshell.cc
#include <iostream>
using namespace std;
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
int quit=0;
//最近一次进程的退出码
int lastcode=0;
//命令行参数的最大长度
#define LINE_SIZE 1024
//命令行字符数组的长度
#define ARGC_SIZE 32
//输入的命令行
char CommandLine[LINE_SIZE];
//当前路径
char pwd[LINE_SIZE];
//字符串指针数组
char* argv[ARGC_SIZE];
//命令的分隔符
#define SPLIT " "
//自定义环境变量表
char myenv[LINE_SIZE];
#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2
char* rdirfilename=NULL;
int rdir=NONE;
//把整体输入的命令分离成一个一个的命令
int splitstring(char* cline)
{
int i=0;
argv[i]=strtok(cline,SPLIT);
if(argv[i]==NULL)
{
return 0;
}
i++;
//最后一次会把NULL赋值给argv[i]
while(argv[i++]=strtok(NULL,SPLIT));
//for(argv[i]=strtok(cline,SPLIT);strtok(NULL,SPLIT);i++)
//{}
return i-1;
}
void check_rdir(char* cmd)
{
char* pos=cmd;
while(*pos)
{
if(*pos=='>')
{
if(*(pos+1)=='>')
{
//追加重定向
(*pos)='\0';
pos++;
(*pos)='\0';
pos++;
while(isspace(*pos))
{
pos++;
}
rdirfilename=pos;
rdir=APPEND_RDIR;
break;
}
else
{
//输出重定向
(*pos)='\0';
pos++;
while(isspace(*pos))
{
pos++;
}
rdirfilename=pos;
rdir=OUT_RDIR;
break;
}
}
else if(*pos=='<')
{
//输入重定向
(*pos)='\0';
pos++;
while(isspace(*pos))
{
pos++;
}
rdirfilename=pos;
rdir=IN_RDIR;
break;
}
pos++;
}
}
//获取当前路径
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
//输入命令行
void interact(char* cline,int sz)
{
getpwd();
printf("[%s@%s %s]$ ",getenv("USER"),getenv("HOSTNAME"),pwd);
char* s=fgets(cline,sz,stdin);
assert(s);
(void)s;
cline[strlen(cline)-1]='\0';
//cout<<strlen(cline)<<endl;
check_rdir(cline);
//cout<<cline<<endl;
}
//内建命令
//需要父进程自己执行
int BuildCommand(int _argc,char* _argv[])
{
if(_argc==2&&strcmp(_argv[0],"cd")==0)
{
chdir(_argv[1]);
//更改路径后需要重新刷新
getpwd();
//修改环境变量表中的PWD
sprintf(getenv("PWD"),"%s",pwd);
lastcode=0;
return 1;
}
if(_argc==2&&strcmp(_argv[0],"export")==0)
{
//这里必须要自己开一个数组存储导出的环境变量
//不然等到下一次运行别的命令的时候该导出的环境变量会被覆盖
//详情可以看后面的图片分析
strcpy(myenv,(char*)_argv[1]);
//到处环境变量
putenv(myenv);
//设置退出码
lastcode=0;
return 1;
}
if(strcmp(_argv[0],"ls")==0)
{
//"--color"是使我们的不同文件有不同的配色
_argv[_argc++]=(char*)"--color";
//指令要以NULL结尾
_argv[_argc]=NULL;
return 0;
}
if(_argc==2&&strcmp(_argv[0],"echo")==0)
{
if(strcmp(_argv[1],"$?")==0)
{
cout<<lastcode<<endl;
lastcode=0;
return 1;
}
else if(*_argv[1]=='$')
{
char* p=(char*)_argv[1]+1;
//获取环境变量
char* val=getenv(p);
if(val!=NULL)
{
//打印环境变量
printf("%s\n",val);
}
lastcode=0;
return 1;
}
else
{
return 0;
}
}
return 0;
}
void NormalExcute()
{
//普通命令
//创建子进程进行程序替换执行
pid_t id=fork();
if(id<0)
{
perror("fork fail");
lastcode=1;
exit(1);
}
if(id==0)
{
//输入重定向
if(rdir==IN_RDIR)
{
int fd=open(rdirfilename,O_RDONLY);
if(fd<0)
{
perror("open fail");
lastcode=5;
exit(5);
}
dup2(fd,0);
}
else if(rdir==OUT_RDIR)//输出重定向
{
int fd=open(rdirfilename,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open fail");
lastcode=6;
exit(6);
}
dup2(fd,1);
}
else if(rdir==APPEND_RDIR)//输出重定向
{
int fd=open(rdirfilename,O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd<0)
{
perror("open fail");
lastcode=7;
exit(7);
}
dup2(fd,1);
}
//子进程
execvp(argv[0],argv);
lastcode=2;
exit(2);
}
else
{
//父进程等待
int status=0;
pid_t childpid=waitpid(-1,&status,0);
if(childpid<0)
{
perror("wait fail");
lastcode=3;
exit(3);
}
else if(childpid==id)
{
cout<<"father wait success!"<<endl;
if(WIFEXITED(status))
{
cout<<"子进程正常退出;"<<"退出码:"<<WEXITSTATUS(status)<<endl;
lastcode=WEXITSTATUS(status);
}
else
{
cout<<"子进程异常退出;"<<"退出信号:"<<(status&0x7F)<<endl;
}
}
}
}
int main()
{
while(!quit)
{
rdirfilename=NULL;
rdir=NONE;
//交互问题,获取命令行
interact(CommandLine,sizeof(CommandLine));
//分割字符串
int argc=splitstring(CommandLine);
if(argc<=0)
{
continue;
}
//判断是否为内建命令
//不能用sizeof(argv),否则会越界
int ret=BuildCommand(argc,argv);
if(!ret)
{
//普通命令
NormalExcute();
}
}
return 0;
}
六、自主实现一个Mystdio.h库
6.1 main.c
6.2 makefile
6.2 Mystdio.h
#pragma once
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <stdlib.h>
#define SIZE 1024
#define Permission 0666
#define FLUSH_NOW 1<<0
#define FLUSH_LINE 1<<1
#define FLUSH_ALL 1<<2
typedef struct IO_file
{
int _fileno;
//char _inbuffer[SIZE];
char _outbuffer[SIZE];
int out_pos;
int flag;
}_FILE;
_FILE* _fopen(const char* file,const char* s);
int _fwrite(const char* msg,size_t size,size_t n,_FILE* fp);
void _fclose(_FILE* fp);
6.3 Mystdio.c
#include "Mystdio.h"
//"w" , "r" , "a"
_FILE* _fopen(const char* file,const char* s)
{
int fd=-1;
if(strcmp(s,"w")==0)
{
fd=open(file,O_WRONLY|O_CREAT|O_TRUNC,Permission);
}
else if(strcmp(s,"a")==0)
{
fd=open(file,O_WRONLY|O_CREAT|O_APPEND,Permission);
}
else if(strcmp(s,"r")==0)
{
fd=open(file,O_RDONLY);
}
if(fd==-1)
{
return NULL;
}
_FILE* fp=(_FILE*)malloc(sizeof(_FILE));
if(fp==NULL)
{
return NULL;
}
fp->_fileno=fd;
memset(fp->_outbuffer,0,sizeof(fp->_outbuffer));
fp->out_pos=0;
fp->flag=FLUSH_LINE;
return fp;
}
int _fwrite(const char* msg,size_t size,size_t n,_FILE* fp)
{
//立即刷新缓冲区
if(fp->flag&FLUSH_NOW)
{
for(size_t i=0;i<n;i++)
{
size_t m=write(fp->_fileno,msg,size);
if(m!=size)
{
return i;
}
}
}
else if(fp->flag&FLUSH_LINE) //行刷新
{
if(msg[strlen(msg)-1]=='\n')
{
for(size_t i=0;i<n;i++)
{
size_t m=write(fp->_fileno,msg,size);
if(m!=size)
{
return i;
}
}
}
else
{
for(size_t i=0;i<n;i++)
{
memcpy(fp->_outbuffer+fp->out_pos,msg,strlen(msg));
fp->out_pos+=strlen(msg);
}
}
}
else if(fp->flag&FLUSH_ALL) //全缓冲
{
for(size_t i=0;i<n;i++)
{
memcpy(fp->_outbuffer+fp->out_pos,msg,strlen(msg));
fp->out_pos+=strlen(msg);
}
}
return n;
}
void _fflush(_FILE* fp)
{
if(fp->out_pos>0)
{
write(fp->_fileno,fp->_outbuffer,fp->out_pos);
}
}
void _fclose(_FILE* fp)
{
if(fp==NULL)
{
return;
}
_fflush(fp);
close(fp->_fileno);
fp->out_pos=0;
free(fp);
fp=NULL;
}
七、基础IO+文件系统(从软件到硬件再到软件整体脉络一览图)
你学会了吗?如果感觉到有所收获,那就点点小心心点点关注呗!后期还会持续更新Linux操作系统的相关知识哦!我们下期见!