Linux程序包管理

令人头疼的程序包管理….学习笔记在此.

来自未来的评论: 现在看来Linux的程序包管理是最轻松的, 比Mac, Windows好管理太多了

先扯扯别的

先来简单谈谈程序相关的一些小东西吧, 说一说为什么程序员写的代码可以在不同的操作系统上跑起来.

最底层当然就是硬件, 在往上一层是操作系统内核, 不同的操作系统的内核的操作是不同的, 同时也提供了不同的系统调用, 这样就会使得每一个系统自成一派, 为了解决这个问题, 在提供系统调用的基础上, 再封装一层接口, 使得程序员在写程序的时候, 只需要面向接口就好, 而不关心系统调用是什么样的, 这样当不同的操作系统的提供的接口都满足某个规范的时候就可以实现程序在不同的操作系统上都可以运行了.

这样的接口规范叫POSIXPortable OS 便携式操作系统. 后面的IX是为了贴合UNIX.LINUX这样的命名规范罢了.

这样程序在源代码的级别上就是一样的了, 事实上所谓源代码文件只不过是一个文本文件罢了, 在经过预处理, 编译器做编译,形成目标代码,这样就可以通过汇编器做汇编成为机器码. 现在仍然不能运行, 因为很多程序是依赖库的. 这样就会产生两个分支,

  • 静态编译:

  • 共享编译: *.so 共享对象

    最后一步, 链接.

Linux下的可执行程序是ELF格式的(这里说的是格式,不是扩展名), 而Windows下的可执行文件是EXE格式的.

因此, 一旦编译成二进制格式后, 程序将会变得不兼容, 因此说, 源代码级别是相同的.

Linux下查看一个二进制程序依赖哪些库的命令是: ldd

1
2
3
4
5
6
7
8
9
root@Ubuntu:~$ ldd /bin/ls
linux-vdso.so.1 => (0x00007fff4ee35000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f99b26f7000)
libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007f99b24ef000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f99b2126000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f99b1ee8000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f99b1ce4000)
/lib64/ld-linux-x86-64.so.2 (0x00005560ba8de000)
libattr.so.1 => /lib/x86_64-linux-gnu/libattr.so.1 (0x00007f99b1ade000)

除了熟悉的API(Application Programming Interface)外, 还有一个叫做ABI(Application Binary Interface).导致Linux下的程序在Windows下不能运行的原因就是这个ABI 不同. 程序在链接时就决定了是如何去寻找库的路径.

为了能跨平台的运行, 就有一种方法是库级别的虚拟化. 只要能解决库文件 以及文件格式的问题 就可以进行跨平台.

Linux –[Wine] – Windows

Windows – [Cywin] – Linux

在上一层, 为了实现跨平台, 那么就提供应用程序接口! 这个最典型的东西就是JavaJVM.


系统级别的开发:

  • C
  • C++
  • ….

应用级别的开发:

  • Java
  • Python
  • PHP
  • perl
  • ruby
  • ….

二进制程序的组成部分:

二进制文件, 库文件, 配置文件, 帮助文件

这些文件都要放在一些目录下, 但是问题就出现了, 一个二进制程序, 我是放在/bin下还是/sbin下,还是/usr/bin ? /usr/sbin ? /usr/local/bin ? /usr/local/sbin ?

想象这样的情况. 一个程序有20个二进制, 10个库文件, 4个配置文件等等…难道每一个都要手工指定吗?

事实上, 最清楚程序内容该放在那里的人应该就是作者了. 然而又有些用户希望能够自己制定位置. 所以就想到, 将整个程序打包.

这样在加上一个程序包管理器, 就可以实现较傻瓜化的安装.

程序包管理器的好处有以下几点:

  • 能够记录程序的安装位置
  • 卸载时能够追踪所有的目录, 不需要再记忆安装位置
  • 自动进行安装, 该放哪就放哪.
  • 可以方便的进行更新, 查询, 校验的操作.

目前相对最早并且还存在的程序包管理器是Debian的, 程序包的安装格式为deb 这个管理器叫做dpt.

接着redhat也推出了自己的包管理工具, 叫做rpm, 其程序包的格式也是rpm 使用perl开发.

目前, rpm已经成为Linux的工业标准, 使用C语言进行了重新开发.

程序包(RPM)

RPM is Package Manager.

每一个RPM包都是要拿到源代码的, 接着将源代码制作成的. 分两种: 普通的rpm包, 源码rpm

源码的命名格式是: name-VERSION.tar.gz

VERSION的构成: major.minor.release

主版本号不会轻易改变,除非是发生了翻天覆地的改变.

次版本号在添加了一个功能或者一些做了优化,以及功能的小改.

发行号也就是在修复一个Bug的时候会进行改变.

rpm包的命名基本和源码包的格式相同: name-VERSION-ARCH.rpm

VERSION会和源码包的版本号一致 而ARCH是为了指明平台而存在的, 其命名的格式如下:

release[这个是RPM的发行号,而不是程序的.].os.[el7…].arch[CPU架构,e.g: x86_64, i386, i686 …]

  • noarch 表示任何硬件架构都可以运行

如果一个程序的大部分功能是一般用户不需要的, 那么直接装上去的话, 会使得体积增大很多.

这个时候就想到进行拆包:

testapp-VERSION-ARCH.rpm 主包

testapp-devel-VERSION-ARCH.rpm 支包/子包

testapp-plugin-filter-VERSION-ARCH.rpm 子包的子包

支包是依赖于主包的, 因此在安装支包的时候, 应该先安装主包.

包之间, 存在依赖关系, 假设现在有, X, Y, Z 三个包.

X <- 依赖 <- Y <-依赖 <- Z

那么这样的话, 要安装 Z ,就要先安装 Y , 而安装 Y 就要先安装 X , 这样很有可能折腾一天, 连一个包都装不上.

为了解决这样的问题, 制作而成的程序包会有两个部分组成. 而整个程序包管理器也有两个部分组成.

程序的组成清单 :

由于rpm在展开前是看不到里面的内容的. 因此就把里面的内容记录在一个文件清单中.当想要获取rpm的内容时, 通过程序包管理器的接口访问这个文件就可以知道了. 这个文件清单每一个程序包独有.

除了清单文件, 还有一些安装和卸载的所用的脚本, 就比如说nginx在安装时是需要进行一次预配置的, 并且是以nginx的用户的身份来运行的, 如果系统中没有nginx用户, 那么脚本会自动的进行用户创建.

数据库(公共):

这个数据库中存放了程序包的名称和版本 依赖关系 功能说明 安装生成的文件路径以及校验码信息.

管理程序包

获取程序包

  • 光盘 : 最直接和最安全的方式就是使用光盘进行获取, 但是这样的程序包一般都是已经过时的.
  • 官方的FTP/HTTP文件服务器 : 虽然是经过验证并且是最较新的, 但是我们需要的包有可能官方不一定会收录进去.
  • 项目站点 : 第三方软件的的官方站点的软件包
  • 第三方组织 : 比如: Fedora-EPEL(最权威), REPL..等等 , 由社区组织进行验证.
  • 自己制作 : 算了吧. : )

一个可能会用到的网站: pkgs.org

CentOS的rpm

现在就来学习使用rpm来进行包的管理.

1
[root@WWW ~]$ man rpm

同大多数命令一样, rpm-v-vv参数, 是为了输出信息和大量调试信息. 先把这个说在最前面.

接着再说下-h, 这次不是显示帮助了, 而是以Hash的形式显示程序包的安装等等..的进度, 其实说白了就是用**#来画一个进度条. 一般来说, 一个#**表示2%的进度.

rpm的安装升级和卸载遵循相似的命令格式, 如下:

1
2
3
4
rpm {-i|--install} [install-options] PACKAGE_FILE ...
rpm {-U|--upgrade} [install-options] PACKAGE_FILE ...
rpm {-F|--freshen} [install-options] PACKAGE_FILE ...
rpm {-e|--erase} [erase-options] PACKAGE_NAME ...

使用RPM进行包安装时, 如果依赖关系检测失败, 则直接报错退出.

一些会用到的安装选项有:

1
2
3
4
5
6
7
8
9
10
11
[install-options]
--test: 测试安装, 但是不执行真正的安装过程; dry run模式
--nodeps: 忽略依赖关系, 使用这个选项并不是一个理智的选择, 但在某些场景(循环依赖)下, 这个选项倒成为了一种解决办法, 更好的方法是一起安装.
--replacepkgs: 进行替换安装, 简单地说就是重装
--nodigest: 不检查包的完整性
--nosignature: 不检查来源合法性
--noscripts: 不执行程序包的脚本: --no$
%pre: 安装前的脚本
%post: 安装后的脚本
%preun: 卸载前的脚本
%postun: 卸载后的脚本

对于升级, 有两种选项: 一个是**-U|–upgrade**, 一个是**-F|–freshen**, 使用前者进行更新的时候, 如果旧版本的程序包不存在, 而会直接进行安装, 而对于后者, 如果旧版的程序包不存在, 则什么都不会做.

注意:

(1) 不要对Linux的内核进行升级, Linux是支持多内核版本共存的. 因此,直接安装新版本内核就好.

(2) 如果原程序包的配置文件被修改过, 升级时, 新版的配置不会覆盖老版本的配置文件, 新版本的配置文件会得到重命名, 一般叫做FILENAME.rpmnew而保留下来

下面再来说说卸载. 在卸载是, 要先知道包的安装情况.

因此, 要先来谈谈包的查询.

1
2
3
4
5
6
7
8
9
10
11
12
[root@WWW ~]$ rpm {-q|query} [select-options] [query-options]
[select-options]
-a: 获取本机安装的所有软件包
-f: 查看指定的文件是由那个程序安装生成的.
[query-options]
--changelog: 查询rpm包的changlog.(更新日志)
-c: 列出该文件的软件包安装的所有的配置文件
-d: 列出该文件的软件包安装的所有的文档
-i: 输出该安装包的信息(几乎你所需要的常用信息都有, 很好用)
--scripts: 列出该安装包的所有脚本(4种, 如果有的话)
-l: 列出安装包安装的所有文件
-R: 查询指定程序包所依赖的CAPABILITY

行了, 现在就来卸载安装的软件吧.

1
2
[root@WWW ~]$ rpm {-e|--erase} [--allmatches] [--justdb] [--nodeps] [--noscripts] [--notriggers] [--test] PACKAGE_NAME ...
--nodeps 忽略依赖关系, 默认如果有包依赖待删除的包的话会拒绝删除.

删除软件包的细节不是很多.

rpm的另一个很重要的功能是进行软件包的校验.

1
2
3
4
5
6
[root@WWW ~]$ rpm {-V|--verify} [select-options] [verify-options]
[select-options]
# 上面说过啦~
[query-options]
# 校验一个软件就是将软件包中已安装的文件信息和rpm数据库中存储的文件元信息进行对比.
# 对比的信息有: 大小, 摘要, 权限, 类型, 所属等等..

现在我们来试一试, 修改一个刚刚装好的redis的文件.

1
2
3
4
5
6
7
8
9
10
[root@WWW ~]$ rpm -ql redis
/etc/logrotate.d/redis
...
/usr/share/doc/redis-3.2.3/README.md
...
/var/run/redis
# 就拿这个README开刀.
[root@WWW ~]$ echo "YOU'RE HACKED!!" >> /usr/share/doc/redis-3.2.3/README.md
[root@WWW ~]$ rpm -V redis
S.5....T. d /usr/share/doc/redis-3.2.3/README.md

输出了一段信息, 只要有信息就表示文件被修改过了. 前方的标记是指改变了哪些元素:

1
2
3
4
5
6
7
8
9
S file Size differs 文件的大小
M Mode differs (includes permissions and file type) 文件权限改变
5 digest (formerly MD5 sum) differs 文件内容改变
D Device major/minor number mismatch 设备文件主次设备号不匹配
L readLink(2) path mismatch # 不管他
U User ownership differs 所属用户改变
G Group ownership differs 所属组改变
T mTime differs 时间戳改变
P caPabilities differ

根据上面的结果, 在校验时可以手工指定不检查的项目, 和上面一样, 直接--noXXXX就好了.

那么, rpm是如何实现包的合法性验证以及完整性验证呢?

软件包的完整性检验是很简单的, 只要软件包的两次计算的特征码没有出现改变, 就说明该包是完整的.

说的明白点, 这个特征码其实就是作者在打包完成后进行的一次MD5计算, 并把这个计算得到的摘要值附在包的后面. 这样当以后检验的时候, 就使用同样的方法进行一次摘要计算.

但事实上, 这种方法并不是很好, 因为你并不能确定这个包后的MD5摘要是作者本人提供的.任何人都可以进行篡改. 所以现在的网站上采取的都是将正确的摘要计算结果放在页面上. 接着只要用户自己进行比对就好了, 这样的策略使得只有当对方网站遭到了入侵的时候才会变的不可靠.

但是这样也不是很好, 每次我下载完成文件都要手动去进行校验, 而且对方网站被入侵的可能性也是存在的.

那么有更好的方案吗?

简单的, 只要把最初附在包最后面的特征码进行一次加密就好了, 这样用户先进行解密就好了. 但是, 为了解密得到特征码, 必须先知道密码, 这样岂不是就会造成密码满天飞的情况了吗? 这里使用到了的技术就是熟悉的公钥加密技术, 是非对称加密的一种.

一般来说, 包的完整性检验是使用的SHA256,而来源合法性是通过RSA进行的加密.

如果手中已经有了密钥文件使用rpm --import < KEY_FILE >;

那么rpm的数据文件都保存在哪里呢? 之前说过/var/lib/是保存程序运行状态的目录, rpm也不例外,在/var/lib下有rpm的目录文件.

1
2
[root@WWW 17:22 [115]/var/lib/rpm]$ ls
Basenames Conflictname __db.001 __db.002 __db.003 Dirnames Group Installtid Name Obsoletename Packages Providename Requirename Sha1header Sigmd5 Triggername

这些其实就是rpm的数据文件, 从名字也可以看出来有: 软件包的依赖, 触发器, md5校验码等等…

这些就是rpm用来做缓存的., 如果在安装包或者卸载包的时候加了-vv的参数, 就看到rpm打开和关闭数据库的操作.

一旦数据文件遭到了破坏(因为是文件), 那么我们的查询种种操作就会失败, 这个时候就要进行数据库的重建.

两个指令:

1
2
3
4
5
[root@WWW ~]$ rpm {--initdb|--rebuilddb}
--initdb: 初始化
如果事先不存在数据库, 则会新建, 否则不会执行任何操作.
--rebuilddb: 重建
无论当前存在与否, 直接进行数据库的建立.

管理程序包工具YUM

yum(Yellowdog Update Modifier)是CentOS上的一个软件包安装的前端工具(C/S架构), 它的工作前提是需要联网, 其实需要连接一个拥有巨大空间的文件服务器,这个服务器中包含了大量的rpm包. 而这样的服务器在网络上有很多,因此需要在配置文件中指定yum要访问的服务器地址.

yum的工作原理简单说是这样的: 当用户要安装程序包的时候, yum接收到用户的指令, 这个时候YUM会尝试寻找本地指向的有程序包的文件服务主机的地址, 这个地址从配置文件中读取, 接着找到了之后, 而远方服务器会有仓库(repository)来存储程序包和一个描述信息(包之间的依赖关系, 包的版本等等信息), 接着yum从服务器上把这个文件下载下来, 放在自己的一个缓存区内.

接着读取这个文件中是否有用户请求的包的信息, 接着还会自动的解析这个文件中对于依赖关系的描述.(查询本地已安装过的包, 将依赖并且没有安装的包列出来) 接着就启动一个文件服务器的客户端, 向远方的服务器请求下载相应的包文件.

同样也是放在本地, 接着就进行安装, 安装完成后就会将这些缓存中的包删除. 但是这个元数据不会被删除. 如果每一次使用都要进行下载, 那岂不是很蠢? 但是如果服务器端的程序包进行了更新, 我怎么知道呢 ? 很简单, 远方服务器会有一个文件单独存放这个元数据文件的校验码.那么yum每次只要进行对这个校验码的请求就可以知道是否需要进行更新元数据文件.

在寻找远端服务器的时候, yum可以基于插件进行加载远端服务器的镜像, 这样就可以一个仓库, 执行多个服务器.yum会优先加载最近的服务器节点.

yum面临的最大的问题是, 程序包安装过程中, 如果发生错误而导致中断, 产生的后果将是不可逆的, 无法修复.因此现在的趋势开始导向dnf [ 不是地下城 :) ] 这个工具, 但是现在CentOS7官方还没有明确的宣布, 所以还是先来学习下yum, 但实际上, 他们两人的命令是很相像的.

进入yum的世界

yum的一个重要组成部分就是yum仓库(repository), 这个仓库中存储了大量的rpm包以及包的相关元数据信息文件(放置在特定的目录下: repodata) 这个目录的位置, 就是我们指定yum仓库的位置

yum支持的文件服务器有:

  • ftp://
  • http://
  • nfs://
  • file://

由于yum也是一个由安装包安装的软件, 所以我们也就可以用上面的rpm命令查看yum生成的相关文件:

1
2
3
4
[root@WWW ~]$ rpm -qc yum
/etc/logrotate.d/yum # 日志滚动文件
/etc/yum.conf # 主配置文件, 文件内引用了 /etc/yum.repos.d/*.repo
/etc/yum/version-groups.conf # 跟yum本身的关系不大

yum的主配置文件/etc/yum.conf包含了/etc/yum.repos.d/*.repo. 大的/etc/yum.conf配置了yum所有仓库的公共配置, 而每一个*.repo文件就是配置的每一个仓库的地址等信息. 一个yum是可以指定多个仓库的, 在安装时会自动从这些仓库中选择出最新的版本.

下面来解析一下yum的公共配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
[main]
cachedir=/var/cache/yum/$basearch/$releasever
keepcache=0 # 是否保存缓存
debuglevel=2
logfile=/var/log/yum.log # 日志记录的位置
exactarch=1 # 是否精确严格匹配硬件架构
obsoletes=1 # 用于更新的处理逻辑
gpgcheck=1 # 是否进行验证检查
plugins=1 # 是否启用插件
installonly_limit=5 # 最大的并行安装数
bugtracker_url=http://bugs.centos.org/set_project.php?project_id=23&ref=http://bugs.centos.org/bug_report_page.php?category=yum
distroverpkg=centos-release # 当前的发行版

很简单吧, 其实这个配置文件时有手册的, yum还支持很多选项的, 例如ssl验证, 代理隧道的配置, 很多.

而每一个仓库自己的配置文件长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[epel] # 这里的id应该是唯一标识
name=EPEL for redhat/centos $releasever - $basearch # 仓库名称*
baseurl=http://mirrors.tencentyun.com/epel/$releasever/$basearch/ # 访问路径* 指向repodata
failovermethod=priority # 故障转移方法: 多个URL下, 如果坏掉了, 怎么处理.
# 两种选项: roundrobin 和 priority
# roundrobin 虽说是轮询吧, 但其实是失败后进行随机的选择, 默认值
# priority 按照优先级进行选择
enabled=1 # 是否启用本仓库 (好蠢的选项.
gpgcheck=1 # 是否进行检查来源和合法性.
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 # 提供一个url, 指向一个密钥文件
# 加星号的两个是最重要的,其他的都是不太重要的可选项 注意=号两侧一定不能出现空格, 否则可能会出现迷之错误
# baseurl是可以指定多个的, 之前说过
# 还有一个重要的配置叫做mirrorlist, 这个指向一个文件, 指向的文件包含了一个url列表, 其实就是一大把baseurl.
# cost 也有可能会用到, 默认是1000 表示开销.

随便找一个镜像站点(阿里云,网易,各种高校), 可以很轻易的找到repodata目录文件, 原来是xml啊, 看过你就知道我在说什么了, 嘻嘻 : )

之前说我觉得这个enable选项是个很蠢的选项, 其实不是, 因为你可以在一个文件中指定多个repo, 这时这个选项就派上用场了.

yum的基本使用(1)

yum的命令格式是下面这样:

1
[root@WWW ~]$ yum [options] [command] [package ...]

yum也是有很多的子命令以实现不同的管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
* install package1 [package2] [...] # 安装包
* update [package1] [package2] [...]
* update-to [package1] [package2] [...]
* update-minimal [package1] [package2] [...]
* check-update
* upgrade [package1] [package2] [...]
* upgrade-to [package1] [package2] [...]
* distribution-synchronization [package1] [package2] [...]
* remove | erase package1 [package2] [...]
* autoremove [package1] [...]
* list [...] # 列出所有的程序包, 凡是最后一栏加上@符号的, 就是已经安装过的.anaconda表示在系统安装的时候就安装的包
* available
* updates
* installed
* info [...]
* provides | whatprovides feature1 [feature2] [...]
* clean [ packages | metadata | expire-cache | rpmdb | plugins | all ]
* makecache [fast]
* groups [...]
* search string1 [string2] [...]
* shell [filename]
* reinstall package1 [package2] [...]
* downgrade package1 [package2] [...]
* deplist package1 [package2] [...]
* repolist [all|enabled|disabled]
* repoinfo [all|enabled|disabled]
* repository-packages <enabled-repoid> <install|remove|remove-or-reinstall|remove-or-distribution-synchronization> [package2] [...]
* version [ all | installed | available | group-* | nogroups* | grouplist | groupinfo ]
* history [info|list|packages-list|packages-info|summary|addon-info|redo|undo|rollback|new|sync|stats]
* load-transaction [txfile]
* updateinfo [summary | list | info | remove-pkgs-ts | exclude-updates | exclude-all | check-running-kernel]
* fssnapshot [summary | list | have-space | create | delete]
* fs [filters | refilter | refilter-cleanup | du]
* check

这些命令…其实都有可能会用到…我尽量用简洁的语言描述一下//.

列出信息类的命令有这些:

1
2
3
4
5
6
[root@WWW ~]$ yum repolist [all|enabled|disabled] # 列出目前的仓库. 建议加上all参数, 这样可以看到每一个仓库的启用状态. 
[root@WWW ~]$ yum repoinfo [all|enabled|disabled] # 列出当前仓库的信息, 版本, 大小, 日期等等...
[root@WWW ~]$ yum list [...] # 列出所有的程序包, 凡是最后一栏加上@符号的, 就是已经安装过的.anaconda表示在系统安装的时候就安装的包
# available 当前没有安装但是仓库有提供的包
# updates 有更新的包
# installed 已经安装在系统的包

接下来就是进行安装工作了:

1
2
[root@WWW ~]$ yum install package1 [package2] [...] # 安装包, 一次可以安装多个包.
# 安装时, 可以在后面跟上应用的版本号以安装不同版本

升级也很简单了:

1
[root@WWW ~]$ yum update [package1] [...] # 可以指明包名, 也可以不指明要单独升级哪一包, 而升级所有可能的包.

与升级相反过来(降级)的指令是:

1
[root@WWW ~]$ yum downgrade package1 [package2] [...]

如果要检查可用的升级使用:

1
[root@WWW ~]$ yum check-update

接下来就是进行程序包的卸载啦:

1
[root@WWW ~]$ yum remove|erase package1 [package2] [...]

那么安装完的程序包,我如何像rpm那样查看程序的信息呢 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@WWW ~]$ yum info [...] # 这里是可以不用指定具体的包名从而输出所有的包的信息...相信没有人会这样查询吧..
# 因此如果是一般的查询的话, 建议还是加上包的名字吧.
[root@WWW ~]$ yum info gcc
Installed Packages
Name : gcc <--- 包名
Arch : x86_64 <--- 需要的硬件架构
Version : 4.8.5 <--- 版本
Release : 11.el7 <--- rpm的版本
Size : 37 M <--- 大小
Repo : installed <--- 仓库类别
From repo : os <--- 来源仓库
Summary : Various compilers (C, C++, Objective-C, Java, ...) <--- 简述
URL : http://gcc.gnu.org
License : GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD
Description : The gcc package contains the GNU Compiler Collection version 4.8.
: You'll need this package in order to compile C code.

是不是感觉怪怪的. 其实rpm -qi所输出的信息是更加有用的啊, 因为中间有包的安装时间等等yum 所不能输出的信息, 因此我推荐使用 rpm 进行查询.

查询子程序(或特性)来源于哪一个软件包, 这一个功能个人感觉yum要优于rpm呐.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@WWW ~]$ yum provides redis-cli
redis-3.2.3-1.el7.x86_64 : A persistent key-value database
Repo : epel
Matched from:
Filename : /usr/bin/redis-cli

redis-3.2.3-1.el7.x86_64 : A persistent key-value database
Repo : @epel
Matched from:
Filename : /usr/bin/redis-cli
# 你看yum告诉我们了, 这个redis-cli来源于哪一个软件包, 以及当前的安装情况. 即使当前的系统没有安装这个程序, yum仍然可以把这个包给抓出来.
# 还记得之前说过的ip的一大堆子命令吗. 试一试ss
[root@WWW ~]$ yum provides ss
iproute-3.10.0-74.el7.x86_64 : Advanced IP routing and network device configuration tools
Repo : os
Matched from:
Filename : /usr/sbin/ss

iproute-3.10.0-74.el7.x86_64 : Advanced IP routing and network device configuration tools
Repo : @os
Matched from:
Filename : /usr/sbin/ss

yum的这个功能等价于rpm-qf选项, 但是yum是不需要指定具体的路径的, 而rpm是不会从$PATH中寻找的. 而且yum输出的信息要更多.

由于查询逻辑不一样( rpm有本地的限制 ), 所以再次推荐使用yum的这个功能而不是rpm -qf.

接下来是一个清理本地缓存的指令, 之前说过yum会将下载的元数据和程序包都缓存到本地, 在程序安装完成之后就会将程序的rpm包删除, 但是元数据会保留下来.

1
[root@WWW ~]$ yum clean [ package | metadata | expire-cache | rpmdb | plugins | all ]

反过来, 如果想要手动进行一次缓存的重建的话, 使用:

1
[root@WWW ~]$ yum makecache

其实这个就是手动进行一次下载, 其实就是在yum每一次下载都会进行一次缓存的重建, 因此没有必要频繁的进行手动的重建.

在我们进行软件包的下载的时候, 还有一个经常要进行的操作就是搜(shou)索(shuo):

1
[root@WWW ~]$ yum search string [string2]

匹配是进行的是模糊匹配, 会同时搜索程序包名和summary信息

很简单, 类似rpm的replace, yum也有一个重新安装的功能, 直接见名知意

1
[root@WWW ~]$ yum reinstall package1 [package2] [...]

yum同样也可以查看一个包的依赖有哪些, 并且要比rpm看到的要更好看.

1
[root@WWW ~]$ yum deplist package1 [package2] [...]

yum是支持事务的, 因此我们也可以查看这些记录, 这个就是yum的history功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@WWW ~]$ yum history
ID | Command line | Date and time | Action(s) | Altered
-------------------------------------------------------------------------------
64 | remove pptpd | 2017-06-18 10:43 | Erase | 1 EE
63 | install redhat-lsb | 2017-06-16 16:10 | Install | 74
62 | remove vim vim-tiny vim- | 2017-06-10 01:23 | Erase | 1 EE
61 | install -y ruby ruby-dev | 2017-06-10 01:23 | Install | 39
60 | update | 2017-06-10 01:17 | I, O, U | 41
59 | install python-devel | 2017-06-10 01:16 | Install | 1
58 | install libncurses5-dev | 2017-06-10 00:15 | Install | 1
57 | remove vim vim-runtime g | 2017-06-10 00:13 | Erase | 1
56 | install ctags | 2017-06-09 17:54 | Install | 1
55 | update | 2017-05-27 12:26 | Update | 3
54 | install redis.x86_64 | 2017-05-21 22:55 | Install | 2
53 | install tcl | 2017-05-21 14:35 | Install | 1
52 | install privoxy | 2017-05-20 17:20 | Install | 1

上面的history后面可以跟上很多的参数:

[info|list|packages-list|packages-info|summary|addon-info|redo|undo|rollback|new|sync|stats]

默认的参数是list, 其实这个功能用的不是很多啦.

继续说yum的事务功能, 当你终止一个正常的安装或者卸载或者凡是涉及到事务的操作时, yum会生成一个事务文件, 保存在/tmp/*.yumtx

这样就随时可以进行回复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--> Running transaction check
---> Package zsh.x86_64 0:5.0.2-25.el7_3.1 will be erased
--> Finished Dependency Resolution
......
Is this ok [y/N]:
Exiting on user command
Your transaction was saved, rerun it with:
yum load-transaction /tmp/yum_save_tx.2017-06-24.16-09.HQGAUR.yumtx
--------------------------------------------------------------------
[root@WWW ~]$ yum load-transaction /tmp/yum_save_tx.2017-06-24.16-09.HQGAUR.yumtx
...
Is this ok [y/N]: y
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
Erasing : zsh-5.0.2-25.el7_3.1.x86_64
Verifying : zsh-5.0.2-25.el7_3.1.x86_64
Removed:
zsh.x86_64 0:5.0.2-25.el7_3.1

yum的包组管理

yum支持一个独特的功能, 叫组管理.

说白了其实就是软件大礼包, 直接安装一个组就是将这个组内所有的包执行安装.

对组的操作和对单个软件包的操作基本类似, 命令也基本通用, 只要在前面加上groups就行了.

1
2
3
4
5
[root@WWW ~]$ yum groups info
[root@WWW ~]$ yum groups install
[root@WWW ~]$ yum groups list
[root@WWW ~]$ yum groups remove
[root@WWW ~]$ yum groups summary

yum的基本使用(2)

现在我们拿到了一张CnetOS的光盘, 如何将这张光盘当成我们的本地yum仓库呢?

  • 挂载光盘到某目录
    • mount -r -t iso9660 /dev/cdrom /media/cdrom
  • 创建配置文件
    • [CentOS7]
    • name =
    • baseurl =
    • gpgcheck =
    • enabled =

其实在命令行中也可以进行配置文件的编写, 例如: --nogpgcheck

yum的命令行选项:

-y : 自动回答为Yes

-q : 静默模式, 不会有输出

--disablerepo=repoidglob : 临时禁用此处指定的repo

--enablerepo=repoidglob : 临时启用此处指定的repo

--noplugins : 禁用所有的插件


在上面的配置文件的章节中 我们发现了一些以$开头的字符, 这些其实就是变量了.

yum的repo的配置中的可用的变量有:

1
2
3
4
$releasever: 当前OS的发行版中主版本号
$arch: 平台 ( i386, i586, i686 ... )
$basearch: 基础平台 ( x86, x86_64, ... )
$YUM0-$YUM9: 自定义变量

这样就拼成了我们看到的URL: http://mirrors.scuec.edu.cn/centos/$releasever/$basearch/os

编译安装程序包

上面再说rpm的时候, 提到过有两种rpm包, 其中有一种是源码rpm包, 这种包的命名是这样的:

testapp-VERSION-release.src.rpm

这个src就是source的意思, 这个包内是不含二进制格式的输出的, 只有源码, 也就是这个需要我们手动进行编译. 安装后使用rpmbuild命令制作成二进制的rpm包, 而后就可以进行安装了.

之所以需要这样的包, 是因为并没有考虑到操作系统, 实际也就是CPU的架构. 这样就可以根据本机实际的情况编译出适合自己(CPU的指令集)的二进制包,. 从命名中也可以发现了, 这个包中并没有arch这个部分.

而这样一个过程也已经在上面说过了, 再重复一遍吧:

源代码 --> 预处理 --> 编译(gcc) --> 汇编 --> 链接 --> 执行

现在我们来想这么一个问题, 一个程序的源文件肯定是很多部分组成的,而且这些文件很可能是存在依赖关系的, 那么在编译的时候我该优先编译哪一个呢?

这就是我们源代码的组织结构, 多文件组织. 显然我们不能就直接进行一个一个的gcc, 那么就需要一个项目管理工具, 就比如说Java中的maven. 而在C/C++中一个著名的项目管理工具就是make.很多人都把这个make当做成了编译器. 这样我们就不用手动的一个一个的进行gcc了.

那么make怎么能做到这么智能的知道去做什么呢? 这依靠一个重要的配置文件 – makefile

而这个makefile又不能直接就存在或者是写死的, 因为这样就失去了灵活性, 每一个用户都希望有自己的需求, 比如: 文件的位置, 以及一些功能的取舍.

实际上的makefile的生成是由一个Makefile.in的模板进行的. 那么这么一个模板文件又是由用户指定选项来选则特性是否启用. 提供给用户进行特性选则的工具叫做configure. 综上, 这么一个过程就是:

1
configure --> Makefile.in --> malefile

这样的话, 我们的编译安装的三个步骤就是:

1
2
3
./configure --[OPTIONS] # 生成makefile, 依赖Makefile.in
make # 编译的步骤
make install # 其实就是将生成的文件放在指定的文件夹

(1)的过程中, 通过选项传递参数, 指定启用特性, 安装路径等, 执行时会参考用户的选项和Makefile.in生成makefile, 接着, 因为有些功能的启用需要依赖其他的模块或者一些外部程序, 所以configure就会进行对所依赖的外部环境做检测.

(2)的过程中, 根据makefile文件, 构建应用程序.

那么回答最初的文件 – configure, 这个文件难道一定是默认就会提供的吗? 不一定. 手工编写这个文件是一个很复杂和麻烦的过程. 所以一般都直接使用开发工具进行生成. 如果下载的源码包是没有提供这些文件的.我们就要使用工具进行生成:

  • autoconf : 生成configure脚本
  • automake: 生成Makefile.in

对于有些软件可能不需要进行configure, 直接make就行, 而有些软件可能make之后直接就是二进制, 直接复制到别的地方就可以使用了.

所以说: 建议在安装前查看INSTALL文档(如果没有查看README)

在进行编译C的源代码的时候, 是有以下的前提的: ( configure就会进行检查 )

  • 开发工具: make, gcc
  • 开发环境: 开发库, 头文件
    • glibc : 标准库

接下来就来详细一点的说明一下那三个步骤:

第一步: configure脚本

  • 选项: 指定安装位置, 指定启用的特性

  • 常见的选项:

  • --prefix=/PATH/TO/SOMEWHERE: 指定默认安装位置
    --sysconfig=/PATH/TO/SOMEWHERE: 配置文件的安装位置
    Optional Features: 可选特性
        --disable-FEATURE
        --enable-FEATURE[=ARG]
    Optional Packages: 可选包
        --with-PACKAGE[=ARG]
        --without-PACKAGE
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    **第二步:** `make`

    **第三步:** `make install`


    **7月14号的更新:**

    上面说的就是最基本的源码编译了, 现在我们再多说一点.

    如果有一份这样的[文件](http://linux.vbird.org/linux_basic/0520source/main.tgz): 他们之间存在函数依赖关系并且还调用了外部库.那么在直接编译的时候就会报错.

    ```bash
    main.c:(.text+0x61): undefined reference to `haha'
    sin_value.c:(.text+0x2f): undefined reference to `sin'
    ...()

缺少库的解决方法就是在编译的时候加上-lm参数 如果你的库不在标准的库路径里面, 那么就需要再指定 -L/path 对于文件间的依赖关系, 就需要先把他们编译成对象文件, 再一起进行链接.

就像这样:

1
2
3
4
5
6
7
[root@WWW c]# gcc -c main.c 
[root@WWW c]# gcc -c sin_value.c
[root@WWW c]# gcc -c cos_value.c
[root@WWW c]# gcc -c haha.c
[root@WWW c]# ls
cos_value.c cos_value.o haha.c haha.o main.c main.o sin_value.c sin_value.o
[root@WWW c]# gcc -o main main.o haha.o sin_value.o cos_value.o -lm

这样就可以看到编译之后的文件了.

这样果然很烦人呢. 有没有一种方法能够直接自动化的进行依赖的解决呢? 这个就是我们要说的make了, 之前只是简单的说了一下, 现在我们就少未来具体的说一说这个.

首先就是我们的makefile的编写. makefile是最终执行make的重要参考, 因此现在我们来了解下这个文件的基本语法吧, 先用上面的那一只当一个范例:

1
2
3
makefile
main: main.o haha.o sin_value.o cos_value.o
gcc -o main main.o haha.o sin_value.o cos_value.o -lm

先把之前生成的文件都删掉, 再执行一次make命令试试:

1
2
3
4
5
6
7
8
9
[root@WWW ~]$ rm -rf *.o main
[root@localhost c]# make
cc -c -o main.o main.c
cc -c -o haha.o haha.c
cc -c -o sin_value.o sin_value.c
cc -c -o cos_value.o cos_value.c
gcc -o main main.o haha.o sin_value.o cos_value.o -lm
[root@localhost c]# ls
cos_value.c cos_value.o haha.c haha.o main main.c main.o makefile sin_value.c sin_value.o

这样就很方便的进行了多文件的编译. 但是你可能要说了, 我可以使用脚本来代替这个过程啊, 为什么非要编写这样的一个文件呢? 看下面的例子吧:

1
2
3
4
5
6
[root@localhost c]# make
make: `main' is up to date.
[root@localhost c]# vim haha.c
[root@localhost c]# make
cc -c -o haha.o haha.c
gcc -o main main.o haha.o sin_value.o cos_value.o -lm

发现了吗, 只要这个文件没有发生改变我们就不会再去消耗CPU去编译他, (编译是一件很吃性能的事情啊) 而且当文件发生了变化, 他也可以安装相关关系来按需编译, 也就是按照相依性来更新执行文件.

现在也许你尝试着进行了make clean 的操作但是发现失败了. 来看一下报错吧:

1
2
[root@localhost c]# make clean
make: *** No rule to make target `clean'. Stop.

他说没有对应的规则来执行. 那么什么才是make的规则呢. 是这样的, 我们的makefie的基本组成是这样的:

1
2
3
4
5
6
7
目标(target): 目标文件1 目标文件2
<tab>gcc -o 新建的可执行文件 目标文件1 目标文件2
target:
<tab>operation
target:
<tab>operation
....

现在我们加上这样的两行:

1
[root@localhost c]# echo -e "clean:\n\trm -rf main.o haha.o sin_value.o cos_value.o" >> makefile 

好了, 再执行一次clean吧:

1
2
[root@localhost c]# make clean
rm -rf main.o haha.o sin_value.o cos_value.o

但是现在我们发现, 这个makefile也太难看了, 参数都是一样的, 有什么办法来优化一下啊.

makefile和我们的shell script一样, 我们可以使用变量来进行文件内容的精简.

1
2
3
4
5
6
7
makefile
LIBS = -lm
OBJS = main.o haha.o sin_value.o cos_value.o
main: ${OBJS}
gcc -o main ${OBJS} ${LIBS}
clean:
rm -f main ${OBJS}

这样就看起来清爽多了.

最后再说一下, 软件的更新,如果使用的是rpm包进行的安装, 那么很容易就可以进行更新. 但是,源码包呢? 这是个问题. 事实上, 只要没有大改架构, 那么小范围的更新都是很方便的就可以进行更新的, 你想蛤, 其实仅仅是几段代码的差距罢了, 这个和我们的git有点像啊. 所以是不是有什么灵感呢? 由一个工具很方便的可以得到这些差距; diff

软件包的更新就依赖于这个, 由一个命令patch就是用来配合diff做这个事情的, 看一下下面的实例就懂了:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# echo "This is version 1.0" > 1
[root@localhost ~]# echo "This is version 2.0" > 2
[root@localhost ~]# diff -u 1 2
--- 1 2017-07-14 12:07:53.520429252 +0800
+++ 2 2017-07-14 12:07:58.752424592 +0800
@@ -1 +1 @@
-This is version 1.0
+This is version 2.0
[root@localhost ~]# diff -u 1 2 > test.patch
[root@localhost ~]# patch -p0 < test.patch
patching file 1
[root@localhost ~]# cat 1
This is version 2.0

只要将unified的diff输出重定向到一个文件, 接着就可以用patch进行更新和还原了, 这个在我们进行内核编译的时候是有很大用处的.

当然基于这个原理, 如果软件的架构发生了变化, 这样就不太能行得通了, 这个方法也只能在提供了patch文件才可以.