目录
🏆一、文件系统
1、空文件,也要在磁盘占据空间。
2、文件=内容+属性。
3、文件操作=对内容 + 对属性 or 对内容和属性
4、标定一个文件,必须使用:文件路径+文件名 [唯一性]
5、如果没有指明对应的文件路径,默认是在当前路径下进行文件访问
对文件的操作本质是进程对文件的操作!
一个文件要被访问,就必须先被打开!
文件操作是十分重要的,不同的语言都有自己独特的文件操作接口。C、C++、Java、python、php、shell等语言都有自己独特的文件操作接口。如果我们要全部掌握这些接口成本是很高的!而如果我们重新考虑文件的位置,以及我们访问文件所必经的路径:
所以为了降低学习成本,就要掌握系统调用接口。
批量化注释的方式:
1、open
①对open接口的介绍
C语言中打开文件的函数是fopen。
而fopen底层是调用了系统的open。
#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: 追加写
O_TRUNC:清空文件再写入
返回值:
成功:新打开的文件描述符
失败:-1
open接口的使用方式和fopen不太一样。首先在C语言中fopen成功返回FILE*指针,那是C中的概念,在操作系统中open接口调用成功则返回file descriptor(文件描述符),它是比C中指针更底层的东西。这一点文件描述符到底还有什么玄妙之处后续再详细介绍。
首先我们来关注open接口的第二个参数:flags,flags是标定打开文件的方式。它是用来作标记的,比如我们在C中常用bool值做标记,但是它只能标记true or false。如果文件显然打开方式有很多,只这两种是不满足我们的需求的。比如以只读、只写、读写、追加等方式打开。而我们要实现这一功能,需要对flags进行独特的设计。Linux借用int的特性,因为int是32个bit位,OS使用每一个bit位来做标记,那么它到底是怎么实现的呢?我们可以借助演示一段代码:
这里的宏ONE TWO THREE FOUR就是类似于一个个的选项。并且这里采用一个比特位表示一个选项,彼此位置不重叠。
我们在使用这些选项采用的是| 操作。
明白了这些,我们就可以简单使用一下open操作。
②接口使用
以只写方式打开一个不存在的文件。
我们发现以只写形式,系统不会自动创建文件,那是因为OS不会自己创建。C语言是经过了封装,如果我们想让它以只写形式的同时还要创建,要再 | 上创建选项。
然而这里的创建的log.txt是没法使用的,因为它的权限是随机的,我们在创建文件的时候要加上文件的权限。这一点也是open接口第三个参数的意义:权限。
所以我们也能理解了为什么OS准备了两套open接口 :
对于已经存在的文件打开时是不需要设置权限的。
同时,如果我们想修改创建文件的权限,可以修改umask值。
因为文件权限设置的方式是:
而umask默认权限是0002.
当然这里flags选项并没有讲完,它需要结合其他接口来演示效果更佳。
2、write接口
在了解了open是如何使用之后,我们再来看一下write的使用方式。老规矩,先来段演示代码:
write接口和fwrite是不一样的。我们在使用fwrite接口的时候,因为strlen不计入'\0',所以我们要+1,为'\0'预留一个位置。而我们在使用write接口的时候,是否需要也预留一个位置给'\0'呢?
那我们不妨打印输出一下:
显然+1结果并不如我们所愿。那么出现这种情况是因为,以'\0'作为字符串的结尾,是C语言的规定,和文件没有关系。而系统看来,它的文件并不需要'\0',它只要字符串的有效内容,所以使用系统接口write向文件写入字符串并不需要给'\0'留位置!!
我们再来看一段代码:
我们发现,我们以只写打开文件操作系统并没有为我们清空文件。这一点也是和C语言不同的。
C语言对这部分做了处理,会自动清空文件重写,而默认操作系统的文件写入是覆盖,它没有清空里面的内容。
如果我们想重写文件时先清空再写入,需要添加选项:O_TRUNC
至此,O_WRONLY选项是只写,如果文件不存在不会自动创建,而O_CREAT选项则是文件不存在就创建,默认情况下,对文件写入是覆盖,不会清空文件,所以O_TRUNC是清空文件选项。
那么如果我想实现追加呢? O_APPEND
3、read接口
ssize_t 是一种系统定制的类型,是有符合整数,可以等于0,大于0,小于0.
从特定的文件fd当中,将数据读到缓冲区里,期望读多少个,就是count。
演示:
至此,我们可以看一下语言对文件的操作本质就是对系统调用接口的封装。
🏆二、深入理解文件描述符fd
1、fd具体实质
我们再来理解一下文件。通过对系统调用接口open、write、read的认识,我们大概清楚文件操作的本质就是进程和被打开文件的关系。而系统中存在大量的文件,其中有的没被打开存储在磁盘上,而被打开的文件就要被OS管理起来。到底是如何管理的呢?
操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构标识文件:
struct file{} ,而这其中包含了文件的大部分属性。
结构体file里面有文件的属性,也有文件描述符。这样讲起来还是有点抽象,我们可以画个图更深入理解:
当文件被打开的时候,它的地址被计入到文件指针数组中,进程通过指针数组管理。而文件描述符本质上讲就是这个指针数组的下标。也就是标识打开文件在指针数组中的位置。而键盘、鼠标、显示器这些也是文件!
当然以上所述是结论,还是举些例子:
我们看到新打开文件的fd文件描述符从3开始,既然他们表示数组下标,为什么不是从0开始呢?这是因为默认打开了三个标准输入输出流:
那么他们的返回值不是文件描述符,而是结构体指针,说明结构体中包含了文件描述符。
验证:
果然他们的fd是0、1、2.所以我们打开的文件都是从3开始。
补充:FILE结构体:
在/usr/include/libio.h路径下:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
2、文件fd的分配规则
文件fd的分配规则是从指针数组下标,从小到大,按照顺序寻找最小的且没有被占用的下标fd。
举例说明:
再运行文件时,没有办法看到printf的内容。这是因为,按照有空插空的原则,这时fd数组下标为1的地方被文件myfile占用,而OS角度来看,它只会内容输出到fd为1(标准输出)的文件。这时就会出现无法打印到屏幕的现象。
3、fd重定向
本来应该输出到显示器,但是这里输出打印到log.txt,这称为重定向。
重定向的本质是:上层用的fd不变,在内核中更改fd对应的struct file*的地址。
这里的上层用的fd不变指的是在上层看来标准输入输出的文件描述符一直是0和1.
简单来说,OS不会去具体查看struct file* 的内容而是根据下标来执行。
①输出重定向
针对重定向,OS为我们提供了一个接口专门用于重定向。
把oldfd下标指向的内容拷贝到newfd下标指向。
②追加重定向
③输入重定向
这里我们要解决几个问题,子进程重定向会影响父进程吗?
我们知道,进程具有独立性,所以子进程不能影响父进程,所以说file*表父子进程是不一致的!
所以对于父子进程来说,子进程要拷贝父进程的进程管理,而如果对子进程进行重定向,那么就会file*表发生改变,而这一操作是不影响父进程的(进程具有独立性)。而文件是属于系统部分的,不需要拷贝给子进程。他们只是根据file*表对文件的处理不同。
所以说Linux的做法不是让子进程和父进程共享一张file_struct的表,而是拷贝一份父进程的表,这样不影响进程间的独立性。而执行进程程序替换的时候,不会影响曾经进程打开的重定向文件。因为你替换的是磁盘上拷贝下来的代码和数据,而重定向这些属于内核维护的数据结构,也就是说不影响pcb。
Linux下一切皆文件:
在OS看来它们都是struct file,没有什么不同,读写方法时直接调用对应的函数指针(多态的思想)。所有的设备和文件,统一都是struct file.
④文件的引用计数
我们close文件,是真的关闭了文件吗?如果真的关闭了文件,那么如果有多个进程打开同一个文件,我关闭了它而别的进程还在使用这样显然是不合适的。所以就有了文件的引用计数,在file结构体中,有一个f_count变量用于统计打开文件的个数。当f_count为0时文件才被关闭,因为当有引用文件,文件不会被关闭。
🏆三、缓冲区的理解
缓冲区本质就是一段内存。
缓冲区的意义是节省进程进行数据IO的时间,因为进程将数据存储到磁盘(访问外设)速度很慢,所以就有了进程先将数据放到缓冲区,缓冲区再将数据存储到磁盘。
而存放到缓冲区的数据,缓冲区有自己的刷新策略:
我们来段代码具体感受缓冲区的刷新策略:
我们发现在fork子进程之后,当重定位到文件中时,C接口的函数,前后打印了两次,而系统接口前后只打印了一次,这就和缓冲区有关。
所以我们知道一个信息,C语言中,存在缓冲区,而write在将数据拷贝到文件中的过程中并不存在缓冲区。但是我们的内核中是存在缓冲区的,这个缓冲区存在于将文件中的数据拷贝到磁盘上这个过程中。
所以说,如果我们使用C语言将数据拷贝到文件,再存储到磁盘,经历了两次将数据拷贝到缓冲区。而使用OS提供的写入,则只经历了一次将数据存入到内核缓冲区。
而内核缓冲区刷新数据到磁盘完全是由操作系统决定的。
那这里也衍生了一个问题,如果操作系统挂掉了,我们在内核缓冲区的数据该怎么办呢?
自己实现的一个迷你版shell命令行控制器:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<stdlib.h>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<errno.h>
#define NUM 1024
#define OPT_NUM 64
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define trimSpace(start) do{\
while(isspace(*start)) ++start;\
}while(0)
//do while(0) 包裹一个代码块
char lineCommand[NUM];
char *myargv[OPT_NUM];//指针数组
int lastCode=0;
int lastSig=0;
int redirType= NONE_REDIR;
char *redirFile=NULL;
void commandCheck(char* commands)
{
assert(commands);
char * start=commands;
char* end=commands+ strlen(commands);
while(start<end)
{
if(*start =='>')
{
*start='\0';
++start;
if(*start=='>')
{
redirType=APPEND_REDIR;
}
else
{
redirType=OUTPUT_REDIR;
}
//要么是输出要么是追加
trimSpace(start);
redirFile=start;
break;
}
else if(*start=='<')
{
//拆分成两个
//"cat < file.txt"
*start='\0';
start++;
trimSpace(start);//
//
//usr
redirType=INPUT_REDIR;
redirFile=start;
break;
}
else
{
start++;
}
}
}
int main()
{
while(1)
{
redirType= NONE_REDIR;
redirFile=NULL;
//输出提示符
printf("用户名@主机名 当前路径# ");
fflush(stdout);
char *s= fgets(lineCommand,sizeof(lineCommand)-1,stdin);//去除\0
assert(s!=NULL);
(void)s;
//清除最后一个\n,abcd\n \n重置为\0
lineCommand[strlen(lineCommand)-1]=0;
//ls -a -l -i "ls" "-a" "-l" -i"
//字符串切割
//"ls -a -l -i" ->"ls" "-a" "-l" "-i"
commandCheck(lineCommand);
//strtok
myargv[0]=strtok(lineCommand," ");
//如果没有子串了,strtok->NULL,myargv[end]=NULL
int i=1;
if(myargv[0]!=NULL&& strcmp(myargv[0],"ls")==0)
{
myargv[i++]=(char*)"--color=auto";
}
//如果没有子串了,strtok->NULL,myargv[end]=NULL
while(myargv[i++]=strtok(NULL," "));
//如果是cd命令,不需要创建子进程,让shell自己执行对应的命令
if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
{
if(myargv[1]!=NULL)chdir(myargv[1]);
continue;
}
if(myargv[0]!=NULL && myargv[1]!=NULL && strcmp(myargv[0],"echo")==0)
{
if(strcmp(myargv[1],"$?")==0)
{
printf("%d,%d\n",lastCode,lastSig);
}
else
{
printf("%s\n",myargv[1]);
}
continue;
}
//测试是否成功,条件编译
#ifdef DEBUG
for(int i=0;myargv[i];i++)
{
printf("myargv[%d]: %s\n",i,myargv[i]);
}
#endif
//#注释掉DEBUG
//执行命令
pid_t id =fork();
assert(id !=-1);
if(id==0)
{
//因为命令是子进程执行的,真正重定向的工作一定要是子进程来完成
//如何重定向,是父进程要给子进程提供信息的
//这里重定向会影响父进程吗?
switch(redirType)
{
case NONE_REDIR:
//什么都不做
break;
case INPUT_REDIR:
{
int fd=open(redirFile,O_RDONLY);
if(fd<0)
{
perror("open");
exit(errno);
}
//重定向的文件已经成功打开了
dup2(fd,0);
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
umask(0);
int flags=O_WRONLY | O_CREAT;
if(redirType==APPEND_REDIR) flags|=O_APPEND;
else flags|=O_TRUNC;
int fd=open(redirFile,flags,0666);
if(fd<0)
{
perror("open");
exit(errno);
}
dup2(fd,1);
}
break;
default:
printf("bug\n");
break;
}
execvp(myargv[0],myargv);
exit(1);
}
int status=0;
pid_t ret= waitpid(id,&status,0);
assert(ret>0);
(void)ret;
lastCode=(status>>8)&0xFF;
lastSig=(status&0x7F);
}
return 0;
}