【Linux】基础IO

需要云服务器等云产品来学习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


本节完

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Linux IO 模型是指 Linux 操作系统中的 IO 处理机制。它的目的是解决多个程序同时使用 IO 设备时的资源竞争问题,以及提供一种高效的 IO 处理方式。 Linux IO 模型主要分为三种:阻塞 IO、非阻塞 IOIO 多路复用。 阻塞 IO 指的是当程序进行 IO 操作时,会被挂起直到 IO 操作完成,这种方式简单易用,但是对于高并发环境不太适用。 非阻塞 IO 指的是程序进行 IO 操作时,如果无法立即完成,会立即返回一个错误码,程序可以通过循环不断地进行 IO 操作来实现轮询的效果。非阻塞 IO 可以提高程序的响应速度,但是会增加程序的复杂度。 IO 多路复用指的是程序可以同时监听多个 IO 设备,一旦有 IO 事件发生,就会立即执行相应的操作IO 多路复用可以提高程序的效率,但是需要程序员手动编写代码来实现。 Linux IO 模型还有其他的实现方式,比如信号驱动 IO 和异步 IO 等。但是这些方式的使用比较复杂,一般不常用。 ### 回答2: Linux中的IO模型是指操作系统在处理输入输出的过程中所遵循的一种方式。它主要包括阻塞IO、非阻塞IO、多路复用IO和异步IO四种模型。 阻塞IO是最简单的IO模型,当一个IO操作发生时,应用程序会被阻塞,直到IO操作完成才能继续执行。这种模型的特点是简单直接,但是当有多个IO操作时会造成线程的阻塞,影响系统的性能。 非阻塞IO是在阻塞IO基础上发展而来的,应用程序在发起一个IO操作后可以继续执行其他任务,不必等待IO操作的完成。但是需要通过轮询来不断地检查IO操作是否完成,效率相对较低。 多路复用IO使用select、poll、epoll等系统调用来监听多个IO事件,当某个IO事件就绪时,应用程序才会进行读写操作,避免了前两种模型的效率问题。多路复用IO模型适用于连接数较多时的场景,如服务器的网络通信。 异步IO是最高效的IO模型,应用程序发起一个IO操作后,立即可以执行其他任务,不需要等待IO操作的完成。当IO操作完成后,操作系统会通知应用程序进行后续处理。异步IO模型常用于高吞吐量、低延迟的应用,如高性能服务器和数据库等。 总之,Linux IO模型提供了多种不同的方式来处理输入输出,每种模型都有其适用的场景和特点。选择合适的IO模型可以提高系统的性能和效率。 ### 回答3: Linux IO模型是指操作系统中用于处理输入输出操作的一种方法或机制。在Linux中,常见的IO模型有阻塞IO、非阻塞IOIO多路复用和异步IO。 阻塞IO是最基本的IO模型,当应用程序发起一个IO请求时,它将一直阻塞等待直到IO操作完成,期间无法做其他任务。虽然简单易用,但是对资源的利用不高。 非阻塞IO在发起一个IO请求后,不会阻塞等待IO操作完成,而是立即返回并继续做其他任务。应用程序需要不断地轮询IO操作状态,直到操作完成。由于需要不断轮询,对CPU的占用较高,但可以提高资源的利用率。 IO多路复用是通过一个线程同时监听多个IO事件,从而实现并发处理多个IO操作。在IO多路复用模型中,应用程序不需要进行轮询,而是通过调用select、poll或epoll等系统调用监听多个文件描述符的IO事件。这样可以在单个线程中处理多个IO操作,提高并发性能。 异步IO模型在发起一个IO请求后,应用程序不需要等待IO操作完成,而是继续做其他任务。当IO操作完成后,操作系统会通知应用程序。异步IO模型需要操作系统的支持,效率较高,但实现较为复杂。 通过选择合适的IO模型,可以根据不同的应用场景来提高IO操作的效率和性能。例如,对于需要同时处理大量连接的服务器应用,IO多路复用是一种常见的选择;而对于需要处理大量IO操作的高性能服务器,则可以考虑使用异步IO模型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凌云志.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值