Linux 基础IO

一.C文件IO相关操作

只有文件名但不带路径的话,默认在当前路径下打开文件(以写的方式打开文件若文件不存在会自动创建文件),那到底什么是当前路径呢?

// test目录下 myproc.内容

#include<stdio.h>
int main()
{
    
    
        FILE* fp = fopen("test.txt","w");
        if(fp == NULL)
        {
    
    
                perror("fopen");
                return 1;
        }
        fclose(fp);
}

在这里插入图片描述

可以看出,当前路径和可执行程序所处的路径没有关系,在哪个路径下运行起来可执行程序,该路径就被称为当前路径

为了让大家看的更清楚,稍微修改一下代码,关闭文件后不让进程终止来查看进程的相关信息

// test目录下 myproc.内容

#include<stdio.h>
int main()
{
    
    
        FILE* fp = fopen("test.txt","w");
        if(fp == NULL)
        {
    
    
                perror("fopen");
                return 1;
        }
        fclose(fp);
        sleep(10000);
}

cwd(current work directory) : 在哪个路径下运行起来可执行程序,进程创建的临时文件都在当前路径下创建
exe : 可执行程序路径
在这里插入图片描述

打开文件,读写文件,关闭文件都是进程运行的时候完成的

#include<stdio.h>
int main()
{
    
    
        FILE* fp = fopen("test.txt","r");
        if(fp == NULL)
        {
    
    
                perror("fopen");
                return 1;
        }
        // 二.
        int count = 5;
        char buffer[64];
        while(count)
        {
    
    
        	   // 以\n为分隔符,一次读取一行内容
                fgets(buffer,sizeof buffer,fp);
                printf("%s",buffer);
                count--;
        }
       
//      一. 
//      int ct = 5;
//      while(ct)
//      {
    
    
//              fputs("hello bit\n",fp);
//              ct--;
//      }
        fclose(fp);
}

在Linux中,一切皆文件,键盘和显示器也可以将其当作文件,那么为什么在C语言当中我们没有打开显示器文件就可以直接使用printf函数进行写入呢?为什么没有打开键盘文件就可以直接使用scanf函数进行读取呢?,原因如下 :

任何进程在运行的时候,默认打开三个输入输出流
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*,(fopen返回值类型,文件指针),FILE* 是C语言的概念,文件描述符是系统级别的概念

在这里插入图片描述

#include<stdio.h>
int main()
{
    
    
        int count = 5;
        char buffer[64];
        while(count)
        {
    
    
                fgets(buffer,sizeof buffer,stdin);
                printf("%s",buffer);
                count--;
        }

        int ct = 5;
        while(ct)
        {
    
    
                fputs("hello world\n",stdout); // strerr
                ct--;
        }
}

由"w" 和 “a” 可以联想到之前讲到的重定向(>)和追加重定向(>>)

test.txt已经存在5行hello world,使用"a"方式打开文件,向文件末尾追加5行hello lyp

#include<stdio.h>
int main()
{
    
    
        FILE* fp = fopen("test.txt","a");
        if(fp == NULL)
        {
    
    
                perror("fopen");
                return -1;
        }
        int count = 5;
        while(count)
        {
    
    
                fputs("hello lyp\n",fp);
                count--;
        }
        fclose(fp);
}

在这里插入图片描述

text.txt中已经存在5行hello world,5行hello lyp,使用"w"方式打开文件,清空原始内容,写入5行hehe

#include<stdio.h>
int main()
{
    
    
        FILE* fp = fopen("test.txt","w");
        if(fp == NULL)
        {
    
    
                perror("fopen");
                return -1;
        }
        int count = 5;
        while(count)
        {
    
    
                fputs("hehe\n",fp);
                count--;
        }
        fclose(fp);
}

在这里插入图片描述

二.系统文件IO

open系统调用接口介绍

#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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
 O_RDONLY: 只读打开
 O_WRONLY: 只写打开
 O_RDWR : 读,写打开
 这三个常量,必须指定一个且只能指定一个
 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
 O_APPEND: 追加写
 mode:
 设置文件权限
 返回值:
 成功:新打开的文件描述符
 失败:-1

flags : 系统函数参数传参标志位

传递给flags的这些常量都是宏,通过如下命令可以查看这些宏的定义,可以发现这些宏的二进制序列只有一个1,这些常量进行或运算传递给flags,open函数内部,再通过与运算判断传递了哪些常量

int open(const char *pathname, int flags, mode_t mode)
{
    
    
	if(flags & X)
	{
    
    }
	if(flags & Y)
	{
    
    }
}
open(argu1,X | Y,argu3);
grep -E 'O_CREAT|O_RDONLY|O_WRONLY' /usr/include/ -R

在这里插入图片描述

下面我们就来使用一下open

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
    
    
	 umask(0);
	 // O_WRONLY|O_CREAT 等价于 fopen 中的"w"
	 int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
     printf("%d\n",fd);
}

我们想以只写的方式打开test.txt文件,若test.txt文件不存在,会创建一个test.txt文件出来,权限为666
但结果权限为664(默认umask为2),所以我们可以把umask设置为0
在这里插入图片描述

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);

        int fd1 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\n",fd1);

        int fd2 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\n",fd2);

        int fd3 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\n",fd3);

        int fd4 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\n",fd4);

        int fd5 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\n",fd5);
}

通过运行结果发现,文件描述符从3开始,这是因为默认打开了标准输入(0),标准输出(1),标准错误(2),所谓的文件描述符是数组下标
在这里插入图片描述

write

 ssize_t write(int fd, const void *buf, size_t count);

fd : 文件描述符,buf : 写的内容 ,count : 期望写入的字节数
ssize_t : 实际写入的字节数
实际写入的字节数 <= 期望写入的字节数(ssize_t <= count)

// test.txt为空,向test.txt写入5行hello world

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);

        int fd1 = open("test.txt",O_WRONLY|O_CREAT,0666);
        printf("%d\n",fd1);

        int count = 5;
        const char* buf = "hello world\n";
        while(count)
        {
    
    
                write(fd1,buf,strlen(buf));
                count--;
        }
}

read

ssize_t read(int fd, void *buf, size_t count);

fd : 文件描述符,buf : 读取的数据存放的地址 ,count : 期望所读的字节数
ssize_t : 实际所读的字节数
实际所读字节数 <= 期望所读字节数(ssize_t <= count)

// test.txt中有5行hello world,从test.txt中读取字符写入到标准输出中

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);

        int fd1 = open("test.txt",O_RDONLY);
        printf("%d\n",fd1);

        char c;
        while(1)
        {
    
    
                ssize_t s = read(fd1,&c,1);
                if(s <= 0)
                {
    
    
                        break;
                }
                write(1,&c,1);
        }
}

C语言提供的IO接口底层都封装了系统调用接口
在这里插入图片描述

为什么要进行封装 ?

(1).保证可读性以及调用简单
(2).保证跨平台性 : 我们可以发现C语言可以在任一平台上跑,原因在于C语言在设计时,与系统强相关的不可跨平台的接口都被C语言封装了一遍(我们上面用C语言所写的fopen(),fclose()等函数底层都封装了open,close等系统调用)

open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数

上面的 fopen fclose fread fwrite fgets fputs都是C标准库当中的函数,我们称之为库函数(libc)。
而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图
在这里插入图片描述

文件描述符 fd

磁盘文件 vs 内存文件

内存文件 : 一个进程可以打开多个文件,在系统中,任何时刻,都可能存在大量的已经打开的文件,对这些文件进行管理依然遵循先描述再组织,struct_file就是描述文件的结构体,以双向链表的方式将其组织起来,对文件的管理就变成了对双链表的增删查改,在内存中有task_struct,struct_file两条双链表,我们想要知道这些被打开的文件哪些属于某一个进程,我们就需要建立进程和文件的对应关系

磁盘文件 : 存在磁盘上的文件并不是只保存了内容,还保存了文件的属性/元信息(文件的名字,创建日期,大小)

文件 = 内容 + 属性

我们将文件打开时,是有两份的,一份在硬盘上,一份在内存里(内存文件更多的是文件的属性信息),当我们想要读写数据时,再延后式的慢慢加载数据到内存(这里会有缓冲区的概念)

// 人和操作系统唯一的交互方式就是进程,而进程和操作系统的交互方式是系统调用接口

open系统调用打开文件的过程

(1). 进程打开一个文件,内存中创建struct_file结构体,存储打开文件的属性信息
(2). 操作系统为该文件分配文件描述符(fd_array数组中未被分配的且最小的下标)
(3). 将struct_file结构体的地址填到fd_array数组对应的位置,返回该文件的文件描述符

由此可知read,write等系统调用为什么要传入fd ?,因为拿到fd可以找到文件对应的struct_file结构体,得到文件的相关信息

重定向

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);
        int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
    
    
                return 1;
        }

        write(1,"hehe\n",5);
        write(1,"hehe\n",5);
        write(1,"hehe\n",5);
        write(1,"hehe\n",5);
        write(1,"hehe\n",5);

        close(fd);
}
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);
        close(1);
        int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
    
    
                return 1;
        }

        write(fd,"hehe\n",5);
        write(fd,"hehe\n",5);
        write(fd,"hehe\n",5);
        write(fd,"hehe\n",5);
        write(fd,"hehe\n",5);

        close(fd);
}

对比这两段代码,我们会发现关闭显示器文件后,fd的值为1,即test.txt文件的文件描述符为1,达到的效果为原本要写入到显示器当中的内容写入到了test.txt中,这种现象叫做输出重定向(>)

重定向的本质 : 修改文件描述符对应的struct_file*的指向

// 输出重定向
cat > test.txt

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);
        close(1);
        int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
    
    
                return 1;
        }
	
        printf("hello world\n");
        fprintf(stdout,"hello fprintf\n");
        fputs("hello fputs %d %d %f %c\n",stdout);
		// 需要刷新缓冲区
        fflush(stdout);

        close(fd);
}

解释这段代码前先进行一下知识的铺垫

实际上,C语言的 fopen 函数打开文件主要做了以下三件事情
(1). 创建FILE结构体
(2). 底层调用open函数打开文件,返回fd,将fd填充到FILE结构体当中的 _fileno(封装的文件描述符)
(3). 返回FILE结构体的地址(FILE*)

所以我们C语言中平常所使用的fread,fwrite,fgets,fputs中拿到我们传递的参数stream(FILE* stream),根据stream指针找到struct FILE结构体,struct FILE结构体中封装了文件的fd,拿到fd后,去task_struct->files_struct->fd_array数组中得到文件对应的struct_file结构体,由此得到文件信息进行读取或写入

之前也提到过,任何一个进程,默认会打开3个文件 : 标准输入(键盘文件),标准输出(显示器文件),标准错误(显示器文件),在C语言中创建对应的FILE结构体,调用open函数打开文件,用返回值fd填充 _fileno,返回FILE结构体的地址给stdin,stdout,stderr(FILE*指针)

再来看这段代码,我们首先关闭了1号文件描述符,open打开文件后,1号文件描述符被分配给了test.txt文件,代码中 printf/fprintf/fputs 参数stream都为stdout,stdout->struct FILE结构体->fd->struct_file结构体,此时struct_file结构体对应test.txt文件,因此内容写入到了test.txt文件当中

// 输入重定向
cat < test.txt

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);
        close(0);
        int fd = open("test.txt",O_RDONLY);
        if(fd < 0)
        {
    
    
                perror("open");
                return 1;
        }
        char buffer[50];
        fgets(buffer,50,stdin);
        printf("%s\n",buffer);
		
		close(fd);
}

// 追加重定向

cat >> test.txt

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);
        close(1);
        int fd = open("test.txt",O_WRONLY|O_APPEND); // 等价于C语言 fopen 中的"a"选项
        if(fd < 0)
        {
    
    
                perror("open");
                return 1;
        }

        printf("hello world\n");
        fprintf(stdout,"hello fprintf\n");
        fputs("hello fputs %d %d %f %c\n",stdout);

		fflush(stdout);
}

凡是显示到显示器上面的内容,都是字符
int a = 1024; printf("%d",a);
printf在格式化输出时,将1024转换成字符写入到显示器文件
凡是从键盘读取的内容,都是字符
int a; scanf("%d",&a);
scanf在格式化输入的时候,将读到的字符根据ascii码转换成整形写入a中
所以,键盘和显示器一般被称之为"字符设备"

缓冲区

(1). 无缓冲

(2). 行缓冲(常见的对显示器进行刷新数据时)
四种刷新方式 : 碰到\n刷新,强制刷新,程序结束刷新,缓冲区满了刷新
(3). 全缓冲(对磁盘文件写入的时候采用全缓冲)
三种刷新方式 : 强制刷新,程序结束刷新,缓冲区满了刷新

我们向显示器,磁盘文件去写入的时候效率是很低的,所以我们把数据积累到内存缓冲区中,定期去刷新,理论上,全缓冲效率最高,无缓冲效率最低

为什么对磁盘文件写入的时候采用全缓冲,对显示器文件写入采用行缓冲 ?
磁盘文件在写入的时候人是不会立即去读的,所以采用全缓冲效率高
显示器文件在写入的时候人是要立即去读的,采用无缓冲的方式可以达到目的,但效率太低,采用全缓冲的方式人无法立即读到信息,因此采用行缓冲在效率和可用性上达到平衡

(1). 这个缓冲区在哪里?
struct FILE结构体里有维护用户缓冲区相关的字段(看一下FILE的结构?)
(2). 这个缓冲区是谁提供的?
C语言自带的缓冲区,每个进程都有自己的缓冲区(进程缓冲区)
(3). OS也是有缓冲的
OS也有自己的缓冲区(内核缓冲区),刷新进程缓冲区的内容并不会将内容直接刷新到文件中,而是刷新到内核缓冲区,由OS定期刷新到文件中(操作系统有自己的刷新机制)

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
		// C库函数
        printf("hello printf\n");
        fprintf(stdout,"hello fprintf\n");

        // system
        const char* msg = "hello write\n";
        write(1,msg,strlen(msg));

        fork();
}

该程序重定向和非重定向的结果是不一样的
在这里插入图片描述

通过运行结果我们可以看出以下两个结果
(1). 重定向还是非重定向会更改进程的缓冲方式
(2). C接口打了两次,系统调用接口打了1次

下面我们来分析一下运行结果
C语言自带缓冲区,系统调用没有缓冲区
(1). 使用printf/fprintf向显示器文件中写入时,采取的是行刷新策略,执行完printf/fprintf后,“hello printf\n” "hello fprintf\n"已经被刷新到了显示器文件当中,write无缓冲,直接写到显示器文件当中,fork()后创建子进程,由于缓冲区中没有数据需要刷新,因此显示器中显示为3行数据

(2). 使用printf/fprintf向文件中写入时,采取的是全缓冲策略,执行完printf/fprintf后,“hello printf\n” "hello fprintf\n"被打印到缓冲区中,write无缓冲,直接写到显示器文件当中,fork()后创建子进程,父进程退出要刷新缓冲区,但因为父子进程具有独立性,改变父进程缓冲区不能影响子进程缓冲区,所以发生写时拷贝,然后子进程再刷新缓冲区

解释一下上面重定向的代码最后的fflush(stdout),以输出重定向为例,如果删掉fflush(stdout),最终test.txt中无内容,原因向文件写入采用全缓冲策略,不将缓冲区内容刷新到文件当中,文件中当然没内容,可能这里有人会有疑问,进程结束之后,会自动刷新缓冲区啊,这当然没错,但我们在最后close(fd),将文件关闭了,操作系统没办法把内容刷新到文件中了(可以使用fclose(stdout))

(1). fflush(stdout);
     close(fd);

(2). fclose(stdout);

(3). 什么都不做,进程退出自动刷新缓冲区内容

stdout和stderr虽然都可以向显示器文件进行写入,但它们的struct_file结构体是不一样的,可以认为打开同一文件,只是对应的结构体不一样,通过下面代码可以进行验证

#include<stdio.h>
#include<string.h>
int main()
{
    
    
        printf("hello printf\n");
        perror("printf");
        fprintf(stderr,"hello fprintf\n");
}                     

在这里插入图片描述

非重定向之前,stdout和strerr都可以向显示器文件写入,但重定向后,stdout无法向显示器写入,stderr仍然可以向显示器写入

如何理解Linux下一切皆文件?

从我们的直觉上来看,磁盘,显示器,网卡,键盘很明显不是文件,系统和这些外设进行沟通只有两种方式I(input)和O(output),内存中存在进程,进程从外设读到信息我们叫做read,向外设写入信息我们叫做write,但是很明显只能向显示器写入(不能读取,read方法为空),只能从键盘中读取(不能写入,write方法为空),即所有的外设硬件都有自己的read,write方法,这些方法绝对不一样,那能不能用同一种方法去处理所有的设备呢?可以的!

在struct file结构体中,存在着函数指针,将来不同的文件打开时,就让函数指针指向不同文件各自对应的read,write方法,换言之,当上层要读写文件时,不需要关心底层打开的是什么文件,只需要调用函数指针所指向的方法即可。所以,在上层读写文件时,我们只需要调用read,write方法就可以完成统一的读写。这种技术在C++中叫做多态

至此,我们站在应用层就可以认为Linux一切皆文件

内核源代码 :
在task_struct中存在 files指针指向 struct files_struct 结构体
在这里插入图片描述

在 struct files_struct 结构体中,存在struct file* fd_array[NR_OPEN_DEFAULT]数组,存储 struct file*
NR_OPEN_DEFAULT默认为32,一般可以扩展到1024

在这里插入图片描述

在struct file 结构体中,存储打开文件的各种信息,其中上文提到的struct file中的函数指针存在 struct file_operations结构体中

在这里插入图片描述

在这里插入图片描述

使用dup2系统调用

#include<unistd.h>

int dup2(int oldfd, int newfd);

dup2函数的作用是将oldfd中的struct file* 指针赋值给newfd中的struct file* 指针,我们可以使用dup2完成输出重定向

#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
        umask(0);
        int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
    
    
                perror("open");
                return 1;
        }
        dup2(fd,1);

        printf("hello printf\n");
        fprintf(stdout,"hello fprintf\n");
        fputs("hello fputs\n",stdout);
        
		return 0;
}                                        

给简易shell中增加重定向功能

(1). fork()创建出的子进程会拷贝父进程的struct files_struct 结构体,即父子进程共用同一份文件(写时拷贝 ?)

(2). 进程程序替换不会影响文件的打开和关闭

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<string.h>
#define LEN 1024
#define NUM 32
int main()
{
    
    
        int type = 0;
        char cmd[LEN];
        char* argv[NUM];
        while(1)
        {
    
    
                printf("[luanyiping@bogon shell]$");
                // 从标准输入中读取指令到cmd中
                fgets(cmd,LEN,stdin);
                char* start = cmd;
                while(*start != '\0')
                {
    
    
                        if(*start == '>')
                        {
    
    
                               *start = '\0';
                                start++;
                                if(*start == '>')
                                {
    
    
                                        type = 1;
                                        *start = '\0';
                                        start++;
                                        break;
                                }
                                 break;
                        }
                        if(*start == '<')
                        {
    
    
                                type = 2;
                                *start = '\0';
                                start++;
                                break;
                        }
                        start++;
                }
                if(*start != '\0')
                {
    
    
                        while(isspace(*start))
                                start++;

                }
                else
                {
    
    
                        start = NULL;
                }

                // 将最后的'\n'变成'\0'
                cmd[strlen(cmd) - 1] = '\0';
                // 以空格为分隔符分割指令
                argv[0] = strtok(cmd," ");
                int i = 1;
                while(argv[i] != strtok(NULL," "))
                {
    
    
                      i++;
                }
                // 创建子进程去完成任务
                pid_t id = fork();
                if(id == 0)
                {
    
    
                        if(start != NULL)
                        {
    
    
                                if(type == 0)
                                {
    
    
                                        int fd = open(start,O_WRONLY|O_CREAT,0666);
                                        if(fd < 0)
                                        {
    
    
                                                perror("open");
                                                exit(2);
                                        }
                                        dup2(fd,1);
                                }
                                else if(type == 1)
                                {
    
    
                                        int fd = open(start,O_WRONLY|O_APPEND);
                                        if(fd < 0)
                                        {
    
    
                                                perror("open");
                                                exit(3);
                                        }
                                        dup2(fd,1);
                                }
                                else if(type == 2)
                          		 {
    
    
                                        int fd = open(start,O_RDONLY);
                                        if(fd < 0)
                                        {
    
    
                                                perror("open");
                                                exit(4);
                                        }
                                        dup2(fd,0);

                                }
                        }
                        // 进程程序替换
                        execvp(argv[0],argv);
                        exit(0);
                }
                // 阻塞式等待子进程退出
                int status = 0;
                pid_t ret = waitpid(id,&status,0);
                if(ret > 0)
                {
    
    
                        printf("child exit code : %d\n",WEXITSTATUS(status));
                }
        }
}                                                                                                                        

理解文件系统

查看一个文件的inode编号,可以使用命令 ls -i

上面提到过,磁盘上的文件由内容 + 属性(元信息)构成,所以一个空文件也会占用磁盘的空间

Linux将文件的属性和内容进行分离存储,内容直接在磁盘存储,属性存储在inode中(inode在磁盘中)

inode是任何一个文件的属性集合,Linux中几乎每一个文件都有一个inode,可能存在大量的inode,为了区分inode,用inode编号

(1). 什么是磁盘?

磁盘 : 一种永久性存储介质,笔记本电脑一般装的是ssd(固态硬盘),磁盘几乎是所有硬件设备中唯一的机械设备,目前所有的普通文件都是在磁盘中存储的

首先看下磁盘的整体结构
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们来看下盘片结构,图中的一圈圈灰色同心圆称之为一条条磁道,最外面称为0磁道,往圆心依次加1,即1磁道,2磁道…,从圆心向外画直线,可以将磁道划分成若干个弧段,每个磁道上的一个弧段称为一个扇区(图中绿色部分),扇区是磁盘的最小组成单元,通常为512字节,而我们计算机存储的所有数据,就放在扇区上,磁头划过扇区时根据电流信号的高低,完成数据的0,1转换,这也是计算机唯一能够识别的数字,从而完成数据的读取和写入
在这里插入图片描述

这里有几个专有名词

磁头 : 磁头和盘面数是相等的,即每个盘片有2个盘面,2个磁头
磁道 : 盘片中每一个同心圆称为一个磁道
柱面: 所有盘面的不同同心圆组成不同的柱面(数据的读写和磁盘分区都是都是按柱面进行的)
扇区: 每个磁道上的一个弧段为一个扇区
寻道时间: 磁头从开始移动到数据所在磁道所需的时间
旋转延迟: 盘片旋转将请求数据所在扇区移至读写磁头下方所需时间,旋转延迟取决于磁盘转速

为什么数据的读写和磁盘分区都是按柱面进行的?

数据的读写按柱面进行,即磁头读写数据时,首先在同一柱面内从"0"磁头开始进行操作,依次向下在同一柱面的不同盘面上进行操作,只有同一柱面的所有磁头读写完毕后磁头才转移到下一柱面,因为选取磁头只需通过电子切换即可,选取柱面需要机械切换,电子切换相当快,比在机械上磁头向邻近磁道移动快得多,所以数据的读写按柱面进行,而不是按盘面进行,也就是说,一个磁道写满数据后,就在同一柱面的下一盘面来写,一个柱面写满后,才移动到下一柱面的扇区开始写数据,读数据也按这种方式进行,这样就提高了硬盘的读写效率

我们可以把磁盘的圆形存储介质看作线性存储介质,当磁盘很大时,为了方便管理,先将磁盘进行分区,再进行格式化
格式化 : 将管理信息写到每一个分区里,其中管理信息是由文件系统决定的

磁盘分区以后,每个分区可能仍然较大,不好管理,所以每个分区被划分成一个个block,一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。

分区格式化之后,inode的个数是确定的

在这里插入图片描述

当我们系统在启动的时候,主板上有一个设备bios,内部有一段存储区域,帮我们找到磁盘的最开始的分区表,根据分区表找到某个分区的最开始有一部分代码(Boot Block)能够帮我们找到操作系统的代码

Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相
同的结构组成。

Super Block :存放文件系统本身的结构信息。记录的信息主要有:block 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了

块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没
有被占用,每个bit表示一个block是否空闲可用(如第1个比特位为1,表示第一个block已经被使用,为0表示第一个block未被使用)。

inode Bitmap : 每个bit表示一个inode是否空闲可用。(如第1个比特位为1,表示第一个inode已经被使用,为0表示第一个inode未被使用)

inode Table : 存放inode

Data blocks :存放文件内容

文件 = inode + 内容,inode在Linux中一般占128字节或256字节,inode会被存储到 inode Table中,inode是一个结构体,存储文件的各种属性信息(文件的大小,权限,拥有者,所属组)

文件的内容存储到 Data blocks 中,Data blocks 由一个个块(4KB)组成,文件的内容填到一个个块中,文件的inode中有一个记录文件所使用块的编号的数组,遍历该数组就能得到文件的内容

Super Block 存储 inode 和 block 的使用信息,哪些被使用,哪些没被使用

总结 :
(1). 请描述一下,创建一个文件的过程以及写入1kb数据的过程

1). 遍历inode Bitmap找到第一个未被使用的inode编号,并将该比特位置为1(表明该inode已经被使用)
2). 将各种属性信息填入 inode Table 对应编号的inode结构体中
3). 遍历Block Bitmap找到第一个未被使用的block块,并将该比特位置为1(表明该block已经被使用), 将该block块的编号填入blocks数组,向该block块中写入数据

(2). 删除一个文件,在做什么?

1).将 inode Bitmap 中对应文件inode编号的比特位置为0
2). 将 Block Bitmap 中文件所占用的block编号对应的比特位置为0

(3). 拷贝文件慢,删除文件快,删除文件后,是可以恢复的, 如果误删了文件,最好的做法是什么都不做,防止之前数据被覆盖掉

inode
{
inode ID;
int blocks[12];
int ref // 引用计数记录硬链接数
// 一些属性信息

}

(4). 如何理解目录?目录创建的过程?

1). 遍历inode Bitmap找到第一个未被使用的inode编号,并将该比特位置为1(表明该inode已经被使用)
2). 将目录的各种属性信息填入 inode Table 对应编号的inode结构体中
3). 在目录下创建文件时,创建文件的过程和上面描述是一致的
4). 遍历Block Bitmap找到第一个未被使用的block块,并将该比特位置为1(表明该block已经被使用), 将该block块的编号填入blocks数组,向该block块中写入数据,写入的数据是文件名和文件对应的inode号

在这里插入图片描述

(5). ls ls -l cat 这些命令在做什么呢?

ls :
找到当前目录的inode,遍历inode中的blocks数组得到目录中的文件名,将其打印出来

ls -l :
找到当前目录的inode,遍历inode中的blocks数组得到目录中的文件名 + inode 编号,由 inode 编号找到各个文件对应的inode,得到各个文件的属性信息,将其打印出来

cat 文件名 :
找到当前目录的inode,遍历inode中的blocks数组找到文件名和对应的inode编号,由文件的inode编号找到inode,遍历inode中的blocks数组将内容打印出来

理解软硬链接

建立软连接

ln -s test test-s

在这里插入图片描述

在这里插入图片描述

test-s 的 inode 编号和 test 的 inode 编号是不同的,因此软链接形成的文件本质是一个独立文件,文件内容保存的是test文件的路径(软链接相当于Windows中的快捷方式)

建立硬链接

ln test test-h

建立硬链接之后,test 的硬链接数由1变成2,test-h的硬链接数也是2,test-h的 inode 编号和 test 的 inode 编号是一样的,硬链接本质没有创建文件,只是建立了一个文件名和已有的 inode 的映射关系,并写入当前目录,相当于给文件取别名,删掉文件时,在目录中将对应的记录删除,硬链接数 - 1,硬链接为0时,则删掉磁盘上的文件(改变block bitmap inode bitmap)

在这里插入图片描述

一个目录的硬链接为2 是因为 目录名 和 . 两组目录名和目录inode有对应关系
在这里插入图片描述

在dir目录下再创建一个 newdir 目录,newdir 目录的硬链接数为2,dir 目录的硬链接数变为3
在这里插入图片描述

一个普通文件的硬链接为1 是因为只有一组文件名和文件inode有对应关系
在这里插入图片描述

因此硬链接的作用是方便目录之间通过相对路径方式进行跳转(也可通过硬链接数 -2 得出目录下有几个子目录)

最后解释一下文件的三个时间:

Access 最后访问时间
Modify 文件内容最后修改时间
Change 属性最后修改时间

stat myproc.c

在这里插入图片描述

三. 动态库和静态库

动静态库本质是可执行程序的半成品,像我们平常使用的printf,scanf…等函数,函数的具体实现就在C库里面,其实我们并不是想要这些函数形成可执行程序,而是基础模块供人使用

在之前的博客中详细讲过生成可执行程序的步骤(在此不再赘述) :
(1). 预处理 -> 生成后缀为 .i 的文件
(2). 编译 -> 生成后缀为 .s 的文件,生成汇编代码
(3). 汇编 -> 生成后缀为 .o 的文件,生成可重定向的二进制文件
(4). 链接 -> 生成可执行程序

举例 : test1.c test2.c test3.c main.c,这四个文件每个经过上面的4步最终形成 test1.o test2.o test3.o main.o,最后经过链接形成可执行程序,如果将来我们想要 test1.o test2.o test3.o 和 main2.o/main3.o/main4.o …等链接形成可执行程序的话,我们就可以将 test1.o test2.o test3.o 打包成一个库,作为基础模块来使用,这样我们就不需要多次编译 test1.c test2.c test3.c 了,节省了时间

所以所有的库本质是 : 一堆 .o 的集合,不包含main,但是包含了大量的方法

动静态库的相关知识 :
(1). 使用 ldd 命令可以查看可执行程序所依赖的动态库
(2). 在Linux中,以.so结尾的叫动态库,以.a结尾的叫静态库
(3). 在Windows中,以.dll结尾的叫动态库,以.lib结尾的叫静态库
(4). 库名字 : 去掉前缀lib,去掉后缀 .so ,.a 及版本号,剩下的就是库名字
(5). 程序是默认动态链接的,加上-static静态链接

动静态库各自的特点 :
静态库和动态库区别在于链接阶段如何处理库,链接成可执行程序,分别称为静态链接,动态链接

静态库 : 在链接阶段,将汇编生成的.o目标文件和库一起链接到可执行程序中,对应的链接方式为静态链接

静态链接缺点 :
1). 空间浪费大,因为将库和汇编生成的目标文件一起链接形成可执行程序,所以可执行程序占用的磁盘空间大,可执行程序运行起来后,将代码和数据加载到内存中,当多个程序运行起来时,会导致内存中存在大量的重复代码
2). 静态库对程序的更新,部署,发布造成麻烦,当静态库更新时,所有程序都需要重新编译

静态链接的优点:
静态链接将库和汇编生成的目标文件一起链接形成可执行程序,当程序运行的时候不再受静态库的影响,无论静态库存在与否,程序依然可以正常运行
在这里插入图片描述

为了解决静态链接的缺点,于是引入了动态链接

动态链接 : 当程序运行起来时,动态库被载入到内存,不同的程序使用相同的动态库,只需要去内存中的动态库里寻找相应的实现即可,这样动态库只需要在内存中存在一份,就可以解决静态链接空间浪费的问题,当动态库更新时,只需要重新编译动态库即可,不需要重新编译我们的程序,解决了静态链接不容易更新的问题

动态链接的缺点:
动态链接是在程序运行时,将动态库加载到内存中,如果动态库不存在,程序就无法正常运行
在这里插入图片描述

// mytest.c 内容

#include<stdio.h>
int main()
{
    
    
	printf("hello world\n");
	return 0;
}
// 动态链接
gcc -o mytest mytest.c
// mytest : 动态可执行程序
// 静态链接
gcc -o mytest mytest.c -static
// mytest : 静态可执行程序

在这里插入图片描述

在这里插入图片描述

制作动静态库

制作静态库

// Makefile

mylib=libcal.a
CC=gcc

$(mylib): add.o sub.o
        ar -rc $(mylib) $^
%.o:%.c
        $(CC) -c $<

.PHONY:clean
clean:
        rm -f $(mylib) *.o

.PHONY:output
output:
        mkdir -p mathlib/lib
        mkdir -p mathlib/include
        cp *.h mathlib/include
        cp *.a mathlib/lib

% 为通配符,表示当前目录下所有 .c 要形成 .o
$< : 将所有.c 一个一个的生成 .o

// add.h
#pragma once
#include<stdio.h>
extern int my_add(int x,int y);

// add.c
#include"add.h"
int my_add(int x,int y)
{
    
    
        return x + y;
}

// sub.h
#pragma once
#include<stdio.h>
extern int my_sub(int x,int y);

// sub.c
#include"sub.h"
int my_sub(int x,int y)
{
    
    
        return x - y;
}

make 一下我们的静态库 libcal.a 就做好了
在这里插入图片描述

静态库做好了,我们怎么样才能将静态库交给别人去使用呢 ?
给别人库的时候,本质上是给别人一份头文件(库的使用说明书) + 一份库文件(.a / .so,库的实现),使用如下命令进行编译就可以使用了

// test.c 内容

#include<stdio.h>
#include<add.h>
int main()
{
    
    
        int a = 10,b = 20;
        printf("%d\n",add(a,b));
        return 0;
}
gcc test.c -I mathlib/include/ -L mathlib/lib -lcal

-I : 指定搜索路径的选项(头文件在哪里)
-L : 显示的指定应该去哪个路径下找库的选项(库文件在哪里)
-l : 库名

在这里插入图片描述

如果不想这么复杂,可以把我们库和头文件的路径拷贝到系统的路径下,但仍然需要带上库名,拷贝的过程就是安装库的过程,安装软件实际上就是将库拷贝到系统路径下,第三方库使用的时候一般都要指明库的名称

cp mathlib/include/* /usr/include/
cp mathlib/lib/libcal.a /lib64/

拷贝之后执行如下命令就可完成编译,不需要再写 -I -L,因为编译器会去系统默认的路径下去找

gcc test.c -lcal

制作动态库

// Makefile

libcal.so:add.o sub.o
        gcc -o $@ $^ -shared

%.o:%.c
        gcc -c $< -fPIC

.PHONY:clean
clean:
        rm -f *.so *.o

.PHONY:output
output:
        mkdir -p mathlib/include
        mkdir -p mathlib/lib
        cp *.h mathlib/include
        cp *.so mathlib/lib

add.h add.c sub.h sub.c test.c 的内容和制作静态库的代码时是一样的

使用这段命令(和静态库一样)

gcc test.c -I mathlib/include/ -L mathlib/lib -lcal

运行起 ./test,会发现系统找不到动态库,但我们不是使用 gcc test.c -I mathlib/include/ -L mathlib/lib -lcal 指定了头文件和库的路径吗? 实际上这只是告诉编译器头文件和库的路径,操作系统却找不到

解决方案 :

(1). LD_LIBRARY_PATH : 程序运行时查找动态库所搜索的路径

默认LD_LIBRARY_PATH是什么都没有的,导出环境变量之后运行./test就可以正常使用了

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/luanyiping/test/10_24/dynamic/mathlib/lib

在这里插入图片描述

(2). ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新

echo /home/luanyiping/test/10_24/dynamic/mathlib/lib > bit.conf
cp bit.conf /etc/ld.so.conf.d/
ldconfig

(3). 拷贝.so文件到系统共享库路径下, 一般指/usr/lib

使用外部库

一.

C++ 后缀 :

  1. .cc
  2. .cpp
  3. .cxx

C++的库
在这里插入图片描述

二.

在安装gcc/g++这样的库时,也把C语言/C++的头文件和库文件下载下来了,然后放在能够让编译器找到的路径下

三.

在Windows中,下载vs时,除了下载编译器本身,实际上也装了C/C++库文件,当我们想使用各种各样的组件,实际上就是把对应的库和头文件拷贝下来安装到vs目录下,使用的时候编译器就能够找到

在这里插入图片描述

// include文件夹
在这里插入图片描述

// lib文件夹

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/DR5200/article/details/120709441