好的代码自己会说话,清晰的逻辑与优雅的结构,是程序员与世界对话的方式。

这是我自己学习Linux系统编程的第五篇笔记。后期我会继续把Linux系统编程笔记开源至博客上。 上一期笔记是关于进程: 【Linux】进程-CSDN博客
https://cloud.tencent.com/developer/article/2530672
1. **文件**存储在 **磁盘**中,而 **磁盘**是一种 **永久性存储介质**,因此文件在磁盘上的保存具有持久性, **断电后数据也不会丢失**。 2. **磁盘**属于 **外部设备**,既可用于 **数据输入**,也可用于 **数据输出**,因此对磁盘文件的所有操作,如读取和写入, **本质上都是对外设的数据传输**,统称为 **IO操作**。
1. **即使是0KB的空文件**, **也会占用磁盘空间**。因为文件不仅包含实际的 **文件数据**,还包含 **文件属性**。 2. 换句话说,文件的本质是 **"文件数据+** **文件属性"**。因此,所有的 **文件操作**,本质上都是对 **文件内容**的操作,或是对 **文件属性**的操作。 3. 对文件的操作,本质上是 **进程在操作系统层面对文件进行访问**。磁盘由操作系统进行统一管理,因此所有对文件的读写操作, **并不是直接由C/C++的标准库函数完成的**,这些库函数只是为用户提供了更便捷的接口。实际上,文件的读写操作最终是通过操作系统提供的 **文件相关系统调用接口**来实现的。
FILE* fopen(char* path,char* mode);
fclose(FILE* fp);
fwrite(void* content, size_t size, size_t num, FILE* fp);
fread(void* content, size_t size, size_t num, FILE* fp);
fprintf(FILE* fp,char* format, ...)使用"w"模式打开文件时,文件内容会被清空,然后从头开始写入新数据,这在功能上等价于Linux中的重定向操作符">"。 使用"a"模式打开文件时,数据会被追加到文件末尾,原有内容不会被清空,这与Linux中的追加重定向操作符">>"的行为一致。 fopen、fclose、fread、fwrite等函数属于C标准库的一部分,我们称之为库函数。4. open、close、read、write等则是操作系统提供的接口,称为系统调用接口。 可以认为,C标准库中的f系列函数本质上是对底层系统调用的封装,目的是提供更友好、便携的接口,方便开发者进行二次开发和跨平台使用。


文件描述符(fd)本质上是一个整数。在C语言中,它被封装在FILE结构体中以便于操作。其中,标准输入(stdin)对应的文件描述符是0,标准输出(stdout)对应1,标准错误(stderr)对应2。 对文件进行任何操作之前,必须先将文件加载到内核中对应的文件缓冲区中。 文件描述符的分配遵循以下规则:在file_struct数组中,找到当前未被使用的最小的一个下标,并将其作为新的文件描述符。

实现输出重定向的关键在于:将文件描述符fd所指向的文件表项复制到下标为1(stdout)的位置。这样一来,原本向1写入的数据就会被写入到fd所指向的文件中。这种操作通常通过系统调用dup2(fd,1)来实现。 追加输出重定向的实现方式与普通输出重定向相同,不同之处在于打开文件时使用了 O_APPEND标志。这样在写入数据时,每次写入的内容都会自动追加到文件末尾,而不会覆盖已有内容。 实现输入重定向的关键在于:将文件描述符fd所指向的文件表项复制到下标为0(stdin)的位置。这样一来,原本从0读取输入的操作就会从fd所指向的文件中读取数据。这种操作通常通过系统调用dup2(fd,0)来实现。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 00644);
printf("fd--->%d\n", fd);
return 0;
}
缓冲区是内存中预留的一块存储空间,用于暂存输入或输出的数据。根据其对应的是输入设备还是输出设备,缓冲区可分为输入缓冲区和输出缓冲区。 在读写文件时,如果没有缓冲区,则每次操作都需要通过系统调用直接访问磁盘,这会导致CPU频繁切换状态并降低效率。采用缓冲机制可以一次性加载大量数据到缓冲区,减少磁盘访问次数,加快数据处理速度。 使用stdio.h库进行输入输出操作时,数据首先进入语言层面的缓冲区,并仅在用户强制刷新或进程正常退出时,才会将这些数据从缓冲区写入文件的内核缓冲区。 在使用C语言库函数进行输入输出操作时:
全缓冲区 | 写满再刷新 |
|---|---|
行缓冲区 | 写满再刷新,遇到换行就刷新 |
无缓冲区 | 没用缓冲区 |
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("hello world!\n");
fflush(stdout);
close(fd);
return 0;
}#include <stdio.h>
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag;//打开方式
int fileno;//文件描述符
char buffer[SIZE];//用户层语言缓冲区
int bufferlen;//缓冲区有效字符个数
int flush_method;
};
typedef struct IO_FILE myfile;
myfile* myfopen(const char* filename,const char* mode);
int myfwrite(const char* ptr,size_t len,myfile* stream);
void myfflush(myfile* stream);
void myclose(myfile* stream);#include "mylibc.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
myfile* myfopen(const char* filename,const char* mode)
{
int fd=0,flag=0;
if(strcmp(mode,"r")==0)
{
flag=O_RDONLY;
fd=open(filename,O_RDONLY);
}
else if(strcmp(mode,"w")==0)
{
flag=O_CREAT|O_WRONLY|O_TRUNC;
fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC, 0666);
}
else if(strcmp(mode,"a")==0)
{
flag=O_CREAT|O_WRONLY|O_APPEND;
fd=open(filename,O_CREAT|O_WRONLY|O_APPEND, 0666);
}
myfile* my=(myfile*)malloc(sizeof(myfile));
my->fileno=fd;
my->flag=flag;
my->bufferlen=0;
my->flush_method=FLUSH_LINE;
memset(my->buffer,0,sizeof(SIZE));
return my;
}
int myfwrite(const char* ptr,size_t len,myfile* stream)
{
memcpy(stream->buffer+stream->bufferlen,ptr,len);
stream->bufferlen+=len;
if(stream->flush_method==FLUSH_LINE && stream->buffer[stream->bufferlen-1]=='\n')
myfflush(stream);
return len;
}
void myfflush(myfile* stream)
{
if(stream->bufferlen > 0)
{
write(stream->fileno,stream->buffer,stream->bufferlen);
fsync(stream->fileno);
}
stream->bufferlen=0;
}
void myclose(myfile* stream)
{
myfflush(stream);
close(stream->fileno);
free(stream);
}#include "mylibc.c"
int main()
{
myfile* my=myfopen("log.txt","a");
char* msg="hello world!\n";
myfwrite(msg,strlen(msg),my);
myclose(my);
return 0;
}

LBA地址:是一种通过线性编号直接访问硬盘上数据块的方法,简化了大容量存储设备的管理和访问。 CHS地址:是一种基于硬盘物理结构,使用柱面、磁头和扇区三个参数来精确定位数据位置的传统寻址方式。 磁盘CHS定址过程:若要向磁盘写入数据,首先需要移动磁头至对应柱面,然后等待盘片旋转,使磁头对准目标扇区的起始位置,方可进行数据的读取或写入操作。
一块磁盘可以被划分为多个"分区"。从Windows的角度来看,会将一块磁盘划分为C盘、D盘、E盘等,这些盘符对应的就是不同的分区。 磁盘的每个分区被划分为多个"块"。其大小在格式化时确定且不可更改,最常见的大小为4KB,即由连续的八个512字节的扇区组成一个"块"。 磁盘作为典型的块设备,其数据读取方式并非以扇区为单位逐个进行,而是由操作系统按块批量读取,以此提升I/O效率和整体性能。

1. 在 **ext2文件系统**中,根据分区大小会被划分为 **若干个块组(Block Group)** ,每个块组都具有相同的结构组成。 2. **Inode和数据块跨组但不跨分区** 。因此在同一个分区内部, **Inode和数据块都是唯一的** 。 每个块组的开头都有一份超级块的副本,但只有第一个块组的超级块是必须存在的,其他块组可以没有。这是为了防止单个扇区损坏导致整个文件系统无法使用。 分区完成后的 格式化操作,是对该分区进行分组,并在每个块组中写入超级块、块组描述符表、块位图、Inode 位图等管理信息。 Data Block:用于存放文件的实际内容,由一个个数据块组成。 Inode Table:用于存储文件的属性信息,包括文件大小、所有者、权限、时间戳等属性。 Block Bitmap :用于追踪数据块的使用状态,它记录了哪些数据块已被占用,以及哪些数据块仍处于空闲状态。 Inode Bitmap :用于指示每个Inode的分配状态,其中每一位表示一个Inode是否空闲可用。 GDT : 用于记录每个块组的属性信息。当一个分区被划分为多个块组时,每个块组都会对应一个块组描述符,每个块组描述符包含了该块组的数据信息:

由于每个分区拥有独立的Inode和数据块,所以只需知道Inode编号,就能在分区内确定其所在的组号和具体位置。然后,通过Inode中记录的映射关系,可以找到存储文件数据的具体数据块。 目录本质上也是一种文件,但在磁盘上并不存在“目录”这一特定概念,只有文件属性和文件内容的区分。目录的属性与其他文件类似,其内容保存的是该目录中的文件名与Inode号之间的映射关系。因此,访问一个文件时,必须能够打开当前所在目录。具体来说,就是需要打开该目录对应的目录文件,根据其中保存的文件名与 Inode 号的映射关系,找到目标文件的 Inode,进而完成文件的访问。
ln -s xxx yyy:创建一个指向文件xxx的软链接yyy。软链接是一种特殊的文件类型,它作为一个独立的实体存在,拥有自己独立的inode编号。 软链接的内容实际上是其所指向的目标文件或目录的路径。这意味着当你访问软链接时,系统会自动重定向到该链接所指向的实际文件或目录。
ln xxx yyy:创建一个文件xxx的硬链接yyy。硬链接是指向同一个inode的另一个文件名。这意味着多个文件都指向存储在磁盘上的同一份实际数据。 硬链接具有以下性质:
感谢您花时间阅读这篇文章!如果您对本文有任何疑问、建议或是想要分享您的看法,请不要犹豫,在评论区留下您的宝贵意见。每一次互动都是我前进的动力,您的支持是我最大的鼓励。期待与您的交流,让我们共同成长,探索技术世界的无限可能!