Linux基础IO(下)

FILE

通过前面学习我们可以知道,对于任何一门语言的文件操作,我们都可以站在系统调用的角度来进行统一看待!这些语言的文件操作的本质就是通过文件描述符来访问文件!然后每个语言都有自己的方式来表示一个文件,但是无论使用那种方式,其本质还是利用文件描述符来寻找文件的!比如C语言中的FILE结构体,C语言用FILE结构体来描述一个被打开的文件!在其内部必定封装了文件描述符!

下面我们可以通过一段代码来研究一下:

#include <stdio.h>
#include <string.h>
#include<unistd.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}

运行结果:
在这里插入图片描述
这种运行结果是符合我们预期的!
printf的本质就是向向1号文件描述符所匹配的文件进行输出,然后现在1号文件描述符所匹配的文件就是显示器文件,因此printf会将内容输出到显示器;
fwrite向stdout所指向的文件中输出内容,在C语言中stdout是标准输出文件,stdout是个FILE*类型指针指向一个FILE结构体,因此在这个结构体(stdout所指向的FILE)所封装的文件描述符是1,因此fwrite本质就是向1号文件描述符所匹配的文件输出,现在1号文件描述符所匹配的文件是显示器,因此fwrite的内容会被输出到显示器;
write是系统调用,write(1,……,……);是向1号文件描述符所匹配的文件输出,现在1号文件描述符所匹配的文件是显示器,因此write是向显示器输出!
那么如果我们现在改变一下1号文件描述符所匹配的文件呢?也就是对于当前程序进行输出重定向!那么运行结果又是什么?

  #include <stdio.h>
   #include <string.h>
   #include<unistd.h>
   #include<sys/types.h>
   #include<sys/stat.h>
   #include<fcntl.h>
   int main()
   {
     umask(0);
    int fd=open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
    dup2(fd,1);
  const char *msg0="hello printf\n";
  const char *msg1="hello fwrite\n";
  const char *msg2="hello write\n";
  printf("%s", msg0);
  fwrite(msg1, strlen(msg0), 1, stdout);
  write(1, msg2, strlen(msg2));
  fork();
  close(fd);                                                                                                                                                                                                    
  return 0;
  }

运行结果:
在这里插入图片描述
首先当我们运行我们的程序的时候,不会在显示器上看到任何输出结构,因为我们改变了1号文件描述所匹配的文件,原来1号文件描述符所匹配的文件是显示器,但是现在进行了输出重定向,1号文件描述符所匹配的文件就是log.txt文件因此我们程序的输出结果会全部输出到log.txt文件;
可是现在问题是:
为什么和第一次运行的结果有点不一样?
为什么出现两次printf两次fprintf?
这一定与我们的fork有关!
但是在解释这一现象之前呢,我们得先有点预备知识;

在C语言的文件操作中呢,诸如fprintf/printf/fwrite等输出函数并不会直接将我们的数据写给OS,而是会先将我们的数据写入一个“缓冲区”中!这个缓冲区是由C语言库自己实现的!我们平常经常所说的缓冲区也就是指的这个缓冲区!这个缓冲区被定义在FILE文件结构体中,每个FILE文件结构体都有一个这样的缓冲区!因此本质上fprintf/printf/fwrite等输出函数本质上就是个拷贝函数!专门将我们的数据拷贝到缓冲区去的!C语言提供的这个缓冲区区呢就是语言级别的缓冲区,简单理解就是在FILE结构体内部封装了一个char类型的大数组!那么C语言什么时候刷新缓冲区!也就是什么时候将我们的数据交给OS呢?这就与缓冲区的刷新机制有关了!
常见的有3中缓冲机制:
1、无缓冲,只要fprintf等函数将数据拷贝到缓冲区,那么就立马将数据刷新给OS;
2、行缓冲,当我们fprintf等函数将数据拷贝到缓冲区过后,fprintf等函数会检查一下缓冲区的最后一个字符是不是’\n’,如果是的话那么就刷新缓冲区!
3、全缓冲,只有等到缓冲区满了我们才会区刷新缓冲区!

C语言针对不同的文件,对应的缓冲区的刷新机制也是不一样的,比如:对于显示器文件的缓冲区刷新机制就是行缓冲,对于普通文件的缓冲区刷新机制就是全缓冲!
可是C语言为什么要提供这个缓冲区?
提高效率,实际上一次IO的代价是非常大的,如果C语言不提供这个缓冲区的话,那么我们程序每输出一段数据,OS就要进行一次IO,次数少还行,但是一旦当前程序IO请求的频率非常高,那么我们的OS是不是大部分时间都在处理我们当前程序的IO请求,OS的使用效率就非常的低!但是如果我们有了一个缓冲区,只有当缓冲填满了或者触发一定刷新机制的时候OS才来帮助我们处理IO请求,一次性就能大部分数据输出出去,是不是OS的工作效率就提高了!

现在我们再来解释一下上面程序的输出现象:
当我们的程序运行到printf的时候,会将我们的数据先写入缓冲区中区,然后又由于当前缓冲区对应的文件就是普通文件(log.txt),那么当前缓冲区的刷新机制就是全缓冲!因此我们的数据会被暂存在log.txt文件的缓冲区中!
接着我们的程序运行到fwrite,fwrite是将数据输出到1号文件描述所匹配的文件中去,也就是log.txt文件但是fwrite并不会直接输出,fwrite会先将数据输出到log.txt文件对应的缓冲区中去,但是现在log.txt文件对应的缓冲区中已经存在的一部分数据,这是上一次printf的输出数据!现在fwrite直接将数据写在其后面,因此现在缓冲区里面的数据就变成了:
在这里插入图片描述
由于log.txt文件对应的缓冲区刷新机制是全缓冲,我们这才写的几个字符,肯定没有将缓冲区装满,因此此时也不会触发缓冲区的刷新机制;
然后程序运行到write来了,write可是系统调用!write就代表着OS因此write会直接将数据写个OS,OS拿到这些数据会就会马上将这些数据刷新到对应的文件(log.txt)文件中,因此我们在log.txt文件中最先看到hello write的语句!
至此,printf和fwrite的数据还存在与缓冲区没有被刷新!
最后程序来到了fork语句,创建了子进程,在最开始的时候子进程是会与父进程共享同一份数据和代码的,那么缓冲区也是数据吧,说白了C语言提供的缓冲区就是char类型的大数组,子进程也会与父进程共享这个char类型的大数组!
在这里插入图片描述
最后fork之后,程序就结束了,也就是进程要退出了,但是在进程退出之前,系统是会自动刷新缓冲区的!也就是要修改缓冲区中的数据!但是现在父子进程都指向的同一份数据,子进程直接清空缓冲区中的数据的话,势必会影响到父进程,为了保持进程间的独立性!OS就会触发写时拷贝技术!当我们的子进程或父进程任意一方向要修改父子进程共享的数据时,OS会在物理内存的其他地方另开一块空间,然后将原来空间中的数据拷贝到新空间中去,然后让需要修改数据的那个进程重新映射到这块新空间!也就是说最后父进程与子进程都指向了两份独立的但是内容完全相同的缓冲区:
在这里插入图片描述
最后子进程退出时,子进程就会将自己缓冲区的数据刷新给OS,OS在刷新到文件,父进程在退出的时候,也会将自己缓冲区的内容刷新给OS,OS在刷新到文件,至此OS将两份一模一样的数据刷新了两次,但是这两份数据来自两个独立的进程!因此我们会在我们的文件上看到两份hello printf、两份fwrite!

自己模拟实现fopen/fclose、fread/fwrite

为了加深我们对于C语言FILE结构体的理解,加深对于C语言缓冲区的理解,我们可以自己手动的实现一个文件操作相关的函数:
头文件:

#pragma once 
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

#define INPUT_SIZE 1024//输入缓冲区的大小
#define OUTPUT_SIZE 1024//输出缓冲区大小

#define FLUSH_NONE 0x1//表示无缓冲策略
#define FLUSH_LINE 0x2//表示/行缓冲策略
#define FLUSH_ALL 0x4//表示全缓冲策略
struct My_FILE//模拟的C语言的FILE结构体,实际上肯定比这个复杂
{
  char INPUT_BUFF[INPUT_SIZE];//输入缓冲区

  char OUTPUT_BUFF[OUTPUT_SIZE];//输出缓冲区

  size_t cur_input_ptr;//输入缓冲区的当前头指针
  size_t cur_output_size;//输出缓冲区当前大小

  int fd;//文件描述符

  int FLUSH;//刷新策略
};
typedef struct My_FILE MYFILE;

 MYFILE *my_fopen(const char *path, const char *mode);//打开文件

 int my_fclose(MYFILE *fp);

 size_t my_fread(void *ptr, size_t size, size_t nmemb, MYFILE *stream);
 
 size_t my_fwrite(const void *ptr, size_t size, size_t nmemb,MYFILE *stream);

 int my_fflush(MYFILE *stream);//


myfile.c:

#include"myfile.h"
MYFILE *my_fopen(const char *path, const char *mode)//打开文件
{
        int flag=0;
      //1、设置文件打开方式
        if(strcmp("w",mode)==0){
          flag=O_CREAT|O_WRONLY|O_TRUNC;
        }
        else if(strcmp("r",mode)==0){
          flag=O_RDONLY;
        }
        else if(strcmp("a",mode)==0){
          flag=O_CREAT|O_WRONLY|O_APPEND;
        }
        else if(strcmp("r+",mode)==0){
          //读写的方式打开文件,如果文件不存在则报错
          flag=O_RDWR;
        }
        else if(strcmp("w+",mode)==0){
          //读写打开文件,如果文件不存在,则创建
          flag=O_RDWR|O_CREAT|O_TRUNC;
        }
        else if(strcmp("a+",mode)==0){
          //打开一个文件,在文件末尾进行读和写,如果文件不存在,则创建
          flag=O_CREAT|O_RDWR|O_APPEND;
        }
        else{
          printf("error!\n");
          return NULL;
        }
        int fd=0;//用于存储文件描述符
        //判断是对文件是进行读取还是写入操作,以此来确定open函数类型
        if(flag&O_CREAT==0)//如果O_CREAT没有设置,那么当文件不存在时就不会被创建!
        {
         fd= open(path,flag);
        }
        else 
        {
        umask(0);
        fd=open(path,flag,0666);//设置被创建的文件的:
        }
        if(fd==-1)//文件打开失败
          return NULL;

        //文件打开成功.malloc一块MYFILE
        MYFILE*ret=(MYFILE*)malloc(sizeof(MYFILE));
        if(ret==NULL)//开辟失败,此时文件已经成功打开,但是MYFILE开辟失败也算文件打开失败
        {
          close(fd);//记得关闭已经打开的文件
          return NULL;
        }
                      
        //初始化MYFILE
        ret->cur_input_ptr=INPUT_SIZE;
        ret->cur_output_size=0;
        ret->fd=fd;
        ret->FLUSH=FLUSH_ALL;//默认全缓冲
        return ret;
}


int my_fclose(MYFILE *fp)//关闭成功返回0,关闭失败返回-1
{
      assert(fp);
        //1、刷新输出缓冲区
        int ret= my_fflush(fp);
      if(ret==-1)//缓冲区刷新失败
        return -1;
        //关闭成功
        free(fp);//释放MYFILE,MYFILE是fopen从堆上申请过来的
      return 0;
}

int my_fflush(MYFILE*fp)
{
    assert(fp);
    //刷新缓冲区的本质就是,将缓冲区的数据写入OS内核
    int ret= write(fp->fd,fp->OUTPUT_BUFF,fp->cur_output_size);
    if(ret==-1)//写入失败
      return -1;
   fp->cur_output_size=0;
    return 0;
}

size_t my_fwrite(const void *ptr, size_t size, size_t nmemb,MYFILE *fp)
{
         assert(fp);
         //本质就是将数据拷贝到输出缓冲区
         size_t  resSize=OUTPUT_SIZE-fp->cur_output_size;//缓冲区还能拷贝的数据
         size_t forwd_size=size*nmemb;//预期写入
         size_t actual_size=0;
         if(resSize>=forwd_size)
         {
           memcpy(fp->OUTPUT_BUFF+fp->cur_output_size,ptr,forwd_size);
            actual_size=forwd_size;
         }
         else{
            
            actual_size=resSize;
           memcpy(fp->OUTPUT_BUFF+fp->cur_output_size,ptr,resSize);
         }
         //判断一下缓冲策略,看一看是否需要进行缓冲区的刷新
         if(fp->FLUSH==FLUSH_ALL)
         {
            if(fp->cur_output_size==OUTPUT_SIZE)//缓冲区满了,刷新缓冲区
              my_fflush(fp);
         }
         else if(fp->FLUSH==FLUSH_LINE)
         {
            if(fp->OUTPUT_BUFF[fp->cur_output_size-1]=='\n')//末尾是'\n',触发行缓冲
              my_fflush(fp);
         }
         else{
            my_fflush(fp);//无缓冲,直接刷新
         }
         fp->cur_output_size+=actual_size;
         return actual_size;
}


size_t my_fread(void *ptr, size_t size, size_t nmemb, MYFILE *fp)
{
      assert(fp);
      //fread本质就是从缓冲区里面读取数据
      //先将数据从文件拷贝到缓冲区
      size_t actual_size=0;
      if(fp->cur_input_ptr==INPUT_SIZE)//缓冲区为空
      {
       size_t n= read(fp->fd,fp->INPUT_BUFF,INPUT_SIZE);//从OS读取数据到输入缓冲区
        if(n==0)
          return 0;
       fp->cur_input_ptr=0;//从0开始读取
      }
      size_t resSize=INPUT_SIZE-fp->cur_input_ptr;
      size_t forwd_size=size*nmemb;
      
      if(resSize>=forwd_size)//读取预期个数
      {
        actual_size=forwd_size;
        memcpy(ptr,fp->INPUT_BUFF+fp->cur_input_ptr,actual_size);
        fp->cur_input_ptr+=actual_size;
      }
      else{//实际不够
        actual_size=resSize;
        memcpy(ptr,fp->INPUT_BUFF+fp->cur_input_ptr,actual_size);
        fp->cur_input_ptr+=actual_size;
      }

      return actual_size;
}

理解文件系统

我们知道一个被打开的文件是会被加载进内存的,那么对于那些没有被打开的文件我们因该怎么看待呢?这些没有被打开的文件又存在与哪里?
未被打开的文件存储于磁盘上!
那么OS是如何看待磁盘的?

OS如何看待磁盘

磁盘嘛,不就是一个一个磁面组成的一种存储结构嘛,然后每个磁面上又分为一圈一圈的磁道嘛,然后每一个磁道又被分为一个一个的扇区嘛,每个扇区一般都是512字节嘛!

可是这有何我们OS有什么关系?
我们小时候都玩过录影带吧:
在这里插入图片描述
录影带可以被我们随意拉长吧!你看这个一圈一圈的录影带是不是和我们的磁盘有点相识,我们的磁盘也是一圈一圈的!为此我们是不是也可以将我们的磁盘也抽象成这样的磁带!然后将这个磁盘无线拉长,这样的话我们的磁盘是不是就是一个非常长的矩形了:
在这里插入图片描述
因此在我们的OS看来磁盘就是一个非常巨大的线性表!

管理磁盘

在OS看来我们的磁盘就是一块非常巨大的线性表,那么OS要不要把磁盘管理起来呢?
要的,但是如果硬管的话,代价非常大,因为磁盘是非常大的;
因此OS会将磁盘进行分区,也就是我们平常做的什么C盘、D盘的操作:
在这里插入图片描述
可是呢,OS认为就算进行了分区,可是每一个分区依旧很大,为此它又对分区进行分组的处理:
在这里插入图片描述
先在我们只要把每个分组管理好,那么这个分区就管理好了,这个分区管理好了,那么这整个磁盘也就管理好了;

接着我们来介绍一下图中出现的英文:
Boo Block(启动块):存在与每个分区的前面,记录整个磁盘的分区情况,存放OS的镜像地址;这个块的数据被刮花,OS就会启动不了,但是我们的数据还存在;
Block group(块组):也就是当前分区下的一个分组;
Super Block(超级块):用于存储当前分区的基本信息:比如当前分区下有多少组,分区中有多少个块,每个块的大小是多少,当前分区的文件系统信息和文件结构!这个块的数据被刮花,就会导致我们的数据丢失,因此这个块的数据非常重要,因此在每个组的开头都会存储着Super Block进行备份,防止数据丢失;
Group Descriptor Table(块组描述符表):看到表,这不就是一个数组嘛,数组中的每一个元素记录着对应组的基本信息,比如:组内可用block,不可用block,和Block bitmap、inodebitmap等区域的划分情况;
inode Table(inode节点表):该表中的每一个inode节点对应一个文件的文件属性信息!
Date blocks(数据块):文件的内容存在与此;
inode bitmap(inode位图):一个比特位对应着inodeTable中的一个inode节点;在inode bitmap中第i号比特位为1,表示inode Table中第i个inode节点被使用!用于记记录,inode Table的使用情况;
block bitmap(数据块位图):与inode bitmap功能类似,用于表示Date block的使用情况;

我们都知道,文件=内容+属性;
在Linux中文件内容和文件属性是分开存储的!
在Linux下,OS是通过inode编号来确定不同的文件的,而不是文件名!文件名是用来给我们用户看的;

如果我们想要创建一个文件,OS会帮我们做些什么事情?
首先,OS会去申请一个inode bitmap中申请一个inode编号,在成功申请到过后,OS会得到一个inode编号,然后inode拿着inode标号在inode Table中寻找inode节点,然后将我们要创建的文件的属性全部写入这个inode节点中去!至此我们就创建出一个文件了!

总结
1、inode与文件名;
在Linux中,文件的inode属性中并不存在文件名!文件名是给用户看的!Linux是通过inode编号来识别不同的文件的;
2、重新认识目录;
在Linux中,一切皆文件,目录也是一个文件,既然是文件,那么它就有属于自己的inode节点;既然是文件,那么它的文件内容是什么呢?是该目录下文件名与inode节点编号的映射关系,在目录中文件名是唯一的,不可重复的!这些文件内容,被存储于目录文件的Date block数据块中;
3、再来重新理解文件的增删查改:
增: 我们要创建一个文件,OS会去inode bitmap申请一个inode编号节点,那么到inode编号过后,再去inode Table中根据得到的inode编号找到inode节点,然后将我们要创建的文件的属性全部写入这个inode节点当中去,随后这个文件就被创建出来了,随后还没完!当前目录下凭空增加了一个文件,当前目录要不要记录下这个变化?答案是要的,因为我们是在当前目录下创建的文件,文件创建出来了,是不是就意味着当前目录的文件内容发生了改变,OS再根据当前目录的inode编号,找到当前inode节点,再根据inode节点找到当前目录的Date block,然后将我们新创建的文件名与之对应的inode编号存入到当前目录下的Date block中;
删: OS删除一个文件并不是直接抹除文件内容和文件属性,首先OS会根据要删除的文件名去当前目录的Date block中查找要删除的文件名所映射的inode编号,然后根据这个inode编号,找到对应文件的inode节点,然后根据inode节点里面的属性将当前文件所占用Date block在block bitmap中的比特位置0(因为block bitmap记录着Date block的使用情况),然后再根据inode编号将当前文件的inode节点所占用的空间也释放了,怎么释放?inode bitmap不是记录着inode Table的使用情况嘛,我们直接根据inode编号推导出当前inode节点再inode bitmap中是第几号bit位,然后将该bit位置0即可,至此我们的inode节点和Date block都不在属于我们的文件,最后OS会去当前目录的Date block中将我们要删除的文件名和其映射的inode编号的映射关系抹去!至此我们的文件再当前目录下就被删除了!
删除一个文件的本质:就是将该文件的inode bitmap置0、block bitmap置0,不会去将inode节点与Date block的每一个比特位置0,这样代价太高!
尽管我们删除了一个文件,但是该文件的文件属性、文件内容依旧存在于磁盘上,只要我们拿到了被删除文件的inode节点,我们就可以逆推回去,恢复文件!我们根据inode编号首先将inode bitmap置1,恢复我们的inode节点,保证我们的inode节点不会被其他文件覆盖!然后我们再根据inode节点里面存储的当前inode节点使用的Date block的编号,将block bitmap对应比特位置1,我们的文间内容就被恢复出来了,至此文件属性+文件内容都恢复出来了我们整个文件也就被恢复出来了!
注意:在我们不小心误删除文件过后,千万不要再创建任何文件,最好的办法就是什么也不做,避免我们的inode节点被其他文件给覆盖了
查: OS会先找到我们要查找的文件所处的目录,然后找到当前目录的inode节点,根据inode节点找到当前目录的Date block,在这里面找到我们要查找的文件对应的inode编号,OS在拿到我们要查找的inode编号过后,根据inode编号找到inode节点,然后打印出inode节点里面的属性!
改: 与查的前提是一样的,OS会先在当前目录的Date block块中找到我们要改的文件的inode编号,然后根据inode编号,找到inode节点,再根据indeo节点里面的属性找到Date block数据块,将数据读入内存,修改完数据过后,OS在将数据写入Date block中,如果Date block不够,OS会去block bitmap申请一块,再来用于存储数据,然后OS会在inode节点中记录下此次申请的几号Date block;
4、inode节点是如何与Date block建立联系的?
在inode节点中有一个数组,int num[N]={};这个数组专门用于当前inode节点所使用了拿些Date block数据块,比如:当前inode节点使用了1、3、5、2、9号Date block,那么在num中就会记录下这些数据块的编号!
假设N=15,是不是说,当前文件只能记录15个节点也就是15x4kb大小的文件内容?
不是的:
在这里插入图片描述
5、有没有可能inode节点用完了,Date block还有剩余或者inode节点还有剩余,Date block用完了?
答案是有可能,如果我们疯狂的创建空文件,那么inode节点不就被用完了,Date block不就有剩余嘛;同时我只创建一个文件,然后将所有数据全部写入这个文件中,那么不就是inode节点有剩余,Date block用完了;
6、ls -i可以查看文件的inode编号!

硬链接

就是多个文件共用同一个inode节点!我们可以简单理解为硬链接就是给inode节点取别名!
创建硬链接命令:ln 源文件 新文件名
在这里插入图片描述
通过使用ls -i命令我们可以查看各个文件的inode编号:
在这里插入图片描述
我们可以发现,log-hard.txt文件与log.txt文件确实是共享的同一个inode节点!因为这两个节点的inode编号是一样的!
同时,我们对log-hard.txt文件进行增删查改,log.txt文件也会受到影响:
在这里插入图片描述
我们可以发现当我们对log-hard.txt文件进行操作时,log.txt文件也会收到影响!
当我们删除log.txt文件过后,我们也可以通过log-hard.txt文件访问:
在这里插入图片描述
这是因为当我们这时候删除文件并不是真正的删除,在inode节点内部,有一个变量叫做引用计数,该变量专门用于记录有多少个文件名映射当前节点,每次我们删除文件时,OS都会将引用计数–,然后判断一下引用计数是不是0,如果是0才会真正的删除文件!
在这里插入图片描述
这个属性我们也经常见到,比如我们经常使用的ll命令就会将其打印出来:
在这里插入图片描述
这就是inode节点的引用计数,也被叫做硬链接数!每当我们创建一个硬链接,这个硬链接数就会++:
在这里插入图片描述
现在我们有一个疑问,就是我们创建一个普通文件的时候,硬链接数是1,这很好理解,硬链接数是1,那么说明只有一个文件名与当前文件名共享一个inode节点,那就是自己本身!
在这里插入图片描述
可是为什么我们创建目录时,目录的硬链接数是2呢?
在这里插入图片描述
目录的硬链接数是2,那么说明除了Dir本身之外在该系统下还有另一个文件名与Dir共享同一个inode节点!那么这个文件名在那呢?
我们进入Dir目录里面,利用ls命令查看Dir目录下什么也没有!可是,Dir目录下真的什么也没有吗?
如果我们的ls命令在戴个-a选项呢?
在这里插入图片描述
我们可以发现,在Dir目录下存在着两个目录!这两个目录我们也认识,一个点 表示当前目录;
两个点 表示上级目录;嗯?一个点表示什么?当前目录!这不就出来了嘛!在我们用户看来,一个点就表示的Dir,但是站在OS的角度来看的话,一个点就表示一个文件名,Dir表示另一个文件名,但是这虽然是两个不同的文件名,但是映射的都是同一个inode节点!
这一点我们可以通过查看Dir的inode编号和.的inode编号来验证:
在这里插入图片描述因此,在我们创建目录的时候,目录的硬链接数是2,这是因为OS会自动在我们的目录下面为我们的目录创建一个硬链接!
一个点都谈完了,我们再来谈谈两个点,两个点表示上级目录,然后上级目录的硬链接数是3,这说明有3个文件名共享着同一个inode节点,除了上级目录本身!还剩下两个!现在Dir目录下的两个点也表示上级目录,也就是说Dir目录下的两个点与上级目录共用一个inode节点,现在还剩下一个!上级目录也是个目录,那么只要是目录OS就会在目录下自动创建一个点和两个点的目录!其中上级目录中不久有一个点,这个点与Dir平起平坐,与上级目录共用同一个inode节点!:
在这里插入图片描述
注意:在Linux中OS是不允许我们用户为目录创建硬链接的,因为这有可能造成环路路径问题!
比如:/home/Saber/Code/
假设这就是一串空目录home目录下除了Saber目录没有其他的,Saber目录下除了Code没有其他的!
假设运行为目录建立硬链接,那么我们假设Code为home的硬链接!
现在我从/目录开始利用find test.c,find在/目录下没找到test.c,然后就会进入home目录下查找,home目录下也没有找到test.c,就会进入Saber目录下查找,Saber目录下也没有找到test.c,就会进入Code目录下查找,Code是home的硬链接,与home共用同一个inode,那么Code目录里面的内容就是Saber目录,find在Code目录下也没找到test.c就会进入Saber目录下查找,这样不久陷入死循环了!
因此OS是不允许我们用户自己为目录创建硬链接的!

软连接

就是重新创建一个文件,在这个文件中存入被建立软连接的文件的路径!
软连接创建命令:ln -s 源文件名 新文件名
在这里插入图片描述
test-soft.c与test.c是两个独立的文件!这一点我们可以查看两者的inode编号验证:
在这里插入图片描述
我们访问软连接文件,实际上是访问的软连接文件所指向的源文件!
我们对于软连接文件的一切操作(除了删除软连接文件,删除软连接文件就是删除软连接文件本身,不会删除软连接文件本身),实际上都是加在软连接文件所指向的源文件身上的!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
同时我们使用vim打开软连接文件时,也是打开的软连接所指向的源文件!
那么软连接用什么应用场景呢?
软连接的方式有点类似与Windowns下的快捷方式,我们可以为路径比较深文件,建立软连接,通过访问软连接我们就能访问到软连接所指向的源文件!
比如:在当前目录下为ls文件建立软连接,然后使用软连接来允许ls命令:
在这里插入图片描述

ACM时间

一个文件都有三个时间:
在这里插入图片描述

Access:最近一次访问时间,不一定每访问一次,就刷新一次,这跟OS的策略有关系;
Modify:最近一次文件内容修改时间;
Change:最近一次文件属性修改时间;

动态库和静态库

Linux下静态库的格式是:libxxx.a;
动态库的格式是:libxxx.so;
其中xxx才是库名!
Windowns下静态库以.lib结尾,动态库以.dll结尾

见一见Linux下的库

这是/usr/lib64/目录下的静态库和动态库
在这里插入图片描述
其中头文件如下:
/usr/include/
在这里插入图片描述

预备知识:
1、我们的Centos系统已经预装了C/C++的库和头文件,头文件提供方法说明,库提供方法实现,头文件和库之间是由对应关系的!头文件和库要组合在一起使用!
2、头文件在预处理阶段就会被展开,而链接器的本质就是链接库!
理解现象:
1、我们平常在用VS2019、VS2022配置C/C++环境时,实际就是在安装C/C++的编译器,和C/C++的标准库以及配套的头文件!
2、我们在使用编译器或编辑器,都会有语法提醒或自动补齐,语法提醒的本质就是编译器会自动的在我们包含的头文件中寻找与我们输入的字符相匹配字符串;

为什么要有库

简单点来说就是为了提高开发效率
不然为什么我们能使用诸如printf/scanf这样的函数,就是因为别人已经帮我们写好了,我们是调用的别人的劳动成果!
假设没有大佬们提供的诸如print和scanf这类函数,那么如果某一天我们的代码中需要一个诸如printf功能的函数,那么我们就得自己写,通过前面的对于文件描述符的学习,我们知道printf底层是封装了系统调用的,我们想要写出一个printf还需要了解一点系统调用的知识!可能在2h的开发中,我们就有1.5h在写这个printf,而且还不能保证我们写的没有bug,这就会极大的拉低我们的开发效率,不利于这门语言的发展!为此,为了提高程序员们的开发效率和本门编程语言的发展,哪些大佬级别的开发人员呢,就帮我们把诸如printf/scanf等类使用的比较频繁的、比较复杂的函数帮我们实现了,然后呢将这些函数都打包在一个文件里面,这个文件就被称为库!我们在下载C/C++相关配置的时候,实际上就是在下这些库!和本门编程语言配套的编译器!

写一写库

我们都知道我们的源文件会经历这几个阶段:
预处理、编译、汇编、链接阶段!
其中在汇编阶段结束,链接还未开始的时候,我们的源文件会形成一个.o文件,这个文件里面全是二进制,这时候的文件叫目标文件,只有我们将这些目标文件链接起来才会形成可执行文件!
在这里插入图片描述
我们的库就是将包含各种方法的源文件编译形成的.o文件打包在一起
库中没有包含main函数的.o文件!
比如上图中的func.o、math.o就会被打包在一起,存入一个文件中,这个文件就叫做库!

制作一个静态库

在Linux下制作一个静态库,首先要用gcc命令将我们需要打包的方法形成.o文件!
比如现在我们有如下文件:
在这里插入图片描述
第一步:利用gcc命令将我们需要制作成静态库的.c文件编译成.o文件,对应上例中,就是使用命令gcc -c Add.c -o Add.ogcc -c Sub.c -o Sub.o形成.o文件:
在这里插入图片描述
第二步:将这两个文件打包成静态库;
使用命令:ar -rc libxxx.a Add.o Sub.o其中xxx是静态库的名字!记住静态库是以.a结尾,我们在利用ar命令编写静态库的时候名字的命名一定要遵守libxxx.a的规则!虽然xxx才是我们真实的库名字!
-r: 插入成员名字……存档(带替换),如果在库中已经存在的成员名称与正在添加的成员名称相同时,则替换掉重名的成员;
也就是说:现在有个静态库中有如下文件:
在这里插入图片描述
-c: 创建存档文件,如果我们指定的文件不存在,则创建它;如果存在就打开!
在这里插入图片描述
至此我们的静态库就制作完成!为例方便操作,我们将头文件和静态库分别打包在不同的目录中!并且删除所有的无关文件!
在这里插入图片描述
既然静态库已经制作完成,那么我们应该如何使用这个静态库?
为例方便演示,我们新创建一个来表示用户:
在这里插入图片描述
测试代码:
在这里插入图片描述
我们会发现,我们直接这样写的话,会出现语法报错,这是因为,在我们写的时候,vim会自动去系统默认存储头文件的路径搜索我们所包含的头文件,在系统路径下没有找到Add.h和Sub.h头文件自然也就报错了,都找不到头文件,vim自然不认识Add()、Sub()是个啥了!
但是没关系,我们在利用gcc编译器编译的时候,我们显示的告诉gcc应该去哪里寻找我们的头文件件和库!
在这里插入图片描述
我们给gcc 带上 -I 头文件路径显示的告诉gcc头文件在哪里就好了,这样的话gcc编译器就会现在我们指定的路径下搜索头文件,再去系统默认路径下搜索!
gcc main.c -I ../include/
在这里插入图片描述
gcc 不认识Add、Sub这些符号,那一定是链接阶段出现了问题,在链接阶段,编译器会为哪些在main.c文件中被使用的但是没有定义的符号去其他源文件或库中寻找这些符号的定义,如果还是没找到!那么就会报链接错误,说编译器不认识这些符号!现在这里只有main.c一个文件,编译器不会去其他文件中找,因此编译器会去库中寻找,去那些库?编译器会去系统默认路径下的标准C库(也就是存放诸如printf/scanf等文件的库)中寻找!去标准库中寻找,自然找不到Add、Sub等符号!因为这些符号的定义并没有在标准C库中,而在我们自己的静态库中,因此我们也要显示的告诉编译器我们的库在哪里,我们需要为gcc带上-L 库所在的路径,告诉编译器先去我们指定的路劲下搜索库,再去系统默认路径下搜索!
gcc mian.c -I ../include -L ../lib-static/
在这里插入图片描述
是的,我们的确也告诉了编译器我们的库在哪里,可是编译器不敢随意猜测我们到底要链接那个库!因为我们的库属于第三方库!(C语言标准库属于第一方库、系统调用库属于地二方库),尽管存放库的路径底下只有一个库文件,只要我们不显示指定编译器也是不敢轻易去链接的!
因此我们需要利用-l 库文件名去指定编译器链接那个库!
在这里插入图片描述
libmath.a真的是库的名字吗?
不是的!只有mymath才是库的名字,其他字段都是一种格式,我们要去掉这种格式!编译器在查找的时候也会遵守这种格式去查找!
因此我们更改静态库名后去查找:
gcc main.c -I ../include -L ../lib-static/ -l mymath
在这里插入图片描述
运行看看:
在这里插入图片描述
成功运行!!!!
可是上面运行静态库的方式真的很挫唉!就链接个静态库就需要指明一大堆路径,编译器不是会自动给默认路径下查找头文件和库嘛,我们可不可以将我们的库和头文件也放入默认路径下?这样编译器在默认路径下查找的时候就能找到我们的头文件和库了!
在Linux下,编译器一般会去/usr/include/路径下搜索头文件,因此我们可以将我们的头文件添加到这个路径下去!同时编译器也会去/usr/lib64/目录下搜索库文件,因此我们可以将我们的静态库添加到这个路径下!
假设现在我处于otherDir目录:
cp ./include/*.h /usr/include///将我们自己的头文件拷贝一份到系统默认存储头文件的路径下;
cp ./lib-static/libmymath.a /usr/lib64///将我们自己的静态库文件拷贝到一份系统默认存储库文件的路径下;
当然,我们在将自己文件拷贝到系统路径下时记得使用root身份或者加上sudo!
在这里插入图片描述
在这里插入图片描述
接着我们在一次来编译一下main.c:
在这里插入图片描述
编译器还是不认识Add、Sub符号,说明链接器并没有链接上我们的静态库,这是为什么?
我们不是已经将我们的静态库放入系统默认路径下了,可是为什么还是不行!
主要还是因为我们的库属于第三方库,链接器是不敢随意猜测我们要链接的库的尽管他知道我们的库就在这个路径下,毕竟我们的库不是官方库,如果我们不指明链接那个的话,链接器是不会去链接的,因此我们需要带上选项 -l 来指定链接那个库!
在这里插入图片描述
运行一下:
在这里插入图片描述
正常运行!

制作一个动态库

制作动态库的步骤与制作静态库的步骤是一样的
第一步同样是制作.o文件,只不过我们在制作动态库的.o文件时我们需要带上-fPIC选项,让我们的.o文件是以“与位置无关码”的形式排布的!
因此我们形成.o文件的的命令:
gcc xxx.c -o xxx.o -c -fPICxxx是文件名,自己随便取
-c: 让我们的xxx.c文件编译到汇编阶段就停止(包括汇编阶段);
-fPIC: 让生成的.o文件以“与位置无关码”的方式生成;
在这里插入图片描述
接着就是将这些.o文件打包进动态库;
上面我们制作静态库的时候用的是ar命令,现在制作动态库时,我们不用ar命令了,就用gcc命令!
gcc不编译器吗?怎么能制作动态库?
主要是因为现在主流的库就是动态库,很多链接器在链接库的时候就是优先采用静态库!
因此gcc编译器中也内置了打包动态库的功能!
因此打包静态库的命令:
gcc xxx.o -o libmymath.so -shared
**-shared:**告诉gcc我们这不命令是打包动态库;
在这里插入图片描述
紧接着我们老办法将动态库和所匹配的头文件放入otherDir目录下的头文件目录和库目录中:
在这里插入图片描述
我们使用一样的办法来编译我们的main.c
在这里插入图片描述
我们显示的告诉他一下:
在这里插入图片描述
我们显示的告诉他一下:
在这里插入图片描述
哦哦!!原来是我们没有告诉编译器要具体要链接那个库,编译器不敢猜测我们要链接哪个库:
我们显示告诉它:
在这里插入图片描述
成功生成可执行程序!
现在我们来运行看看:
在这里插入图片描述
这是怎么回事,我们不是已经告诉编译器我们要链接的库在哪里,并且要链接那个库,为什么还会出现找不到库的报错?
对的!没错,我们的确是告诉了gcc我们要链接的库在哪,具体要链接哪一个库,因此我们的链接器并没有在链接阶段报错!
可是!!!!我们没有告诉OS系统啊!!!
这关OS什么事?
主要是因为,在链接阶段链接器发现我们链接的是静态库过后,就会采用动态链接,而动态链接呢是不会把我们所使用的符号的定义从库中加载进我们自己的源文件的,链接器只会将我们所使用的符号在动态库中的地址给拷贝进来,当我们的程序真正运行起来时,需要调用这个符号时,再由我们的OS根据这个符号的地址去对应库中将这个符号对应的实现加载进内存供我们的程序使用!
可是现在OS不知道我们的库在哪里,它光拿着这个符号的地址去哪里找?
OS会去自己的默认库路径下寻找,如果在库路径中没有找到对应的动态库就会出现刚才我们的报错!
可为什么我们用静态库链接的时候,OS就不会报错呢?
主要是因为静态库,链接器采用的是静态链接,静态链接就是直接将我们所使用到的符号从库中将其定义完全拷贝进我们的源文件,我们的源文件就有了这些符号的定义,当我们的程序在运行的时候能够在自己的代码段找到这些符号的定义,自然不需要OS去找这些符号的定义啦,自然不会报错啦!
采用静态链接大大的提高了我们程序的独立性!但同时也是我们的程序体积变的膨胀!
采用动态库相比于静态链接来说,减少了程序的体积,可以让多个程序共享同一个动态库,减少了数据冗余!
我们如何解决OS找不到动态库的问题?
1、将我们的动态库的路径添加到LD_LIBRARY_PATH环境变量中去;
OS会默认去这个环境变量中寻找动态库;
在这里插入图片描述
如果没有这个环境变量的话,我们可以利用export自己导一个;
在这里插入图片描述
命令export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:动态库所在目录(不需要特意指明是哪个动态库)
接着我们再来运行我们的程序:
在这里插入图片描述
我们的程序成功运行!
可是这只是一种临时的方案,当我们重新登陆xShell的时候我们之前导入LD_LIBRARY_PATH环境变量的路径就会消失,要想让此路径一直存留在LD_LIBRARY_PATH环境变量中,就需要将我们的路径添加到配置文件中!
在这里插入图片描述
2、将我们的库文件添加到/usr/lib64/路径下,或者在这个路径下面为我们的库建立软连接!
我们选择采用建立软连接的方法:
ln -s 源文件 目标文件
在这里插入图片描述
在这里插入图片描述

我们再来运行一下我们的程序:
在这里插入图片描述
成功运行!
3、ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
但是方法2呢有点污染我们的uusr/lib64/共享库目录了;
实际上OS除了会去/usr/lib64/目录下寻找动态库之外,还会去etc/ld.so/conf.d/目录下的各个.conf文件中读取动态库路径,然后去读到的路径下寻找动态库!
因此我们可以先创建一个.conf文件:
在这里插入图片描述
然后再mymath.conf文件中写入我们自己动态库的绝对路径:
在这里插入图片描述
然后指向ldconfig命令来使我们的配置文件生效:
在这里插入图片描述
在这里插入图片描述
依旧正常运行!

关于动静态库的一点小实验

1、当动静态库同时存在时,编译器默认采用动态链接:
在这里插入图片描述
现在我将动静态库放在一个目录下:
现在我们来编译main.c:
在这里插入图片描述
2、当一个目录中既有动态库又有静态库时,我们想让编译器链接静态库,可以为编译器带上-static选项,这样编译器就会去链接静态库:
在这里插入图片描述
3、如果一个目录下只有静态库,那么编译器没得选只能选择链接静态库!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当我们删掉所有关于动态库的什么默认路径问题时,我们在运行main发现可以独立运行,那么说明我们的main.c对于mymath的确是采用的静态链接:
在这里插入图片描述

注意: 云服务器一般只会提供C/C++标准动态库
如果想要下载C/C++标准静态库,可是使用一下命令:
sudo yum -y install glibc-static//下载C标准静态库
sudo yum -y install libstdc++-static//下载C++标准静态库

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

南猿北者

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

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

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

打赏作者

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

抵扣说明:

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

余额充值