Linux学习记录——십유 基础IO(1)


学习C语言的时候,有文件操作的知识,但那只是学到了操作函数,然后直接用即可,至于原理如何,并不清楚。这篇会逐渐写出来文件的通用知识。

当文件没被打开的时候,文件是存储在磁盘中的;当操作者使用函数打开文件后,文件会被加载进内存里。文件是由内容+属性构成的,所以文件的什么会被加载进内存?不管是什么,属性是一定要有的。加载进内存这个过程,是操作系统做的,让他这样做的是操作者引起的进程。进程令系统加载文件,那么内存中一定不会只有一个文件,多个文件在内存中的时候,操作系统对它们会进行管理,管理的方式就是先描述再组织,如同系统管理内存数据一样,对于文件,系统也会用结构体struct file来存储它们的各项数据,比如属性,以及还有指向下一个文件的指针和其他指针,所以对多个文件的管理就变成了链表的管理。这里也体现出了Linux里一切皆文件的思想。

文件描述符

在C语言中有一系列文件操作函数,Linux也一样。

在这里插入图片描述

和fopen有所不同,返回数据的类型并不是FILE*而int。如果执行无误那就返回文件描述符,错误则返回-1。描述符先不管,先看函数

在这里插入图片描述

用man查看,会发现有很多参数,函数先不管,先想一个问题,操作系统如何了解操作者传给它的标志位,也就是函数参数这些?传多个标志位的时候,我们会多设几个位置,但是如果标志位太多这样做也不行,而系统对它的处理就是把这个标志位当做位图结构,一个int的参数有32个比特位,每个比特位就表示一个传过来的参数,也就是一个标志位。

写一段相对应的代码

#include <stdio.h>  

#define one 0x1
#define two 0x2
#define three 0x4
#define four 0x8
#define five 0x10

void Print(int flags)//flags就是标志位
{
    if(flags & one) printf("hello 1\n");
    if(flags & two) printf("hello 2\n");
    if(flags & three) printf("hello 3\n");
    if(flags & four) printf("hello 4\n");
    if(flags & five) printf("hello 5\n");
}

int main()
{
    printf("------------------------\n");
    Print(one);
    Print(two);
    Print(three);
    Print(four | five);
    Print(one | four | five);
    Print(two | three | four | five);
    //上面也就是二进制位的计算
    return 0;
}

在这里插入图片描述

现在回到myfile.c,写文件相关的代码

    return 0;
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>     

#define LOG "log.txt"
 
int main()
{    
    int fd = open(LOG, O_WRONLY | O_CREAT);                                                                             
    if(fd == -1)  
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    close(fd);    
    return 0;
}

两个参数的意思就是只读,如果读不了那就创建。

在这里插入图片描述

但是创建出来的文件的权限不够好,可以添加第三个参数,表示文件权限应该是多少。

int main()
{
    umask(0);
    int fd = open(LOG, O_WRONLY | O_CREAT, 0666);

还可以使用umask改一下权限编码。

然后往文件里写东西

int main()
{
    umask(0);
    int fd = open(LOG, O_WRONLY | O_CREAT, 0666);
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    const char* msg = "hello world";
    int cnt = 5;
    while(cnt)
    {
        char line[128];
        snprintf(line, sizeof(line), "%s, %d\n", msg, cnt);
        write(fd, line, strlen(line) + 1);
        cnt--;
    }
    close(fd);
    return 0;
}

往缓冲区里写,然后write读取。正常地运行,但是如下图

在这里插入图片描述

在这里插入图片描述

还有一个问题,O_CREAT会延续之前的内容,不会清空文件而继续写。

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

在这里插入图片描述

在这里插入图片描述

所以当进行文件写入时,底层会传递O_等选项。C语言还有a用来追加,Linux也有O_APPEND用来追加,但必须先写文件才能追加。

int fd = open(LOG, O_WRONLY| O_CREAT | O_APPEND, 0666); 

读取文件O_RDONLY,int fd = open(LOG, O_RDONLY);

int main()
{
    umask(0);
    //int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd = open(LOG, O_RDONLY);
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    }
    else printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno));
    char buffer[1024];
    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
    if(n > 0)
    {
       buffer[n] = '\0';
       printf("%s\n", buffer);
    }
    close(fd);
    return 0;
}

在这里插入图片描述
所以像fopen,fclose,open,close这些,就是库函数和系统调用,库函数封装了系统调用,fopen里就封装了系统的open。

----------------------------
无论是打开还是写入文件,都是用户层在访问硬件,通过系统调用来访问,他们不能越过操作系统去访问。无论哪个语言,文件操作的时候都是调用OS的open,write等函数,只不过各个语言封装形式有所不同。

---------------------------

刚才这个程序,如果正常运行,fd会得到返回值,但是为什么是3?

任何一个进程在启动的时候都会默认打开3个文件,标准输入,标准输出,标准错误,它们本质上都是文件,而它们在语言层面的表现就是像C语言中stdin,stdout,stderr,C++中cin,cout, cerr一样,不过C++里那是一个类。

#include <iostream>
#include <cstdio>

int main()
{
    //Linux里一切皆文件,向显示器打印本质就是说向文件写入
    //C
    printf("printf->stdin\n");
    fprintf(stdout, "printf->stdout\n");
    fprintf(stderr, "printf->stderr\n");

    //C++
    std::cout << "cout->cout" << std::endl;
    std::cerr << "cerr->cerr" << std::endl;
}

所有打印的语句都会出现在显示器上。三个标准文件是这样的

标准输入—设备文件–>键盘文件
标准输出—设备文件–>显示器文件
标准错误—设备文件–>显示器文件

所以3之前的012对应的就是标准输入、输出、错误,分别占据0 1 2,所以我们得到的就是3。这几个数字,也就是文件描述符,也就是open返回值,是数组的下标。这里为什么会出现数组?

在系统底层,用户通过进程命令操作系统打开文件,会建立一个进程的pcb,在内存中也会建立文件的struct file,只要找到struct file,就能找到文件的内容和属性。一个进程可以打开多个文件,进程会再建立一个files_struct结构体,里面有一个struct file类型的指针数组,每一个元素都是struct file的指针,用来找到这个进程打开的文件,进程pcb里又有一个struct files_struct* 类型的指针指向files_struct结构体,所以进程就可以找到所有自己打开的文件了,数组下标也就能够解释了。

当用write往文件写入内容时,这个进程会找到用户给的文件描述符所对应的文件,每个文件会链接一个缓冲区,write函数把要写的内容拷贝到这个缓冲区,至于缓冲区什么时候刷新到文件里,由系统决定。所以这些IO类write等函数,本质上是在用户空间和内核空间进行数据的来回拷贝。

如何理解“一切皆文件”

一个进程pcb通过一个结构体,结构体里有数组,通过下标访问每个文件的结构体struct file。file有文件的内容+属性,还有缓冲区。不止这些,像键盘,显示器,网卡,显卡等这些外设,它们都有自己的IO函数,比如读写函数,而Linux下每个文件结构体里都有一些函数指针,来指向这些外设自己的IO函数。在上层,系统有read,write等函数,前面写到过,它们的本质是向缓冲区里拷贝数据,然后函数指针调用底层不同设备的方法,那么就可以把数据放到设备里。这就是一切皆文件。底层无论有什么差异,最终都变成一个公式,函数指针调用函数去操作数据,把缓冲区的数据放到设备里。

继续深入

FILE是什么?为什么C语言中文件操作函数的类型是FILE

FILE是一个结构体,是由C语言的库提供的。

在这里插入图片描述

底层有系统创建的文件结构体,再往上一层就是系统调用接口,比如read,write等接口,再往上就是语言库,C语言里有FILE相关的结构体,里面有在库中文件的所有属性。C语言想用fopen函数,就需要FILE结构体,想要调用open函数就得需要fd。所以FILE结构体必定封装了fd,这样才能用系统调用接口。

同理,其他语言的文件库也必须有文件描述符。

回到描述符的问题,虽然现在已经知道这是数组下标了,但还是有问题的。打开多个文件,依次打印3456789后,如果把3这个文件给去掉,再添加一遍,那么还会是3吗?还是10?

写一些代码

    int fd1 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd2 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd3 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd4 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);

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

在这里插入图片描述

在这之前加上这一句fclose(stdin);

在这里插入图片描述

0就出来了。再关闭stderr就会这样

在这里插入图片描述

进程中,文件描述符的分配规则:在文件描述符表中,最小的,没有被使用的数组元素,分配给新文件。

现在针对1做一些操作

在这里插入图片描述

关闭标准输出

在这里插入图片描述

没有输出,并且这时候LOG,也就是log.txt的内容却是you can see me!。本来要打印到显示器的内容打印到了文件了,也就是发生了重定向。在语言层上,printf是向标准输出打印,而这时候标准输出关掉了,取而代之的是log.txt,所以就发生了重定向。而这个过程上层并不知道。

所以重定向的原理就是在上层无法感知的情况下,在OS内部,更改进程对应的文件描述符表中特定下标的指向。

除了输入和输出重定向,还有追加重定向。只要O_TRUNC改成O_APPEND就可。

像stdout,cout都是向1号描述符对应的文件打印,而cerr则是2;输出重定向只改的是1号对应的指向,2号不影响。

现在写个代码,把常规信息打印到log.normal,异常消息打印到log.error中。

在这里插入图片描述

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

#define LOG "log.txt"
#define LOGN "lognormal.txt"
#define LOGE "logerror.txt"

int main()
{
    close(1);
    open(LOGN, O_WRONLY | O_CREAT | O_APPEND, 0666);
    close(2);
    open(LOGE, O_WRONLY | O_CREAT | O_APPEND, 0666);
    printf("hello  printf->stdout\n");
    printf("hello  printf->stdout\n");
    fprintf(stdout, "fprintf->stdout\n");
    fprintf(stdout, "fprintf->stdout\n");
    fprintf(stderr, "fprintf->stderr\n");
    fprintf(stderr, "fprintf->stderr\n");
    return 0;
}

要是想都放到一个文件里,./a.out > log.txt >2&1

但是这样不方便。有dup2函数可以重定向,两个参数为oldfd,newfd,newfd会是oldfd的拷贝,所以最终只有oldfd这个数值,比如要把1重定向到fd上,那么oldfd就是fd,1就是newfd。

    int fd = open(LOGN, O_CREAT | O_WRONLY | O_APPEND, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd, 1);
    printf("11111111\n");
    close(fd);

如何理解缓冲区

int main()
{
    fprintf(stdout, "hello fprintf\n");
    const char* msg = "hello write\n";
    write(1, msg, strlen(msg));
    return 0;
}

在这里插入图片描述

如果在最后加上fork()。

int main()
{
    fprintf(stdout, "hello fprintf\n");
    const char* msg = "hello write\n";
    write(1, msg, strlen(msg));
    fork();
    return 0;
}

在这里插入图片描述

为什么会出现三行?去掉fork就是正常的两行,所以一定是fork影响了这里的结果。

在操作系统里,有进程pcb,文件结构体,指向标准输入输出等的指针,而在这上层,有C语言库。当我们自己的程序调用fprintf后,会通过struct FILE调用系统调用接口,C库会封装fd,也还有给我们准备好的缓冲区。当我们写入数据,会先把数据放到C的缓冲区里,然后函数返回,暂时还没有把数据放到系统内部的缓冲区。至于数据如何刷新给系统,C库有自己的策略,按照自己的策略去调用write给对应的文件缓冲区里。

C库常有的策略有:

1、无缓冲(也就是直接调用write把数据放到系统中)
2、行缓冲(遇到\n就把\n这之前的数据放到系统中)
3、全缓冲(全部写完后在放到系统中)

显示器采用行缓冲。普通文件采用全缓冲。

缓冲区能够节省调用者的时间,让它有更多的时间去做别的事,因为系统调用是有时间代价的,这样交给C库缓冲区后,进程可以继续忙自己的事。而C库会把所有要缓冲的数据都输入进来后,在送给系统,节省时间。

那么C的缓冲区在哪里?缓冲区在FILE结构体里,比较复杂。在操作者使用fopen等函数时,FILE结构体就会在库中建立好。

在上面的代码中,write是系统调用,所以和C的缓冲区无关,它直接去找系统。像显示器打印时,两条语句都是带\n的,所以在fork之前,fprintf的内容就已经通过行缓冲打印出来了,那么fork就无用;而当重定向到文件里后,就是全缓冲策略了,fprintf的内容会在C的缓冲区里,当创建子进程后,刚才的就是父进程写入的数据,而子进程又会写一遍,当刷新时,就出来了两次fprintf的内容。

模拟实现FILE

链接: main.c

链接: mystdio.c

链接: mystdio.h

mystdio.h

#pragma once

#include <stdio.h>

#define NUM 1024
#define BUFF_NONE 0x1
#define BUFF_LINE 0x2
#define BUFF_ALL  0x4

typedef struct _MY_FILE
{
    int fd;
    char outputbuffer[NUM];
    int flags;
    int current;
}MY_FILE;

MY_FILE* my_fopen(const char* path, const char* mode);
size_t my_fwrite(const void* ptr, size_t size, size_t nmemb, MY_FILE* stream);
int my_fclose(MY_FILE* fp);
int my_fflush(MY_FILE* ff);

main.c

#include "mystdio.h"
#include <string.h>
#include <unistd.h>

#define MYFILE "log.txt"

int main()
{
    MY_FILE* fp = my_fopen(MYFILE, "w");
    if(fp == NULL) return 1;
    const char* str = "hello myfwrite";
    int cnt = 500;
    //操作文件
    while(cnt)
    {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "%s:%d", str, cnt--);
        size_t size = my_fwrite(buffer, strlen(buffer), 1, fp);
        sleep(1);
        printf("成功写入: %lu个字节\n", size);
        //my_fflush(fp);]
        if(cnt % 5 == 0)
        {
            my_fwrite("\n", strlen("\n"), 1, fp);
        }
    }
    my_fclose(fp);
    return 0;
}

mystdio.c

#include "mystdio.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <malloc.h>
#include <unistd.h>
#include <assert.h>

MY_FILE* my_fopen(const char* path, const char* mode)
{
    //1、识别标志位
    int flag = 0;
    if(strcmp(mode, "r") == 0) flag |= O_RDONLY;
    else if(strcmp(mode, "w") == 0) flag |= (O_CREAT | O_WRONLY | O_TRUNC);
    else if(strcmp(mode, "a") == 0) flag |= (O_CREAT | O_WRONLY | O_APPEND);
    else
    {
        //r+, w+, a+
        ;
    }

    //2、尝试打开文件 
    mode_t m = 0666;
    int fd = 0;
    if(flag & O_CREAT) fd = open(path, flag, m);
    else fd = open(path ,flag);
    if(fd < 0) return NULL;

    //3、给用户返回MY_FILE对象,需要先进行构建
    MY_FILE* mf = (MY_FILE*)malloc(sizeof(MY_FILE));
    if(mf == NULL)
    {
        close(fd);
        return NULL;
    }

    //4、初始化MY_FILE对象
    mf->fd = fd;
    mf->flags = BUFF_LINE;
    memset(mf->outputbuffer, '\0', sizeof(mf->outputbuffer));//初始化缓冲区
    mf->current = 0;

    //5、返回打开的文件
    return mf;
}

//返回的是一次实际写入的字节数
size_t my_fwrite(const void* ptr, size_t size, size_t nmemb, MY_FILE* stream)
{
    //1、缓冲区满就写入
    if(stream->current == NUM) my_fflush(stream);

    //2、根据缓冲区剩余情况进行数据拷贝
    size_t user_size = size * nmemb;
    size_t my_size = NUM - stream->current;
    size_t writen = 0;
    if(my_size >= user_size)
    {
        memcpy(stream->outputbuffer + stream->current, ptr, user_size);
        //3、更新计数器字段
        stream->current += user_size;
        writen = user_size;
    }
    else//有多少空间拷多少
    {
        memcpy(stream->outputbuffer + stream->current, ptr, my_size);
        //3、更新计数器字段
        stream->current += my_size;
        write = my_size;
    }

    //4、开始计划刷新
    //不发生刷新的本质就是不进行写入,IO,调用系统调用。所以写函数调用就会非常快,数据会暂时保存在缓冲区中
    //可以在缓冲区积压多份数据,统一进行刷新写入,本质就是一次IO可以IO更多的数据
    if(stream->flags & BUFF_ALL)
    {
        if(stream->current == NUM) my_fflush(stream);
    }
    else if(stream->flags & BUFF_LINE)
    {
        if(stream->outputbuffer[stream->current - 1] == '\n') my_fflush(stream);
    }
    else
    {
        ;
    }
    return writen;
}

int my_fflush(MY_FILE* ff)
{
    assert(ff);
    //将用户缓冲区中的数据,通过系统调用接口,冲刷给OS
    write(ff->fd, ff->outputbuffer, ff->current);
    ff->current = 0;
    fsync(fp->fd);
    return 0;
}

int my_fclose(MY_FILE* fp)
{
    assert(fp);
    //1、冲刷缓冲区
    if(fp->current > 0) my_fflush(fp);
    //2、关闭文件
    close(fp->fd);
    //3、释放堆空间
    free(fp);
    //4、指针置为NULL
    fp = NULL;
    return 0;
}

当使用文件函数时,系统会先把数据放到缓冲区里,系统调用接口会根据缓冲区刷新策略用fd把数据放到系统内部的文件缓冲区里,最后根据策略把数据从缓冲区刷新到磁盘。

用户也可以强制刷新,把内核的文件缓冲区的数据刷新出来。fsync接口。

结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值