【Linux】基础IO

2023-12-22 21:47:57

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

1.回顾C语言的文件操作

有一个笑话:把大象装冰箱需要几步?“打开冰箱门,把大象装进去,关闭冰箱门”。实际上,我们对文件的操作也是这样的:“打开文件,读写文件,关闭文件”。这里演示一下C语言对文件的操作。详细内容可以去博主的其他文章了解【C语言进阶】文件操作

1.1 打开和关闭

#include <stdio.h>
#define FILE_NAME "log.txt"
int main()
{
    // fopen的返回值类型是FILE*,返回一个文件指针,打开失败返回NULL
    FILE* fp = fopen(FILE_NAME, "r");//fopen传参数:1.文件名 2.打开方式:"w"只写, "r"只读, "a"追加, "w+"读写, "r+"读写, "a+"追加读写
    if(fp == NULL)
    {
        perror("open");//打印失败原因
        return 1;
    }
    else
    {
        printf("%s打开成功\n", FILE_NAME);
    }

    //文件打开了就需要关闭
    int ret = fclose(fp);//fclose传参数:FILE*类型,要关闭的文件指针
    //fclose的返回值类型int,如果关闭成功返回0,关闭失败返回EOF
    if(ret == 0)
    {
        printf("%s关闭成功", FILE_NAME);
    }
    else
    {
        perror("关闭失败");//打印失败原因
        return 2;
    }
    return 0;
}

image-20231219202605902
image-20231219202941263

1.2 文件读写

文件的读写是对应的,一个读对应着一个写,有以下几种读写方式

int fputc(int c, FILE *stream);                                          // 向stream写入一个字符
int fgetc(FILE *stream);                                                 // 从stream读取一个字符
int fputs(const char *s, FILE *stream);                                  // 向stream写入一个字符串
char *fgets(char *s, int size, FILE *stream);                            // 从stream读取一个字符串
int fprintf(FILE *stream, const char *format, ...);                      // 向stream中格式化写入
int fscanf(FILE *stream, const char *format, ...);                       // 从stream中格式化读取
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); // 向stream中以二进制的方式写入
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);        // 从stream中以二进制的方式读取

1. 文件写入

image-20231220012124680

image-20231220011853418

2. 文件读取

image-20231220013103070

image-20231220013002195

2. 系统文件I/O

2.1 比特位传递选项

在C语言中,我们想要传递标记的话,一般使用一个整数来传递一个标记,但是我们知道一个int有32个比特位,所以实际上,我们可以通过比特位来传递选项,这是一个非常巧妙的设计:使用每一个比特位代表一个选项,1表示具有该选项,0表示不具有该选项,如果想要表达同时具有某一些选项,那就将这些选项对应的值按位或在一起

例如:

#include <stdio.h>
//选项的宏定义,每个宏对应一个比特位
#define ONE (1 << 0)
#define TWO (1 << 1)
#define THREE (1 << 2)
#define FOUR (1 << 3)
void show(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()
{
    show(ONE);
    printf("-------------------\n");
    show(TWO);
    printf("-------------------\n");
    show(ONE | TWO);
    printf("-------------------\n");
    show(ONE | TWO | THREE);
    printf("-------------------\n");
    show(ONE | TWO | THREE | FOUR);
    return 0;
}

image-20231221003119489

2.2 打开和关闭

写在前面:对于任何一种语言来说,一定都提供了自己的文件操作接口,那么每一种语言的文件操作都是不同的,所以我们的学习成本很高。但是,所有的程序都是运行在操作系统上的,所以对于任何一种语言,他最终都会让操作系统来执行对应的文件操作。所以现在我们直接学习最底层的操作系统层面的文件操作。以后碰到什么类型的文件操作,本质上都是调用操作系统层面的某一个程序来执行,就能够触类旁通。

我们在上文中提到了C语言提供的文件操作函数,那么实际上fopen,fclose这两个C语言函数在内部是封装了两个系统调用:openclose的。

image-20231221003836069

头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数原型:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数解释:
pathname:要打开的文件名;
flags:标识位,表示打开文件的模式
    O_RDONLY:只读打开
	O_WRONLY:只写打开
	O_RDWR:读写打开
		上述这三个模式必须指定一个且只能指定一个打开
	O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限(这个选项是一个建议选项)
	O_APPEND:追加写
    O_TRUNC:如果文件存在,那么打开的时候就清空
mode:创建新文件的权限
返回值:如果调用成功,返回打开文件的文件描述符,如果调用失败,则返回-1,同时设置错误码

image-20231221004924440

头文件:
#incldue <unistd.h>
函数原型:
int close(int fd);
参数解释:
fd:file descriptor文件描述符
返回值:如果调用成功,返回0,如果调用失败就返回-1,同时设置错误码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT);//以只读的方式打开,如果文件不存在就创建文件(这里选项或起来表示同时传递)
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("%s打开成功", FILE_NAME);

    int ret = close(fd);//关闭文件
    if(ret == -1)
    {
        perror("close");
        return 2;
    }
    printf("%s关闭成功", FILE_NAME);
    return 0;
}

image-20231221104008724

但是,这里创建的log.txt看起来怪怪的,他的权限是乱码,我们在使用fopen的时候就没有出现这种问题。这是因为在创建文件的过程中,需要确定创建的文件的权限(包括对文件拥有者,所属组和other的权限),在C语言提供的接口中的封装内置了传递创建权限。但是使用系统调用的时候需要我们自己手动传入,这也就是为什么要穿入mode参数的原因。如果不传的话,那么创建的文件的权限就是随机值。

image-20231221105556292

image-20231221105739085

关于umask的问题

在上面的实例中,注意我们传入的权限是666,但是最终自动创建出来的文件的权限是664,这是因为普通用户的默认文件掩码是0002,在【Linux】基本知识和权限这篇文章中我们提到过

image-20231221110351088

所以在新建文件的时候,文件的默认权限为0666 & ~0002 = 0664 。如果想要让自己创建的文件权限被重新设置,可以在代码里加上umask(0)把文件掩码设置为0。这样创建出来的文件的权限就是666了

image-20231221111013137

image-20231221111050069

注意:这里我们更改的是子进程的文件掩码,它是由父进程shell继承来的,由于进程的独立性,所以我们更改子进程的文件掩码不会影响到父进程

2.3 文件读写

1. 文件写入:write

(1)普通写入

系统为文件的读写提供了两个系统调用接口:readwrite,用于读写文件

image-20231221124424144

头文件:
#include <unistd.h>
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数解释:
fd:要写入的文件描述符
buf:写入内容的起始位置
count:需要写入的字节数
返回值:调用成功,返回写入成功的的字节数,如果返回值为0表示没写入任何东西,如果调用失败返回-1,同时设置错误码

image-20231221130158740

如果要写入字符串的话,写入的长度不用加一,因为字符串的\0作为字符串结尾是C语言规定的,关操作系统什么事?因此我们在进行字符串的写入的时候,只需要写入字符串的有效内容即可

image-20231221130409862


2. 文件读取read

image-20231221111428634

头文件:
#include <unistd.h>
函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数解释:
fd:文件描述符
buf:从文件fd中读取count个字符到buf中
count:指定需要读取的字节数
返回值:如果调用成功,那么就返回读取的字节数(返回值为0表示已经读到文件末尾了),如果返回值小于传入的count,不是调用失败,而是由于一些原因导致没有读取到指定个数的字节。如果调用失败,就返回-1,同时设置错误码。

image-20231221150421423

image-20231221131718574

文件描述符的感性理解

在应用层面,由于使用了树状结构的目录,所以可以通过文件路径+文件名的方式来指定唯一文件,但是在操作系统层面,OS看不见这些内容。所以对于打开了的文件,需要给他们分配一个唯一标识符:文件描述符file descriptor

2.4 open中O_APPEND和O_TRUNC的使用

1. O_TRUNC

在上述的代码中,我们想要重新写入log.txt中的内容

image-20231221150923705

image-20231221150856298

看到结果更我们想象的不太一样,多了一些东西。这是因为在写入的时候没有进行清空。需要在打开的时候加上O_TRUNC选项

int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);

image-20231221151229008

所以C语言提供的fopen接口中的“w”选项在内部就是对open进行了这样的调用。

2. O_APPEND

如果想要对文件进行追加操作,在C语言中只需要在fopen中传入“a”/"a+"即可。实际上在内部调用open的时候,只需要在传入的打开模式中或上O_APPEND即可。

image-20231221152349186

image-20231221152423679

可以看到已经成功追加。

3. 文件操作的理解

1. 文件操作的本质就是进程对被打开文件的操作

2. 我们知道,一个进程可以打开多个文件,所以系统中肯定存在着大量的已经打开文件,那么这些已经打开文件是需要被管理起来的,也就是需要先描述,再组织因此,操作系统为了把这些已经打开的文件管理起来,就实现了一个内核数据结构struct file,在这个数据结构中包含了文件的大部分属性

image-20231221153458564

3. 进程和被打开文件之间的关联:进程和被打开文件通过open的返回值来进行关联,所有的被打开文件都有一个唯一的返回值,我们把它叫做文件描述符(file descriptor)

4. 文件描述符

4.1 引入

既然我们说到,OS通过文件描述符管理所有被打开的文件,那么我们来看一看这个文件描述符到底是什么

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FILE_NAME(number) "log"#number".txt"//拼接出文件名

int main()
{
    int  fd0 = open(FILE_NAME(0), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int  fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int  fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int  fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int  fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fd0=%d\n", fd0);
    printf("fd1=%d\n", fd1);
    printf("fd2=%d\n", fd2);
    printf("fd3=%d\n", fd3);
    printf("fd4=%d\n", fd4);
    return 0;
}

image-20231221161920536

我们可以看到文件描述符是一些小整数,这会让我们想到数组(数组的下标就是连续的小整数)那为什么是从3开始的呢?0,1,2去哪里了?

在学习C语言的过程中,我们了解过,有三个流是被默认打开的:stdin(标准输入),stdout(标准输出),stderr(标准错误),这三个流的类型也是FILE*类型。但是我们知道OS不会管FILE*类型,他只关注文件的fp,所以在FILE结构体内部肯定有一个字段是保存fd的

/usr/include/libio.h文件中我们能够找到C语言提供的FILE结构体的声明,在里面能够找到_fileno字段,这个字段就是该文件对应的fd。

image-20231221171249867

所以我们可以通过stdout->_fileno来查看对应的fd:

int main()
{
    printf("stdin:%d\n", stdin->_fileno);
    printf("stdout:%d\n", stdout->_fileno);
    printf("stderr:%d\n", stderr->_fileno);
    return 0;
}

image-20231221182055060

所以,到此我们知道原来0,1,2默认被占用,我们的C语言封装了接口,同时也封装了操作系统内的文件描述符。

现在我们知道文件描述符就是0,1,2,3这样的小数字,那么文件描述符的本质到底是什么?

4.2 文件描述符的本质

image-20231222201214137

一个文件,在被打开之前是存放在磁盘上的,如果需要对文件进行操作的话,就需要把文件加载到磁盘上,也就是打开文件的过程,文件打开之后就会被操作系统管理起来,我们之前说到了“先描述”是使用一个结构体struct file*来描述一个被打开了的文件,“再组织”这件事情,操作系统是使用了一个结构struct files_struct来组织,在这个结构中存在着一个数组叫做进程的文件描述符表strcut file* fd_array[]用于存放struct file的指针,至此我们就将已经打开的文件管理起来了。

那么还有一个问题:之前说了进程和被打开的文件需要关联起来,这是怎么关联的呢?

在进程的strcut task_struct中有一个字段struct files_strcut *files指向进程的文件描述符表,这样就把进程和被打开的文件管理起来了。

而我们所说的文件描述符,本质上就是strcut file *fd_array[]的数组下标

经过了上述的结构构件,现在如果进程相访问一个被打开文件,就会经过以下过程

  • 拿到要访问文件的fd
  • struct task_struct中找到struct files_struct *files
  • 再通过这个files找到对应的fd_array
  • 通过数组下标,也就是fd就能访问到对应文件的struct file结构体。

4.3文件描述符的分配规则

文件描述符本质上就是数组下标,那么这个数组的下表是怎么进行分配的呢?

我们知道,一个进程的的文件描述符表的前三个是默认被打开的,那么如果我们把0对应的关掉

image-20231221203154788

image-20231221203154788

那么如果我们把0对应的关掉,那么新打开的文件的fd就会被设置为0。

接下来试一试关掉stderr:

image-20231221203442855

所以我们可以得出结论:文件描述符的分配遵循从小到大的顺序,在fd_array中找到最小的没有被分配的位置,对应的下标分配为当前打开文件的fd,所以在最开始我们的实例中打开的第一个文件的fd是3,是因为0,1,2被默认打开的stdin、stdout、stderr占用。

问题一:为什么在我们的代码中把stdin和stderr关掉了,没有对系统产生影响,后面还是能够正常输入?

我们知道,被打开的文件是需要和进程关联起来的,这种关联是通过进程PCB指向的fd_array中对stdin和stderr的地址的引用,我们使用close关掉对应的文件描述符,是在用户层面的,但是在操作系统层面使用了引用计数,用户层调用close只会让引用计数减一,只有当引用计数为0的时候OS才会关闭这个文件


问题二:上述的演示中关掉了0和2,为什么没有关掉1?

接下来演示一下关掉1的现象:

image-20231221205525004

根据前面的规则,我们知道当我们关闭了1之后,新文件的fd就会被分配给1。调用printf实际上就是把字符串写进stdout中,现在的stdout->_fileno对应的就是我们打开的文件,也就是log.txt,所以按道理来说这个输出就被写进了log.txt。

那么现在我们看一下log.txt中的内容有没有:

image-20231221210318438

诶?为什么没有啊?不应该啊?

这其实是因为缓冲区没有被刷新的原因,在写入之后手动刷新一下缓冲区就行了

image-20231221210507236

本来我们应该把打印往显示器文件里打印,最后经过我们的一系列操作把输出的结果写到了文件里。也就是本来应该写到显示器,却写到了文件,这种操作我们称之为重定向

5. 重定向

5.1 重定向的概念

我们接触过的重定向实际上也就三种:<输入 >输出 >>追加

重定向典型的特征就是:在上层用的fd不变情况下,在内核中更改fd对应的struct file*的地址

重定向的本质就是上层的fd不变,在内核中更改fd对应的struct_file*的地址

5.2 重定向的接口

在上述的例子中,我们看到了重定向的实现方式:关闭指定的流的文件描述符,然后把重定向目标文件打开,让它的fd被分配为指定的数值。但是这种手动实现的方式有点挫。

事实上,操作系统也提供了系统调用接口来做这件事:dup系列接口,这里我们只说dup2

image-20231221213725813
头文件:
#incldue <unistd>
函数原型:
int dup2(int oldfd, int newfd);
参数解释:
oldfd:用于替换的数组下标
newfd:被替换的数组下标
返回值:调用成功,返回新的文件描述符,如果调用失败就返回-1,同时设置错误码

dup2的作用就是将fd_array数组中oldfd下标对应的元素拷贝到newfd下标对应的元素位置,最后留下的oldfd和newfd对应的内容都是原来oldfd中的内容。注意这个oldfd和newfd分别是什么。

5.3 重定向的实现

1. 输出重定向

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define  FILE_NAME "log.txt"
int main()
{
    umask(0);
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open:");
        return 1;
    }

    dup2(fd, 1);//使用dup进行输出重定向(也就是替换fd为1的位置)
    printf("hello world!\n");

    return 0;
}

image-20231222105239726

2. 追加重定向

所谓的追加重定向,本质上就是将文件的覆盖写变成追加写,也就是把打开文件的方式由O_TRUNC变成O_APPEND

那实现的代码也就显而易见了:

int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);//打开文件的方式变成O_APPEND

image-20231222105957601

image-20231222105935117

3. 输入重定向

输出重定向的前提是文件必须存在

以读的方式打开文件,然后使用dup更换打开的文件的fd和stdin的fd。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define  FILE_NAME "log.txt"
int main()
{
    umask(0);
    int fd = open(FILE_NAME, O_RDONLY);
    if(fd < 0)
    {
        perror("open:");
        return 1;
    }
    dup2(fd, 0);//使用dup进行输入重定向(也就是替换fd为0的位置)
    char line[64];
    while(1)
    {
        printf(">");//输入提示
        char* ret = fgets(line, sizeof(line), stdin);
        if(ret == NULL)
            break;
        printf("%s\n",line);//打印stdin读取到的内容
    }
    return 0;
}

image-20231222110616959

5.4 之前实现的minishell添加重定向

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>

#define NUM 1024//输入缓冲区大小
#define OPT_NUM 64//命令参数最大个数

#define NONE_REDIR 0//重定向类型的宏定义
#define INPUT_REDIR 1
#define APPEND_REDIR 2
#define OUTPUT_REDIR 3
#define ERROR_REDIR 4

#define trimSpace(start) {\
    while(isspace(*start)) ++start;\
}while(0) //去空格的宏定义

char lineCommand[NUM];//输入缓冲区
char* argv[OPT_NUM];
int EXIT_CODE;

int redirType = NONE_REDIR;//重定向类型和目标文件的全局变量
char* redirFile = NULL;

void checkCommand(char* commands)//解析重定向命令,将重定向信息保存在redirType和redirFile中
{
    assert(commands);
    char* start = commands;
    char* end = commands + strlen(commands);
    while(start < end)
    {
        if(*start == '>')
        {
            *start = '\0';
            ++start;
            if(*start == '>')
            {
                //append
                redirType = APPEND_REDIR;
                ++start;
            }
            else
            {
                //output
                redirType = OUTPUT_REDIR;
            }
            trimSpace(start);
            redirFile = start;
            break;
        }
        else if(*start == '<')
        {
            //intput
            *start = '\0';
            ++start;
            trimSpace(start);
            //填写重定向信息(type和file)
            redirType = INPUT_REDIR;
            redirFile = start;
            break;
        }
        else
        {
            ++start;
        }
    }
}

int main()
{
    while(1)//死循环,因为Shell要一只运行着
    {
        //初始化全局变量
        redirType = NONE_REDIR;
        redirFile = NULL;
        //打印输出命令提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);//由于打印命令提示符的时候没有换行,所以这里手动刷新缓冲区
        //获取输入
        char* str = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);//最后一个位置用于在极端情况下保证字符串内有'\0'
        assert(str);//判断合法性
        (void)str;
        lineCommand[strlen(lineCommand) - 1] = '\0';//消除行命令中的换行

        checkCommand(lineCommand);//解析重定向命令

        //命令解析(字符串切割)
        argv[0] = strtok(lineCommand, " ");
        int i = 1;

        if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)//识别ls,自动加上颜色选项
        {
            argv[i++] = (char*)"--color=auto";
        }

        while(argv[i++] = strtok(NULL, " "));//使用字符串切割函数依次拿到每个参数

        if(argv[0] != NULL && strcmp(argv[0], "cd") == 0)
        {
            if(argv[1] != NULL)
            {
                chdir(argv[1]);
            }
            else
            {
                printf("no such file or directory\n");
            }
            continue;
        }
        if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)
        {
            if(strcmp(argv[1], "$?") == 0)
            {
                printf("%d\n", EXIT_CODE);
                EXIT_CODE = 0;
            }
            else
            {
                printf("%s\n", argv[1]);
            }
            continue;
        }

        //创建子进程
        pid_t id = fork();
        if(id == -1)
        {
            perror("fork");
            exit(errno);
        }
        else if(id == 0)
        {
            //child
            //用子进程来实现重定向的内容
            //但是子进程如何执行重定向是由父进程来告诉子进程的(如何告诉?redirType和redirFile)
            switch(redirType)
            {
                case NONE_REDIR:
                    break;//如果没有任何重定向的话就直接执行程序替换
                case INPUT_REDIR:
                {
                    int fd = open(redirFile, O_RDONLY);
                    if(fd < 0)
                    {//如果打开失败就直接返回
                        perror("open:");
                        exit(errno);
                    }
                    //使用dup2重定向
                    dup2(fd, 0);
                }
                break;
                case OUTPUT_REDIR:
                case APPEND_REDIR:
                {
                    int flags = O_WRONLY | O_CREAT;
                    if(redirType == OUTPUT_REDIR) flags |= O_TRUNC;
                    else flags |= O_APPEND;
                    int fd = open(redirFile, flags, 0666);
                    if(fd < 0)
                    {
                        perror("open:");
                        exit(errno);
                    }
                    dup2(fd, 1);
                }
                break;
                default:
                    printf("bug?\n");
            }

            //进程程序替换
            execvp(argv[0], argv);
            //执行到此处的时候,证明进程替换错误
            perror("exec:");
            exit(errno);
        }
        else
        {
            //parent
            //进程等待
            int status = 0;//退出状态
            pid_t ret = waitpid(id, &status, 0);//阻塞等待
            EXIT_CODE = (status >> 8) & 0xFF;
            if(ret == -1)
            {
                perror("wait fail");
                exit(errno);
            }
        }
    }
    return 0;
}

image-20231222131544682

6. Linux下一切皆文件的理解

由于体系结构的限制,我们知道,一切的数据计算都需要经过内存再进行计算,计算之后再将结果写入/刷新到外设(键盘、显示器、磁盘、网卡等)。其中将数据读取到内存、将数据写到外设的过程就是就是I/O的过程。那么在操作系统层面,这些软硬件资源是需要被管理起来的。我们说管理的本质是“先描述,再组织”。所以操作系统为了将这些外设管理起来,就使用了一个struct file结构体来抽象化。这个结构体内部包含了各种的文件属性,比如:外设类型,外设状态,外设的读写函数的指针等等。这些是OS层面的抽象的内容。

在硬件和驱动层面:对于每一个外设,他们都会实现一个驱动程序,在这个驱动程序中包含了这个硬件的读写方法,在OS层面上只需要将struct file中的读写函数指针的字段指向这里的读写方法,就能够在OS层面调用到这些读写方法。同时,在OS层面不关心这些外设到底是什么,只关心这个文件的内容。这就是Linux下一切皆文件

image-20231222170456437


本节完

文章来源:https://blog.csdn.net/weixin_63249832/article/details/135160425
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。