Linux:基础IO

引言:

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_structfiles 字段中。

  • 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 系统调用通常与 lstatfstat 一起使用,以获取特定类型文件的信息。

#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后产生子进程,子进程同时也共享父进程的缓冲区

printffprintf 是缓冲的。这意味着输出的数据首先会被存储在缓冲区中,等到缓冲区满了或者程序正常结束时才会输出。因为 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值