引言:
C语言是怎样进行文件操作的?
#include<stdio.h>
#include<string.h>
int main(){
FILE* fp=fopen("test.txt","w");
if(!fp){
printf("fopen error!\n");
}
const char msg="hello world\n";
int count=5;
while(count--){
fwrite(msg,strlen(msg),1,fp);
}
close(fp);
return 0;
}
对文件的读写需要用到 fopen、fread、fwrite 等系统底层函数,而用户进程每调用一次系统函数都要从用户态切换到内核态,等执行完毕后再返回用户态,这种切换要花费一定时间成本(对于高并发程序而言,这种状态的切换会影响到程序性能)。
读取文件:
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("test.txt", "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用检测流上的文件结束符:如果遇到文件正常结束,函数返回值为非零值;如果文件异常结束,函数返回值值为0。
C程序在启动的时候会默认启动三个输入输出流,他们分别是:
extern FILE* stdin; //键盘
extern FILE* stdout; //显示器,可以打印到显示器上
extern FILE* stderr; //标准错误,收集错误,也属于显示器
他们三个在代码层面上都是文件指针的类型,也就是*FILE
很多情况下,操作系统中存在着许多被同时打开的文件。这些被打开的文件都是由磁盘打开的,操作系统需要对文件进行管理。
如何管理?
通过某种描述文件属性的数据结构,最后转换成指针之间的映射;从操作文件改为操作指针
文件=属性+内容,
以w打开文件的话,文件如果不存在,就在当前路径下新建指定文件,且默认打开文件的时候会把目标文件先清空
输出重定向伴随着文件操作
理解文件
操作文件的本质是进程在操作文件
文件一开始在磁盘上,本质上在硬件部分的存储
但是用户没有权利直接写入,操作系统是硬件的管理者,用户需要通过操作系统进行写入
操作系统为我们提供系统调用的接口,用的是C/C++对系统调用接口的封装
访问文件也是通过系统调用来访问的
系统如何访问
open:
在
open
系统调用中,flags
是一个由多个标志位组合而成的整数,常见的标志包括:
O_RDONLY
: 以只读模式打开文件。O_WRONLY
: 以只写模式打开文件。O_RDWR
: 以读写模式打开文件。O_CREAT
: 如果文件不存在,则创建文件。需要指定mode
参数。O_EXCL
: 如果文件已经存在,则返回错误(与O_CREAT
一起使用)。O_TRUNC
: 如果文件已存在,并且以写模式打开,则将文件截断为零长度。O_APPEND
: 以追加模式打开文件,将数据写入文件末尾。
系统的接口:
#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);//修改umask
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0)
{
perror("open fail");
return 1;
}
close(fd);//记得关掉
return 0;
}
关闭文件的接口:
open函数中的第二个参数:O_WRONLY | O_CREAT是宏,代表的是打开文件以读取模式
还有别的:O_WRONLY,只写;O_RDWR,读写
open会 返回一个文件描述符(非负整数),用于后续的读写操作。打开文件后,记得使用
在操作系统设计中,系统调用接口可能会用到标志位来指示特定的功能或选项。使用比特位传递标志位可以有效地利用每个整数的多个位,从而在一个整数中存储多个独立的标志位,理解下来其实就像用二进制来表示有还是无
这种做法提高了效率,并减少了需要传递的数据量。例如,一个32位的整数可以用32个位来表示32个不同的标志位,这样只需传递一个整数,就可以传递多个开关状态。
我们也可以自己写一个具有这种传递位图标记位的函数:
#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;
}
可以用标记位组合的方式向一个函数传递多个标记位(比如只传递1,就只打印1;传递1、2、3和4,就启用上面函数的对应部分)
我们把文件打开以后就涉及到写入了
写入的接口函数是: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;
}
执行结果:
注意:当出现这个提示的时候说明文件权限对于你来说没有“写”权限,需要自己chmod设置一下
chmod 664 example.txt//设置权限,即rw-rw-r
不过这种方式不能在上次的基础写,每次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 = "this is first time.\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 file\n";
write(fd, message, strlen(message));
close(fd);
return 0;
}
插播:
vim一直提示我们是否要恢复缓存文件,同时我们也找不到.swp文件,这意味着vim缓存问题
关掉shell重新打开就好了
如果想每次打开的时候清空文件:
#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;
}
注意我们增加了一个参数:O_TRUNC 是open系统调用中的一个标志,用于控制文件在打开时的行为。它的作用是如果文件已经存在,并且以写模式(O_WRONLY或 O_RDWR)打开,则将文件截断为零长度。简而言之, O_TRUNC 会清空文件的内容。
如何在前面的基础上追加呢?
使用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去哪里了?
open默认打开的文件流:
0:标准输入 键盘
1:标准输出 显示器
2:标准错误 显示器
fd的本质是什么?为什么可以通过这个东西写进显示器?
操作系统对于要打开的文件要创建内核数据结构struct_file,使用双链表来管理这些文件
从对文件的管理变为对链表的管理,指向文件内核级的缓存
task_struct
: 代表一个进程,包含一个指向files_struct
的指针。这个指针位于t
task_struct
的files
字段中。
files_struct
: 代表一个进程的文件描述符表,包含一个指向struct file
的指针数组。这个指针数组用于跟踪进程打开的所有文件。
所以我们使用fd的时候其实是内核的进程在映射指针数组的下标
fd(File Descriptor)在这里就叫文件描述符,它是一个非负整数,每个打开的文件或者I/O资源在操作系统中都会对应一个唯一的文件描述符(也就是不同数字对应指针数组下的不同文件)。
无论读,还是写,都要及时让操作系统把文件的内容读到缓冲区
open在这中间做了什么呢?
1.创建文件
2.开辟该文件的缓存空间,加载文件数据
3.获取该进程的fd
4.file地址,填入对应的表下标中
5.返回下标
open的参数有很多个,上文已经提到;在不同的环境要选择不同的参数
fd=0/1/2的时候,指向的是硬件(终端相关联的输入输出设备,这些设备可以被视为硬件资源(例如,键盘和显示器)
//键盘
//函数指针
void (*read)(...)
void (*write)(...)
//函数
void k_read();
void k_write();
//显示器
void screen_read();
void screen_write();
除了键盘显示器,还有:
//鼠标
void mouse_read();
void mouse_write();
面向对象的概念中有类的存在,类是对一类事物抽象出来的概念;类由属性、构造函数和方法组成
这个struct_file这样格式的结构体就是对应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;
}
fileno:将标准I/O库中的文件流(file)转换为底层的文件描述符(fd)。
#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++代码可以编译并在不同平台上运行
所有语言都有跨平台性(当然,跨平台性的实现也有很多种,c/c++是通过库的跨平台性,而java是通过虚拟机等等方法)
因为所有语言都具有跨平台性,所以它们要对不同平台的系统调用进行封装,不同语言进行封装的时候,文件接口就会有差别
在c++中,cin、cout、cerr都被叫做类,这些类中包含了他们自己的文件描述符
当我们打开文件时,操作系统会创建一个结构体(在这里也是一种类),通过描述我们打开的文件的属性和方法来描述文件,这个结构体就是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;
}
stat
是一个系统调用,用于检索文件的元数据。这些信息包括文件的大小、最后一次修改时间、权限、文件类型等。stat
系统调用通常与lstat
和fstat
一起使用,以获取特定类型文件的信息。
#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);//根据stat返回的文件大小申请内存
n = read(fd, file_buffer, st.st_size);//读取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;
}
你看,写进log.txt里了
这就叫重定向
printf/fprintf默认是向stdout中打印的,stdout有对应的struct FILE,里面对应的_fileno==1(也就是文件描述符)
重定向的本质是改变文件描述符下的对应关系
但是我们可以发现如果不加fflush的话是默认没有打印内容的
struct FILE*里有语言级别的缓冲区,所以需要刷新才能打印出来
来看一个函数:dup2
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体验,同时提高整体的效率
用户缓冲区是指程序在用户空间中分配的内存区域,用于存储将要发送或接收的数据。用户空间是指应用程序可以直接访问的内存区域,通常受操作系统的保护,避免直接访问硬件资源。
内核缓冲区是操作系统内核在内核空间中分配的内存区域,用于暂时存储数据以便进行 I/O 操作。内核空间是受保护的内存区域,只有操作系统内核及其受信任的组件可以直接访问。
缓冲区的刷新策略
1.立即刷新:从内核到外设的刷新,比如fflush(stdout),再比如fsync(int fd)...
2.行刷新:一般应用于显示器(\n)
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;
}
当重定向之后变为全缓冲,文件缓冲区并没有被写满,所以会把它暂时保存起来,在stdout对应的缓冲区,在fork时并没刷新,还在缓冲区(因为没写满);而在fork后产生子进程,子进程同时也共享父进程的缓冲区
printf
和fprintf
是缓冲的。这意味着输出的数据首先会被存储在缓冲区中,等到缓冲区满了或者程序正常结束时才会输出。因为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 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
};
我们也可以自己写一个文件操作的代码
mystdio.h
#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 4
struct _myFILE {
unsigned int flags;
int fileno;
char cache[LINE_SIZE];
int cap;
int pos;
};//定义我们的文件结构体,相当于C里的FILE
typedef struct _myFILE myFILE;
ssize_t my_fwrite(myFILE*p,const char* data,int len);
myFILE* my_fopen(const char* path, const char* flag);
void my_fclose(myFILE* fp);
void my_fflush(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);//按字节拷贝,strcpy只能拷贝字符串,但是memcpy可以用于各种数据类型
fp->pos = len;
if ((fp->pos & 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 {
//nothing
}
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;//返回一个myFILE类型的指针
}
void my_fclose(myFILE* fp) {
my_fflush(fp);
close(fp->fileno);
free(fp);
}
test.c
#include"mystdio.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;
}
注意编译的时候要把这两个.c文件一起编译了
先编译mystdio.h,生成一个对象文件.o:
gcc -c -g mystdio.c
然后再一起编译:
gcc -o test.out -g test.c mystdio.o
为什么只有-10和-9,因为我们的buffer限制了写入的大小,超出的只会覆盖
sderr的意义
标准输出重定向,只会改变fd=2的文件
我们输出的信息只有两种:正确/错误
错误的信息就打印到sderr里,这样只需要一次就可以从文件层面把正确信息和错误信息分开了
怎么用?
./a.out 1>ok.txt 2>err.txt
//执行文件 正确信息打入ok.txt 错误信息打到err.txt
./a.out 1>all.txt 2>&1
//执行文件 将信息重定向输出到all.txt 将错误信息也重定向到all.txt里
磁盘
我们的文件在没打开的时候都存在磁盘里,问储存时用0和1表示,但是表现在物理层面不同,比如如在磁盘上的磁性极性变化。
磁盘是一个机械设备,外设,比较慢,性价比高
外设(外部设备)是指连接到计算机主机外部的各种设备,外设通常不包含在计算机的主机(即中央处理器、内存和主板)内部,而是通过各种接口(如USB、HDMI、SATA等)与主机连接。
磁盘本质是机械设备,优点是稳定和便宜
磁盘分为桌面级磁盘(民用,给普通人)和企业级磁盘(通常要求更高的可靠性和性能,适用于服务器和数据中心)
磁道: 磁盘的盘面被划分成一个个磁道。 这样的一个“圈”就是一个磁道
扇区: 一个磁道又被划分成一个个扇区,每个扇区就是一个“磁盘块”。各个扇区存放的数据量相同(如1KB)
盘面,柱面:
清理磁盘的时候就是全部写为0或1,当然也可以物理上烧掉毁掉x
磁盘上读写的基本单位是扇区:512字节,4kb
1盘片上有n个磁道,1磁道有m扇区
文件被存在扇区,怎么找到指定的扇区呢?
先找到对应的磁头(Header),再找到指定的磁道(柱面)(Cylinder),找到指定的扇区
(Sector),这是CHS定址法;体现在物理上就是盘片旋转时就是在定位扇区,磁头摆动就是在定位磁道
文件 = 内容 + 属性
我们的文件是被封装为结构体存在数组里的,像这样:
struct disk_array[N];
每一个磁道都有一百个扇区
磁盘和OS对文件的转换管理:
index / 1000 = H
temp / 100 = C
temp % 100 = S//给文件分区,H、C、S区
index % 1000 = temp; [0, 999]
只要到时候把编号交给磁盘就OK
文件 = 很多个sector(扇区)数组的下标
一般而言,OS和磁盘交互的时候,基本单位是4kb(规定如此)
要有8个sector,8个连续的扇区(块大小)
文件由许多块组成,对于OS来说,读取文件也是以块为单位。所以只要我们知道磁盘总大小,有多少个块,计算块和扇区的转换,然后用扇区进行CHS地址就找到了
除了CHS后,还有别的方法:
逻辑区块地址(LBA,Logical Block Addressing)是一种用于硬盘等存储设备的地址寻址方式。在LBA模式下,存储设备上的数据被分为多个连续的逻辑块,每个块都有一个唯一的编号,称为逻辑区块地址(LBA)。这种寻址方式与传统的CHS(柱面-磁头-扇区)寻址不同,更加简化了磁盘访问和数据管理。
所谓c盘和d盘的区别就是不同的分区,实际上的磁盘只有一块
文件系统
我们每次ll的时候,可以看见文件的信息,从左到右分别是:
文件的性质 文件的硬盘连接数量 文件的拥有者 文件的所属组 文件占的内存 文件最近修改的日期 文件名
这是ll的工作原理
如果我们想查看更多的详细信息,可以使用stat
stat -f /
:用于查看文件系统层级的信息。stat filename
:用于查看具体文件或目录的详细信息。
举个例子:
在Linux下有许多种文件系统,例如:
ext2/ext3/ext4:3是2的进阶版,4是3的进阶版
XFS:一种高性能的64位文件系统,擅长处理大文件,常用于服务器环境。
Btrfs:现代Linux文件系统,具备高级功能如快照、压缩、和子卷等,适合复杂的存储需求。
ReiserFS:一种日志文件系统,以其高效的小文件处理能力著称。
大概就是这些,当然不止这些
我们来解释一下ext2,也就是最基本的早期文件系统:
这是ext2的文件系统图,ext2将磁盘划分为多个块组,一个块组,也就是block的大小是系统格式化化的时候设置的,每个块组都包含以下内容:
启动块(Boot Block)的大小是确定的
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group,每个Block Group都有着相同的结构组成
Block group 0又包含了很多块:
超级块(Super Block):,存储文件系统的全局信息,如总块数、空闲块数、inode数量等。
组描述符(Group Descriptor):描述每个块组的布局和位置。
块位图(Block Bitmap):跟踪每个块是否被分配,用bit位表示块号
inode位图(Inode Bitmap):跟踪块组中每个inode是否已被分配。
inode表(Inode Table):存储文件的元数据(如文件大小、权限、时间戳等)。
数据块(Data Block):存储文件数据,像这样:
int datablocks[15];
其中 Data Block的直接映射的下标是[0,11],12,13是指向其他数据块,为了进行扩容
14指向的是索引,索引指向其他索引
也可以通过间接映射建立更多的文件,但是不建议跨区,磁盘可能会重新选址
将属性和数据分开存放,具体是这样工作的:
查看文件的inode:
inode是什么?每个文件和目录在文件系统中都有一个inode(索引节点),帮助操作系统快速定位和管理文件,但是不包含文件名,相当于文件的身份证(pid是进程的身份证)。
linux下文件属性也是用结构体来描述的:
struct innode
{
int size;
mode_t mode;
int creater;
int time;
...
int innode_number;
...
}
创建文件时,在文件系统中发生了什么?
1.存储属性:先找到一个空闲的inode号分配给文件
2.存储数据:存到块里
3.记录分配情况:内核在inode上的磁盘分布区记录了存储数据的信息(存到哪些块里了?)
4.添加新的文件名到目录
在不同的目录下生成的abc文件的inode不一样
我们的目录=文件属性+文件内容
文件内容是通过inode来映射文件
一个目录下不能建立同名文件,因为文件名在一个目录必须唯一,查找文件就是找到inode编号
目录的r本质是是否允许我们读取目录的内容,文件名指innode的映射关系
目录的w是新建文件,最后要向当前所处的目录内容中写入文件名和innode的映射关系
这是目录相对于其他文件的自己的特性
删除文件的时候,本质上不是真的删除了,而是将位图中的文件属性和内容置为无效
所以文件误删了是可以恢复的,但是很容易被覆盖
所以我们的回收站其实就是一个目录,在删除文件放到回收站的时候,只是把它移到目录里去了
目录也是文件,也有自己的inode,所以在我们使用路径的时候,其实就是操作系统在一层层的找文件的inode(OS会缓存文件的inode),先找到根目录的inode,然后向树状结构一层层的找
ls /dev/vda*//查看我们的系统盘
df -h//查看一台服务器磁盘使用空间
进入分区本质是进入指定的目录
分区:Linux来说无论有几个分区,分给哪一目录使用,他归根结底只有一个根目录,一个独立且唯一的文件结构,Linux中每个分区都是用来组成整个文件系统的一部分。
我们的服务器一般只有一块(小服务器)
在根目录下进行的操作一般都在/dev/vda下进行
在Linux中也可以构建大文件,/dev/zero是设备提供的,这是自己形成了10M的文件:
dd if=/dev/zero of=disk.img bs=1M count=10
格式化:
mkfs.ext4 disk.img
挂载:指的就是将设备文件中的顶级目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。
Linux采用了一种叫"载入"的处理方法,它的整个文件系统中包含了一整套的文件和目录,且将一个分区和一个目录联系起来,这是要载入的一个分区将使它的存储空间在一个,目录下获得。
我们可以试试:
可以看到挂载成功
访问分区有前置内容: 分区-->写入文件系统(格式化)-->挂载到指定的目录下-->进入该目录-->在指定的分区中进行文件操作,这样就可以把磁盘-操作系统-文件系统串在一起啦!
目录是由我们的内核系统提前写入并组织的
Linux内核在被使用的时候,存在大量的解析完毕的路径,要对访问的路径做管理
那要怎么管理捏?
内核有个结构包含路径解析,一个文件一个dentry
struct dentry
{
struct dentry *next;
listnode list;
}