> 文档中心 > Part1—Linux基础

Part1—Linux基础

目录

一、初识Linux操作系统

1、常见名词

(1)POSIX

(2) Linux 内核

 (3)命令解析器

(4)硬链接计数

 (5)软连接

 二、gcc

1、gcc工作流程

 2、GCC常用命令

 3、多文件编程

4、gcc与g++

三、静态库和动态

静态库

动态库

动态库介绍

 生成动态链接库

 动态库的使用:

 解决动态库无法加载问题:

 库的工作原理:

 解决方案

 动态库/静态库优缺点


一、初识Linux操作系统

1、常见名词

(1)POSIX

POSIX是可移植操作系统接口(Portable Operating System Interface of UNIX)的缩写,POSIX标准定义了操作系统应该为应用程序提供的接口标准,是在各种UNIX操作系统上运行的软件的一系列API标准的总称

POSIX 标准是对 UNIX 操作系统的经验和实践的总结,对 操作系统调用的服务接口进行了标准化,保证所编制的应用程序在源代码一级可以在多种操作系统上进行移植

(2) Linux 内核

Linux 系统从应用角度来看,分为内核空间和用户空间两个部分。内核空间是 Linux 操作系统的主要部分,但是仅有内核的操作系统是不能完成用户任务的。丰富并且功能强大的应用程序包是一个操作系统成功的必要件。

Linux 的内核主要由 5 个子系统组成:进程调度、内存管理、虚拟文件系统、网络接口、进程间通信。

  1. 进程调度 SCHED
    • 进程调度指的是系统对进程的多种状态之间转换的策略。Linux 下的进程调度有 3 种策略:SCHED_OTHER、SCHED_FIFOSCHED_RR
      • SCHED_OTHER:分时调度策略(默认),是用于针对普通进程时间片轮转调度策略

      • SCHED_FIFO:实时调度策略,是针对运行的实时性要求比较高运行时间短的进程调度策略

      • SCHED_RR:实时调度策略,是针对实时性要求比较高、运行时间比较长的进程调度策略。

  2. 内存管理 MMU
    1. 内存管理是多个进程间的内存共享策略。在 Linux 中,内存管理主要说的是虚拟内存。每个进程的虚拟内存有不同的地址空间,多个进程的虚拟内存不会冲突。
  3. 虚拟文件系统 VFS
    1. 目前 Linux 下最常用的文件格式是 ext2 和 ext3。
  4. 网络接口
  5. 进程间通信
    1. Linux 下的进程间的通信方式主要有管道、信号、消息队列、共享内存和套接字等方法。

 (3)命令解析器

命令解析器在 Linux 操作系统中就是一个进程 (运行的应用程序), 它的名字叫做 bash 通常我们更习惯将其称之为 shell (即: sh)。

当用户打开一个终端窗口,并输入相关指令, 按回车键, 这时候命令解析器就开始工作了, 具体步骤如下:

  • 在 Linux 中有一个叫做 PATH 的环境变量,里边存储了一些系统目录 (windows也有, 叫 Path)
  • 命令解析器需要依次搜索 PATH 中的各个目录,检查这些目录中是否有用户输入的指令
    • 如果找到了,执行该目录下的可执行程序,用户输入的命令就被执行完毕了
    • 如果没有找到,继续搜索其他目录,最后还是没有找到,会提示命令找不到,因此无法被执行

(4)硬链接计数

语法: ln 源文件 硬链接文件的名字(可以带路径)

硬链接计数是一个整数,如果这个数为 N (N>=1),就说明在一个或者多个目录下共有 N 个文件,但是这 N 个文件并不占用多块磁盘空间,他们使用的是同一块磁盘空间。如果通过其中一个文件修改了磁盘数据,那么其他文件中的内容也就变了。

每当我们给给磁盘文件创建一个硬链接(使用 ln),磁盘上就会出现一个新的文件名,硬链接计数加 1,但是这新文件并不占用任何的磁盘空间,文件名还是映射到原来的磁盘地址上。

创建硬链接只是多了一个新的文件名, 拷贝文件不仅多了新的文件名在磁盘上数据也进行了拷贝。

目录是不允许创建硬链接的。

硬链接和软链接不同,它是通过文件名直接找对应的硬盘地址,而不是基于路径,因此 源文件使用相对路径即可,无需为其制定绝对路径。

 (5)软连接

软连接相当于 windows 中的快捷方式。

语法: ln -s 源文件路径 软链接文件的名字(可以带路径)

在创建软链接的时候, 命令中的 源文件路径建议使用绝对路径,这样才能保证创建出的软链接文件在任意目录中移动都可以访问到链接的那个源文件。

 二、gcc

1、gcc工作流程

GCC 编译器对程序的编译分为 4 个阶段:预处理(预编译)、编译和优化、汇编和链接。

GCC 的编译器可以将这 4 个步骤合并成一个。

  1.  预处理:在这个阶段主要做了三件事: 展开头文件 、宏替换 、去掉注释行;
    1. 这个阶段需要 GCC 调用预处理器来完成,最终得到的还是源文件,文本格式
  2. 编译:这个阶段需要 GCC 调用编译器对文件进行编译,最终得到一个汇编文件
  3. 汇编:这个阶段需要 GCC 调用汇编器对文件进行汇编,最终得到一个二进制文件(目标文件)
  4. 链接:这个阶段需要 GCC 调用链接器对程序需要调用的库进行链接,最终得到一个可执行的二进制文件

 2、GCC常用命令

-I directory (大写的 i)      :指定 include 包含文件的搜索目录( gcc *.c -o calc -I ./include)

-g                                    :在编译的时候,生成调试信息,该程序可以被调试器调试

-D                                   :在程序编译的时候,指定一个宏(gcc test.c -o app -D DEBUG)

-w                                   :不生成任何警告信息,不建议使用,有些时候警告就是错误

-Wall                              :生成所有警告信息

-On                                :n 的取值范围:0~3。

                                        编译器的优化选项的 4 个级别,-O0 表示没有优化,

                                        -O1 为缺省值,-O3 优化级别最高。

-l                                    :在程序编译的时候,指定使用的库

-L                                   :指定编译的时候,搜索的库的路径

-shared                          :生成共享目标文件。通常用在建立共享库时

-std                                :指定 C 方言,如:-std=c99,gcc 默认的方言是 GNU C

-o                                   : 指定生成的文件名(gcc test.c -o app)

 3、多文件编程

GCC 可以自动编译链接多个文件,不管是目标文件还是源文件,都可以使用同一个命令编译到一个可执行文件中。

# 假设string.h,string.c,main.c都在同一个目录。# 方式1:直接生成可执行程序 test$ gcc -o test string.c main.c# 方式2:先将源文件编成目标文件,然后进行链接得到可执行程序$ gcc –c string.c main.c$ gcc –o test string.o main.o# 运行可执行程序$ ./test

4、gcc与g++

  1. 在代码编译阶段(第二个阶段):
    1. 后缀为 .c 的,gcc 把它当作是 C 程序,而 g++ 当作是 C++ 程序

    2. 后缀为.cpp 的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些

    3. g++ 会调用 gcc,对于 C++ 代码,两者是等价的,也就是说 gcc 和 g++ 都可以编译 C/C++ 代码

  2. 链接阶段(最后一个阶段):
    1. gcc 和 g++ 都可以自动链接到标准 C 库
    2. g++ 可以自动链接到标准 C++ 库,gcc 如果要链接到标准 C++ 库需要加参数 -lstdc++
  3. 关于 __cplusplus 宏的定义
    1. g++ 会自动定义__cplusplus 宏,但是这个不影响它去编译 C 程序

    2. gcc 需要根据文件后缀判断是否需要定义 __cplusplus 宏 (规则参考第一条)

 

# 编译 c 程序$ gcc test.c -o test# 使用gcc$ g++ test.c -o test# 使用g++# 编译 c++ 程序$ g++ test.cpp -o test# 使用g++$ gcc test.cpp -lstdc++ -o test     # 使用gcc

三、静态库和动态库

        程序中调用的库有两种 静态库和动态库,不管是哪种库文件本质是还是源文件,只不过是二进制格式只有计算机能够识别,作为一个普通人就无能为力了。

        在项目中使用库一般有两个目的,一个是为了使程序更加简洁不需要在项目中维护太多的源文件,另一方面是为了源代码保密,毕竟不是所有人都想把自己编写的程序开源出来。

静态库

在 Linux 中,静态库由程序 ar 生成,现在静态库已经不像之前那么普遍了,这主要是由于程序都在使用动态库。关于静态库的命名规则如下:

  • Linux 中静态库 lib 作为前缀.a 作为后缀,中间是库的名字自己指定即可,即: libxxx.a
  • Windows 中静态库一般以 lib 作为前缀,以 lib 作为后缀,中间是库的名字需要自己指定,即: libxxx.lib

 生成静态链接库:

  1. 对源文件进行汇编操作 (使用参数 -c) ,得到二进制格式的目标文件 (.o 格式)。
  2. 通过 ar 工具将目标文件打包就可以得到静态库文件了 (libxxx.a)。

 使用 ar 工具创建静态库的时候需要三个参数:

  1. 参数c创建一个库,不管库是否存在,都将创建。
  2. 参数s创建目标文件索引,这在创建较大的库时能加快时间。
  3. 参数r:在库中插入模块 (替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
# 第一步# 执行如下操作, 默认生成二进制的 .o 文件# -c 参数位置没有要求$ gcc 源文件(*.c) -c# 第二步$ ar rcs 静态库的名字(libxxx.a) 原材料(*.o)# 第三步# 发布静态库1. 提供头文件 **.h2. 提供制作出来的静态库 libxxx.a

静态库的使用:

当我们得到了一个可用的静态库之后,需要将其放到一个目录中,然后根据得到的头文件编写测试代码,对静态库中的函数进行调用。

# 1. 首先拿到了发布的静态库`head.h` 和 `libcalc.a`# 2. 将静态库, 头文件, 测试程序放到一个目录中准备进行测试.├── head.h   # 函数声明├── libcalc.a# 函数定义(二进制格式)└── main.c   # 函数测试# 3. 编译测试程序 main.c$ gcc main.c -o app# 4. 编译的时候指定库信息-L: 指定库所在的目录(相对或者绝对路径)-l: 指定库的名字, 掐头(lib)去尾(.a) ==> calc# -L -l, 参数和参数值之间可以有空格, 也可以没有  -L./ -lcalc$ gcc main.c -o app -L ./ -l calc# 查看目录信息, 发现可执行程序已经生成了$ tree.├── app   # 生成的可执行程序├── head.h├── libcalc.a└── main.c

动态库

动态库介绍

动态链接库是程序运行时加载的库,当动态链接库正确部署之后运行的多个程序可以使用同一个加载到内存中的动态库,因此在 Linux 中动态链接库也可称之为共享库

动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的

 关于动态库的命名规则如下:

  1. 在 Linux 中动态库以 lib 作为前缀,以.so 作为后缀,中间是库的名字自己指定即可,即: libxxx.so
  2. 在 Windows 中动态库一般以 lib 作为前缀,以 dll 作为后缀,中间是库的名字需要自己指定,即: libxxx.dll

 生成动态链接库

  1. 生成动态链接库是直接使用 gcc 命令并且需要添加 -fPIC(-fpic) 以及 -shared 参数
  2. -fPIC 或 -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置
  3. -shared参数的作用是告诉编译器生成一个动态链接库。

 生成动态链接库的具体步骤如下:

  • (1)将源文件进行汇编操作,需要使用参数 -c, 还需要添加额外参数 -fpic /-fPIC,得到二进制目标文件(*.o)
# 得到若干个 .o文件$ gcc 源文件(*.c) -c -fpic
  • (2)将得到的.o 文件打包成动态库,还是使用 gcc, 使用参数 -shared 指定生成动态库 (位置没有要求)
$ gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)
  • (3)发布动态库和头文件
    # 发布 1. 提供头文件: xxx.h 2. 提供动态库: libxxx.so

例子:  

# 1. 将.c汇编得到.o, 需要额外的参数 -fpic/-fPIC,head.h在include目录$ gcc add.c div.c mult.c sub.c -c -fpic -I ./include/# 2. 将得到 .o 打包成动态库, 使用gcc , 参数 -shared$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so ├── include│   └── `head.h   ===> 和动态库一起发布├── `libcalc.so   ===> 生成的动态库├── main.c├── mult.c├── mult.o├── sub.c└── sub.o

 动态库的使用:

当我们得到了一个可用的动态库之后,需要将其放到一个目录中,然后根据得到的头文件编写测试代码,对动态库中的函数进行调用。

# 1. 拿到发布的动态库`head.h   libcalc.so# 2. 基于头文件编写测试程序, 测试动态库中提供的接口是否可用`main.c`# 示例目录:.├── head.h   ==> 函数声明├── libcalc.so      ==> 函数定义└── main.c   ==> 函数测试# 在编译的时候指定动态库相关的信息: 库的路径 -L, 库的名字 -l$ gcc main.c -o app -L./ -lcalc# 查看是否生成了可执行程序$ tree.├── app # 生成的可执行程序├── head.h├── libcalc.so└── main.c# 执行生成的可执行程序, 错误提示 ==> 可执行程序执行的时候找不到动态库# gcc 通过指定的动态库信息生成了可执行程序,但是可执行程序运行却提示无法加载到动态库。$ ./app ./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

 解决动态库无法加载问题:

 库的工作原理:

  1. 静态库如何被加载
    1. 在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中
    2. 当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。
  2. 动态库如何被加载
    1. 在程序编译的最后一个阶段也就是链接阶段
      1. 在 gcc 命令中虽然指定了库路径 (使用参数 -L ), 但是这个路径并没有记录到可执行程序中只是检查了这个路径下的库文件是否存在
      2. 同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字。
    2. 可执行程序被执行起来之后:
      1. 程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息
      2. 动态库中的函数在程序中被调用, 这个时候动态库才加载到内存,如果不被调用就不加载到内存
      3. 动态库的检测和内存加载操作都是由动态链接来完成的
        1. 动态链接器:动态链接器是一个独立于应用程序的进程属于操作系统,当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L 指定的路径。
        2. 那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
          1. 可执行文件内部的 DT_RPATH
          2. 系统的环境变量 LD_LIBRARY_PATH

          3. 系统动态库的缓存文件 /etc/ld.so.cache;

          4. 存储动态库 / 静态库的系统目录 /lib/, /usr/lib 等 

        3. 按照以上四个顺序,依次搜索,找到之后结束遍历,最终还是没找到,动态连接器就会提示动态库找不到的错误信息。

 解决方案

可执行程序生成之后,根据动态链接器的搜索路径,我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。

  1.  方案 1: 将库路径添加到环境变量 LD_LIBRARY_PATH
    1. 找到相关的配置文件
      用户级别: ~/.bashrc —> 设置对当前用户有效系统级别: /etc/profile —> 设置对所有用户有效
    2. 使用 vim 打开配置文件,在文件最后添加这样一句话
      # 自己把路径写进去就行了export LIBRARY_PATH=$LIBRARY_PATH:动态库的绝对路径
    • 让修改的配置文件生效:

      修改了用户级别的配置文件,关闭当前终端,打开一个新的终端配置就生效了

      修改了系统级别的配置文件,注销或关闭系统,再开机配置就生效了

      不想执行上边的操作,可以执行一个命令让配置重新被加载:

      # 修改的是哪一个就执行对应的那个命令
      # source 可以简写为一个 . , 作用是让文件内容被重新加载
      $ source ~/.bashrc          (. ~/.bashrc)
      $ source /etc/profile       (. /etc/profile)

  2. 方案 2: 更新 /etc/ld.so.cache 文件
    • 找到动态库所在的绝对路径(不包括库的名字)比如:/home/robin/Library/
    • 使用 vim 修改 /etc/ld.so.conf 这个文件,将上边的路径添加到文件中 (独自占一行)
    • 更新 /etc/ld.so.conf 中的数据到 /etc/ld.so.cache 中($ sudo ldconfig )
  3. 方案 3: 拷贝动态库文件到系统库目录 /lib/ 或者 /usr/lib 中 (或者将库的软链接文件放进去)
    • # 库拷贝sudo cp /xxx/xxx/libxxx.so /usr/lib# 创建软连接sudo ln -s /xxx/xxx/libxxx.so /usr/lib/libxxx.so

验证 :

在启动可执行程序之前,或者在设置了动态库路径之后,我们可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库,这个命令叫做 ldd

# 语法:$ ldd 可执行程序名# 举例:$ ldd applinux-vdso.so.1 =>  (0x00007ffe8fbd6000)    libcalc.so => /home/robin/Linux/3Day/calc/test/libcalc.so (0x00007f5d85dd4000)    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5d85a0a000)    /lib64/ld-linux-x86-64.so.2 (0x00007f5d85fd6000)  ==> 动态链接器, 操作系统提供

 动态库/静态库优缺点

静态库:

  1. 优点:
    1. ​​​​​​​静态库被打包到应用程序中加载速度快
    2. 发布程序无需提供静态库,移植方便
  2. 缺点:
    1. ​​​​​​​​​​​​​​相同的库文件数据可能在内存中被加载多份,消耗系统资源,浪费内存(例如,静态库占用1M,2000个这样的程序运行,差不多占用2GB的空间)
    2. 库文件更新需要重新编译项目文件,生成新的可执行程序,浪费时间。

动态库:

  1. 优点:
    1. ​​​​​​​​​​​​​​可实现不同进程间的资源共享(动态库在内存中只存在一份拷贝,避免了静态库浪费空间的问题)
    2. 动态库升级简单,只需要替换库文件无需重新编译应用程序
    3. 程序猿可以控制何时加载动态库,不调用库函数的话,动态库不会被加载
  2. 缺点:
    1. ​​​​​​​​​​​​​​加载速度比静态库慢,以现在计算机的性能可以忽略
    2. 发布程序需要提供依赖的动态库