【Linux】基础IO(重难点之一)(学习复习兼顾)

🏠 大家好,我是 兔7 ,一位努力学习C++的博主~💬

🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀

🚀 如有不懂,可以随时向我提问,我会全力讲解~

🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!

🔥 你们的支持是我创作的动力!

🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!

🧸 人的心态决定姿态!

🚀 本文章CSDN首发!


目录

0. 前言

1. 回顾C文件接口

总结

2. 系统文件I/O

系统函数传参标志位

小知识:

2.1 open函数返回值

2.2 文件描述符fd

2.3  0 & 1 & 2

总结:

2.4 文件描述符的分配规则

2.5 重定向

小知识:

3. 上述原理概括

3.1 fd  and  buffer

3.2 重定向的原理

4. 拓展内容

5. 使用 dup2 系统调用

6. FILE

7. 理解文件系统

7.1 inode

7.2 理解链接

硬链接和软连接的区别:

8. 动态库和静态库

8.1 静态库

生成静态库

打包给别人使用

如何使用

8.2 动态库

生成动态库

打包给别人用

如何使用

8.3 总结

8.4 使用外部库


0. 前言

        此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

        大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

        感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

1. 回顾C文件接口

        首先带大家回顾一下C语言是如何调用文件接口的:

  1 #include<stdio.h>                                                                                                                                 
  2 
  3 int main()
  4 {
  5   FILE* fp = fopen("log.txt", "w");
  6   if(fp == NULL)
  7   {
  8     perror("fopen");
  9     return 1;
 10   }
 11 
 12   fclose(fp);
 13   return 0;
 14 }

FILE *fopen( const char *filename, const char *mode );

        首先第一个参数是文件的名称,第二个参数是打开文件的方式。

        这段代码就是以 'w' 的形式打开 log.txt 这个文件,如果 fp 为 NULL 证明打开失败,如果不为空就打开成功,然后进行操作,最后关闭文件。

        因为我前面讲过进程了,所以到这里大家可能就可以明白:所谓的打开文件,不是代码静态编译时打开的,一定是进程运行的时候打开的,也就是进程帮我们打开的,所以一个文件必然要和进程产生关联。

        那么在这里我就想问大家一个问题,什么叫 "当前路径" 呢?

接下来我们先运行这段代码:

        大家可以看到,因为是以 "w" 形式打开的,所以没有 log.txt 文件就会创建一个文件,而且是在 "当前路径" 进行创建的,那么 "当前路径" 是不是就是当前可执行程序所处的路径呢

我们继续来看:

         当我在上级目录下运行的时候,发现 log.txt 是在上级目录下创建的,而 c_file 里面反而没有,那么这说明了说明呢?

        "当前路径" 不是当前可执行程序所处的路径!!

        大家不要着急,接下来给大家看一个东西,大家尝试理解。

        休眠是为了方便观察右图的内容,然后发现在当前工作目录下生成了,当然,这个和可执行所在目录是相同的,那么接下来就在其他的工作目录下运行:

        可见当我在可执行目录的上级目录下运行,结果在上级目录下生成了 log.txt 文件,也就是在 cwd 这个当前工作目录下生成了 log.txt 文件,也就是说:

        "当前路径" 进程运行时所处的路径(当前工作目录)!!


        接下来再演示一下C中写入字符的方式:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6   FILE* fp = fopen("log.txt", "w");
  7   if(fp == NULL)
  8   {
  9     perror("fopen");
 10     return 1;
 11   }
 12 
 13   int count = 5;
 14   while(count)
 15   {
 16     fputs("hello world!\n", fp);
 17     count--;
 18   }
 19 
 20   fclose(fp);
 21   return 0;                                                                                                              
 22 }

        当然还有 fgetc fgets fwrite fread fseek ftell ... 那么既然演示了写,那就再演示一下读:

    4 int main()
    5 {
    6   FILE* fp = fopen("log.txt", "r");
    7   if(fp == NULL)
    8   {
    9     perror("fopen");
   10     return 1;
   11   }
   12 
   13   int count = 5;                                                                                                       
   14   char buffer[100];
   15   while(count)
   16   {
   17     fgets(buffer ,sizeof(buffer), fp);
   18     printf(buffer);
   19     count--;
   20   }
   21 
   22   //int count = 5;
   23   //while(count)
   24   //{
   25   //  fputs("hello world!\n", fp);
   26   //  count--;
   27   //}
   28 
   29   fclose(fp);
   30   return 0;
   31 }

        可见将 log.txt 中的内容读取出来了。


        前面我们说过,打开文件,一定是进程运行的时候打开的,那也就是说读、写、关闭都是进程完成的:

        我们可以看到stdout 和 stderr都可以从显示器上显示出来,它俩的差别我把文件描述符讲完再来讲它俩的区别。

        因为标准输入、标准输出、标准错误在C语言、C++、Java、python......中都有这个概念,也就是说这样的功能是操作系统(OS)提供的。

        既然读、写都展示了,再展示最后一个接口,追加:

        通过描述 a 和 w ,我们可以向到 ">"  ">>" 也就是重定向和追加重定向。

总结

  • 打开文件的方式
r   Open text file for reading.
    The stream is positioned at the beginning of the file.

r+  Open for reading and writing.
    The stream is positioned at the beginning of the file.

w   Truncate(缩短) file to zero length or create text file for writing.
    The stream is positioned at the beginning of the file.

w+  Open for reading and writing.
    The file is created if it does not exist, otherwise it is truncated.
    The stream is positioned at the beginning of the file.
a   Open for appending (writing at end of file).
    The file is created if it does not exist.
    The stream is positioned at the end of the file.
a+  Open for reading and appending (writing at end of file).
    The file is created if it does not exist. The initial file position
    for reading is at the beginning of the file,
    but output is always appended to the end of the file.

        打开文件的时候还可以加 b 类似于 rb ,是默认以二进制的方式打开,默认的情况下是文本式,还有就是 r+ ,是默认创建不创建文件的问题。

        如上,是C语言的文件相关操作,接下来就要进入正式内容啦~!

2. 系统文件I/O

        操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码。

        接下来我们先看看系统调用接口:

        通常来说,我们都用第二个open,因为它复杂,第一个参数跟fopen的第一个参数一样。第二个参数跟fopen的第二个参数形式上和用法上都有区别。第三个参数是打开这个文件时所给它赋予的默认权限。

pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

参数:
 O_RDONLY: 只读打开
 O_WRONLY: 只写打开
 O_RDWR : 读,写打开
 这三个常量,必须指定一个且只能指定一个
 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
 O_APPEND: 追加写
返回值:
 成功:新打开的文件描述符
 失败:-1

          当然这个返回值跟fopen就有很大的区别了:open的返回值如果成功会返回文件描述符,其中一定要区分一下,FILE* 是文件指针,是C语言的概念,而 file descriptor(文件描述符) 是系统级别的概念,会返回一个整数,如果打开失败,就会返回 -1 。

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 
  6 int main()
  7 {
  8   int fd = open("log.txt", O_WRONLY, 666);
  9 
 10   printf("fd: %d\n",fd);                                                                                                
 11   return 0;
 12 }

        我们看到,open 的返回值是 -1,也就是操作失败了,所以也就是说,O_WRONLY 只是写入,不具备创建的能力,所以,如果我们想要打开文件进行写入,而且是如果文件不存在就创建它,还需要或上一个选项 -> O_CREAT :

        由此我们可以看到创建出来了。所以其实 O_WRONLY 和 O_CREAT 组合起来就是C语言中 w 选项!它们是上下级的关系,因为它们是上下级的关系,所以 fopen 中肯定调用了 open 。

        有的小伙伴可能很好奇 O_WRONLY 和 O_CREAT 是什么东西,其实它们就是两种标志位,那么我们就先来谈谈:

系统函数传参标志位

        int 有32个bit位,故理论上可以传递32种标志位(有没有另说),但是我们要想并行传入两个、三个以上的标志位,那么我们用这个方式就是不方便的,所以操作系统是这么搞的:

// 二进制序列中,只有一个比特位是1
#define X ox1 // 0000 ... 0001
#define Y ox2 // 0000 ... 0010
#define Z ox4 // 0000 ... 0100

open(arg1, arg2 = X | Y, arg3)
{
    if(arg2 & X){
    // 如果为真就代表有这个选项
    // 如果为假...
    }

    if(arg2 & Y){
    // 如果为真就代表有这个选项
    // 如果为假...
    }

}

        用比特位的方式可以同时传递多种标志是一种方式,其实也就是C++中的位图 (bit map)(这个我以后会在C++中单独写一篇博客!)

        就相当于我们定义一组宏,应用传参上传宏,内部直接宏判断,然后就实现了用比特位的方式不断往参数中传递多种选项的一种方式。

        我们在Linux下来看看:

        所以我们现在也看到了,事实也就是我说的那样。

        其实我们在创建 log.txt 的时候,发现权限很诡异:

        但这其实是被改过的,因为这个进程在创建进程的时候,权限是受到我之前在讲命令中的权限的时候讲到过: umask 影响的。

         如果我们就想创建权限是 666 也是有办法的,还有这里我要说一点,其实仔细看的话,我们上面用的 666 用 umask 的 002 一抵消,其实是有问题的,正确的权限应该是 664 ,但是我们看到的不是如此,所以我们在用 open 创建文件的时候,权限应该加上一个 0 ,也就是写成 0666,和 umask 给的 0002 保持一致(这里提一嘴:前面的0是跟进程有关的):

        这样我们就创建出了 666 这样权限的文件了。

        其实我们可以看到 fd 的返回值是3,那么这个3又是什么呢?接下来进行测试创建多个文件进行打印:

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 
  6 int main()
  7 {
  8   umask(0);
  9 
 10   int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);
 11   printf("fd: %d\n",fd1);
 12   int fd2 = open("log.txt", O_WRONLY | O_CREAT, 0666);
 13   printf("fd: %d\n",fd2);
 14   int fd3 = open("log.txt", O_WRONLY | O_CREAT, 0666);
 15   printf("fd: %d\n",fd3);
 16   int fd4 = open("log.txt", O_WRONLY | O_CREAT, 0666);
 17   printf("fd: %d\n",fd4);
 18   int fd5 = open("log.txt", O_WRONLY | O_CREAT, 0666);
 19   printf("fd: %d\n",fd5);
 20                                                                                                                                                  
 21   return 0;
 22 }

        由此,我们可以看到规律,我们的文件描述符打印出来的是连续的 3、4、5、6、7,其中 -1 表示的是失败,那么 0、1、2 呢?

        我们前面讲过一个概念:C语言中默认会打开三个输入输出流,默认对应的文件类型是 FILE* ,我们也说过 FILE* 是C语言的概念,在底层对应的标准输入就是 0,标准输出就是 1.标准错误就是 2。换言之,0、1、2 被默认打开了所以再打开时,默认就从 3 开始打开。细心的小伙伴看到从 0 开始一直连续的数字,就想到了数组。

        其实!所谓的文件描述符,本质就是数组的下标!这个后面再说~

        接下来我们就要在实现在文件中写东西了,那么我们先来对比下C语言和底层的 write 和 read :

        因为 count 的位置基本都是要用 strlen 进行描述,那么 strlen() 中需要 +1 么?

        大家想一想, '\0' 是C语言的规定,C++有 '\0' 么?其实如果用C语言组成是有的,但是如果用 stl 中的 string 是没有的,所以其实 '\0' 是C语言的规定,也就是C认识 ‘\0’ ,而文件是不认的,那么我也给大家演示下吧:

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 
  8 int main()
  9 {
 10   umask(0);
 11 
 12   int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);
 13   if(fd1 < 0)
 14   {
 15     printf("open error!\n");
 16     return -1;
 17   }
 18   printf("fd: %d\n",fd1);
 19 
 20   int count = 5;
 21   const char* msg = "hello world!\n"; // message
 22   while(count)
 23   {                                                                                                                                              
 24     write(fd1, msg, strlen(msg));
 25     count--;
 26   }
 27 
 28   close(fd1);
 49   return 0;
 50 }

        我们可以看到,打印是正确的,如果 strlen 里 +1 的话:

        可能会出现上面两种情况,第一种是虽然打印了,但是会出现奇怪的字符,这就是因为不认识 ‘\0’ 而打印出的乱码,第二种则的直接吞掉了 ‘\n’ ,总之都很奇怪,所以是不需要 +1 的。

小知识:

        因为fopen、fclose ... 这些涉及到 IO 的接口在Linux中底层一定封装了 open、close ... 也就是调用了 open、close ... 。

        上面说到的默认打开 0、1、2 ,那么也就是说我们也可以直接用 write 直接在 1 中写,也就是直接写到屏幕文件中:

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 
  8 int main()
  9 {
 10   umask(0);
 11                                                                                                                                                  
 12   int fd1 = open("log.txt", O_RDONLY);
 13   if(fd1 < 0)
 14   {
 15     printf("open error!\n");
 16     return -1;
 17   }
 18   printf("fd: %d\n",fd1);
 19 
 20   char c;
 21   while(1)
 22   {
 23     ssize_t s = read(fd1, &c, 1);
 24     if(s <= 0)
 25       break;
 26 
 27     write(1, &c, 1); // 1是stdout
 28   }
 38   close(fd1);
 49   return 0;
 50 }

2.1 open函数返回值

        在认识返回值之前,先来回顾一下两个概念: 系统调用 和 库函数 (这两个我在前面都讲过)。

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

        系统调用接口和库函数的关系,一目了然。

        所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。 

2.2 文件描述符fd

        通过对open函数的学习,我们知道了文件描述符就是一个小整数。

2.3  0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器。

        接下来先将一下 磁盘文件内存文件

        那么 磁盘文件内存文件 有什么差别呢?

        那么这里其实是不是和 进程 和 程序 之间有点相似呢?程序和磁盘文件对应,进程和内存文件对应。

        接下来先重要以内存文件进行讲解(磁盘文件在文件系统的时候讲解)。

什么叫做进程创建时打开0、1、2?

        因为0叫做键盘,1和2叫做显示器,键盘、显示器都属于硬件,既然属于硬件就能从操作系统中识别到,既然能识别到就将它们当文件看,所以当一个进程启动的时候开打0、1、2 本质就是操作系统将键盘、显示器、显示器形成 struct file 默认就让 struct files_struct 中的0、1、2指向对应位置,至此就默认打开了。

总结:

        当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 *files, 指向一张表 files_struc ,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

        而且在同一个服务器上不同的终端打开的文件在磁盘中可以是同一个文件,但是在内存中是不同发 file ,因为是不同的进程。

2.4 文件描述符的分配规则

如果我们在打开多文件的时候,把 1 关掉,会怎么样呢?

         我们会发现什么都没有,因为 printf 用的就是 1 号文件描述符,把 1 关掉了当然就不能在显示器上打印啦~

如果我们在打开多文件的时候,把 0 关掉,会怎么样呢?

        我们会发现是从 0 开始的。

再同时关掉 2 呢?

        所以文件描述符的分配规则是:从最小的但是没有被使用的开始分配

2.5 重定向

        如果我们关闭 1 ,然后再对 1 进行写入呢?

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7                                                                                                                                                  
  8 int main()
  9 {
 10   umask(0);
 11   close(1);
 12   int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
 13   if(fd < 0){
 14     return -1;
 15   }
 16 
 17   write(1, "hello\n", 6);
 18   write(1, "hello\n", 6);
 19   write(1, "hello\n", 6);
 20   write(1, "hello\n", 6);
 21   write(1, "hello\n", 6);
 22 
 23   close(fd);
 61   return 0;
 62 }

        因为 close(1) ,此时再向 1 中写的时候,就直接写到了文件里,这个就叫做重定向

        之前我们说过,printf 中肯定包括 1 这个文件描述符,那么如图:

        我们会发现什么都没有!当时我都以为我猜到了结果,结果真是没想到!

        这里有一个概念,C语言它的数据并不是立马写到内存(操作系统)里,而是写到C语言的缓冲区里,所以我们需要刷新 stdout 。

        因为 printf 里也没有 stdout 啊,所以换一组接口:

        所以这就叫做重定向。

重定向本质是修改什么呢?

        重定向本质是修改文件描述符 fd 下标对应的 struct file* 的内容。

stdout 我们刚才说的就是跟 1 有关系:

        我们知道,虽然C语言的底层是调用了系统调用的,系统调用会得到一个 fd (文件描述符),但是C语言其实是不能直接使用 fd 的。

        所以其实表面上看 open 那的代码和 pirintf 那是代码是没有关系的,但在底层是有关系的!这个关系就是通过 fd 产生的关联,close(1) 的操作也就是输出重定向的原始原理。

fopen究竟在做什么?

  1. 给调用的用户申请 struct FILE 结构体变量,并返回地址(FILE*)。
  2. 在底层通过 open 打开文件,并返回 fd ,把 fd 填充进 FILE 变量中的 fileno 。

        所以之前的 fread fwrite fclose fputs fgets 在底层读写文件时都要转化为 fd 进行操作,只不过为了我们操作,它的返回值就是 FILE* ,但是 FILE* 可以找到 fd 。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 #include<sys/stat.h>
  5 #include<fcntl.h>
  6 
  7 int main()
  8 {
  9   close(0);
 10   umask(0);
 11   //int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
 12   int fd = open("log.txt", O_RDONLY);
 13   if(fd < 0){
 14     perror("open");
 15     return -1;
 16   }
 17                                                                                                                                                         
 18   // C  FILE*,stdout(FILE*)
 19   // stdout -> 1
 20   printf("hello printf\n");
 21   fprintf(stdout, "hello fprintf\n");
 22   fputs("hello fputs %d %f %c\n", stdout);
 23   fflush(stdout);
 24 
 25   char buffer[100];
 26   fgets(buffer, 100, stdin);
 27   printf("%s\n",buffer);
 28   close(fd);
 29   return 0;
 30 }

        本来应该是从标准输入里拿字符,但是我们将标准输入关闭了,那么也就从 log.txt 中读取字符串了,这也就是输入重定向

        当然我们之前还讲过 ">>" 这个是追加重定向,但其实这个和重定向关系不大,它和我们打开的方式有关系。

        我们看到,我们用了 O_APPEND 这个追加的操作,但还是没有写入到 log.txt 。

        其实 O_WRONLY | O_APPEND 才是C语言中的 a ,也就是追加重定向

小知识:

        凡是显示到显示器上面的内容,都是字符,凡是从键盘读取的内容都是字符。所以,键盘和显示器一般被称为 "字符设备" 。

        其实我们在输入6778的时候,显示出来的虽然是6778,但其实不是6778(六千七百七十八),其实是 6字符、7字符、7字符、8字符,所以被我们人识别的是6778(六千七百七十八),所以其实我们的 printf 是格式化输出,所谓的格式化输出,就是将 int double float,转成一个个字符显示出来,而 int a = 0 ; scanf("%d", &a);  输入:1234   是将 1字符、2字符、3字符、4字符按照 %d(整形) 写入到a变量中,也就是格式化输入

        其中格式化输入和格式化输出在转的时候是由 pinrtf 和 scanf 自己完成的,而其中转的依据就是 ASCII 表。

        所以:

        printf 和 fprintf 都自带格式化,而 fputs 打印出来的 %d %f %c 没有做任何解释,也就是说 fputs 将 %d %f %c 当作字符了。

3. 上述原理概括

3.1 fd  and  buffer

        所以 fd 本质就是数组的下标。

3.2 重定向的原理

        接下来用例子来演示一下这两个的区别:

         这一点也就证实了标准输出和标准错误都往显示器上打印,但是:

        我们可以看到,当我们重定向时,perror 和 fprintf(stderr) 是直接打印在了屏幕上,而 printf 和 fprintf(stdout) 则是打印到了文件中。

        换言之,其实这个 ">" 重定向的是 1号 文件描述符!2号 文件描述符没有被重定向,所以2号 文件描述符照样从显示器中打印,并不受重定向符号的影响。

        所以虽然它们都是从显示器上打,但本质上是不同的!

4. 拓展内容

        我们经常说 Linux 下一切皆文件,那么磁盘、显示器、网卡、键盘是文件么?从我们的直觉上来看,很明显不是文件啊。

        但是磁盘、显示器、网卡、键盘这些都是外设,我们曾经也讲过一个知识点就是冯诺依曼,所以我们系统和硬件沟通的时候,最终都是两种沟通,一个就是 I ,一个就是 O ,也就是 IO (intput && output)。

        如果有些小伙伴对多态有些陌生,那么我之前也写过一篇关于多态的文章,链接也就放在下面的,学过没学过的都可以去看看,写的很详细哦~

【C++】多态(C++重中之重)(学习与复习兼顾)_兔7的博客-CSDN博客

5. 使用 dup2 系统调用

        如果我们想重定向的时候,我们只是想让下标为 fd 的指针里的内容(指针)拷贝到另一个 fd 下标里的指针,这样就完成了重定向,所以其实我们没必要用 close() fclose() 进行关闭操作,所以我们在重定向时就可以用 dup2 。下面我们就来了解一下!

        首先肯定是 copy ,拷贝的是 fd_array[i] 里面的内容,不是 fd 。

        还是将 oldfd 拷贝到 newfd 里,也就是说最后的内容和 oldfd 一样,所以我们要想重定向的话,oldfd 里应该填 fd ,newfd 里应该填 1 。所谓的重定向也就是对 1 进行重定向。

        接下来就来进行操作:

        我们可以看到,成功了,所以以后我们用的时候直接就用 dup2 就好了。

        比方说如果用 fd 去覆盖 1 ,那么 1 有没有关,这就涉及到别的选项了,所以其实挺推荐我们写的时候也可以在 dup 前关闭 1 ,这样也就更明确些了。

        有的同学可能感觉,这么些的差别和我们上面的差别有什么区别,其实差别很大。

        我们这么写的意思就是我们在文件打开之后再进行重定向,而前面的方法只能是先 close ,然后再打开,这里就有个问题是我们必须要把 close 和 open 放在一起,要不然 close 的文件就可能被其他人用了。但是我们如果 dup 的话就可以指定的重定向。所以更推荐用 dup2 !

6. FILE

        因为这点内容我在前面已经讲过了,那么在这里我就再将之前讲过的内容进行总结一下吧。

  1. 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
  2. 所以C库当中的FILE结构体内部,必定封装了fd。
  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后。
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲

        综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的"上层", 是对系统调用的"封装",但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体:

typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
在/usr/include/libio.h
struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;
 int _fileno; //封装的文件描述符
#if 0
 int _blksize;
#else
 int _flags2;
#endif
 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];
 /* char* _save_gptr; char* _save_egptr; */
 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

         前面讲的知识相信大家都清楚,如果想再深入理解缓冲区的概念,那么我也是在这篇文章的基础上又写了一篇针对于缓冲区的文章,大家有兴趣可以去看看,链接我就放在下面了:

缓冲区的概念真的理解么?带你揭开缓冲区的面纱~_兔7的博客-CSDN博客


        还记得我们在最开始的时候不是模拟实现过自己的shell么?接下来,我们就用上面学的重定向来继续优化一下我们自己的shell!

        那么我就先把之前写的shell内容先给大家,但是这里就不讲了,大家如果有没看的可以去看看:

【Linux】Linux进程控制(学习复习兼顾)_兔7的博客-CSDN博客

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 #include<sys/wait.h>
    5 #include<sys/types.h>
    6 #include<string.h>
    7 
    8 #define LEN 1024
    9 #define NUM 32
   10 
   11 int main()
   12 {
   13   char cmd[LEN];
   14   char* myarg[NUM];
   15   while(1)
   16   {
   17     printf("[兔7@my-centos_mc dir]$ ");
   18     fgets(cmd, LEN, stdin);
   19     // 我们创建出来的子进程要执行命令(命令再cmd中)
   20     // 要执行命令就要将一个个命令拆开才可以调用
   21     // 所以要解析字符串
   22     //
   23     // 将最后一个命令的\n去掉(换成\0就行了)                                                                                                           
   24     cmd[strlen(cmd) - 1] = '\0';
   25     myarg[0] = strtok(cmd, " ");
   26     int i = 1;
   27     while(myarg[i] = strtok(NULL, " "))
   28     {
   29       i++;
   30     }
   31 
   32     pid_t id = fork();
   33     if(id == 0)//child
   34     {
   35       execvp(myarg[0], myarg);
   36       exit(-1);//随便写的                                                                                                                             
   37     }
   38     int status = 0;
   39     pid_t ret = waitpid(id, &status, 0);
   40     if(ret > 0)
   41     {
   42       printf("exit code: %d\n", WEXITSTATUS(status));
   43     }
   44   }
   45 
   46   return 0;
   47 }

        上面就是之前的代码,在我们进行分析前,我们需要知道要想重定向,就必须找到重定向的标志,如果是输出重定向也就是 ">" ,而且符号的左边一般是命令,右边是文件,所以我们就需要写一个判断来将命令和文件区分开:

 

        所以也就最后得到的结论就是创建子进程后,结构体内的内容都来自父进程的拷贝,所以理应也指向同一文件,然后我们经过分析:因为我们的目的就是为了创建进程而不是创建文件,所以其实文件是不需要拷贝的。

        理解了这里我们就该实现重定向了,我们知道,我们可以把目标文件打开,然后我们就可以对文件进行重定向,我们如果把父进程重定向了,那么子进程也是受影响的。换句话说,子进程是会继承父进程打开的文件信息的。

        我们都知道这样是可以的,但是其实我们是不推荐的,第一因为我们在(输出)重定向的时候,父进程也没干什么,它就是在等。第二是我们重定向是需要让子进程去执行命令,所以我们只需要对子进程进行重定向就好了,你重定向你的,别影响父进程。

        基于这两点,所以我决定将重定向的实现放在子进程里。

        最后得到的结论也就是:进程替换不会影响进程打开的文件信息

        通过上面的两个蓝色字体的结论,我们就可以进行写代码了:

        到这里我们的输出重定向就完成了,接下来就来使用一下:

        根据上面相同的方法,我们也可以完成追加重定向,无非就是在一个 ">" 后再加一个判断进行判断是不是两个 ">" 进而判断是不是追加重定向,然后只需要将如果没有文件该为创建这个选项改为追加选项就可以了。输入重定向也是再加一个判断是不是 "<" ,然后 dup2(fd, 0) ,也就是只要将内容重定向到标准输入就可以了,接下来我先演示结果,最后将代码给大家!

 

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 #include<sys/wait.h>
    5 #include<sys/types.h>
    6 #include<sys/stat.h>
    7 #include<fcntl.h>
    8 #include<string.h>
    9 #include<ctype.h>
   10 
   11 #define LEN 1024
   12 #define NUM 32
   13 
   14 int main()
   15 {
   16   // 为1是输出重定向
   17   // 为2是追加重定向
   18   // 为3是输入重定向
   19   int flag = 0;
   20   char cmd[LEN];
   21   char* myarg[NUM];
   22   while(1)
   23   {                                                                                                                                                   
   24     printf("[兔7@my-centos_mc dir]$ ");
   25     fgets(cmd, LEN, stdin);
   26     // 我们创建出来的子进程要执行命令(命令再cmd中)
   27     // 要执行命令就要将一个个命令拆开才可以调用
   28     // 所以要解析字符串
   29     //
   30     // 将最后一个命令的\n去掉(换成\0就行了)
   31     cmd[strlen(cmd) - 1] = '\0';
   32 
   33     char* start = cmd;
   34     while(*start != '\0')
   35     {
   36       if(*start == '>')                                                                                                                               
   37       {
   38         flag = 1;
   39         *start = '\0';
   40         start++;
   41         if(*start == '>')
   42         {
   43           flag = 2;
   44           start++;
   45         }
   46         break;
   47       }
   48       else if(*start == '<')
   49       {
   50         flag = 3;
   51         *start = '\0';
   52         start++;
   53         break;
   54       }
   55       start++;
   56     }
   57 
   58     if(*start != '\0')//不是\0后面就是目标文件
   59     {
   60       while(isspace(*start))                                                                                                                          
   61       {
   62         start++;
   63       }
   64     }
   65     else
   66     {
   67       start = NULL;
   68     }
   69 
   70     myarg[0] = strtok(cmd, " ");
   71     int i = 1;
   72     while(myarg[i] = strtok(NULL, " "))
   73     {
   74       i++;
   75     }
   76 
   77     pid_t id = fork();
   78     if(id == 0)//child
   79     {
   80       if(start != NULL)
   81       {
   82         if(flag == 1){
   83           int fd = open(start, O_WRONLY|O_CREAT, 0644);
   84           if(fd < 0)                                                                                                                                  
   85           {
   86             perror("'>'open");
   87             exit(-1);
   88           }
   89 
   90           dup2(fd, 1);
   91         }
   92         else if (flag == 2)
   93         {
   94           int fd = open(start, O_WRONLY|O_APPEND, 0644);
   95           if(fd < 0)
   96           {
   97             perror("'>>'open");
   98             exit(-1);
   99           }
  100 
  101           dup2(fd, 1);
  102         }
  103 
  104         else if(flag == 3)
  105         {
  106           int fd = open(start, O_RDONLY);
  107           if(fd < 0)
  108           {
  109             perror("'<'open");
  110             exit(-1);
  111           }
  112 
  113           dup2(fd, 0);
  114         }
  115         //我们这里没有必要去关闭文件
  116         //因为进程结束操作系统会完成清理工作
  117       }                                                                                                                                               
  118 
  119       execvp(myarg[0], myarg);
  120       exit(0);//随便写的
  121     }
  122     int status = 0;
  123     pid_t ret = waitpid(id, &status, 0);
  124     if(ret > 0)
  125     {
  126       printf("exit code: %d\n", WEXITSTATUS(status));
  127     }
  128   }
  129 
  130   return 0;
  131 }

7. 理解文件系统

        我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。

每行包含7列:

  • 模式
  • 硬链接数
  • 文件所有者
  • 大小
  • 最后修改时间
  • 文件名

        在我之前的文章中,其他地方的数字或者字符我都讲过哪个是哪个,除了现在这红方块中的一串 1 。那么接下来我就用这个数字来展开讲解。

        所以我们通过上面的讲解,我们差不多理解了一些 inode 的概念,那么接下来我们可能就能理解一个结论了:

        所以实际上,我们用 ls 操作打印的时候,我们打印的只是文件的属性信息,用 cat 操作打印的则是文件的内容信息。(当然不止这两个命令,我只是演示)

        过程就是 ls 这个命令变成进程,在用户空间的本质是:磁盘中的文件是由文件内容+属性的,也就是文件信息,所以当我们打印的时候也就是将文件信息或内容加载到操作系统,然后由操作系统再到用户!

        其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

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

         其中访问时间(accsee)的变化不是很容易观察到,而且其实也没有意义,只有当我们:

        因为不能存在同名文件,所以我们这里 touch test.c 也就相当于重命名了吧,这样才会更改。所以我们不管它,我们只看下面两个。

        在我们的外语水平里,这个 Modify 和 Change 都是相同的意思,都是改的意思,那么这里会有什么区别呢?

        所以当我们改内容的时候,属性基本上都会改,因为不管你怎么改,你的大小基本都会改变,所以只要改文件内容,文件属性就会改。

        那么有的小伙伴就有疑惑,既然都会改,那么为什么要设置两个呢。其实当 Change 改了, Modify 并不一定会改,比方说:

        所以这里我们就只改了文件属性,没有动内容,所以Change 改了, Modify 并不一定会改。


        首先我们知道磁盘是外设,提到外设大家肯定都能联想到:效率低,也能想到冯诺依曼体系结构:

        中间的就是磁盘,右边的图是磁盘的纵制图。

        还记得小时候上课时英语老师用的磁带么:

        因为磁带是以圆形卷起来的,但是其实我们可以把英语老师的磁带抽出来,变成线性的样子,也就是:圆形存储介质,可以看做成为线性存储介质

        也就是说,我们其实可以将磁盘看作线性结构!

7.1 inode

        为了能解释清楚inode我们先简单了解一下文件系统:

         Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下。
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  • i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等。
  • 数据区:存放文件内容。

         将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。

为了说明问题,我们将上图简化:

        为了理解这个图,接下来我再引入一下:

        那么我们现在就更能理解 ls 、 ls -l 、 cat 命令是再做什么了:

        那么创建文件的过程我们也就可以解释了:

         既然创建文件了,那么这个文件就得有它的 inode号,也就是从inode Bitmap中申请一个 inode号 ,然后从 inode Table 里的对应位置,把当前文件的 inode 属性填进去,然后如果有数据写进去了,那就在 Block Bitmap 中申请 block ,并且建立好 inode 和 block 之间的对应关系,然后将数据写进去,然后再将这个文件的文件名和 inode 号填写进当前目录的 inode Table 中。


创建一个新文件主要有一下4个操作:

  1. 存储属性
        内核先找到一个空闲的i节点(这里是790231)。内核把文件信息记录到其中。
  2. 存储数据
        该文件需要存储在三个磁盘块,内核找到空闲块:300,500,800(随便写的)。将内核缓冲区的第一块数据 复制到300,下一块复制到500,以此类推。
  3. 记录分配情况
        文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. 添加文件名到目录
        新的文件名 new.txt 。
        linux如何在当前的目录中记录这个文件?内核将入口(790231,new.txt )添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

7.2 理解链接

        首先我先来操作,让大家更容易理解软连接

        我们可以看到最后形成了软连接,而且我们可以看到 mytest 的 inode 是 790202 ,它的 inode 是 790235 ,也就是说,一个文件对应一个 inode ,也就是软连接形成的软连接文件本质是一个独立文件,也就是它有自己的 inode ,有自己对立的数据。而它数据的里面保存的是 mytest 对应的路径。这个东西特别想我们使用 windows 中的快捷方式

        所以:

        使用软连接的文件也照样可以跑 mytest 这个可执行程序!

        刚才我们创建的时候加 -s 创建的是软连接,如果我们不加则创建的就是硬链接

        这里我们就会发现,硬链接从 1 变为 2 了,而且原始的文件和硬链接它们的 inode 是一样的,而软连接的 inode 是独立的。

        所以其实硬链接的本质没有创建文件!只是建立了一个文件名和已有的 inode 的映射关系,并写入当前目录!(有点像C++中的取别名)

硬链接和软连接的区别:

  1. 软连接是一个独立的文件,有自己的 inode ,硬链接没有独立的 inode !
  2. 硬链接的本质没有创建文件!只是建立了一个文件名和已有的 inode 的映射关系,并写入当前目录!

        为了证明就是一个别名,我就先验证一下:

        我们可以看到,虽然删除了原始的可执行,但是通过硬链接的文件还是可以运行的,删除了 mytest 只是将一组映射关系删除了,但是不影响另一个。

        当然软连接不能用了,因为存的是 mytest 的路径, mytest 都没了,有路径也没用了。

        我们可以看到,我们通过硬链接也可以将 mytest "恢复"出来。

那么硬链接有什么用呢?

        我们可以看到,如果创建一个文件(新创建的,里面没有自己新创建其它的文件),那么它的硬链接数就是 2 了,其他的文件都是 1 。

        因为其它的文件在任何情况下,只有一组文件名和当前的 inode 有映射的关系。

        那 dir 文件呢?

        首先我们知道,我们在当前目录,然后 cd . 还是当前目录,所以我们可以看到,我们进入 dir 文件,可以看到 dir 中的 . 和 dir 文件的 inode 是相同的,所以,当前这个 . 就是 dir 这个目录的别名。

        所以当我们用 ./mytest 的时候,实际上就是运行的 hs/mytest ,hs里的 mytest,但是当然不能这么写,这么写是运行不起来的,只是在语义上进行证明。

        所以之所以是 2 ,是因为 dir 自己这个文件,和 dir 文件里的 . 。

        然后我们再来看一个现象:

        我们可以看到,为什么此时 dir 的硬链接变成 3 了呢?

        我们在返回上级目录的时候,是不是用cd .. 这个命令,那么 .. 这个命令是不是也存在于这个文件,所以:

        我们看到在 newdir 里的 .. 也是 790235 。

        最后总结一下,为什么 dir 的硬连接数是 3 呢:

        所以硬链接的作用就是:让我们在目录之间方面通过 . && .. 跳转(通过相对路径跳转)

        所以我们可以知道在 dir 这个目录下有多少子目录,因为 dir 算一个硬链接, dir 里的 . 也算一个,其他的就是子目录了。

        但是这里只是这里永远描述的都是相邻路径。


        到这里有一个问题:之前我们说过,一个文件对应一个 inode 那么我们如果对一个 inode 创建了很多的硬链接,当我们删除一个的时候,只是将这个文件名和 inode 的映射关系在这个目录的数据区删掉。那么目标文件(inode)什么时候被删掉呢?这个 inode 怎么知道有多少个文件名指向它呢(与它建立映射关系)?

        首先我们知道 inode 是一个结构体,在结构体中有一个变量就是计数器,如果有一个文件与这个 inode 建立映射关系,那么这个计数器就++,如果删除那么就 - - ,所以最后的这个计数器为几的时候,就有多少个不同的文件名指向它,所以当它减到 0 的时候,这个 inode 就会被清除掉,我们把这个计数器称之为引用计数

        而我们这里说的引用计数就是我们现在查看到的这里的硬连接数。所以我们创建一个文件的时候,这个文件默认的硬连接数就是 1 ,当我们删掉这个文件时候这个文件就真的被删掉了。


        还记得我们之前说过的权限么,我相信有的小伙伴当时在与文件操作的权限上的对应关系还是有点不理解,看看通过我们将到这里,会不会有一个新的认识呢。

权限:

  1. 进入一个目录需要什么权限? x
  2. 读取一个目录需要什么权限? r
  3. 在目录下创建文件需要什么权限? w

        x:所谓的进入目录就将我们当前的shell的进程的当前工作目录改成当前目录,而且进入目录就相当于运行了这个文件。

        r:我们要读取目录的时候,读取到的文件是不是都在当前目录的数据区存放呢?所以当我们 ls 打印文件名的时候,本质就是读取这个当前目录的数据区。既然是读取这个当前目录的数据区,那么就必须要有读权限。

        如果你要读取任何一个文件,你就必须知道这个文件的 inode ,但我们要读取这个文件需要知道这个 inode 之前还得先知道这个文件的文件名,所以我们需要根据文件名找到对应的 inode ,然后再找到它对应的属性。所以不管是读取这个文件的名字,还是读取文件的属性,都必须要有读权限。

        w:我们之所以能创建文件,本质上是在当前目录上建立了文件名和 inode 的映射关系,然后填到了当前目录的数据区,所以就必须要向当前目录写入,所以就必须要有写权限。

8. 动态库和静态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

        其实动静态库本质是可执行程序的"半成品"

        printf,scanf,abs ... ... 这些接口,使用者只要会用就可以,那么这些接口函数的具体实现在哪里呢?

        首先这些函数肯定是由代码写出来的,既然是代码,就必须要被编译,只要是要被编译,就要C程序中的文本代码翻译成可执行程序,也就是将文本翻译成二进制文件的过程。但是不是要将 printf,scanf,abs ... ... 这些函数形成可执行程序,而是要将这些函数编程基础模块供别人使用。

        其实我们在完成 printf 的时候不仅仅是因为我们调用了 printf ,其中还有我们的 Linux 在它的系统层面上编译程序时把库也接进来了。当然,我们也可以看一下:

        那么我这圈起来的文件都在哪里呢?当然我们可以找一找:

        第二行看到的不就是软连接么!当然链接的那个文件我们也可以找到,可以看到这个就是一个库文件,也就是汇编之后的目标文件(.o)。

        接下来就讲讲动静态库各自的特征

8.1 静态库

生成静态库

        生成静态库的工具是 ar 。

 

 

        这样我们就形成了静态库,如果上面的 Makefile 还是看不懂的话,那么其实和下面的操作是一样的:

打包给别人使用

        首先我们需要在上面的 Makefile 中增加:

        我们可以看到生成了一个 mathlib 的文件,里面就是这种形状:

        这个时候,我们只要将 mathlib 给别人,别人就可以使用了。

如何使用

        我们先写一个用来调用它的程序:

        当我们运行的时候,发现它没有找到 add.h 这个文件。

        至于 stdio.h 为什么可以找到,是因为这个 stdio.h 这个头文件就在系统的 /usr/include/stdio.h 这个库目录下,所以编译器是可以找的到的,而我们的 add.h 这个头文件是在我们当前目录下  include 里,是我们自定义的,所以它找不到,那么为了让其找到:

        我们可以看到,这里虽然还是报错了,但是报的不是没有找到 add.h 这个文件了,所以它是找到了的。

        只不过它还是说 my_add 是不存在的,因为我们没有告诉编译器 my_add 是一个方法,虽然头文件找到了,但是函数没有。因为头文件里只是声明了一下,具体的实现没有给。所以我们需要找到那个库!

        那为什么我们以前编译C的时候没指定呢?

        是因为C库本身就是在对应路径下呢,这些路径都是系统路径,编译器是可以找到的,而我们写的,编译器是找不到的,所以需要加上。

         我们发现还是没还是报这个错误,其实是因为虽然我们这里的 mathlib/lib 里只有一个库,但是如果有多个呢?那么到底链接哪一个就是问题了,所以我们必须要指定链接哪一个库!前面说过,库的名字是去掉前缀,去掉后缀,剩下的就是库名!

        我们发现,最后就生成了 a.out 这个可执行程序。

        所以,我们把我们的库给别人,给别人的是一组头文件,一组 lib ,这个头文件里只包含了函数的声明。

        那么我们如果不想使用这么多选项也是可以的。

        我们之所以要使用这么多选项是因为我们自己实现的头文件和库没有在系统里,如果把我们的头文件和库拷贝到系统路径下,那么我们也就不需要带那些选项了:

        我们先通过上面两个命令进行拷贝到系统路径下,拷贝完后再运行,发现不是说的头文件的问题了,还是没有定义 my_add ,这个是因为虽然不用指定库路径和头文件路径了,因为已经再系统路径下了,但是我们还需要指定库名字,也就是还要带 -l 。

        至于为什么C语言不需要,是因为我们编译的是C语言,编译器默认找的就是C库了,所以它知道C库是什么,就默认编译了,但是这里它不知道,所以就是要带 -l 。

        所以我们刚才拷贝的过程其实就是安装库的过程

        比方说我们如果把静态库打包好了,如果想给别人的时候,然后再配上一个安装脚本,其实就是将文件拷贝到系统路径下就好了。


        当然,这样做还是很不好的,因为时间长了也不知道自己写的代码是什么样的,而且也不要污染别人的库,所以我们要将添加进去的删掉:

        注意不要删错,其实我们删除的过程就是卸载库的过程


8.2 动态库

生成动态库

         生成动态库就不用 ar 了,直接就 gcc 或者 g++ 。

  • shared: 表示生成共享库格式
  • fPIC:产生位置无关码(position independent code)
  • 库名规则:libxxx.so

        这里给大家解释一下产生位置无关码

        静态库是直接将代码拷贝到可执行程序里,加载到内存里,然后就可以直接在进程中使用了。

        而动态库则需要你的程序和库的程序产生二次交互的过程。那么你的程序一定要能找到对应的库,但是库呢在内存中加载到哪里,映射到哪个区域都是不确定的,所以我们一定要让库的地址产生与位置无关的地址,这样就可以在任何地方映射还是可以产生关联。

打包给别人用

        接下来就来进行操作:

        我们可以看到生成了对应的动态库。

        那么如果我们想打包动态库给别人用,也是需要给别人提供一组头文件和一个库文件,和静态库是一样的!

如何使用

        我们还是想静态库那样将头文件放到 include 里, libcal.so 放到 lib 里,然后再进行编译即可:

         我们可以发现,成功的形成了 mytest 这个可执行程序。

        但是动态库还是有点不一样,我们可以看到,如果我们直接 ./mytest 这个可执行程序,结果运行不起来。

        原因是:当加载这个动态库的时候,没有找到这个文件。

        因为我们前面的 -I -L -l 是在编译期间告诉编译器头文件和库在哪里以及是哪个库,但是当编译成功的时候可执行程序已经有了。

        当 ./mytest 的时候要将可执行变成进程,但是将可执行变成进程就一定要将当前代码加载到内存,但是加载到内存的时候,和它同步的关联的动态库找不到了,这次是操作系统找不到了。因为操作系统发现这个可执行依赖一个库,但是库不知道是哪个。

        当然也可以证明这个说法:

        所以就是上面说的,虽然在编译的时候告诉编译器了,但是在程序运行的时候已经和编译器没有关系了,这个时候是系统找不到了。

这里有三种方法:

  1. 将这个 libcal.so 这个库拷贝到系统路径下(不推荐)
  2. 在系统中做配置(ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新)
  3. 导出一个环境变量 LD_LIBRARY_PATH ,它代表的是程序运行时,动态查找库时所要搜索的路径。

        接下来我就用方法三来操作:

        我们导入环境变量后就发现在 LD_LIBRARY_PATH 中就能找到动态库了!

        此时的 ldd ,就可以发现 libcal.so 就能够找到了。

        找到之后就发现可以直接运行了!

8.3 总结

        其实上面的方法二也是可以使用的。

        我们可以看到这个配置文件里其实就是一个路径。

接下来我就做一下配置:

        我们发现,最后也成功的运行了。

        但其实后面用的最多的还是将库拷贝到目录下。

8.4 使用外部库

        系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses库)

        其实直接编译是可以跑的,没有出现要引入数学库这个概念,其原因是因为现在的头文件包含了编译器自动帮我们找到,所以没有问题。

        不过我们也可以自己找:

        可以看到这个 m 出来了。然后我们 ./a.out 是可以运行的。

         如上就是 基础IO 的所有知识,接下来要将 进程间通信 ,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

        再次感谢大家观看,感谢大家支持!

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NPC Online

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

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

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

打赏作者

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

抵扣说明:

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

余额充值