【Linux06-基础IO】4.5万字的基础IO讲解

news2024/11/17 5:29:34

前言

本期分享基础IO的知识,主要有:

  • 复习C语言文件操作
  • 文件相关的系统调用
  • 文件描述符fd
  • 理解Linux下一切皆文件
  • 缓冲区
  • 文件系统
  • 软硬链接
  • 动静态库的理解和制作
  • 动静态编译

博主水平有限,不足之处望请斧正!


C语言文件操作

#再谈文件

先回忆文件相关知识,结合已学操作系统知识,来构建共识:

  • 文件 = 内容+属性
    • 空文件也占空间(属性要占)
    • 文件操作 = 对属性/内容的操作
  • 标定一个文件,必须用 文件路径 + 文件名 的方式(具有唯一性)
    • 没有指明文件路径时,默认在当前路径(进程的cwd)进行文件访问
  • 操作文件的一定是进程 (程序只有载入内存运行,才会执行文件操作)
  • 文件只有被“打开”后才能访问
    • “打开”:用户进程调用接口 ==> 操作系统“打开”文件
    • 所以文件可分为:
      • 被打开的文件
      • 未被打开的文件

重点

  • 操作文件的一定是进程(程序只有载入内存运行,文件操作才会被执行)
  • 文件只有被“打开”(调用系统接口,系统来打开文件)之后才能操作

有了这些共识,我们可以得出结论:

文件操作 = 进程 操作 被打开文件(的内容/属性)


C语言文件接口使用

文件接口使用

写:

#include <stdio.h>

#define FILE_NAME "log.txt"

int main()
{
    //opt: r  读(不存在出错) / w写(不存在创建,存在则自动清空数据) 
  	//		 r+ 读写(不存在创建) / w+读写(不存在出错)
  	//	   a  追加 / a+
    FILE* pf = fopen(FILE_NAME, "w");
    if(NULL == pf)
    {
        perror("fopen");
        return 1;
    }

    int cnt = 5;
    while(cnt)
    {
        fprintf(pf, "%s: %d\n", "Hello Bacon!", cnt--);
    }

    fclose(pf);

    return 0;
}
[bacon@VM-12-5-centos 2]$ make
gcc -o test test.c -std=c99
[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ ls
log.txt  makefile  test  test.c
[bacon@VM-12-5-centos 2]$ cat log.txt 
Hello Bacon!: 5
Hello Bacon!: 4
Hello Bacon!: 3
Hello Bacon!: 2
Hello Bacon!: 1

读:

#include <stdio.h>

#define FILE_NAME "log.txt"

int main()
{
    FILE* pf = fopen(FILE_NAME, "r");
    if(NULL == pf)
    {
        perror("fopen");
        return 1;
    }

    char buf[64];
    //fgets默认添加\0,因为C语言字符串以\0结尾,而fgets获取的是字符串
    //为什么要-1?以防极端情况fgets想添加\0添不上
    while(fgets(buf, sizeof(buf) - 1, pf) != NULL)
    {
        printf("%s", buf); //log.txt内已经带了\n
    }
   
    fclose(pf);

    return 0;
}
Hello Bacon!: 5
Hello Bacon!: 4
Hello Bacon!: 3
Hello Bacon!: 2
Hello Bacon!: 1

用都会用,但感觉差口气儿。


理解文件接口

首先提出一个问题:C语言有文件接口,其他语言有吗?

一定有的。

为什么?因为文件在磁盘,磁盘是硬件,只有操作系统能访问硬件。

那我们又能知道,语言层面的文件接口,都必须调用系统接口。

为什么?因为操作系统对下管理硬件,对上通过提供系统调用的方式给用户良好的运行环境,任何人想访问硬件,都得过操作系统这关,它提供系统调用,我们就能访问;不提供,就不能。语言想进行文件操作,也只能通过系统调用。

  • 任何人想访问硬件只能通过系统调用
  • 学习文件接口,要学习不变的系统调用

文件相关的系统调用

open

NAME
       open, creat - open and possibly create a file or device

SYNOPSIS
       #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);
RETURN VALUE
       open()  and  creat()  return  the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).

先看第二个open:

  • 作用:打开,或可能创建一个文件或设备
  • 参数
    • pathname:要打开的文件路径
    • flags:标记位(通过比特位传递选项)
    • mode:权限
  • 返回值:成功返回文件描述符file descriptor,失败返回-1

为什么有两个open

前者是不需要创建文件时用的,不需要给默认权限;后者是需要创建文件时用的,需要给默认权限mode

#通过比特位传递选项
#define ONE   (1 << 0)
#define TWO   (1 << 1)
#define THREE (1 << 2)
#define FOUR  (1 << 3)

void bitOption(int flags)
{
    if(flags & ONE)   printf("ONE\n");
    if(flags & TWO)   printf("TWO\n");
    if(flags & THREE) printf("THREE\n");
    if(flags & FOUR)  printf("FOUR\n");
}

int main()
{
		bitOption(ONE | TWO | FOUR);
  
    return 0;
}

对于openflags,有几个宏:

  • O_RDONLY 只读
  • O_WRONLY 只写
  • O_RDWR 读或写
  • O_CREAT 文件不存在则创建文件
  • O_TRUNC 文件存在则清空文件内容
  • O_APPEND 追加

close

NAME
       close - close a file descriptor

SYNOPSIS
       #include <unistd.h>

       int close(int fd);
  • 作用:关掉文件描述符对应的文件
  • 参数
    • fd
  • 返回值:成功返回0,失败返回-1

打开并关闭文件:

int main()
{
    int fd = open(FILE_NAME, O_WRONLY);
    if(fd == -1 )
    {
        perror("open");
        return 1;
    }
  
    close(fd);
		return 0;
}
[bacon@VM-12-5-centos 2]$ ./test 
open: No such file or directory

怎么找不到呢?以写方式打开,默认不是会创建吗?

:我们不能把库函数那套照搬到系统调用上,“自动创建”是C库函数自己做的事。

int fd = open(FILE_NAME, O_WRONLY | O_CREAT);

给上了O_CREATE,我们现在要创建文件,却没给默认权限会怎么样?

[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ ll
total 20
-rw--ws--T 1 bacon bacon    0 Jan 20 19:28 log.txt
-rw-rw-r-- 1 bacon bacon   68 Jan 20 10:48 makefile
-rwxrwxr-x 1 bacon bacon 8456 Jan 20 19:25 test
-rw-rw-r-- 1 bacon bacon 1354 Jan 20 19:25 test.c

根据权限也能知道,log.txt现在完全就是乱的。

正确用法:

int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ ll
total 20
-rw-rw-r-- 1 bacon bacon    0 Jan 20 19:33 log.txt
-rw-rw-r-- 1 bacon bacon   68 Jan 20 10:48 makefile
-rwxrwxr-x 1 bacon bacon 8456 Jan 20 19:33 test
-rw-rw-r-- 1 bacon bacon 1360 Jan 20 19:33 test.c

log.txt经过umask,得到正确的权限。若我们不想要默认的umask,可以自己设置

NAME
       umask - set file mode creation mask

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>

       mode_t umask(mode_t mask);
...
umask(0);
...
[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ ll
total 20
-rw-rw-rw- 1 bacon bacon    0 Jan 20 19:40 log.txt
-rw-rw-r-- 1 bacon bacon   68 Jan 20 10:48 makefile
-rwxrwxr-x 1 bacon bacon 8512 Jan 20 19:39 test
-rw-rw-r-- 1 bacon bacon 1374 Jan 20 19:39 test.c

write

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);
  • 作用:向文件描述符写入
  • 参数
    • fd
    • buf 要被写入的数据
    • count 要写入的数据的字节个数
  • 返回值:返回写入的字节个数

buf为什么是void*数据类型是语言的事,操作系统眼里都是二进制!

向文件写入(不存在则创建,存在则清空):

int main()
{
    umask(0);
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd == -1)
    {
        perror("open");
        return 1;
    }
    
    int cnt = 5;
    char outBuf[64];
    while(cnt)
    {
        sprintf(outBuf, "%s %d\n", "Hello Bacon", cnt--);

        //第三个参数要不要+1把\0也写入文件?不用:\0结尾是C语言的事,和文件无关!
        write(fd, outBuf, strlen(outBuf)); 
    }

    close(fd);
		return 0;
}
[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ ll
total 24
-rw-rw-rw- 1 bacon bacon   70 Jan 20 21:32 log.txt
-rw-rw-r-- 1 bacon bacon   68 Jan 20 10:48 makefile
-rwxrwxr-x 1 bacon bacon 8664 Jan 20 21:32 test
-rw-rw-r-- 1 bacon bacon 1678 Jan 20 21:32 test.c
[bacon@VM-12-5-centos 2]$ cat log.txt 
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1

open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666); = fopen(FILE_NAME, "w");

追加:

int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ ./test 
[bacon@VM-12-5-centos 2]$ cat log.txt 
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1

read

NAME
       read - read from a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t read(int fd, void *buf, size_t count);
  • 作用:从文件描述符对应的文件读
  • 参数
    • fd
    • buf 被读入数据存放的地方
    • count 要读入数据的字节个数
  • 返回值:返回成功读入的字节个数,返回0代表读到了文件结尾

从文件读:

int main()
{
    umask(0);
    int fd = open(FILE_NAME, O_RDONLY);
    if(fd == -1 )
    {
        perror("open");
        return 1;
    }
    
    char buf[1024];
b  
    ssize_t read_ret = read(fd, buf, sizeof(buf) - 1);
    if(read_ret > 0) buf[read_ret] = 0; //我们读入字符串后,在结尾加上\0
    printf("%s\n", buf);

    close(fd);
		return 0;
}
[bacon@VM-12-5-centos 2]$ ./test 
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1
Hello Bacon 5
Hello Bacon 4
Hello Bacon 3
Hello Bacon 2
Hello Bacon 1


#文件描述符fd

我们说“文件操作 = 进程 操作 被打开文件”,那就有个问题,当打开很多文件的时候,操作系统如何管理这些被打开的文件?一定是四步走:抽象、具象、组织、操作。我们说的文件操作,其实就是这四步中的“操作”。但其他我们还不清楚, 现在来谈谈文件的抽象、具象和组织。

文件的抽象

struct file{
		//文件的大部分属性
}

文件的具象和组织

先来看一个现象:

#define FILE_NAME(num) "log.txt"#num

int main()
{
    int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT, 0666);
    int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT, 0666);
    int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT, 0666);
    int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT, 0666);

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

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

    return 0;
}
[bacon@VM-12-5-centos 2]$ ./mytest 
fd1 = 3
fd2 = 4
fd3 = 5
fd4 = 6
从3开始?

为什么fd是从3开始, 既然是连续的整数,“0、1、2”哪去了?

我们研究fd,不妨也研究一下它的一种封装:

调用C语言的文件接口时必须用FILE* pf,调用系统调用时必须用int fd。而且前者底层脱不开后者,那这库函数想玩儿,必须用到fdFILE内一定有一个字段是fd

很巧的是,默认打开的三个标准输入输出流恰好是FILE*类型。

NAME
       stdin, stdout, stderr - standard I/O streams

SYNOPSIS
       #include <stdio.h>

       extern FILE *stdin;
       extern FILE *stdout;
       extern FILE *stderr;

其中会不会有什么联系?

#define FILE_NAME(num) "log.txt"#num

int main()
{
    printf("fd of stdin  = %d\n", stdin->_fileno); //获取FILE结构体内类似fd的字段
    printf("fd of stdout = %d\n", stdout->_fileno);//获取FILE结构体内类似fd的字段
    printf("fd of stderr = %d\n", stderr->_fileno);//获取FILE结构体内类似fd的字段
    int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT, 0666);
    int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT, 0666);
    int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT, 0666);
    int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT, 0666);

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

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

    return 0;
}
fd of stdin  = 0
fd of stdout = 1
fd of stderr = 2
fd1 = 3
fd2 = 4
fd3 = 5
fd4 = 6

果然!“0、1、2”分别对应默认打开的三个标准输入输出流stdin、stdout、stderr


连续整数?

我们也发现,被打开文件的描述符是连续的整数,这不禁让人想到数组的下标啊。

文件的具象和组织具体如图所示:

在这里插入图片描述

fd的本质就是数组的下标,是fd_array的下标。

将以上内容串联下,就能知道OS是怎么管理文件的了。


文件的管理

  • 抽象、具象:struct file(有文件的大部分属性)

  • 组织:task_struct对象内有struct files_struct* files指向struct files_struct对象

    其中又有**struct file* fd_array[],每一个元素都是一个FILE指针,它构建了进程(task_struct)和被打开文件(struct file)的映射关系,它的下标被称作文件描述符fd**

  • 操作:openclosewriteread


fd的分配规则

实践出真知。

int main()
{
    close(0);

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

    close(fd);

    return 0;
}
[bacon@VM-12-5-centos 3]$ ./mytest 
fd = 0
int main()
{
    close(2);

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

    close(fd);

    return 0;
}
[bacon@VM-12-5-centos 3]$ ./mytest 
fd = 2

分配规则:从小到大分配最小的未被占用fd

再关1试试:

int main()
{
    close(1);

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

    close(fd);

    return 0;
}
[bacon@VM-12-5-centos 3]$ ./mytest 
[bacon@VM-12-5-centos 3]$ 

关掉fd为1的文件就不打印结果了,为什么?

printf本质其实就是写入到fd为1的stdout内,然后读取stdout,输出到显示器上。需要注意,

系统默认stdoutfd为1。

也就是,即使我们把fd为1的stdout关掉,系统仍然会从fd为1的文件中读取并输出到显示器。

按这么说,关掉fd为1的stdout,我再打开log.txt,会从小到大找最小的未被占用fd,即 1,那我的log.txt岂不是成了系统眼里的stdout

在这里插入图片描述

那也代表printf会向我的log.txt写入?

[bacon@VM-12-5-centos 3]$ ./mytest 
[bacon@VM-12-5-centos 3]$ cat log.txt1
[bacon@VM-12-5-centos 3]$ 

文件里没有啊?其实这跟缓冲区有关,我们刷新一下就可以看到。

int main()
{
    close(1);

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

    fflush(stdout);

    close(fd);

    return 0;
}
[bacon@VM-12-5-centos 3]$ ./mytest 
[bacon@VM-12-5-centos 3]$ cat log.txt
fd = 1

本来是输出到stdout(向stdout写入),一通操作后,输出到了log.txt(向log.txt写入),这就是重定向。


#重定向

是什么

改变向文件输入/输出的“方向”。

本质就是,修改struct file* fd_array中的元素(即更改某fd对应的FILE*指针)。

在这里插入图片描述

*1这个fd对应的指向原本是stdout,经过closeopen,最终指向了log.txt1


分类

  • > 输出重定向
  • >> 追加重定 向
  • < 输入重定向

但是close + open有点挫

有没有别的办法进行重定向?有的


怎么做

可以通过一个系统调用dup2

NAME
       dup, dup2, dup3 - duplicate a file descriptor

SYNOPSIS
       #include <unistd.h>

       int dup(int oldfd);
       int dup2(int oldfd, int newfd);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <fcntl.h>              /* Obtain O_* constant definitions */
       #include <unistd.h>

       int dup3(int oldfd, int newfd, int flags);

其中,dup2是我们最常用的,

       dup2() makes newfd be the copy of oldfd, closing newfd first if necessary
  • 作用:让newfd对应元素变成oldfd对应元素的一份拷贝(fd_arrays[newfd] = fd_arrays[oldfd]
  • 返回值:成功返回newfd,失败返回-1
#define FILE_NAME "log.txt"

int main()
{
    //原本输出(写入)到stdout,现在要输出(写入)到我自己的log.txt
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);

    int dup2_ret = dup2(fd, 1);
  	assert(dup2_ret != -1);

    printf("fd = %d\n", fd);

    fflush(stdout);

    close(fd);

    return 0;
}
[bacon@VM-12-5-centos 3]$ ./mytest 
[bacon@VM-12-5-centos 3]$ ls
log.txt  makefile  mytest  mytest.c
[bacon@VM-12-5-centos 3]$ cat log.txt 
fd = 3

在这里插入图片描述

fd_array[1] = fd_array[fd]

以上是输出重定向,其他两个重定向也很简单。

追加重定向:

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);

    dup2(fd, 1);

    printf("fd = %d\n", fd);

    fflush(stdout);

    close(fd);

    return 0;
}
[bacon@VM-12-5-centos 3]$ cat log.txt 
fd = 3
[bacon@VM-12-5-centos 3]$ ./mytest 
[bacon@VM-12-5-centos 3]$ ./mytest 
[bacon@VM-12-5-centos 3]$ ./mytest 
[bacon@VM-12-5-centos 3]$ cat log.txt 
fd = 3
fd = 3
fd = 3
fd = 3

输入重定向:

int main()
{
    //输入重定向:原本从stdin输入(读),现在从我自己的log.txt输入(读)
    int fd = open(FILE_NAME, O_RDONLY, 0666);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }
    
    dup2(fd, 0);

    char line[64];
    while(1)
    {
        printf("> ");
        if(fgets(line, sizeof(line), stdin) == NULL) break;
        printf("%s", line);
    }

    close(fd);

    return 0;
}   
[bacon@VM-12-5-centos 3]$ cat log.txt 
hello world!
I'm Bacon!
[bacon@VM-12-5-centos 3]$ ./mytest 
> hello world!
> I'm Bacon!
> [bacon@VM-12-5-centos 3]$ 

#把重定向加入shell

shell的原理是程序替换,所以重定向的工作一定是子进程来做,而父进程只需要规定子进程如何重定向。

实现一个redirCheck函数,获取命令中的重定向信息,交给子进程来真正重定向。

redirCheck实现思路:

  • 全局变量redirType 保存重定向的类型
  • 全局变量redirFile保存要重定向至的文件名
  • 父进程调用redirCheck函数来获取前两者(子进程也能共享到) ,并处理输入的命令
    • 处理命令:将重定向标识置空
      • arg交给shell处理
      • 后面的文件名保存,不需要交给shell(重定向标识置空后,不会读取到文件名了)
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#include <errno.h>

#define NUM 1024
#define OPT_NUM 32

#define NONE_REDIR   0
#define INPUT_REDIR  1
#define APPEND_REDIR 2
#define OUTPUT_REDIR 3

#define trimSpace(start) do{\
    while(isspace(*start)) ++start; \
}while(0)

char  lineCommand[NUM];
char* myargv[OPT_NUM];
int   lastCode = 0;
int   lastSig = 0;


//重定向信息
int redirType = NONE_REDIR;
char* redirFile = NULL;

//重定向检查:
//1. 获取重定向信息:重定向类型和重定向的文件
//2. 处理命令,将arg和文件名分离,arg需要给shell用,文件名保存即可
//"ls -a -l > myfile" ==> "ls -a -l \0 myfile"
void redirCheck(char* commands)
{
   assert(commands); 

   char* start = commands;
   char* end = commands + strlen(commands);
   while(start < end)
   {
       if(*start == '>') //确定重定向类型
       {
           *start = '\0'; //切割arg和文件
           ++start;
           if(*start == '>') //追加重定向
           {
                redirType = APPEND_REDIR;
                ++start; //跳过第二个'>'
           }
           else //输出重定向
           {
               redirType = OUTPUT_REDIR; //获取重定向类型
           }
           trimSpace(start); //宏函数过滤空格
           redirFile = start; //获取重定向文件
           break;
       }
       else if(*start == '<')
       {
           *start = '\0';
           ++start;
           trimSpace(start);
           redirType = INPUT_REDIR;
           redirFile = start;
           break;
       }
       else 
       {
           ++start;
       }
   }
}


int main()
{
    while(1)
    {
        redirType = NONE_REDIR;
        redirFile = NULL;

        printf("[%s@%s]# ", getenv("USER"), getenv("HOSTNAME"));
        fflush(stdout);
        
        char* ret = fgets(lineCommand, sizeof(lineCommand) - 1, stdin); 
        assert(ret != NULL); 
        lineCommand[strlen(lineCommand) - 1] = 0;
    
        redirCheck(lineCommand);

        myargv[0] = strtok(lineCommand, " ");

        int i = 1;
        while(myargv[i++] = strtok(NULL, " "));
    
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }

        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("exitCode = %d  |  exitSig = %d\n", lastCode, lastSig);
            }
            else 
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }

        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {
            //重定向
            switch(redirType)
            {
                case NONE_REDIR:
                    break;
                case INPUT_REDIR:
                {
                    int fd = open(redirFile, O_RDONLY);
                    if(fd < 0)
                    {
                        perror("open redirFile");
                        exit(errno);
                    }
                    dup2(fd, 0);
                }
                break;
                case OUTPUT_REDIR:
                case APPEND_REDIR: 
                {
                    int flags = O_WRONLY | O_CREAT; 
                    if(redirType == APPEND_REDIR) flags |= O_APPEND;
                    else flags |= O_TRUNC;
                    int fd = open(redirFile, flags, 0666);
                    if(fd < 0)
                    {
                        perror("open redirFile");
                        exit(errno);
                    }
                    dup2(fd, 1);
                }
                break;
                default:
                    printf("something wrong...\n");
                    break;
            }

            execvp(myargv[0], myargv);
            exit(1);
        }

        int status = 0;
        pid_t wait_ret = waitpid(id, &status, 0);
        assert(wait_ret > 0);

        lastCode = (status >> 8) & 0xFF;
        lastSig = status & 0x7F;
    }
    return 0;
}
[bacon@VM-12-5-centos myshell]$ ./myshell 
[bacon@VM-12-5-centos]# ls -a > log.txt
[bacon@VM-12-5-centos]# ls
log.txt  makefile  myshell  myshell.c
[bacon@VM-12-5-centos]# cat log.txt
.
..
log.txt
makefile
myshell
myshell.c
[bacon@VM-12-5-centos]# ls -a -l >> log.txt
[bacon@VM-12-5-centos]# cat log.txt
.
..
log.txt
makefile
myshell
myshell.c
total 40
drwxrwxr-x 2 bacon bacon  4096 Jan 26 20:29 .
drwxrwxr-x 6 bacon bacon  4096 Jan 26 17:49 ..
-rw-rw-r-- 1 bacon bacon    40 Jan 26 20:29 log.txt
-rw-rw-r-- 1 bacon bacon   115 Jan 19 10:26 makefile
-rwxrwxr-x 1 bacon bacon 13824 Jan 26 20:29 myshell
-rw-rw-r-- 1 bacon bacon  4650 Jan 26 20:29 myshell.c
[bacon@VM-12-5-centos]# cat < log.txt
.
..
log.txt
makefile
myshell
myshell.c
total 40
drwxrwxr-x 2 bacon bacon  4096 Jan 26 20:29 .
drwxrwxr-x 6 bacon bacon  4096 Jan 26 17:49 ..
-rw-rw-r-- 1 bacon bacon    40 Jan 26 20:29 log.txt
-rw-rw-r-- 1 bacon bacon   115 Jan 19 10:26 makefile
-rwxrwxr-x 1 bacon bacon 13824 Jan 26 20:29 myshell
-rw-rw-r-- 1 bacon bacon  4650 Jan 26 20:29 myshell.c

子进程重定向会不会影响父进程的struct files_struct

不影响,struct files_struct是进程相关的数据结构,而进程间具有独立性。

需要注意的是,创建子进程时并不会为子进程把所有已打开文件再拷贝一份,因为进程管理是进程管理,文件管理是文件管理。

子进程执行程序替换会不会影响因重定向打开的文件?

不影响,程序替换只是用磁盘上的代码和数据替换进程原来的代码和数据, 跟这些内核数据结构没有任何关系。

在这里插入图片描述

#引用计数

肯定存在多个进程同时打开一个文件的情况,这时如果某个进程退出了,对文件有什么影响?

这就要提到引用计数。对于某个文件file,其引用计数ref_count表示的是有多少地方指向自己,也就是引用自己。

  1. 3个进程引用自己,ref_count= 3,文件不关闭

    此时其中的一个进程退出…

  2. 2个进程引用自己,ref_count = 2,文件不关闭

    再有一个退出…

  3. ref_count = 1 ,文件不关闭

    再有一个退出…

  4. ref_count = 0,文件关闭。

比如某个进程close,OS只是会--ref_count

从中我们可以体会多个进程打开文件是怎么个样子了。


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

拿硬件举例:

在这里插入图片描述

  • 我们通过冯诺依曼体系结构可以知道,想要操作键盘磁盘这类外设的数据,必须读取到内存,处理完后再写入回去,这中操作也叫IO
  • 操作系统要管理硬件,就需要抽象、具象、组织、操作
    • 抽象方面,各个硬件分别有自己的抽象
    • 操作方面,各个硬件为了能操作硬件的数据,都要提供读写方法(若某个设备不需要读/写方法,可以读/写方法内可以什么都不做)
    • *这些硬件的读写方法,其实就在其驱动程序里
  • 对于这些硬件,操作系统怎么看?
  • 全部看做文件,属性都按需抽象出来!管你来的是哪个硬件,属性该填的填,至于你们的读写,你自己提供,我只需要用一个指针找到你提供的,用现成的

最后一步,全部看做文件,也就是图中struct file这一层,就叫做**VFS(virtual file system)**,虚拟文件系统。很像是一种封装:虚拟文件系统让我们不需要关心底层硬件管理的各种不同细节,也能让上层的软件,能够用单一的方式,来跟底层不同的硬件沟通。

为什么说Linux下,一切皆是文件?因为有VFS来“统一度量衡”。

我们可以看看源码的细节,来验证我们上面的说法。

struct file 内是有指针指向文件操作方法的

struct file {
	...
	const struct file_operations	*f_op;
	...
}
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, struct dentry *, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
};

其他我们没学过,

	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

这两个我们是知道的。


#缓冲区

先来看一个现象。

int main()
{
    //C
    printf("I'm printf\n");
    fprintf(stdout, "I'm fprintf\n");
    fputs("I'm fputs\n", stdout);

    //system call
    const char* msg = "I'm write\n";
    write(1, msg, strlen(msg));

    return 0;
}
[bacon@VM-12-5-centos 5-buffer]$ ./test 
I'm printf
I'm fprintf
I'm fputs
I'm write

这都没问题,但是……

int main()
{
    //C
    printf("I'm printf\n");
    fprintf(stdout, "I'm fprintf\n");
    fputs("I'm fputs\n", stdout);

    //system call
    const char* msg = "I'm write\n";
    write(1, msg, strlen(msg));

    fork();

    return 0;
}
[bacon@VM-12-5-centos 5-buffer]$ ./test 
I'm printf
I'm fprintf
I'm fputs
I'm write
[bacon@VM-12-5-centos 5-buffer]$ ./test  > log.txt
[bacon@VM-12-5-centos 5-buffer]$ cat log.txt 
I'm write
I'm printf
I'm fprintf
I'm fputs
I'm printf
I'm fprintf
I'm fputs

fork后打印是正常的,但是重定向怎么不对劲?

我们得先了解缓冲区。

是什么

一段内存。


为什么

我们可以通过寄特产的例子来理解:

广东人张三和东北人李四是网友,一天张三想寄点特产给李四,两种方案:

  1. 自己骑自己的单车,自己送,花上几个月
  2. 放到楼下顺丰快递点,顺丰送,只需要下个楼的时间

自然是后者好,因为前者太浪费自己的时间。快递行业的意义其实就在其中:节省发送者的时间。

把寄特产换成写文件:

  • 张三就是进程,要把数据从内存输出到磁盘
  • 李四就是文件,在磁盘中等待被写入

张三要寄特产,即进程要输出到磁盘时

  • 进程自己访问,就相当于自己骑单车从广东到东北
  • 拷贝到缓冲区让缓冲区定期刷新,就相当于找顺丰帮忙寄

那么为什么要有缓冲区也明了了:节省进程访问外设的时间。


不对啊,拷贝到缓冲区?我可没干这事啊!

与其说我们用的IO接口,如fwrite是写入到文件的函数,不如说它们是拷贝函数——从进程拷贝到“缓冲区”或外设中。


还有个问题,我把特产给顺丰,他什么时候给我送呢?

我把数据拷贝到缓冲区,它什么时候给我刷新呢?


缓冲区的刷新策略

缓冲区是为了节省进程时间,那我们看,一坨数据,怎么刷新到外设最能体现缓冲区的意义?

少量多次 or 多量少次?

自然是后者。就是访问外外设这个操作慢,最大程度减少外设访问,即多量少次地刷新,才最节省时间。

但也不能无脑减少外设访问,比如显示器:我们习惯按行看,它等半天给你突然刷一整屏,不合理。

所以,虽然最大程度减少外设访问才最节省时间,但不同场景也需要不同的刷新策略:

  • 无缓冲:立即刷新
  • 行缓冲:按行刷新(如显示器)
  • 全缓冲:缓冲区慢刷新(如磁盘文件)
  • *用户强制刷新(如调用fflush
  • *进程退出

说这么多,对缓冲区有一定认识了,但很重要的一个,位置,还不知道。


缓冲区的位置

[bacon@VM-12-5-centos 5-buffer]$ ./test 
I'm printf
I'm fprintf
I'm fputs
I'm write
[bacon@VM-12-5-centos 5-buffer]$ ./test  > log.txt
[bacon@VM-12-5-centos 5-buffer]$ cat log.txt 
I'm write
I'm printf
I'm fprintf
I'm fputs
I'm printf
I'm fprintf
I'm fputs

可以告诉大家,这种现象,就是和缓冲区有关。但我们现有知识分析不出个所以然,能确定的是:缓冲区一定不在内核(否则write也会打印两次)。

所以,我们先前讨论的缓冲区,都是用户级语言层面的缓冲区(如stdinstdoutstderr内就有)

stdinstdoutstderr都是FILE类型,那也说明,语言层面的缓冲区就在FILE内

回想一下,我们fflush手动刷新缓冲区,是不是要传递FILE指针?我们关闭文件(文件关闭后会刷新缓冲区),是不是要传递FILE指针?是的,这里也对应上了。

[bacon@VM-12-5-centos 6-basic_IO]$ vim /usr/include/stdio.h

在这里插入图片描述

[bacon@VM-12-5-centos 6-basic_IO]$ vim /usr/include/libio.h

在这里插入图片描述

*这一堆指针,维护的就是语言层面的缓冲区


有了语言层面缓冲区的理解,就解释“fork后打印是正常的,但是重定向不对劲?”这个现象了。

int main()
{
    //C
    printf("I'm printf\n");
    fprintf(stdout, "I'm fprintf\n");
    fputs("I'm fputs\n", stdout);

    //system call
    const char* msg = "I'm write\n";
    write(1, msg, strlen(msg));

    fork();

    return 0;
}
[bacon@VM-12-5-centos 5-buffer]$ ./test 
I'm printf
I'm fprintf
I'm fputs
I'm write
[bacon@VM-12-5-centos 5-buffer]$ ./test  > log.txt
[bacon@VM-12-5-centos 5-buffer]$ cat log.txt 
I'm write
I'm printf
I'm fprintf
I'm fputs
I'm printf
I'm fprintf
I'm fputs

缓冲区在被打开的文件中

  1. 没有重定向看到4条信息:

    1. stdout写入,刷新策略:行缓冲
    2. C接口中,\n使得缓冲区刷新
    3. fork前刷新,数据不在缓冲区内
    4. fork创建子进程,stdout被父子进程共享
    5. 而后整个程序跑完,也就是fork后紧跟着的就是父/子进程退出,需要进行缓冲区刷新(即修改)
    6. 本来要对缓冲区修改,但缓冲区没有数据,不会进行修改的操作,不会发生写实拷贝
    7. 平平淡淡打印4条信息
  2. 重定向了看到7条信息:

    1. 普通文件写入,刷新策略:全缓冲
    2. C接口中,\n无法使得缓冲区刷新,数据量也达不到全缓冲的条件
    3. 没有刷新,数据还在缓冲区内
    4. fork创建子进程,log.txt被父子进程共享
    5. 而后整个程序跑完,也就是fork后紧跟着的就是父/子进程退出,需要进行缓冲区刷新(即修改)
    6. 对共享的被打开文件的缓冲区修改 = 写时拷贝!
    7. 这也代表,父子进程虽然共享被打开的文件,但是被打开文件的缓冲区已经又拷贝了一份,父子进程各有一份缓冲区,退出时分别刷新
    8. 所以出人意料地,C接口在父子进程退出时分别打印了一次,总共6次,加上系统调用1次,最终打印了7条信息
  3. write“不问世事”?

    :write并不用FILE,而是用fd,也就是write不会用到语言层面提供的缓冲区

简单来说:

  • fork:创建了子进程,父子进程退出分别会刷新自己的缓冲区。而刷新本质是一种拷贝,也就是修改

  • 未重定向:要写入的文件是显示器文件,默认行刷新

    \n使stdout的数据立即刷新,父子进程共享的缓冲区,在退出时想刷新,也并不会发生写实拷贝,因为缓冲区内没数据

  • 重定向:要写入的文件是普通文件,默认全刷新

    \n不能使stdout的数据立即刷新,父子进程共享的缓冲区,在退出时想刷新,会发生写实拷贝,因为缓冲区内有数据

  • 只有C语言接口发生写实拷贝?

    • C:struct file stdout内申请的缓存区空间被父子进程共享且要被写入,缓冲区发生写时拷贝。父子进程退出时分别刷新自己的缓冲区,最终打印双份信息
    • 系统调用:通过fd来写入stdout,没有C提供的缓冲区

光说不练假把式,来自己搞一个行缓冲的缓冲区玩玩。

#自己封装行缓冲的缓冲区

makefile

main:main.c myclib.c
	gcc -o $@ $^ -std=c99

.PHONY:clean
clean:
	rm -f main
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE 1024
#define SYNC_NOW  (1 << 1)
#define SYNC_LINE (1 << 2)
#define SYNC_FULL (1 << 3)

typedef struct myFILE
{
    int flags; //标记位:刷新方式
    int fileno;
    int cap; //缓冲区容量
    int size; //缓冲区大小
    char buffer[SIZE];
}myFILE;


myFILE* myfopen(const char* pathName, const char* mode);
void myfwrite(const void* ptr, int num, myFILE* pf);
void myfclose(myFILE* pf);
void myfflush(myFILE* pf);
#include "myclib.h"

myFILE* myfopen(const char* pathName, const char* mode)
{
    //获取打开模式
    int openFlags = 0;
    int defalutMode = 0666;
    if(strcmp(mode, "r") == 0)
        openFlags |= O_RDONLY;
    else if(strcmp(mode, "w") == 0)
        openFlags |= (O_WRONLY | O_CREAT | O_TRUNC);
    else if(strcmp(mode, "a") == 0)
        openFlags |= (O_WRONLY | O_CREAT | O_APPEND);

    //根据是否需要创建文件,来调用不同的open
    int fd = 0;
    if(openFlags & O_RDONLY)
        fd = open(pathName, openFlags);
    else 
        fd = open(pathName, openFlags, defalutMode);

    if(fd < 0)
    {
        const char* errMsg = strerror(errno);
        write(2, errMsg, strlen(errMsg));
        return NULL;
    }

    //开辟并设置myFILE对象
    myFILE* pf = (myFILE*)malloc(sizeof(myFILE));
    assert(pf);

    pf->flags = SYNC_LINE; //默认行刷新
    pf->fileno = fd;
    pf->cap = SIZE;
    pf->size = 0;
    memset(pf->buffer, 0, SIZE);

    return pf;
}

void myfwrite(const void* ptr, int num, myFILE* pf)
{
    //1. 写入缓冲区
    memcpy(pf->buffer + pf->size, ptr, num); //不考虑缓冲区溢出
    pf->size += num;

    //2. 判断是否需要刷新
    if(pf->flags & SYNC_NOW)
    {
        write(pf->fileno, pf->buffer, pf->size);
        pf->size = 0;
    }
    else if(pf->flags & SYNC_FULL)
    {
        if(pf->size == pf->cap)
        {
            write(pf->fileno, pf->buffer, pf->size);
            pf->size = 0;
        }
    }
    else if(pf->flags & SYNC_LINE)
    {
        if(pf->buffer[pf->size - 1] == '\n')
        {
            write(pf->fileno, pf->buffer, pf->size);
            pf->size = 0;
        }
    }
}

void myfclose(myFILE* pf)
{
    myfflush(pf);
    close(pf->fileno);
}

void myfflush(myFILE* pf)
{
    if(pf->size > 0)
        write(pf->fileno, pf->buffer, pf->size);
    fsync(pf->fileno); //强制进行外设刷新,下文会提到

    pf->size = 0; //惰性删除(后来的数据会覆盖)
}
#include "myclib.h"

int main()
{
    myFILE* pf = myfopen("./log.txt", "w");
    assert(pf);

    int cnt = 5;
    const char* msg = "testing myclib...\n";
    while(1)
    {
        myfwrite(msg, strlen(msg), pf);
        sleep(1);
        --cnt;
        if(cnt == 0) break;
    }

    myfclose(pf);

    return 0;
}

在这里插入图片描述


#include "myclib.h"

int main()
{
    myFILE* pf = myfopen("./log.txt", "w");
    assert(pf);

    int cnt = 5;
    const char* msg = "testing myclib...";
    while(1)
    {
        myfwrite(msg, strlen(msg), pf);
        sleep(1);
        --cnt;
        if(cnt == 0) break;
    }

    myfclose(pf);

    return 0;
}

在这里插入图片描述


不对不对,刚刚说“系统调用:通过fd来写入stdout,没有C提供的缓冲区”,按这个说法,write没有缓冲区,直接写到磁盘上的文件?按理来说不可能,因为这样效率也太低了呀!是的,操作系统层面也有自己的缓冲区。

所以我们就能把缓冲区分两种。

分类

  • 语言层面的缓冲区(上面谈的一大堆都是关于语言层面的缓冲区)
  • 系统层面的内核缓冲区

系统调用fsync

NAME
       fsync, fdatasync - synchronize a file's in-core state with storage device

SYNOPSIS
       #include <unistd.h>

       int fsync(int fd);

可以将文件的内核状态同步到存储设备,也就是刷新内核缓冲区。我们自己封装的语言级缓冲区就用到了:

void myfflush(myFILE* pf)
{
    if(pf->size > 0)
        write(pf->fileno, pf->buffer, pf->size);
    fsync(pf->fileno); //强制进行外设刷新,下文会提到

    pf->size = 0; 
}

至于内核缓冲区的刷新策略,就不是我们能决定的了;位置我们也没有过多了解的必要。

学习了缓冲区,我们对IO操作的理解就更深刻了。


IO操作的流程

语言接口output操作:

  1. 信息写入struct file对象中的语言缓冲区
  2. 语言缓冲区按刷新策略刷新到内核缓冲区
  3. 内核缓冲区自主决定何时刷新到外设

系统调用output操作:

  1. 信息写入内核缓冲区
  2. 内核缓冲区自主决定何时刷新到外设

至此,通过对文件的深入理解,“基础IO”对我们来说真的就是“基础”了。现在看,学C语言时对文件的理解简直太浅显,有了现在的理解,还会怕IO操作玩不明白嘛!


先前我们谈论的都是被打开的文件,那没打开的文件呢?能肯定的是,它们肯定在磁盘上静静地躺着。

那磁盘上的这么老多文件都是怎么管理的呢?可以理解为“静态管理”,按规则将文件们放到各自的位置。这一整个管理的体系就叫文件系统。


文件系统

是什么

文件系统是一种在存储设备上管理文件的软件系统

  • 从系统角度来看,文件系统是对文件存储设备的空间进行组织和分配
  • 具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。

磁盘的物理结构

磁盘是计算机中唯一的机械结构,而且还是外部设备,所以它相对其他设备非常慢。

*虽然我们自己很少用很少见磁盘了,但因为性价比高,不会像ssd那样过多访问可能被击穿等等原因,很多企业仍主要用磁盘

在这里插入图片描述

在这里插入图片描述

磁头就像笔,盘面就像纸。运转时,盘片高速旋转,磁头悬在盘面上,不直接和盘面接触。这个场景就像一架波音747在离地1米的距离高速飞行,所以一粒灰尘都很可能超过“1米”,造成磕碰导致的盘片损毁。

在背面,还有硬件电路和伺服系统,可以控制磁头和盘片等,实现寻址等功能。

那到底磁盘到底是怎样存储数据的,怎样表示“0、1”的?

可以这样理解:盘面上有很多个小小磁铁,每个磁铁有N、S极,通过磁头充磁、消磁,可以对它们的南北极调转,从而达到“0、1”的效果。

充磁消磁可以通过加热的方式实现。磁盘内具体是通过电子流动来实现。

有兴趣的朋友可以看看磁盘是怎么运转的


磁盘的存储结构

了解存储结构,肯定要具体到某个盘面来讨论。

在这里插入图片描述

就是一圈圈同心圆组成的,“同心圆”叫磁道,分割出的一块块扇形区域就叫扇区

磁盘寻址的时候,基本单位就是扇区(一般512bytes)。一块一块的,这也是为什么有“块设备文件”。


#CHS寻址模式

那在一面上,是怎么定位一个扇区的?

首先要确定磁道(某圈),接着通过磁道找到对应的扇区(某圈上的某部分)。

那是如何定位某个磁道的?

磁头的来回摆动就能定位。

又是如何通过磁道找到对应扇区的?

盘片的旋转就能配合磁头找到对应扇区。在盘片一次旋转的时间(单位时间)内,通过磁头找到扇区并读取数据。

上面这一点也是磁盘的技术难点:提高转速,将单位时间缩短不难;难的是让磁头读取数据的能力跟上缩短的单位时间。

在这里插入图片描述

需要提一下柱面的概念:不同盘片上半径相同的所有磁道,立体地看就像圆柱,故称柱面。(其实就是磁道)

但是这个概念好像没有存在的必要吧?

并不是的,因为每面一个的磁头并不能各动各的,一个动全都一起动。**那么当磁头定位好、准备寻找扇区的时候,所有磁头就能一起出动,一找就是n个磁头一起找,效率MAX。**所以这个柱面的概念是有必要的。


聊到这,我们可以下结论了,在磁盘中到底如何定位一个扇区?

先移动磁头,找到对应磁道(柱面);再让盘片旋转,所有面的磁头一起找目标扇区

  • 柱面 = cylinder
  • 磁头 = head
  • 扇区 = sector

这就是CHS寻址模式。


磁盘的逻辑结构

讲磁盘的逻辑结构,可以先看看磁带。

在这里插入图片描述

磁带的存储介质是一种黑色的塑料材质,卷起来像挖掉中心的圆,这个圆又是很多同心圆组成。**把它们扯出来,拉直,其实是一条塑料。**这磁带像不像磁盘的盘片?那磁盘的结构也是和这磁带一样,“扯出来拉直是一条”?是的,

磁盘的逻辑结构是线性结构。

虽然磁盘物理上是圆形的,但我们可以把它想象成线性的。
在这里插入图片描述

上图就很好地表示了对磁盘的抽象,逻辑结构变成了一个数组,我们将磁盘看作一个sector_arr[],每一个元素是一个扇区。所以要找到一个扇区,只要知道这个扇区的下标。这样的下标也叫逻辑块地址(Logic Block Address)。说白了,这个数组的下标(逻辑块地址)标识了一个数据区块

逻辑是逻辑,你终究还得存到物理结构中,LBA地址如何对应上CHS寻址模式?

比如,现有一个磁盘

  • 盘面 = 4
  • 每面的磁道 = 10
  • 每个磁道上的扇区 = 100
  • 扇区大小 = 512bytes

LBA地址,sector_arr[123],对应到物理结构上是什么样?

这个磁盘大小是4 * 10 * 100 * 512 bytes,sector_arr的下标范围是4 * 10 * 100

也可得每一个盘面有1000个扇区。

  • 第几号盘面?123 / 1000 = 0——第0号盘面
  • 第几号磁道? 123 / 100 = 1——第1号磁道
  • 第几号扇区?123 % 100 = 23——第23号扇区

sector_arr[123] == 第0号盘面中第1号磁道的第23号扇区

如上,我们的逻辑结构就能和物理结构搭起来了。

而且,我们也能对磁盘来一套管理组合拳:抽象、具象、组织、操作。

管理也到位了。

但,为什么要通过逻辑结构抽象出LBA地址,直接CHS不行吗?

  1. 便于管理(数组肯定比一个三维结构好管理)
  2. 对OS的代码和硬件解耦(硬件的底层变了也不影响我OS的代码,只要你能转换我给你的LBA地址)

磁盘的单次读取

512bytes太小了,经过测试,性能不高,所以,

磁盘单次读取一般是4KB

不会有点浪费吗,万一我只访问1bit的数据呢?

这其实是一种以空间换时间的做法:

  1. 对于OS来说,一次读取4KB的性能好(提高IO的效率)

  2. 是一种数据预加载,提高cpu缓存命中率,效率更高

    *局部性原理:访问某一部分数据,它周围的数据很有可能也要被访问。

内存其实也是以为4KB为单位,每个单位称为页框

磁盘中的文件(尤其是可执行文件),都是被划分成4KB的块,这样的块称为页帧


分区和分组

即便如此,4KB对于动辄500GB的磁盘来说还是太小。

解决:先分区,再分组(本质是分治,管理好最小子问题,就能管理整个问题)。

在这里插入图片描述

那这也代表,对于这么大的磁盘,想讨论文件系统,只需要讨论这5GB中的构成就够了。

分区

在这里插入图片描述

分组

在这里插入图片描述

  • Super Block:保存的是整个分区的文件系统的信息
    • 它属于整个分区,所以对于每个分组来说,不是必须有的
    • 每个分组都保存一份可以理解成备份,能实现文件系统的恢复
  • Group Descriptor Table:保存分组的属性信息
  • Inode BitMap:保存分组内部inode的使用情况(对应位置的inode是否被使用)
  • Block BitMap:保存分组内部Block的使用情况(对应位置的Data Block是否被使用)
  • Inode Table:保存分组内可用(已使用+未使用)的inode
  • Data Blocks:保存分组内部所有文件的数据块(可动态变化)

#inode

是什么

索引节点,存储了文件的很多属性,在文件系统中非常重要。

之前我们提到过,文件 = 属性 + 内容,在Linux中二者是分开存储的

  • 文件的几乎所有属性保存在自己的大小不变的inode中(文件名并不存在inode)
  • 文件的内容保存在Data Blocks

也就是说,某一个文件的属性保存在Inode Table中的某一个Inode,内容则保存在Data Blocks中的n个数据块中


那我们看一个场景:在Linux下创建一个文件。

需要先找Inode Table中未使用的inode,再找Data Blocks中未使用的数据块。BitMap就是干这事的。

Inode BitMap通过某个比特位的0/1,来表示对应位置的Inode是否被使用,如

0      0      0      1      0      1      1      0
未使用  未使用  未使用  已使用  未使用  已使用  已使用  未使用

Block BitMap同理。


那整个分组有多少个Data Block,已经使用了多少,还有没少没被使用……这些问题又如何解决?

组描述表Group Descriptor Table内就保存了这些数据。

而文件需要区分彼此的inode,因此每个inode都有自己的id。

[bacon@VM-12-5-centos 7-file_system]$ ls -li
total 4
922143 drwxrwxr-x 2 bacon bacon 4096 Feb 17 08:40 dir
922141 -rw-rw-r-- 1 bacon bacon    0 Feb 17 08:39 log1.txt
922142 -rw-rw-r-- 1 bacon bacon    0 Feb 17 08:40 log2.txt

*ls -i可以显示文件inode的id。

这个inode的编号有什么用?查找文件时就是通过inode的编号来查找


找到了一个文件,想获取它的内容,可以到Data Blocks去拿,但是怎么确定这么多数据块中,哪些是和我这个文件有关的呢?

inode中,有一个字段保存了相关属性

struct inode
{
    int id;
    mode_t mode;
    ...
    int blocks[15]; //保存数据块编号
    ...
}

在这里插入图片描述

如此就能确认哪些数据块和当前文件有关。但这样能标识的数据块还是太少(15 * 4KB / 1024B = 60KB)。

其实不然,谁说数据块里只能保存文件内容?数据块内也能保存其他数据块编号!(多级索引)

所以文件和inode、Data Blocks间一定有方式建立关系。


基于文件系统,重新理解文件

有了上面的认识,对于创建文件也有了新的理解。

创建一个文件:

  1. Inode BitMap中某个位置从0变成1
  2. 根据BitMap中的位置,找到Inode Table中对应位置的inode,把文件属性填入
  3. 如果需要写入:数据写入Data Blocks,Block BitMap中对应位置的比特位从0变成1
  4. inode中填入数据块编号(建立inode和Data Blocks间的联系)
  5. 返回inode编号

在这里插入图片描述

同理,读取一个文件:

  1. 根据文件inode编号找到Inode Table中属于此文件的inode
  2. 通过inode中保存的数据块编号,找到数据块
  3. 读取

同理,删除一个文件:

  1. Inode BitMap中某个位置从1变成0
  2. Block BitMap中标识此文件数据块的比特位从1变成0

这是一种惰性删除,下次有别的文件来,会将原本的属性、内容都覆盖掉。哪些是自己的数据,下一个来的文件自己也清楚。

以前可能看见过恢复数据这样的操作,跟这里的惰性删除就有关系。


等等,你刚刚说“查找文件时就是通过inode的编号来查找”。

但,我们Linux用了这么久,文件相关的操作一直都用的是文件名,这个inode编号?……

[bacon@VM-12-5-centos 7-file_system]$ ls -li 
total 1
922143 drwxrwxr-x 2 bacon bacon 4096 Feb 17 08:40 dir
[bacon@VM-12-5-centos 7-file_system]$ ls 922143
ls: cannot access 922143: No such file or directory

没用啊?

任何一个文件,一定在某个目录下。目录是文件,那么目录也有自己的数据块,里边存什么呢?

目录的数据块中,存的是文件名和inode编号的映射关系。

同一目录下,不能有同名文件。那么在某个目录下,文件名就是唯一的key值,可以通过这个key值索引到inode。

文件名对人友好,便于区分和管理,但对计算机来说,inode编号更简单。

:表面上,我们是用文件名来进行各种查找和其他操作;本质上,还是通过inode编号。


我们以前讲权限的时候,谈到“要在某个目录下创建文件必须要有这个目录的写入权限”,为什么?

因为查找文件时就是通过inode的编号来查找,在某个目录下通过文件名创建文件,需要写入文件名和inode编号的映射关系,也就需要对这个目录的数据块写入。

“要读文件,必须有这个目录的读取权限”,为什么?

因为查找文件时就是通过inode的编号来查找,读取某个目录下的某个文件,需要知道文件名和inode编号的映射关系,也就需要读取这个目录中数据块的内容。

所以创建文件和删除文件最后一步还要对目录的数据块进行操作。


以上的这些储备,其实可以让我们更好地学习软硬链接。


#软硬链接

先见见猪跑,创建一个软链接。

[bacon@VM-12-5-centos 8-link]$ touch myfile.txt
[bacon@VM-12-5-centos 8-link]$ ln -s myfile.txt soft_file.link
[bacon@VM-12-5-centos 8-link]$ ls -li
total 0
922145 -rw-rw-r-- 1 bacon bacon  0 Feb 17 11:53 myfile.txt
922146 lrwxrwxrwx 1 bacon bacon 10 Feb 17 11:53 soft_file.link -> myfile.txt

再来一个硬链接。

[bacon@VM-12-5-centos 8-link]$ ln myfile.txt hard_file.link
[bacon@VM-12-5-centos 8-link]$ ll -i
total 0
922145 -rw-rw-r-- 2 bacon bacon  0 Feb 17 11:53 hard_file.link
922145 -rw-rw-r-- 2 bacon bacon  0 Feb 17 11:53 myfile.txt
922146 lrwxrwxrwx 1 bacon bacon 10 Feb 17 11:53 soft_file.link -> myfile.txt

一个inode对应一个文件名,但这里hard_file.linkmyfile.txt的inode编号居然是一样的,而且权限后的数字也变了。

  • 有独立inode的链接文件被称作原文件的软链接
  • 没有独立inode的链接文件被称作原文件的硬链接

现在你脑子里肯定有各种疑惑,软硬链接到底做了什么,权限后的数字又是什么?别急,我们一步步来。

硬链接做了什么?

[bacon@VM-12-5-centos 8-link]$ ll -i
total 0
922145 -rw-rw-r-- 2 bacon bacon  0 Feb 17 11:53 hard_file.link
922145 -rw-rw-r-- 2 bacon bacon  0 Feb 17 11:53 myfile.txt
922146 lrwxrwxrwx 1 bacon bacon 10 Feb 17 11:53 soft_file.link -> myfile.txt
[bacon@VM-12-5-centos 8-link]$ echo "hello bacon" >> myfile.txt 
[bacon@VM-12-5-centos 8-link]$ echo "hello bacon" >> myfile.txt 
[bacon@VM-12-5-centos 8-link]$ ll -i
total 8
922145 -rw-rw-r-- 2 bacon bacon 24 Feb 17 12:03 hard_file.link
922145 -rw-rw-r-- 2 bacon bacon 24 Feb 17 12:03 myfile.txt
922146 lrwxrwxrwx 1 bacon bacon 10 Feb 17 11:53 soft_file.link -> myfile.txt
[bacon@VM-12-5-centos 8-link]$ cat myfile.txt 
hello bacon
hello bacon
[bacon@VM-12-5-centos 8-link]$ cat hard_file.link 
hello bacon
hello bacon

根据以上的现象,我们可以确定:硬链接没有新增文件(inode没有给硬链接分配其inode)。既然如此,硬链接文件用的肯定是别人的inode和数据块。

是什么

没错,硬链接的本质,其实就是在目录的数据块中写入一对新的“inode编号和文件名映射关系”(键值对)

  • 原文件:922145 <==> myfile.txt
  • 硬链接文件:922145 <==> hard_file.link

那权限后的数字是什么?

是inode中的一个计数器,其实就是引用计数,表示了当前inode的硬链接数。

[bacon@VM-12-5-centos 8-link]$ rm myfile.txt 
[bacon@VM-12-5-centos 8-link]$ ll -i
total 4
922145 -rw-rw-r-- 1 bacon bacon 24 Feb 17 12:03 hard_file.link
922146 lrwxrwxrwx 1 bacon bacon 10 Feb 17 11:53 soft_file.link -> myfile.txt

删除原文件,相当于引用计数–,还剩下1。我们也能得出一个结论:

文件的硬链接数 减至0,文件才算真正删除。


但,新建一个文件时,硬链接数怎么也是1?

因为文件本身就具有文件名和inode编号的映射关系,也算是硬链接。

诶?那这一通操作,不就是重命名吗?

是的,mv重命名的本质就是向目标目录的数据块中写入一对新的映射关系,再将原来的删除。


再来看看软链接。

在这里插入图片描述

删除之后,被soft_file.link链接的这个文件本质上还是存在的,但是它这里一直闪烁表示失效,

[bacon@VM-12-5-centos 8-link]$ cat soft_file.link 
cat: soft_file.link: No such file or directory

想操作也说找不到。为什么呢?

软链接的本质,其实就是在目录的数据块中写入一对新的“文件名(路径)和文件名映射关系”

不是用inode编号链接(建立映射)的,而是通过文件名(路径)链接。

因此,删除原文件后,软链接保存的路径找不到原文件,就失效了。按这道理,我们重新创建一个文件,让软链接保存的路径能找到文件,就又可以了?

在这里插入图片描述

是的,只不过这个文件已经是一个全新的文件了。


作用

软链接的作用:“快捷方式”。

windows下:
在这里插入图片描述

Linux下:

#在./bin/exe/a/b/c/test下写了一个打印hello link的小程序
[bacon@VM-12-5-centos 8-link]$ mkdir -p ./bin/exe/a/b/c/test
[bacon@VM-12-5-centos 8-link]$ cd ./bin/exe/a/b/c/test/
[bacon@VM-12-5-centos test]$ touch test.c
[bacon@VM-12-5-centos test]$ vim test.c
[bacon@VM-12-5-centos test]$ gcc -o test test.c
[bacon@VM-12-5-centos test]$ ll
total 16
-rwxrwxr-x 1 bacon bacon 8360 Feb 17 14:56 test
-rw-rw-r-- 1 bacon bacon   77 Feb 17 14:56 test.c
[bacon@VM-12-5-centos test]$ cd -
/home/bacon/linux/6-basic_IO/8-link
#回到/home/bacon/linux/6-basic_IO/8-link,想执行一下test却很费劲
[bacon@VM-12-5-centos 8-link]$ ./bin/exe/a/b/c/test/test
hello link
#软链接
[bacon@VM-12-5-centos 8-link]$ ln -s ./bin/exe/a/b/c/test/test test
[bacon@VM-12-5-centos 8-link]$ ll
total 4
drwxrwxr-x 3 bacon bacon 4096 Feb 17 14:55 bin
lrwxrwxrwx 1 bacon bacon   25 Feb 17 14:59 test -> ./bin/exe/a/b/c/test/test
[bacon@VM-12-5-centos 8-link]$ ./test
hello link

硬链接的作用:作目录树状结构中的“父指针”。

[bacon@VM-12-5-centos 2]$ touch file.txt
[bacon@VM-12-5-centos 2]$ mkdir dir
[bacon@VM-12-5-centos 2]$ ll
total 4
drwxrwxr-x 2 bacon bacon 4096 Feb 17 15:11 dir
-rw-rw-r-- 1 bacon bacon    0 Feb 17 15:11 file.txt

file.txt的硬链接数是1可以理解,但为什么同样是新建,dir却是2?这就要把...拉出来讲讲了。

[bacon@VM-12-5-centos 2]$ ll -i
total 4
922157 drwxrwxr-x 2 bacon bacon 4096 Feb 17 15:11 dir
922156 -rw-rw-r-- 1 bacon bacon    0 Feb 17 15:11 file.txt
[bacon@VM-12-5-centos 2]$ cd dir
[bacon@VM-12-5-centos dir]$ ll -ai
total 8
922157 drwxrwxr-x 2 bacon bacon 4096 Feb 17 15:11 .
922155 drwxrwxr-x 3 bacon bacon 4096 Feb 17 15:11 ..

. 其实就又是一个硬链接。

在这里插入图片描述

[bacon@VM-12-5-centos dir]$ mkdir another_dir
[bacon@VM-12-5-centos dir]$ cd ..
[bacon@VM-12-5-centos 2]$ tree
.
|-- dir
|   `-- another_dir
`-- file.txt

2 directories, 1 file
[bacon@VM-12-5-centos 2]$ ll -i
total 4
922157 drwxrwxr-x 3 bacon bacon 4096 Feb 17 15:29 dir
922156 -rw-rw-r-- 1 bacon bacon    0 Feb 17 15:11 file.txt

当我们在dir内创建another_dirdir的硬链接数变成了3?

在这里插入图片描述

..也是一层链接!


但是并不能给目录建立硬链接。

[bacon@VM-12-5-centos 2]$ ls
dir  file.txt
[bacon@VM-12-5-centos 2]$ ln dir hardlink_for_dir
ln: ‘dir’: hard link not allowed for directory

为什么?

为一个目录创建硬链接,也就必须对其子目录和子文件创建硬链接,也必须对其子目录的子目录和子目录的子文件创建硬链接,也必须……这一趟下来可太复杂了,也很容易死循环。比如根目录的某个下级目录中有根目录的硬链接,当我们访问这个硬链接,会走进根目录,走进根目录最终又会走到这个下级目录,又会走到根目录的硬链接……

不对啊,...不正是给目录建立的硬链接吗,这不是自相矛盾吗?

这俩货 是特殊情况,也是OS自己来设置的,所以百分百放心不会有问题。


最后我们再解释一下文件的AMC时间。

#文件的AMC时间

  • Access:文件最后访问时间
  • Modify:文件属性最后修改时间
  • Change:文件内容最后修改时间
[bacon@VM-12-5-centos 3]$ touch test.txt
[bacon@VM-12-5-centos 3]$ stat test.txt 
  File: ‘test.txt’
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d	Inode: 922160      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
#新建后,三个时间是统一的
Access: 2023-02-17 15:58:34.797701756 +0800
Modify: 2023-02-17 15:58:34.797701756 +0800
Change: 2023-02-17 15:58:34.797701756 +0800
 Birth: -
#更改了文件属性
[bacon@VM-12-5-centos 3]$ chmod o-r test.txt 
[bacon@VM-12-5-centos 3]$ stat test.txt 
  File: ‘test.txt’
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: fd01h/64769d	Inode: 922160      Links: 1
Access: (0660/-rw-rw----)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
Access: 2023-02-17 15:58:34.797701756 +0800
Modify: 2023-02-17 15:58:34.797701756 +0800
#Change时间对应更新
Change: 2023-02-17 15:58:59.508849528 +0800
 Birth: -
#更改了文件内容
[bacon@VM-12-5-centos 3]$ echo "hello Modify" >> test.txt 
[bacon@VM-12-5-centos 3]$ stat test.txt 
  File: ‘test.txt’
  Size: 13        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 922160      Links: 1
Access: (0660/-rw-rw----)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
Access: 2023-02-17 15:59:27.807018723 +0800
#更改了文件内容,所以Modify时间更新
Modify: 2023-02-17 15:59:26.462010682 +0800
#但是没再改文件属性,Change时间却更新?
Change: 2023-02-17 15:59:26.462010682 +0800
 Birth: -

为什么?因为改了文件内容,文件属性也很可能更改了。

[bacon@VM-12-5-centos 3]$ stat test.txt 
  File: ‘test.txt’
  Size: 13        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 922160      Links: 1
Access: (0660/-rw-rw----)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
Access: 2023-02-17 15:59:27.807018723 +0800
Modify: 2023-02-17 15:59:26.462010682 +0800
Change: 2023-02-17 15:59:26.462010682 +0800
 Birth: -
#访问文件
[bacon@VM-12-5-centos 3]$ cat test.txt 
hello Modify
[bacon@VM-12-5-centos 3]$ stat test.txt 
  File: ‘test.txt’
  Size: 13        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 922160      Links: 1
Access: (0660/-rw-rw----)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
#Access时间没更新
Access: 2023-02-17 15:59:27.807018723 +0800
Modify: 2023-02-17 15:59:26.462010682 +0800
Change: 2023-02-17 15:59:26.462010682 +0800
 Birth: -
#访问文件
[bacon@VM-12-5-centos 3]$ cat test.txt 
hello Modify
#访问文件
[bacon@VM-12-5-centos 3]$ cat test.txt 
hello Modify
#访问文件
[bacon@VM-12-5-centos 3]$ cat test.txt 
hello Modify
[bacon@VM-12-5-centos 3]$ stat test.txt 
  File: ‘test.txt’
  Size: 13        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 922160      Links: 1
Access: (0660/-rw-rw----)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
#Access时间还是没更新
Access: 2023-02-17 15:59:27.807018723 +0800
Modify: 2023-02-17 15:59:26.462010682 +0800
Change: 2023-02-17 15:59:26.462010682 +0800

怎么回事?如果真的每次访问都更新一下Access时间,就意味着每次访问都要对磁盘上的数据更改,效率太低。所以Access的更新策略优化成了“一定时间/一定次数后更新”。


#动静态库

概念回顾

以前学习这部分,主要的概念就是动静态链接。

静态链接:程序编译的时候就将静态库的代码链接(拷贝)进可执行程序,执行时无需链接

动态链接:程序执行时才去链接动态库的代码(多个程序可以共享同一动态库的代码)

  1. 链接动态库的可执行文件中,只需要保存所用函数的地址表,而不是函数对应的机器码。
  2. 可执行程序执行前,会根据地址表把磁盘上动态库中的代码拷贝到内存,这个行为称动态链接

具体见这篇文章:【Linux03-基本工具之GCC】Linux下的C语言编译器


库的本质

以前,张三想用我写的一些接口,但我不想把源码暴露给他,就给他.o可重定向目标文件.h头文件,他也能正常用。可是,一旦源文件多了,.o也多了,非常不方便。

最后,人们决定将多个.o文件组合到一起,打包形成一个文件,这个文件就是

而库的打包方式等,又决定了库的类型(动/静态库)。

了解了库的本质,我们可以更深入地学习动静态库和动静态链接。


静态库

我们提到,库的本质就是将多个.o文件打包,如何打包呢?这里要提到一个命令:ar

  • 作用:ararchive的缩写,意为归档。这个命令是GNU的归档命令。可以打包.o文件
  • 选项
    • -r = replace
    • -c = create

my_add.h

#pragma once 

#include <stdio.h>

int Add(int x, int y);

my_add.c

#include "my_add.h"

int Add(int x, int y)
{
    printf("%d + %d = ", x, y);
    return x + y;
}

my_sub.h

#pragma once 

#include <stdio.h>

int Sub(int x, int y);

my_sub.c

#include "my_sub.h"

int Sub(int x, int y)
{
    printf("%d - %d = ", x, y);
    return x - y;
}

makefile

libmycal.a:my_add.o my_sub.o
	ar -rc $@ $^
my_add.o:my_add.c
	gcc -c my_add.c
my_sub.o:my_sub.c
	gcc -c my_sub.c

.PHONY:clean
clean:
	rm -f *.o libmycal.a

bash

[bacon@VM-12-5-centos 9-lib]$ make
gcc -c my_add.c
gcc -c my_sub.c
ar -rc libmycal.a my_add.o my_sub.o
[bacon@VM-12-5-centos 9-lib]$ file libmycal.a 
libmycal.a: current ar archive #libmycal.a是归档文件

如此就有了一个静态库。那么如何交付呢

交付库 = 打包压缩(动/静态库文件 + 对应的头文件)

先来打包:

libmycal.a:my_add.o my_sub.o
	ar -rc $@ $^
my_add.o:my_add.c
	gcc -c my_add.c
my_sub.o:my_sub.c
	gcc -c my_sub.c

.PHONY:release
	mkdir -p mylib/include
	mkdir -p mylib/lib 
	cp -f *.h mylib/include
	cp -f *.a mylib/lib

.PHONY:clean
clean:
	rm -f *.o libmycal.a
[bacon@VM-12-5-centos 9-lib]$ make release
mkdir -p mylib/include
mkdir -p mylib/lib 
cp -f *.h mylib/include
cp -f *.a mylib/lib
[bacon@VM-12-5-centos 9-lib]$ ll
total 36
...
drwxrwxr-x 4 bacon bacon 4096 Feb 17 19:33 mylib
...
[bacon@VM-12-5-centos 9-lib]$ tree mylib
mylib
|-- include
|   |-- my_add.h
|   `-- my_sub.h
`-- lib
    `-- libmycal.a

2 directories, 3 files

再来压缩:

[bacon@VM-12-5-centos 9-lib]$  tar czf mylib.tgz mylib
[bacon@VM-12-5-centos 9-lib]$ ll
total 40
...
-rw-rw-r-- 1 bacon bacon  945 Feb 17 19:36 mylib.tgz
...

这时,只需要把mylib.tgz发给张三就可以了。

在这里插入图片描述

解压完后,还要安装。安装的本质其实是拷贝

  • 把头文件拷贝到系统的头文件路径中
  • 把库文件拷贝到系统的库文件路径中

但我们先不安装,直接用。

第一种方案:指明路径

[bacon@VM-12-5-centos libuser]$ gcc -o cal main.c
main.c:1:20: fatal error: my_add.h: No such file or directory
 #include "my_add.h"
                    ^
compilation terminated.

使用一下,发现找不到头文件。

头文件默认搜索顺序:. ==> 系统默认路径

但头文件在./mylib/include,并不在.,也不在系统默认路径

解决:指定头文件搜索路径——gcc -I ./mylib/include

[bacon@VM-12-5-centos libuser]$ gcc -o cal main.c -I ./mylib/include/
/tmp/ccbnzfvK.o: In function `main':
main.c:(.text+0xf): undefined reference to `Add'
main.c:(.text+0x2f): undefined reference to `Sub'
collect2: error: ld returned 1 exit status

但链接的时候又找不到Add定义,说明是库找不到。

解决:指定库文件搜索路径——gcc -L ./mylib/lib

[bacon@VM-12-5-centos libuser]$ gcc -o cal main.c -I ./mylib/include/ -L ./mylib/lib
/tmp/ccpjHdMn.o: In function `main':
main.c:(.text+0xf): undefined reference to `Add'
main.c:(.text+0x2f): undefined reference to `Sub'
collect2: error: ld returned 1 exit status

还不行?其实,想要链接第三方库(非/usr/bin/lib下的库),必须要指明库名。

不对啊,我们以前用标准库都不用指明库名呢?

仅仅使用C标准库,gcc能识别;用C++的标准库,g++也能识别。但第三方库,对于编译器来说无法识别,必须指明库名。

解决:指定库文件名称——gcc -l libmycal.a

[bacon@VM-12-5-centos libuser]$ gcc -o cal main.c -I ./mylib/include/ -L ./mylib/lib -l libmycal.a
/usr/bin/ld: cannot find -llibmycal.a
collect2: error: ld returned 1 exit status

还不行???别忘了,库名称是去掉前缀lib和后缀.a/.so

bacon@VM-12-5-centos libuser]$ gcc -o cal main.c -I ./mylib/include/ -L ./mylib/lib -l mycal
[bacon@VM-12-5-centos libuser]$ ll
total 20
-rwxrwxr-x 1 bacon bacon 8480 Feb 17 20:00 cal
-rw-rw-r-- 1 bacon bacon  136 Feb 17 19:45 main.c
drwxrwxr-x 4 bacon bacon 4096 Feb 17 19:33 mylib
[bacon@VM-12-5-centos libuser]$ ./cal
10 + 20 = 30
10 - 20 = -10

完事。

[bacon@VM-12-5-centos libuser]$ ./cal
10 + 20 = 30
10 - 20 = -10
[bacon@VM-12-5-centos libuser]$ file cal
cal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=c23e0c892d73684e3d2ae775a9d405551526cc52, not stripped

咱们用的是静态库,但还是动态链接?虽然gcc默认动态链接,但这只是建议动态链接。 到底什么链接还是得看具体给了什么库。

  • 只有动态:动态链接
  • 只有静态:静态链接
  • 动静都有:动态链接

我们用了printf,需要C语言的库。尽管系统内可能动静态库都有,但gcc默认用动态库,所以整体就是动态链接。


第二种方案:安装

[bacon@VM-12-5-centos mylib]$ ll
total 8
drwxrwxr-x 2 bacon bacon 4096 Feb 17 19:33 include
drwxrwxr-x 2 bacon bacon 4096 Feb 17 19:33 lib
#拷贝头文件到系统默认路径
[bacon@VM-12-5-centos mylib]$ sudo cp ./include/* /usr/include
[bacon@VM-12-5-centos mylib]$ ls  /usr/include/my_add.h
/usr/include/my_add.h
#拷贝库文件到系统默认路径
[bacon@VM-12-5-centos mylib]$ sudo cp ./lib/*  /lib64/
[bacon@VM-12-5-centos mylib]$ ls /lib64/libmycal.a 
/lib64/libmycal.a
[bacon@VM-12-5-centos libuser]$ gcc main.c
/tmp/ccnB76rp.o: In function `main':
main.c:(.text+0xf): undefined reference to `Add'
main.c:(.text+0x2f): undefined reference to `Sub'
collect2: error: ld returned 1 exit status

还是不行:即便能在默认路径找了,但是库很多,还是无法确定要链接哪个,得指定库名。

[bacon@VM-12-5-centos libuser]$ gcc main.c -l mycal
[bacon@VM-12-5-centos libuser]$ ll
total 20
-rwxrwxr-x 1 bacon bacon 8480 Feb 17 20:20 a.out
-rw-rw-r-- 1 bacon bacon  136 Feb 17 19:45 main.c
drwxrwxr-x 4 bacon bacon 4096 Feb 17 19:33 mylib
[bacon@VM-12-5-centos libuser]$ ./a.out 
10 + 20 = 30
10 - 20 = -10

做完测试,还是要把这些东西删掉,免得污染。

[bacon@VM-12-5-centos libuser]$ sudo rm /usr/include/my_*
[bacon@VM-12-5-centos libuser]$ sudo rm /usr/lib64/libmycal.a 

这个过程,就叫卸载。


动态库

相比静态库,仅多了一个选项:-fPIC。作用是,在编译生成.o文件时,产生位置无关码(position independent code)。

[bacon@VM-12-5-centos 2]$ cat makefile
libmycal.so:my_add.o my_sub.o
	gcc -shared -o $@ $^ 
my_add.o:my_add.c
	gcc -c -fPIC my_add.c
my_sub.o:my_sub.c
	gcc -c -fPIC my_sub.c

.PHONY:release
release:
	mkdir -p mylib/include
	mkdir -p mylib/lib 
	cp -f *.h mylib/include
	cp -f *.so mylib/lib

.PHONY:clean
clean:
	rm -rf *.o libmycal.so mylib
[bacon@VM-12-5-centos 2]$ make
gcc -c -fPIC my_add.c
gcc -c -fPIC my_sub.c
gcc -shared -o libmycal.so my_add.o my_sub.o 

[bacon@VM-12-5-centos 2]$ clear
[bacon@VM-12-5-centos 2]$ make release
mkdir -p mylib/include
mkdir -p mylib/lib 
cp -f *.h mylib/include
cp -f *.so mylib/lib
[bacon@VM-12-5-centos 2]$ ll
total 40
...
drwxrwxr-x 4 bacon bacon 4096 Feb 17 20:48 mylib
...
[bacon@VM-12-5-centos 2]$ tree mylib/
mylib/
|-- include
|   |-- my_add.h
|   `-- my_sub.h
`-- lib
    `-- libmycal.so

2 directories, 3 files
[bacon@VM-12-5-centos 2]$ cp -rf mylib ../../libuser
[bacon@VM-12-5-centos libuser]$ pwd
/home/bacon/linux/6-basic_IO/libuser
[bacon@VM-12-5-centos libuser]$ gcc -o cal main.c -Imylib/include -Lmylib/lib -lmycal
[bacon@VM-12-5-centos libuser]$ ll
total 20
-rwxrwxr-x 1 bacon bacon 8432 Feb 17 20:57 cal
-rw-rw-r-- 1 bacon bacon  136 Feb 17 20:56 main.c
drwxrwxr-x 4 bacon bacon 4096 Feb 17 20:56 mylib
[bacon@VM-12-5-centos libuser]$ ./cal 
./cal: error while loading shared libraries: libmycal.so: cannot open shared object file: No such file or directory

发现加载动态库的时候有问题。

[bacon@VM-12-5-centos libuser]$ ldd cal
	linux-vdso.so.1 =>  (0x00007ffffe9aa000)
	libmycal.so => not found
	libc.so.6 => /lib64/libc.so.6 (0x00007fce6e43e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fce6e80c000)

库文件的路径和名称我都交代了啊,怎么还找不到?

我们是给gcc交代的,但是当程序编译完,就没gcc的事儿了,运行前动态链接还得找OS。OS和shell也得知道库在哪里。

我们的库不在系统路径下,所以运行前OS找不到这个库。

解决1:把我们的路径追加到环境变量LD_LIBRARY_PATH,shell运行时会自动到这里找

[bacon@VM-12-5-centos libuser]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/bacon/linux/6-basic_IO/libuser/mylib/lib
[bacon@VM-12-5-centos libuser]$ echo $LD_LIBRARY_PATH 
:/home/bacon/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/bacon/linux/6-basic_IO/libuser/mylib/lib
[bacon@VM-12-5-centos libuser]$ gcc -o cal main.c -Imylib/include -Lmylib/lib -lmycal
[bacon@VM-12-5-centos libuser]$ ll
total 20
-rwxrwxr-x 1 bacon bacon 8432 Feb 17 21:08 cal
-rw-rw-r-- 1 bacon bacon  136 Feb 17 20:56 main.c
drwxrwxr-x 4 bacon bacon 4096 Feb 17 20:56 mylib
[bacon@VM-12-5-centos libuser]$ ./cal
10 + 20 = 30
10 - 20 = -10

完事。

但这样导的环境变量是临时的,用来测试一下还行,长久用还得每次导,不方便。


解决2:拷贝到系统路径下

不演示了。


解决3:在/etc/ld.so.conf.d添加配置文件

*shell也会在这里的配置文件中找

[bacon@VM-12-5-centos libuser]$ cd /etc/ld.so.conf.d
[bacon@VM-12-5-centos ld.so.conf.d]$ sudo touch test.conf
[bacon@VM-12-5-centos ld.so.conf.d]$ ll
total 16
-rw-r--r-- 1 root root 26 Feb 24  2022 bind-export-x86_64.conf
-rw-r--r-- 1 root root 19 Aug  9  2019 dyninst-x86_64.conf
-r--r--r-- 1 root root 63 Jun 28  2022 kernel-3.10.0-1160.71.1.el7.x86_64.conf
-rw-r--r-- 1 root root 17 Oct  2  2020 mariadb-x86_64.conf
-rw-r--r-- 1 root root  0 Feb 17 21:14 test.conf

test.conf中,添加我们的库的路径。

[bacon@VM-12-5-centos ld.so.conf.d]$ cat test.conf
/home/bacon/linux/6-basic_IO/libuser/mylib/lib
[bacon@VM-12-5-centos libuser]$ ./cal
./cal: error while loading shared libraries: libmycal.so: cannot open shared object file: No such file or directory
[bacon@VM-12-5-centos libuser]$ ldd cal 
	linux-vdso.so.1 =>  (0x00007ffedcda4000)
	libmycal.so => not found
	libc.so.6 => /lib64/libc.so.6 (0x00007fc53ebba000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fc53ef88000)

还是找不到……因为配置文件还没更新。

使用ldconfig(加载配置文件)命令即可。

[bacon@VM-12-5-centos ld.so.conf.d]$ sudo ldconfig
[bacon@VM-12-5-centos etc]$ cd /home/bacon/linux/6-basic_IO/libuser
[bacon@VM-12-5-centos libuser]$ ldd cal
	linux-vdso.so.1 =>  (0x00007ffe7855f000)
	libmycal.so => /home/bacon/linux/6-basic_IO/libuser/mylib/lib/libmycal.so (0x00007fa5450db000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fa544d0d000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fa5452dd000)
[bacon@VM-12-5-centos libuser]$ ./cal
10 + 20 = 30
10 - 20 = -10

完事。


解决4:软链接

#把配置文件去掉
[bacon@VM-12-5-centos ld.so.conf.d]$ sudo rm -f test.conf
[bacon@VM-12-5-centos ld.so.conf.d]$ ll
total 16
-rw-r--r-- 1 root root 26 Feb 24  2022 bind-export-x86_64.conf
-rw-r--r-- 1 root root 19 Aug  9  2019 dyninst-x86_64.conf
-r--r--r-- 1 root root 63 Jun 28  2022 kernel-3.10.0-1160.71.1.el7.x86_64.conf
-rw-r--r-- 1 root root 17 Oct  2  2020 mariadb-x86_64.conf
[bacon@VM-12-5-centos ld.so.conf.d]$ sudo ldconfig
[bacon@VM-12-5-centos ld.so.conf.d]$ cd -
/home/bacon/linux/6-basic_IO/libuser
#无法找到动态库
[bacon@VM-12-5-centos libuser]$ ./cal 
./cal: error while loading shared libraries: libmycal.so: cannot open shared object file: No such file or directory
[bacon@VM-12-5-centos libuser]$ ldd cal
	linux-vdso.so.1 =>  (0x00007ffd4b1e1000)
	libmycal.so => not found
	libc.so.6 => /lib64/libc.so.6 (0x00007fd56a245000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd56a613000)
#软链接
[bacon@VM-12-5-centos libuser]$ ln -s ~/linux/6-basic_IO/libuser/mylib/lib/libmycal.so libmycal.so
[bacon@VM-12-5-centos libuser]$ ll
total 20
-rwxrwxr-x 1 bacon bacon 8432 Feb 17 21:08 cal
lrwxrwxrwx 1 bacon bacon   58 Feb 17 21:29 libmycal.so -> /home/bacon/linux/6-basic_IO/libuser/mylib/lib/libmycal.so
-rw-rw-r-- 1 bacon bacon  136 Feb 17 20:56 main.c
drwxrwxr-x 4 bacon bacon 4096 Feb 17 20:56 mylib
[bacon@VM-12-5-centos libuser]$ ./cal
10 + 20 = 30
10 - 20 = -10

运行时找在当前路径下找libmycal.so,会软链接至/home/bacon/linux/6-basic_IO/libuser/mylib/lib/libmycal.so

完事。


#安装第三方库

来一个黑框框图形化界面的库。

先下载:

[bacon@VM-12-5-centos ~]$ sudo yum install ncurses-devel

检查一下是有的:

[bacon@VM-12-5-centos test]$ ls /usr/include/ncurses.h -l
lrwxrwxrwx 1 root root 8 Feb 18 09:42 /usr/include/ncurses.h -> curses.h

写个hello world:

#include <string.h>
#include <ncurses.h>

int main(){
     initscr();
     raw();
     noecho();
     curs_set(0);
 
     const char* c = "Hello, World!";
 
     mvprintw(LINES/2,(COLS-strlen(c))/2,c);
     refresh();
 
     getch();
     endwin();
 
     return 0;
}

编译:

[bacon@VM-12-5-centos test]$ gcc -o test test.c -lncurses

效果:

在这里插入图片描述


理解动静态编译

静态编译

使用静态库、静态链接的编译行为称静态编译。

静态库的链接

静态库会在编译时拷贝(链接)到可执行程序。

静态库的“加载”

其实静态库不需要加载,它就嵌在可执行程序中。

类似绝对编址:编译时,静态库的代码具体在哪里,已经根据虚拟地址空间的内存划分规则被写死了。

运行时静态库的访问

那这样的可执行程序运行时,静态库的代码在哪里呢?

只能是虚拟地址空间中的代码段区域。往后进程访问静态库中的代码(如printf),都是通过绝对地址,找到printf的入口。

usec.c

#include <stdio.h>

int main()
{
    printf("hello wolrd!\n");
    return 0;
}
[bacon@VM-12-5-centos test]$ gcc -static -o s_usec usec.c
[bacon@VM-12-5-centos test]$ lld s_usec 
-bash: lld: command not found
[bacon@VM-12-5-centos test]$ ldd s_usec 
	not a dynamic executable #静态链接,不保存相对地址

静态编译:

  1. 可执行程序中存静态库被使用的部分和其绝对地址
  2. 程序要访问静态库部分就通过绝对地址访问

在这里插入图片描述

动态编译

使用动态库,动态链接的编译行为称动态编译。

动态库的链接

我们前面提到的-fPIC产生位置无关码,其实就是一种相对编址。

可执行程序中会保存位置无关码。这些相对地址是动态库中被使用部分在动态库中的相对地址

动态库的加载

程序载入内存时,通过相对地址把动态库的一部分载入代码段

动态库的访问

程序即将访问动态库部分时,通过相对地址把动态库的一部分载入代码段

[bacon@VM-12-5-centos test]$ gcc -o usec usec.c 
#动态链接,可执行内保存相对地址
[bacon@VM-12-5-centos test]$ ldd usec
	linux-vdso.so.1 =>  (0x00007ffef80f4000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f6026e16000) #printf的相对地址
	/lib64/ld-linux-x86-64.so.2 (0x00007f60271e4000)

动态编译:

  1. 可执行程序中存动态库被使用部分在动态库中的相对地址
  2. 程序即将访问动态库部分时,通过相对地址把动态库的一部分载入内存
  3. 把动态库的一部分通过页表映射到共享区(此时库的起始位置确定)。
  4. 访问动态库部分 = 共享区中库的起始位置 + 可执行中存的相对位置

在这里插入图片描述

为什么编译源文件要加-fPIC?这样编译出来的.o中,函数地址都是相对地址(位置无关码)了。

为什么打包成库时要加-shared?这样打包出来的库文件就是动态库格式。


可算结束了,这一章节可真不简单。

表层需要讲的其实就是文件描述符、软硬链接、动静态库,但底层牵扯的知识又很多。只有把背后的知识了解清楚,我们才能真正理解表层内容。


很高兴你能看到这,本期的分享告一段落了。

这里是培根的blog,期待与你共同进步

下期见~

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

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

相关文章

SQLSERVER2019安装步骤过程

第一步官网下载SQLSERVER软件包 目前官网只能下载最新版本2022版本。 通过迅雷下载网址 SQL Server 2019 Enterprise (x64) - DVD (Chinese-Simplified)企业版 ed2k://|file|cn_sql_server_2019_enterprise_x64_dvd_2bfe815a.iso|1632086016|58C258FF0F1D006DD3C1F5F17AF3E…

ELK_Elasticsearch环境搭建

目录 一、Windows安装elasticsearch 1、安装JDK 2、下载和解压 3、配置文件 4、启动 5、检查ES是否启动成功 6、浏览器访问 二、 Windows安装Kibana 一、Windows安装elasticsearch 1、安装JDK 安装JDK&#xff0c;至少1.8.0_73以上版本&#xff0c;验证&#xff1a;j…

dbForge Source Control for SQL Server 2.5.X Crack

SQL Server功能概述 的 dbForge 源代码管理 dbForge Source Control for SQL Server 是一个可视化的 SSMS 插件&#xff0c;具有简单易用的界面&#xff0c;可帮助您轻松跟踪 SQL Server 数据库对象中的更改内容、更改时间和原因。该工具使您能够将数据库连接到多个版本控制系统…

【Python学习笔记】40.Python3 SMTP发送邮件

前言 SMTP&#xff08;Simple Mail Transfer Protocol&#xff09;即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则&#xff0c;由它来控制信件的中转方式。 Python3 SMTP发送邮件 python的smtplib提供了一种很方便的途径发送电子邮件。它对smtp协议进行了…

CSS基础选择器,你认识多少?

前言在上一文初识CSS中&#xff0c;我们了解到了其格式&#xff1a;选择器{ }在初步尝试使用时&#xff0c;我们笼统的直接输入了p { }以选择p标签来对其操作&#xff0c;而这一章节里&#xff0c;我们再进一步探索有关基础选择器的相关内容&#xff0c;理解选择器的作用。选择…

2019蓝桥杯真题平方序列(填空题) C语言/C++

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 小明想找到两个正整数 X 和 Y&#xff0c;满足2019<X<Y;2019^2, X^2, Y^2组成等差数列。 请你求出在所有可能的解中&#xff0c;XY 的最小值是多少&#xff1f…

QT(17)- QNetworkAccessManager

QT&#xff08;17&#xff09;- QNetworkAccessManager1 简介2 公有类型3 函数3.1 addStrictTransportSecurityHosts3.2 autoDeleteReplies3.3 cache3.4 clearAccessCache3.5 clearConnectionCache3.6 connectToHost3.7 connectToHostEncrypted3.8 cookieJar3.9 deleteResource…

Metasploit框架基础(一)

文章目录前言一、基础认知二、批量POC/EXP的构想三、poc检测框架的简单实现四、xray五、Meatsploit框架参考前言 Metasploit 一款渗透测试框架漏洞利用的集合与构建和定制满足你的需求的基础漏洞利用和验证的工具 这几个说法都是百度或者官方文档中出现的手法&#xff0c;说…

ASEMI代理FGH60N60SFD,安森美FGH60N60SFD原装IGBT

编辑-Z 安森美FGH60N60SFD原装IGBT参数&#xff1a; 型号&#xff1a;FGH60N60SFD 集电极到发射极电压&#xff08;VCES&#xff09;&#xff1a;600V 栅极到发射极电压&#xff08;VGES&#xff09;&#xff1a;20V 收集器电流&#xff08;IC&#xff09;&#xff1a;120…

【Spark分布式内存计算框架——Spark SQL】9. Dataset(下)RDD、DF与DS转换与面试题

5.3 RDD、DF与DS转换 实际项目开发中&#xff0c;常常需要对RDD、DataFrame及Dataset之间相互转换&#xff0c;其中要点就是Schema约束结构信息。 1&#xff09;、RDD转换DataFrame或者Dataset 转换DataFrame时&#xff0c;定义Schema信息&#xff0c;两种方式转换为Dataset时…

由浅入深掌握 Python 进程间通信的各类方式

由浅入深掌握 Python 多进程间通信各类方式1、为什么要掌握进程间通信2、进程间各类通信方式简介3、消息机制通信1) 管道 Pipe 通信方式2) 消息队列Queue 通信方式4、同步机制通信(1) 进程间同步锁 – Lock(2) 子进程间协调机制 -- Event5、共享内存方式通信(1) 共享变量(2) 共…

【Bluetooth开发】蓝牙开发入门

BLE 蓝牙设备在生活中无处不在&#xff0c;但是我们也只是将其作为蓝牙模块进行使用&#xff0c;发送简单的AT命令实现数据收发。 那么&#xff0c;像对于一些复杂的使用场合&#xff1a;“车载蓝牙”、"智能手表"、“蓝牙音箱”等&#xff0c;我们不得不去了解底层…

千锋教育+计算机四级网络-计算机网络学习-04

UDP概述 UDP协议 面向无连接的用户数据报协议&#xff0c;在传输数据前不需要先建立连接&#xff1b;目地主机的运输层收到UDP报文后&#xff0c;不需要给出任何确认 UDP特点 相比TCP速度稍快些简单的请求/应答应用程序可以使用UDP对于海量数据传输不应该使用UDP广播和多播应用…

VectorDraw Web Library 10.1003.0.1 Crack

将 CAD 绘图和矢量对象显示添加到您的 HTML5 应用程序。 VectorDraw Web Library 是一个矢量图形库&#xff0c;旨在不仅可以打开 CAD 绘图&#xff0c;还可以在任何支持 HTML 5 标准的平台&#xff08;例如 Windows、Android、IOS 和 Linux&#xff09;上显示通用矢量对象。它…

MySQL 9:MySQL存储引擎

数据库存储引擎是数据库的底层软件组织&#xff0c;数据库管理系统&#xff08;DBMS&#xff09;使用数据引擎来创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技术、锁定级别等。 许多不同的数据库管理系统现在支持许多不同的数据引擎。 MySQL的核心是…

字符串匹配 - 模式预处理:KMP 算法(Knuth-Morris-Pratt)

Knuth-Morris-Pratt算法&#xff08;简称KMP&#xff09;是最常用的字符串匹配算法之一。算法简介如下算法解释主要来源于这里&#xff0c;但是通常很难阅读完全&#xff0c;我推荐你直接进入下一节 图例解释部分。我们来观察一下朴素的字符串匹配算法的操作过程。如下图&#…

Linux调试器gdb

本文已收录至《Linux知识与编程》专栏&#xff01; 作者&#xff1a;ARMCSKGT 演示环境&#xff1a;CentOS 7 ​ 目录 前言 正文 下载gdb 生成可调式文件 进入gdb gdb常用指令 查看代码 l 运行程序 r 断点设置 b 显示信息 info 查看断点 info b 删除断点 d …

CentOS7突然没法上网【Network 中wired 图标消失】

参考文章(七种办法)&#xff1a;CentOS 7 右上角网络连接图标消失,设置网络有线消失解决办法 正常图标消失&#xff0c;先在 终端命令 依次执行以下命令 service NetworkManager stop service network restart service NetworkManager start 一、问题真烦 CentOS7图形化界面安装…

Day893.MySQL 实例健康状态检测方法 -MySQL实战

MySQL 实例健康状态检测方法 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于MySQL 实例健康状态检测方法的内容。 在一主一备的双 M 架构里&#xff0c;主备切换只需要把客户端流量切到备库&#xff1b;而在一主多从架构里&#xff0c;主备切换除了要把客户端流量切…

搭建企业级docker仓库—Harbor

一、简介 docker 官方提供的私有仓库 registry&#xff0c;用起来虽然简单 &#xff0c;但在管理的功能上存在不足。 Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器&#xff0c;harbor使用的是官方的docker registry(v2命名是distribution)服务去完成。harbor在…