Linux -- 文件【上】

一、定义

1、狭义理解

  • 文件在磁盘里
  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设(即是输出设备也是输入设备)
  • 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出简称IO。

2、广义理解

  • Linux下一切皆文件(键盘、显示器、网卡、磁盘...这些都是抽象化的过程)。

3、文件操作的归类认知

  • 对于0KB的空文件是占用磁盘空间的
  • 文件是文件属性(元数据)和文件内容的集合(文件=属性(原数据)+内容)
  • 所有的文件操作本质是文件内容操作和文件属性操作

4、系统角度

  • 对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统
  • 文件的读写本质不是通过C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的

二、C语言文件操作

C语言文件操作接口如下:

|------------|---------------------|
| 文件操作函数 | 功能 |
| fopen | 打开文件 |
| fclose | 关闭文件 |
| fputc | 写入一个字符 |
| fgetc | 读取一个字符 |
| fputs | 写入一个字符串 |
| fgets | 读取一个字符串 |
| fprintf | 格式化写入数据 |
| fscanf | 格式化读取数据 |
| fwrite | 向二进制文件写入数据 |
| fread | 从二进制文件读取数据 |
| fseek | 设置文件指针的位置 |
| ftell | 计算当前文件指针相对于起始位置的偏移量 |
| rewind | 设置文件指针到文件的起始位置 |
| ferror | 判断文件操作过程中是否发生错误 |
| feof | 判断文件指针是否读取到文件末尾 |

读写方式如下:

|------------|----------------------|---------------|
| 文件使用方式 | 含义 | 如果指定文件不存在 |
| "r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
| "w"(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
| "a"(追加) | 向文本文件尾添加数据 | 出错 |
| "rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
| "wb"(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
| "ab"(追加) | 向一个二进制文件尾添加数据 | 出错 |
| "r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
| "w+"(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
| "a+"(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
| "rb+"(读写) | 为了读和写打开一个二进制文件 | 出错 |
| "wb+"(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
| "ab+"(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |

下面是一个使用C语言文件的示例:

cpp 复制代码
#include<stdio.h>
#include<string.h>

int main()
{
	FILE *fp = fopen("log.txt", "w");
	if(fp == NULL)
	{
		perror("fopen");
		return 1;
	}

	const char *msg = "hello tata: ";
	int cnt = 1;
	while(cnt <= 10)
	{
		char buffer[1024];
		snprintf(buffer, sizeof(buffer), "%s%d", msg, cnt++);
		fwrite(buffer, strlen(buffer), 1, fp);
	}

	fclose(fp);

	return 0;
}

一般而言如果没有定义对应的log.txt文件,系统会在当前路径自动创建该文件。并且当前路径并不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。比如我们可以在上级目录执行myfile文件:

可以看见log.txt是在该对应路径创建的,而不是对应可执行文件所在目录创建的。

其中我们也可以通过监视进程的方式(ls/proc/[进程id] -l 命令)观察一下:

然后我们可以看见两个软连接cwd和exe,分别对应的就是进程运行时我们所处的路径 ,以及可执行文件所处路径。


三、stdin & stdout & stderr

我们常说Linux下一切皆文件,那么我们的键盘与显示器自然也是文件。我们向键盘输入数据,本质就是操作系统向键盘文件中读取数据;我们能从显示器看见数据,本质就是操作系统向显示器文件写入数据。但是我们在使用键盘与显示器时并没有手动进行任何文件相关的读写操作,那我们又是如何对键盘文件与显示器文件进行读写的呢?

答案自然是操作系统自动帮我们打开的,任何进程在运行时,操作系统都会默认打开三个输入输出流,分别为:标准输入流 ,标准输出流 以及标准错误流 。对于C语言分别就是:stdin、stdout以及stderr。对于C++分别就是:cin、cout和cerr,自然其他语言也会有相似的概念,因为这是操作系统所支持的,而不是某个语言所独有的。

我们可以在Linux中的man查看对应的声明:

其中标准输入流对应的就是我们的键盘,而标准输出流与标准错误流对应的就是我们显示器。

这三个流的类型都是FILE*的文件指针:

cpp 复制代码
 #include <stdio.h>
 extern FILE *stdin;
 extern FILE *stdout;
 extern FILE *stderr;

其中我们也可以通过fputs函数验证一下:

我们再以w方式打开已存在的log.txt文件,观察一下会发生什么:


四、系统文件I/O

在前面我们学习操作系统时知道,为了方便用户使用,一般我们会对系统接口进行封装。我们的文件操作也不例外,像fopen,fclose等接口本质其实对操作系统提供的文件接口的封装。接下来我们就来学习一下系统提供的文件接口。

1、open函数

首先我们来介绍文件打开操作的系统接口。

  • pathname:表示打开或者创建的目标文件,若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。
  • flags:表示打开文件的方式。
  • mode:表示创建文件的默认权限(八进制数)。

open函数的返回值 是一个整数,表示文件描述符 fd,失败返回-1。

其中常用文件打开方式有如下几个:

参数选项 含义
O_RDONLY 以只读的方式打开文件
O_WRNOLY 以只写的方式打开文件
O_APPEND 以追加的方式打开文件
O_RDWR 以读写的方式打开文件
O_CREAT 当目标文件不存在时,创建文件
O_TRUNC 打开文件前,先清空文件
O_EXCL 确保文件不存在,与O_CREAT一起用时,如果文件已经存在,打开操作会失败
O_SYNC 同步写入,写入操作会立即同步到磁盘,而不是缓存
O_NONBLOCK 非阻塞模式,对于某些设备文件,操作不会阻塞

如果想同时兼具多个打开方式,可以使用逻辑与|链接两个选项。比如说我们想打开文件并且文件不存在时创建文件,可以写成:

cpp 复制代码
O_WRNOLY|O_CREAT

这些选项本质也就是一个宏定义,其中flags是一个整型,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。

所以我们也可以使用按位与&操作来检测是否设置某个选项:

cpp 复制代码
if (flags&O_RDONLY){
    //设置了O_RDONLY选项
}
if (flags&O_WRONLY){
    //设置了O_WRONLY选项
}
if (flags&O_RDWR){
    //设置了O_RDWR选项
}
if (flags&O_CREAT){
    //设置了O_CREAT选项
}
//...

并且如果我们打开的文件已存在就使用第一个接口(两个参数),如果打开的文件不存在就需要使用第二个接口(三个参数),即需要为创建的文件设置默认权限。

如果我们要为文件设置默认权限,就需要考虑文件默认掩码umask的影响。我们之前讲过文件的默认权限为:mode&(~mask),我们除了可以在命令行通过指令umask 八进制数来修改默认的掩码umask(默认为002)外,还能在程序中调用umask函数进行修改。比如我们将umask设置为0:

cpp 复制代码
umask(0); //将文件默认掩码设置为0

最后再来探究一下open的返回值,也就是文件描述符fd。

cpp 复制代码
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    umask(0);//设置文件掩码为0
    int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
	printf("fd1:%d\n", fd1);
	printf("fd2:%d\n", fd2);
	printf("fd3:%d\n", fd3);
	printf("fd4:%d\n", fd4);
	printf("fd5:%d\n", fd5);
    return 0;
}

运行之后我观察到文件描述符是从3开始的,并且依次递增,这其实并不是偶然。

当然这只是文件成功返回的情况,如果文件打开失败,那将返回-1。

2、close函数

我们可以调用系统接口close来关闭指定文件,其原型为:

int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

3、write函数

同样我们也能通过系统接口write对文件进行写入,其原型为:

ssize_t write(int fd, const void *buf, size_t count);

其中fd指的是文件描述符,buf为用户缓冲区,而count为期望写的字节数。如果写入成功返回实际写入的字节数,若写入失败则返回-1。

注意:ssize_t其实就是一个有符号整型,具体来说就是被typedef重新定义过:typedef int ssize_t

以下我们可以利用write函数对一个log.txt文件进行写入:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd=open("log.txt",O_WRONLY|O_CREAT);
    if(fd<0)
    {
        //open error
        perror("open fail:");
        return 1;
    }
    const char*msg="hello tata!\n";
    for(int i=0;i<8;i++)
    {
        write(fd,msg,strlen(msg));
    }
    close(fd);
    return 0;
}

4、read函数

同样我们也能通过系统接口read对文件进行读写,其原型为:

ssize_t read(int fd, void *buf, size_t count);

其中fd指的是文件描述符,buf为用户缓冲区,而count为期望读的字节数。如果读出成功返回实际读出的字节数,若读出失败则返回-1。

以下我们可以利用read函数对一个log.txt文件进行读出:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd=open("log.txt",O_RDONLY);
    if(fd<0)
    {
        perror("open fail:");
        return 1;
    }
    char buf[1024]={'\0'};
    ssize_t ret=read(fd,buf,1023);
    if(ret>0)
    printf("%s",buf);
    close(fd);
    return 0;
}

5、文件描述符 fd

在我们的操作系统中,文件是由我们进程所打开的,存在大量进程就意味着存在大量被打开的文件。为了方便我们对文件进行管理,我们就将每个文件struct file链入我们的双向链表之中。

cpp 复制代码
struct File
{
  //包含了打开文件的相关属性
  //链接属性
};

而一个文件也可能被多个进程所读写,为了让操作系统能够准确识别每个进程对应的文件,我们就一定要让进程与我们的文件建立联系。事实也是如此,我们的进程控制块task_struct中就存在一个指针指向一个名为struct file_struct的结构体,这个结构体中存在一个结构体指针数组struct file*fd_array\[\]分别存放着着每个文件struct file的地址。这样我们的进程就与文件建立起了联系。

一般我们的指针数组struct file*fd_array\[\]的0,1,2下标分别对应我们的标准输入流,标准输出流,标准错误流这三个文件,而这些下标就是我们所说的文件描述符 -- fd 。这也解释了我们打开文件的描述符为什么从3开始,并且依次递增。并且,通过对应的文件描述符,进程只需要找到对应的指针数组fd_array就能访问对应的文件,这也是为什么我们文件的系统调用接口的参数一定会有fd的原因。

当然如果我们在中途关掉某个文件,操作系统就会为该下标重新分配对应的文件。

cpp 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
    close(0);
    close(2);
	int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
	printf("fd1:%d\n", fd1);
	printf("fd2:%d\n", fd2);
	printf("fd3:%d\n", fd3);
	printf("fd4:%d\n", fd4);
	printf("fd5:%d\n", fd5);
	return 0;
}

我们也知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。如果与我们的文件管理联系起来,就是一个磁盘文件log.txt加载进内存形成内存文件,最后加入对应双向链表中管理起来。

当文件存储在磁盘上时,我们称之为磁盘文件 。而当磁盘文件被加载到内存中后,就变成了内存文件。磁盘文件与内存文件的关系,恰似程序和进程的关系。程序在运行起来后成为进程,同样,磁盘文件在加载到内存后成为内存文件。磁盘文件主要由两部分构成,即文件内容 和文件属性 。文件内容指的是文件中存储的数据,而文件属性则是文件的一些基本信息,包括文件名、文件大小以及文件创建时间等。这些文件属性也被称为元信息。在文件加载到内存的过程中,一般会先加载文件的属性信息。这是因为在很多情况下,我们可能只需要了解文件的基本属性,而不一定立即需要对文件内容进行操作。当确实需要对文件内容进行读取、输入或输出等操作时,才会延后式地加载文件数据。这样的设计可以提高系统的效率,避免在不必要的时候浪费资源加载大量的文件数据。

5.1 文件描述符的分配规则

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 int main()
 {
	 int fd = open("myfile", O_RDONLY);
	 if(fd < 0){
	 perror("open");
	 return 1;
	 }
	 printf("fd: %d\n", fd);
	 }
	 close(fd);
	 return 0;
}

输出发现是fd:3。关闭0或者2,再看:

cpp 复制代码
#include <stdio.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 int main()
 {
	 close(0);
	 //close(2);
	 int fd = open("myfile", O_RDONLY);
	 if(fd < 0){
	 perror("open");
	 return 1;
	 }
	 printf("fd: %d\n", fd);
	 close(fd);
	 return 0;
 }

发现是结果是:fd:0或者fd:2。可见:

文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

5.2 重定向原理

那如果关闭1呢?看代码:

cpp 复制代码
#include <stdio.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <stdlib.h>
 int main()
 {
	 close(1);
	 int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
	 if(fd < 0){
	 perror("open");
	 return 1;
	 }
	 printf("fd: %d\n", fd);
	 fflush(stdout);
	 close(fd);
	 exit(0);
 }

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile当中,其中,fd=1。这种现象叫做输出重定向 。常见的重定向有:>,>>,<。

那重定向的本质是什么呢?

5.3 dup2系统调用重定向

函数原型如下:

  • 原型:
  • 函数功能:dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先关闭文件描述符为newfd的文件。
  • 函数返回值: 如果调用成功,返回newfd,否则返回-1。

使用dup2函数时,需要注意以下两点:

  • 如果oldfd不是有效的文件描述符,dup2就会调用失败,此时文件描述符为newfd的文件没有被关闭。
  • 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("./log", O_CREAT | O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    close(1);
    dup2(fd, 1);
    for (;;) {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0) {
            perror("read");
            break;
        }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

printf是C库当中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd: 1,但此时,fd: 1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

那追加和输入重定向如何完成呢?

5.4 在myshell中添加重定向功能

  • 先定义一个整数记录重定向方式 一个字符串记录重定向文件
  • TrimSpace跳过重定向字符后的空格 指向文件名开始位置
  • 每次调用RedirCheck先清空文件和让文件描述符为0
  • 然后从后向前查找 > >> <
  • 每次让命令行参数表清空重定及其之后的内容 设置为\0即可 命令正常执行
  • 然后根据找到的符号 调用跳过空格函数 在让文件名指向end之后的内容
  • 设置重定向方式即可
  • Execute根据redir 调用open打开文件 然后dup2重定向即可
  • 之后再让子进程正常执行即可

问题:进程替换会不会影响重定向结果?

不会,因为进程替换只是替换程序的代码和数据,而文件结构体和文件描述符等内核数据结构不受到影响。

cpp 复制代码
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
int redir = NONE_REDIR;
std::string filename;
void RedirCheck(char cmd[])
{
	redir = NONE_REDIR;
	filename.clear();
	int end = strlen(cmd)-1;
	while (end > 0)
	{
		if (cmd[end] == '>')
		{
			if (cmd[end - 1] == '>')
			{
				cmd[end - 1] = 0;
				redir = APPEND_REDIR;
			}
			else
			{
				cmd[end] = 0;
				redir =OUTPUT_REDIR;
			}
			TrimSpace(cmd, ++end);
			filename = cmd + end;
			break;
		}
		else if (cmd[end] == '<')
		{
			cmd[end] = 0;
			redir = INPUT_REDIR;
			TrimSpace(cmd, ++end);
			filename = cmd + end;
			break;
		}
		else
		{
			end--;
		}
	}
}
int Execute()
{
	pid_t id = fork();
	if (id == 0)
	{
		int fd = -1;
		// 子进程检测重定向情况
		if (redir == INPUT_REDIR)
		{
			fd = open(filename.c_str(), O_RDONLY);
			if (fd < 0) exit(1);
			dup2(fd, 0);
			close(fd);
		}
		else if (redir == OUTPUT_REDIR)
		{
			fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
			if (fd < 0) exit(2);
			dup2(fd, 1);
			close(fd);
		}
		else if (redir == APPEND_REDIR)
		{
			fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
			if (fd < 0) exit(2);
			dup2(fd, 1);
			close(fd);
		}
		execvp(g_argv[0], g_argv);
		exit(1);
	}
	int status = 0;
	pid_t rid=waitpid(id, &status, 0);
	if (rid > 0)
	{
		lastcode = WEXITSTATUS(status);
	}
	return 0;
}
int main()
{
	//初始化环境变量表
	InitEnv()**加粗样式**;
	while (1)
	{
		//打印命令行提示符
		PrintCommandPrompt();
		//获取命令行输入
		char commandline[COMMAND_SIZE];
		if (!GetCommandLine(commandline, sizeof(commandline)))
		{
			continue;
		}
		RedirCheck(commandline);
		cout << redir << "->" << filename << endl;
		//填充命令行参数表
		if (!CommandParse(commandline))
		{
			continue;
		}
		//处理内建命令
		if (CheckAndExecBuiltion())
		{
			continue;
		}
		//执行命令
		Execute();
	}
	return  0;
}

5.5 stdout VS stderr

之前我们学了标准输出流与标准错误流对应的设备都是显示器,那么我们可能疑惑的是,那么这两者之间有什么区别呢?

我们可以先看一下这段代码:

cpp 复制代码
#include<stdio.h>
int main()
{
    printf("stdout:hello printf!\n");
    perror("stderr:hello perror!");
    fprintf(stdout,"stdout:hello fprintf!\n");
    fprintf(stderr,"stderr:hello fprintf!\n");
    return 0;
}

如果直接运行的话,肯定会全部打印。但是如果对该执行文件进行输出重定向的话,标准错误流的文件内容就不会重定向进新文件中。

这是因为输出重定向默认关闭的是1号文件描述符,并没有关闭2号文件描述符。我们都知道2文件描述符是标准错误,对应的也是显示器文件,所以这里我们就可以打印出cerr和stderr。

但是其实我们重定向的完整写法是这样的,./myfile 1 > log.txt,把1重定向到log.txt文件,不写默认就是把标准输出重定向。

所以如果我们要把标准输入和标准输出分别写不同的文件,就需要对标准输出和标准输出都进行重定向。利用这一特性,我们就可以以后将错误信息单独提前出来,打印到日志系统中。

当然如果想将标准输出与标准输出的内容输出到同一文件中,也可以使用类似的指令。

2>&1相当于把1的内容拷贝到2里面!此时标准错误也会向log.txt写入了

问题:有了标准输出为什么要有标准错误?

虽然标准输出和标准输入都是向显示器文件写入,我们要向显示器打印有两种方式。程序需要方便Debug输出日志,有了标准错误我们就可以通过重定向的方式,把程序要输出的信息和Debug的信息分离到不同的文件,方便形成日志。


五、Linux下一切皆文件

首先,在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息,甚至管道也是文件。将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。

在Linux系统中,"一切皆文件"是一个重要的设计理念。这一理念的实现涉及到多个层面的技术和机制。首先,外设与内存进行交互,像键盘、显示器等外设都有诸如read、write等读写方法。但由于各种外设的硬件结构不同,这些方法在底层实现是不一样的,并且都是在硬件的驱动层完成。

那么,Linux是如何做到"一切皆文件"的呢?首先Linux引入了软件的虚拟层 VFS(虚拟文件系统)。VFS会统一维护每一个文件的结构体struct file,这个结构体包含了一批函数指针。这些函数指针能够直接指向底层的方法。在上层,我们可以以统一的struct_file的方式去看待文件。因此,"一切皆文件"指的是在 VFS层面上的看待方式,而非在驱动层。

这样做最明显的好处是,开发者仅需要使用一套API和开发工具,即可调取Linux系统中绝大部分的资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE)的操作都可以用write函数来进行。

这种实现方式与C++中的多态类似。在 C++中,父类指针指向谁,调用的就是谁的方法。在 C 语言中,可以通过函数指针做到指向不同的对象时执行不同的方法,实现多态的性质。在Linux中,每个struct file中包含很多函数指针,这样在struct file上层看来,所有的文件都是调用统一的接口,而在底层则通过函数指针指向不同硬件的方法,实现与具体硬件对应的逻辑。

之前我们讲过,当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体,该结构体定义在/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h下,以下展示了该结构部分我们关系的内容 :

cpp 复制代码
struct file {
    ...
    struct inode    *f_inode;    /* cached value */
    
    const struct file_operations    *f_op;
    
    ...
    atomic_long_t    f_count;    // 表示打开文件的引用计数,如果有多个文件指针指向它,就会增加f_count的值。
    unsigned int    f_flags;    // 表示打开文件的权限
    fmode_t         f_mode;     // 设置对文件的访问模式,例如:只读,只写等。所有的标志在头文件<fcntl.h>中定义
   loff_t          f_pos;       // 表示当前读写文件的位置
    ...
} __attribute__((aligned(4)));  /* lest something weird decides that 2 is OK */

值得关注的是struct file中的f_op指针指向了一个file_operations结构体,这个结构 体中的成员除了struct module*owner其余都是函数指针。该结构和struct file都在fs.h下。

cpp 复制代码
struct file_operations {
    struct module *owner;
    loff_t (*lseek)(struct file *, loff_t, int);
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*readdir)(struct file *, void *, filldir_t);
    unsigned int (*poll)(struct file *, struct poll_table_struct *);
    int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
    int (*mmap)(struct file *, struct vm_area_struct *);
    // mmap 用来请求将设备内存映射到进程的地址空间。如果这个方法是 NULL,mmap 系统调用返回 -ENODEV。
    int (*open)(struct inode *, struct file *);
    // 打开一个文件
    int (*flush)(struct file *, fl_owner_t id);
    // flush 操作在进程关闭它的设备文件描述符的拷贝时调用;
    int (*release)(struct inode *, struct file *);
    // 在文件结构被释放时引用这个操作。如同 open,release 可以为 NULL。
    int (*fsync)(struct file *, struct dentry *, int datasync);
    // 用户调用来刷新任何挂着的数据。
    int (*aio_fsync)(struct kiocb *, int datasync);
    int (*fasync)(int, struct file *, int);
    int (*lock)(struct file *, int, struct file_lock *);
    // lock 方法用来实现文件加锁;加锁对常规文件是必不可少的特性,但是设备驱动几乎从不实现它。
    ssize_t (*readpage)(struct file *, struct page *, unsigned long, unsigned long);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock)(struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};

file_operation就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

介绍完相关代码,我们用一张图总结:

上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct file下file_operation中的各种函数回调,让我们开发者只用file便可调取 Linux系统中绝大部分的资源!!这便是"Linux下一切皆文件"的核心理解。

相关推荐
律宏阔5 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔5 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴5 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千6 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei376 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei376 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei376 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh6 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW6 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
百万蹄蹄向前冲6 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js