Linux中输入和输出基本过程

news2024/10/23 6:16:22

目录

Linux中输入和输出基本过程

文件内核级缓冲区

何为重定向

子进程与缓冲区

手撕一个简单的shell(版本2)

判断重定向命令与截取

执行重定向

简单实现stdio.h中的文件相关操作

FILE结构体

fopen函数

fwrite函数

fflush函数

fclose函数


Linux中输入和输出基本过程

文件内核级缓冲区

前面在如何理解Linux一切皆文件的特点中提到为了保证在Linux中所有进程访问文件时的方式趋近相同,在file结构体中存在一个files_operations结构体指针,对应的结构体保存所有文件操作的函数指针(这个结构体也被称为操作表)

每一个file结构体中除了有自己的操作表以外还有一个文件的内核级缓冲区,这个缓冲区不同于语言层面的缓冲区,在调用底层系统接口的读或者写时,会有一方先将内容保存到该缓冲区,再将内容移动到指定设备

例如,对于读函数read来说,以从磁盘文件读取文件内容为例,首先操作系统会从磁盘中读取文件内容到缓冲区,再由read函数从缓冲区将内容读取到指定的设备;对于write函数来说,首先操作系统会将内存中需要写入的内容先写入缓冲区,再由write函数从缓冲区写入到磁盘中。除了前面提到的两个单一过程外,还要一种复合过程:对文件的内容进行修改。这一种过程既涉及到读取,也涉及到写入,所以首先需要通过操作系统读入需要修改的内容到内核级缓冲区,由read函数将缓冲区中的数据读到内存,在程序运行中对读取到的内容进行修改,再通过write函数将修改后的内容写入到缓冲区,最后由操作系统从缓冲区写入到磁盘

如果抽象化一下read函数和write函数的过程可以发现这两个函数的基本行为就是读写缓冲区,所以这两个函数也可以理解为拷贝函数,read函数即为将缓冲区中的数据拷贝到内存中的指定位置,write函数即为将内存中的数据拷贝到缓冲区

上面整个过程中,read函数和write函数只完成对缓冲区中的数据进行处理,但是缓冲区中的数据何时移动到指定的设备由操作系统自主决定

如果用户向自主决定何时将内核级缓冲区中的数据刷新到内核级缓冲区可以使用 fsync函数,其原型如下:
int fsync(int fd);

读或者写过程示意图如下:

何为重定向

前面提到,文件描述符是Linux中每一个文件的唯一标识符,也就是说,通过文件描述符可以唯一确定一个文件,而stdinstdoutstderr是每一个C语言程序默认打开的三个文件,对应的文件描述符为0、1和2,而之所以文件描述符是从0开始,本质是因为文件描述符是fd_array数组的下标,当用户再打开一个文件时,对应的文件结构指针就会存储到fd_array的指定位置,而因为下标0、1和2已经被占用,所以新开的文件对应的下标只能从3开始

现在关闭0号位置的文件,观察效果,例如下面的代码:

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

int main()
{
    close(0);
    int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT);

    printf("%d\n", fd1);
    printf("%d\n", fd2);
    printf("%d\n", fd3);
    printf("%d\n", fd4);

    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);

    return 0;
}

编译运行上面的代码后可以看到结果如下:

如果关闭的是2号文件,观察效果,例如下面的代码:

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

int main()
{
    close(2);
    int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT);

    printf("%d\n", fd1);
    printf("%d\n", fd2);
    printf("%d\n", fd3);
    printf("%d\n", fd4);

    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);

    return 0;
}

编译运行上面的代码后可以看到结果如下:

从上面的代码中可以看出,在Linux中,一个文件被打开时,文件描述符的分配原则是在fd_array中找到最小的且没有被其他file结构指针占用的位置

以关闭0号位置为例,示意图如下:

在上面的演示中,先关闭了某一个文件描述符较前的文件,再打开另一个文件,打开的这个文件所处的位置是就是被关闭的那个文件的位置,此时再使用输出语句输出内容,原本应该输出到文件描述符较前的原始文件中,比如前面的stdin,后面却输出到了文件中test1.txt,这个过程就称为文件重定向,主要原理就是文件描述符是固定不变的,而输出和输入只认文件描述符,不会在意这个文件描述符对应的位置指向的是哪一个文件

在Linux中,存在一个系统调用接口使得可以在程序中实现重定向,这个函数为dup2,其原型如下:

int dup2(int oldfd, int newfd);

在Linux操作手册中,dup2函数的描述如下:

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary

参数解释:

  1. oldfd:该参数表示待拷贝的文件的文件指针对应的文件描述符
  2. newfd:该参数表示被覆盖的文件的文件指针对应的文件描述符

示意图如下:

所以,使用dup2就可以实现使用printf函数本应该输出到stdout中,而输出到test1.txt的效果,代码如下:

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

int main()
{
    int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);

    dup2(fd1, 1);

    printf("%d\n", fd1);
    printf("%d\n", fd2);
    printf("%d\n", fd3);
    printf("%d\n", fd4);

    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);

    return 0;
}

需要注意,因为在使用dup2函数之前,stdout已经被打开了,此时open函数打开test1.txt文件时就只能使用3号下标的位置,所以可以看到dup2函数的确实现了将内容输出到test1.txt文件而不是stdout中,此时示意图如下:

这里需要注意一个细节:尽管dup2函数会关闭被覆盖的文件stdout,但是关闭后被拷贝的文件test1.txt占用了被覆盖的文件stdout的位置,所以再打开其他文件依旧会从7号位置开始,例如下面的代码演示:

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

int main()
{
    int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);

    dup2(fd1, 1);
    int fd5 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666); // 重定向后打开一个新文件
    
    printf("%d\n", fd1);
    printf("%d\n", fd2);
    printf("%d\n", fd3);
    printf("%d\n", fd4);
    printf("%d\n", fd5);


    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    close(fd5);

    return 0;
}

编译运行上面的代码后可以看到结果如下:

但是,前面的代码中,只演示了关闭0号位置和2号位置的文件,如果此时关闭1号位置的文件,再打开同样的四个文件,输出每一个文件的文件描述符,从前面的规律可以推出,输出语句会因为1号位置的文件是test1.txt而将内容输出到test1.txt中,例如下面的代码:

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

int main()
{
    close(1);
    int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);

    printf("%d\n", fd1);
    printf("%d\n", fd2);
    printf("%d\n", fd3);
    printf("%d\n", fd4);

    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);

    return 0;
}

编译运行上面的代码后可以看到不论是test1.txt还是其他文件都没有显示需要的内容,控制台也没有正常打印需要的内容

但是如果将代码修改为如下:

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

int main()
{
    // ...
    fflush(stdout);

    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);

    return 0;
}

编译运行上面的代码后可以看到结果如下:

现在就引入了一个问题,为什么加了fflush(stdout)就可以看到需要的效果

前面提到,在调用操作系统接口中的读写函数时会涉及到一个文件内核级缓冲区,这个缓冲区是为了减少操作系统进行IO操作时产生的时间和空间的消耗,但是如果用户使用的是语言级别的接口,例如fread或者fwrite等,那么此时就会出现用户的接口到操作系统的消耗,这个消耗主要来自于语言级函数多次调用系统接口时创建函数栈帧,所以为了减少这种情况下的时间和空间消耗,就存在了语言级别的缓冲区

但是,在实现进度条时提到了\n可以刷新语言级别的缓冲区,此处为什么用了\n也没有刷新,原因就在于语言级别的缓冲区刷新情况有三种:

  1. 行刷新,主要是stdout文件时的刷新
  2. 满刷新,当语言级别的缓冲区写满时刷新
  3. 不缓冲

而此处的\n就属于行刷新,但是此时的printf认识的fd为1的文件并不是stdout,所以行刷新就失效了,转换成默认的满刷新,所以需要调用fflush(stdout)将当前语言级别缓冲区中的数据调用写函数刷新到操作系统的内核级缓冲区,再由操作系统自主刷新到输出设备中

如果将前面的代码修改为如下,观察效果:

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

int main()
{
    close(1);
    int fd1 = open("test1.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd2 = open("test2.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd3 = open("test3.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    int fd4 = open("test4.txt", O_WRONLY | O_TRUNC | O_CREAT, 0666);
    
    printf("%d\n", fd1);
    printf("%d\n", fd2);
    printf("%d\n", fd3);
    printf("%d\n", fd4);

    return 0;
}

编译运行上面的代码后可以看到结果如下:

可以看到此时尽管没有fflush(stdout)也可以正常将内容输出到test1.txt,主要原因是当程序即将结束时,会进行缓冲区自动刷新,相当于自动进行了一次fflush(stdout),而前面调用close(fd)时之所以没有刷新缓冲区是因为系统内部的文件已经被关闭,语言级的缓冲区无法将内容通过系统调用刷新到系统内核级缓冲区

所以,由上面的结果可以推出,在C语言中,文件结构FILE肯定包含文件描述符和缓冲区,而C语言的所有输入输出函数本质也是拷贝函数,只是源头或者目标是其结构体中的缓冲区,对应的源码如下:

struct _IO_FILE {
    int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
     //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ 
    char* _IO_read_ptr; /* Current read pointer */ 
    char* _IO_read_end; /* End of get area. */ 
    char* _IO_read_base; /* Start of putback+get area. */
    char* _IO_write_base; /* Start of put area. */
    char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */
    struct _IO_marker *_markers;
    struct _IO_FILE *_chain;
    int _fileno; //封装的文件描述符#if 0
    int _blksize;
#else
    int _flags2;
#endif
    _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1];
 /* char* _save_gptr; char* _save_egptr; */
    _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

子进程与缓冲区

前面演示的都是只有一个进程打开关闭文件,如果此时在自动刷新语言级别的缓冲区之前创建了一个子进程,观察下面代码的执行效果:

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

int main()
{
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
    dup2(fd, stdout->_fileno);
    // C库函数
    printf(" hello printf\n");
    fprintf(stdout, " hello fprintf\n");
    const char *message = " hello fwrite\n";
    fwrite(message, 1, strlen(message), stdout);

    // 系统调用
    const char *w = " hello write\n";
    write(1, w, strlen(w));

    // 创建子进程
    fork();
    return 0;
}

编译运行上面的代码,查看log.txt文件中的内容:

可以看到,除了系统调用接口write函数以外,其他的C语言库函数均出现两次打印的情况,本质就是因为语言级缓冲区自动刷新到系统内核级缓冲区机制,而因为fork创建了子进程,所以子进程在不修改的情况下会共享父进程中的所有内容,包括语言级缓冲区,所以在最后程序结束时,因为父进程没有刷新语言级缓冲区,所以创建子进程之后,父子进程都要进行一次刷新操作,所以父子进程都通过系统调用接口将数据写入到内核级缓冲区中,而之所以write函数没有写入两次,就是因为父进程已经调用完成write函数了,系统内核级缓冲区已经有对应的内容,所以此时子进程就不需要再调用了,最后由操作系统决定将所有内容刷新到文件中

手撕一个简单的shell(版本2)

判断重定向命令与截取

在第一个版本中已经实现了shell基本的功能,但是前面并没有解决重定向问题,本次版本中主要实现重定向的效果

首先观察一条命令目的是将指定内容重定向到指定文件时的基本结构,以ls+重定向为例:

// 输出重定向
ls -l > test.txt
// 追加重定向
ls -l >> test.txt
// 输入重定向
ls -l < test.txt

可以看到,重定向时需要使用到三个字符:>>><,所以可以通过判断这三个字符判断是否是重定向,接下来的问题就转化为如何判断

在前面获取用户输入的命令时,将用户的输入当成一串字符串看待,接下来在另一个函数中处理该字符串(对字符串进行切割),所以判断其中的字符就需要在切割字符串时处理,并且需要在调用strtok函数切割前处理

可以考虑从尾部向前遍历字符串,如果遇到了三个字符中的其中一个就停止,在这个过程中,需要注意两个字符>>>,在Linux中,对于追加重定向的符号>>来说,不允许两个>中含有空格,所以判断是>后还需要判断前一个字符是不是>,所以判断逻辑主要分成两种:

  1. 是否是>
    1. 再判断前一个字符是否是>
  2. 是否是<
这里需要注意出现的先后,不要单独将 >>拿出来判断,此时如果先写判断是否是 >的逻辑且判断 >>else if的分支时,就会出现判断 >>始终无法进入

反向遍历到三个符号中的一个之后进入对应分支中就可以开始处理分割问题,在C语言中,一个字符串的结尾即为\0,所以可以将指定位置置为\0达到分割的效果,分割分为三种情况:

  1. >:将>位置置为0,此时前面的内容即为普通命令,后面的内容即为文件名
  2. >>:将两个>位置置为0,此时前面的内容即为普通命令,后面的内容即为文件名
  3. <:将<位置置为0,此时前面的内容即为普通命令,后面的内容即为文件名

需要注意,获取到的文件名不一定一开始就是有效字符,还需要处理第一个有效字符前的空格问题,所以这里可以考虑使用一个宏函数或者内联函数解决,基本思路就是一直遍历到第一个有效字符为止:

宏函数如下:

// 跳过空白字符
#define Trim(pos) do {\
    while(isspace(*pos))\
    {\
        pos++;\
    }\
}while(0)
上面的宏函数中使用 do...while(0)结构只是确保主体逻辑代码被代码块包裹,使结构更加清晰

内联函数如下:

// 二级指针
inline void trim(char** pos) 
{
    while(isspace(*(*pos)))
    {
        (*pos)++;
    }
}

指针引用:

// 指针引用
inline void trim(char*& pos)
{
    while(isspace(*pos))
    {
        pos++;
    }
}

示例代码如下:

// 重定向状态
enum redirStatus
{
    NONE,
    APPEND,
    TRUNCATE,
    INPUT 
};
// 重定向状态码
int redirCode;
// 文件名
char* filename;

void parseInput()
{
    // ...

    // 重定向
    redirCode = NONE;
    filename = nullptr;

    printf("command start: %s\n", input_arr);
    // 判断是否存在重定向符号
    size_t end = strlen(input_arr) - 1;
    while(end)
    {
        if(input_arr[end] == '>')
        {
            // 判断前面一个字符是否是>
            if(input_arr[end - 1] == '>')
            {
                redirCode = APPEND;
                // 切割字符串
                input_arr[end - 1] = 0;
                input_arr[end] = 0;
                // 获取文件名,重定向符的下一个位置
                filename = &input_arr[end+1];
                // 去除空格
                trim(filename);
                // 查找到重定向符号后就可以停止遍历
                break;
            }
            else 
            {
                // 输出重定向
                redirCode = TRUNCATE;
                // 切割字符串
                input_arr[end] = 0;
                // 获取文件名
                filename = &input_arr[end+1];
                // 去除空格
                trim(filename);
                // 查找到重定向符号后就可以停止遍历
                break;
            }
        }
        else if(input_arr[end] == '<')
        {
            // 输入重定向
            redirCode = INPUT;
            // 切割字符串
            input_arr[end] = 0;
            // 获取文件名
            filename = &input_arr[end+1];
            // 去除空格
            trim(filename);
            // 查找到重定向符号后就可以停止遍历
            break;
        }
        else 
        {
            // 一直向前查找
            --end;
        }
    }

    printf("redir: %d\n", redirCode);
    printf("filename: %s\n", filename);
    printf("command end: %s\n", input_arr);

    // 拆分读取的字符串
    for(char* ch = strtok(input_arr, " "); (bool)ch; ch = strtok(nullptr, " "))
    {
       global_argv[global_argc++] = ch; 
    }
}

执行重定向

执行重定向不可以使用父进程执行,如果使用父进程执行,则后续执行的指令都会受到重定向的影响,所以考虑使用子进程执行

但是这里涉及到一个问题:程序替换时,被替换的代码会受到重定向的影响吗?答案是会的,因为重定向本质改的是files_struct结构体,而该结构体存在于进程中,当前这个进程是子进程,程序替换本质只是替换子进程的代码和数据,但是不会替换进程PCB,所以重定向会影响到被替换的代码

本质重定向就是改变输出文件,所以对于三种重定向来说,只需要在不同类型的重定向中执行不同的重定向逻辑即可:

  1. 输出重定向:以覆写以及不存在新建的模式打开文件并将stdout位置更改为该文件
  2. 追加重定向:以追加写以及不存在新建的模式打开文件并将stdout位置更改为该文件
  3. 输入重定向:以只读模式打开文件并将stdin位置更改为该文件
下面的代码中涉及到一些错误情况的处理

示例代码如下:

void executeProgram()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 判断是哪一种重定向
        if(redirCode == TRUNCATE)
        {
            // 输出重定向
            // 1. 打开文件
            // 确保文件不为空
            if(filename)
            {
                int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
                if(fd < 0)
                {
                    exit(FILEOPENFAIL);
                }
                // 2. 重定向stdout文件为新创文件
                dup2(fd, stdout->_fileno);
            }
            else 
            {
                exit(FILENOTEXISTS);
            }
        }
        else if(redirCode == APPEND)
        {
            if(filename)
            {
                int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
                if(fd < 0)
                {
                    exit(FILEOPENFAIL);
                }
                // 2. 重定向stdout文件为新创文件
                dup2(fd, stdout->_fileno);
            }
            else
            {
                exit(FILENOTEXISTS);
            }
        }
        else if(redirCode == INPUT)
        { 
            if(filename)
            {
                int fd = open(filename, O_RDONLY);
                if(fd < 0)
                {
                    exit(FILEOPENFAIL);
                }
                // 2. 重定向stdout文件为新创文件
                dup2(fd, stdin->_fileno);
            }
            else
            {
                exit(FILENOTEXISTS);
            }
        }
        // 重定向后恢复redirCode标记便于下次使用
        redirCode = NONE;
        // 程序替换
        execvpe(global_argv[0], global_argv, global_env);
        exit(0);
    }
    // ...
}

在上面代码中,尽管开启了文件,但是可以不需要关闭文件,因为当一个进程结束并被父进程回收之后,该进程的PCB以及相关的文件结构都会被释放,此时文件就会被自动关闭,所以在Linux中,文件描述符的声明周期随着进程的结束而结束

简单实现stdio.h中的文件相关操作

本次简单实现主要实现FILE结构体、fwrite函数、fopen函数、fflush函数和fclose函数

FILE结构体

根据前面的学习,可以知道该结构体主要会存在缓冲区和文件描述符

而因为C语言的缓冲区有三种缓冲方式,所以定义一个flag标记缓冲区刷新模式,其值有如下三种:

enum 
{
    LINE, // 行刷新
    FULL, // 满刷新
    NONE, // 不缓冲
};

所以此时的FILE定义为如下:

#define SIZE 1024
struct myFILE
{
    int _fileno; // 文件描述符
    int _flag; // 刷新方式标记
    char _buffer[SIZE]; // 缓冲区
    int capacity; // 缓冲区容量
    int size; // 缓冲区有效数据个数
};

typedef struct IO_FILE myFILE; 

fopen函数

在C语言中,fopen函数本质就是打开文件,所以使用到两个参数:

  1. 文件名:定义为const char* filename
  2. 打开模式:定义为const char* mode

在函数中,为了代码健壮性可以考虑先判断filename是否为空,如果不为空就调用系统接口打开文件,但是打开文件需要考虑打开方式mode,本次主要考虑三种:wra,依次判断并打开文件即可

接下来考虑函数的返回值问题,对于标准库中的fopen函数来说,其返回值为FILE*,但是如果在fopen函数中直接返回一个局部变量就会出现野指针问题,所以考虑在函数内部返回一个在堆中开辟的空间

myFILE* myfopen(const char* filename, const char* mode)
{
    // 判断文件是否存在
    if(!filename)
    {
        return NULL;
    }
    // 判断模式是否为空
    if(!mode)
    {
        return NULL;
    }

    // 开辟返回的空间
    myFILE* myfile = (myFILE*)malloc(sizeof(myFILE));
    // 文件和模式都不为空时可以开始打开文件
    // 判断是何种模式
    if(strcmp(mode, "w") == 0)
    {
        // 写模式打开
        int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
        myfile->_fileno = fd;
        myfile->_flag = LINE;
        myfile->_capacity = SIZE;
        myfile->_size = 0;
    }
    else if(strcmp(mode, "r") == 0)
    {
        // 写模式打开
        int fd = open(filename, O_RDONLY); 
        myfile->_fileno = fd;
        myfile->_flag = LINE;
        myfile->_capacity = SIZE;
        myfile->_size = 0;
    }
    else if(strcmp(mode, "a") == 0)
    {
        // 写模式打开
        int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND); 
        myfile->_fileno = fd;
        myfile->_flag = LINE;
        myfile->_capacity = SIZE;
        myfile->_size = 0; 
    }
    
    return myfile;
}

fwrite函数

写函数本质是将函数的内容拷贝到语言级缓冲区,拷贝过程中需要注意要从缓冲区已有的内容之后开始拷贝,接着如果是行刷新就进行刷新操作,最后返回写入内容个数

本次只考虑行刷新
int myfwrite(myFILE* stream, const void* ptr, int num)
{
    // 写函数的本质是将数据拷贝到缓冲区
    memcpy(stream->_buffer+stream->_size, ptr, num);
    stream->_size += num;

    // 判断刷新方式
    if(stream->_flag == LINE && stream->_size > 0 && stream->_buffer[stream->_size - 1] == '\n')
    {
        myfflush(stream);
    }

    return num;
}

fflush函数

对于刷新函数来说,只要语言级缓冲区有内容就可以调用系统接口write函数将语言级缓冲区的内容写入内核级缓冲区,写完成后将缓冲区的大小更新为0。可以考虑调用fsync函数每一次刷新就将内核级缓冲区的内容刷新到文件中

void myfflush(myFILE* stream)
{
    if(stream->_size > 0)
    {
        // 刷新的本质是将缓冲区中的数据拷贝到系统缓冲区
        write(stream->_fileno, stream->_buffer, stream->_size);
        // 刷新到外部设备
        fsync(stream->_fileno);
        // 更新缓冲区的_size
        stream->_size = 0;
    }
}

fclose函数

关闭函数需要注意关闭之前需要刷新

void myfclose(myFILE* stream)
{
    // 关闭之前需要自动刷新
    if(stream->_size > 0)
    {
        myfflush(stream);
    }

    close(stream->_fileno);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2221379.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue+TypeScript+SpringBoot的WebSocket基础教学

成品图&#xff1a; 对WebSocket的理解&#xff08;在使用之前建议先了解Tcp&#xff0c;三次握手&#xff0c;四次挥手 &#xff09;&#xff1a; 首先页面与WebSocket建立连接、向WebSocket发送信息、后端WebSocket向所有连接上WebSoket的客户端发送当前信息。 推荐浏览网站…

燕山大学23级经济管理学院 10.18 C语言作业

燕山大学23级经济管理学院 10.18 C语言作业 文章目录 燕山大学23级经济管理学院 10.18 C语言作业1C语言的基本数据类型主要包括以下几种&#xff1a;为什么设计数据类型&#xff1f;数据类型与知识体系的对应使用数据类型时需要考虑的因素 21. 逻辑运算符2. 真值表3. 硬件实现4…

设计模式(UML图、类之间关系、设计原则)

目录 一.类的UML图 1.类的UML图 2.类之间的关系 2.1 继承关系&#xff1a; 2.2关联关系 2.2.1单项关联 2.2.2双向关联 2.2.3自关联 2.3聚合关系 2.4组合模式 2.5依赖关系 二、设计三原则 2.1单一职责原则 2.2开放封闭原则 2.3依赖倒转原则 一.类的UML图 1.类的…

考研篇——数据结构王道3.2.2_队列的顺序实现

目录 1.实现方式说明2.代码实现2.12.1.1 代码12.1.2 代码22.1.3 代码3 2.22.2.1 代码42.2.5 代码52.2.6 代码6 总结 1.实现方式说明 多在选择题中考察 队尾指针&#xff08;rear&#xff09;有两种指向方式&#xff1a; 队尾指针指向队尾元素的位置&#xff0c;队尾指针指向…

9个3D直播场景推荐

在科技日新月异的今天&#xff0c;3D直播技术正逐步渗透至文旅领域&#xff0c;为游客带来前所未有的沉浸式体验。以下是vLive虚拟直播九个精心推荐的文旅3D直播场景&#xff0c;它们不仅展现了各地独特的文化魅力&#xff0c;还通过高科技手段让游客仿佛穿越时空&#xff0c;身…

ArcGIS应用指南:多尺度渔网创建

在GIS中&#xff0c;创建渔网矢量文件是GIS中的一项常见任务&#xff0c;通过将研究区域划分为规则的网格&#xff0c;可以更精细地分析和管理城市空间数据。本文以厦门市行政区为例&#xff0c;详细介绍了如何创建不同尺度的渔网矢量网格&#xff0c;以适应不同区域的发展特点…

代码随想录算法训练营第46期Day35

leetcode.452.用最少数量的箭射爆气球 class Solution { public:static bool cmp(const vector<int>& a,const vector<int>& b){//对Xend进行排序return a[1]<b[1]; } //这个题比较好贪&#xff0c;我们每次射只需要射Xend就可以了&#xff0c;然后用此…

OpenCL内存模型

OpenCL将内存划分成主机内存和设备内存。主机内存可在主机上使用&#xff0c;其并不在OpenCL的定义范围内。使用对应的OpenCL API可以进行主机和设备的数据传输&#xff0c;或者通过共享虚拟内存接口进行内存共享。而设备内存&#xff0c;指定是能在执行内核中使用的内存空间。…

什么是不同类型的微服务测试?

大家好&#xff0c;我是锋哥。今天分享关于【什么是不同类型的微服务测试&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; 什么是不同类型的微服务测试&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 微服务架构中的测试可以分为多种类…

【ROS2】构建导航工程

1、ROS小车组成 ROS小车由三大件组成:运动底盘、ROS主控、导航传感器。 1.1 运动底盘 运动底盘的硬件由车轮、电机(带编码器)、电机驱动器、STM32控制器、电池等组成。 涉及的知识点主要为:STM32单片机程序、机器人运动学分析 1)STM32单片机程序 单片机程序框架如下:…

在Linux命令行下载Google Drive大文件(解决Google Drive下载慢的问题)

文章目录 1、使用gdown命令2、复制链接3、替换为Linux下载链接 注意&#xff1a;在Linux命令行进行 1、使用gdown命令 wget只能下载小文件&#xff0c;大文件需要用到gdown pip install gdown# 如果不能够直接安装&#xff0c;使用以下命令 git clone https://github.com/wk…

基于Spring Boot + Vue程序员云书店系统设计与实现

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

MySQL9.0安装教程zip手动安装(Windows)

本章教程&#xff0c;主要介绍如何在Windows上安装MySQL9.0&#xff0c;通过zip方式进行手动安装。 一、下载MySQL压缩包 下载地址&#xff1a;https://downloads.mysql.com/archives/community/ 二、解压MySQL压缩包 将下载好的压缩包&#xff0c;进行解压缩&#xff0c;并且将…

要让AI(任何一款绘图AI)把一个己有风格的图片画到一个实物商品上的窍门

本教程适合midjourney, comfyui, stable diffusion 己有图片 希望生成效果 我们希望&#xff0c;在一个现实世界真实IPhone手机上可以有一个这样的小魔女作为一个手机的展示&#xff0c;同时手机处于开机状态&#xff0c;在手机的屏幕上有一个这样的戴帽子的穿蓝色小披风的小…

阿里Dataworks使用循环节点和赋值节点完成对mongodb分表数据同步

背景 需求将MongoDB数据入仓MaxCompute 环境说明 MongoDB 100个Collections&#xff1a;orders_1、orders_2、…、orders_100 前期准备 1、MongoDB数据源配置 需要先保证DW和MongoDB网络是能够联通的&#xff0c;需要现在集成任务中配置MongoDB的数据源信息。 具体可以查…

Java项目-基于springboot框架的学习选课系统项目实战(附源码+文档)

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

Linux基础命令(入门)

linux 用户 root 用户 一个特殊的管理帐户 也被称为超级用户 root已接近完整的系统控制 对系统损害几乎有无限的能力 除非必要,不要登录为 root 普通&#xff08; 非特权 &#xff09; 用户权限有限 造成损害的能力比较有限 linux的哲学思想&#xff08;优点&#xf…

vue3学习记录-组件通信

vue3学习记录-组件通信 1.父子组件通信2.兄弟组件传值2.1 以父组件为媒介2.2 发布订阅模式2.3 使用mitt2.3.1 全局使用2.3.2 局部使用 1.父子组件通信 父组件&#xff1a; <template>父组件原有的title:{{ title }}<p>---</p><com :title"title&qu…

jmeter使用文档

文章目录 一、安装使用1、下载2、bin/jmeter.properties介绍 二、windows使用1、微调&#xff08;1&#xff09;界面样式&#xff08;2&#xff09;修改语言 2、简单使用3、各组件详解&#xff08;1&#xff09;CSV 数据文件配置&#xff08;2&#xff09;BeanShell取样器 三、…

【Linux线程】Linux多线程实践:深入生产者消费者模型

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;Linux “ 登神长阶 ” &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀Linux多线程 &#x1f4d2;1. 生产者消费者模型&#x1f4dc;2. 基于BlockingQueue的生产者消…