【Linux】5.0基础IO

C基础文件操作

C程序会默认打开三个输入输出流, stdin(键盘), stdout(显示器), stderror(显示器)

STDIN(3)                   Linux Programmer's Manual                  STDIN(3)
NAME
       stdin, stdout, stderr - standard I/O streams

SYNOPSIS
       #include <stdio.h>

       extern FILE *stdin;   //键盘
       extern FILE *stdout;  //显示器
       extern FILE *stderr;  //显示器

int main()  
  {
    
    
    //写文件 w刷新写入 a追加写入                                                                                                                                                        
    FILE* fp = fopen("log.txt", "w");
    if (fp == NULL)
    {
    
                      
      perror("fopen");    
      return 1;        
    }                  
    const char* msg = "hello world\n";    
    fputs(msg, fp);    
        
    //读文件           
    //FILE* fp = fopen("log.txt", "r");  
    //if (fp == NULL){  
    //  perror("fopen failed");  
    //  return 1;      
    //}                
    //char buffer[64] = {0};  
    //while (fgets(buffer, 64, fp) != NULL);  
    //printf("%s", buffer);  
    //检查是否关闭文件成功
    if (!feof(fp))   
      printf("file close failed\n");  
    else   
      printf("file close successs\n");  
    return 0;  
}

系统文件I/O原理

在这里插入图片描述

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);
       int creat(const char *pathname, mode_t mode);

int flags 参数详解
O_WRONLY, O_RDONLY,O_CREAT等都是只有一个比特位是1的数据,并且不重复。所以一个int类型的flag可以传递32个标志位。操作系统可以通过(O_WRONLY & flags)来检测某个标志位是否被标记

文件描述符 fd

标准输入 0 标准输出 1 标准错误 2
所有的文件操作,表现上都是进程执行对应的函数,进程对文件的操作,要求操作系统必须要先打开文件,所以必须要将文件相关的属性信息加载到内存。操作系统中存在大量的进程,每个进程都要打开很多文件,那么操作系统就必须要将打开的大量文件管理起来。

如果一个文件,没有被打开,那这个文件在哪里呢?在磁盘上创建一个空文件,该文件要不要占用磁盘空间?这显然是必须的,文件有属性,属性也是数据。
磁盘文件 = 文件内容 + 文件属性
文件操作:对文件内容操作,对文件的属性操作

那么如何管理打开的文件呢?
在这里插入图片描述
在操作系统中,每一个文件被打开都会将文件的属性放入一个叫做struct file类型的结构体,然后通过双链表将这些结构体链接起来。

但是这么多文件,都分别属于哪一些进程呢? 在进程创建时会创建PCB,还会创建一个命为struct
files_struct的结构体,PCB中有一个struct files_struct* fs 的指针指向structfiles_struct,这个结构体中有一个数组,叫做struct file*fd_array[],数组中的指针指向操作系统中的struct file双链

那操作系统又该怎么管理硬件呢?

在这里插入图片描述
通过struct file,每一个文件的struct file中都有一个指针,它指向一张存储函数指针的表,这些函数指针直接指向底层方法。所以我们对不同文件进行操作,操作系统就可以通过文件的struct file调用不同的底层方法,来对不同的硬件进行操作。所以在操作系统看来,所有硬件的管理都是对struct file使用一套接口,在其看来一切皆文件

文件描述符的分配规则
规则:在 files_struct 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

int main()    
{
    
        
  // fd的分配规则以及输出重定向
  close(1);     //关闭标准输出
  int fd = open("./log.txt", O_WRONLY | O_CREAT, 0644);  //打开一个文件    
  printf("fd = %d\n", fd);                               //输出这个文件的文件描述符fd
  printf("hello world\n");                               //使用printf打印hello wrold
  return 0;                                                                                                                                                                             
}    

输出结果

[clx@VM-20-6-centos redirect]$ make
gcc -o test test.c -std=c99
[clx@VM-20-6-centos redirect]$ ./test
[clx@VM-20-6-centos redirect]$ cat log.txt
fd = 1
hello world

总结:关闭文件描述符1后再次打开文件,新文件分配的文件描述符为一。这是我们又发现一个问题,原本应该打印在显示器上的数据跑到了log.txt文件中,这就是输出重定向

//输入重定向
int main()
{
    
    
  close(0);                                        //关闭标准输入
  int fd = open("./log.txt", O_RDONLY);           
  char buffer[128] = {
    
    0};
  printf("fd = %d", fd);                           //打印文件描述符
  while (fgets(buffer, sizeof(buffer) - 1, stdin)) //获取数据到buffer中
  {
    
    
    printf("%s", buffer);                          //打印buffer
  }
  return 0;
}

//输出结果
[clx@VM-20-6-centos redirect]$ cat log.txt   //原来log.txt文件中的信息
fd = 1
hello world
[clx@VM-20-6-centos redirect]$ ./test        //新文件分配到fd = 0, 本应该从标准输入读取变成了从新文件读取
fd = 0fd = 1
hello world


输入/输出重定向

SYNOPSIS
       #include <stdio.h>
       extern FILE *stdin;
       extern FILE *stdout;
       extern FILE *stderr;

SYNOPSIS
       #include <stdio.h>
       FILE *fopen(const char *path, const char *mode);

FILE* 和 我们的fd有什么关系呢?
原来FILE 是一个C语言层面的结构体,这个结构体中有一个变量叫做_fileno,这个变量就是我们的fd。在stdin中_fileno就是1,在标准输出stdout中_fileno 就是2

int main()
{
    
    
  printf("stdin->_fileno = %d\n", stdin->_fileno);
  printf("stdout->_fileno = %d\n", stdout->_fileno);
  printf("stderr->_fileno = %d\n", stderr->_fileno);
  return 0;
}
//结果
[clx@VM-20-6-centos redirect]$ ./test
stdin->_fileno = 0
stdout->_fileno = 1
stderr->_fileno = 2

所以我们的C语言接口就是通过FILE*这个指针 找到 FILE这个结构体,通过这个结构体中的_fileno变量,再传递给系统调用接口进行操作,至于_fieno变量到底对应着什么文件,C语言接口并不关心,这就为重定向数据提供了条件

dup2() 重定向函数
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
让newfd 成为 oldfd 的一份拷贝,必要时先关闭newfd

SYNOPSIS
       #include <unistd.h>
       int dup(int oldfd);
       int dup2(int oldfd, int newfd);

dup2并非只是两个整形的拷贝,其底层原理是将struct files_struct 中的那个数组中的oldfd下标的指针拷贝到newfd下标的空间

int main()
{
    
    
  int fd = open("./log.txt", O_WRONLY | O_CREAT, 0644);
  if (fd < 0){
    
    
    perror("open failed\n");
  }
  dup2(fd, 1);                          //将下标1指针用下标3的指针覆盖,那么此时使用c语言接口
  printf("hello printf\n");             //对stdout即标准输出操作就是对log.txt文件进行对应操作
  fprintf(stdout, "hello wrold\n");
  fputs("hello fputs\n", stdout);
  return 0;
}
//结果
[clx@VM-20-6-centos redirect]$ ./test         //运行程序,标准输出并未打印东西
[clx@VM-20-6-centos redirect]$ cat log.txt    //本应该打印在标准输出的数据都到了log.txt中
hello printf
hello wrold
hello fputs

这也印证了C函数接口对于stdout, stdin它们只认识其中的fd,对于fd对应的文件到底是不是标准输入输出,C语言接口并不关心,其只要对着对应的fd 文件进行操作即可

echo > 指令分析

echo "hello world" > log.txt 
实现原理 
1.读取拆分字符串 ,检测到echo创建子进程 fork()
2.检测到 > 重定向符号 调用 dup2(fd, 1),对子进程的fd数组进行操作
3.调用exec* 函数进行进程程序替换, execvp("echo", argv)

为什么我们所有的进程都会打开标准输入/输出/错误呢?
因为所有的进程都是父进程bash的子进程,而bash是命令行解释器,打开这三个文件是必要的,由bash创建的子进程都继承了这bash的文件管理方式

标准错误重定向

int main()
{
    
    
  char msg1[128] = "hello stdout\n";
  char msg2[128] = "hello stderr\n";
  fprintf(stdout, msg1);
  fprintf(stderr, msg2);
  return 0;
}
[clx@VM-20-6-centos redirect]$ ./test > log.txt
hello stderr
[clx@VM-20-6-centos redirect]$ cat log.txt
hello stdout

我们发现本应该重定向到log.txt中的hello stderr竟然还是打印到了显示器上,这是因为标准错误有些特殊,我们不能直接对其使用>重定向符进行重定向。需要在后面增加一条语句 2 >&1,即为./test > log.txt 2 >&1 ,bash会先执行前面的语句,后面的语句意为将fd = 2的文件中的数据剪贴到1

用户/内核缓冲区

现象1

//输出重定向
int main()    
{
    
        
  close(1);              //将标准输出关闭,打开一个文件 
  int fd = open("./log.txt", O_WRONLY | O_CREAT, 0644);    
  if (fd < 0){
    
        
    perror("open failed\n");    
  }                                                                                                                                                                                     
  printf("hello wrold\n");    //输出三句话
  printf("hello wrold\n");    
  printf("hello wrold\n");    
  close(fd);                  //关闭文件描述符为fd文件
}    

//运行结果
[clx@VM-20-6-centos redirect]$ make
gcc -o test test.c -std=c99
[clx@VM-20-6-centos redirect]$ ./test
[clx@VM-20-6-centos redirect]$ cat log.txt 
[clx@VM-20-6-centos redirect]$ 

我们发现不管是标准输出中,还是我们的文件中,都没有打印的数据。
在这里插入图片描述

现象分析:当我们将标准输出关闭,将文件重定向到标准输出的位置上时,则printf通过的stdout找到的struct file已经不再是标准输出文件。而是一个普通磁盘文件,那么通过struct file中的函数指针调用的方法也不再是刷新到显示器(行缓冲)的方法了,调用的是刷新到磁盘文件的方法(全缓冲)。那么三个printf执行完用户缓冲区并不会刷新,之后close(fd)操作,C语言缓冲区就无法通过fd找到对应的文件内核缓冲区,在进程退出的时候也就无法刷新出去,所以不论在标准输出还是log.txt中我们都找不到目标数据

解决方案:在关闭fd前面调用fclose( )函数主动关闭C语言缓冲区,就可以提前将数据刷新到文件内核缓冲区了。或者调用fflush()函数主动刷新缓冲区

现象2

int main()    
{
    
        
  int fd = open("./log.txt", O_WRONLY | O_CREAT, 0644);    
  if (fd < 0){
    
        
    perror("open failed\n");    
  }    
  printf("hello wrold\n");    
  printf("hello wrold\n");    
  printf("hello wrold\n");    
  fork();                                                                                                                                                                               
}    

[clx@VM-20-6-centos redirect]$ > log.txt          //清空log.txt
[clx@VM-20-6-centos redirect]$ make
gcc -o test test.c -std=c99
[clx@VM-20-6-centos redirect]$ ./test             //运行
hello wrold
hello wrold
hello wrold
[clx@VM-20-6-centos redirect]$ ./test > log.txt   //运行 + 重定向
[clx@VM-20-6-centos redirect]$ cat log.txt
hello wrold
hello wrold
hello wrold
hello wrold
hello wrold
hello wrold

现象:当我们直接运行test时,其打印了三条hello world, 但是如果我们将test运行的内容重定向到文件中,竟然打印了六条hello world,这是为什么呢?
现象解析:根据现象一我们知道,重定向会改变缓冲区的刷新方式。
若没有重定向printf( )函数的数据直接行刷新到OS的缓冲区,在最后fork()创建子进程共享父子进程代码后可以一起退出。
若有重定向,刷新方式更改为全刷新了,数据滞留在用户级缓冲区中,经过fork()函数创建子进程后,进程想要退出那么就要刷新缓冲区,若父进程缓冲区先刷新,那么父进程的代码和数据会发生改变,子进程就会发生写时拷贝,拷贝缓冲区数据,之后自己退出时也刷新一遍缓冲区。子进程先刷新同理。所以缓冲区内数据会被刷新两边,也就打印了六条hello world

stdout, stdin, cin, cout, iostream, fstream 类都会包含缓冲区 ,std::endl 可以帮助我们刷新缓冲区

Linux EXT文件系统

磁盘是我们计算机中的一个机械设备 {例外:SSD,FLASH卡,usb) 用于存储文件
磁盘存储原理
在这里插入图片描述
我们可以给磁盘的每一个扇区编号,然后对这些编号进行某种函数操作就可以拆分成扇面,磁道,扇区等信息,传给磁盘就可以找到需要修改的区域.

磁盘的映射关系就是逻辑区块地址(Logical Block Address, LBA)是描述计算机存储设备上数据所在区块的通用机制,一般用在像硬盘这样的辅助记忆设备。就像虚拟内存和物理内存的映射关系一样

磁盘的管理
在这里插入图片描述
在这里插入图片描述

//那么下面着一些指令站在操作系统角度,其做了什么呢?
[clx@VM-20-6-centos test]$ touch hello.c
[clx@VM-20-6-centos test]$ echo hello world > hello.c
[clx@VM-20-6-centos test]$ cat hello.c
hello world
[clx@VM-20-6-centos test]$ rm hello.c

touch hello.c 创建一个文件,首先通过文件的文件名给新文件生成一个inode值,然后打开在当前目录的
文件内容查询是否有inode,若没有则建立一个新的映射关系存储到目录文件内容中。然后根据这个inode在
块组inode bitmap 对应位置设置成1,并分配Data block存储内容,分配数据块对应位图位置设置为1

echo hello world > hello.c 在当前目录内容中通过文件名查找hello.c对应的inode值,进入存储该
inode的块组找到文件的属性信息,通过属性信息进入放文件内容的Data block,若数据较大则分配更多的Data
block,并将编号传给文件的属性和位图

cat hello.c 在当前目录中查找对应inode值,通过inode找到文件属性信息,进而找到文件内容将其打印

rm hello.c 在当前目录找到该文件inode值,通过inode找到块组的inode bitmap,将对应位置设置成0,
然后通过文件属性信息将其分配的Data block编号取出,将Block的位图中将对应数据设置成0,删除目录中
的映射关系

所以在Linux上将文件删除后,文件的属性,内容数据任然存在,只需要将inode对应的两个位图中有效位置设置成1即可恢复文件

软链接和硬链接

链接的价值:帮助我们快速找到深层目录中的文件

软链接:ln -s 路径1/文件名1 路径2/文件名2

[clx@VM-20-6-centos test]$ ln -s ./hello/world/linux/a.out my_exe  //建立软链接
若省略路径则默认当前路径下,若省略路径2/文件名2 则会在当前目录下创建一个和文件名1相同名字的文件
//软链接使用方法
[clx@VM-20-6-centos test]$ ll
total 0
[clx@VM-20-6-centos test]$ mkdir -p hello/world/linux //创建一个深层目录
[clx@VM-20-6-centos test]$ cd hello/world/linux/      //来到深层目录下写一个可执行程序
[clx@VM-20-6-centos linux]$ touch test.c
[clx@VM-20-6-centos linux]$ vim test.c
[clx@VM-20-6-centos linux]$ cat test.c
#include <stdio.h>
int main()
{
    
    
  printf("hello world\n");
  return 0;
}
[clx@VM-20-6-centos linux]$ gcc test.c 
[clx@VM-20-6-centos linux]$ ll
total 16
-rwxrwxr-x 1 clx clx 8360 Oct 19 22:52 a.out        //生成可执行程序成功
-rw-rw-r-- 1 clx clx   73 Oct 19 22:51 test.c
[clx@VM-20-6-centos linux]$ cd ../../..             //回退到深度较浅的目录中
[clx@VM-20-6-centos test]$ ll
total 4
drwxrwxr-x 3 clx clx 4096 Oct 19 22:50 hello
[clx@VM-20-6-centos test]$ ln -s ./hello/world/linux/a.out my_exe  //建立软链接
[clx@VM-20-6-centos test]$ ll
total 4
drwxrwxr-x 3 clx clx 4096 Oct 19 22:50 hello
lrwxrwxrwx 1 clx clx   25 Oct 19 22:55 my_exe -> ./hello/world/linux/a.out
[clx@VM-20-6-centos test]$ ./my_exe            //运行my_exe <=>运行a.out程序
hello world

硬链接:ln 路径1/文件名1 路径2/文件名2

[clx@VM-20-6-centos use_lib]$ ln hello/world/linux/a.out a.exe   //创建a.exe 和深目录中的a.out进行硬链接
//硬链接使用方法
[clx@VM-20-6-centos use_lib]$ mkdir -p hello/world/linux/  //创建一个深目录
[clx@VM-20-6-centos use_lib]$ cd hello/world/linux/ 
[clx@VM-20-6-centos linux]$ ll
total 0
[clx@VM-20-6-centos linux]$ touch hello.c
[clx@VM-20-6-centos linux]$ vim hello.c
[clx@VM-20-6-centos linux]$ cat hello.c                    //深目录中有一个可执行文件
#include <stdio.h>
int main()
{
    
    
  printf("hello world\n");
  return 0;
}
[clx@VM-20-6-centos linux]$ ll
total 4
-rw-rw-r-- 1 clx clx 73 Oct 20 09:50 hello.c
[clx@VM-20-6-centos linux]$ gcc hello.c
[clx@VM-20-6-centos linux]$ ll
total 16
-rwxrwxr-x 1 clx clx 8360 Oct 20 09:50 a.out
-rw-rw-r-- 1 clx clx   73 Oct 20 09:50 hello.c
[clx@VM-20-6-centos linux]$ cd ../../..                  //回退到较浅目录中
[clx@VM-20-6-centos use_lib]$ ll
total 4
drwxrwxr-x 3 clx clx 4096 Oct 20 09:49 hello
[clx@VM-20-6-centos use_lib]$ ln hello/world/linux/a.out a.exe   //创建a.exe 和深目录中的a.out进行硬链接
[clx@VM-20-6-centos use_lib]$ ll
total 16
-rwxrwxr-x 2 clx clx 8360 Oct 20 09:50 a.exe
drwxrwxr-x 3 clx clx 4096 Oct 20 09:49 hello
[clx@VM-20-6-centos use_lib]$ ./a.exe                   //运行a.exe <=> 运行a.out
hello world
[clx@VM-20-6-centos use_lib]$ 

软硬链接的区别
在这里插入图片描述
软链接是有自己独有的inode的!软链接是一个独立的文件,有自己的文件属性也有自己的数据块(保存指向的文件的所在路径和文件名称)
硬链接本质不是一个独立的文件,而是一个文件名和inode编号的映射关系,没有自己的inode

文件的重要属性

指令 stat +文件名称 可以查看文件的一些基本属性信息在这里插入图片描述**Modify和Change两个时间都是实时修改的,对文件内容修改也可能会引起文件属性的变化,比如内容增多文件的大小就会变大。因为文件被访问的次数会比修改的次数出现的更加频繁,为了避免频繁的写入操作系统对Access的修改会有所延迟,每次刷新时间在内核中更新,然后定时刷新到文件中

Makefile 执行原理
一个test.c文件经过make指令后生成可执行程序test,那么test.c的Modify和Change一定是比test的更早的,如果test.c文件经过了重写,属性或者内容有变化,那么其的Modefy和Change就会更新到当前时间,比之前生成的test的Modify和Change时间要晚,那么Makefile 就可以根据这两个时间来判断两次make之间是否对test.c进行修改,若没进行操作就不再浪费时间进行编译

[clx@VM-20-6-centos use_lib]$ stat test.c   
  File: ‘test.c’
  Size: 73        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 918300      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/     clx)   Gid: ( 1001/     clx)
Access: 2022-10-20 10:40:42.572378712 +0800
Modify: 2022-10-20 10:40:41.604374881 +0800
Change: 2022-10-20 10:40:41.604374881 +0800
 Birth: -
[clx@VM-20-6-centos use_lib]$ make
gcc -o test test.c 
[clx@VM-20-6-centos use_lib]$ stat test
  File: ‘test’
  Size: 8360      	Blocks: 24         IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 918295      Links: 1
Access: (0775/-rwxrwxr-x)  Uid: ( 1001/     clx)   Gid: ( 1001/     clx)
Access: 2022-10-20 10:41:14.562505354 +0800
Modify: 2022-10-20 10:41:13.398500746 +0800
Change: 2022-10-20 10:41:13.398500746 +0800
 Birth: -
[clx@VM-20-6-centos use_lib]$ stat test.c
  File: ‘test.c’
  Size: 73        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 918300      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/     clx)   Gid: ( 1001/     clx)
Access: 2022-10-20 10:40:42.572378712 +0800
Modify: 2022-10-20 10:40:41.604374881 +0800
Change: 2022-10-20 10:40:41.604374881 +0800
 Birth: -

动态库和静态库

库的基本概念
一般库分为动态库和静态库(它们都是文件)
在Linux中, 如果是动态库:库文件是以.so 为后缀的
如果是静态库:库文件是以.a作为后缀的
库文件的命名: lib(前缀) + XXX(库名称) + .so/.a(后缀) + …
库的真实名字:去掉前缀和后缀,剩下来的就是库名称

库的组成
库由三部分组成: 1. .h文件(告诉我们库的接口以及调用方法) 2.库(二进制文件,由.c 文件处理成.o文件然后制作)3.文档

为什么要定义和声明分离呢?
因为我们的库是二进制文件,人无法读取其中细节,只能通过.h文件了解其中有什么方法和怎么调用方法,但是无法知道方法的具体实现细节。声明定义分离很好的做到了封装的效果。如果无需保密,则也可以定义到同一个文件中,比如一些开源软件的源码

安装静态库
一般的服务器可能没有内置的静态库,而只有动态库,静态库需要我们自行下载

sudo yum install glibc-static libstdc++-static

查看链接信息
指令file + 可执行程序名称 就可以得到一部分文件属性,其中包含链接信息
gcc编译器默认使用动态链接,但是我们可以通过-static 选项手动选择使用静态链接
在这里插入图片描述

可以观察到动态链接生成的可执行程序明显比静态链接生成的小很多

查看程序使用的动态库
指令:ldd + 可执行程序名称
ldd 命令可以列出可执行程序依赖的动态库和动态库的路径

[clx@VM-20-6-centos use_lib]$ ldd test                  //动态链接程序使用的库
	linux-vdso.so.1 =>  (0x00007ffe06b63000)
	/$LIB/libonion.so => /lib64/libonion.so (0x00007f403050a000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f4030023000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007f402fe1f000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f40303f1000)
[clx@VM-20-6-centos use_lib]$ ldd test_static           //静态链接程序没有使用动态库
	not a dynamic executable

静态库的实现

指令: ar -rc 生成静态库

[clx@VM-20-6-centos lesson_10_20]$ ll
total 12
drwxrwxr-x 3 clx clx 4096 Oct 20 18:56 creat_lib        //在这里创建lib
drwxrwxr-x 3 clx clx 4096 Oct 20 19:08 test_mylib       //对生成lib进行测试
drwxrwxr-x 2 clx clx 4096 Oct 20 11:02 use_lib
[clx@VM-20-6-centos lesson_10_20]$ cd creat_lib
total 24
-rw-rw-r-- 1 clx clx   60 Oct 20 16:45 add.c
-rw-rw-r-- 1 clx clx   57 Oct 20 16:45 add.h
-rw-rw-r-- 1 clx clx  185 Oct 20 18:55 Makefile
drwxrwxr-x 2 clx clx 4096 Oct 20 18:56 output
-rw-rw-r-- 1 clx clx   60 Oct 20 16:46 sub.c
-rw-rw-r-- 1 clx clx   57 Oct 20 16:46 sub.h

#pragma once 
#include <stdio.h>

int add(int a, int b);
[clx@VM-20-6-centos creat_lib]$ cat add.c
#include "add.h"

int add(int a, int b)
{
    
    
  return a + b;
}
[clx@VM-20-6-centos creat_lib]$ cat sub.c
#include "sub.h"

int sub(int a, int b)
{
    
    
  return a - b;
}
[clx@VM-20-6-centos creat_lib]$ cat sub.h
#pragma once 
#include <stdio.h>

int sub(int a, int b);

[clx@VM-20-6-centos creat_lib]$ cat Makefile     //编辑的Makefile
libmy_ope.a:add.o sub.o                          //库是二进制文件,使用.o文件制作而成
	 ar -rc $@ $^                                // ar -rc 是制作静态库的指令
%.o:%.c                                          //使用.o 文件依赖 .c 文件
		gcc -c $<
.PHONY:clean
clean:
		rm -rf *.o libmy_ope.a
.PHONY:output                                    //输出库 库由二进制库和.h文件和文档组成
output:
		mkdir output 
		cp ./*.h output 
		cp libmy_ope.a output


[clx@VM-20-6-centos lesson_10_20]$ cd test_mylib  //进入test_mylib目录
[clx@VM-20-6-centos test_mylib]$ ll
total 0
[clx@VM-20-6-centos test_mylib]$ ll
total 0
[clx@VM-20-6-centos test_mylib]$ cp -r ../creat_lib/output .  //将我们制作的静态库拷贝过来
[clx@VM-20-6-centos test_mylib]$ mv output mylib              //改个名字
[clx@VM-20-6-centos test_mylib]$ ll
total 4
drwxrwxr-x 2 clx clx 4096 Oct 20 19:05 mylib
[clx@VM-20-6-centos test_mylib]$ touch test.c                 //创建一个test.c
[clx@VM-20-6-centos test_mylib]$ vim test.c
[clx@VM-20-6-centos test_mylib]$ cat test.c                   //写一个程序调用我们的库
#include "add.h"
#include "sub.h"

int main()
{
  printf("%d\n", add(20, 10));
  printf("%d\n", sub(20, 10));
  return 0;
}

[clx@VM-20-6-centos test_mylib]$ clear
[clx@VM-20-6-centos test_mylib]$ ll
total 8
drwxrwxr-x 2 clx clx 4096 Oct 20 19:05 mylib
-rw-rw-r-- 1 clx clx  124 Oct 20 19:07 test.c
[clx@VM-20-6-centos test_mylib]$ gcc test.c -I./mylib -L./mylib -l my_ope  //编译链接
//-I + 声明路径 -L + 库路径 -l + 库的真实名称
[clx@VM-20-6-centos test_mylib]$ ll
total 20
-rwxrwxr-x 1 clx clx 8472 Oct 20 19:08 a.out
drwxrwxr-x 2 clx clx 4096 Oct 20 19:05 mylib
-rw-rw-r-- 1 clx clx  124 Oct 20 19:07 test.c
[clx@VM-20-6-centos test_mylib]$ ./a.out              //调用成功
30
10
[clx@VM-20-6-centos test_mylib]$ touch Makefile       //为我们的测试编写Makefile
[clx@VM-20-6-centos test_mylib]$ vim Makefile
[clx@VM-20-6-centos test_mylib]$ cat Makefile 
test:test.c
		gcc -o $@ $^ -I./mylib -L./mylib -lmy_ope
.PHONY:clean
clean:
		rm -f test

以下是这个实验的重点

1.
libmy_ope.a:add.o sub.o                          //库是二进制文件,使用.o文件制作而成
	 ar -rc $@ $^                                // ar -rc 是制作静态库的指令
2.调用
[clx@VM-20-6-centos test_mylib]$ gcc test.c -I./mylib -L./mylib -l my_ope  //编译链接
//-I + 声明路径 -L + 库路径 -l + 库的真实名称

为什么我们之前写的代码,也用了库,为啥就没有指明这些选项呢?? 这是因为那些库,在系统的默认路径下: /lib64, /usr/lib,
/usr/include 编译器是可以识别这些路径的

我们当然可以通过指令将我们的库和头文件移动到系统默认路径下,但是这样的行为严重不推荐

动态库的实现

具体实现和静态库的实现非常相近,这里取最重要的两部分进行说明
1.gcc + -shared选项 制作动态库

//1.制作库
[clx@VM-20-6-centos creat_dynalib]$ cat Makefile
libmydyna.so:add.o sub.o
		gcc -shared -o $@ $^    gcc + -shared 选项制作动态库,使得对象可共享
%.o:%.c 
		gcc -fPIC -c $<              制作动态库的.c 生成.文件时附带fPIC选项
.PHONY:clean 
clean: 
		rm -f *.o libmydyna.so 
.PHONY:output
output:	
		mkdir mydynalib 
		cp *.h mydynalib 
		cp libmydyna.so mydynalib

加 fPIC 选项 加上fPIC 选项生成的动态库,显然是位置无关的,这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。
加了 fPIC 实现真正意义上的多个进程共享 so 文件。
对于不加 fPIC,则加载 so 文件时,需要对代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个 .so 文件代码段的进程在内核里都会生成这个 .so 文件代码段的 copy。每个 copy 都不一样,取决于这个 .so 文件代码段和数据段内存映射的位置。
但是不加 fPIC 编译的 so 文件的优点是加载速度比较快。

2.动态库的使用

test:test.c
		gcc -o $@ $^ -I./mydynalib -L./mydynalib -lmydyna  //调用我们的动态库
.PHONY:clean
clean:
		rm -f test

但是我们发现,和静态库相同的方式进行操作确实可以生成可执行文件,但是可执行文件运行就会报错,这是为什么呢?
原因是操作系统不知道我们库的地址,因为我们使用-I, -L选项传入的地址是供编译器使用的,程序编译好后想要运行就会变成一个进程,代码,数据,以及相关库需要加载到内存中但是加载器并不知道我们自己写的库在哪里,所以我们还需要配置环境变量来告诉它

[clx@VM-20-6-centos test_dynalib]$ ll
total 24
-rw-rw-r-- 1 clx clx   97 Oct 21 08:05 Makefile
drwxrwxr-x 2 clx clx 4096 Oct 21 08:04 mydynalib
-rwxrwxr-x 1 clx clx 8432 Oct 21 08:18 test
-rw-rw-r-- 1 clx clx  120 Oct 21 07:57 test.c
[clx@VM-20-6-centos test_dynalib]$ pwd
/home/clx/Lesson_Linux/22_10_lesson/lesson_10_20/test_dynalib
//载入我们的库路径到环境变量到LD_LIBRARY_PATH中
[clx@VM-20-6-centos test_dynalib]$ export  LD_LIBRARY_PATH=/home/clx/Lesson_Linux/22_10_lesson/lesson_10_20/test_dynalib/mydynalib

加载器会通过这个环境变量去对应目录下查找我们的库并将其加载到内存中
这样我们就可以调用自己的静态库让程序跑起来了

[clx@VM-20-6-centos test_dynalib]$ make
gcc -o test test.c -I./mydynalib -L./mydynalib -lmydyna
[clx@VM-20-6-centos test_dynalib]$ ./test
30
-10

环境变量会随着关机而重置,如果想要一直能够调用自己的库,需要将库和头文件放入系统文件配置中,并且进行系统配置。在初学情况下不太建议进行这样的操作,这里就不赘述了

猜你喜欢

转载自blog.csdn.net/m0_69442905/article/details/127387022