FILE*fp=fopen("./log.txt","w");//以写的形式打开
FILE*fp=fopen("./log.txt","r");//以读的形式打开
FILE*fp=fopen("./log.txt","a");//以追加的形式打开
int main()
{
FILE* fp = fopen("myfile", "w");
if (!fp) {
printf("fopen error!\n");
}
const char* msg = "hello world!\n";
int count = 5;
while (count--) {
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
int main()
{
FILE* fp = fopen("myfile", "r");
if (!fp) {
printf("fopen error!\n");
}
char buf[1024];
const char* msg = "hello world!\n";
while (1) {
ssize_t s = fread(buf, 1, strlen(msg), fp);
if (s > 0) {
buf[s] = 0;
printf("%s", buf);
}
}
fclose(fp);
return 0;
}
fputs("hello world\n", fp);//写入,没有就创建log.txt再写入
char buffer[64];
fgets(buffer, sizeof(buffer), fp);//读取到buffer
这里就不一一举例说明C语言的文件操作了。
想看可以看这篇具体C语言文件操作
C语言默认打开 stdin stdout stderr三个流
C++默认打开 cin cout cerr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
以上所有写入的操作都要往文件或者硬件设备写入,而文件也就是磁盘,也是硬件。最终都是访问硬件,而OS我们之前说了,它是硬件的管理者,所以所有语言上对文件的操作都要贯穿OS。
而我们也知道,OS不相信任何人,所以访问OS需要通过系统调用接口。
几乎所有语言,封装的比如fopen fclose fread fwrite等等,底层一定要使用OS提供的系统调用接口。
下面我们就说说系统文件IO
int main()
{
int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);
int fd1=open("./log.txt",O_WRONLY|O_CREAT,0644);
int fd2=open("./log.txt",O_WRONLY|O_CREAT,0644);
int fd3=open("./log.txt",O_WRONLY|O_CREAT,0644);
int fd4=open("./log.txt",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
perror("open");
}
printf("fd:%d\n",fd);
printf("fd:%d\n",fd1);
printf("fd:%d\n",fd2);
printf("fd:%d\n",fd3);
printf("fd:%d\n",fd4);
close(fd);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
如果对fd进行打印,发现fd为3,如果再不断打开其他文件,发现fd为4,5,6,7。。。为什么会有这样的现象,0,1,2去哪了?
答:fd为文件描述符后面具体说,0,1,2分别对应着
-1为打开失败。
我们现在根据fd,012345…能联想到什么?数组下标!后面慢慢解释。
所有的文件操作,表现上都是进程执行对应的函数,进程对文件的操作为,先打开文件,然后将文件相关的属性信息加载到内存,系统中是不是存在大量的进程,进程可以打开多个文件,那是不是系统中会有更多打开的文件,那么OS要不要管理起来?很明显需要!那如何管理?先描述,再组织!
struct file{…}
int main()
{
int fd=open("./log.txt",O_WRONLY|O_CREAT,0644);
if(fd<0)
{
perror("open");
return 1;
}
const char*msg="hello world!\n";
int cnt=5;
while(cnt--)
{
write(fd,msg,strlen(msg));//返回值为实际写入的字节数
}//这就在log.txt写入了5条hello world!
close(fd);
return 0;
}
int main()
{
int fd=open("./log.txt",O_RDONLY);
if(fd<0)
{
perror("open");
return 1;
}
char buffer[1024];
ssize_t s=read(fd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
printf("%s\n",buffer);
}
close(fd);
return 0;
}
open的返回值是OS给我们的。进程打开文件,OS内一定打开了更多的文件,OS必须对这些文件进行管理。
一个文件没有打开的时候它在哪?在磁盘上!
创建一个空文件,该文件也要属性,属性就是数据,那么该文件就会占磁盘空间。
磁盘文件=文件内容+文件属性。
所以可以对文件内容操作,也可以对文件属性操作(权限、命名、位置等)。
我们之前说了,系统中有很多打开的文件,那么OS就要对其进行管理,怎么管理?一定是先描述,再组织!
先描述再组织就是说要有结构体和组织起来结构体的数据结构。
描述文件的结构体是:
struct file{…//文件相关属性};
现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
所以我们还可以用以下方式去输出到显示器上
int main()
{
const char*msg="hello world\n";
write(1,msg,strlen(msg));
write(2,msg,strlen(msg));
char buff[64];
ssize_t s=read(0,buff,sizeof(buff)-1);
buff[s]=0;
printf("echo# %s\n",buff);
return 0;
}
给新文件分配的fd,是从fd_arr[]指针数组中找一个最小的,没有被使用的,作为新的fd。
close(0);
int fd=open("./log.txt",O_WRONLY|O_CREAT);
printf("hello world\n");
比如我们如果先close(0),那么再int fd=open(“log.txt”…)
现在的fd就是0,close(2),那么就用2。
但是如果关闭close(1)呢?有个奇怪的现象,没有打印任何东西,而是输出到文件中了,我们竟然完成了一个重定向!
为啥嘞?
printf为C语言层的打印函数,它的本质是向标准输出打印。
C语言默认会打开:
FILE*stdin
FILE*stdout
FILE*stderr
而FILE*是C语言层上的结构体指针,指向一个结构体
struct FILE{
//一定包含了一个整数是对应在系统层面的
//是这个文件打开对应的fd
};
在系统层面现在的fd==1的数组下标已经指向了log.txt,但是C语言层面结构体内的fd仍未1,它是不能管OS怎么做的,C语言层它只认识fd=1。
所以我们printf在语言层我们还是向stdout(fd=1)打印,然后要调用系统接口,让OS做事,给OS说我要向fd=1打印输出语句了,你帮我弄一下。此时OS系统层因为fd=1指向的是log.txt,就把语句输出到此文件中了。这就完成了一次输出重定向工作。
int fd=open("./log.txt",O_WRONLY|O_CREAT|O_APPEND);
//这样是追加重定向。
输入重定向:
//本来要从标准输入进行读取,现在重定向,从log.txt文件读取到line
int main()
{
close(0);
int fd=open("./log.txt",O_RDONLY);
printf("fd:%d\n",fd);
char line[128];
while(fgets(line,sizeof(line)-1,stdin))
{
printf("%s\n",line);
}
return 0
}
我们验证一下,C语言默认打开的三个流结构体内存在fd
printf("stdin->%d\n",stdin->_fileno);
printf("stdout->%d\n",stdout->_fileno);
printf("stderr->%d\n",stderr->_fileno);
FILE*fp=fopen("./log.txt","r");
if(fp==NULL)
{
perror("fopen");
return 1;
}
printf("fp->%d\n",fp->_fileno);
我们非得先关闭0,1,2才能完成重定向吗?
下面有个系统调用接口:
#include
int dup2(int oldfd, int newfd);
如果执行dup2(fd,1),作用如下图
重定向前,若newfd已经有打开的文件,就先关闭它,重定向后,oldfd和newfd都指向oldfd的文件。
下面我们来使用dup2来实现一下输出输入重定向
//输出重定向
int fd=open("./log.txt",O_WRONLY|O_TRUNC);
dup2(fd,1);
printf("hello world1\n");
fprintf(stdout,"hello world2\n");
fputs("hello world3\n",stdout);
//输入重定向
int fd=open("./log.txt",O_RDONLY);
dup2(fd,0);
char buff[1024];
scanf("%s",buff);
printf("%s\n",buff);
执行exec*程序替换的时候,会不会影响我们曾经打开的所有文件呢?
答:不会!
替换只是替换代码和数据,没有创建新的进程,不影响进程,更不会影响到PCB中的管理打开文件的数据结构。
父进程如果曾经打开了标准输入,标准输出,标准错误,那么子进程也会继承下去。
为什么我们所有进程都会默认打开这仨?
因为bash操纵系统打开的,而之后的都是bash的子进程所有都继承了。
以上我们所有代码最后都没有close(fd),当我们写了话,会发现,重定向到log.txt的内容没有了,怎么回事?这就引出来缓冲区的概念了。我们往后继续看!
简述一下对文件描述符的理解?
在进程中每打开一个文件,都会创建相应的文件描述信息struct file,这个描述信息被添加在PCB的struct file_struct中,以数组的形式进行管理,该数组是指针数组,数组每个元素指向一个文件描述体struct file,文件描述符就是该数组下标,让用户通过文件描述符找到对应的文件,操作文件。
简述一下重定向的实现原理?
每个文件描述符都是内核PCB中文件描述信息数组的下标,每个下标里的数组元素对应一个文件的描述体struct file,用于操作文件。而重定向就是在不改变所操作文件描述符的情况下,通过改变文件描述符下标对应数组元素中文件描述信息,让其指向其他的文件,进而实现改变操作的文件(比如dup2系统调用接口)。
const char*msg1="hello 标准输出\n";
write(1,msg1,strlen(msg1));
const char*msg2="hello 标准错误\n";
write(2,msg2,strlen(msg2));
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
close(1);
现在一切正常,但如果我是./test > log.txt
呢?
我们会发现:
①显示器只输出hello 标准错误
②log.txt里面只有hello 标准输出
这些原因到底是怎么回事?
首先解释一点,> 重定向是将fd=1的标准输出重定向,所以标准错误的输出不受影响,因为标准错误的文件描述符为2
const char*msg2="hello 标准错误\n";
write(2,msg2,strlen(msg2));
我们接着解释:
首先我们得了解一下用户到OS的刷新策略:
①立即刷新(不缓冲)。
②行刷新(行缓冲\n)比如向显示器打印。
③全缓冲,缓冲区满了才刷新,比如向磁盘文件写入。
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
因为:当我们close(1),没有重定向时,这两条语句最后都有\n所以是行刷新策略,遇到\n就直接从C缓冲区刷新到OS内核缓冲区了,最后也就可以被输出。
但是当我们重定向,行刷新的策略变为全缓冲,需要将C缓冲区写满才会刷新到OS缓冲区,这时close(1),数据还来不及刷新到OS内核缓冲区,因为fd关闭了,无法通过fd,将数据再刷新到OS缓冲区了,所以最后也无法将数据写入磁盘文件。log.txt就没有了。
那为什么这句可以写入?
const char*msg1="hello 标准输出\n";
write(1,msg1,strlen(msg1));
因为人家可是直接write的系统调用函数呀,人家可是在内核层的,是立即刷新的。
如果你想解决可以在close()前加上fflush。
再来看一个现象
int main()
{
const char*msg1="hello 标准输出\n";
write(1,msg1,strlen(msg1));
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
fork();
return 0;
}
正常运行打印正常,但如果重定向到log.txt为什么,系统调用函数没有拷贝,而C语言接口函数都拷贝了一份?
原因是:
重定向后刷新策略改变,从行刷新变成全缓冲,这时数据会暂时保存在C语言缓冲区,当你fork()创建子进程,因为C语言缓冲区里有数据,要将数据写入到OS缓冲区,会发生写时拷贝。所以结果就知道了。
如果不想发生这种情况,需要在fork()前添加fflush(stdout),这样,C语言缓冲区没有数据,就不会发生写时拷贝了。
不仅仅是C语言,任何上层语言,都存在缓冲区。
之前我们谈到的都是打开的文件,那如果一个文件没有被打开,它在哪里保存着呢?
在磁盘上!
文件=文件内容+文件属性
磁盘是我们计算机中的一个机械设备。
磁盘写入基本单位是:扇区为512字节。
盘面,磁道,磁头,扇区。。。
具体是什么样的呢?
我们还看到Struct inode
结构体里还有一个int inode_number
,这个就是inode编号!
文件名在系统层面是没有意义的,文件名是给用户用的,在Linux中,真正标识一个文件是通过文件的inode编号!一个文件对应一个inode对应其编号!
这就好像国家统计我们百姓信息,不是靠姓名标识哪个人(万一有重名),而是靠每个人的身份证号。
ls -i file 可以查看文件file的inode号
说完inode Table 和 Data block,还有Block Bitmap和inode Bitmap是干嘛的?
有没有想过,我们是怎么快速知道,哪个inode和block是否已经被使用了呢?
这两个位图就起作用了!
拿inode Bitmap举例
0000 1010
从右向左
比特位的位置含义:inode编号
比特位的内容含义:特定inode编号是否被占用
目录也是文件,所以也有inode编号,也有数据块,那放什么呢数据块,比如放文件名和inode编号的映射关系等。
我们所有创建的文件都在一个特定目录下。
如果我们想删除一个文件怎么办?
只需要将位图里相应比特位1置为0即可!
我们有时听说谁谁谁删库跑路了,那如果不小心误删了文件怎么办?
那就是啥也被做,让懂哥过来恢复把0->1。不让你乱动就是怕你把刚才删除那块空间覆盖了,就不好恢复了。
⭐感谢阅读,我们下期再见
如有错 欢迎提出一起交流