理解 Linux 文件结构:一份简单易懂的入门教程
个人主页:chian-ocean
文章专栏-Linux
前言:
Linux 文件系统是指 Linux 操作系统用于组织和管理文件、目录及其元数据(如权限、时间戳等)的系统。文件系统定义了文件的存储、访问和管理的方式,并提供了数据持久性和组织结构。
C语言文件操作
C语言文件
-
FILE*
是一个指向文件的结构体,通过它,程序可以与文件进行交互。 -
文件指针由标准库函数如
fopen()
创建和返回,文件的读写操作通过这个指针来执行。 -
文件指针常用于文件操作函数,如
fopen()
、fread()
、fwrite()
、fclose()
等。
#include <stdio.h>
#include <string.h>
int main()
{
// 打开名为 "log.txt" 的文件以进行写入("w" 模式)
FILE* fd = fopen("log.txt", "w"); //路径默认在当前工作路径
if (fd == NULL) // 如果fopen失败(例如文件无法打开)
{
perror("fopen"); // 输出错误信息,指示fopen失败
return 1; // 返回错误代码(1),表示程序失败
}
const char* msg = "hello linux!\n"; // 定义要写入文件的字符串消息
// 使用strlen计算消息长度,并将消息写入文件
fwrite(msg, strlen(msg), 1, fd);
fclose(fd); // 写入完成后关闭文件
return 0; // 返回0,表示程序成功执行
}
理解当前工作路径:
- 在进程文件读写的时候会进程会记录当前的工作目录(
cwd
),如图:在/home/ocean/linux/file/filetest
路径下创建文件。
- 我们在代码上加上
chdir("/home/ocean/linux/file");
就会更改当前的工作路径,如图查看到的cwd
是: /home/ocean/linux/file
再次执行代码后文件会创建在 /home/ocean/linux/file
这个路径下
文件打开方式
r
- 打开文件进行读取,文件指针定位在文件的开始位置。
- 如果文件不存在,打开失败。
r+
- 打开文件进行读取和写入,文件指针定位在文件的开始位置。
- 如果文件不存在,打开失败。
w
- 打开文件进行写入,如果文件存在,则截断文件至零长度;如果文件不存在,创建文件。
- 文件指针定位在文件的开始位置。
w+
- 打开文件进行读取和写入,如果文件不存在,创建文件;如果文件存在,截断文件至零长度。
- 文件指针定位在文件的开始位置。
a
- 打开文件进行附加写入,写入的内容会追加到文件的末尾;如果文件不存在,创建文件。
- 文件指针定位在文件的末尾。
a+
- 打开文件进行读取和附加写入,读取时从文件的开始位置开始,写入时追加到文件末尾;如果文件不存在,创建文件。
本质上没有必要记那么多,如果需要直接去看官方的文档即可
文件的系统调用
在操作系统中,文件操作通常通过系统调用(system calls
)进行。这些系统调用直接与操作系统的内核交互,以进行文件的创建、读取、写入、删除等操作。
open()
- 功能:打开文件,返回文件描述符。
- 语法:
int open(const char *pathname, int flags, mode_t mode);
- 参数:
pathname
:要打开的文件路径。flags
:指定文件的打开模式(如只读、写入、追加等)。mode
:文件权限,如果文件是新建的,指定文件权限。
- 返回值:成功返回文件描述符(一个非负整数),失败返回 -1,并设置
errno
。
理解flags
flags
参数允许通过按位“或”运算符(|
)将多个标志组合在一起。这样,可以灵活地指定多种行为。例如,你可以使用 O_WRONLY | O_CREAT | O_TRUNC
来实现“以写模式打开文件,如果文件不存在则创建文件,如果文件已存在则截断文件”的行为。
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
为什么会用 |
呢
#include <stdio.h>
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
// 用于根据标志位显示相应功能的函数
void show (int flags)
{
// 检查 flags 的每一位,如果相应的位被设置,则输出对应的信息
if(flags & ONE) cout << "func 1" << endl; // 如果 flags 的最低位(0)被设置,则输出 "func 1"
if(flags & TWO) cout << "func 2" << endl; // 如果 flags 的第二位(1)被设置,则输出 "func 2"
if(flags & THREE) cout << "func 3" << endl; // 如果 flags 的第三位(2)被设置,则输出 "func 3"
if(flags & FOUR) cout << "func 4" << endl; // 如果 flags 的第四位(3)被设置,则输出 "func 4"
}
int main()
{
// 调用 show 函数,传递不同的 flags,分别测试不同的功能组合
show(ONE | TWO); // 传递 ONE | TWO,即 flags = 0001 | 0010 = 0011 -> func 1 和 func 2
show(ONE | THREE); // 传递 ONE | THREE,即 flags = 0001 | 0100 = 0101 -> func 1 和 func 3
show(ONE | TWO | THREE); // 传递 ONE | TWO | THREE,即 flags = 0001 | 0010 | 0100 = 0111 -> func 1, func 2 和 func 3
return 0; // 程序正常结束
}
代码解释:
- 宏定义:使用
#define
来定义常量ONE
、TWO
、THREE
和FOUR
,它们分别表示不同的二进制位:ONE
代表二进制的第 1 位(0001
)。TWO
代表二进制的第 2 位(0010
)。THREE
代表二进制的第 3 位(0100
)。FOUR
代表二进制的第 4 位(1000
)。
show
函数:这个函数接受一个整数flags
,然后检查每一位是否被设置。如果某一位被设置(即该位为1
),则打印相应的功能名:- 使用按位与操作符
&
来检查每个标志位是否被设置。 - 如果某个标志位被设置,
if (flags & ONE)
等判断条件为true
,执行相应的cout
输出。
- 使用按位与操作符
main
函数:调用show()
函数并传递不同的flags
值,展示不同的功能组合:show(ONE | TWO)
:将ONE
和TWO
进行按位“或”运算,得到0011
,所以会显示"func 1"
和"func 2"
。show(ONE | THREE)
:将ONE
和THREE
进行按位“或”运算,得到0101
,所以会显示"func 1"
和"func 3"
。show(ONE | TWO | THREE)
:将ONE
、TWO
和THREE
进行按位“或”运算,得到0111
,所以会显示"func 1"
、"func 2"
和"func 3"
。
生成:
这个大致就是flag的 |
的本质
常用的falg
:
Flag | Description |
---|---|
O_RDONLY | 以只读方式打开文件 |
O_WRONLY | 以只写方式打开文件 |
O_RDWR | 以可读可写方式打开文件 |
O_CREAT | 如果文件不存在,则创建文件 |
O_TRUNC | 如果文件已存在,则将文件内容截断为零长度 |
O_APPEND | 以追加模式打开文件(所有写入都添加到文件末尾) |
理解mode
详细:参考
在 open()
函数中,mode
参数是用于指定新创建文件的权限和访问控制。该参数仅在使用 O_CREAT
标志时才需要提供,表示当文件不存在时,open()
会创建新文件并根据 mode
参数设置文件的权限。mode
参数通常是一个三位八进制数字,表示文件所有者、文件所属组和其他用户的权限。
mode
参数的基本结构
mode
参数采用 三位八进制数 的形式,表示文件的权限。每一位代表不同用户类别的访问权限,分别是:
- 第一位:所有者(Owner)的权限 4
- 第二位:所属组(Group)的权限 2
- 第三位:其他用户(Others)的权限 1
权限(八进制) | 权限描述 | 权限组合 |
---|---|---|
777 | 所有用户具有读、写、执行权限 | rwxrwxrwx |
755 | 所有者具有读、写、执行权限,组和其他用户有读、执行权限 | rwxr-xr-x |
644 | 所有者具有读、写权限,组和其他用户只有读取权限 | rw-r--r-- |
600 | 所有者具有读、写权限,组和其他用户没有权限 | rw------- |
write
- 功能:将数据写入文件。
- 语法:
ssize_t write(int fd, const void *buf, size_t count);
- 参数:
fd
:文件描述符。buf
:缓冲区,包含要写入的数据。count
:要写入的字节数。
- 返回值:返回实际写入的字节数,或返回
-1
表示错误。
close()
- 功能:关闭打开的文件。
- 语法:
int close(int fd);
- 参数:
fd
:文件描述符。
- 返回值:成功返回 0,失败返回
-1
示例:
#include <stdio.h> // 引入标准输入输出库,用于printf和perror等
#include <string.h> // 引入字符串处理库,用于处理字符串(如strlen)
#include <unistd.h> // 引入POSIX标准库,用于访问系统级函数(如write和close)
#include <sys/types.h> // 引入定义文件操作需要的类型
#include <sys/stat.h> // 引入文件状态相关定义
#include <fcntl.h> // 引入文件控制函数,如open
int main()
{
// 使用 O_CREAT、O_TRUNC 和 O_WRONLY 打开文件 'log.txt'
// O_CREAT:如果文件不存在,创建文件
// O_TRUNC:如果文件已经存在,清空文件内容
// O_WRONLY:只写模式打开文件
// 0666:设置文件权限,所有用户都有读写权限
int fd = open("log.txt", O_CREAT | O_TRUNC | O_WRONLY, 0666);
// 检查文件是否成功打开,如果返回值小于0,表示打开失败
if (fd < 0)
{
perror("open"); // 如果打开文件失败,输出错误信息
return -1; // 返回错误代码,程序退出
}
// 定义要写入文件的字符串
const char* str = "hello linux\n";
// 定义要写入文件的次数
int cnt = 5;
// 循环写入文件5次
while (cnt--)
{
// 每次调用 write() 写入字符串到文件
write(fd, str, strlen(str));
}
// 关闭文件
close(fd);
return 0; // 正常退出程序
}
代码解释:
open()
函数:使用O_CREAT
、O_TRUNC
和O_WRONLY
标志打开文件log.txt
。如果文件不存在,将会创建文件;如果文件已存在,使用O_TRUNC
将文件内容清空。文件权限设置为0666
,即所有用户都可以读写该文件。- 错误处理:如果
open()
调用失败,会返回负值,使用perror("open")
输出错误信息并退出程序。 write()
函数:将字符串"hello linux\n"
写入文件。写入操作会在文件fd
中进行,strlen(str)
确保写入的字符数是字符串的实际长度。- 文件操作:代码通过
write()
循环写入文件 5 次,每次写入字符串"hello linux\n"
。 close()
函数:在完成写入后,通过close(fd)
关闭文件。
这份代码类似于fopen 、fwrite 、fclose,进行文件写入,这里面运用系统调用,但是 f
系类运用的是C语言函数库中函数,是对上述系统调用的再次封装。
文件描述符
文件描述符(File Descriptor) 是操作系统用来表示打开文件的一个整数标识符。它是一个指向内核中文件对象的引用,用于标识进程对文件的访问。每个进程都有一个文件描述符表,记录着当前进程打开的文件及其相关信息。
标准输入、标准输出、标准错误:
- 每个进程启动时,操作系统会为它创建三个标准的文件描述符:
0
:标准输入(stdin
),通常用于接收输入。1
:标准输出(stdout
),通常用于显示输出。2
:标准错误(stderr
),用于输出错误信息。
文件描述符的分配: 当进程通过 open()
打开文件时,操作系统会分配一个文件描述符,该文件描述符对应于该文件的内核级数据结构。文件描述符是进程与操作系统文件系统之间的桥梁,进程通过它进行文件操作(如读取、写入)。
任务结构 task_struct
:
task_struct
是 Linux 内核中表示进程的数据结构,每个正在运行的进程都有一个对应的task_struct
。- 图中提到的
struct files_struct *files
表示每个进程都有一个files_struct
,它包含了该进程打开的所有文件描述符。
文件描述符数组 fd_array[]
:
fd_array[]
是一个数组,里面保存了指向文件结构(struct file
)的指针。每个struct file
代表着一个已打开的文件。- 例如,
fd_array[0]
代表标准输入(stdin),fd_array[1]
代表标准输出(stdout),fd_array[2]
代表标准错误输出(stderr)。 - 数组中的其他位置保存着该进程打开的其他文件。
struct file
结构:
- 每个
struct file
结构体代表一个具体的文件,它包含了文件的状态信息,比如文件的当前偏移量、文件访问模式、权限等。 - 这些文件结构会通过
next
指针形成一个链表,允许操作系统管理多个文件。
所以0 1 2 对应标准输入、输出、错误流 正好与之对应
0号文件
#include <iostream> // 引入输入输出流库,用于标准输入输出操作
#include <string.h> // 引入字符串操作库(如 strlen、memcpy 等)
#include <unistd.h> // 引入 UNIX 标准头文件,包含对系统调用(如 read、write 等)的定义
#include <sys/types.h> // 引入定义系统数据类型的库,如 pid_t、off_t 等
#include <sys/stat.h> // 引入文件状态信息定义的库(如文件权限、文件类型等)
#include <fcntl.h> // 引入文件控制操作库,包含文件操作的标志,如 O_CREAT、O_WRONLY 等
using namespace std; // 使用标准命名空间,避免每次使用标准库的元素时都要加上 "std::"
int main () // 主函数
{
// 读取标准输入的数据,最多读取 100 个字节,存入缓冲区 buf 中
char buf[1024]; // 定义一个字符数组来存储读取的数据
ssize_t size = read(0, buf, 100); // 0 代表标准输入(stdin)
buf[size] = '\0'; // 添加字符串结束符,确保读取的数据是一个有效的 C 风格字符串
close(fd); // 关闭打开的文件描述符,释放资源
cout << buf << endl; // 输出读取到的内容到标准输出(屏幕)
return 0; // 程序正常结束,返回 0
}
代码功能概述:
- 读取标准输入的数据:
- 从标准输入(如键盘)读取最多 100 个字节的数据,存储到缓冲区
buf
中。
- 从标准输入(如键盘)读取最多 100 个字节的数据,存储到缓冲区
- 输出读取的数据:
- 输出读取到的数据内容。
1号文件
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() // 主函数
{
// 定义要写入文件的字符串
const char* str = "hello linux\n"; // 这个字符串将被写入输出(标准输出)
// 定义要写入的次数
int cnt = 5; // 设置写入的次数为 5
// 循环写入文件 5 次
while (cnt--) // 当 cnt 不为 0 时,循环执行写入操作
{
// 每次调用 write() 将字符串写入标准输出(文件描述符 1 对应标准输出)
write(1, str, strlen(str)); // write() 的参数:1 表示标准输出,str 表示要写入的内容,strlen(str) 表示字符串长度
}
return 0; // 程序执行完毕,返回 0 表示成功
}
代码功能概述:
- 定义字符串
str
:
- 程序定义了一个常量字符串
"hello linux\n"
,它将被重复写入标准输出(通常是屏幕)。
- 定义写入次数
cnt
:
cnt
初始化为 5,表示要将字符串写入标准输出 5 次。
- 循环写入标准输出:
- 使用
write()
系统调用将str
内容输出到标准输出(文件描述符 1)。 write(1, str, strlen(str))
通过write
系统调用将字符串的每次输出写入到标准输出。- 每次循环调用
write()
,直到cnt
达到 0。
output: