快学,再不学普洱就要超过你们了
在C阶段进行的文件操作有哪些呢?
#include<stdio.h>
#include<string.h>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;
}
进行文件操作本质是我们的程序跑起来了,文件打开和关闭是CPU在执行我们的代码
读文件:
#include <stdio.h>
#include <string.h>
int main()
{FILE* fp = fopen("myfile", "r");if (!fp){printf("fopen error!\n");}char buf[1024];const char* msg = "hello world!\n";while (1){size_t s = fread(buf, 1, strlen(msg), fp);if (s > 0){buf[s] = 0;printf("%s", buf);}if (feof(fp)){break;}}fclose(fp);return 0;
}
fread是将文件的数据读到缓冲区里
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
buffer是自己设定的缓冲区,size是要读取数据的大小,char就是1,int就是4,count是要读取的个数,stream是文件指针,返回值是实际从文件中读取的基本单元个数
feof是用于在文件读取已经结束的时候,判断是读取失败导致的结束还是遇到文件尾正常的结束
将内容输出到显示器上有很多种方法:
#include<stdio.h>
#include<string.h>
int main()
{const char* msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}
均可:
C默认会打开三个输入输出流:stdin, stdout, stderr
这三个流的类型都是FILE*, fopen返回值类型:文件指针
文件没有被打开的时候在哪呢?
很显然在磁盘
进程能打开很多文件吗?
可以啊
系统中还支持多进程
在很多的情况下,OS内部存在大量的被打开的文件
那操作系统要不要对打开的文件进行管理呢?
肯定可以啊,,,
应该是存在某种描述文件属性的数据结构,最后转换成指针之间的映射
文件=属性+内容
以w打开文件的话,文件如果不存在,就在当前路径下新建指定文件,且默认打开文件的时候会把目标文件先清空
输出重定向伴随着文件操作
理解文件
操作文件的本质是进程在操作文件
文件在开始的时候在磁盘上,磁盘是外设,本质是硬件
但是用户没有权利直接写入,OS是硬件的管理者,需要通过操作系统进行写入
操作系统为我们提供系统调用的接口,用C/C++是对系统调用接口的封装
访问文件也可以系统调用
C++进行文件操作是这样的:
#include<iostream>
#include<string>
#include<fstream>#define FILENAME "log.txt"int main()
{std::ofstream out(FILENAME, std::ios::binary);if (!out.is_open()){return 1;}std::string message = "hello C++\n";out.write(message.c_str(), message.size());out.close();return 0;
}
看看系统调用接口:
使用一下系统调用接口:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT);if (fd < 0){perror("open fail");return 1;}return 0;
}
在Linux中创建文件的时候要告诉它起始权限,否则将会是乱码
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open fail");return 1;}return 0;
}
关于最终的权限还是由umask掩码和起始权限共同决定
umask掩码也可以人为设定:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open fail");return 1;}return 0;
}
在传参的时候传递标记位一般是传int,但是有的时候我们只需要知道有无,int有32个bit位
一般用比特位进行标志位的传递,在OS设计系统调用接口的时候比较常用
这个:O_WRONLY | O_CREAT是宏
我们可以自己设计一个传递位图标记位的函数:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>#define ONE 1
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)void Print(int flag)
{if (flag & ONE){printf("one\n");}if (flag & TWO){printf("two\n");}if (flag & THREE){printf("three\n");}if (flag & FOUR){printf("four\n");}
}
int main()
{Print(ONE);printf("\n");Print(ONE | TWO);printf("\n");Print(ONE | TWO | THREE);printf("\n");Print(ONE | TWO | THREE | FOUR);printf("\n");return 0;
}
可以用标记位组合的方式向一个函数中传递多个标记位
在系统中提供的关闭文件的接口是:
在Linux中提供的向文件中写入的接口是write
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open fail");return 1;}const char* message = "Hello Linux file\n";write(fd, message, strlen(message));close(fd);return 0;
}
这种写入是在上次基础上进行写入
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open fail");return 1;}const char* message = "Hello Linux\n";//const char* message = "Hello Linux file\n";write(fd, message, strlen(message));close(fd);return 0;
}
打开文件的时候想要清空怎么办捏?
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0){perror("open fail");return 1;}const char* message = "Hello Linux file\n";write(fd, message, strlen(message));close(fd);return 0;
}
这是写方式打开,存在就创建,不存在就清空
想要进行追加写入该怎么办呢?
就是APPEND啦~
看这个代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fda:%d\n", fda);int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fdb:%d\n", fdb);int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fdc:%d\n", fdc);int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fdd:%d\n", fdd);return 0;
}
看输出结果:
为何打印出是此种结果呢?
012去哪了?
0:标准输入 键盘
1:标准输出 显示器
2:标准错误 显示器
在C中默认打开的文件流:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{const char* msg = "hello pineapple\n";write(1,msg,strlen(msg));return 0;
}
就输出啦
刚输出的3456是文件描述符fd
fd的本质是什么?为什么可以通过这个数字向显示器文件中写入?
操作系统对被打开的文件要创建内核数据结构struct_file,对这些内核数据结构采用双链表的形式进行管理
对文件的管理变为对链表的增删查改
它指向文件内核级的缓存
进程:文件 = 1:n
在进程的PCB中存在struct files_struct *files,它指向struct files_struct,struct files_struct里面有一个指针数组struct file* fd_array[N],数组就有下标,到时只需要把描述文件的结构体变量地址存放在这个指针数组中即可
所以文件描述符fd的本质是内核的进程和文件映射关系的数组的下标
无论读和写,都要在合适的时候让OS把文件内容读到文件缓冲区中
那open是在做什么呢?
1.创建file
2.开辟文件缓冲区的空间,加载文件数据
3.查进程的文件描述符表
4.file地址,填入对应的表下标中
5.返回下标
还是看看这个open:
open函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则使用两个参数的open
012是硬件,要理解一切皆文件
这些设备都是IO设备(外设)
他们间的映射是通过指针完成的
比如键盘:
//函数指针
void (*read)(...)
void (*write)(...)
//函数
void k_read();
void k_write();
显示器鼠标同理:
void screen_read();
void screen_write();void mouse_read();
void mouse_write();
想要访问显示器,就找到对应的struct file让函数指针找到对应函数就可以了
类有属性和方法,struct file就是类
在OS内,系统在访问文件的时候,只认文件描述符fd
那么C是如何用FILE* 访问文件呢?
FILE是C提供的一个结构体类型
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{FILE* fp = fopen("log.txt", "w");if (fp == NULL){return 1;}printf("fd:%d\n", fp->_fileno);fwrite("hello", 5, 1, fp);fclose(fp);return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{printf("stdin->fd:%d\n", stdin->_fileno);printf("stdout->fd:%d\n", stdout->_fileno);printf("stderr->fd:%d\n", stderr->_fileno);FILE* fp = fopen("log.txt", "w");if (fp == NULL){return 1;}printf("fd:%d\n", fp->_fileno);FILE* fp1 = fopen("log1.txt", "w");if (fp1 == NULL){return 1;}printf("fd:%d\n", fp1->_fileno);FILE* fp2 = fopen("log2.txt", "w");if (fp2 == NULL){return 1;}printf("fd:%d\n", fp2->_fileno);fclose(fp);return 0;
}
封装了系统的细节
可以使用系统调用(系统不同提供的调用接口,代码不具跨平台性),也可以用语言提供的方法
C语言本身有源代码---标准库
C标准库对于不同系统会生成不同版本的C标准库
所有的语言都具有跨平台性
C语言为什么要这样做呢?
因为所有语言都具有跨平台性,所以它们要对不同平台的系统调用进行封装,不同语言进行封装的时候,文件接口就会有差别
cout,cin,cerr都被叫做类
内部也包含了相应的文件描述符
文件描述符就是从0开始的小整数,当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,于是就有了file结构体:表示一个已经打开的文件对象。
进程执行open系统调用,所以必须让进程和文件关联起来
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分包含一个指针数组,每个元素都是一个指向打开文件的指针,本质上,文件描述符就是该数组的下标
只要拿着文件描述符,就可以找到对应的文件
重定向
读文件操作:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>const char* filename = "log.txt";int main()
{struct stat st;int n = stat(filename, &st);if (n < 0){return 1;}printf("file size:%lu\n", st.st_size);int fd = open(filename, O_CREAT | O_RDONLY);if (fd < 0){perror("open fail");return 1;}close(fd);return 0;
}
康康:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>const char* filename = "log.txt";int main()
{struct stat st;int n = stat(filename, &st);if (n < 0){return 1;}printf("file size:%lu\n", st.st_size);int fd = open(filename, O_RDONLY);if (fd < 0){perror("open fail");return 2;}printf("fd:%d\n", fd);char* file_buffer = (char*)malloc(st.st_size + 1);n = read(fd, file_buffer, st.st_size);if (n > 0){file_buffer[n] = '\0';printf("%s\n", file_buffer);}free(file_buffer);close(fd);return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<sys/types.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
const char* filename = "log.txt";int main()
{close(0);int fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);if(fd<0){perror("open fail");return 1; } printf("fd:%d\n",fd); close(fd); return 0;
}
文件描述符有一套对应的分配规则
它会查找自己的文件描述表,分配最小的没有被分配的fd
printf和fprintf默认是向显示器中打印,但是这样可以向文件内打印:
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<sys/types.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>const char* filename = "log.txt";int main()
{close(1);int fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);if(fd<0){perror("open fail");return 1;}printf("fd:%d\n",fd);fprintf(stdout,"fprintf,fd:%d\n",fd);fflush(stdout);close(fd);return 0;
}
这个过程叫做重定向
printf/fprintf默认是向stdout中打印的,stdout有对应的struct FILE,里面对应的_fileno==1
重定向的本质是在内核中改变文件描述符表特定下标的内容,和上层无关
但是我们可以发现如果不加fflush的话是默认没有打印内容的
struct FILE*里有语言级别的缓冲区,所以需要刷新才能打印出来
看个函数,dup2,完成对文件描述符下标所对应内容的拷贝
进行标准的输出重定向应该:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>const char* filename="log.txt";int main()
{int fd = open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);dup2(fd,1);printf("hello world\n");fprintf(stdout,"hello world\n");return 0;
}
所以如果改一下的话,实现像以前一样的多终端打印则可以:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>const char* filename = "log.txt";int main()
{int fd = open("/dev/pts/2", O_CREAT | O_WRONLY | O_TRUNC, 0666);if (fd < 0){perror("open fail\n");return 1;}dup2(fd, 1);printf("hello world\n");fprintf(stdout, "hello world\n");fflush(stdout);return 0;
}
缓冲区
缓冲区即有用户级缓冲区,又有内核级缓冲区
功能主要有两种:解耦和提高效率
提高效率是指能提高使用者的效率
调用系统调用需要成本,可提高IO刷新的效率
缓冲区要给上层提供高效的IO体验,同时提高整体的效率
刷新策略
那缓冲区的刷新策略是什么呢?
1.立即刷新:从内核到外设的刷新,比如fflush(stdout),再比如fsync(int fd)...
2.行刷新:一般应用于显示器
3.全缓冲,缓冲区写满,才刷新,应对普通文件
还有特殊情况是进程退出系统自动刷新
缓冲区刷新策略和用户级内核级无关,是通用的(均码)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>const char* filename = "log.txt";int main()
{printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char* msg = "hello write\n";write(1, msg, strlen(msg));fork();return 0;
}
在我们默认打印的时候是向显示器打印的,显示器刷新的策略为行刷新
当重定向到文件中,刷新策略会发生变化
所以log.txt会显示成这样的原因是,重定向之后,刷新策略发生变化,普通文件采用全缓冲策略进行刷新,假设没有fork,不论是否重定向,数据都直接写入操作系统内部,文件缓冲区数据已经存在,fork变得无意义
当重定向之后变为全缓冲,文件缓冲区并没有被写满,所以会把它暂时保存起来,在stdout对应的缓冲区,在fork时并没刷新,还在缓冲区(因为没写满),而在fork时父子进程都会刷新
所以语言级别的printf和fprintf被打印了两次
库函数输出两次,系统调用只输出一次:
库函数写入文件时是全缓冲,写入显示器是行缓冲
printf、 fwrite 库函数会自带缓冲区
当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲,放在缓冲区中的数据不会被立即刷新(fork之后也不会)
进程退出之后,会统一刷新,写入文件当中
fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。 write 没有变化,说明没有所谓的缓冲
printf fwrite 库函数会自带缓冲区,write 系统调用没有带缓冲区 (均指用户级缓冲区)
为了提升整机性能,OS也会提供相关内核级缓冲区
那这个缓冲区谁提供捏?
printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,write 没有缓冲区,printf fwrite 有,该缓冲区是二次加上的,由C标准库提供
每一个文件都有一个自己的缓冲区
这是FILE结构体:
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 0int _blksize;
#elseint _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
};
shell加个重定向
在进行重定向的时候,进程中的程序替换不会影响进程关联的或者打开的文件
因为进程替换的是代码和数据,内核数据结构不受影响
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<ctype.h>
#include<errno.h>
#include<sys/wait.h>#define SkipPath(p) do{ p += (strlen(p) - 1); while (*p != '/')p--;}while(0)#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32#define None_REDIR 0
#define In_REDIR 1
#define Out_REDIR 2
#define App_REDIR 3#define SkipSpace(usercommand,pos) do{\while (1){\if(isspace(usercommand[pos]))\pos++;\else break;\}\
}while (0)int redir_type = None_REDIR;
char* filename = NULL;int lastcode = 1;
char cwd[SIZE * 2];
char* gArgv[NUM];void Die()
{exit(1);
}const char* Home()
{const char* home = getenv("HOME");if (home == NULL){return "/";}return home;
}void Cd()
{const char* path = gArgv[1];if (path == NULL){path = Home();}chdir(path);char temp[SIZE * 2];getcwd(temp, sizeof(temp));snprintf(cwd, sizeof(cwd), "PWD=%s", temp);putenv(cwd);
}const char* GetUserName()
{const char* name = getenv("USER");if (name == NULL){return "None";}return name;
}const char* GetHostName()
{const char* hostname = getenv("HOSTNAME");if (hostname == NULL){return "None";}return hostname;
}const char* GetCwd()
{const char* cwd = getenv("PWD");if (cwd == NULL){return "None";}return cwd;
}void MakeCommandLine()
{char line[SIZE];const char* username = GetUserName();const char* hostname = GetHostName();const char* cwd = GetCwd();SkipPath(cwd);snprintf(line, sizeof(line), "[%s@%s %s]#", username, hostname, strlen(cwd) == 1 ? "/" : cwd + 1);printf("%s", line);fflush(stdout);
}int GetUserCommand(char command[], size_t n)
{char* s = fgets(command, n, stdin);if (s == NULL){return -1;}command[strlen(command) - 1] = ZERO;return strlen(command);
}void splitCommand(char command[], size_t n)
{gArgv[0] = strtok(command, SEP);int index = 1;while ((gArgv[index++] = strtok(NULL, SEP)));
}void ExecuteCommand()
{pid_t id = fork();if (id < 0){Die();}else if (id == 0){if (filename != NULL){if (redir_type == In_REDIR){int fd = open(filename, O_RDONLY);dup2(fd, 0);}else if (redir_type == Out_REDIR){int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC);dup2(fd, 1);}else if (redir_type == App_REDIR){int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND);dup2(fd, 1);}else{}}execvp(gArgv[0], gArgv);exit(errno);}else{int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);if (lastcode != 0){printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);}}}
}int CheckBuildin()
{int yes = 0;const char* enter_cmd = gArgv[0];if (strcmp(enter_cmd, "cd") == 0){yes = 1;Cd();}else if (strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;printf("%d\n", lastcode);lastcode = 0;}return yes;
}void CheckRedir(char usercommand[])
{int pos = 0;int end = strlen(usercommand);while (pos<end){if (usercommand[pos] == '>'){if (usercommand[pos + 1] == '>'){usercommand[pos++] = 0;pos++;redir_type = App_REDIR;SkipSpace(usercommand, pos);filename = usercommand + pos;}else{usercommand[pos++] = 0;pos++;redir_type = In_REDIR;SkipSpace(usercommand, pos);filename = usercommand + pos;}}else if (usercommand[pos] == '<'){usercommand[pos++] = 0;redir_type = In_REDIR;SkipSpace(usercommand,pos);filename = usercommand + pos;}else{pos++;}}
}int main()
{int quit = 0;while (!quit){redir_type = None_REDIR;filename = NULL;MakeCommandLine();char usercommand[SIZE];int n = GetUserCommand(usercommand, sizeof(usercommand));if (n < 0){return 1;}CheckRedir(usercommand);splitCommand(usercommand, sizeof(usercommand));n = CheckBuildin();if (n){continue;}ExecuteCommand();}return 0;
}
封装库
带不带\n可以决定刷新的效率:
mystdio.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>#define LINE_SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4struct _myFILE
{unsigned int flags;int fileno;char cache[LINE_SIZE];int cap;int pos; //下次写入的位置
};typedef struct _myFILE myFILE;ssize_t my_fwrite(myFILE* fp, const char* data, int len);
void my_fflush(myFILE* fp);
myFILE* my_fopen(const char* path, const char* flag);
void my_fclose(myFILE* fp);
mystdio.c
#include"mystdio.h"ssize_t my_fwrite(myFILE* fp, const char* data, int len)
{memcpy(fp->cache + fp->pos, data, len);fp->pos += len;if ((fp->flags & FLUSH_LINE) && fp->cache[fp->pos - 1] == '\n'){my_fflush(fp);}return len;
}void my_fflush(myFILE* fp)
{write(fp->fileno, fp->cache, fp->pos);fp->pos = 0;
}myFILE* my_fopen(const char* path, const char* flag)
{int flag1 = 0;int iscreate = 0;mode_t mode = 0666;if (strcmp(flag, "r") == 0){flag1 = (O_RDONLY);}else if (strcmp(flag, "w") == 0){flag1 = (O_WRONLY | O_CREAT | O_TRUNC);iscreate = 1;}else if (strcmp(flag, "a") == 0){flag1 = (O_WRONLY | O_CREAT | O_APPEND);iscreate = 1;}else{}int fd = 0;if (iscreate){fd = open(path, flag1, mode);}else{fd = open(path, flag1);}if (fd < 0){return NULL;}myFILE* fp = (myFILE*)malloc(sizeof(myFILE));if (!fp){return NULL;}fp->fileno = fd;fp->flags = FLUSH_LINE;fp->cap = LINE_SIZE;fp->pos = 0;return fp;
}void my_fclose(myFILE* fp)
{my_fflush(fp);close(fp->fileno);free(fp);
}
test.c
#include"mystdio.h"
#include<stdio.h>
#include<unistd.h>
#include<string.h>#define FILE_NAME "log.txt"int main()
{myFILE* fp = my_fopen(FILE_NAME,"w");if (fp == NULL){return 1;}const char* str = "hello world";int cnt = 10;char buffer[128];while (cnt){sprintf(buffer, "%s - %d\n", str, cnt);my_fwrite(fp, buffer, strlen(buffer));cnt--;sleep(1);}my_fclose(fp);return 0;
}
说实话真挺恶心的
stderr为何存在
’>标准输出重定向,只会更改1号fd里面的内容
stderr存在主要是因为我们输出的信息有两类:正确or错误
错误信息单独往stderr中打印,这样只需要一次就可以从文件层面把正确信息和错误信息分开了
将正确信息和错误信息分开只需:
./a.out 1>ok.txt 2>err.txt
正确错误一起:
./a.out 1>all.txt 2>&1
有个接口perror
它的本质是向2打印
#include<iostream>
int main()
{std::cout << "hello cout" << std::endl;std::cerr << "hello cerr" << std::endl;return 0;
}
系统中存在非常多的文件,而被打开的文件只是少量的
没有被打开的文件到底在哪存放呢?
都在磁盘耶,就叫磁盘文件
打开需找到需定位(要在磁盘中找到 -- 文件路径+文件名)
本质是研究文件如何存取
磁盘存储结构
磁盘长这样:
01是规定出的结果,可能在物理上会有不同的表现
盘片可读可写可擦除,一片两面都可写数据
磁盘本质是机械设备,优点是稳定和便宜
磁盘分为桌面级磁盘(民用)和企业级磁盘
就真的以为自己是华山峰的人了~
磁盘是一个机械设备,外设,比较慢,性价比高
康康磁盘的存储结构
磁盘清理:烧掉/全都写成0或1
每个圈都是磁道
读写的基本单位是扇区:512字节,4kb
1片 = n磁道
1磁道 = m扇区
怎样找到指定位置的扇区呢?
先找到对应的磁头(Header),再找到指定的磁道(柱面)(Cylinder),找到指定的扇区
(Sector),这是CHS定址法
盘片旋转:定位扇区
磁头左右摆动:定位磁道
文件 = 内容 + 属性
OS对磁盘这样的设备进行管理及抽象
为什么要这样进行抽象呢?
OS直接用CHS耦合度太高,而且抽象后能方便内核实现磁盘管理
每个扇区都对应成数组下标,变成:
struct disk_array[N];
每一个磁道都有一百个扇区
这样转化:
index / 1000 = H
index % 1000 = temp; [0, 999]
temp / 100 = C
temp % 100 = S
只要到时候把编号交给磁盘就OK
文件 = 很多个sector数组的下标
一般而言,OS未来和磁盘交互的时候,基本单位是4kb(规定如此)
要有8个sector,8个连续的扇区(块大小)
文件由很多块构成
对于OS而言,读取数据可以以块为单位
只要知道一个起始和磁盘的总大小,有多少个块,每个块的块号,如何转换到对应的多个CHS地址就全知道了
LBA:逻辑区块地址(Logical Block Address,LBA)
LBA block[N];
电脑的磁盘只有一块,所谓C盘D盘是不同的分区
磁盘分区管理好一组能管理好每一个组,分治思想
文件系统
文件的磁盘存储本质是文件内容+文件属性数据
在ll的时候能看到这些:
文件名+文件元数据
每行的七列:
模式,硬链接数,文件所有者,组,大小,最后修改时间,文件名
ll是读取存储在磁盘上的文件信息并显示出来:
stat能看到更多的信息:
先看看整体的架构:
Linux ext2文件系统,磁盘文件系统图(内核内存映像不同),磁盘是块设备,硬盘分区被划分为一个个的block
一个block的大小是由格式化的时候确定的,并且不可以更改
启动块(Boot Block)的大小是确定的
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group,每个Block Group都有着相同的结构组成
data blocks是数据区,存放文件内容
block bitmap是块位图,记录着data block中的哪个数据块已经被占用,哪个数据块没有被占用,比特位的位置表示块号,比特位的内容表示该块是否被占用
innode里面的也都是块,innode位图,innode Bitmap里每个bit表示一个innode是否空闲可用
Linux中文件的属性是一个大小固定的集合体
里面有文件的属性
struct innode
{int size;mode_t mode;int creater;int time;...int innode_number;...
}
一个文件一个innode
innode内部不包含文件名
内核层面每一个文件都要有innode number,通过innode号标识一个文件
GDT:块组描述符,描述块组属性信息
超级块(Super Block):存放文件系统本身的结构信息,记录的信息:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息
Super Block的信息被破坏 == 整个文件系统结构被破坏,并不是每个系统都有,某几个有,具有健壮性,让文件系统变得更稳定
在每一个分区内部分组,写入文件系统的管理数据(格式化),在磁盘中写入文件系统
本质上只能通过innode找文件,找文件需要先知道innode号
innode编号是以分区为单位的
innode的范围属于块组,data block也是这样的
i节点表:存放文件属性:文件大小,所有者,最近修改时间等
数据区:存放文件内容
一般是这样的:
int datablocks[15];
其中data block的直接映射的下标是[0,11],12,13是指向其他数据块,为了进行扩容
14指向的是索引,索引指向其他索引
通过间接映射建立更多的文件
但是不建议跨区,磁盘可能会重新选址
将属性和数据分开存放,具体是这样工作的:
创建新文件的四个操作:
1.存储属性
内核先找到一个空闲的i节点把文件信息记录到其中
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800
将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推
3. 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表
4. 添加文件名到目录
新的文件名abc
那linux如何在当前的目录中记录这个文件捏?
内核将入口(929279,abc)添加到目录文件
文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来
这是新分区的结果:
怎样拿到innode呢?
一直用的是文件名
目录是不是文件捏?
是哎
目录=文件属性+文件内容
文件内容:文件名和innode的映射关系
一个目录下不能建立同名文件:因为文件名在一个目录必须唯一,查找文件顺序:找到innode编号
目录的r本质是是否允许我们读取目录的内容,文件名指innode的映射关系
目录的w是新建文件,最后要向当前所处的目录内容中写入文件名和innode的映射关系
删除文件只需要找到组里的位图,删除文件并不删除属性和内容,只是删除对应的位图(删除快是因为删除只需要把文件属性和内容设置为无效即可)
那么如果文件被误删可以恢复么?
可以a,但是如果文件被误删很容易被覆盖内容,如果新建文件进行复用可能真的找不到
Linux的回收站本质是目录!
目录是图形化显示
从回收站恢复数据本质是把数据mv回来
哦对还有个事,我的电子榨菜呜呜
没东西听了
要找到指定的文件则必须找到该文件所在的目录,打开需要根据文件名:innode
目录是文件,要找到目录名和innode
找到根目录才能依次找到各级目录,需要进行逆向的路径解析
这是由OS自己做的
在Linux中定位一个文件,在任何时候都要有路径的原因
Linux目录结构是树状的,而Linux会缓存路径结构
要确定文件在哪个分区里,拿到文件的innode更前提的是知道在哪个分区
innode只在分区内有效,那怎么知道innode在哪个分区有效呢?
进入分区本质是进入指定的目录
运维少年最关注的事,,,
罪恶根源:
配置服务器的盘一般就一块(配置比较低啦)
在根目录下进行的操作一般都在/dev/vda下进行
在Linux中也可以构建大文件,/dev/zero是设备提供的,这是自己形成了10M的文件
dd if=/dev/zero of=disk.img bs=1M count=10
这样格式化:
mkfs.ext4 disk.img
挂载是这样的:
可以看到它挂载到这个目录:
访问分区有前置内容: 分区-->写入文件系统(格式化)-->挂载到指定的目录下-->进入该目录-->在指定的分区中进行文件操作
任何文件都要有目录,而只要有目录,挂载到相应的路径访问到字符串的前缀,在哪个目录里,就在哪个分区下
目录通常是谁提供的捏?
肯定是你或者你的进程提供的呀
由内核文件系统提前写入并组织好,然后我们提供的
路径解析不麻烦吗,麻烦啊!
所以Linux内核在被使用的时候,存在大量的解析完毕的路径,要对访问的路径做管理
那要怎么管理捏?
内核有个结构包含路径解析,一个文件一个dentry
struct dentry
{struct dentry *next;listnode list;
}
形成一棵树帮助我们找到对应的文件