Linux(基础IO)_1

打开文件本质就是将需要的文件属性加载到内存当中。os一定会同时存在大量的被打开的文件,那么操作系统如何管理这些被打开的文件呢?先描述再组织。先描述本质就是要构建在内存的文件结构体,然后用链表等数据结构将这些文件对象串联到一起,对文件的管理就变成了对链表的增删查改。

每一个被打开的文件,都要在os内对应文件结构体的实例化对象,可以将struct file结构体用某种数据结构连接接起来->在os内部,对被打开的文件进行管理,就会被转换成为对链表的增删查改。

文件可以分钟两大类,一是磁盘文件,二是内存文件(被打开的文件)。文件是由进程(用户)让os打开的,所以我们之前的所有文件操作都可以转换成为进程和被打开文件的关系。进程就是struct task_struct,所以二者的关系可以理解成为进程控制块和文件对象之间的关系。

语言层面文件读取

fopen:w默认写方式打开文件,如果文件不存在就创建它。默认如果只是打开,文件内容会自动被清空,同时,每次进行写入的时候,都会从最开始进行写入。a不会清空文件,而是每一次写入都是从文件尾部进行写入的,追加。

int fputs(const char *s, FILE *stream):向特定的文件流当中写入

int fprintf(FILE *streamint fputs(const char *s, FILE *stream), const char *format, ...):参数比printf多了一个文件,我们的printf默认是向显示器打印消息,而fprintf可以向指定的文件流打印消息,并不冲突,因为Linux当中一切皆是文件。

int sprintf(char *str, const char *format, ...):将格式化的信息写入自定义的缓冲区里面str

  int main()    
  {    
    FILE *fp = fopen(LOG, "w");    
    if(fp == NULL)    
    {    
      perror("fopen");//fopem:xxx    
E>    return ;    
    }    
    //正常进行文件操作    
    const char *msg = "hello world";    
    int cnt = 5;    
    while(cnt--)    
    {    
      //fputs(msg, fp);//往fp中写入msg    
      //fprintf(fp,"%s :%d:wjj",msg,cnt);    
      //fprintf(stdout,"%s :%d:wjji\n",msg,cnt);    
      char buffer[256];    
      snprintf(buffer,sizeof(buffer),"%s :%d:wjj\n",msg,cnt);    
      fputs(buffer,fp);//将写入buffer的内容放文件当中                                                                           
    }    
    fclose(fp);    
    return 0;    
  }  

char *fgets(char *s, int size, FILE *stream):从文件流当中按行读取对应的文件,将读到的文件放入缓冲区当中。读取 (size-1) 个字符或到达换行符或文件末尾。

系统层面的文件读取 

os一般会如何让用户给自己传递标志位的?

我们这么做的?int func(int flag_1,flag_2,flag_3·····)。

系统这么做的?int func(int flag),其中flag是一个整数,意味着flag有32个比特位,我们可以用一个比特位表示一个标志位,我们一个int就可以同时至少传递32个标志位,这就是位图。

#include <stdio.h>                                                                                                             
#define ONE 0x1      
#define TWO 0x2      
#define THREE 0x4      
#define FOUR 0x8      
#define FIVE 0x10      
// 0000 0000 0000 0000 0000 0000 0000 0000      
void Print(int 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);      
    printf("--------------------------\n");      
    Print(TWO);      
    printf("--------------------------\n");      
    Print(FOUR);      
    printf("--------------------------\n");      
    Print(ONE|TWO);      
    printf("--------------------------\n");      
    Print(ONE|TWO|THREE);      
    printf("--------------------------\n");      
    Print(ONE|TWO|THREE|FOUR|FIVE);      
    printf("--------------------------\n");      
    return 0;      
}

未来封装一个函数可以设置多个标志位,而这里每一个标志位都是一个宏,内部对宏值做判断,外部想传几个标志位就传几个标志位。

ssize_t write(int fd, const void *buf, size_t count):向文件描述符fd写入一个缓冲区buf数据,写入的大小是count,返回值是实际写入的数值大小。

int open(const char *pathname, int flags):pathname是文件路径加文件名,flags是打开文件的选项,一次可以传递多个标志位,O_ALLEND、O_CREAT等这些传递的参数其实都是宏,每一个宏值在比特位都是不重叠的。O_CREAT文件存在就打开不存在创建,O_WRONLY就是以只读的方式打开,如果没有该文件就会打开失败返回的文件描述符就是-1。O_TRUNC是写入文件的时候对文件做清空。O_APPEND是追加的意思,并不会追加写的意思,如果需要追加写就得采用O_APPEND | O_WRONLY的方式,所以可以理解O_APPEND或者O_TRUNC是一种(写入)策略,前者是追加(写入)策略,后者是清空(写入)策略。

ssize_t read(int fd, void *buf, size_t count):从一个文件描述符中当将我们的数据读取到buffer里面,读取的数据大小是count个。

int open(const char *pathname, int flags, mode_t mode):mode是打开文件的类型

open() and creat() return the new file descriptor, or -1 if an error occurred。系统接口的返回值是int,这个整数称为文件描述符。而不是file*。

int main()    
  {    
    int fd = open(LOG,O_WRONLY);    
    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));    
    return 0;    
  }


当文件不存在的时候我们采用O_CREAT创建文件会发现文件的权限是乱码的,因为我们在Linux当中创建文件是需要有对应的权限的,所以必须添加第三个参数mode_t mode,这个参数是如果当我们想要新建一个文件的时候,它的权限应该是多少呢?

如果我们想要在创建文件的时候不受到umask的影响,直接创建出我想要的权限的文件呢?umask函数接口可以影响当前进程启动的时候属于他自己的umask,那么到底听从他自己的umask还是听从系统的umask呢?采用就近原则,自己设置的umask更近就听从自己的umask。umask(0):将自己的权限掩码设置为0。

int main()      
{      
  umask(0);      
  //int fd = open(LOG,O_WRONLY|O_CREAT,0666);默认不会对原始文件的内容做清空,向文件写入但是不清空.
  //int fd = open(LOG,O_WRONLY|O_CREAT|O_TRUNC,0666);向文件写入的时候先清空再写入
  //int fd = open(LOG,O_WRONLY|O_CREAT|O_APPEND,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 wjj";      
  int cnt = 5;      
  while(cnt)      
  {      
    char line[128];                                                                                                  
    snprintf(line,sizeof(line),"%s,%d\n",msg,cnt);  
    write(fd,line,strlen(line));//这里的strlen不需要加上1,\0是c语言的规定,不是文件的规定。所以一般我们在进行文件写入的时候我们不需要把\0带进去,未来从文件中读取字符串的时候,再把\0加上就可以了构成c风格的字符串。文件用的是系统调用接口是不认识我们的\0的,或者说文件会把\0解释成乱码。
    cnt--;  
  }  
  close(fd);  
  return 0;  
} 

c语言和这些系统调用接口的关系?库函数底层调用了系统调用接口,是上下层的关系。

所有的语言要进行文件操作必须要调用系统调用接口,只不过不同的语言对系统的调用接口做了不同的个性化封装,对应不同语言的文件操作方法是不一样的,这种现象是因为我们要满足不同语言的语法范式,我们可以站在系统调用接口的层面上来认为所有的文件操作都要落实到这些系统接口的身体上。

 

#include <iostream>
#include <cstdio>
int main()
{
    //因为Linux下一切皆文件,所以,向显示器打印,本质就是向文件中写入, 如何理解?TODO
    //C
    printf("hello printf->stdout\n");
    fprintf(stdout, "hello fprintf->stdout\n");
    fprintf(stderr, "hello fprintf->stderr\n");
    //C++
    std::cout << "hello cout -> cout" << std::endl;
    std::cerr << "hello cerr -> cerr" << std::endl;
}

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

标准输出和标准错误都会想显示器打印,但是其实是不一样的,因为上面当我们重定向到log.txt的时候会发现标准输入和标准错误是不一样的。

正常情况下我们看到的文件描述符应该是从0、1、2开始的,但是我们上面的代码可以发现,我们的文件描述符是3,因为任何一个进程在启动的时候,默认会打开当前进程的三个文件:标准输入(stdin、cin)、标准输入(stdout、cout)、标准错误(stderr、cerr)分别是默认的文件描述符0、1、2,所以0、1、2已经默认被操作系统占用了。进程中,文件描述符的分配规则:在文件描述符表中,找最小的、没有被使用的数组元素分配给新文件。

文件描述符(open对应的返回值)本质就是数组下标,每一次创建开打文件的行为,都会在内核数据当中创建一个struct file对象,其中我们知道打开文件调用open函数是代码中的一行,所以是现有一个进程,让后回去执行打开文件的操作,那么进程与打开文件的关系到底是如何维护的呢?首先操作系统为了维护文件系统,定义了一个struct files_struct结构体,这个结构体里面包含了一个数组,这个数组的类型是struct file* fd_array[],今天在内存中加载一个文件,就要为文件文件创建一个struct file对象,里面包含文件属性和内容,然后我们的操作系统就要在当前执行打开该文件的进程的 struct files_struct的struct file* fd_array[]数组内向下进行遍历,找到一处位置填到这个位置里面,而我们进程的struct task_struct结构体里面有一个成员变量是struct files_struct* files的指针,所以未来一个进程只要找到pcb就可以通过该进程pcb的struct files_struct* files成员找到struct file* fd_array[] 数组,只要有数组就有对应的下标。所以总结一下就是当我们从磁盘里面load一个文件的时候,操作系统为了管理文件进行先描述再组织创建了struct file结构对象,因为打开的行为一定是某个操作进程来打开的,所以struct file会找到该进程下面的pcb然后一步一步向下找到进程对应的struct file* fd_array[] 数组,遍历数组找到空闲位置添入,并向应用程返回文件描述符也就是对应的数组下标。以后read\write\close这些函数接口必须传递文件描述符也就是数组下标,就可以通过找到该进程去一步一步向下找到进程对应的struct file* fd_array[] 数组,通过文件描述符也就是数组索引找到对应的文件对象struct file,从而实现对文件的操作。

每一个文件对象struct file在操作系统内部都要匹配一个缓冲区,当我们比如调用write(3,buffer,xx)函数的时候是将buffer的内容拷贝到当前进程的下标为3的文件struct file(这个struct file要通过进程pcb成员一步一步向下寻找)的缓冲区内当中,我已我们的io类函数本质是拷贝类函数(这里的拷贝是用户空间和内核空间进行数据的来回拷贝),当拷贝完成write调用结束,后续这些数据什么时候刷新到对应的外设(这里是磁盘中的指定位置)当中有操作系统自主决定。同理read也并不是直接调用read去访问磁盘当中的数据,而是当我调用read的时候,操作系统才把数据放到缓冲区里,然后未来我调用read的时候结合文件描述符通过进程pcb找到文件的缓冲区,将缓冲区的内容拷贝过来这就是读取,压根就不关心底层(外设)实现的差异化。当让操作系统也有自己的read和write。而操作系统的read和write本质上就是拷贝,只要把应用层的数据拷贝到struct file的缓冲区里面,然后在调用底层不同对应的设备方法,它自己就可以把数据放到对应的外设里面。从而文件对象往上就是Linux一切皆文件,换而言之,我们在访问外设的时候在我们看来全都是文件,底层的差异化我们通过了函数指针的方式屏蔽了。进程只认内核的struct file而不认底层的驱动程序和外设,这时候外设的差别就完全被屏蔽掉了。

操作系统在进行文件读写的时候,用open打开文件,然后用文件描述符(实际就是数组下标)进行标定找到struct file,而对于文件的管理我们也知道是先描述再组织,当我们打开一个文件的时候,将文件导入内存的时候,我们得先再操作系统内部创建struct file对象,该struct file对象里面包含了三个重要的东西,分别是文件的属性、匹配的缓冲区以及操作方法,它的操作方法是以函数指针的形式存在的,最后可以指向底层不同的方法,从而可以实现用同样的函数名访问底层不同的方法。

我们访问操作系统的本质是以进程的方式访问操作系统的,进程看到的都是文件所以我们看到的也是文件。 未来将数据用操作系统的读写方法拷贝到struct file的缓冲区里面,再用底层硬件不同的读写方法然后就可以实现差异化的向不同的设备读写数据了。

在当前的进程看来,全都是文件对象 ,最终将数据拷贝放入struct file的缓冲区里面,然后对应进行底层读写的时候只需要调用底层对应的方法(这些方法也就是函数接口在对应的外设驱动当中)来完成对应的读写就可以了

当前再操作系统的层面,我们必须访问文件描述符才能找到文件。任何语言层访问外设或者问及那,必须经历OS。c语言中的fopen函数返回值FILE*中的FILE是什么?是谁提供的?和内核struct file有关系吗?这里的FILE实际上是一个结构体,该结构体是c语言提供的,和内核struct file没有任何关系。struct FILE必定封装了我们所谓的文件描述符fd,因为比如fopen分装了open,fopen的返回值struct FILE也必定封装了open的返回值fd。fd就是struct FILE当中的_fileno成员。c语言就是通过该成员在系统层面访问文件的。同理在c++层面cin、cout、cerror着些类对象同样要封装fd文件描述符。因为语言层面访问文件必须要经过系统调用接口,没有文件描述符,拿什么访问我们的外设呢?

 

我们是如何做到上述的重定向的呢?在我们的语言层面当中,printf是向stdout也就是1号文件描述符打印,也就是printf只认stdout,而stdout只认1数字,我们是先close掉1然后将它用open log.txt来替代,这件事情其实上层不知道,stdout只认1文件描述符,因为stdout封装的是1。所以上层向stdout标准输出中输出最后底层用write向一号里面去写,他以为自己是向显示器写入但是最终是向文件当中写入,这就叫做重定向。追加重定向就是将open中的O_TRUNC改写成为O_APPEND。

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

现在就可以解释向标准输出和标准错误打印是不一样的?stdout、cout都是向1号文件描述符对应的文件打印,而stderr、cerr都是向2号文件描述符对应的文件打印。1号和2号==都可以同时都指向标准输出,但是2号在打印的时候,你改的是1号的指向,并没有影响2号,所以2号依旧是向显示器打印,所以输出重定向更改的只是1号文件描述符对应的指向,并不会影响2号,所以2号继续向显示器进行打印。以后所有正常的信息都是采用printf或者cout向1号文件描述符进行打印,所有的错误信息都是采用perror或者cerr向2号文件描述符进行打印。如果我们也想将标准错误重定向到指定的文件可以将代码./a.out>log.txt更改为./a.out>log.txt 2>&1就可以将错误和标准信息都打印到log.txt当中,其中2>&1是去除1号文件描述符的地址,将2号文件描述符的内容放进去,也可以分成两个部分看待,先是./a.out>log.txt前面可以写个1不写也就是默认把对应的标注输出重定向到log.txt,2>&1把1里面的内容写到2下标所对应的内容当中,那么2和1都要指向log.txt,有点绕。./a.out 1>log.txt 2>err.txt将标准和错误信息分表打印到两个不同的文件当中。

#define LOG_NORMAL "logNormal.txt"
#define LOG_ERROR "logError.txt"
close(1);
open(LOG_NORMAL, O_WRONLY | O_CREAT | O_APPEND, 0666);
close(2);
open(LOG_ERROR, O_WRONLY | O_CREAT | O_APPEND, 0666);
//上述代码可以将代码输出的标准错误和标准输出内容做分类,方便进行排查问题。未来程序一旦报错了我们一般更想看到的是perror的信息。但是真正进行重定向后面不是很方便,因为要先关闭一个文件,再紧接着打开一个文件。int dup2(int oldfd, int newfd)是用来进行冲定向的,用法:dup2(3,1)将打印到显示器的内容打印到3号文件描述符对应的文件当中。
int main()
{
    int fd = open(LOG_NORMAL, O_CREAT|O_WRONLY|O_APPEND, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd, 1);
    printf("hello world, hello bit!\n");
    close(fd);
    return 0;
}

如果我们想让我们的shell支持重定向呢?

如何理解缓冲区的概念?进度条的输出缓冲区在哪里,为什么要存在?和struct file的缓冲区是一个概念吗?

int main()    
{    
  //C库    
  fprintf(stdout, "hello fprintf\n");                                                                     
  //系统调用    
  const char *msg = "hello write\n";    
  write(1, msg, strlen(msg)); //这里strlen不需要加1,因为linux下一切皆是文件,向显示器写入其实和向普通文件  写入没有区别,是一样的,不需要我们所谓的\。        
  fork(); //给人感觉打印完了在进行fork会有点多此一举,但是实则不然,看下面的现象。
  return 0;    
}  

如果把fork注释掉,无论是往显示器打还是往文件里面打都是两行。

fprintf()->FILE*->struct FILE(封装了fd,还会在结构体内预留一部分缓冲区,这一部分是c语言提供的缓冲区),所以当进程调用fprintf(stdout, "hello fprintf\n")并不是将hello fprintf拷贝到了系统的内部,而是把数据放到了struct FILE的缓冲区里面,然后这个函数就可以直接返回了,并没有花费太多时间从再从用户层(c语言层面)写道操作系统内部了,c语言库会结合一定的刷新策略将缓冲区的数据写到操作系统内部(调用的是write(FILE->fd,xxxx)的系统接口)的file_struct当中(这个结构体也会有缓冲区)。

常见的刷新策略:是指从c语言维护的缓冲区刷新到操作系统当中1、无缓冲(不提供缓冲,当我们调用fprintf的时候,我们的底层直接调用写给操作系统);2、行缓冲(当我们把数据写到c语言的缓冲区的时候,只要遇到\n就会把\n之前的内容刷新到我们的操作系统当中);3、全缓冲(把缓冲区写满了才会刷新给操作系统)。一般显示器采用的刷新策略采用的是行缓冲,普通文件采用的是全缓冲。

为什么要有缓冲区?节省了调用者的时间,因为系统调用也是要花费时间的,系统调用相比一般函数的成本其实更高一些,进程把数据交给c语言当中struct FILE的缓冲区,这个时候就可以返回了,进程可以继续向下走了,剩下的工作就是c语言层面定期将内容刷新到操作系统当中,这要不是每一次写到缓冲区都要调用系统接口刷新一次,大大提高了效率,节省了调用者的时间。

这个缓冲区在哪里呢?在进行fopen打开文件的时候会得到struct FILE结构体,缓冲区就在这个结构体里面。

上面的现象的解释:我们的write接口是系统调用,没有缓冲区,直接调用就写给操作系统了。fprintf这个c语言层面的接口就有了缓冲区了,如果向显示器打印,是实行的行缓冲,当我们调用fork的时候,在这之前我们的hello fprintf和hello write已经由于带了\n的原因从缓冲区被刷新走了,刷新走了之后我们再调用fork是没有任何意义的。一旦我们重定向到文件,缓冲策略就成了全缓冲,这并不影响write系统接口直接刷新到操作系统,但是会影响fprintf函数,因为hello fprintf并不能写满缓冲区,所以在fork之前hello fprintf还呆在缓冲区内,fork之后,父子两进程都要对缓冲区进行刷新,就会发生写实拷贝,所以最终会向文件当中打印两次。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值